Cycle.js drivers to use in Chrome extensions and WebExtensions.
Forked from cycle-extensions to rewrite it with xstream. Additionally support for the tabs
and windows
APIs was added, and the MessagesDriver
now supports more than one simultaneous connection.
In the Web Extension architecture, logic and state both live in a long-running background page. The user interface lives in a separate page called a popup. The two pages communicate by passing messages between each other. The background page awaits connections; the popup initiates them.
messagesDriver
accepts a stream of messages to send to the other page and returns a stream of messages received from the other page.
makeMessagesDriver
takes a single named argument, shouldInitiate
. Set it to false
in the background page and true
in any other pages.
Here's an example of a simple counter. Notice that when the popup opens, it sends a request for the current state to the background page. The response it receives creates the first frame of DOM.
import { run } from '@cycle/run'
import xs from 'xstream'
import sampleCombine from 'xstream/extra/sampleCombine'
import {
makeMessagesDriver,
newMessage,
pickType,
} from 'cycle-web-extensions'
function Background({ messages: messages$ }) {
const initialState = { count: 0 }
const state$ = messages$.reduce(
(state, message) => {
if (message.type === 'increment') { return { ...state, count: state.count + 1 } }
if (message.type === 'decrement') { return { ...state, count: state.count - 1 } }
return state
},
initialState,
)
return {
messages: xs.merge(
state$.map(state => newMessage(['state_changed', state])),
pickType(messages$, 'current_state_requested')
.compose(sampleCombine(state$))
.map(([, state]) => newMessage(['state_changed'], state))
),
}
}
run(
Background,
{ messages: makeMessagesDriver({ shouldInitiate: false }) },
)
// and in your popup script
import {
makeMessagesDriver,
newMessage,
} from 'cycle-web-extensions'
import { button, div } from 'hyperscript-helpers'
function Popup({ DOM, messages: messages$ }) {
const count$ = pickType(messages$, 'state_changed')
.map(payload => payload.count) // Why no pluck in xstream?
return {
messages: xs.merge(
DOM.select('.increment').events('click').mapTo(newMessage(['increment'])),
DOM.select('.decrement').events('click').mapTo(newMessage(['decrement'])),
).startWith(newMessage('current_state_requested')),
DOM: count$.map(
count => (
div([
`The count is now ${count}.`,
div([button('.decrement', ['-1']), button('.increment', ['+1'])]),
])
),
),
}
}
function intent(DOM) {
const increment$ = DOM.select('.increment').events('click')
.mapTo(newMessage(['increment']))
const decrement$ = DOM.select('.decrement').events('click')
.mapTo(newMessage(['decrement']))
return {
messages: xs.merge(increment$, decrement$)
.startWith(newMessage(['current_state_requested'])),
}
}
run(
Popup,
{
DOM: makeDOMDriver('#root'),
messages: makeMessagesDriver({ shouldInitiate: true }),
},
)
- TODO: Document this
- TODO: Document this
To see a real extension written with these drivers, check out nspaeth/tab-manager.
yarn add cycle-web-extensions