Skip to content

Latest commit

 

History

History
313 lines (200 loc) · 6 KB

README.md

File metadata and controls

313 lines (200 loc) · 6 KB

@motorcycle/mostly-dom -- 5.0.0

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

Get it

yarn add @motorcycle/mostly-dom
# or
npm install --save @motorcycle/mostly-dom

API Documentation

All functions are curried!

DomSinks

Sinks type returns by a DOM component.

export type DomSinks = { readonly view$: Stream<VNode> }

DomSource

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>
}

DomSources

Sources type expected by a DOM component.

export type DomSources<A = Element, B = Event> = { readonly dom: DomSource<A, B> }

Types

Virtual DOM node type from mostly-dom

// All other types are used directly from mostly-dom
// https://github.com/TylorS167/mostly-dom

hyperscript-helpers

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>
}

makeDomComponent(element: Element): (sinks: DomSinks) => DomSources

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 }
  }
}