Motorcycle.ts adapter for mostly-dom. Built on @motorcycle/dom.
yarn add @motorcycle/mostly-dom
# or
npm install --save @motorcycle/mostly-dom
All functions are curried!
Sinks type returns by a DOM component.
export type DomSinks = { readonly view$: Stream<VNode> }
DomSource type as defined by @motorcycle/dom
interface DomSource {
query(cssSelector: CssSelector): DomSource
elements<El extends Element = Element>(): Stream<ReadonlyArray<El>>
events<Ev extends Event = Event>(eventType: StandardEvents, options?: EventListenerOptions): Stream<Ev>
cssSelectors(): ReadonlyArray<CssSelector>
}
Sources type expected by a DOM component.
export type DomSources<A = Element, B = Event> = { readonly dom: DomSource<A, B> }
Virtual DOM node type from mostly-dom
// All other types are used directly from mostly-dom
// https://github.com/TylorS167/mostly-dom
Functions for describing your views.
Re-exported from mostly-dom
See an example
import { VNode, div, h1, button } from '@motorcycle/mostly-dom'
function view(amount: number): VNode {
return div([
h1(`Clicked ${amount} times!`),
button('Click me')
])
}
See the code
export * from 'mostly-dom'
export * from './isolate'
export * from './makeDomComponent'
isolate<Sources extends DomSources, Sinks extends DomSinks>(component: Component<Sources, Sinks>, key: string, sources: Sources): Sinks
Isolates a component by adding an isolation class name to the outermost DOM element emitted by the component’s view stream.
The isolation class name is generated by appending the given isolation key
to the prefix $$isolation$$-
, e.g., given foo
as key
produces
$$isolation$$-foo
.
Isolating components are useful especially when dealing with lists of a specific component, so that events can be differentiated between the siblings. However, isolated components are not isolated from access by an ancestor DOM element.
Note that isolate
is curried.
See an example
import { empty } from '@motorcycle/stream'
import { createDomSource } from '@motorcycle/dom'
const sources = createDomSource(empty())
const sinks = isolate(MyComponent, `myIsolationKey`, sources)
See the code
export const isolate: IsolatedComponent = curry3(function isolate<
Sources extends DomSources,
Sinks extends DomSinks
>(component: Component<Sources, Sinks>, key: string, sources: Sources): Sinks {
const { dom } = sources
const isolatedDom = dom.query(`.${KEY_PREFIX}${key}`)
const sinks = component(Object.assign({}, sources, { dom: isolatedDom }))
const isolatedSinks = Object.assign({}, sinks, { view$: isolateView(sinks.view$, key) })
return isolatedSinks
})
const KEY_PREFIX = `__isolation__`
function isolateView(view$: Stream<VNode>, key: string) {
const prefixedKey = KEY_PREFIX + key
return tap(vNode => {
const { props: { className: className = EMPTY_CLASS_NAME } } = vNode
const needsIsolation = className.indexOf(prefixedKey) === -1
if (needsIsolation)
vNode.props.className = removeSuperfluousSpaces(
join(CLASS_NAME_SEPARATOR, [className, prefixedKey])
)
}, view$)
}
const EMPTY_CLASS_NAME = ``
const CLASS_NAME_SEPARATOR = ` `
function removeSuperfluousSpaces(str: string): string {
return str.replace(RE_TWO_OR_MORE_SPACES, CLASS_NAME_SEPARATOR)
}
const RE_TWO_OR_MORE_SPACES = /\s{2,}/g
export interface IsolatedComponent {
<Sources extends DomSources, Sinks extends DomSinks>(
component: Component<Sources, Sinks>,
key: string,
sources: Sources
): Sinks
<Sources extends DomSources, Sinks extends DomSinks>(
component: Component<Sources, Sinks>,
key: string
): Component<Sources, Sinks>
<Sources extends DomSources, Sinks extends DomSinks>(
component: Component<Sources, Sinks>
): IsolatedComponentArity2<Sources, Sinks>
}
export interface IsolatedComponentArity2<Sources extends DomSources, Sinks extends DomSinks> {
(key: string, sources: Sources): Sinks
(key: string): Component<Sources, Sinks>
}
Takes an element and returns a DOM component function.
See an example
import {
makeDomComponent,
DomSources,
DomSinks,
VNode,
events,
query,
div,
h1,
button
} from '@motorcycle/mostly-dom'
import { run } from '@motorcycle/run'
const element = document.querySelector('#app')
if (!element) throw new Error('unable to find element')
run(Main, makeDomComponent(element))
function Main(sources: DomSources): DomSinks {
const { dom } = sources
const click$: Stream<Event> = events('click', query('button'))
const amount$: Stream<number> = scan(x => x + 1, 0, click$)
const view$: Stream<VNode> = map(view, amount$)
return { view$ }
}
function view(amount: number) {
return div([
h1(`Clicked ${amount} times`),
button(`Click me`)
])
}
See the code
export function makeDomComponent(element: Element): IOComponent<DomSinks, DomSources> {
const rootVNode = elementToVNode(element)
const wrapVNode = map(vNodeWrapper(element))
const patch = scan(init(), rootVNode)
return function Dom(sinks: DomSinks): DomSources {
const { view$ } = sinks
const elementVNode$ = patch(wrapVNode(view$))
const element$ = hold(toElement(elementVNode$))
const dom = createDomSource(element$)
drain(element$)
return { dom }
}
}