@motorcycle/mostly-dom
Version:
Motorcycle.ts adapter for mostly-dom. Built on @motorcycle/dom.
314 lines (200 loc) • 6.15 kB
Markdown
Motorcycle.ts adapter for mostly-dom. Built on @motorcycle/dom.
```sh
yarn add @motorcycle/mostly-dom
npm install --save @motorcycle/mostly-dom
```
All functions are curried!
<p>
Sinks type returns by a DOM component.
</p>
```typescript
export type DomSinks = { readonly view$: Stream<VNode> }
```
<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> }
```
<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
```
<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 />
<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 />