@cycle/dom
Version:
The standard DOM Driver for Cycle.js, based on Snabbdom
1,720 lines (1,544 loc) • 53 kB
text/typescript
import * as assert from 'assert';
import isolate from '@cycle/isolate';
import xs, {Stream, MemoryStream} from 'xstream';
import fromDiagram from 'xstream/extra/fromDiagram';
import delay from 'xstream/extra/delay';
import concat from 'xstream/extra/concat';
import {setup} from '@cycle/run';
import {
h,
svg,
div,
span,
h2,
h3,
h4,
button,
makeDOMDriver,
DOMSource,
MainDOMSource,
VNode,
thunk,
} from '../../src/index';
function createRenderTarget(id: string | null = null) {
const element = document.createElement('div');
element.className = 'cycletest';
if (id) {
element.id = id;
}
document.body.appendChild(element);
return element;
}
describe('isolateSource', function() {
it('should return source also with isolateSource and isolateSink', function(done) {
function app(_sources: {DOM: MainDOMSource}) {
return {
DOM: xs.of(h('h3.top-most')),
};
}
const {sinks, sources, run} = setup(app, {
DOM: makeDOMDriver(createRenderTarget()),
});
const dispose = run();
const isolatedDOMSource = sources.DOM.isolateSource(
sources.DOM,
'top-most'
);
// Make assertions
assert.strictEqual(typeof isolatedDOMSource.isolateSource, 'function');
assert.strictEqual(typeof isolatedDOMSource.isolateSink, 'function');
dispose();
done();
});
});
describe('isolateSink', function() {
it('should add an isolate field to the vtree sink', function(done) {
function app(_sources: {DOM: MainDOMSource}) {
const vtree$ = xs.of(h3('.top-most'));
return {
DOM: _sources.DOM.isolateSink(vtree$, 'foo'),
};
}
const {sinks, sources, run} = setup(app, {
DOM: makeDOMDriver(createRenderTarget()),
});
let dispose: any;
// Make assertions
sinks.DOM.take(1).addListener({
next: (vtree: VNode) => {
assert.strictEqual(vtree.sel, 'h3.top-most');
assert.strictEqual(Array.isArray((vtree.data as any).isolate), true);
assert.deepStrictEqual((vtree.data as any).isolate, [
{type: 'total', scope: 'foo'},
]);
setTimeout(() => {
dispose();
done();
});
},
});
dispose = run();
});
it('should not redundantly repeat the scope className', function(done) {
function app(_sources: {DOM: MainDOMSource}) {
const vtree1$ = xs.of(span('.tab1', 'Hi'));
const vtree2$ = xs.of(span('.tab2', 'Hello'));
const first$ = _sources.DOM.isolateSink(vtree1$, '1');
const second$ = _sources.DOM.isolateSink(vtree2$, '2');
const switched$ = concat(
xs.of(1).compose(delay(50)),
xs.of(2).compose(delay(50)),
xs.of(1).compose(delay(50)),
xs.of(2).compose(delay(50)),
xs.of(1).compose(delay(50)),
xs.of(2).compose(delay(50))
)
.map(i => (i === 1 ? first$ : second$))
.flatten();
return {
DOM: switched$,
};
}
const {sinks, sources, run} = setup(app, {
DOM: makeDOMDriver(createRenderTarget()),
});
let dispose: any;
// Make assertions
sinks.DOM.drop(2)
.take(1)
.addListener({
next: (vtree: VNode) => {
assert.strictEqual(vtree.sel, 'span.tab1');
assert.strictEqual(Array.isArray((vtree.data as any).isolate), true);
assert.strictEqual((vtree.data as any).isolate.length, 1);
assert.deepStrictEqual((vtree.data as any).isolate, [
{type: 'total', scope: '1'},
]);
dispose();
done();
},
});
dispose = run();
});
});
describe('isolation', function() {
it('should prevent parent from DOM.selecting() inside the isolation', function(done) {
function app(_sources: {DOM: MainDOMSource}) {
const child$ = _sources.DOM.isolateSink(
xs.of(div('.foo', [h4('.bar', 'Wrong')])),
'ISOLATION'
);
const vdom$ = xs
.combine(xs.of(null), child$)
.map(([_, child]) => h3('.top-most', [child, h2('.bar', 'Correct')]));
return {
DOM: vdom$,
};
}
const {sinks, sources, run} = setup(app, {
DOM: makeDOMDriver(createRenderTarget()),
});
sources.DOM.select('.bar')
.elements()
.drop(1)
.take(1)
.addListener({
next: (elements: Array<Element>) => {
assert.strictEqual(Array.isArray(elements), true);
assert.strictEqual(elements.length, 1);
const correctElement = elements[0];
assert.notStrictEqual(correctElement, null);
assert.notStrictEqual(typeof correctElement, 'undefined');
assert.strictEqual(correctElement.tagName, 'H2');
assert.strictEqual(correctElement.textContent, 'Correct');
done();
},
});
run();
});
it('should not occur with scope ":root"', function(done) {
function app(_sources: {DOM: MainDOMSource}) {
const child$ = _sources.DOM.isolateSink(
xs.of(div('.foo', [h4('.bar', 'Not wrong')])),
':root'
);
const vdom$ = xs
.combine(xs.of(null), child$)
.map(([_, child]) => h3('.top-most', [child, h2('.bar', 'Correct')]));
return {
DOM: vdom$,
};
}
const {sinks, sources, run} = setup(app, {
DOM: makeDOMDriver(createRenderTarget()),
});
sources.DOM.select('.bar')
.elements()
.drop(1)
.take(1)
.addListener({
next: (elements: Array<Element>) => {
assert.strictEqual(Array.isArray(elements), true);
assert.strictEqual(elements.length, 2);
const notWrongElement = elements[0];
assert.notStrictEqual(notWrongElement, null);
assert.notStrictEqual(typeof notWrongElement, 'undefined');
assert.strictEqual(notWrongElement.tagName, 'H4');
assert.strictEqual(notWrongElement.textContent, 'Not wrong');
const correctElement = elements[1];
assert.notStrictEqual(correctElement, null);
assert.notStrictEqual(typeof correctElement, 'undefined');
assert.strictEqual(correctElement.tagName, 'H2');
assert.strictEqual(correctElement.textContent, 'Correct');
done();
},
});
run();
});
it('should apply only between siblings when given scope ".foo"', function(done) {
function app(_sources: {DOM: MainDOMSource}) {
const foo$ = _sources.DOM.isolateSink(
xs.of(div('.container', [h4('.header', 'Correct')])),
'.foo'
);
const bar$ = _sources.DOM.isolateSink(
xs.of(div('.container', [h3('.header', 'Wrong')])),
'.bar'
);
const vdom$ = xs
.combine(foo$, bar$)
.map(([foo, bar]) =>
div('.top-most', [foo, bar, h2('.header', 'Correct')])
);
return {
DOM: vdom$,
};
}
const {sinks, sources, run} = setup(app, {
DOM: makeDOMDriver(createRenderTarget()),
});
// Assert parent has total access to its children
sources.DOM.select('.header')
.elements()
.drop(1)
.take(1)
.addListener({
next: (elements: Array<Element>) => {
assert.strictEqual(Array.isArray(elements), true);
assert.strictEqual(elements.length, 3);
assert.strictEqual(elements[0].tagName, 'H4');
assert.strictEqual(elements[0].textContent, 'Correct');
assert.strictEqual(elements[1].tagName, 'H3');
assert.strictEqual(elements[1].textContent, 'Wrong');
assert.strictEqual(elements[2].tagName, 'H2');
assert.strictEqual(elements[2].textContent, 'Correct');
// Assert .foo child has no access to .bar child
sources.DOM.isolateSource(sources.DOM, '.foo')
.select('.header')
.elements()
.take(1)
.addListener({
next: (els: Array<Element>) => {
assert.strictEqual(Array.isArray(els), true);
assert.strictEqual(els.length, 1);
assert.strictEqual(els[0].tagName, 'H4');
assert.strictEqual(els[0].textContent, 'Correct');
done();
},
});
},
});
run();
});
it('should apply only between siblings when given scope "#foo"', function(done) {
function app(_sources: {DOM: MainDOMSource}) {
const foo$ = _sources.DOM.isolateSink(
xs.of(div('.container', [h4('.header', 'Correct')])),
'#foo'
);
const bar$ = _sources.DOM.isolateSink(
xs.of(div('.container', [h3('.header', 'Wrong')])),
'#bar'
);
const vdom$ = xs
.combine(foo$, bar$)
.map(([foo, bar]) =>
div('.top-most', [foo, bar, h2('.header', 'Correct')])
);
return {
DOM: vdom$,
};
}
const {sinks, sources, run} = setup(app, {
DOM: makeDOMDriver(createRenderTarget()),
});
// Assert parent has total access to its children
sources.DOM.select('.header')
.elements()
.drop(1)
.take(1)
.addListener({
next: (elements: Array<Element>) => {
assert.strictEqual(Array.isArray(elements), true);
assert.strictEqual(elements.length, 3);
assert.strictEqual(elements[0].tagName, 'H4');
assert.strictEqual(elements[0].textContent, 'Correct');
assert.strictEqual(elements[1].tagName, 'H3');
assert.strictEqual(elements[1].textContent, 'Wrong');
assert.strictEqual(elements[2].tagName, 'H2');
assert.strictEqual(elements[2].textContent, 'Correct');
// Assert .foo child has no access to .bar child
sources.DOM.isolateSource(sources.DOM, '#foo')
.select('.header')
.elements()
.take(1)
.addListener({
next: (els: Array<Element>) => {
assert.strictEqual(Array.isArray(els), true);
assert.strictEqual(els.length, 1);
assert.strictEqual(els[0].tagName, 'H4');
assert.strictEqual(els[0].textContent, 'Correct');
done();
},
});
},
});
run();
});
it('should work with thunks', function(done) {
function app(_sources: {DOM: MainDOMSource}) {
const child$ = _sources.DOM.isolateSink(
xs.of<VNode>(thunk('div.foo', () => div('.foo', [h4('.bar', 'Wrong')]), [])),
'ISOLATION'
);
const vdom$ = xs
.combine(xs.of(null), child$)
.map(([_, child]) => h3('.top-most', [child, h2('.bar', 'Correct')]));
return {
DOM: vdom$,
};
}
const {sinks, sources, run} = setup(app, {
DOM: makeDOMDriver(createRenderTarget()),
});
sources.DOM.select('.bar')
.elements()
.drop(1)
.take(1)
.addListener({
next: (elements: Array<Element>) => {
assert.strictEqual(Array.isArray(elements), true);
assert.strictEqual(elements.length, 1);
const correctElement = elements[0];
assert.notStrictEqual(correctElement, null);
assert.notStrictEqual(typeof correctElement, 'undefined');
assert.strictEqual(correctElement.tagName, 'H2');
assert.strictEqual(correctElement.textContent, 'Correct');
done();
},
});
run();
});
it('should allow using elements() in an isolated main() fn', function(done) {
function main(_sources: {DOM: MainDOMSource}) {
const elem$ = _sources.DOM.select(':root').elements();
const vnode$ = elem$.map(elem =>
h('div.bar', 'left=' + (elem[0] as any).offsetLeft)
);
return {
DOM: vnode$,
};
}
const {sinks, sources, run} = setup(isolate(main), {
DOM: makeDOMDriver(createRenderTarget()),
});
sources.DOM.select(':root')
.element()
.drop(1)
.take(1)
.addListener({
next: (root: Element) => {
const barElem = root.querySelector('.bar') as Element;
assert.notStrictEqual(barElem, null);
assert.notStrictEqual(typeof barElem, 'undefined');
assert.strictEqual(barElem.tagName, 'DIV');
assert.strictEqual(barElem.textContent, 'left=8');
done();
},
});
run();
});
it('should allow parent to DOM.select() in its own isolation island', function(done) {
function app(_sources: {DOM: MainDOMSource}) {
const {isolateSource, isolateSink} = _sources.DOM;
const islandElement$ = isolateSource(_sources.DOM, 'island')
.select('.bar')
.elements();
const islandVDom$ = isolateSink(
xs.of(div([h3('.bar', 'Correct')])),
'island'
);
const child$ = isolateSink(
islandVDom$.map(islandVDom =>
div('.foo', [islandVDom, h4('.bar', 'Wrong')])
),
'ISOLATION'
);
const vdom$ = child$.map(child => h3('.top-most', [child]));
return {
DOM: vdom$,
island: islandElement$,
};
}
const drivers = {
DOM: makeDOMDriver(createRenderTarget()),
island(sink: Stream<Array<Element>>) {},
};
const {sinks, sources, run} = setup(app, drivers);
sinks.island
.drop(1)
.take(1)
.addListener({
next: (elements: Array<Element>) => {
assert.strictEqual(Array.isArray(elements), true);
assert.strictEqual(elements.length, 1);
const correctElement = elements[0];
assert.notStrictEqual(correctElement, null);
assert.notStrictEqual(typeof correctElement, 'undefined');
assert.strictEqual(correctElement.tagName, 'H3');
assert.strictEqual(correctElement.textContent, 'Correct');
done();
},
});
run();
});
it('should isolate DOM.select between parent and (wrapper) child', function(done) {
function Frame(_sources: {DOM: MainDOMSource; content$: Stream<any>}) {
const click$ = _sources.DOM.select('.foo').events('click');
const vdom$ = _sources.content$.map(content =>
h4('.foo.frame', {style: {backgroundColor: 'lightblue'}}, [content])
);
return {
DOM: vdom$,
click$,
};
}
function Monalisa(_sources: {DOM: MainDOMSource}): any {
const {isolateSource, isolateSink} = _sources.DOM;
const islandDOMSource = isolateSource(_sources.DOM, '.island');
const monalisaClick$ = islandDOMSource.select('.foo').events('click');
const islandDOMSink$ = isolateSink(
xs.of(span('.foo.monalisa', 'Monalisa')),
'.island'
);
const click$ = _sources.DOM.select('.foo').events('click');
const frameDOMSource = isolateSource(_sources.DOM, 'myFrame');
const frame = Frame({DOM: frameDOMSource, content$: islandDOMSink$});
const outerVTree$ = isolateSink(frame.DOM, 'myFrame');
return {
DOM: outerVTree$,
frameClick: frame.click$,
monalisaClick: monalisaClick$,
click: click$,
};
}
const {sources, sinks, run} = setup(Monalisa, {
DOM: makeDOMDriver(createRenderTarget()),
frameClick: () => {},
monalisaClick: () => {},
click: () => {},
});
let dispose: any;
const frameClick$ = sinks.frameClick.map((ev: any) => ({
type: ev.type,
tagName: (ev.target as HTMLElement).tagName,
}));
const _monalisaClick$ = sinks.monalisaClick.map((ev: any) => ({
type: ev.type,
tagName: (ev.target as HTMLElement).tagName,
}));
const grandparentClick$ = sinks.click.map((ev: any) => ({
type: ev.type,
tagName: (ev.target as HTMLElement).tagName,
}));
// Stop the propagtion of the second click
sinks.monalisaClick
.drop(1)
.take(1)
.addListener({
next: (ev: Event) => ev.stopPropagation(),
});
let totalClickHandlersCalled = 0;
let frameClicked = false;
frameClick$.addListener({
next: (event: any) => {
assert.strictEqual(frameClicked, false);
assert.strictEqual(event.type, 'click');
assert.strictEqual(event.tagName, 'H4');
frameClicked = true;
totalClickHandlersCalled++;
},
});
// Monalisa should receive two clicks
let monalisaClicked = 0;
_monalisaClick$.addListener({
next: (event: any) => {
assert.strictEqual(monalisaClicked < 2, true);
assert.strictEqual(event.type, 'click');
assert.strictEqual(event.tagName, 'SPAN');
monalisaClicked++;
totalClickHandlersCalled++;
},
});
// The grandparent should receive sibling isolated events
// from the monalisa even though it is passed into the
// total isolated Frame
let grandparentClicked = false;
grandparentClick$.addListener({
next: (event: any) => {
assert.strictEqual(event.type, 'click');
assert.strictEqual(event.tagName, 'SPAN');
assert.strictEqual(grandparentClicked, false);
grandparentClicked = true;
totalClickHandlersCalled++;
assert.doesNotThrow(() => {
setTimeout(() => {
assert.strictEqual(totalClickHandlersCalled, 4);
dispose();
done();
}, 10);
});
},
});
sources.DOM.select(':root')
.element()
.drop(1)
.take(1)
.addListener({
next: (root: Element) => {
const frameFoo = root.querySelector('.foo.frame') as HTMLElement;
const monalisaFoo = root.querySelector(
'.foo.monalisa'
) as HTMLElement;
assert.notStrictEqual(frameFoo, null);
assert.notStrictEqual(monalisaFoo, null);
assert.notStrictEqual(typeof frameFoo, 'undefined');
assert.notStrictEqual(typeof monalisaFoo, 'undefined');
assert.strictEqual(frameFoo.tagName, 'H4');
assert.strictEqual(monalisaFoo.tagName, 'SPAN');
assert.doesNotThrow(() => {
setTimeout(() => frameFoo.click(), 0);
setTimeout(() => monalisaFoo.click());
setTimeout(() => monalisaFoo.click());
});
},
});
dispose = run();
});
it('should allow a child component to DOM.select() its own root', function(done) {
function app(_sources: {DOM: MainDOMSource}) {
const child$ = _sources.DOM.isolateSink(
xs.of(span('.foo', [h4('.bar', 'Wrong')])),
'ISOLATION'
);
return {
DOM: child$.map(child => h3('.top-most', [child])),
};
}
const {sinks, sources, run} = setup(app, {
DOM: makeDOMDriver(createRenderTarget()),
});
const {isolateSource} = sources.DOM;
let dispose: any;
isolateSource(sources.DOM, 'ISOLATION')
.select('.foo')
.elements()
.drop(1)
.take(1)
.addListener({
next: (elements: Array<Element>) => {
assert.strictEqual(Array.isArray(elements), true);
assert.strictEqual(elements.length, 1);
const correctElement = elements[0];
assert.notStrictEqual(correctElement, null);
assert.notStrictEqual(typeof correctElement, 'undefined');
assert.strictEqual(correctElement.tagName, 'SPAN');
setTimeout(() => {
dispose();
done();
});
},
});
dispose = run();
});
it('should allow DOM.selecting svg elements', function(done) {
function App(_sources: {DOM: MainDOMSource}) {
const triangleElement$ = _sources.DOM.select('.triangle').elements();
const svgTriangle = svg({attrs: {width: 150, height: 150}}, [
svg.polygon({
attrs: {
class: 'triangle',
points: '20 0 20 150 150 20',
},
}),
]);
return {
DOM: xs.of(svgTriangle),
triangleElement: triangleElement$,
};
}
function IsolatedApp(_sources: {DOM: MainDOMSource}) {
const {isolateSource, isolateSink} = _sources.DOM;
const isolatedDOMSource = isolateSource(_sources.DOM, 'ISOLATION');
const app = App({DOM: isolatedDOMSource});
const isolateDOMSink = isolateSink(app.DOM, 'ISOLATION');
return {
DOM: isolateDOMSink,
triangleElement: app.triangleElement,
};
}
const drivers = {
DOM: makeDOMDriver(createRenderTarget()),
triangleElement: (sink: any) => {},
};
const {sinks, sources, run} = setup(IsolatedApp, drivers);
// Make assertions
sinks.triangleElement
.drop(1)
.take(1)
.addListener({
next: (elements: Array<Element>) => {
assert.strictEqual(elements.length, 1);
const triangleElement = elements[0];
assert.notStrictEqual(triangleElement, null);
assert.notStrictEqual(typeof triangleElement, 'undefined');
assert.strictEqual(triangleElement.tagName, 'polygon');
done();
},
});
run();
});
it('should allow DOM.select()ing its own root without classname or id', function(done) {
function app(_sources: {DOM: MainDOMSource}) {
const child$ = _sources.DOM.isolateSink(
xs.of(span([h4('.bar', 'Wrong')])),
'ISOLATION'
);
return {
DOM: child$.map(child => h3('.top-most', [child])),
};
}
const {sinks, sources, run} = setup(app, {
DOM: makeDOMDriver(createRenderTarget()),
});
const {isolateSource} = sources.DOM;
isolateSource(sources.DOM, 'ISOLATION')
.select('span')
.elements()
.drop(1)
.take(1)
.addListener({
next: (elements: Array<Element>) => {
assert.strictEqual(Array.isArray(elements), true);
assert.strictEqual(elements.length, 1);
const correctElement = elements[0];
assert.notStrictEqual(correctElement, null);
assert.notStrictEqual(typeof correctElement, 'undefined');
assert.strictEqual(correctElement.tagName, 'SPAN');
done();
},
});
run();
});
it('should allow DOM.select()ing all elements with `*`', function(done) {
function app(_sources: {DOM: MainDOMSource}) {
const child$ = _sources.DOM.isolateSink(
xs.of(span([div([h4('.foo', 'hello'), h4('.bar', 'world')])])),
'ISOLATION'
);
return {
DOM: child$.map(child => h3('.top-most', [child])),
};
}
const {sinks, sources, run} = setup(app, {
DOM: makeDOMDriver(createRenderTarget()),
});
const {isolateSource} = sources.DOM;
isolateSource(sources.DOM, 'ISOLATION')
.select('*')
.elements()
.drop(1)
.take(1)
.addListener({
next: (elements: Array<Element>) => {
assert.strictEqual(Array.isArray(elements), true);
assert.strictEqual(elements.length, 4);
done();
},
});
run();
});
it('should select() isolated element with tag + class', function(done) {
function app(_sources: {DOM: MainDOMSource}) {
return {
DOM: xs.of(
h3('.top-most', [
h2('.bar', 'Wrong'),
div({isolate: [{type: 'total', scope: 'foo'}]}, [
h4('.bar', 'Correct'),
]),
])
),
};
}
const {sinks, sources, run} = setup(app, {
DOM: makeDOMDriver(createRenderTarget()),
});
const isolatedDOMSource = sources.DOM.isolateSource(sources.DOM, 'foo');
isolatedDOMSource
.select('h4.bar')
.elements()
.drop(1)
.take(1)
.addListener({
next: (elements: Array<Element>) => {
assert.strictEqual(elements.length, 1);
const correctElement = elements[0];
assert.notStrictEqual(correctElement, null);
assert.notStrictEqual(typeof correctElement, 'undefined');
assert.strictEqual(correctElement.tagName, 'H4');
assert.strictEqual(correctElement.textContent, 'Correct');
done();
},
});
run();
});
it('should allow isolatedDOMSource.events() to work without crashing', function(done) {
function app(_sources: {DOM: MainDOMSource}) {
return {
DOM: xs.of(
h3('.top-most', [
div({isolate: [{type: 'total', scope: 'foo'}]}, [
h4('.bar', 'Hello'),
]),
])
),
};
}
const {sinks, sources, run} = setup(app, {
DOM: makeDOMDriver(createRenderTarget()),
});
let dispose: any;
const isolatedDOMSource = sources.DOM.isolateSource(sources.DOM, 'foo');
isolatedDOMSource.events('click').addListener({
next: (ev: Event) => {
dispose();
done();
},
});
isolatedDOMSource
.select('div')
.elements()
.drop(1)
.take(1)
.addListener({
next: (elements: Array<Element>) => {
assert.strictEqual(elements.length, 1);
const correctElement = elements[0] as HTMLElement;
assert.notStrictEqual(correctElement, null);
assert.notStrictEqual(typeof correctElement, 'undefined');
assert.strictEqual(correctElement.tagName, 'DIV');
assert.strictEqual(correctElement.textContent, 'Hello');
setTimeout(() => {
correctElement.click();
});
},
});
dispose = run();
});
it('should process bubbling events from inner to outer component', function(done) {
function app(_sources: {DOM: MainDOMSource}) {
return {
DOM: xs.of(
h3('.top-most', [
h2('.bar', 'Wrong'),
div({isolate: [{type: 'sibling', scope: '.foo'}]}, [
h4('.bar', 'Correct'),
]),
])
),
};
}
const {sinks, sources, run} = setup(app, {
DOM: makeDOMDriver(createRenderTarget()),
});
let dispose: any;
const isolatedDOMSource = sources.DOM.isolateSource(sources.DOM, '.foo');
let called = false;
sources.DOM.select('.top-most')
.events('click')
.addListener({
next: (ev: Event) => {
assert.strictEqual(called, true);
dispose();
done();
},
});
isolatedDOMSource
.select('h4.bar')
.events('click')
.addListener({
next: (ev: Event) => {
assert.strictEqual(called, false);
called = true;
},
});
isolatedDOMSource
.select('h4.bar')
.elements()
.drop(1)
.take(1)
.addListener({
next: (elements: Array<Element>) => {
assert.strictEqual(elements.length, 1);
const correctElement = elements[0] as HTMLElement;
assert.notStrictEqual(correctElement, null);
assert.notStrictEqual(typeof correctElement, 'undefined');
assert.strictEqual(correctElement.tagName, 'H4');
assert.strictEqual(correctElement.textContent, 'Correct');
setTimeout(() => {
correctElement.click();
});
},
});
dispose = run();
});
it('should stop bubbling the event if the currentTarget was removed', function(done) {
function main(_sources: {DOM: MainDOMSource}) {
const childExistence$ = _sources.DOM.isolateSource(_sources.DOM, 'foo')
.select('h4.bar')
.events('click')
.map(() => false)
.startWith(true);
return {
DOM: childExistence$.map(exists =>
div([
div('.top-most', {isolate: 'top'}, [
h2('.bar', 'Wrong'),
exists
? div({isolate: [{type: 'total', scope: 'foo'}]}, [
h4('.bar', 'Correct'),
])
: null,
]),
])
),
};
}
const {sinks, sources, run} = setup(main, {
DOM: makeDOMDriver(createRenderTarget()),
});
let dispose: any;
const topDOMSource = sources.DOM.isolateSource(sources.DOM, 'top');
const fooDOMSource = sources.DOM.isolateSource(sources.DOM, 'foo');
let parentEventHandlerCalled = false;
topDOMSource
.select('.bar')
.events('click')
.addListener({
next: (ev: any) => {
parentEventHandlerCalled = true;
done('this should not be called');
},
});
fooDOMSource
.select('.bar')
.elements()
.drop(1)
.take(1)
.addListener({
next: (elements: Array<Element>) => {
assert.strictEqual(elements.length, 1);
const correctElement = elements[0] as HTMLElement;
assert.notStrictEqual(correctElement, null);
assert.notStrictEqual(typeof correctElement, 'undefined');
assert.strictEqual(correctElement.tagName, 'H4');
assert.strictEqual(correctElement.textContent, 'Correct');
setTimeout(() => {
correctElement.click();
setTimeout(() => {
assert.strictEqual(parentEventHandlerCalled, false);
dispose();
done();
}, 150);
});
},
});
dispose = run();
});
it('should handle a higher-order graph when events() are subscribed', done => {
let errorHappened = false;
let clickDetected = false;
function Child(_sources: {DOM: MainDOMSource}) {
return {
DOM: _sources.DOM.select('.foo')
.events('click')
.debug(() => {
clickDetected = true;
})
.replaceError(() => {
errorHappened = true;
return xs.empty();
})
.mapTo(1)
.startWith(0)
.map(num => div('.container', [h3('.foo', 'Child foo')])),
};
}
function main(_sources: {DOM: MainDOMSource}) {
const first = isolate(Child, 'first')(_sources);
const second = isolate(Child, 'second')(_sources);
const oneChild = [first];
const twoChildren = [first, second];
const vnode$ = xs
.periodic(50)
.take(1)
.startWith(-1)
.map(i => (i === -1 ? oneChild : twoChildren))
.map(children =>
xs
.combine(...children.map(child => child.DOM))
.map(childVNodes => div('.parent', childVNodes))
)
.flatten();
return {
DOM: vnode$,
};
}
const {sinks, sources, run} = setup(main, {
DOM: makeDOMDriver(createRenderTarget()),
});
let dispose: any;
sources.DOM.select(':root')
.element()
.drop(2)
.take(1)
.addListener({
next: (root: Element) => {
const parentEl = root.querySelector('.parent') as HTMLElement;
const foo = parentEl.querySelectorAll('.foo')[1] as HTMLElement;
assert.notStrictEqual(parentEl, null);
assert.notStrictEqual(typeof parentEl, 'undefined');
assert.notStrictEqual(foo, null);
assert.notStrictEqual(typeof foo, 'undefined');
assert.strictEqual(parentEl.tagName, 'DIV');
setTimeout(() => {
assert.strictEqual(errorHappened, false);
foo.click();
setTimeout(() => {
assert.strictEqual(clickDetected, true);
dispose();
done();
}, 50);
}, 100);
},
});
dispose = run();
});
it('should handle events when child is removed and re-added', done => {
let clicksCount = 0;
function Child(_sources: {DOM: MainDOMSource}) {
_sources.DOM.select('.foo')
.events('click')
.addListener({
next: () => {
clicksCount++;
},
});
return {
DOM: xs.of(div('.foo', ['This is foo'])),
};
}
function main(_sources: {DOM: MainDOMSource}) {
const child = isolate(Child)(_sources);
// make child.DOM be inserted, removed, and inserted again
const innerDOM$ = xs
.periodic(120)
.take(2)
.map(x => x + 1)
.startWith(0)
.map(x => (x === 1 ? xs.of(div()) : (child.DOM)))
.flatten();
return {
DOM: innerDOM$,
};
}
const {sinks, sources, run} = setup(main, {
DOM: makeDOMDriver(createRenderTarget()),
});
let dispose: any;
sources.DOM.select(':root')
.element()
.drop(1)
.take(3)
.addListener({
next: (root: Element) => {
setTimeout(() => {
const foo = root.querySelector('.foo');
if (!foo) {
return;
}
(foo as any).click();
}, 0);
},
});
setTimeout(() => {
assert.strictEqual(clicksCount, 2);
dispose();
done();
}, 500);
dispose = run();
});
it('should handle events when parent is removed and re-added', done => {
let clicksCount = 0;
function Child(_sources: {DOM: MainDOMSource}) {
_sources.DOM.select('.foo')
.events('click')
.addListener({
next: () => {
clicksCount++;
},
});
return {
DOM: xs.of(div('.foo', ['This is foo'])),
};
}
function main(_sources: {DOM: MainDOMSource}) {
const child = isolate(Child, 'child')(_sources);
// change parent key, causing it to be recreated
const x$ = xs
.periodic(120)
.map(x => x + 1)
.startWith(0)
.take(4);
const innerDOM$ = xs
.combine<number, VNode>(x$, child.DOM)
.map(([x, childVDOM]) =>
div(`.parent${x}`, {key: `key${x}`}, [childVDOM, `${x}`])
);
return {
DOM: innerDOM$,
};
}
const {sinks, sources, run} = setup(main, {
DOM: makeDOMDriver(createRenderTarget()),
});
let dispose: any;
sources.DOM.select(':root')
.element()
.drop(1)
.take(4)
.addListener({
next: (root: Element) => {
setTimeout(() => {
const foo = root.querySelector('.foo');
if (!foo) {
return;
}
(foo as any).click();
}, 0);
},
});
setTimeout(() => {
assert.strictEqual(clicksCount, 4);
dispose();
done();
}, 800);
dispose = run();
});
it('should handle events when parent is removed and re-added, and has isolation scope', done => {
let clicksCount = 0;
function Child(_sources: {DOM: MainDOMSource}) {
_sources.DOM.select('.foo')
.events('click')
.addListener({
next: () => {
clicksCount++;
},
});
return {
DOM: xs.of(div('.foo', ['This is foo'])),
};
}
function Parent(_sources: {DOM: MainDOMSource}) {
const child = isolate(Child, 'child')(_sources);
// change parent key, causing it to be recreated
const x$ = xs
.periodic(120)
.map(x => x + 1)
.startWith(0)
.take(4);
const innerDOM$ = xs
.combine<number, VNode>(x$, child.DOM)
.map(([x, childVDOM]) =>
div(`.parent${x}`, {key: `key${x}`}, [childVDOM, `${x}`])
);
return {
DOM: innerDOM$,
};
}
function main(_sources: {DOM: MainDOMSource}) {
const parent = isolate(Parent, 'parent')(_sources);
return {
DOM: parent.DOM,
};
}
const {sinks, sources, run} = setup(main, {
DOM: makeDOMDriver(createRenderTarget()),
});
let dispose: any;
sources.DOM.select(':root')
.element()
.drop(1)
.take(4)
.addListener({
next: (root: Element) => {
setTimeout(() => {
const foo = root.querySelector('.foo');
if (!foo) {
return;
}
(foo as any).click();
}, 0);
},
});
setTimeout(() => {
assert.strictEqual(clicksCount, 4);
dispose();
done();
}, 800);
dispose = run();
});
it(
'should allow an isolated child to receive events when it is used as ' +
'the vTree of an isolated parent component',
done => {
let dispose: any;
function Component(_sources: {DOM: MainDOMSource}) {
_sources.DOM.select('.btn')
.events('click')
.addListener({
next: (ev: Event) => {
assert.strictEqual((ev.target as HTMLElement).tagName, 'BUTTON');
dispose();
done();
},
});
return {
DOM: xs.of(div('.component', {}, [button('.btn', {}, 'Hello')])),
};
}
function main(_sources: {DOM: MainDOMSource}) {
const component = isolate(Component)(_sources);
return {DOM: component.DOM};
}
function app(_sources: {DOM: MainDOMSource}) {
return isolate(main)(_sources);
}
const {sinks, sources, run} = setup(app, {
DOM: makeDOMDriver(createRenderTarget()),
});
sources.DOM.element()
.drop(1)
.take(1)
.addListener({
next: (root: Element) => {
const element = root.querySelector('.btn') as HTMLElement;
assert.notStrictEqual(element, null);
setTimeout(() => element.click());
},
});
dispose = run();
}
);
it(
'should allow an sibling isolated child to receive events when it is used as ' +
'the vTree of an isolated parent component',
done => {
let dispose: any;
function Component(_sources: {DOM: MainDOMSource}) {
_sources.DOM.select('.btn')
.events('click')
.addListener({
next: (ev: Event) => {
assert.strictEqual((ev.target as HTMLElement).tagName, 'BUTTON');
dispose();
done();
},
});
return {
DOM: xs.of(
div(
'.component',
{
props: {className: 'mydiv'},
},
[button('.btn', {}, 'Hello')]
)
),
};
}
function main(_sources: {DOM: MainDOMSource}) {
const component = isolate(Component, '.foo')(_sources);
return {DOM: component.DOM};
}
function app(_sources: {DOM: MainDOMSource}) {
return isolate(main)(_sources);
}
const {sinks, sources, run} = setup(app, {
DOM: makeDOMDriver(createRenderTarget()),
});
sources.DOM.element()
.drop(1)
.take(1)
.addListener({
next: (root: Element) => {
const element = root.querySelector('.btn') as HTMLElement;
assert.notStrictEqual(element, null);
setTimeout(() => element.click());
},
});
dispose = run();
}
);
it(
'should allow an isolated child to receive events when it is used as ' +
'the vTree of an isolated parent component when scope is explicitly ' +
'specified on child',
done => {
let dispose: any;
function Component(_sources: {DOM: MainDOMSource}) {
_sources.DOM.select('.btn')
.events('click')
.addListener({
next: (ev: Event) => {
assert.strictEqual((ev.target as HTMLElement).tagName, 'BUTTON');
dispose();
done();
},
});
return {
DOM: xs.of(div('.component', {}, [button('.btn', {}, 'Hello')])),
};
}
function main(_sources: {DOM: MainDOMSource}) {
const component = isolate(Component, 'foo')(_sources);
return {DOM: component.DOM};
}
function app(_sources: {DOM: MainDOMSource}) {
return isolate(main)(_sources);
}
const {sinks, sources, run} = setup(app, {
DOM: makeDOMDriver(createRenderTarget()),
});
sources.DOM.element()
.drop(1)
.take(1)
.addListener({
next: (root: Element) => {
const element = root.querySelector('.btn') as HTMLElement;
assert.notStrictEqual(element, null);
setTimeout(() => element.click());
},
});
dispose = run();
}
);
it(
'should allow an isolated child to receive events when it is used as ' +
'the vTree of an isolated parent component when scope is explicitly ' +
'specified on parent',
done => {
let dispose: any;
function Component(_sources: {DOM: MainDOMSource}) {
_sources.DOM.select('.btn')
.events('click')
.addListener({
next: (ev: Event) => {
assert.strictEqual((ev.target as HTMLElement).tagName, 'BUTTON');
dispose();
done();
},
});
return {
DOM: xs.of(div('.component', {}, [button('.btn', {}, 'Hello')])),
};
}
function main(_sources: {DOM: MainDOMSource}) {
const component = isolate(Component)(_sources);
return {DOM: component.DOM};
}
function app(_sources: {DOM: MainDOMSource}) {
return isolate(main, 'foo')(_sources);
}
const {sinks, sources, run} = setup(app, {
DOM: makeDOMDriver(createRenderTarget()),
});
sources.DOM.element()
.drop(1)
.take(1)
.addListener({
next: (root: Element) => {
const element = root.querySelector('.btn') as HTMLElement;
assert.notStrictEqual(element, null);
setTimeout(() => element.click());
},
});
dispose = run();
}
);
it(
'should allow an isolated child to receive events when it is used as ' +
'the vTree of an isolated parent component when scope is explicitly ' +
'specified on parent and child',
done => {
let dispose: any;
function Component(_sources: {DOM: MainDOMSource}) {
_sources.DOM.select('.btn')
.events('click')
.addListener({
next: (ev: Event) => {
assert.strictEqual((ev.target as HTMLElement).tagName, 'BUTTON');
dispose();
done();
},
});
return {
DOM: xs.of(div('.component', {}, [button('.btn', {}, 'Hello')])),
};
}
function main(_sources: {DOM: MainDOMSource}) {
const component = isolate(Component, 'bar')(_sources);
return {DOM: component.DOM};
}
function app(_sources: {DOM: MainDOMSource}) {
return isolate(main, 'foo')(_sources);
}
const {sinks, sources, run} = setup(app, {
DOM: makeDOMDriver(createRenderTarget()),
});
sources.DOM.element()
.drop(1)
.take(1)
.addListener({
next: (root: Element) => {
const element = root.querySelector('.btn') as HTMLElement;
assert.notStrictEqual(element, null);
setTimeout(() => element.click());
},
});
dispose = run();
}
);
it(
'should maintain virtual DOM list sanity using keys, in a list of ' +
'isolated components',
done => {
const componentRemove$ = xs.create<any>();
function Component(_sources: {DOM: MainDOMSource}) {
_sources.DOM.select('.btn')
.events('click')
.addListener({
next: (ev: Event) => {
componentRemove$.shamefullySendNext(null);
},
});
return {
DOM: xs.of(div('.component', {}, [button('.btn', {}, 'Hello')])),
};
}
function main(_sources: {DOM: MainDOMSource}) {
const remove$ = componentRemove$
.compose(delay(50))
.fold(acc => acc + 1, 0);
const first = isolate(Component, 'first')(_sources);
const second = isolate(Component, 'second')(_sources);
const vdom$ = xs
.combine(first.DOM, second.DOM, remove$)
.map(([vdom1, vdom2, r]) => {
if (r === 0) {
return div([vdom1, vdom2]);
} else if (r === 1) {
return div([vdom2]);
} else if (r === 2) {
return div([]);
} else {
done('This case must not happen.');
return div();
}
});
return {DOM: vdom$};
}
const {sinks, sources, run} = setup(main, {
DOM: makeDOMDriver(createRenderTarget()),
});
let dispose: any;
sources.DOM.element()
.drop(1)
.take(1)
.addListener({
next: (root: Element) => {
const components = root.querySelectorAll('.btn');
assert.strictEqual(components.length, 2);
const firstElement = components[0] as HTMLElement;
const secondElement = components[1] as HTMLElement;
setTimeout(() => {
firstElement.click();
}, 100);
setTimeout(() => {
secondElement.click();
}, 300);
setTimeout(() => {
assert.strictEqual(root.querySelectorAll('.component').length, 0);
dispose();
done();
}, 500);
},
});
dispose = run();
}
);
it('should allow null or undefined isolated child DOM', function(done) {
function child(_sources: {DOM: MainDOMSource}) {
const visible$ = xs
.periodic(50)
.take(1)
.fold((acc, _) => !acc, true);
const vdom$ = visible$.map(visible => (visible ? h4('child') : null));
return {
DOM: vdom$,
};
}
function main(_sources: {DOM: MainDOMSource}) {
const childSinks = isolate(child, 'child')(_sources);
const vdom$ = childSinks.DOM.map((childVDom: VNode) =>
div('.parent', [childVDom, h2('part of parent')])
);
return {
DOM: vdom$,
};
}
const {sinks, sources, run} = setup(main, {
DOM: makeDOMDriver(createRenderTarget()),
});
let dispose: any;
sources.DOM.element()
.drop(1)
.take(1)
.addListener({
next: (root: Element) => {
const parentEl = root.querySelector('.parent') as Element;
assert.strictEqual(parentEl.childNodes.length, 2);
assert.strictEqual(parentEl.children[0].tagName, 'H4');
assert.strictEqual(parentEl.children[0].textContent, 'child');
assert.strictEqual(parentEl.children[1].tagName, 'H2');
assert.strictEqual(
parentEl.children[1].textContent,
'part of parent'
);
},
});
sources.DOM.element()
.drop(2)
.take(1)
.addListener({
next: (root: Element) => {
const parentEl = root.querySelector('.parent') as Element;
assert.strictEqual(parentEl.childNodes.length, 1);
assert.strictEqual(parentEl.children[0].tagName, 'H2');
assert.strictEqual(
parentEl.children[0].textContent,
'part of parent'
);
dispose();
done();
},
});
dispose = run();
});
it('should allow recursive isolation using the same scope', done => {
function Item(_sources: {DOM: MainDOMSource}, count: number) {
const childVdom$: Stream<VNode> =
count > 0
? isolate(Item, '0')(_sources, count - 1).DOM
: xs.of<any>(null);
const highlight$ = _sources.DOM.select('button')
.events('click')
.mapTo(true)
.fold((x, _) => !x, false);
const vdom$ = xs
.combine(childVdom$, highlight$)
.map(([childVdom, highlight]) =>
div([
button('.btn', highlight ? 'HIGHLIGHTED' : 'click me'),
childVdom,
])
);
return {DOM: vdom$};
}
function main(_sources: {DOM: MainDOMSource}) {
const vdom$ = Item(_sources, 3).DOM;
return {DOM: vdom$};
}
const {sinks, sources, run} = setup(main, {
DOM: makeDOMDriver(createRenderTarget()),
});
let dispose: any;
sources.DOM.element()
.drop(1)
.take(1)
.addListener({
next: (root: Element) => {
const buttons = root.querySelectorAll('.btn');
assert.strictEqual(buttons.length, 4);
const firstButton = buttons[0];
const secondButton = buttons[1];
const thirdButton = buttons[2] as HTMLElement;
const forthButton = buttons[3];
setTimeout(() => {
thirdButton.click();
}, 100);
setTimeout(() => {
assert.notStrictEqual(firstButton.textContent, 'HIGHLIGHTED');
assert.notStrictEqual(secondButton.textContent, 'HIGHLIGHTED');
assert.strictEqual(thirdButton.textContent, 'HIGHLIGHTED');
assert.notStrictEqual(forthButton.textContent, 'HIGHLIGHTED');
dispose();
done();
}, 300);
},
});
dispose = run();
});
it('should not lose event delegators when components are moved around', function(done) {
function component(_sources: {DOM: MainDOMSource}) {
const click$ = _sources.DOM.select('.click-me')
.events('click')
.mapTo('clicked');
return {
DOM: xs.of(button('.click-me', 'click me')),
click$,
};
}
function app(_sources: {DOM: MainDOMSource}) {
const comp = isolate(component, 'child')(_sources);
const position$ = fromDiagram('1-2|');
return {
DOM: xs.combine(position$, comp.DOM).map(([position, childDom]) => {
const children =
position === '1'
? [div([childDom]), div()]
: [div(), div([childDom])];
return div(children);
}),
c