UNPKG

@motorcycle/mostly-dom

Version:

Motorcycle.ts adapter for mostly-dom. Built on @motorcycle/dom.

314 lines (200 loc) 6.15 kB
# @motorcycle/mostly-dom -- 4.1.0 Motorcycle.ts adapter for mostly-dom. Built on @motorcycle/dom. ## Get it ```sh yarn add @motorcycle/mostly-dom # or npm install --save @motorcycle/mostly-dom ``` ## API Documentation All functions are curried! #### DomSinks <p> Sinks type returns by a DOM component. </p> ```typescript export type DomSinks = { readonly view$: Stream<VNode> } ``` #### DomSource <p> DomSource type as defined by @motorcycle/dom </p> ```typescript 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> } ``` #### DomSources <p> Sources type expected by a DOM component. </p> ```typescript export type DomSources<A = Element, B = Event> = { readonly dom: DomSource<A, B> } ``` #### Types <p> Virtual DOM node type from mostly-dom </p> ```typescript // All other types are used directly from mostly-dom // https://github.com/TylorS167/mostly-dom ``` #### hyperscript-helpers <p> Functions for describing your views. Re-exported from [`mostly-dom`](https://github.com/TylorS167/mostly-dom) </p> <details> <summary>See an example</summary> ```typescript import { VNode, div, h1, button } from '@motorcycle/mostly-dom' function view(amount: number): VNode { return div([ h1(`Clicked ${amount} times!`), button('Click me') ]) } ``` </details> <details> <summary>See the code</summary> ```typescript export * from 'mostly-dom' export * from './isolate' export * from './makeDomComponent' ``` </details> <hr /> #### isolate\<Sources extends DomSources, Sinks extends DomSinks\>(component: Component\<Sources, Sinks\>, key: string, sources: Sources): Sinks <p> 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. </p> <details> <summary>See an example</summary> ```typescript import { empty } from '@motorcycle/stream' import { createDomSource } from '@motorcycle/dom' const sources = createDomSource(empty()) const sinks = isolate(MyComponent, `myIsolationKey`, sources) ``` </details> <details> <summary>See the code</summary> ```typescript 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> } ``` </details> <hr /> #### makeDomComponent(element: Element): (sinks: DomSinks) =\> DomSources <p> Takes an element and returns a DOM component function. </p> <details> <summary>See an example</summary> ```typescript 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`) ]) } ``` </details> <details> <summary>See the code</summary> ```typescript 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 } } } ``` </details> <hr />