UNPKG

5.24 kBPlain TextView Raw
1import {Driver, FantasyObservable} from '@cycle/run';
2import {init, Module, VNode, toVNode} from 'snabbdom';
3import xs, {Stream, Listener} from 'xstream';
4import concat from 'xstream/extra/concat';
5import sampleCombine from 'xstream/extra/sampleCombine';
6import {DOMSource} from './DOMSource';
7import {MainDOMSource} from './MainDOMSource';
8import {VNodeWrapper} from './VNodeWrapper';
9import {getValidNode, checkValidContainer} from './utils';
10import defaultModules from './modules';
11import {IsolateModule} from './IsolateModule';
12import {EventDelegator} from './EventDelegator';
13
14function makeDOMDriverInputGuard(modules: any) {
15 if (!Array.isArray(modules)) {
16 throw new Error(
17 `Optional modules option must be an array for snabbdom modules`
18 );
19 }
20}
21
22function domDriverInputGuard(view$: Stream<VNode>): void {
23 if (
24 !view$ ||
25 typeof view$.addListener !== `function` ||
26 typeof view$.fold !== `function`
27 ) {
28 throw new Error(
29 `The DOM driver function expects as input a Stream of ` +
30 `virtual DOM elements`
31 );
32 }
33}
34
35export interface DOMDriverOptions {
36 modules?: Array<Partial<Module>>;
37 reportSnabbdomError?(err: any): void;
38}
39
40function dropCompletion<T>(input: Stream<T>): Stream<T> {
41 return xs.merge(input, xs.never());
42}
43
44function unwrapElementFromVNode(vnode: VNode): Element {
45 return vnode.elm as Element;
46}
47
48function defaultReportSnabbdomError(err: any): void {
49 (console.error || console.log)(err);
50}
51
52function makeDOMReady$(): Stream<null> {
53 return xs.create<null>({
54 start(lis: Listener<null>) {
55 if (document.readyState === 'loading') {
56 document.addEventListener('readystatechange', () => {
57 const state = document.readyState;
58 if (state === 'interactive' || state === 'complete') {
59 lis.next(null);
60 lis.complete();
61 }
62 });
63 } else {
64 lis.next(null);
65 lis.complete();
66 }
67 },
68 stop() {},
69 });
70}
71
72function addRootScope(vnode: VNode): VNode {
73 vnode.data = vnode.data || {};
74 vnode.data.isolate = [];
75 return vnode;
76}
77
78function makeDOMDriver(
79 container: string | Element | DocumentFragment,
80 options: DOMDriverOptions = {}
81): Driver<Stream<VNode>, MainDOMSource> {
82 checkValidContainer(container);
83 const modules = options.modules || defaultModules;
84 makeDOMDriverInputGuard(modules);
85 const isolateModule = new IsolateModule();
86 const patch = init([isolateModule.createModule() as Partial<Module>].concat(modules));
87 const domReady$ = makeDOMReady$();
88 let vnodeWrapper: VNodeWrapper;
89 let mutationObserver: MutationObserver;
90 const mutationConfirmed$ = xs.create<null>({
91 start(listener) {
92 mutationObserver = new MutationObserver(() => listener.next(null));
93 },
94 stop() {
95 mutationObserver.disconnect();
96 },
97 });
98
99 function DOMDriver(vnode$: Stream<VNode>, name = 'DOM'): MainDOMSource {
100 domDriverInputGuard(vnode$);
101 const sanitation$ = xs.create<null>();
102
103 const firstRoot$ = domReady$.map(() => {
104 const firstRoot = getValidNode(container) || document.body;
105 vnodeWrapper = new VNodeWrapper(firstRoot);
106 return firstRoot;
107 });
108
109 // We need to subscribe to the sink (i.e. vnode$) synchronously inside this
110 // driver, and not later in the map().flatten() because this sink is in
111 // reality a SinkProxy from @cycle/run, and we don't want to miss the first
112 // emission when the main() is connected to the drivers.
113 // Read more in issue #739.
114 const rememberedVNode$ = vnode$.remember();
115 rememberedVNode$.addListener({});
116
117 // The mutation observer internal to mutationConfirmed$ should
118 // exist before elementAfterPatch$ calls mutationObserver.observe()
119 mutationConfirmed$.addListener({});
120
121 const elementAfterPatch$ = firstRoot$
122 .map(
123 firstRoot =>
124 xs
125 .merge(rememberedVNode$.endWhen(sanitation$), sanitation$)
126 .map(vnode => vnodeWrapper.call(vnode))
127 .startWith(addRootScope(toVNode(firstRoot)))
128 .fold(patch, toVNode(firstRoot))
129 .drop(1)
130 .map(unwrapElementFromVNode)
131 .startWith(firstRoot as any)
132 .map(el => {
133 mutationObserver.observe(el, {
134 childList: true,
135 attributes: true,
136 characterData: true,
137 subtree: true,
138 attributeOldValue: true,
139 characterDataOldValue: true,
140 });
141 return el;
142 })
143 .compose(dropCompletion) // don't complete this stream
144 )
145 .flatten();
146
147 const rootElement$ = concat(domReady$, mutationConfirmed$)
148 .endWhen(sanitation$)
149 .compose(sampleCombine(elementAfterPatch$))
150 .map(arr => arr[1])
151 .remember();
152
153 // Start the snabbdom patching, over time
154 rootElement$.addListener({
155 error: options.reportSnabbdomError || defaultReportSnabbdomError,
156 });
157
158 const delegator = new EventDelegator(rootElement$, isolateModule);
159
160 return new MainDOMSource(
161 rootElement$,
162 sanitation$,
163 [],
164 isolateModule,
165 delegator,
166 name
167 );
168 }
169
170 return DOMDriver as any;
171}
172
173export {makeDOMDriver};