@motorcycle/mostly-dom
Version:
Motorcycle.ts adapter for mostly-dom. Built on @motorcycle/dom.
88 lines (75 loc) • 3.02 kB
text/typescript
import { Component, Stream } from '@motorcycle/types'
import { DomSinks, DomSources } from './'
import { curry3, join } from '@typed/prelude'
import { VNode } from 'mostly-dom'
import { tap } from '@motorcycle/stream'
/**
* 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.
*
* @name isolate<Sources extends DomSources, Sinks extends DomSinks>(component: Component<Sources, Sinks>, key: string, sources: Sources): Sinks
*
* @example
* import { empty } from '@motorcycle/stream'
* import { createDomSource } from '@motorcycle/dom'
*
* const sources = createDomSource(empty())
* const sinks = isolate(MyComponent, `myIsolationKey`, sources)
*/
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>
}