UNPKG

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