288 | commit();
289 | mode = MODE_WHITESPACE;
290 | }
291 | else {
292 | buffer += char;
293 | }
294 |
295 | if (mode === MODE_TAGNAME && buffer === '!--') {
296 | mode = MODE_COMMENT;
297 | current = current[0];
298 | }
299 | }
300 | }
301 | commit();
302 |
303 | if (MINI) {
304 | return current.length > 2 ? current.slice(1) : current[1];
305 | }
306 | return current;
307 | };
308 |
309 | const CACHES = new Map();
310 |
311 | const regular = function(statics) {
312 | let tmp = CACHES.get(this);
313 | if (!tmp) {
314 | tmp = new Map();
315 | CACHES.set(this, tmp);
316 | }
317 | tmp = evaluate(this, tmp.get(statics) || (tmp.set(statics, tmp = build(statics)), tmp), arguments, []);
318 | return tmp.length > 1 ? tmp : tmp[0];
319 | };
320 |
321 | const custom = function() {
322 | const result = (MINI ? build : regular).apply(this, arguments);
323 | if (result) {
324 | return Array.isArray(result)
325 | ? this(result)
326 | : typeof result === 'object'
327 | ? result
328 | : this([result]);
329 | }
330 | };
331 |
332 | const wrapper = function() {
333 | const h = custom.bind(this);
334 | return (this.wrap || h).apply(h, arguments);
335 | };
336 |
337 | export default wrapper;
338 |
--------------------------------------------------------------------------------
/src/hydrate.d.ts:
--------------------------------------------------------------------------------
1 | import { JSXInternal } from './jsx';
2 | import { ElementChildren, FunctionComponent } from './shared';
3 |
4 | interface VNode {
5 | type: string
6 | _props: object
7 | _children: VNode[]
8 | _isSvg: boolean
9 | }
10 |
11 | export function hydrate(delta: VNode, root?: Node): Node;
12 |
13 | export const dhtml: (strings: TemplateStringsArray, ...values: any[]) => VNode | VNode[];
14 | export const dsvg: (strings: TemplateStringsArray, ...values: any[]) => VNode | VNode[];
15 |
16 | export function d(
17 | type: string,
18 | props:
19 | | JSXInternal.HTMLAttributes &
20 | Record
21 | | null,
22 | ...children: ElementChildren[]
23 | ): VNode | VNode[];
24 | export function d(
25 | type: FunctionComponent,
26 | props:
27 | | JSXInternal.HTMLAttributes &
28 | Record
29 | | null,
30 | ...children: ElementChildren[]
31 | ): VNode | VNode[];
32 | export function d(
33 | children: ElementChildren[]
34 | ): VNode | VNode[];
35 | export namespace d {
36 | export import JSX = JSXInternal;
37 | }
38 |
39 | export function ds(
40 | type: string,
41 | props:
42 | | JSXInternal.SVGAttributes &
43 | Record
44 | | null,
45 | ...children: ElementChildren[]
46 | ): VNode | VNode[];
47 | export function ds(
48 | type: FunctionComponent,
49 | props:
50 | | JSXInternal.SVGAttributes &
51 | Record
52 | | null,
53 | ...children: ElementChildren[]
54 | ): VNode | VNode[];
55 | export function ds(
56 | children: ElementChildren[]
57 | ): VNode | VNode[];
58 | export namespace ds {
59 | export import JSX = JSXInternal;
60 | }
61 |
--------------------------------------------------------------------------------
/src/hydrate.js:
--------------------------------------------------------------------------------
1 | import { h, hs, api } from './index.js';
2 | import htm from './htm.js';
3 |
4 | export const d = context();
5 | export const ds = context(true);
6 |
7 | // `export const html = htm.bind(h)` is not tree-shakeable!
8 | export function dhtml() {
9 | return htm.apply(d, arguments);
10 | }
11 |
12 | // `export const svg = htm.bind(hs)` is not tree-shakeable!
13 | export function dsvg() {
14 | return htm.apply(ds, arguments);
15 | }
16 |
17 | export const _ = {};
18 |
19 | let isHydrated;
20 |
21 | /**
22 | * Create a sinuous `treeify` function.
23 | * @param {boolean} isSvg
24 | * @return {Function}
25 | */
26 | export function context(isSvg) {
27 | return function () {
28 | if (isHydrated) {
29 | // Hydrate on first pass, create on the rest.
30 | return (isSvg ? hs : h).apply(null, arguments);
31 | }
32 |
33 | let vnode;
34 |
35 | function item(arg) {
36 | if (arg == null);
37 | else if (arg === _ || typeof arg === 'function') {
38 | // Components can only be the first argument.
39 | if (vnode) {
40 | addChild(vnode, arg);
41 | } else {
42 | vnode = { type: arg, _children: [] };
43 | }
44 | } else if (Array.isArray(arg)) {
45 | vnode = vnode || { _children: [] };
46 | arg.forEach(item);
47 | } else if (typeof arg === 'object') {
48 | if (arg._children) {
49 | addChild(vnode, arg);
50 | } else {
51 | vnode._props = arg;
52 | }
53 | } else {
54 | // The rest is made into a string.
55 | if (vnode) {
56 | addChild(vnode, { type: null, _props: arg });
57 | } else {
58 | vnode = { type: arg, _children: [] };
59 | }
60 | }
61 |
62 | if (isSvg) vnode._isSvg = isSvg;
63 | }
64 |
65 | function addChild(parent, child) {
66 | parent._children.push(child);
67 | child._parent = parent;
68 | }
69 |
70 | Array.from(arguments).forEach(item);
71 |
72 | return vnode;
73 | };
74 | }
75 |
76 | /**
77 | * Hydrates the root node with a passed delta tree structure.
78 | *
79 | * `delta` looks like:
80 | * {
81 | * type: 'div',
82 | * _props: { class: '' },
83 | * _children: []
84 | * }
85 | *
86 | * @param {object} delta
87 | * @param {Node} [root]
88 | * @return {Node} Returns the `root`.
89 | */
90 | export function hydrate(delta, root) {
91 | if (!delta) {
92 | return;
93 | }
94 |
95 | if (typeof delta.type === 'function') {
96 | // Support Components
97 | delta = delta.type.apply(
98 | null,
99 | [delta._props].concat(delta._children.map((c) => c()))
100 | );
101 | }
102 |
103 | let isFragment = delta.type === undefined;
104 | let isRootFragment;
105 | let el;
106 |
107 | if (!root) {
108 | root = document.querySelector(findRootSelector(delta));
109 | }
110 |
111 | function findRootSelector(delta) {
112 | let selector = '';
113 | let prop;
114 | if (delta._props && (prop = delta._props.id)) {
115 | selector = '#';
116 | } else if (delta._props && (prop = delta._props.class)) {
117 | selector = '.';
118 | } else if ((prop = delta.type)) {
119 | // delta.type is truthy
120 | } else {
121 | isRootFragment = true;
122 | return delta._children && findRootSelector(delta._children[0]());
123 | }
124 |
125 | return (
126 | selector +
127 | (typeof prop === 'function' ? prop() : prop)
128 | .split(' ')
129 | // Escape CSS selector https://bit.ly/36h9I83
130 | .map((sel) => sel.replace(/([^\x80-\uFFFF\w-])/g, '\\$1'))
131 | .join('.')
132 | );
133 | }
134 |
135 | function item(arg) {
136 | if (arg instanceof Node) {
137 | el = arg;
138 | // Keep a child pointer for multiple hydrate calls per element.
139 | el._index = el._index || 0;
140 | } else if (Array.isArray(arg)) {
141 | arg.forEach(item);
142 | } else if (el) {
143 | let target = filterChildNodes(el)[el._index];
144 | let current;
145 | let prefix;
146 |
147 | const updateText = (text) => {
148 | el._index++;
149 |
150 | // Leave whitespace alone.
151 | if (target.data.trim() !== text.trim()) {
152 | if (arg._parent._children.length !== filterChildNodes(el).length) {
153 | // If the parent's virtual children length don't match the DOM's,
154 | // it's probably adjacent text nodes stuck together. Split them.
155 | target.splitText(target.data.indexOf(text) + text.length);
156 | if (current) {
157 | // Leave prefix whitespace intact.
158 | prefix = current.match(/^\s*/)[0];
159 | }
160 | }
161 | // Leave whitespace alone.
162 | if (target.data.trim() !== text.trim()) {
163 | target.data = text;
164 | }
165 | }
166 | };
167 |
168 | if (target) {
169 | // Skip placeholder underscore.
170 | if (arg === _) {
171 | el._index++;
172 | } else if (typeof arg === 'object') {
173 | if (arg.type === null && target.nodeType === 3) {
174 | // This is a text vnode, add noskip so spaces don't get skipped.
175 | target._noskip = true;
176 | updateText(arg._props);
177 | } else if (arg.type) {
178 | hydrate(arg, target);
179 | el._index++;
180 | }
181 | }
182 | }
183 |
184 | if (typeof arg === 'function') {
185 | current = target ? target.data : undefined;
186 | prefix = '';
187 | let hydrated;
188 | let marker;
189 | let startNode;
190 | api.subscribe(() => {
191 | isHydrated = hydrated;
192 |
193 | let result = arg();
194 | if (result && result._children) {
195 | result = result.type ? result : result._children;
196 | }
197 |
198 | const isStringable =
199 | typeof result === 'string' || typeof result === 'number';
200 | result = isStringable ? prefix + result : result;
201 |
202 | if (hydrated || (!target && !isFragment)) {
203 | current = api.insert(el, result, marker, current, startNode);
204 | } else {
205 | if (isStringable) {
206 | updateText(result);
207 | } else {
208 | if (Array.isArray(result)) {
209 | startNode = target;
210 | target = el;
211 | }
212 |
213 | if (isRootFragment) {
214 | target = el;
215 | }
216 |
217 | hydrate(result, target);
218 | current = [];
219 | }
220 |
221 | if (!isRootFragment && target) {
222 | marker = api.add(el, '', filterChildNodes(el)[el._index]);
223 | } else {
224 | marker = api.add(el.parentNode, '', el.nextSibling);
225 | }
226 | }
227 |
228 | isHydrated = false;
229 | hydrated = true;
230 | });
231 | } else if (typeof arg === 'object') {
232 | if (!arg._children) {
233 | api.property(el, arg, null, delta._isSvg);
234 | }
235 | }
236 | }
237 | }
238 |
239 | [root, delta._props, delta._children || delta].forEach(item);
240 |
241 | return el;
242 | }
243 |
244 | /**
245 | * Filter out whitespace text nodes unless it has a noskip indicator.
246 | *
247 | * @param {Node} parent
248 | * @return {Array}
249 | */
250 | function filterChildNodes(parent) {
251 | return Array.from(parent.childNodes).filter(
252 | (el) => el.nodeType !== 3 || el.data.trim() || el._noskip
253 | );
254 | }
255 |
--------------------------------------------------------------------------------
/src/hydrate.md:
--------------------------------------------------------------------------------
1 | # Sinuous Hydrate
2 |
3 | Sinuous Hydrate is a small add-on for [Sinuous](https://github.com/luwes/sinuous) that provides fast hydration of static HTML. The HTML or SVG that is defined with this API doesn't have to be exactly the same as the HTML coming from the server. It's perfectly valid to only define the attributes that have any dynamic values in it. This is intentionally done to minimize duplication.
4 |
5 | # Example
6 |
7 | ```js
8 | import { observable } from 'sinuous';
9 | import { hydrate, d } from 'sinuous/hydrate';
10 | import { openLogin } from './auth.js';
11 |
12 | const isActive = observable('');
13 |
14 | hydrate(
15 | dhtml`
16 |
17 | `
18 | );
19 |
20 | hydrate(
21 | dhtml`
22 | isActive(!isActive() ? ' is-active' : '')}
25 | />
26 | `
27 | );
28 |
29 | hydrate(
30 | dhtml`
31 |
32 | `
33 | );
34 | ```
35 |
36 | # API
37 |
38 | ### hydrate(tree, [root]) ⇒ Node
39 |
40 | Hydrates the root node with the dynamic HTML.
41 | Passing the root node is not needed if it can be derived from the `id` or `class` attribute of the root HTML or SVG tree.
42 |
43 | **Returns**: Node
- Returns the root node.
44 |
45 | | Param | Type | Description |
46 | | ------ | ------------------- | ----------------------- |
47 | | tree | Object
| Virtual tree structure. |
48 | | [root] | Node
| Root node. |
49 |
50 | ### dhtml`` or d()
51 |
52 | Creates a virtual tree structure for HTML.
53 | Looks like:
54 |
55 | ```js
56 | {
57 | type: 'div',
58 | _props: { class: '' },
59 | _children: []
60 | }
61 | ```
62 |
63 | ### dsvg`` or ds()
64 |
65 | Creates a virtual tree structure for SVG.
66 |
67 | ### \_
68 |
69 | A placeholder for content in tags that get skipped. The placeholder prevents duplication of long static texts in JavaScript which would add unnecessary bytes to your bundle.
70 |
71 | For example:
72 |
73 | ```js
74 | import { hydrate, dhtml, _ } from 'sinuous/hydrate';
75 |
76 | document.body.innerHTML = `
77 |
78 |
Banana
79 |
80 |
81 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
82 | eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim
83 | ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
84 | aliquip ex ea commodo consequat. Duis aute irure dolor in
85 | reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
86 | pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
87 | culpa qui officia deserunt mollit anim id est laborum.
88 |
89 |
Bom
90 |
91 |
92 | `;
93 |
94 | hydrate(dhtml`
95 |
96 |
${_}
97 |
98 |
${_}
99 |
${_}
100 |
101 |
102 | `);
103 | ```
104 |
--------------------------------------------------------------------------------
/src/index.d.ts:
--------------------------------------------------------------------------------
1 | export = sinuous;
2 | export as namespace sinuous;
3 |
4 | import { JSXInternal } from './jsx';
5 | import { HyperscriptApi } from './h';
6 | import * as _shared from './shared'
7 | import * as _o from './observable';
8 |
9 | import FunctionComponent = _shared.FunctionComponent;
10 | import ElementChildren = _shared.ElementChildren;
11 |
12 | declare module 'sinuous/jsx' {
13 | namespace JSXInternal {
14 | interface DOMAttributes {
15 | children?: ElementChildren;
16 | }
17 | }
18 | }
19 |
20 | // Adapted from Preact's index.d.ts
21 | // Namespace prevents conflict with React typings
22 | declare namespace sinuous {
23 | export import JSX = JSXInternal;
24 |
25 | export import observable = _o.observable;
26 | export import o = _o.o;
27 |
28 | const html: (strings: TemplateStringsArray, ...values: unknown[]) => HTMLElement | DocumentFragment;
29 | const svg: (strings: TemplateStringsArray, ...values: unknown[]) => SVGElement | DocumentFragment;
30 |
31 | // Split HyperscriptApi's h() tag into functions with more narrow typings
32 | function h(
33 | type: string,
34 | props:
35 | | JSXInternal.HTMLAttributes &
36 | Record
37 | | null,
38 | ...children: ElementChildren[]
39 | ): HTMLElement;
40 | function h(
41 | type: FunctionComponent,
42 | props:
43 | | JSXInternal.HTMLAttributes &
44 | Record
45 | | null,
46 | ...children: ElementChildren[]
47 | ): HTMLElement | DocumentFragment;
48 | function h(
49 | tag: ElementChildren[] | [],
50 | ...children: ElementChildren[]
51 | ): DocumentFragment;
52 | namespace h {
53 | export import JSX = JSXInternal;
54 | }
55 |
56 | function hs(
57 | type: string,
58 | props:
59 | | JSXInternal.SVGAttributes &
60 | Record
61 | | null,
62 | ...children: ElementChildren[]
63 | ): SVGElement;
64 | function hs(
65 | type: FunctionComponent,
66 | props:
67 | | JSXInternal.SVGAttributes &
68 | Record
69 | | null,
70 | ...children: ElementChildren[]
71 | ): SVGElement | DocumentFragment;
72 | function hs(
73 | tag: ElementChildren[] | [],
74 | ...children: ElementChildren[]
75 | ): DocumentFragment;
76 | namespace hs {
77 | export import JSX = JSXInternal;
78 | }
79 |
80 | /** Sinuous API */
81 | interface SinuousApi extends HyperscriptApi {
82 | // Hyperscript
83 | hs: unknown>(closure: T) => ReturnType;
84 |
85 | // Observable
86 | subscribe: typeof _o.subscribe;
87 | cleanup: typeof _o.cleanup;
88 | root: typeof _o.root;
89 | sample: typeof _o.sample;
90 | }
91 |
92 | const api: SinuousApi;
93 |
94 | }
95 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Sinuous by Wesley Luyten (@luwes).
3 | * Really ties all the packages together.
4 | */
5 | import {
6 | o,
7 | observable,
8 | computed,
9 | subscribe,
10 | cleanup,
11 | root,
12 | sample,
13 | } from './observable.js';
14 | import { api } from './h.js';
15 | import htm from './htm.js';
16 |
17 | // Minified this is actually smaller than Object.assign(api, { ... })
18 | api.subscribe = subscribe;
19 | api.cleanup = cleanup;
20 | api.root = root;
21 | api.sample = sample;
22 |
23 | api.hs = (...args) => {
24 | const prevIsSvg = api.s;
25 | api.s = true;
26 | const el = h(...args);
27 | api.s = prevIsSvg;
28 | return el;
29 | };
30 |
31 | // Makes it possible to intercept `h` calls and customize.
32 | export const h = (...args) => api.h.apply(api.h, args);
33 |
34 | // Makes it possible to intercept `hs` calls and customize.
35 | export const hs = (...args) => api.hs.apply(api.hs, args);
36 |
37 | // `export const html = htm.bind(h)` is not tree-shakeable!
38 | export const html = (...args) => htm.apply(h, args);
39 |
40 | // `export const svg = htm.bind(hs)` is not tree-shakeable!
41 | export const svg = (...args) => htm.apply(hs, args);
42 |
43 | export { api, o, observable, computed };
44 |
--------------------------------------------------------------------------------
/src/map.d.ts:
--------------------------------------------------------------------------------
1 | import { Observable } from './observable';
2 |
3 | export function map(
4 | items: ((...args: unknown[]) => T[]) | Observable,
5 | expr: (item: T, i: number, items: T[]) => Node,
6 | cleaning?: boolean
7 | ): DocumentFragment;
8 |
--------------------------------------------------------------------------------
/src/map.js:
--------------------------------------------------------------------------------
1 | /* Adapted from Stage0 - The MIT License - Pavel Martynov */
2 | /* Adapted from DOM Expressions - The MIT License - Ryan Carniato */
3 | import { api } from './index.js';
4 |
5 | export const GROUPING = '__g';
6 | export const FORWARD = 'nextSibling';
7 | export const BACKWARD = 'previousSibling';
8 |
9 | /**
10 | * Map over a list of items that create DOM nodes.
11 | *
12 | * @param {Function} items - Function or observable that creates a list.
13 | * @param {Function} expr
14 | * @param {boolean} [cleaning]
15 | * @return {DocumentFragment}
16 | */
17 | export function map(items, expr, cleaning) {
18 | const { subscribe, root, sample, cleanup } = api;
19 |
20 | // Disable cleaning for templates by default.
21 | if (cleaning == null) cleaning = !expr.$t;
22 |
23 | let parent = document.createDocumentFragment();
24 | const beforeNode = add(parent, '');
25 | const afterNode = add(parent, '');
26 | const disposers = new Map();
27 |
28 | const unsubscribe = subscribe((a) => {
29 | const b = items();
30 | return sample(() =>
31 | reconcile(
32 | a || [],
33 | b || [],
34 | beforeNode,
35 | afterNode,
36 | createFn,
37 | cleaning && disposeAll,
38 | cleaning && dispose
39 | )
40 | );
41 | });
42 |
43 | cleanup(unsubscribe);
44 | cleanup(disposeAll);
45 |
46 | function disposeAll() {
47 | disposers.forEach((d) => d());
48 | disposers.clear();
49 | }
50 |
51 | function dispose(node) {
52 | let disposer = disposers.get(node);
53 | disposer && disposer();
54 | disposers.delete(node);
55 | }
56 |
57 | function createFn(parent, item, i, data, afterNode) {
58 | // The root call makes it possible the child's computations outlive
59 | // their parents' update cycle.
60 | return cleaning
61 | ? root((disposeFn) => {
62 | const node = add(parent, expr(item, i, data), afterNode);
63 | disposers.set(node, disposeFn);
64 | return node;
65 | })
66 | : add(parent, expr(item, i, data), afterNode);
67 | }
68 |
69 | return parent;
70 | }
71 |
72 | // This is almost straightforward implementation of reconcillation algorithm
73 | // based on ivi documentation:
74 | // https://github.com/localvoid/ivi/blob/2c81ead934b9128e092cc2a5ef2d3cabc73cb5dd/packages/ivi/src/vdom/implementation.ts#L1366
75 | // With some fast paths from Surplus implementation:
76 | // https://github.com/adamhaile/surplus/blob/master/src/runtime/content.ts#L86
77 | // And working with data directly from Stage0:
78 | // https://github.com/Freak613/stage0/blob/master/reconcile.js
79 | // This implementation is tailored for fine grained change detection and adds support for fragments
80 | export function reconcile(
81 | a,
82 | b,
83 | beforeNode,
84 | afterNode,
85 | createFn,
86 | onClear,
87 | onRemove
88 | ) {
89 | // When parent was a DocumentFragment, then items got appended to the DOM.
90 | const parent = afterNode.parentNode;
91 |
92 | let length = b.length;
93 | let i;
94 |
95 | // Fast path for clear
96 | if (length === 0) {
97 | let startMark = beforeNode.previousSibling;
98 | if ((startMark && startMark.previousSibling) || afterNode.nextSibling) {
99 | removeNodes(parent, beforeNode.nextSibling, afterNode);
100 | } else {
101 | parent.textContent = '';
102 | if (startMark) {
103 | parent.appendChild(startMark);
104 | }
105 | parent.appendChild(beforeNode);
106 | parent.appendChild(afterNode);
107 | }
108 |
109 | onClear && onClear();
110 | return [];
111 | }
112 |
113 | // Fast path for create
114 | if (a.length === 0) {
115 | for (i = 0; i < length; i++) {
116 | createFn(parent, b[i], i, b, afterNode);
117 | }
118 | return b.slice();
119 | }
120 |
121 | let aStart = 0;
122 | let bStart = 0;
123 | let aEnd = a.length - 1;
124 | let bEnd = length - 1;
125 | let tmp;
126 | let aStartNode = beforeNode.nextSibling;
127 | let aEndNode = afterNode.previousSibling;
128 | let bAfterNode = afterNode;
129 | let mark;
130 |
131 | fixes: while (true) {
132 | // Skip prefix
133 | while (a[aStart] === b[bStart]) {
134 | bStart++;
135 | aStartNode = step(aStartNode, FORWARD);
136 | if (aEnd < ++aStart || bEnd < bStart) break fixes;
137 | }
138 |
139 | // Skip suffix
140 | while (a[aEnd] === b[bEnd]) {
141 | bEnd--;
142 | bAfterNode = step(aEndNode, BACKWARD, true);
143 | aEndNode = bAfterNode.previousSibling;
144 | if (--aEnd < aStart || bEnd < bStart) break fixes;
145 | }
146 |
147 | break;
148 | }
149 |
150 | // Fast path for shrink
151 | if (bEnd < bStart) {
152 | while (aStart <= aEnd--) {
153 | tmp = step(aEndNode, BACKWARD, true);
154 | mark = tmp.previousSibling;
155 | removeNodes(parent, tmp, aEndNode.nextSibling);
156 | onRemove && onRemove(tmp);
157 | aEndNode = mark;
158 | }
159 | return b.slice();
160 | }
161 |
162 | // Fast path for add
163 | if (aEnd < aStart) {
164 | while (bStart <= bEnd) {
165 | createFn(parent, b[bStart++], bStart, b, bAfterNode);
166 | }
167 | return b.slice();
168 | }
169 |
170 | // Positions for reusing nodes from current DOM state
171 | const P = new Array(bEnd + 1 - bStart);
172 | // Index to resolve position from current to new
173 | const I = new Map();
174 | for (i = bStart; i <= bEnd; i++) {
175 | P[i] = -1;
176 | I.set(b[i], i);
177 | }
178 |
179 | // Re-using `length` variable for reusing nodes count.
180 | length = 0;
181 | let toRemove = [];
182 | for (i = aStart; i <= aEnd; i++) {
183 | tmp = I.get(a[i]);
184 | if (tmp) {
185 | P[tmp] = i;
186 | length++;
187 | } else {
188 | toRemove.push(i);
189 | }
190 | }
191 |
192 | // Fast path for full replace
193 | if (length === 0) {
194 | return reconcile(
195 | reconcile(a, [], beforeNode, afterNode, createFn, onClear),
196 | b,
197 | beforeNode,
198 | afterNode,
199 | createFn
200 | );
201 | }
202 |
203 | // Collect nodes to work with them
204 | const nodes = [];
205 | tmp = aStartNode;
206 | for (i = aStart; i <= aEnd; i++) {
207 | nodes[i] = tmp;
208 | tmp = step(tmp, FORWARD);
209 | }
210 |
211 | for (i = 0; i < toRemove.length; i++) {
212 | let index = toRemove[i];
213 | tmp = nodes[index];
214 | removeNodes(parent, tmp, step(tmp, FORWARD));
215 | onRemove && onRemove(tmp);
216 | }
217 |
218 | const longestSeq = longestPositiveIncreasingSubsequence(P, bStart);
219 | // Re-use `length` for longest sequence length.
220 | length = longestSeq.length - 1;
221 |
222 | for (i = bEnd; i >= bStart; i--) {
223 | if (longestSeq[length] === i) {
224 | bAfterNode = nodes[P[longestSeq[length]]];
225 | length--;
226 | } else {
227 | if (P[i] === -1) {
228 | tmp = createFn(parent, b[i], i, b, bAfterNode);
229 | } else {
230 | tmp = nodes[P[i]];
231 | insertNodes(parent, tmp, step(tmp, FORWARD), bAfterNode);
232 | }
233 | bAfterNode = tmp;
234 | }
235 | }
236 |
237 | return b.slice();
238 | }
239 |
240 | let groupCounter = 0;
241 |
242 | function add(parent, value, endMark) {
243 | let mark;
244 |
245 | if (typeof value === 'string') {
246 | value = document.createTextNode(value);
247 | } else if (!(value instanceof Node)) {
248 | // Passing an empty array creates a DocumentFragment.
249 | value = api.h([], value);
250 | }
251 |
252 | if (
253 | value.nodeType === 11 &&
254 | (mark = value.firstChild) &&
255 | mark !== value.lastChild
256 | ) {
257 | mark[GROUPING] = value.lastChild[GROUPING] = ++groupCounter;
258 | }
259 |
260 | // If endMark is `null`, value will be added to the end of the list.
261 | parent.insertBefore(value, endMark);
262 |
263 | // Explicit undefined to store if frag.firstChild is null.
264 | return mark || value;
265 | }
266 |
267 | function step(node, direction, inner) {
268 | const key = node[GROUPING];
269 | if (key) {
270 | node = node[direction];
271 | while (node && node[GROUPING] !== key) {
272 | node = node[direction];
273 | }
274 | }
275 | return inner ? node : node[direction];
276 | }
277 |
278 | function removeNodes(parent, node, end) {
279 | let tmp;
280 | while (node !== end) {
281 | tmp = node.nextSibling;
282 | parent.removeChild(node);
283 | node = tmp;
284 | }
285 | }
286 |
287 | function insertNodes(parent, node, end, target) {
288 | let tmp;
289 | while (node !== end) {
290 | tmp = node.nextSibling;
291 | parent.insertBefore(node, target);
292 | node = tmp;
293 | }
294 | }
295 |
296 | // Picked from
297 | // https://github.com/adamhaile/surplus/blob/master/src/runtime/content.ts#L368
298 |
299 | // return an array of the indices of ns that comprise the longest increasing subsequence within ns
300 | function longestPositiveIncreasingSubsequence(ns, newStart) {
301 | let seq = [];
302 | let is = [];
303 | let l = -1;
304 | let pre = new Array(ns.length);
305 |
306 | for (var i = newStart, len = ns.length; i < len; i++) {
307 | var n = ns[i];
308 | if (n < 0) continue;
309 | var j = findGreatestIndexLEQ(seq, n);
310 | if (j !== -1) pre[i] = is[j];
311 | if (j === l) {
312 | l++;
313 | seq[l] = n;
314 | is[l] = i;
315 | } else if (n < seq[j + 1]) {
316 | seq[j + 1] = n;
317 | is[j + 1] = i;
318 | }
319 | }
320 |
321 | for (i = is[l]; l >= 0; i = pre[i], l--) {
322 | seq[l] = i;
323 | }
324 |
325 | return seq;
326 | }
327 |
328 | function findGreatestIndexLEQ(seq, n) {
329 | // invariant: lo is guaranteed to be index of a value <= n, hi to be >
330 | // therefore, they actually start out of range: (-1, last + 1)
331 | let lo = -1;
332 | let hi = seq.length;
333 |
334 | // fast path for simple increasing sequences
335 | if (hi > 0 && seq[hi - 1] <= n) return hi - 1;
336 |
337 | while (hi - lo > 1) {
338 | var mid = ((lo + hi) / 2) | 0;
339 | if (seq[mid] > n) {
340 | hi = mid;
341 | } else {
342 | lo = mid;
343 | }
344 | }
345 |
346 | return lo;
347 | }
348 |
--------------------------------------------------------------------------------
/src/observable.d.ts:
--------------------------------------------------------------------------------
1 | export interface Observable {
2 | (): T;
3 | (nextValue: T): T;
4 | }
5 | export function observable(value: T): Observable;
6 | export function o(value: T): Observable;
7 | export function computed unknown>(observer: T, seed?: unknown): T;
8 | export function S unknown>(observer: T, seed?: unknown): T;
9 |
10 | export function subscribe(observer: () => T): () => void;
11 | export function unsubscribe(observer: () => T): void;
12 | export function isListening(): boolean;
13 | export function root(fn: () => T): T;
14 | export function sample(fn: () => T): T;
15 | export function transaction(fn: () => T): T;
16 | export function on unknown>(observables: Observable[], fn: T, seed?: unknown, onchanges?: boolean): T;
17 | export function cleanup unknown>(fn: T): T;
18 |
--------------------------------------------------------------------------------
/src/observable.js:
--------------------------------------------------------------------------------
1 | const EMPTY_ARR = [];
2 | let tracking;
3 | let queue;
4 |
5 | /**
6 | * Returns true if there is an active observer.
7 | * @return {boolean}
8 | */
9 | export function isListening() {
10 | return !!tracking;
11 | }
12 |
13 | /**
14 | * Creates a root and executes the passed function that can contain computations.
15 | * The executed function receives an `unsubscribe` argument which can be called to
16 | * unsubscribe all inner computations.
17 | *
18 | * @param {Function} fn
19 | * @return {*}
20 | */
21 | export function root(fn) {
22 | const prevTracking = tracking;
23 | const rootUpdate = () => {};
24 | tracking = rootUpdate;
25 | resetUpdate(rootUpdate);
26 | const result = fn(() => {
27 | _unsubscribe(rootUpdate);
28 | tracking = undefined;
29 | });
30 | tracking = prevTracking;
31 | return result;
32 | }
33 |
34 | /**
35 | * Sample the current value of an observable but don't create a dependency on it.
36 | *
37 | * @example
38 | * computed(() => { if (foo()) bar(sample(bar) + 1); });
39 | *
40 | * @param {Function} fn
41 | * @return {*}
42 | */
43 | export function sample(fn) {
44 | const prevTracking = tracking;
45 | tracking = undefined;
46 | const value = fn();
47 | tracking = prevTracking;
48 | return value;
49 | }
50 |
51 | /**
52 | * Creates a transaction in which an observable can be set multiple times
53 | * but only trigger a computation once.
54 | * @param {Function} fn
55 | * @return {*}
56 | */
57 | export function transaction(fn) {
58 | let prevQueue = queue;
59 | queue = [];
60 | const result = fn();
61 | let q = queue;
62 | queue = prevQueue;
63 | q.forEach((data) => {
64 | if (data._pending !== EMPTY_ARR) {
65 | const pending = data._pending;
66 | data._pending = EMPTY_ARR;
67 | data(pending);
68 | }
69 | });
70 | return result;
71 | }
72 |
73 | /**
74 | * Creates a new observable, returns a function which can be used to get
75 | * the observable's value by calling the function without any arguments
76 | * and set the value by passing one argument of any type.
77 | *
78 | * @param {*} value - Initial value.
79 | * @return {Function}
80 | */
81 | function observable(value) {
82 | function data(nextValue) {
83 | if (arguments.length === 0) {
84 | if (tracking && !data._observers.has(tracking)) {
85 | data._observers.add(tracking);
86 | tracking._observables.push(data);
87 | }
88 | return value;
89 | }
90 |
91 | if (queue) {
92 | if (data._pending === EMPTY_ARR) {
93 | queue.push(data);
94 | }
95 | data._pending = nextValue;
96 | return nextValue;
97 | }
98 |
99 | value = nextValue;
100 |
101 | // Clear `tracking` otherwise a computed triggered by a set
102 | // in another computed is seen as a child of that other computed.
103 | const clearedUpdate = tracking;
104 | tracking = undefined;
105 |
106 | // Update can alter data._observers, make a copy before running.
107 | data._runObservers = new Set(data._observers);
108 | data._runObservers.forEach((observer) => (observer._fresh = false));
109 | data._runObservers.forEach((observer) => {
110 | if (!observer._fresh) observer();
111 | });
112 |
113 | tracking = clearedUpdate;
114 | return value;
115 | }
116 |
117 | // Tiny indicator that this is an observable function.
118 | // Used in sinuous/h/src/property.js
119 | data.$o = 1;
120 | data._observers = new Set();
121 | // The 'not set' value must be unique, so `nullish` can be set in a transaction.
122 | data._pending = EMPTY_ARR;
123 |
124 | return data;
125 | }
126 |
127 | /**
128 | * @namespace
129 | * @borrows observable as o
130 | */
131 | export { observable, observable as o };
132 |
133 | /**
134 | * Creates a new computation which runs when defined and automatically re-runs
135 | * when any of the used observable's values are set.
136 | *
137 | * @param {Function} observer
138 | * @param {*} value - Seed value.
139 | * @return {Function} Computation which can be used in other computations.
140 | */
141 | function computed(observer, value) {
142 | observer._update = update;
143 |
144 | // if (tracking == null) {
145 | // console.warn("computations created without a root or parent will never be disposed");
146 | // }
147 |
148 | resetUpdate(update);
149 | update();
150 |
151 | function update() {
152 | const prevTracking = tracking;
153 | if (tracking) {
154 | tracking._children.push(update);
155 | }
156 |
157 | _unsubscribe(update);
158 | update._fresh = true;
159 | tracking = update;
160 | value = observer(value);
161 |
162 | tracking = prevTracking;
163 | return value;
164 | }
165 |
166 | // Tiny indicator that this is an observable function.
167 | // Used in sinuous/h/src/property.js
168 | data.$o = 1;
169 |
170 | function data() {
171 | if (update._fresh) {
172 | if (tracking) {
173 | // If being read from inside another computed, pass observables to it
174 | update._observables.forEach((o) => o());
175 | }
176 | } else {
177 | value = update();
178 | }
179 | return value;
180 | }
181 |
182 | return data;
183 | }
184 |
185 | /**
186 | * @namespace
187 | * @borrows computed as S
188 | */
189 | export { computed, computed as S };
190 |
191 | /**
192 | * Run the given function just before the enclosing computation updates
193 | * or is disposed.
194 | * @param {Function} fn
195 | * @return {Function}
196 | */
197 | export function cleanup(fn) {
198 | if (tracking) {
199 | tracking._cleanups.push(fn);
200 | }
201 | return fn;
202 | }
203 |
204 | /**
205 | * Subscribe to updates of an observable.
206 | * @param {Function} observer
207 | * @return {Function}
208 | */
209 | export function subscribe(observer) {
210 | computed(observer);
211 | return () => _unsubscribe(observer._update);
212 | }
213 |
214 | /**
215 | * Statically declare a computation's dependencies.
216 | *
217 | * @param {Function|Array} obs
218 | * @param {Function} fn - Callback function.
219 | * @param {*} [seed] - Seed value.
220 | * @param {boolean} [onchanges] - If true the initial run is skipped.
221 | * @return {Function} Computation which can be used in other computations.
222 | */
223 | export function on(obs, fn, seed, onchanges) {
224 | obs = [].concat(obs);
225 | return computed((value) => {
226 | obs.forEach((o) => o());
227 |
228 | let result = value;
229 | if (!onchanges) {
230 | result = sample(() => fn(value));
231 | }
232 |
233 | onchanges = false;
234 | return result;
235 | }, seed);
236 | }
237 |
238 | /**
239 | * Unsubscribe from an observer.
240 | * @param {Function} observer
241 | */
242 | export function unsubscribe(observer) {
243 | _unsubscribe(observer._update);
244 | }
245 |
246 | function _unsubscribe(update) {
247 | update._children.forEach(_unsubscribe);
248 | update._observables.forEach((o) => {
249 | o._observers.delete(update);
250 | if (o._runObservers) {
251 | o._runObservers.delete(update);
252 | }
253 | });
254 | update._cleanups.forEach((c) => c());
255 | resetUpdate(update);
256 | }
257 |
258 | function resetUpdate(update) {
259 | // Keep track of which observables trigger updates. Needed for unsubscribe.
260 | update._observables = [];
261 | update._children = [];
262 | update._cleanups = [];
263 | }
264 |
--------------------------------------------------------------------------------
/src/observable.md:
--------------------------------------------------------------------------------
1 | # Sinuous Observable
2 |
3 | Sinuous Observable is a tiny reactive library. It shares the core features of [S.js](https://github.com/adamhaile/S) to be the engine driving the reactive dom operations in [Sinuous](https://github.com/luwes/sinuous).
4 |
5 | ## Features
6 |
7 | - Automatic updates: when an observable changes, any computation that read the old value will re-run.
8 | - Automatic disposals: any child computations are automatically disposed when the parent is re-run.
9 |
10 | # API
11 |
12 | ### Functions
13 |
14 | - [isListening()](#isListening) ⇒ boolean
15 | - [root(fn)](#root) ⇒ \*
16 | - [sample(fn)](#sample) ⇒ \*
17 | - [transaction(fn)](#transaction) ⇒ \*
18 | - [observable(value)](#observable) ⇒ function
19 | - [computed(observer, value)](#computed) ⇒ function
20 | - [cleanup(fn)](#cleanup) ⇒ function
21 | - [subscribe(observer)](#subscribe) ⇒ function
22 | - [on(obs, fn, [seed], [onchanges])](#on) ⇒ function
23 | - [unsubscribe(observer)](#unsubscribe)
24 |
25 |
26 |
27 | ### isListening() ⇒ boolean
28 |
29 | Returns true if there is an active observer.
30 |
31 | **Kind**: global function
32 |
33 | ---
34 |
35 |
36 |
37 | ### root(fn) ⇒ \*
38 |
39 | Creates a root and executes the passed function that can contain computations.
40 | The executed function receives an `unsubscribe` argument which can be called to
41 | unsubscribe all inner computations.
42 |
43 | **Kind**: global function
44 |
45 | | Param | Type |
46 | | ----- | --------------------- |
47 | | fn | function
|
48 |
49 | ---
50 |
51 |
52 |
53 | ### sample(fn) ⇒ \*
54 |
55 | Sample the current value of an observable but don't create a dependency on it.
56 |
57 | **Kind**: global function
58 |
59 | | Param | Type |
60 | | ----- | --------------------- |
61 | | fn | function
|
62 |
63 | **Example**
64 |
65 | ```js
66 | computed(() => {
67 | if (foo()) bar(sample(bar) + 1);
68 | });
69 | ```
70 |
71 | ---
72 |
73 |
74 |
75 | ### transaction(fn) ⇒ \*
76 |
77 | Creates a transaction in which an observable can be set multiple times
78 | but only trigger a computation once.
79 |
80 | **Kind**: global function
81 |
82 | | Param | Type |
83 | | ----- | --------------------- |
84 | | fn | function
|
85 |
86 | ---
87 |
88 |
89 |
90 | ### observable(value) ⇒ function
91 |
92 | Creates a new observable, returns a function which can be used to get
93 | the observable's value by calling the function without any arguments
94 | and set the value by passing one argument of any type.
95 |
96 | **Kind**: global function
97 |
98 | | Param | Type | Description |
99 | | ----- | --------------- | -------------- |
100 | | value | \*
| Initial value. |
101 |
102 | ---
103 |
104 |
105 |
106 | ### computed(observer, value) ⇒ function
107 |
108 | Creates a new computation which runs when defined and automatically re-runs
109 | when any of the used observable's values are set.
110 |
111 | **Kind**: global function
112 | **Returns**: function
- Computation which can be used in other computations.
113 |
114 | | Param | Type | Description |
115 | | -------- | --------------------- | ----------- |
116 | | observer | function
| |
117 | | value | \*
| Seed value. |
118 |
119 | ---
120 |
121 |
122 |
123 | ### cleanup(fn) ⇒ function
124 |
125 | Run the given function just before the enclosing computation updates
126 | or is disposed.
127 |
128 | **Kind**: global function
129 |
130 | | Param | Type |
131 | | ----- | --------------------- |
132 | | fn | function
|
133 |
134 | ---
135 |
136 |
137 |
138 | ### subscribe(observer) ⇒ function
139 |
140 | Subscribe to updates of an observable.
141 |
142 | **Kind**: global function
143 |
144 | | Param | Type |
145 | | -------- | --------------------- |
146 | | observer | function
|
147 |
148 | ---
149 |
150 |
151 |
152 | ### on(obs, fn, [seed], [onchanges]) ⇒ function
153 |
154 | Statically declare a computation's dependencies.
155 |
156 | **Kind**: global function
157 | **Returns**: function
- Computation which can be used in other computations.
158 |
159 | | Param | Type | Description |
160 | | ----------- | ------------------------------------------- | ----------------------------------- |
161 | | obs | function
\| Array
| |
162 | | fn | function
| Callback function. |
163 | | [seed] | \*
| Seed value. |
164 | | [onchanges] | boolean
| If true the initial run is skipped. |
165 |
166 | ---
167 |
168 |
169 |
170 | ### unsubscribe(observer)
171 |
172 | Unsubscribe from an observer.
173 |
174 | **Kind**: global function
175 |
176 | | Param | Type |
177 | | -------- | --------------------- |
178 | | observer | function
|
179 |
180 | ---
181 |
182 | # Example
183 |
184 | ```js
185 | import observable, { S } from 'sinuous/observable';
186 |
187 | var order = '',
188 | a = observable(0),
189 | b = S(function () {
190 | order += 'b';
191 | return a() + 1;
192 | }),
193 | c = S(function () {
194 | order += 'c';
195 | return b() || d();
196 | }),
197 | d = S(function () {
198 | order += 'd';
199 | return a() + 10;
200 | });
201 |
202 | console.log(order); // bcd
203 |
204 | order = '';
205 | a(-1);
206 |
207 | console.log(order); // bcd
208 | console.log(c()); // 9
209 |
210 | order = '';
211 | a(0);
212 |
213 | console.log(order); // bcd
214 | console.log(c()); // 1
215 | ```
216 |
--------------------------------------------------------------------------------
/src/shared.d.ts:
--------------------------------------------------------------------------------
1 | import { Observable } from './observable';
2 |
3 | export type ElementChild =
4 | | Node
5 | | Function
6 | | Observable
7 | | object
8 | | string
9 | | number
10 | | boolean
11 | | null
12 | | undefined;
13 | type ElementChildren = ElementChild[] | ElementChild;
14 |
15 | export interface FunctionComponent {
16 | (props: object, ...children: ElementChildren[]): Node
17 | (...children: ElementChildren[]): Node
18 | }
19 |
--------------------------------------------------------------------------------
/src/template.d.ts:
--------------------------------------------------------------------------------
1 | export function t(key: string): () => string;
2 | export function o(key: string): () => string;
3 |
4 | interface CloneFunction {
5 | (props: Record): Node;
6 | }
7 |
8 | export function template(elementRef: () => Node): CloneFunction;
9 |
10 | interface FillFunction {
11 | (props: Record): Node;
12 | }
13 |
14 | export function fill(elementRef: () => Node): FillFunction;
15 |
--------------------------------------------------------------------------------
/src/template.js:
--------------------------------------------------------------------------------
1 | import { api } from './index.js';
2 |
3 | let recordedActions;
4 |
5 | /**
6 | * Observed template tag.
7 | * @param {string} key
8 | * @return {Function}
9 | */
10 | export function o(key) {
11 | return t(key, true);
12 | }
13 |
14 | /**
15 | * Template tag.
16 | * @param {string} key
17 | * @param {boolean} [observed]
18 | * @param {boolean} [bind]
19 | * @return {Function}
20 | */
21 | export function t(key, observed, bind) {
22 | const tag = function () {
23 | // eslint-disable-next-line
24 | const { el, name, endMark } = this;
25 |
26 | const action = (element, endMark, propName, value) => {
27 | if (propName == null) {
28 | // Store state on the unique endMark per action.
29 | const state = endMark || element;
30 |
31 | // Performance optimization for when the tag is the only content child.
32 | // Default current value to empty string which makes a text insert faster.
33 | if (
34 | endMark &&
35 | endMark._current === undefined &&
36 | element.firstChild === element.lastChild &&
37 | element.firstChild === endMark
38 | ) {
39 | endMark._current = '';
40 | }
41 |
42 | state._current = api.insert(element, value, endMark, state._current);
43 | } else {
44 | api.property(element, value, propName);
45 | }
46 | };
47 |
48 | action._el = el;
49 | action._endMark = endMark;
50 | action._propName = name;
51 | action._key = key;
52 | action._observed = observed;
53 | action._bind = bind;
54 | recordedActions.push(action);
55 | };
56 |
57 | // Tiny indicator that this is a template tag.
58 | // Used in sinuous/h/src/property.js
59 | tag.$o = 2;
60 |
61 | return tag;
62 | }
63 |
64 | /**
65 | * Creates a template function.
66 | * @param {Function} elementRef
67 | * @param {boolean} noClone
68 | * @return {Function}
69 | */
70 | export function template(elementRef, noClone) {
71 | const prevRecordedActions = recordedActions;
72 | recordedActions = [];
73 |
74 | const tpl = elementRef();
75 |
76 | const cloneActions = recordedActions;
77 | recordedActions = prevRecordedActions;
78 |
79 | let fragment = tpl.content || (tpl.parentNode && tpl);
80 | if (!fragment) {
81 | fragment = document.createDocumentFragment();
82 | fragment.appendChild(tpl);
83 | }
84 |
85 | let stamp = fragment.cloneNode(true);
86 |
87 | if (!noClone) {
88 | cloneActions.forEach((action) => {
89 | action._paths = createPath(fragment, action._el);
90 | action._endMarkPath =
91 | action._endMark && createPath(action._el, action._endMark);
92 | });
93 | }
94 |
95 | function create(props, forceNoClone) {
96 | // Explicit check for a boolean here, this fn tends to be used in Array.map.
97 | if (forceNoClone === false || forceNoClone === true) noClone = forceNoClone;
98 |
99 | const keyedActions = {};
100 | let root;
101 | if (noClone) {
102 | if (fragment._childNodes) {
103 | fragment._childNodes.forEach((child) => fragment.appendChild(child));
104 | }
105 | root = fragment;
106 | } else {
107 | root = stamp.cloneNode(true);
108 | }
109 |
110 | // Set a custom property `props` for easy access to the passed argument.
111 | if (root.firstChild) {
112 | root.firstChild.props = props;
113 | }
114 |
115 | // These paths have to be resolved before any elements are inserted.
116 | cloneActions.forEach((action) => {
117 | action._target = noClone ? action._el : getPath(root, action._paths);
118 | action._endMarkTarget = noClone
119 | ? action._endMark
120 | : action._endMarkPath && getPath(action._target, action._endMarkPath);
121 | });
122 |
123 | cloneActions.forEach((action) => {
124 | api.action(action, props, keyedActions)(action._key, action._propName);
125 | });
126 |
127 | // Copy the childNodes after inserting the values. This is needed for
128 | // fills with primitive values that stay the same between renders.
129 | fragment._childNodes = Array.from(fragment.childNodes);
130 |
131 | return root;
132 | }
133 |
134 | // Tiny indicator that this is a template create function.
135 | create.$t = true;
136 |
137 | return create;
138 | }
139 |
140 | api.action = (action, props, keyedActions) => {
141 | const target = action._target;
142 |
143 | // In the `data` module `key` and `propName` are transformed for special cases.
144 | return (key, propName) => {
145 | let value = props[key];
146 | if (value != null) {
147 | action(target, action._endMarkTarget, propName, value);
148 | }
149 |
150 | if (action._observed) {
151 | if (!keyedActions[key]) {
152 | keyedActions[key] = [];
153 |
154 | Object.defineProperty(props, key, {
155 | get() {
156 | if (action._bind) {
157 | if (propName in target) {
158 | return target[propName];
159 | }
160 | return target;
161 | }
162 | return value;
163 | },
164 | set(newValue) {
165 | value = newValue;
166 | keyedActions[key].forEach((action) => action(newValue));
167 | },
168 | });
169 | }
170 | keyedActions[key].push(
171 | action.bind(null, target, action._endMarkTarget, propName)
172 | );
173 | }
174 | };
175 | };
176 |
177 | function createPath(root, el) {
178 | let paths = [];
179 | let parent;
180 | while ((parent = el.parentNode) !== root.parentNode) {
181 | paths.unshift(Array.from(parent.childNodes).indexOf(el));
182 | el = parent;
183 | }
184 | return paths;
185 | }
186 |
187 | function getPath(target, paths) {
188 | paths.forEach((depth) => (target = target.childNodes[depth]));
189 | return target;
190 | }
191 |
--------------------------------------------------------------------------------
/src/template.md:
--------------------------------------------------------------------------------
1 | # Sinuous Template
2 |
3 | A template can look something like this:
4 |
5 | ```js
6 | import { h } from 'sinuous';
7 | import { template, t, o } from 'sinuous/template';
8 |
9 | const Row = template(
10 | () => html`
11 |
12 |
13 | ${o('label')}
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | `
22 | );
23 | ```
24 |
25 | - `o` is an observable tag.
26 | It adds a proxy on the passed object property and repeats the recorded tag action when set.
27 | - `t` is a normal tag.
28 |
29 | The `Row` in this case would accept a object like so
30 |
31 | ```js
32 | Row({ id: 1, label: 'Banana', selected: 'peel' });
33 | ```
34 |
--------------------------------------------------------------------------------
/test/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | ---
2 | env:
3 | browser: true
4 |
5 | rules:
6 | fp/no-mutating-methods: off
7 | fp/no-loops: off
8 | fp/no-rest-parameters: off
9 |
--------------------------------------------------------------------------------
/test/_polyfills.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | /**
4 | * Protect window.console method calls, e.g. console is not defined on IE
5 | * unless dev tools are open, and IE doesn't define console.debug
6 | *
7 | * Chrome 41.0.2272.118: debug,error,info,log,warn,dir,dirxml,table,trace,assert,count,markTimeline,profile,profileEnd,time,timeEnd,timeStamp,timeline,timelineEnd,group,groupCollapsed,groupEnd,clear
8 | * Firefox 37.0.1: log,info,warn,error,exception,debug,table,trace,dir,group,groupCollapsed,groupEnd,time,timeEnd,profile,profileEnd,assert,count
9 | * Internet Explorer 11: select,log,info,warn,error,debug,assert,time,timeEnd,timeStamp,group,groupCollapsed,groupEnd,trace,clear,dir,dirxml,count,countReset,cd
10 | * Safari 6.2.4: debug,error,log,info,warn,clear,dir,dirxml,table,trace,assert,count,profile,profileEnd,time,timeEnd,timeStamp,group,groupCollapsed,groupEnd
11 | * Opera 28.0.1750.48: debug,error,info,log,warn,dir,dirxml,table,trace,assert,count,markTimeline,profile,profileEnd,time,timeEnd,timeStamp,timeline,timelineEnd,group,groupCollapsed,groupEnd,clear
12 | */
13 | (function() {
14 | // Union of Chrome, Firefox, IE, Opera, and Safari console methods
15 | var methods = [
16 | 'assert',
17 | 'cd',
18 | 'clear',
19 | 'count',
20 | 'countReset',
21 | 'debug',
22 | 'dir',
23 | 'dirxml',
24 | 'error',
25 | 'exception',
26 | 'group',
27 | 'groupCollapsed',
28 | 'groupEnd',
29 | 'info',
30 | 'log',
31 | 'markTimeline',
32 | 'profile',
33 | 'profileEnd',
34 | 'select',
35 | 'table',
36 | 'time',
37 | 'timeEnd',
38 | 'timeStamp',
39 | 'timeline',
40 | 'timelineEnd',
41 | 'trace',
42 | 'warn'
43 | ];
44 | var length = methods.length;
45 | var console = (window.console = window.console || {});
46 | var method;
47 | var noop = function() {};
48 | while (length--) {
49 | method = methods[length];
50 | // define undefined methods as noops to prevent errors
51 | if (!console[method]) console[method] = noop;
52 | }
53 | })();
54 |
--------------------------------------------------------------------------------
/test/_utils.js:
--------------------------------------------------------------------------------
1 | export function normalizeSvg(html) {
2 | // IE doesn't support `.outerHTML` of an SVG element.
3 | const htmlStr =
4 | typeof html === 'string'
5 | ? html
6 | : new XMLSerializer().serializeToString(html);
7 |
8 | // Normalization logic from Preact test helpers.
9 | return normalizeAttributes(
10 | htmlStr.replace(' xmlns="http://www.w3.org/2000/svg"', '')
11 | );
12 | }
13 |
14 | export function normalizeAttributes(htmlStr) {
15 | return htmlStr.replace(
16 | /<([a-z0-9-]+)((?:\s[a-z0-9:_.-]+=".*?")+)((?:\s*\/)?>)/gi,
17 | (s, pre, attrs, after) => {
18 | let list = attrs
19 | .match(/\s[a-z0-9:_.-]+=".*?"/gi)
20 | .sort((a, b) => (a > b ? 1 : -1));
21 | if (~after.indexOf('/')) after = '>' + pre + '>';
22 | return '<' + pre + list.join('') + after;
23 | }
24 | );
25 | }
26 |
27 | export function fragInnerHTML(fragment) {
28 | return [].slice
29 | .call(fragment.childNodes)
30 | .map(c => c.outerHTML)
31 | .join('');
32 | }
33 |
34 | export function beforeEach(test, handler) {
35 | return function tapish(name, listener) {
36 | test(name, function(assert) {
37 | var _end = assert.end;
38 | assert.end = function() {
39 | assert.end = _end;
40 | listener(assert);
41 | };
42 |
43 | handler(assert);
44 | });
45 | };
46 | }
47 |
48 | export function stripExpressionMarkers(value) {
49 | return value;
50 | }
51 |
--------------------------------------------------------------------------------
/test/h/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | ---
2 | env:
3 | browser: true
4 |
5 | rules:
6 | fp/no-mutating-methods: off
7 | fp/no-loops: off
8 |
--------------------------------------------------------------------------------
/test/h/add-node.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import { h } from 'sinuous';
3 | import { add as addNode } from '../../src/h.js';
4 |
5 | let counter = 0;
6 |
7 | test('addNode inserts fragment', function(t) {
8 | const parent = document.createElement('div');
9 | parent.appendChild(document.createTextNode('test'));
10 |
11 | const fragment = document.createDocumentFragment();
12 | fragment.appendChild(h('h1'));
13 | addNode(parent, fragment);
14 |
15 | t.equal(parent.innerHTML, 'test ');
16 | t.end();
17 | });
18 |
19 | test('addNode inserts fragment w/ marker', function(t) {
20 | const parent = document.createElement('div');
21 | parent.appendChild(document.createTextNode('test'));
22 |
23 | const marker = parent.appendChild(document.createElement('span'));
24 | const fragment = document.createDocumentFragment();
25 | fragment.appendChild(h('h1'));
26 | fragment.appendChild(h('h2'));
27 | addNode(parent, fragment, marker, ++counter);
28 |
29 | t.equal(parent.innerHTML, 'test ');
30 | t.end();
31 | });
32 |
33 | test('addNode inserts strings', function(t) {
34 | const parent = document.createElement('div');
35 | addNode(parent, '⛄');
36 | t.equal(parent.innerHTML, '⛄');
37 | t.end();
38 | });
39 |
40 | test('addNode inserts numbers', function(t) {
41 | const parent = document.createElement('div');
42 | addNode(parent, 99);
43 | t.equal(parent.innerHTML, '99');
44 | t.end();
45 | });
46 |
47 | test('addNode inserts nodes', function(t) {
48 | const parent = document.createElement('div');
49 | const node = document.createElement('div');
50 | t.equal(addNode(parent, node), node);
51 | t.end();
52 | });
53 |
--------------------------------------------------------------------------------
/test/h/hyperscript.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import spy from 'ispy';
3 | import { o, h, hs } from 'sinuous';
4 |
5 | test('simple', function(t) {
6 | t.equal(h('h1').outerHTML, ' ');
7 | t.equal(h('h1', 'hello world').outerHTML, 'hello world ');
8 | t.end();
9 | });
10 |
11 | test('nested', function(t) {
12 | t.equal(
13 | h('div', h('h1', 'Title'), h('p', 'Paragraph')).outerHTML,
14 | ''
15 | );
16 | t.end();
17 | });
18 |
19 | test('arrays for nesting is ok', function(t) {
20 | t.equal(
21 | h('div', [h('h1', 'Title'), h('p', 'Paragraph')]).outerHTML,
22 | ''
23 | );
24 | t.end();
25 | });
26 |
27 | test('can use namespace in name', function(t) {
28 | t.equal(h('myns:mytag').outerHTML, ' ');
29 | t.end();
30 | });
31 |
32 | // test('can use id selector', function(t) {
33 | // t.equal(h('div#frame').outerHTML, '
');
34 | // t.end();
35 | // });
36 |
37 | // test('can use class selector', function(t) {
38 | // t.equal(h('div.panel').outerHTML, '
');
39 | // t.end();
40 | // });
41 |
42 | // test('can default element types', function(t) {
43 | // t.equal(h('.panel').outerHTML, '
');
44 | // t.equal(h('#frame').outerHTML, '
');
45 | // t.end();
46 | // });
47 |
48 | test('can set properties', function(t) {
49 | let a = h('a', { href: 'http://google.com' });
50 | t.equal(a.href, 'http://google.com/');
51 | let checkbox = h('input', { name: 'yes', type: 'checkbox' });
52 | t.equal(checkbox.outerHTML, ' ');
53 | t.end();
54 | });
55 |
56 | test('(un)registers an event handler', function(t) {
57 | // don't try the focus event, valid tests fail in IE11
58 |
59 | let click = spy();
60 | let btn = h('button', { onclick: click }, 'something');
61 | document.body.appendChild(btn);
62 |
63 | btn.click();
64 | t.equal(click.callCount, 1, 'click called');
65 |
66 | h(btn, { onclick: false });
67 | btn.click();
68 | t.equal(click.callCount, 1, 'click still called only once');
69 |
70 | btn.parentNode.removeChild(btn);
71 | t.end();
72 | });
73 |
74 | test('(un)registers an observable event handler', function(t) {
75 | // don't try the focus event, valid tests fail in IE11
76 |
77 | let click = spy();
78 | let onclick = o(click);
79 | let btn = h('button', { onclick }, 'something');
80 | document.body.appendChild(btn);
81 |
82 | btn.click();
83 | t.equal(click.callCount, 1, 'click called');
84 |
85 | onclick(false);
86 | btn.click();
87 | t.equal(click.callCount, 1, 'click still called only once');
88 |
89 | btn.parentNode.removeChild(btn);
90 | t.end();
91 | });
92 |
93 | // test('registers event handlers', function(t) {
94 | // let click = spy();
95 | // let btn = h('button', { events: { click: () => click } }, 'something');
96 | // document.body.appendChild(btn);
97 |
98 | // btn.click();
99 | // t.equal(click.callCount, 1, 'click called');
100 |
101 | // btn.parentNode.removeChild(btn);
102 | // t.end();
103 | // });
104 |
105 | // test('can use bindings', function(t) {
106 | // h.bindings.innerHTML = (el, value) => (el.innerHTML = value);
107 |
108 | // let el = h('div', { $innerHTML: 'look ma, no node value ' });
109 | // t.equal(el.outerHTML, 'look ma, no node value
');
110 | // t.end();
111 | // });
112 |
113 | test('sets styles', function(t) {
114 | let div = h('div', { style: { color: 'red' } });
115 | t.equal(div.style.color, 'red');
116 | t.end();
117 | });
118 |
119 | test('sets styles as text', function(t) {
120 | let div = h('div', { style: 'color: red' });
121 | t.equal(div.style.color, 'red');
122 | t.end();
123 | });
124 |
125 | // test('sets classes', function(t) {
126 | // let div = h('div', { classList: { play: true, pause: true } });
127 | // t.assert(div.classList.contains('play'));
128 | // t.assert(div.classList.contains('pause'));
129 | // t.end();
130 | // });
131 |
132 | test('sets attributes', function(t) {
133 | let div = h('div', { attrs: { checked: 'checked' } });
134 | t.assert(div.hasAttribute('checked'));
135 | t.end();
136 | });
137 |
138 | test('sets data attributes', function(t) {
139 | let div = h('div', { 'data-value': 5 });
140 | t.equal(div.getAttribute('data-value'), '5'); // failing for IE9
141 | t.end();
142 | });
143 |
144 | test('sets aria attributes', function(t) {
145 | let div = h('div', { 'aria-hidden': true });
146 | t.equal(div.getAttribute('aria-hidden'), 'true');
147 | t.end();
148 | });
149 |
150 | // test('sets refs', function(t) {
151 | // let ref;
152 | // let div = h('div', { ref: el => (ref = el) });
153 | // t.equal(div, ref);
154 | // t.end();
155 | // });
156 |
157 | test("boolean, number, get to-string'ed", function(t) {
158 | let e = h('p', true, false, 4);
159 | t.assert(e.outerHTML.match(/truefalse4<\/p>/));
160 | t.end();
161 | });
162 |
163 | // test('unicode selectors', function(t) {
164 | // t.equal(h('.⛄').outerHTML, '
');
165 | // t.equal(h('span#⛄').outerHTML, ' ');
166 | // t.end();
167 | // });
168 |
169 | test('can use fragments', function(t) {
170 | const insertCat = () => 'cat';
171 | let frag = h([h('div', 'First'), insertCat, h('div', 'Last')]);
172 |
173 | const div = document.createElement('div');
174 | div.appendChild(frag);
175 | t.equal(div.innerHTML, 'First
catLast
');
176 | t.end();
177 | });
178 |
179 | test('can use components', function(t) {
180 | const insertCat = ({ id, drink }) => h('div', { id, textContent: drink });
181 |
182 | let frag = h([
183 | h('div', 'First'),
184 | h(insertCat, { id: 'cat', drink: 'milk' }),
185 | h('div', 'Last')
186 | ]);
187 |
188 | const div = document.createElement('div');
189 | div.appendChild(frag);
190 | t.equal(
191 | div.innerHTML,
192 | 'First
milk
Last
'
193 | );
194 | t.end();
195 | });
196 |
--------------------------------------------------------------------------------
/test/h/insert-bugs.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import { o, h, html } from 'sinuous';
3 | import { insert } from '../../src/h.js';
4 |
5 | test('empty fragment clear bug', t => {
6 | let scratch = h('div');
7 | h(document.body, scratch);
8 |
9 | const value = o(99);
10 | const props = { val: value };
11 | const comp = ({ val }) => html`
12 | Hello world
13 | Bye bye ${val}
14 | `;
15 |
16 | const comp2 = ({ val }) => html`
17 | Bye world
18 | Hello hello ${val}
19 | `;
20 |
21 | let active = o(comp);
22 | const res = html`
23 | Dynamic Components
24 |
25 | ${() => {
26 | const c = active();
27 | return c(props);
28 | }}
29 | `;
30 | scratch.appendChild(res);
31 |
32 | const emptyFrag = () => document.createDocumentFragment();
33 |
34 | t.equal(scratch.innerHTML, `Dynamic Components Hello world Bye bye 99
`);
35 |
36 | active(comp2);
37 | t.equal(scratch.innerHTML, `Dynamic Components Bye world Hello hello 99
`);
38 |
39 | active(emptyFrag);
40 | t.equal(scratch.innerHTML, `Dynamic Components `);
41 |
42 | active(emptyFrag);
43 | t.equal(scratch.innerHTML, `Dynamic Components `);
44 |
45 | t.end();
46 | });
47 |
48 | test('insert 9', t => {
49 | let scratch = h('div');
50 | h(document.body, scratch);
51 |
52 | let active = o(1);
53 |
54 | const Comp = title => html`
55 |
56 | 9
57 | ${() => {
58 | active();
59 | return html`
60 |
61 | 9
62 | ${() => html`
63 |
${title}
64 | `}
65 |
66 | `;
67 | }}
68 |
`;
69 |
70 | const el = Comp('Yo');
71 | insert(scratch, el);
72 |
73 | active(2);
74 | active(3);
75 |
76 | t.equal(scratch.innerHTML, '');
77 |
78 | t.end();
79 | });
80 |
--------------------------------------------------------------------------------
/test/h/insert-markers.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import { h } from 'sinuous';
3 | import { insert } from '../../src/h.js';
4 |
5 | // insert with Markers
6 | // beforeafter
7 |
8 | function insertValue(val) {
9 | const parent = clone(container);
10 | insert(parent, val, parent.childNodes[1]);
11 | return parent;
12 | }
13 |
14 | // IE doesn't clone empty text nodes
15 | function clone(el) {
16 | const cloned = el.cloneNode(true);
17 | cloned.textContent = '';
18 | [].slice.call(el.childNodes).forEach(n => cloned.appendChild(n.cloneNode()));
19 | return cloned;
20 | }
21 |
22 | const container = document.createElement('div');
23 | container.appendChild(document.createTextNode('before'));
24 | container.appendChild(document.createTextNode(''));
25 | container.appendChild(document.createTextNode('after'));
26 |
27 | test('inserts nothing for null', t => {
28 | const res = insertValue(null);
29 | t.equal(res.innerHTML, 'beforeafter');
30 | t.equal(res.childNodes.length, 3);
31 | t.end();
32 | });
33 |
34 | test('inserts nothing for undefined', t => {
35 | const res = insertValue(undefined);
36 | t.equal(res.innerHTML, 'beforeafter');
37 | t.equal(res.childNodes.length, 3);
38 | t.end();
39 | });
40 |
41 | test('inserts nothing for false', t => {
42 | const res = insertValue(false);
43 | t.equal(res.innerHTML, 'beforeafter');
44 | t.equal(res.childNodes.length, 3);
45 | t.end();
46 | });
47 |
48 | test('inserts nothing for true', t => {
49 | const res = insertValue(true);
50 | t.equal(res.innerHTML, 'beforeafter');
51 | t.equal(res.childNodes.length, 3);
52 | t.end();
53 | });
54 |
55 | test('inserts nothing for null in array', t => {
56 | const res = insertValue(['a', null, 'b']);
57 | t.equal(res.innerHTML, 'beforeabafter');
58 | t.equal(res.childNodes.length, 6);
59 | t.end();
60 | });
61 |
62 | test('inserts nothing for undefined in array', t => {
63 | const res = insertValue(['a', undefined, 'b']);
64 | t.equal(res.innerHTML, 'beforeabafter');
65 | t.equal(res.childNodes.length, 6);
66 | t.end();
67 | });
68 |
69 | test('can insert strings', t => {
70 | let res = insertValue('foo');
71 | t.equal(res.innerHTML, 'beforefooafter');
72 | t.equal(res.childNodes.length, 4);
73 |
74 | res = insertValue('');
75 | t.equal(res.innerHTML, 'beforeafter');
76 | t.end();
77 | });
78 |
79 | test('can insert a node', t => {
80 | const node = document.createElement('span');
81 | node.textContent = 'foo';
82 | t.equal(insertValue(node).innerHTML, 'beforefoo after');
83 | t.end();
84 | });
85 |
86 | test('can re-insert a node, thereby moving it', t => {
87 | var node = document.createElement('span');
88 | node.textContent = 'foo';
89 |
90 | const first = insertValue(node),
91 | second = insertValue(node);
92 |
93 | t.equal(first.innerHTML, 'beforeafter');
94 | t.equal(second.innerHTML, 'beforefoo after');
95 | t.end();
96 | });
97 |
98 | test('can insert an array of strings', t => {
99 | t.equal(
100 | insertValue(['foo', 'bar']).innerHTML,
101 | 'beforefoobarafter',
102 | 'array of strings'
103 | );
104 | t.end();
105 | });
106 |
107 | test('can insert an array of nodes', t => {
108 | const nodes = [document.createElement('span'), document.createElement('div')];
109 | nodes[0].textContent = 'foo';
110 | nodes[1].textContent = 'bar';
111 | t.equal(
112 | insertValue(nodes).innerHTML,
113 | 'beforefoo bar
after'
114 | );
115 | t.end();
116 | });
117 |
118 | test('can insert a changing array of nodes', t => {
119 | let container = document.createElement('div'),
120 | marker = container.appendChild(document.createTextNode('')),
121 | span1 = document.createElement('span'),
122 | div2 = document.createElement('div'),
123 | span3 = document.createElement('span'),
124 | current;
125 | span1.textContent = '1';
126 | div2.textContent = '2';
127 | span3.textContent = '3';
128 |
129 | current = insert(container, [], marker, current);
130 | t.equal(container.innerHTML, '');
131 |
132 | current = insert(container, [span1, div2, span3], marker, current);
133 | t.equal(container.innerHTML, '1 2
3 ');
134 |
135 | current = insert(container, [div2, span3], marker, current);
136 | t.equal(container.innerHTML, '2
3 ');
137 |
138 | current = insert(container, [div2, span3], marker, current);
139 | t.equal(container.innerHTML, '2
3 ');
140 |
141 | current = insert(container, [span3, div2], marker, current);
142 | t.equal(container.innerHTML, '3 2
');
143 |
144 | current = insert(container, [], marker, current);
145 | t.equal(container.innerHTML, '');
146 |
147 | current = insert(container, [span3], marker, current);
148 | t.equal(container.innerHTML, '3 ');
149 |
150 | current = insert(container, [div2], marker, current);
151 | t.equal(container.innerHTML, '2
');
152 | t.end();
153 | });
154 |
155 | test('can insert nested arrays', t => {
156 | t.equal(
157 | insertValue(['foo', ['bar', 'blech']]).innerHTML,
158 | 'beforefoobarblechafter',
159 | 'array of array of strings'
160 | );
161 | t.end();
162 | });
163 |
--------------------------------------------------------------------------------
/test/h/insert.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import { o, h, html } from 'sinuous';
3 | import { insert } from '../../src/h.js';
4 |
5 | const insertValue = val => {
6 | const parent = container.cloneNode(true);
7 | insert(parent, val);
8 | return parent;
9 | };
10 |
11 | // insert
12 | // beforeafter
13 | const container = document.createElement('div');
14 |
15 | test('inserts observable into simple text', t => {
16 | let scratch = h('div');
17 | h(document.body, scratch);
18 |
19 | const counter = o(0);
20 | scratch.appendChild(html`
21 | Here's a list of items: Count: ${counter}
22 | `);
23 | t.equal(scratch.innerHTML, `Here's a list of items: Count: 0`);
24 |
25 | counter(counter() + 1);
26 | t.equal(scratch.innerHTML, `Here's a list of items: Count: 1`);
27 |
28 | t.end();
29 | });
30 |
31 | test('inserts fragments', t => {
32 | const frag = o(html`
33 | Hello world
34 | Bye bye
35 | `);
36 | const res = html`
37 | ${frag}
38 | `;
39 | t.equal(res.innerHTML, 'Hello world Bye bye
');
40 | t.equal(res.children.length, 2);
41 |
42 | frag(
43 | html`
44 | Cool
45 | Beans
46 | `
47 | );
48 | t.equal(res.innerHTML, 'Cool Beans
');
49 | t.equal(res.children.length, 2);
50 |
51 | frag('make it a string');
52 | t.equal(res.innerHTML, 'make it a string');
53 | t.equal(res.childNodes.length, 4);
54 |
55 | frag(
56 | html`
57 | Cool
58 | Beans
59 | `
60 | );
61 | t.equal(res.innerHTML, 'Cool Beans
');
62 | t.equal(res.children.length, 2);
63 |
64 | t.end();
65 | });
66 |
67 | test('inserts long fragments', t => {
68 | const frag = o(html`
69 | Hello world
70 | Bye bye
71 | Hello again
72 | `);
73 | const res = html`
74 | ${frag}
75 | `;
76 | t.equal(
77 | res.innerHTML,
78 | 'Hello world Bye bye
Hello again
'
79 | );
80 | t.equal(res.children.length, 3);
81 |
82 | frag(html`
83 | Hello again
84 | Bye bye
85 | Hello world
86 | `);
87 | t.equal(
88 | res.innerHTML,
89 | 'Hello again
Bye bye
Hello world '
90 | );
91 | t.equal(res.children.length, 3);
92 |
93 | t.end();
94 | });
95 |
96 | test('inserts nothing for null', t => {
97 | const res = insertValue(null);
98 | t.equal(res.innerHTML, '');
99 | t.equal(res.childNodes.length, 0);
100 | t.end();
101 | });
102 |
103 | test('inserts nothing for undefined', t => {
104 | const res = insertValue(undefined);
105 | t.equal(res.innerHTML, '');
106 | t.equal(res.childNodes.length, 0);
107 | t.end();
108 | });
109 |
110 | test('inserts nothing for false', t => {
111 | const res = insertValue(false);
112 | t.equal(res.innerHTML, '');
113 | t.equal(res.childNodes.length, 0);
114 | t.end();
115 | });
116 |
117 | test('inserts nothing for true', t => {
118 | const res = insertValue(true);
119 | t.equal(res.innerHTML, '');
120 | t.equal(res.childNodes.length, 0);
121 | t.end();
122 | });
123 |
124 | test('inserts nothing for null in array', t => {
125 | const res = insertValue(['a', null, 'b']);
126 | t.equal(res.innerHTML, 'ab');
127 | t.equal(res.childNodes.length, 3);
128 | t.end();
129 | });
130 |
131 | test('inserts nothing for undefined in array', t => {
132 | const res = insertValue(['a', undefined, 'b']);
133 | t.equal(res.innerHTML, 'ab');
134 | t.equal(res.childNodes.length, 3);
135 | t.end();
136 | });
137 |
138 | test('can insert stringable', t => {
139 | let res = insertValue('foo');
140 | t.equal(res.innerHTML, 'foo');
141 | t.equal(res.childNodes.length, 1);
142 |
143 | res = insertValue(11206);
144 | t.equal(res.innerHTML, '11206');
145 | t.equal(res.childNodes.length, 1);
146 | t.end();
147 | });
148 |
149 | test('can insert a node', t => {
150 | const node = document.createElement('span');
151 | node.textContent = 'foo';
152 | t.equal(insertValue(node).innerHTML, 'foo ');
153 | t.end();
154 | });
155 |
156 | test('can re-insert a node, thereby moving it', t => {
157 | const node = document.createElement('span');
158 | node.textContent = 'foo';
159 |
160 | const first = insertValue(node),
161 | second = insertValue(node);
162 |
163 | t.equal(first.innerHTML, '');
164 | t.equal(second.innerHTML, 'foo ');
165 | t.end();
166 | });
167 |
168 | test('can insert an array of strings', t => {
169 | t.equal(insertValue(['foo', 'bar']).innerHTML, 'foobar', 'array of strings');
170 | t.end();
171 | });
172 |
173 | test('can insert an array of nodes', t => {
174 | const nodes = [document.createElement('span'), document.createElement('div')];
175 | nodes[0].textContent = 'foo';
176 | nodes[1].textContent = 'bar';
177 | t.equal(insertValue(nodes).innerHTML, 'foo bar
');
178 | t.end();
179 | });
180 |
181 | test('can insert a changing array of nodes 1', t => {
182 | var parent = document.createElement('div'),
183 | current = '',
184 | n1 = document.createElement('span'),
185 | n2 = document.createElement('div'),
186 | n3 = document.createElement('span'),
187 | n4 = document.createElement('div'),
188 | orig = [n1, n2, n3, n4];
189 |
190 | n1.textContent = '1';
191 | n2.textContent = '2';
192 | n3.textContent = '3';
193 | n4.textContent = '4';
194 |
195 | var origExpected = expected(orig);
196 |
197 | // identity
198 | test([n1, n2, n3, n4]);
199 |
200 | // 1 missing
201 | test([n2, n3, n4]);
202 | test([n1, n3, n4]);
203 | test([n1, n2, n4]);
204 | test([n1, n2, n3]);
205 |
206 | // 2 missing
207 | test([n3, n4]);
208 | test([n2, n4]);
209 | test([n2, n3]);
210 | test([n1, n4]);
211 | test([n1, n3]);
212 | test([n1, n2]);
213 |
214 | // 3 missing
215 | test([n1]);
216 | test([n2]);
217 | test([n3]);
218 | test([n4]);
219 |
220 | // all missing
221 | test([]);
222 |
223 | // swaps
224 | test([n2, n1, n3, n4]);
225 | test([n3, n2, n1, n4]);
226 | test([n4, n2, n3, n1]);
227 |
228 | // rotations
229 | test([n2, n3, n4, n1]);
230 | test([n3, n4, n1, n2]);
231 | test([n4, n1, n2, n3]);
232 |
233 | // reversal
234 | test([n4, n3, n2, n1]);
235 |
236 | function test(array) {
237 | current = insert(parent, array, undefined, current);
238 | t.equal(parent.innerHTML, expected(array));
239 | current = insert(parent, orig, undefined, current);
240 | t.equal(parent.innerHTML, origExpected);
241 | }
242 |
243 | function expected(array) {
244 | return array.map(n => n.outerHTML).join('');
245 | }
246 |
247 | t.end();
248 | });
249 |
250 | test('can insert nested arrays', t => {
251 | let current = insertValue(['foo', ['bar', 'blech']]);
252 | t.equal(current.innerHTML, 'foobarblech', 'array of array of strings');
253 | t.end();
254 | });
255 |
256 | test('can update text with node', t => {
257 | const parent = container.cloneNode(true);
258 |
259 | let current = insert(parent, '🧬');
260 | t.equal(parent.innerHTML, '🧬', 'text dna');
261 |
262 | insert(parent, h('h1', '⛄️'), undefined, current);
263 | t.equal(parent.innerHTML, '⛄️ ');
264 | t.end();
265 | });
266 |
267 | test('can update content with text with marker', t => {
268 | const parent = container.cloneNode(true);
269 | const marker = parent.appendChild(document.createTextNode(''));
270 |
271 | let current = insert(parent, h('h1', '⛄️'), marker);
272 | t.equal(parent.innerHTML, '⛄️ ');
273 |
274 | insert(parent, '⛄️', marker, current);
275 | t.equal(parent.innerHTML, '⛄️');
276 | t.end();
277 | });
278 |
279 | test('can update content with text and observable with marker', t => {
280 | const parent = container.cloneNode(true);
281 | const marker = parent.appendChild(document.createTextNode(''));
282 |
283 | const reactive = o('reactive');
284 | const dynamic = o(99);
285 |
286 | insert(parent, h('h1', reactive, '⛄️', dynamic), marker);
287 | t.equal(parent.innerHTML, 'reactive⛄️99 ');
288 |
289 | dynamic(77);
290 | t.equal(parent.innerHTML, 'reactive⛄️77 ');
291 |
292 | reactive(1);
293 | t.equal(parent.innerHTML, '1⛄️77 ');
294 |
295 | dynamic('');
296 | t.equal(parent.innerHTML, '1⛄️ ');
297 |
298 | reactive('');
299 | t.equal(parent.innerHTML, '⛄️ ');
300 |
301 | insert(parent, '⛄️', marker, parent.children[0]);
302 | t.equal(parent.innerHTML, '⛄️');
303 | t.end();
304 | });
305 |
--------------------------------------------------------------------------------
/test/h/svg.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import { hs, svg } from 'sinuous';
3 | import { normalizeSvg } from '../../test/_utils.js';
4 |
5 | test('normalizeSvg', function(t) {
6 | // IE11 adds xmlns and has a self closing tags.
7 | t.equal(
8 | normalizeSvg(
9 | ' '
10 | ),
11 | ' '
12 | );
13 | t.end();
14 | });
15 |
16 | test('supports SVG', function(t) {
17 | const svg = hs(
18 | 'svg',
19 | { class: 'redbox', viewBox: '0 0 100 100' },
20 | hs('path', { d: 'M 8.74211 7.70899' })
21 | );
22 |
23 | t.equal(
24 | normalizeSvg(svg),
25 | ' '
26 | );
27 | t.end();
28 | });
29 |
30 | test('can add an array of svg elements', function(t) {
31 | const circles = [1, 2, 3];
32 | t.equal(
33 | normalizeSvg(
34 | svg`
35 | ${() => circles.map(c => svg` `)}
36 | `
37 | ),
38 | ' '
39 | );
40 | t.end();
41 | });
42 |
--------------------------------------------------------------------------------
/test/h/utils.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import { removeNodes } from '../../src/h.js';
3 |
4 | test('removeNodes', function(t) {
5 | const parent = document.createElement('div');
6 | let first = parent.appendChild(document.createComment(''));
7 | parent.appendChild(document.createElement('span'));
8 | let endMark = parent.appendChild(document.createTextNode(''));
9 |
10 | removeNodes(parent, first, endMark);
11 |
12 | t.equal(parent.innerHTML, '');
13 | t.equal(parent.childNodes.length, 1);
14 |
15 | t.end();
16 | });
17 |
--------------------------------------------------------------------------------
/test/htm/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 |
14 | import test from 'tape';
15 | import htm from '../../src/htm.js';
16 |
17 | const h = (tag, props, ...children) => ({ tag, props, children });
18 | const html = htm.bind(h);
19 |
20 | test('empty', t => {
21 | t.deepEqual(html``, undefined);
22 | t.end();
23 | });
24 |
25 | test('single named elements', t => {
26 | t.deepEqual(
27 | html`
28 |
29 | `,
30 | { tag: 'div', props: null, children: [] }
31 | );
32 | t.deepEqual(
33 | html`
34 |
35 | `,
36 | { tag: 'div', props: null, children: [] }
37 | );
38 | t.deepEqual(
39 | html`
40 |
41 | `,
42 | { tag: 'span', props: null, children: [] }
43 | );
44 | t.end();
45 | });
46 |
47 | test('multiple root elements', t => {
48 | t.deepEqual(
49 | html`
50 | />
51 | `,
52 | h([
53 | { tag: 'a', props: null, children: [] },
54 | { tag: 'b', props: null, children: [] },
55 | { tag: 'c', props: null, children: [] }
56 | ])
57 | );
58 | t.end();
59 | });
60 |
61 | test('single dynamic tag name', t => {
62 | t.deepEqual(
63 | html`
64 | <${'foo'} />
65 | `,
66 | { tag: 'foo', props: null, children: [] }
67 | );
68 | function Foo() {}
69 | t.deepEqual(
70 | html`
71 | <${Foo} />
72 | `,
73 | { tag: Foo, props: null, children: [] }
74 | );
75 | t.end();
76 | });
77 |
78 | test('single boolean prop', t => {
79 | t.deepEqual(
80 | html`
81 |
82 | `,
83 | { tag: 'a', props: { disabled: true }, children: [] }
84 | );
85 | t.end();
86 | });
87 |
88 | test('two boolean props', t => {
89 | t.deepEqual(
90 | html`
91 |
92 | `,
93 | { tag: 'a', props: { invisible: true, disabled: true }, children: [] }
94 | );
95 | t.end();
96 | });
97 |
98 | test('single prop with empty value', t => {
99 | t.deepEqual(
100 | html`
101 |
102 | `,
103 | { tag: 'a', props: { href: '' }, children: [] }
104 | );
105 | t.end();
106 | });
107 |
108 | test('two props with empty values', t => {
109 | t.deepEqual(
110 | html`
111 |
112 | `,
113 | { tag: 'a', props: { href: '', foo: '' }, children: [] }
114 | );
115 | t.end();
116 | });
117 |
118 | test('single prop with empty name', t => {
119 | t.deepEqual(
120 | html`
121 |
122 | `,
123 | { tag: 'a', props: { '': 'foo' }, children: [] }
124 | );
125 | t.end();
126 | });
127 |
128 | test('single prop with static value', t => {
129 | t.deepEqual(
130 | html`
131 |
132 | `,
133 | { tag: 'a', props: { href: '/hello' }, children: [] }
134 | );
135 | t.end();
136 | });
137 |
138 | test('single prop with static value followed by a single boolean prop', t => {
139 | t.deepEqual(
140 | html`
141 |
142 | `,
143 | { tag: 'a', props: { href: '/hello', b: true }, children: [] }
144 | );
145 | t.end();
146 | });
147 |
148 | test('two props with static values', t => {
149 | t.deepEqual(
150 | html`
151 |
152 | `,
153 | { tag: 'a', props: { href: '/hello', target: '_blank' }, children: [] }
154 | );
155 | t.end();
156 | });
157 |
158 | test('single prop with dynamic value', t => {
159 | t.deepEqual(
160 | html`
161 |
162 | `,
163 | { tag: 'a', props: { href: 'foo' }, children: [] }
164 | );
165 | t.end();
166 | });
167 |
168 | test('slash in the middle of tag name or property name self-closes the element', t => {
169 | t.deepEqual(
170 | html`
171 |
172 | `,
173 | { tag: 'ab', props: null, children: [] }
174 | );
175 | t.deepEqual(
176 | html`
177 |
178 | `,
179 | { tag: 'abba', props: { pr: true }, children: [] }
180 | );
181 | t.end();
182 | });
183 |
184 | test('slash in a property value does not self-closes the element, unless followed by >', t => {
185 | t.deepEqual(html`/>`, {
186 | tag: 'abba',
187 | props: { prop: 'val/ue' },
188 | children: []
189 | });
190 | t.deepEqual(
191 | html`
192 |
193 | `,
194 | { tag: 'abba', props: { prop: 'value' }, children: [] }
195 | );
196 | t.deepEqual(html`/>`, {
197 | tag: 'abba',
198 | props: { prop: 'value/' },
199 | children: []
200 | });
201 | t.end();
202 | });
203 |
204 | test('two props with dynamic values', t => {
205 | function onClick() {}
206 | t.deepEqual(
207 | html`
208 |
209 | `,
210 | { tag: 'a', props: { href: 'foo', onClick }, children: [] }
211 | );
212 | t.end();
213 | });
214 |
215 | test('prop with multiple static and dynamic values get concatenated as strings', t => {
216 | t.deepEqual(
217 | html`
218 |
219 | `,
220 | { tag: 'a', props: { href: 'beforefooafter' }, children: [] }
221 | );
222 | t.deepEqual(
223 | html`
224 |
225 | `,
226 | { tag: 'a', props: { href: '11' }, children: [] }
227 | );
228 | t.deepEqual(
229 | html`
230 |
231 | `,
232 | { tag: 'a', props: { href: '1between1' }, children: [] }
233 | );
234 | t.deepEqual(
235 | html`
236 |
237 | `,
238 | { tag: 'a', props: { href: '/before/foo/after' }, children: [] }
239 | );
240 | t.deepEqual(
241 | html`
242 |
243 | `,
244 | { tag: 'a', props: { href: '/before/foo' }, children: [] }
245 | );
246 | t.end();
247 | });
248 |
249 | test('prop with multiple static and observables', t => {
250 | const observableMock = () => 'foo';
251 |
252 | t.equal(
253 | html`
254 |
255 | `.props.href(),
256 | 'beforefooafter'
257 | );
258 | t.equal(
259 | html`
260 |
261 | `.props.href(),
262 | 'fooafter'
263 | );
264 | t.equal(
265 | html`
266 |
267 | `.props.href(),
268 | 'foo1'
269 | );
270 | t.equal(
271 | html`
272 |
273 | `.props.href(),
274 | '1betweenfoo'
275 | );
276 | t.equal(
277 | html`
278 | 'foo'}/after />
279 | `.props.href(),
280 | '/before/foo/after'
281 | );
282 | t.equal(
283 | html`
284 |
285 | `.props.href(),
286 | '/before/foo'
287 | );
288 | t.end();
289 | });
290 |
291 | test('spread props', t => {
292 | t.deepEqual(
293 | html`
294 |
295 | `,
296 | { tag: 'a', props: { foo: 'bar' }, children: [] }
297 | );
298 | t.deepEqual(
299 | html`
300 |
301 | `,
302 | { tag: 'a', props: { b: true, foo: 'bar' }, children: [] }
303 | );
304 | t.deepEqual(
305 | html`
306 |
307 | `,
308 | { tag: 'a', props: { b: true, c: true, foo: 'bar' }, children: [] }
309 | );
310 | t.deepEqual(
311 | html`
312 |
313 | `,
314 | { tag: 'a', props: { b: true, foo: 'bar' }, children: [] }
315 | );
316 | t.deepEqual(
317 | html`
318 |
319 | `,
320 | { tag: 'a', props: { b: '1', foo: 'bar' }, children: [] }
321 | );
322 | t.deepEqual(
323 | html`
324 |
325 | `,
326 | h('a', { x: '1' }, h('b', { y: '2', c: 'bar' }))
327 | );
328 | t.deepEqual(
329 | html`
330 | d: ${4}
331 | `,
332 | h('a', { b: 2, c: 3 }, 'd: ', 4)
333 | );
334 | t.deepEqual(
335 | html`
336 |
337 | `,
338 | h('a', { c: 'bar' }, h('b', { d: 'baz' }))
339 | );
340 | t.end();
341 | });
342 |
343 | test('multiple spread props in one element', t => {
344 | t.deepEqual(
345 | html`
346 |
347 | `,
348 | { tag: 'a', props: { foo: 'bar', quux: 'baz' }, children: [] }
349 | );
350 | t.end();
351 | });
352 |
353 | test('mixed spread + static props', t => {
354 | t.deepEqual(
355 | html`
356 |
357 | `,
358 | { tag: 'a', props: { b: true, foo: 'bar' }, children: [] }
359 | );
360 | t.deepEqual(
361 | html`
362 |
363 | `,
364 | { tag: 'a', props: { b: true, c: true, foo: 'bar' }, children: [] }
365 | );
366 | t.deepEqual(
367 | html`
368 |
369 | `,
370 | { tag: 'a', props: { b: true, foo: 'bar' }, children: [] }
371 | );
372 | t.deepEqual(
373 | html`
374 |
375 | `,
376 | { tag: 'a', props: { b: true, c: true, foo: 'bar' }, children: [] }
377 | );
378 | t.end();
379 | });
380 |
381 | test('closing tag', t => {
382 | t.deepEqual(
383 | html`
384 |
385 | `,
386 | { tag: 'a', props: null, children: [] }
387 | );
388 | t.deepEqual(
389 | html`
390 |
391 | `,
392 | { tag: 'a', props: { b: true }, children: [] }
393 | );
394 | t.end();
395 | });
396 |
397 | test('auto-closing tag', t => {
398 | t.deepEqual(
399 | html`
400 | />
401 | `,
402 | { tag: 'a', props: null, children: [] }
403 | );
404 | t.end();
405 | });
406 |
407 | test('non-element roots', t => {
408 | t.deepEqual(
409 | html`
410 | foo
411 | `,
412 | h(['foo'])
413 | );
414 | t.deepEqual(
415 | html`
416 | ${1}
417 | `,
418 | h([1])
419 | );
420 | t.deepEqual(
421 | html`
422 | foo${1}
423 | `,
424 | h(['foo', 1])
425 | );
426 | t.deepEqual(
427 | html`
428 | foo${1}bar
429 | `,
430 | h(['foo', 1, 'bar'])
431 | );
432 | t.end();
433 | });
434 |
435 | test('text child', t => {
436 | t.deepEqual(
437 | html`
438 | foo
439 | `,
440 | { tag: 'a', props: null, children: ['foo'] }
441 | );
442 | t.deepEqual(
443 | html`
444 | foo bar
445 | `,
446 | { tag: 'a', props: null, children: ['foo bar'] }
447 | );
448 | t.deepEqual(
449 | html`
450 | foo "
451 | `,
452 | {
453 | tag: 'a',
454 | props: null,
455 | children: ['foo "', { tag: 'b', props: null, children: [] }]
456 | }
457 | );
458 | t.end();
459 | });
460 |
461 | test('dynamic child', t => {
462 | t.deepEqual(
463 | html`
464 | ${'foo'}
465 | `,
466 | { tag: 'a', props: null, children: ['foo'] }
467 | );
468 | t.end();
469 | });
470 |
471 | test('mixed text + dynamic children', t => {
472 | t.deepEqual(
473 | html`
474 | ${'foo'}bar
475 | `,
476 | { tag: 'a', props: null, children: ['foo', 'bar'] }
477 | );
478 | t.deepEqual(
479 | html`
480 | before${'foo'}after
481 | `,
482 | { tag: 'a', props: null, children: ['before', 'foo', 'after'] }
483 | );
484 | t.deepEqual(
485 | html`
486 | foo${null}
487 | `,
488 | { tag: 'a', props: null, children: ['foo', null] }
489 | );
490 | t.end();
491 | });
492 |
493 | test('element child', t => {
494 | t.deepEqual(
495 | html`
496 |
497 | `,
498 | h('a', null, h('b', null))
499 | );
500 | t.end();
501 | });
502 |
503 | test('multiple element children', t => {
504 | t.deepEqual(
505 | html`
506 |
507 | `,
508 | h('a', null, h('b', null), h('c', null))
509 | );
510 | t.deepEqual(
511 | html`
512 |
513 | `,
514 | h('a', { x: true }, h('b', { y: true }), h('c', { z: true }))
515 | );
516 | t.deepEqual(
517 | html`
518 |
519 | `,
520 | h('a', { x: '1' }, h('b', { y: '2' }), h('c', { z: '3' }))
521 | );
522 | t.deepEqual(
523 | html`
524 |
525 | `,
526 | h('a', { x: 1 }, h('b', { y: 2 }), h('c', { z: 3 }))
527 | );
528 | t.end();
529 | });
530 |
531 | test('mixed typed children', t => {
532 | t.deepEqual(
533 | html`
534 | foo
535 | `,
536 | h('a', null, 'foo', h('b', null))
537 | );
538 | t.deepEqual(
539 | html`
540 | bar
541 | `,
542 | h('a', null, h('b', null), 'bar')
543 | );
544 | t.deepEqual(
545 | html`
546 | before after
547 | `,
548 | h('a', null, 'before', h('b', null), 'after')
549 | );
550 | t.deepEqual(
551 | html`
552 | before after
553 | `,
554 | h('a', null, 'before', h('b', { x: '1' }), 'after')
555 | );
556 | t.deepEqual(
557 | html`
558 |
559 | before${'foo'}
560 |
561 | ${'bar'}after
562 |
563 | `,
564 | h('a', null, 'before', 'foo', h('b', null), 'bar', 'after')
565 | );
566 | t.end();
567 | });
568 |
569 | test('hyphens (-) are allowed in attribute names', t => {
570 | t.deepEqual(
571 | html`
572 |
573 | `,
574 | h('a', { 'b-c': true })
575 | );
576 | t.end();
577 | });
578 |
579 | test('NUL characters are allowed in attribute values', t => {
580 | t.deepEqual(
581 | html`
582 |
583 | `,
584 | h('a', { b: '\0' })
585 | );
586 | t.deepEqual(
587 | html`
588 |
589 | `,
590 | h('a', { b: '\0', c: 'foo' })
591 | );
592 | t.end();
593 | });
594 |
595 | test('NUL characters are allowed in text', t => {
596 | t.deepEqual(
597 | html`
598 | \0
599 | `,
600 | h('a', null, '\0')
601 | );
602 | t.deepEqual(
603 | html`
604 | \0${'foo'}
605 | `,
606 | h('a', null, '\0', 'foo')
607 | );
608 | t.end();
609 | });
610 |
611 | test('cache key should be unique', t => {
612 | html`
613 |
614 | `;
615 | t.deepEqual(
616 | html`
617 |
618 | `,
619 | h('a', { b: '\0' })
620 | );
621 | t.notDeepEqual(
622 | html`
623 | ${''}9aaaaaaaaa${''}
624 | `,
625 | html`
626 | ${''}0${''}aaaaaaaaa${''}
627 | `
628 | );
629 | t.notDeepEqual(
630 | html`
631 | ${''}0${''}aaaaaaaa${''}
632 | `,
633 | html`
634 | ${''}.8aaaaaaaa${''}
635 | `
636 | );
637 | t.end();
638 | });
639 |
640 | test('do not mutate spread variables', t => {
641 | const obj = {};
642 | html`
643 |
644 | `;
645 | t.deepEqual(obj, {});
646 | t.end();
647 | });
648 |
649 | test('ignore HTML comments', t => {
650 | t.deepEqual(
651 | html`
652 |
653 | `,
654 | h('a', null)
655 | );
656 | t.deepEqual(
657 | html`
658 |
662 | `,
663 | h('a', null)
664 | );
665 | t.deepEqual(
666 | html`
667 |
668 | `,
669 | h('a', null)
670 | );
671 | t.deepEqual(
672 | html`
673 | Hello, world
674 | `,
675 | h('a', null)
676 | );
677 | t.end();
678 | });
679 |
--------------------------------------------------------------------------------
/test/hydrate/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | ---
2 | env:
3 | mocha: true
4 |
5 | globals:
6 | assert: false
7 | should: false
8 | expect: false
9 | sinon: false
10 |
--------------------------------------------------------------------------------
/test/hydrate/selector.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import { dhtml, hydrate } from 'sinuous/hydrate';
3 | import { observable } from 'sinuous';
4 |
5 | test('hydrate selects root node via id selector', function(t) {
6 | document.body.innerHTML = `
7 |
8 | something
9 |
10 | `;
11 |
12 | const div = hydrate(dhtml`
13 |
14 | something
15 |
16 | `);
17 |
18 | t.equal(div, document.querySelector('#root'));
19 |
20 | div.parentNode.removeChild(div);
21 | t.end();
22 | });
23 |
24 | test('hydrate selects root node via class selector', function(t) {
25 | document.body.innerHTML = `
26 |
27 | something
28 |
29 | `;
30 |
31 | const div = hydrate(dhtml`
32 |
33 | something
34 |
35 | `);
36 |
37 | t.equal(div, document.querySelector('.root.pure'));
38 | t.equal(div, document.querySelector('.root'));
39 | t.equal(div, document.querySelector('.pure'));
40 |
41 | div.parentNode.removeChild(div);
42 | t.end();
43 | });
44 |
45 | test('hydrate selects root node via tag selector', function(t) {
46 | document.body.innerHTML = `
47 | something
48 | `;
49 |
50 | const btn = hydrate(dhtml`
51 | something
52 | `);
53 |
54 | t.equal(btn, document.querySelector('button'));
55 |
56 | btn.parentNode.removeChild(btn);
57 | t.end();
58 | });
59 |
60 | test('hydrate selects root node via partial class selector', function(t) {
61 | document.body.innerHTML = `
62 |
63 | something
64 |
65 | `;
66 |
67 | const isActive = observable('');
68 | const div = hydrate(dhtml`
69 |
70 | isActive(isActive() ? '' : ' is-active')}
72 | title="Apply pressure"
73 | >
74 | something
75 |
76 |
77 | `);
78 |
79 | const btn = div.children[0];
80 | btn.click();
81 | t.equal(div.className, 'root pure is-active', 'click called');
82 |
83 | t.equal(div, document.querySelector('.root.pure'));
84 | t.equal(div, document.querySelector('.root'));
85 | t.equal(div, document.querySelector('.pure'));
86 |
87 | div.parentNode.removeChild(div);
88 | t.end();
89 | });
90 |
--------------------------------------------------------------------------------
/test/hydrate/svg.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import { normalizeSvg } from '../_utils.js';
3 | import { ds, dsvg, hydrate } from 'sinuous/hydrate';
4 | import { observable } from 'sinuous';
5 |
6 | test('supports hydrating SVG via hyperscript', function(t) {
7 | document.body.innerHTML = ` `;
8 |
9 | const delta = ds(
10 | 'svg',
11 | { class: 'redbox', viewBox: '0 0 100 100' },
12 | ds('path', { d: 'M 8.74211 7.70899' })
13 | );
14 |
15 | const svg = hydrate(delta, document.querySelector('svg'));
16 |
17 | t.equal(
18 | normalizeSvg(svg),
19 | ' '
20 | );
21 | t.end();
22 | });
23 |
24 | test('supports hydrating SVG', function(t) {
25 | document.body.innerHTML = ` `;
26 |
27 | const delta = dsvg`
28 |
29 |
30 |
31 | `;
32 |
33 | const el = hydrate(delta, document.querySelector('svg'));
34 |
35 | t.equal(
36 | normalizeSvg(el),
37 | ' '
38 | );
39 | t.end();
40 | });
41 |
42 | test('can hydrate an array of svg elements', function(t) {
43 | document.body.innerHTML = ` `;
44 |
45 | const circles = observable([1, 2, 3]);
46 | const delta = dsvg`
47 | ${() => circles().map(c => dsvg` `)}
48 |
49 | ${() => circles().map(c => dsvg` `)}
50 | `;
51 |
52 | const el = hydrate(delta, document.querySelector('svg'));
53 |
54 | t.equal(
55 | normalizeSvg(el),
56 | ' '
57 | );
58 |
59 | circles([1, 2, 3, 4]);
60 |
61 | t.equal(
62 | normalizeSvg(el),
63 | ' '
64 | );
65 |
66 | t.end();
67 | });
68 |
--------------------------------------------------------------------------------
/test/map/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | ---
2 | env:
3 | browser: true
4 |
5 | rules:
6 | no-console: off
7 |
--------------------------------------------------------------------------------
/test/map/dispose.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import { root } from 'sinuous/observable';
3 | import { o, h } from 'sinuous';
4 | import { map } from 'sinuous/map';
5 |
6 | function lis(str) {
7 | return '' + str.split(',').join(' ') + ' ';
8 | }
9 |
10 | test('disposer index works correctly', function(t) {
11 | let one = o(1);
12 | let two = o(2);
13 | let three = o(3);
14 | let four = o(4);
15 | let five = o(5);
16 | // initialize 1, 2, 3, at indexes 0, 1, 2
17 | const list = o([one, two, three]);
18 | const el = h(
19 | 'ul',
20 | map(list, item => h('li', item))
21 | );
22 | t.equal(el.innerHTML, lis('1,2,3'));
23 |
24 | // insert 4, 5 at index 0, 1 overwriting disposers[0, 1]
25 | list([four, five, one, two, three]);
26 | t.equal(el.innerHTML, lis('4,5,1,2,3'));
27 |
28 | // remove 1, 2, with disposal index 0, 1, freeing disposers[0, 1]
29 | list([four, five, three]);
30 | t.equal(el.innerHTML, lis('4,5,3'));
31 |
32 | one(11);
33 | two(22);
34 | three(33);
35 | four(44);
36 | five(55);
37 |
38 | t.equal(el.innerHTML, lis('44,55,33'));
39 |
40 | t.end();
41 | });
42 |
43 | test('last algorithm insertNodes -> disposes correct index', function(t) {
44 | let one = o(1);
45 | let two = o(2);
46 | let three = o(3);
47 | let four = o(4);
48 | let five = o(5);
49 | const list = o([one, two, three, four, five]);
50 | const el = h(
51 | 'ul',
52 | map(list, item => h('li', item))
53 | );
54 | t.equal(el.innerHTML, lis('1,2,3,4,5'));
55 |
56 | list([one, four, five, three, two]);
57 | t.equal(el.innerHTML, lis('1,4,5,3,2'));
58 |
59 | list([one, two, three, four]);
60 | t.equal(el.innerHTML, lis('1,2,3,4'));
61 |
62 | one(11);
63 | two(22);
64 | three(33);
65 | four(44);
66 | five(55);
67 |
68 | t.equal(el.innerHTML, lis('11,22,33,44'));
69 |
70 | t.end();
71 | });
72 |
73 | test('swap backward -> disposes correct index', function(t) {
74 | let one = o(1);
75 | let two = o(2);
76 | let three = o(3);
77 | let four = o(4);
78 | let five = o(5);
79 | const list = o([one, two, three, four, five]);
80 | const el = h(
81 | 'ul',
82 | map(list, item => h('li', item))
83 | );
84 | t.equal(el.innerHTML, lis('1,2,3,4,5'));
85 |
86 | list([one, two, five, three, four]);
87 | t.equal(el.innerHTML, lis('1,2,5,3,4'));
88 |
89 | list([one, two, three, four]);
90 | t.equal(el.innerHTML, lis('1,2,3,4'));
91 |
92 | one(11);
93 | two(22);
94 | three(33);
95 | four(44);
96 | five(55);
97 |
98 | t.equal(el.innerHTML, lis('11,22,33,44'));
99 |
100 | t.end();
101 | });
102 |
103 | test('swap forward -> disposes correct index', function(t) {
104 | let one = o(1);
105 | let two = o(2);
106 | let three = o(3);
107 | let four = o(4);
108 | let five = o(5);
109 | const list = o([one, two, three, four, five]);
110 | const el = h(
111 | 'ul',
112 | map(list, item => h('li', item))
113 | );
114 | t.equal(el.innerHTML, lis('1,2,3,4,5'));
115 |
116 | list([two, three, one, four, five]);
117 | t.equal(el.innerHTML, lis('2,3,1,4,5'));
118 |
119 | list([two, three, four, five]);
120 | t.equal(el.innerHTML, lis('2,3,4,5'));
121 |
122 | one(11);
123 | two(22);
124 | three(33);
125 | four(44);
126 | five(55);
127 |
128 | t.equal(el.innerHTML, lis('22,33,44,55'));
129 |
130 | t.end();
131 | });
132 |
133 | test('removing one observable diposes correct index', function(t) {
134 | let two = o(2);
135 | let four = o(4);
136 | let six = o(6);
137 | const list = o([1, two, 3, four, 5, six, 7]);
138 | const el = h(
139 | 'ul',
140 | map(list, item => h('li', item))
141 | );
142 | t.equal(el.innerHTML, lis('1,2,3,4,5,6,7'));
143 |
144 | list([1, two, four, 3, 5, six, 7]);
145 | t.equal(el.innerHTML, lis('1,2,4,3,5,6,7'));
146 |
147 | list([1, two, 3, six, 7]);
148 | four(44);
149 | two(22);
150 | t.equal(el.innerHTML, lis('1,22,3,6,7'));
151 |
152 | two(2);
153 | four(4444);
154 | six(66);
155 | t.equal(el.innerHTML, lis('1,2,3,66,7'));
156 |
157 | t.end();
158 | });
159 |
160 | test('explicit dispose works and disposes observables', function(t) {
161 | let four = o(4);
162 | const list = o([1, 2, 3, four]);
163 | let dispose;
164 | const el = root(d => {
165 | dispose = d;
166 | return h(
167 | 'ul',
168 | map(list, item => h('li', item))
169 | );
170 | });
171 | t.equal(el.innerHTML, lis('1,2,3,4'));
172 |
173 | list([1, 2, four, 3]);
174 | t.equal(el.innerHTML, lis('1,2,4,3'));
175 |
176 | four(44);
177 | t.equal(el.innerHTML, lis('1,2,44,3'));
178 |
179 | dispose();
180 |
181 | four(44444);
182 | t.equal(el.innerHTML, lis('1,2,44,3'));
183 |
184 | list([9, 7, 8, 6]);
185 | t.equal(el.innerHTML, lis('1,2,44,3'));
186 |
187 | t.end();
188 | });
189 |
190 | test('emptying list disposes observables', function(t) {
191 | let four = o(4);
192 | const list = o([1, 2, 3, four]);
193 |
194 | const el = h(
195 | 'ul',
196 | map(list, item => h('li', item))
197 | );
198 | t.equal(el.innerHTML, lis('1,2,3,4'));
199 |
200 | list([1, 2, four, 3]);
201 | t.equal(el.innerHTML, lis('1,2,4,3'));
202 |
203 | four(44);
204 | t.equal(el.innerHTML, lis('1,2,44,3'));
205 |
206 | list([]);
207 | four(44444);
208 | t.equal(el.innerHTML, '');
209 |
210 | t.end();
211 | });
212 |
--------------------------------------------------------------------------------
/test/map/map-basic.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import { root } from 'sinuous/observable';
3 | import { o, h, html } from 'sinuous';
4 | import { map } from 'sinuous/map';
5 |
6 | const list = o([]);
7 | const show = o(true);
8 | const fallback = o(html``);
9 |
10 | let div;
11 | let dispose;
12 | root(d => {
13 | dispose = d;
14 | div = html`
15 |
16 | ${() => show()
17 | ? html`${map(list, item => html`${item}`)}`
18 | : html`${fallback}`
19 | }
20 |
21 | `;
22 | });
23 |
24 | test('Basic map - create', t => {
25 | list([['a', 1], ['b', 2], ['c', 3], ['d', 4]]);
26 | t.equal(div.innerHTML, 'a1b2c3d4');
27 | t.end();
28 | });
29 |
30 | test('Basic map - update', t => {
31 | list([['b', 2, 99], ['a', 1], ['c']]);
32 | t.equal(div.innerHTML, 'b299a1c');
33 | t.end();
34 | });
35 |
36 | test('Basic map - clear', t => {
37 | list([]);
38 | t.equal(div.innerHTML, '');
39 | t.end();
40 | });
41 |
42 | test('Basic map - update 2', t => {
43 | show(false);
44 | list([['b', 2, 99], ['a', 1], ['c']]);
45 | t.equal(div.innerHTML, '');
46 | t.end();
47 | });
48 |
49 | test('Basic map - clear 2', t => {
50 | show(true);
51 | list([]);
52 | fallback('');
53 | t.equal(div.innerHTML, '');
54 | t.end();
55 | });
56 |
57 | test('Basic map - update 3', t => {
58 | div.insertBefore(h('i'), div.firstChild);
59 | div.insertBefore(h('b'), div.firstChild);
60 |
61 | div.appendChild(h('i'));
62 | div.appendChild(h('b'));
63 |
64 | list([['b', 2, 99], ['a', 1], ['c']]);
65 | t.equal(div.innerHTML, ' b299a1c ');
66 | t.end();
67 | });
68 |
69 | test('Basic map - update 4', t => {
70 | list([]);
71 | show(false);
72 | fallback(html``);
73 | t.equal(div.innerHTML, ' ');
74 | t.end();
75 | });
76 |
77 | test('Basic map - update 5', t => {
78 | show(true);
79 | fallback(11);
80 | t.equal(div.innerHTML, ' ');
81 | t.end();
82 | });
83 |
84 | test('Basic map - dispose', t => {
85 | dispose();
86 | t.end();
87 | });
88 |
--------------------------------------------------------------------------------
/test/map/map-fragments.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import * as api from 'sinuous/observable';
3 | import { o, h } from 'sinuous';
4 | import { map } from 'sinuous/map';
5 |
6 | const root = api.root;
7 |
8 | let div;
9 | const n1 = 'a',
10 | n2 = 'b',
11 | n3 = 'c',
12 | n4 = 'd';
13 | const list = o([n1, n2, n3, n4]);
14 | let dispose;
15 | const Component = () =>
16 | root(d => {
17 | dispose = d;
18 | div = h(
19 | 'div',
20 | map(list, item => h([item, item]))
21 | );
22 | });
23 |
24 | function apply(t, array) {
25 | list(array);
26 | t.equal(div.innerHTML, array.map(p => `${p}${p}`).join(''));
27 | list([n1, n2, n3, n4]);
28 | t.equal(div.innerHTML, 'aabbccdd');
29 | }
30 |
31 | test('Create map control flow', t => {
32 | Component();
33 |
34 | t.equal(div.innerHTML, 'aabbccdd');
35 | t.end();
36 | });
37 |
38 | test('1 missing', t => {
39 | apply(t, [n2, n3, n4]);
40 | apply(t, [n1, n3, n4]);
41 | apply(t, [n1, n2, n4]);
42 | apply(t, [n1, n2, n3]);
43 | t.end();
44 | });
45 |
46 | test('2 missing', t => {
47 | apply(t, [n3, n4]);
48 | apply(t, [n2, n4]);
49 | apply(t, [n2, n3]);
50 | apply(t, [n1, n4]);
51 | apply(t, [n1, n3]);
52 | apply(t, [n1, n2]);
53 | t.end();
54 | });
55 |
56 | test('3 missing', t => {
57 | apply(t, [n1]);
58 | apply(t, [n2]);
59 | apply(t, [n3]);
60 | apply(t, [n4]);
61 | t.end();
62 | });
63 |
64 | test('all missing', t => {
65 | apply(t, []);
66 | t.end();
67 | });
68 |
69 | test('swaps', t => {
70 | apply(t, [n2, n1, n3, n4]);
71 | apply(t, [n3, n2, n1, n4]);
72 | apply(t, [n4, n2, n3, n1]);
73 | t.end();
74 | });
75 |
76 | test('rotations', t => {
77 | apply(t, [n2, n3, n4, n1]);
78 | apply(t, [n3, n4, n1, n2]);
79 | apply(t, [n4, n1, n2, n3]);
80 | t.end();
81 | });
82 |
83 | test('reversal', t => {
84 | apply(t, [n4, n3, n2, n1]);
85 | t.end();
86 | });
87 |
88 | test('full replace', t => {
89 | apply(t, ['e', 'f', 'g', 'h']);
90 | t.end();
91 | });
92 |
93 | test('swap backward edge', t => {
94 | list(['milk', 'bread', 'chips', 'cookie', 'honey']);
95 | list(['chips', 'bread', 'cookie', 'milk', 'honey']);
96 | t.end();
97 | });
98 |
99 | test('dispose', t => {
100 | dispose();
101 | t.end();
102 | });
103 |
--------------------------------------------------------------------------------
/test/map/map-objects.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import * as api from 'sinuous/observable';
3 | import { o, h, html } from 'sinuous';
4 | import { map } from 'sinuous/map';
5 |
6 | const root = api.root;
7 |
8 | function divs(str) {
9 | return '' + str.split(',').join('
') + '
';
10 | }
11 |
12 | const one = { text: o(1) };
13 | const two = { text: o(2) };
14 | const three = { text: o(3) };
15 | const four = { text: o(4) };
16 | const five = { text: o(5) };
17 | const list = o([one, two, three, four, five]);
18 |
19 | const div = document.createElement('div');
20 | let dispose;
21 | root(d => {
22 | dispose = d;
23 | div.appendChild(
24 | map(
25 | list,
26 | item =>
27 | html`
28 | ${item.text}
29 | `
30 | )
31 | );
32 | });
33 |
34 | test('Object reference - create', t => {
35 | t.equal(div.innerHTML, divs('1,2,3,4,5'));
36 | t.end();
37 | });
38 |
39 | test('Object reference - update', t => {
40 | list([three, one, four, two]);
41 | t.equal(div.innerHTML, divs('3,1,4,2'));
42 | t.end();
43 | });
44 |
45 | test('Object reference - update 2', t => {
46 | list([one, three, two, four]);
47 | t.equal(div.innerHTML, divs('1,3,2,4'));
48 | t.end();
49 | });
50 |
51 | test('Object reference - update 3', t => {
52 | list([five, three, four]);
53 | t.equal(div.innerHTML, divs('5,3,4'));
54 | t.end();
55 | });
56 |
57 | test('Object reference - clear', t => {
58 | list([]);
59 | t.equal(div.innerHTML, '');
60 | t.end();
61 | });
62 |
63 | test('Object reference - dispose', t => {
64 | dispose();
65 | t.end();
66 | });
67 |
--------------------------------------------------------------------------------
/test/map/map.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import * as api from 'sinuous/observable';
3 | import { o, h, html } from 'sinuous';
4 | import { map } from 'sinuous/map';
5 |
6 | const root = api.root;
7 |
8 | let div;
9 | const n1 = 'a',
10 | n2 = 'b',
11 | n3 = 'c',
12 | n4 = 'd';
13 | const list = o([n1, n2, n3, n4]);
14 | let dispose;
15 | const Component = () =>
16 | root(d => {
17 | dispose = d;
18 | div = h(
19 | 'div',
20 | map(list, item => html`${item}`)
21 | );
22 | });
23 |
24 | function apply(t, array) {
25 | list(array);
26 | t.equal(div.innerHTML, array.join(''));
27 | list([n1, n2, n3, n4]);
28 | t.equal(div.innerHTML, 'abcd');
29 | }
30 |
31 | test('Create map control flow', t => {
32 | Component();
33 |
34 | t.equal(div.innerHTML, 'abcd');
35 | t.end();
36 | });
37 |
38 | test('1 missing', t => {
39 | apply(t, [n2, n3, n4]);
40 | apply(t, [n1, n3, n4]);
41 | apply(t, [n1, n2, n4]);
42 | apply(t, [n1, n2, n3]);
43 | t.end();
44 | });
45 |
46 | test('2 missing', t => {
47 | apply(t, [n3, n4]);
48 | apply(t, [n2, n4]);
49 | apply(t, [n2, n3]);
50 | apply(t, [n1, n4]);
51 | apply(t, [n1, n3]);
52 | apply(t, [n1, n2]);
53 | t.end();
54 | });
55 |
56 | test('3 missing', t => {
57 | apply(t, [n1]);
58 | apply(t, [n2]);
59 | apply(t, [n3]);
60 | apply(t, [n4]);
61 | t.end();
62 | });
63 |
64 | test('all missing', t => {
65 | apply(t, []);
66 | t.end();
67 | });
68 |
69 | test('swaps', t => {
70 | apply(t, [n2, n1, n3, n4]);
71 | apply(t, [n3, n2, n1, n4]);
72 | apply(t, [n4, n2, n3, n1]);
73 | t.end();
74 | });
75 |
76 | test('rotations', t => {
77 | apply(t, [n2, n3, n4, n1]);
78 | apply(t, [n3, n4, n1, n2]);
79 | apply(t, [n4, n1, n2, n3]);
80 | t.end();
81 | });
82 |
83 | test('reversal', t => {
84 | apply(t, [n4, n3, n2, n1]);
85 | t.end();
86 | });
87 |
88 | test('full replace', t => {
89 | apply(t, ['e', 'f', 'g', 'h']);
90 | t.end();
91 | });
92 |
93 | test('swap backward edge', t => {
94 | list(['milk', 'bread', 'chips', 'cookie', 'honey']);
95 | list(['chips', 'bread', 'cookie', 'milk', 'honey']);
96 | t.end();
97 | });
98 |
99 | test('dispose', t => {
100 | dispose();
101 | t.end();
102 | });
103 |
--------------------------------------------------------------------------------
/test/observable/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | ---
2 | env:
3 | mocha: true
4 |
5 | globals:
6 | assert: false
7 | should: false
8 | expect: false
9 | sinon: false
10 |
11 | rules:
12 | fp/no-rest-parameters: off
13 |
--------------------------------------------------------------------------------
/test/observable/S.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import spy from 'ispy';
3 | import { o, S } from 'sinuous/observable';
4 |
5 | // Tests from S.js
6 |
7 | test('generates a function', function(t) {
8 | t.plan(1);
9 | var f = S(function() {
10 | return 1;
11 | });
12 | t.assert(typeof f === 'function');
13 | });
14 |
15 | test('returns initial value of wrapped function', function(t) {
16 | t.plan(1);
17 | var f = S(function() {
18 | return 1;
19 | });
20 | t.equal(f(), 1);
21 | });
22 |
23 | test('occurs once intitially', function(t) {
24 | var callSpy = spy();
25 | S(callSpy);
26 | t.equal(callSpy.callCount, 1);
27 | t.end();
28 | });
29 |
30 | test('does not re-occur when read', function(t) {
31 | var callSpy = spy(),
32 | f = S(callSpy);
33 | f();
34 | f();
35 | f();
36 |
37 | t.equal(callSpy.callCount, 1);
38 | t.end();
39 | });
40 |
41 | test('updates when S.data is set', function(t) {
42 | var d = o(1),
43 | fevals = 0;
44 |
45 | S(function() {
46 | fevals++;
47 | return d();
48 | });
49 | fevals = 0;
50 |
51 | d(1);
52 | t.equal(fevals, 1);
53 | t.end();
54 | });
55 |
56 | test('does not update when S.data is read', function(t) {
57 | var d = o(1),
58 | fevals = 0;
59 |
60 | S(function() {
61 | fevals++;
62 | return d();
63 | });
64 | fevals = 0;
65 |
66 | d();
67 | t.equal(fevals, 0);
68 | t.end();
69 | });
70 |
71 | test('updates return value', function(t) {
72 | var d = o(1),
73 | f = S(function() {
74 | return d();
75 | });
76 |
77 | d(2);
78 | t.equal(f(), 2);
79 | t.end();
80 | });
81 |
82 | test('set works from other computed', function(t) {
83 | var banana = o();
84 | var count = 0;
85 | S(() => {
86 | count++;
87 | return banana() + ' shake';
88 | });
89 | t.equal(count, 1);
90 |
91 | var carrot = o();
92 | S(() => {
93 | console.log('banana false');
94 | banana(false);
95 |
96 | carrot() + ' soup';
97 |
98 | console.log('banana true');
99 | banana(true);
100 | });
101 |
102 | carrot('carrot');
103 | t.equal(count, 5);
104 |
105 | banana(false);
106 | t.equal(count, 6);
107 |
108 | t.end();
109 | });
110 |
111 | (function() {
112 | var i, j, e, fevals, f;
113 |
114 | function init() {
115 | i = o(true);
116 | j = o(1);
117 | e = o(2);
118 | fevals = 0;
119 | f = S(function() {
120 | fevals++;
121 | return i() ? j() : e();
122 | });
123 | fevals = 0;
124 | }
125 |
126 | test('updates on active dependencies', function(t) {
127 | init();
128 | j(5);
129 | t.equal(fevals, 1);
130 | t.equal(f(), 5);
131 | t.end();
132 | });
133 |
134 | test('does not update on inactive dependencies', function(t) {
135 | init();
136 | e(5);
137 | t.equal(fevals, 0);
138 | t.equal(f(), 1);
139 | t.end();
140 | });
141 |
142 | test('deactivates obsolete dependencies', function(t) {
143 | init();
144 | i(false);
145 | fevals = 0;
146 | j(5);
147 | t.equal(fevals, 0);
148 | t.end();
149 | });
150 |
151 | test('activates new dependencies', function(t) {
152 | init();
153 | i(false);
154 | fevals = 0;
155 | e(5);
156 | t.equal(fevals, 1);
157 | t.end();
158 | });
159 | })();
160 |
161 | test('does not register a dependency', function(t) {
162 | var fevals = 0,
163 | d;
164 |
165 | S(function() {
166 | fevals++;
167 | d = o(1);
168 | });
169 |
170 | fevals = 0;
171 | d(2);
172 | t.equal(fevals, 0);
173 | t.end();
174 | });
175 |
176 | test('reads as undefined', function(t) {
177 | var f = S(function() {});
178 | t.equal(f(), undefined);
179 | t.end();
180 | });
181 |
182 | test('reduces seed value', function(t) {
183 | var a = o(5),
184 | f = S(function(v) {
185 | return v + a();
186 | }, 5);
187 | t.equal(f(), 10);
188 | a(6);
189 | t.equal(f(), 16);
190 | t.end();
191 | });
192 |
193 | (function() {
194 | var d, fcount, f, gcount, g;
195 |
196 | function init() {
197 | (d = o(1)),
198 | (fcount = 0),
199 | (f = S(function() {
200 | fcount++;
201 | return d();
202 | })),
203 | (gcount = 0),
204 | (g = S(function() {
205 | gcount++;
206 | return f();
207 | }));
208 | }
209 |
210 | test('does not cause re-evaluation', function(t) {
211 | init();
212 | t.equal(fcount, 1);
213 | t.end();
214 | });
215 |
216 | test('does not occur from a read', function(t) {
217 | init();
218 | f();
219 | t.equal(gcount, 1);
220 | t.end();
221 | });
222 |
223 | test('does not occur from a read of the watcher', function(t) {
224 | init();
225 | g();
226 | t.equal(gcount, 1);
227 | t.end();
228 | });
229 |
230 | test('occurs when computation updates', function(t) {
231 | init();
232 | d(2);
233 | t.equal(fcount, 2);
234 | t.equal(gcount, 2);
235 | t.equal(g(), 2);
236 | t.end();
237 | });
238 | })();
239 |
240 | // test("throws when continually setting a direct dependency", function () {
241 | // var d = S.data(1);
242 |
243 | // t.equal(function () {
244 | // S(function () { d(); d(2); });
245 | // }).toThrow();
246 | // });
247 |
248 | // test("throws when continually setting an indirect dependency", function () {
249 | // var d = S.data(1),
250 | // f1 = S(function () { return d(); }),
251 | // f2 = S(function () { return f1(); }),
252 | // f3 = S(function () { return f2(); });
253 |
254 | // t.equal(function () {
255 | // S(function () { f3(); d(2); });
256 | // }).toThrow();
257 | // });
258 |
259 | // test("throws when cycle created by modifying a branch", function () {
260 | // var d = S.data(1),
261 | // f = S(function () { return f ? f() : d(); });
262 |
263 | // t.equal(function () { d(0); }).toThrow();
264 | // });
265 |
266 | test('propagates in topological order', function(t) {
267 | //
268 | // c1
269 | // / \
270 | // / \
271 | // b1 b2
272 | // \ /
273 | // \ /
274 | // a1
275 | //
276 | var seq = '',
277 | a1 = o(true),
278 | b1 = S(function() {
279 | a1();
280 | seq += 'b1';
281 | }),
282 | b2 = S(function() {
283 | a1();
284 | seq += 'b2';
285 | }),
286 | c1 = S(function() {
287 | b1(), b2();
288 | seq += 'c1';
289 | });
290 |
291 | seq = '';
292 | a1(true);
293 |
294 | t.equal(seq, 'b1b2c1');
295 | t.end();
296 | });
297 |
298 | test('only propagates once with linear convergences', function(t) {
299 | // d
300 | // |
301 | // +---+---+---+---+
302 | // v v v v v
303 | // f1 f2 f3 f4 f5
304 | // | | | | |
305 | // +---+---+---+---+
306 | // v
307 | // g
308 | var d = o(0),
309 | f1 = S(function() {
310 | return d();
311 | }),
312 | f2 = S(function() {
313 | return d();
314 | }),
315 | f3 = S(function() {
316 | return d();
317 | }),
318 | f4 = S(function() {
319 | return d();
320 | }),
321 | f5 = S(function() {
322 | return d();
323 | }),
324 | gcount = 0,
325 | g = S(function() {
326 | gcount++;
327 | return f1() + f2() + f3() + f4() + f5();
328 | });
329 |
330 | gcount = 0;
331 | d(0);
332 | t.equal(gcount, 1);
333 | t.end();
334 | });
335 |
336 | test('only propagates once with exponential convergence', function(t) {
337 | // d
338 | // |
339 | // +---+---+
340 | // v v v
341 | // f1 f2 f3
342 | // \ | /
343 | // O
344 | // / | \
345 | // v v v
346 | // g1 g2 g3
347 | // +---+---+
348 | // v
349 | // h
350 | var d = o(0),
351 | f1 = S(function() {
352 | return d();
353 | }),
354 | f2 = S(function() {
355 | return d();
356 | }),
357 | f3 = S(function() {
358 | return d();
359 | }),
360 | g1 = S(function() {
361 | return f1() + f2() + f3();
362 | }),
363 | g2 = S(function() {
364 | return f1() + f2() + f3();
365 | }),
366 | g3 = S(function() {
367 | return f1() + f2() + f3();
368 | }),
369 | hcount = 0,
370 | h = S(function() {
371 | hcount++;
372 | return g1() + g2() + g3();
373 | });
374 |
375 | hcount = 0;
376 | d(0);
377 | t.equal(hcount, 1);
378 | t.end();
379 | });
380 |
--------------------------------------------------------------------------------
/test/observable/child.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import spy from 'ispy';
3 | import { o, S, transaction, observable, sample } from 'sinuous/observable';
4 |
5 | test('parent cleans up inner subscriptions', function(t) {
6 | let i = 0;
7 |
8 | const data = o(null);
9 | const cache = o(false);
10 |
11 | let childValue;
12 | let childValue2;
13 |
14 | const child = d => {
15 | S(function nested() {
16 | childValue = d();
17 | i++;
18 | });
19 | return 'Hi';
20 | };
21 |
22 | const child2 = d => {
23 | S(function nested2() {
24 | childValue2 = d();
25 | });
26 | return 'Hi';
27 | };
28 |
29 | S(function cacheFun(prev) {
30 | const d = !!data();
31 | if (d === prev) {
32 | return prev;
33 | }
34 | cache(d);
35 | return d;
36 | });
37 |
38 | // Run 1st time
39 | S(function memo() {
40 | cache();
41 | child2(data);
42 | child(data);
43 | });
44 |
45 | // 2nd
46 | data('name');
47 | t.equal(childValue, 'name');
48 | t.equal(childValue2, 'name');
49 |
50 | // 3rd
51 | data(null);
52 | t.equal(childValue, null);
53 | t.equal(childValue2, null);
54 |
55 | // 4th
56 | data('name2');
57 | t.equal(childValue, 'name2');
58 | t.equal(childValue2, 'name2');
59 |
60 | t.equal(i, 4);
61 | t.end();
62 | });
63 |
64 | test('parent cleans up inner conditional subscriptions', function(t) {
65 | let i = 0;
66 |
67 | const data = o(null);
68 | const cache = o(false);
69 |
70 | let childValue;
71 |
72 | const child = d => {
73 | S(function nested() {
74 | childValue = d();
75 | i++;
76 | });
77 | return 'Hi';
78 | };
79 |
80 | S(function cacheFun(prev) {
81 | const d = !!data();
82 | if (d === prev) {
83 | return prev;
84 | }
85 | cache(d);
86 | return d;
87 | });
88 |
89 | const memo = S(() => {
90 | const c = cache();
91 | return c ? child(data) : undefined;
92 | });
93 |
94 | let view;
95 | S(() => (view = memo()));
96 |
97 | t.equal(view, undefined);
98 |
99 | // Run 1st time
100 | data('name');
101 | t.equal(childValue, 'name');
102 |
103 | t.equal(view, 'Hi');
104 |
105 | // 2nd
106 | data('name2');
107 | t.equal(childValue, 'name2');
108 |
109 | // data is null -> cache is false -> child is not run here
110 | data(null);
111 | t.equal(childValue, 'name2');
112 |
113 | t.equal(view, undefined);
114 |
115 | t.equal(i, 2);
116 | t.end();
117 | });
118 |
119 | test('parent cleans up inner conditional subscriptions w/ other child', function(t) {
120 | let i = 0;
121 |
122 | const data = o(null);
123 | const cache = o(false);
124 |
125 | let childValue;
126 | let childValue2;
127 |
128 | const child = d => {
129 | S(function nested() {
130 | childValue = d();
131 | i++;
132 | });
133 | return 'Hi';
134 | };
135 |
136 | const child2 = d => {
137 | S(function nested2() {
138 | childValue2 = d();
139 | });
140 | return 'Hi';
141 | };
142 |
143 | S(function cacheFun(prev) {
144 | const d = !!data();
145 | if (d === prev) {
146 | return prev;
147 | }
148 | cache(d);
149 | return d;
150 | });
151 |
152 | // Run 1st time
153 | const memo = S(() => {
154 | const c = cache();
155 | child2(data);
156 | return c ? child(data) : undefined;
157 | });
158 |
159 | let view;
160 | S(() => (view = memo()));
161 |
162 | t.equal(view, undefined);
163 |
164 | // 2nd
165 | data('name');
166 | t.equal(childValue, 'name');
167 | t.equal(childValue2, 'name');
168 |
169 | t.equal(view, 'Hi');
170 |
171 | // 3rd
172 | data(null);
173 | t.equal(childValue, 'name');
174 | t.equal(childValue2, null);
175 |
176 | t.equal(view, undefined);
177 |
178 | // 4th
179 | data('name2');
180 | t.equal(childValue, 'name2');
181 | t.equal(childValue2, 'name2');
182 |
183 | t.equal(i, 2);
184 | t.end();
185 | });
186 |
187 | test('deeply nested cleanup of subscriptions', function(t) {
188 | const data = o(null);
189 |
190 | const spy1 = spy();
191 | spy1.delegate = () => {
192 | spy2();
193 | };
194 |
195 | const spy2 = spy();
196 | spy2.delegate = () => {
197 | data();
198 | child3();
199 | };
200 |
201 | const spy3 = spy();
202 | spy3.delegate = () => {
203 | data();
204 | };
205 |
206 | const child1 = () => {
207 | S(spy1);
208 | return 'Hi';
209 | };
210 |
211 | const child3 = () => {
212 | S(spy3);
213 | return 'Hi';
214 | };
215 |
216 | S(() => {
217 | child1();
218 | });
219 |
220 | t.equal(spy1.callCount, 1);
221 | t.equal(spy3.callCount, 1);
222 |
223 | data('banana');
224 |
225 | t.equal(spy3.callCount, 2);
226 |
227 | t.end();
228 | });
229 |
230 | test('insures that new dependencies are updated before dependee', function(t) {
231 | var order = '';
232 | var a = o(0);
233 |
234 | var b = S(function x() {
235 | order += 'b';
236 | console.log('B');
237 | return a() + 1;
238 | });
239 |
240 | var c = S(function y() {
241 | order += 'c';
242 | console.log('C');
243 | return b() || d();
244 | });
245 |
246 | function z() {
247 | order += 'd';
248 | console.log('D');
249 | return a() + 10;
250 | }
251 | var d = S(z);
252 |
253 | t.equal(order, 'bcd', '1st bcd test');
254 |
255 | order = '';
256 | a(-1);
257 |
258 | t.equal(b(), 0, 'b equals 0');
259 | t.equal(order, 'bcd', '2nd bcd test');
260 | t.equal(d(), 9, 'd equals 9');
261 | t.equal(c(), 9, 'c equals d(9)');
262 |
263 | order = '';
264 | a(0);
265 |
266 | t.equal(order, 'bc', '3rd bcd test');
267 | t.equal(c(), 1);
268 | t.end();
269 | });
270 |
271 | test('unrelated state via transaction updates view correctly', function(t) {
272 | const data = observable(null),
273 | trigger = observable(false),
274 | cache = observable(sample(() => !!trigger())),
275 | child = data => {
276 | S(() => console.log('nested', data().length));
277 | return 'Hi';
278 | };
279 |
280 | S(prev => {
281 | const d = !!data();
282 | if (d === prev) return prev;
283 | cache(d);
284 | return d;
285 | });
286 |
287 | const memo = S(() => (cache() ? child(data) : undefined));
288 |
289 | let view;
290 | S(() => (view = memo()));
291 | t.equal(view, undefined);
292 |
293 | transaction(() => {
294 | trigger(true);
295 | data('name');
296 | });
297 | t.equal(view, 'Hi');
298 |
299 | transaction(() => {
300 | trigger(true);
301 | data('name2');
302 | });
303 |
304 | transaction(() => {
305 | data(undefined);
306 | trigger(false);
307 | });
308 | t.equal(view, undefined);
309 |
310 | t.end();
311 | });
312 |
--------------------------------------------------------------------------------
/test/observable/dispose.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import { o, S, root } from 'sinuous/observable';
3 |
4 | test("disables updates and sets computation's value to undefined", function(t) {
5 | root(function(dispose) {
6 | var c = 0,
7 | d = o(0),
8 | f = S(function() {
9 | c++;
10 | return d();
11 | });
12 |
13 | t.equal(c, 1);
14 | t.equal(f(), 0);
15 |
16 | d(1);
17 |
18 | t.equal(c, 2);
19 | t.equal(f(), 1);
20 |
21 | dispose();
22 |
23 | d(2);
24 |
25 | t.equal(c, 2);
26 | t.equal(f(), 1);
27 | t.end();
28 | });
29 | });
30 |
31 | // unconventional uses of dispose -- to insure S doesn't behaves as expected in these cases
32 |
33 | test('works from the body of its own computation', function(t) {
34 | root(function(dispose) {
35 | var c = 0,
36 | d = o(0),
37 | f = S(function() {
38 | c++;
39 | if (d()) dispose();
40 | d();
41 | });
42 |
43 | t.equal(c, 1);
44 |
45 | d(1);
46 | t.equal(c, 2);
47 |
48 | d(2);
49 | t.equal(c, 2);
50 |
51 | t.end();
52 | });
53 | });
54 |
55 | test('works from the body of a subcomputation', function(t) {
56 | root(function(dispose) {
57 | var c = 0,
58 | d = o(0),
59 | f = S(function() {
60 | c++;
61 | d();
62 | S(function() {
63 | if (d()) dispose();
64 | });
65 | });
66 |
67 | t.equal(c, 1);
68 |
69 | d(1);
70 |
71 | t.equal(c, 2);
72 |
73 | d(2);
74 |
75 | t.equal(c, 2);
76 | t.end();
77 | });
78 | });
79 |
--------------------------------------------------------------------------------
/test/observable/observable.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import spy from 'ispy';
3 | import {
4 | o,
5 | subscribe,
6 | unsubscribe,
7 | cleanup,
8 | isListening
9 | } from 'sinuous/observable';
10 |
11 | test('initial value can be set', function(t) {
12 | let title = o('Groovy!');
13 | t.equal(title(), 'Groovy!');
14 | t.end();
15 | });
16 |
17 | test('runs function on subscribe', function(t) {
18 | subscribe(t.pass);
19 | t.end();
20 | });
21 |
22 | test('observable can be set without subscription', function(t) {
23 | let title = o();
24 | title('Groovy!');
25 | t.equal(title(), 'Groovy!');
26 | t.end();
27 | });
28 |
29 | test('isListening', function(t) {
30 | let title = o();
31 | t.assert(!isListening());
32 | subscribe(() => {
33 | title();
34 | t.assert(isListening());
35 | });
36 | t.end();
37 | });
38 |
39 | test('updates when the observable is set', function(t) {
40 | let title = o();
41 | let text;
42 | subscribe(() => (text = title()));
43 |
44 | title('Welcome to Sinuous!');
45 | t.equal(text, 'Welcome to Sinuous!');
46 |
47 | title('Groovy!');
48 | t.equal(text, 'Groovy!');
49 |
50 | t.end();
51 | });
52 |
53 | test('observable unsubscribe', function(t) {
54 | let title = o('Initial title');
55 | let text;
56 | const unsubscribe = subscribe(() => (text = title()));
57 |
58 | title('Welcome to Sinuous!');
59 | t.equal(text, 'Welcome to Sinuous!');
60 |
61 | unsubscribe();
62 |
63 | title('Groovy!');
64 | t.equal(text, 'Welcome to Sinuous!');
65 |
66 | t.end();
67 | });
68 |
69 | test('nested subscribe', function(t) {
70 | let apple = o('apple');
71 | let lemon = o('lemon');
72 | let onion = o('onion');
73 | let tempApple;
74 | let tempLemon;
75 | let tempOnion;
76 |
77 | let veggieSpy;
78 | const fruitSpy = spy();
79 | fruitSpy.delegate = () => {
80 | tempApple = apple();
81 |
82 | veggieSpy = spy();
83 | veggieSpy.delegate = () => {
84 | tempOnion = onion();
85 | };
86 |
87 | subscribe(veggieSpy);
88 |
89 | tempLemon = lemon();
90 | };
91 |
92 | subscribe(fruitSpy);
93 |
94 | t.equal(tempApple, 'apple');
95 | t.equal(tempLemon, 'lemon');
96 | t.equal(tempOnion, 'onion');
97 | t.equal(fruitSpy.callCount, 1);
98 | t.equal(veggieSpy.callCount, 1);
99 |
100 | onion('peel');
101 | t.equal(tempOnion, 'peel');
102 | t.equal(fruitSpy.callCount, 1);
103 | t.equal(veggieSpy.callCount, 2);
104 |
105 | lemon('juice');
106 | t.equal(tempLemon, 'juice');
107 | t.equal(fruitSpy.callCount, 2);
108 | // this will be a new spy that was executed once
109 | t.equal(veggieSpy.callCount, 1);
110 |
111 | t.end();
112 | });
113 |
114 | test('one level nested subscribe cleans up inner subscriptions', function(t) {
115 | let apple = o('apple');
116 | let lemon = o('lemon');
117 | let grape = o('grape');
118 | let onion = o('onion');
119 | let bean = o('bean');
120 | let carrot = o('carrot');
121 | let onions = '';
122 | let beans = '';
123 | let carrots = '';
124 |
125 | subscribe(() => {
126 | apple();
127 | subscribe(() => (onions += onion()));
128 | grape();
129 | subscribe(() => (beans += bean()));
130 | subscribe(() => (carrots += carrot()));
131 | lemon();
132 | });
133 |
134 | apple('juice');
135 | lemon('juice');
136 | grape('juice');
137 |
138 | bean('bean');
139 |
140 | t.equal(onions, 'onion'.repeat(4));
141 | t.equal(beans, 'bean'.repeat(5));
142 | t.end();
143 | });
144 |
145 | test('three level nested subscribe cleans up inner subscriptions', function(t) {
146 | let apple = o('apple');
147 | let lemon = o('lemon');
148 | let grape = o('grape');
149 | let onion = o('onion');
150 | let bean = o('bean');
151 | let carrot = o('carrot');
152 | let peanut = o('peanut');
153 | let onions = 0;
154 | let beans = 0;
155 | let carrots = 0;
156 | let peanuts = 0;
157 |
158 | const unsubscribe = subscribe(() => {
159 | apple();
160 | subscribe(() => {
161 | bean();
162 | beans += 1;
163 | subscribe(() => {
164 | onions += 1;
165 | onion();
166 | subscribe(() => peanut() && (peanuts += 1));
167 | });
168 | });
169 | grape();
170 | subscribe(() => carrot() && (carrots += 1));
171 | lemon();
172 | });
173 |
174 | apple('juice');
175 | lemon('juice');
176 | grape('juice');
177 | t.equal(beans, 4);
178 |
179 | bean('bean');
180 | t.equal(beans, 5);
181 |
182 | onion('onion');
183 | onion('onion');
184 | onion('onion');
185 | t.equal(onions, 8);
186 |
187 | peanut('peanut');
188 | peanut('peanut');
189 | t.equal(peanuts, 10);
190 |
191 | unsubscribe();
192 |
193 | apple('juice');
194 | lemon('juice');
195 | grape('juice');
196 |
197 | bean('bean');
198 | t.equal(beans, 5);
199 |
200 | onion('onion');
201 | onion('onion');
202 | onion('onion');
203 | t.equal(onions, 8);
204 |
205 | peanut('peanut');
206 | peanut('peanut');
207 | t.equal(peanuts, 10);
208 |
209 | t.end();
210 | });
211 |
212 | test('standalone unsubscribe works', function(t) {
213 | let carrot = o();
214 | const computed = spy();
215 | computed.delegate = () => {
216 | carrot();
217 | };
218 | subscribe(computed);
219 | carrot('juice');
220 |
221 | unsubscribe(computed);
222 | carrot('juice');
223 |
224 | t.equal(computed.callCount, 2);
225 | t.end();
226 | });
227 |
228 | test('cleanup cleans up on update', function(t) {
229 | let carrot = o();
230 | let button = document.createElement('button');
231 | // IE11 requires the button to be in dom before `button.click()` works.
232 | document.body.appendChild(button);
233 | let count = 0;
234 |
235 | const computed = spy();
236 | computed.delegate = () => {
237 | carrot();
238 | const onClick = () => (count += 1);
239 | button.addEventListener('click', onClick);
240 | };
241 |
242 | const unsubscribe = subscribe(computed);
243 | carrot(9);
244 | carrot(10);
245 | button.click();
246 | t.equal(count, 3);
247 | unsubscribe();
248 |
249 | count = 0;
250 | button = document.createElement('button');
251 | document.body.appendChild(button);
252 |
253 | const computedWithCleanup = spy();
254 | computedWithCleanup.delegate = () => {
255 | carrot();
256 | const onClick = () => (count += 1);
257 | button.addEventListener('click', onClick);
258 | cleanup(() => button.removeEventListener('click', onClick));
259 | };
260 |
261 | subscribe(computedWithCleanup);
262 | carrot(9);
263 | carrot(10);
264 | button.click();
265 | t.equal(count, 1);
266 |
267 | t.end();
268 | });
269 |
--------------------------------------------------------------------------------
/test/observable/on.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import spy from 'ispy';
3 | import { o, root, on } from 'sinuous/observable';
4 |
5 | test('registers a dependency', function(t) {
6 | root(function() {
7 | var d = o(1),
8 | callSpy = spy(),
9 | f = on(d, function() {
10 | callSpy();
11 | });
12 |
13 | t.equal(callSpy.callCount, 1);
14 |
15 | d(2);
16 |
17 | t.equal(callSpy.callCount, 2);
18 | });
19 | t.end();
20 | });
21 |
22 | test('prohibits dynamic dependencies', function(t) {
23 | root(function() {
24 | var d = o(1),
25 | callSpy = spy(),
26 | s = on(
27 | function() {},
28 | function() {
29 | callSpy();
30 | return d();
31 | }
32 | );
33 |
34 | t.equal(callSpy.callCount, 1);
35 |
36 | d(2);
37 |
38 | t.equal(callSpy.callCount, 1);
39 | });
40 | t.end();
41 | });
42 |
43 | test('allows multiple dependencies', function(t) {
44 | root(function() {
45 | var a = o(1),
46 | b = o(2),
47 | c = o(3),
48 | callSpy = spy(),
49 | f = on(
50 | function() {
51 | a();
52 | b();
53 | c();
54 | },
55 | function() {
56 | callSpy();
57 | }
58 | );
59 |
60 | t.equal(callSpy.callCount, 1);
61 |
62 | a(4);
63 | b(5);
64 | c(6);
65 |
66 | t.equal(callSpy.callCount, 4);
67 | });
68 | t.end();
69 | });
70 |
71 | test('allows an array of dependencies', function(t) {
72 | root(function() {
73 | var a = o(1),
74 | b = o(2),
75 | c = o(3),
76 | callSpy = spy(),
77 | f = on([a, b, c], function() {
78 | callSpy();
79 | });
80 |
81 | t.equal(callSpy.callCount, 1);
82 |
83 | a(4);
84 | b(5);
85 | c(6);
86 |
87 | t.equal(callSpy.callCount, 4);
88 | });
89 | t.end();
90 | });
91 |
92 | test('modifies its accumulator when reducing', function(t) {
93 | root(function() {
94 | var a = o(1),
95 | c = on(
96 | a,
97 | function(sum) {
98 | return sum + a();
99 | },
100 | 0
101 | );
102 |
103 | t.equal(c(), 1);
104 |
105 | a(2);
106 |
107 | t.equal(c(), 3);
108 |
109 | a(3);
110 | a(4);
111 |
112 | t.equal(c(), 10);
113 | });
114 | t.end();
115 | });
116 |
117 | test('suppresses initial run when onchanges is true', function(t) {
118 | root(function() {
119 | var a = o(1),
120 | c = on(
121 | a,
122 | function() {
123 | return a() * 2;
124 | },
125 | 0,
126 | true
127 | );
128 |
129 | t.equal(c(), 0);
130 |
131 | a(2);
132 |
133 | t.equal(c(), 4);
134 | });
135 | t.end();
136 | });
137 |
--------------------------------------------------------------------------------
/test/observable/perf/index.js:
--------------------------------------------------------------------------------
1 | const start = Date.now();
2 |
3 | if (process.env.PERSIST) {
4 | const fs = require('fs');
5 | const logFile = __dirname + '/perf.txt';
6 | // clear previous results
7 | if (fs.existsSync(logFile)) fs.unlinkSync(logFile);
8 |
9 | exports.logMeasurement = function(msg) {
10 | console.log(msg);
11 | fs.appendFileSync(logFile, '\n' + msg, 'utf8');
12 | };
13 | } else {
14 | exports.logMeasurement = function(msg) {
15 | console.log(msg);
16 | };
17 | }
18 |
19 | require('./perf.js');
20 |
21 | // This test runs last..
22 | require('tape')(t => {
23 | exports.logMeasurement(
24 | '\n\nCompleted performance suite in ' +
25 | (Date.now() - start) / 1000 +
26 | ' sec.'
27 | );
28 | t.end();
29 | });
30 |
--------------------------------------------------------------------------------
/test/observable/perf/lib/reactive.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | var defaultContext = this;
3 | var __id = 0;
4 |
5 | function $R(fnc, context) {
6 | var rf = function() {
7 | var dirtyNodes = topo(rf);
8 | var v = dirtyNodes[0].run.apply(rf, arguments);
9 | dirtyNodes.slice(1).forEach(function (n) { n.run(); } );
10 | return v;
11 | };
12 | rf.id = __id++;
13 | rf.context = context || defaultContext;
14 | rf.fnc = fnc;
15 | rf.dependents = [];
16 | rf.dependencies = [];
17 | rf.memo = $R.empty;
18 | return $R.extend(rf, reactiveExtensions, $R.pluginExtensions);
19 | }
20 | $R.version = "1.0.0";
21 | $R._ = {};
22 | $R.empty = {};
23 | $R.state = function (initial) {
24 | var rFnc = $R(function () {
25 | return this.val;
26 | });
27 | rFnc.context = rFnc;
28 | rFnc.val = initial;
29 | rFnc.set = $R(function(value) { this.val = value; return this(); }.bind(rFnc));
30 | rFnc.modify = $R(function(transform) { return this.set(transform(this.val)); }.bind(rFnc));
31 | return rFnc;
32 | };
33 | $R.extend = function(o) {
34 | var extensions = Array.prototype.slice.call(arguments, 1);
35 | extensions.forEach(function (extension) {
36 | if (extension) {
37 | for (var prop in extension) { o[prop] = extension[prop]; }
38 | }
39 | });
40 | return o;
41 | };
42 | $R.pluginExtensions = {};
43 |
44 | var reactiveExtensions = {
45 | _isReactive: true,
46 | toString: function () { return this.fnc.toString(); },
47 | get: function() { return this.memo === $R.empty ? this.run() : this.memo; },
48 | run: function() {
49 | var unboundArgs = Array.prototype.slice.call(arguments);
50 | return this.memo = this.fnc.apply(this.context, this.argumentList(unboundArgs));
51 | },
52 | bindTo: function() {
53 | var newDependencies = Array.prototype.slice.call(arguments).map(wrap);
54 | var oldDependencies = this.dependencies;
55 |
56 | oldDependencies.forEach(function (d) {
57 | if (d !== $R._) { d.removeDependent(this); }
58 | }, this);
59 |
60 | newDependencies.forEach(function (d) {
61 | if (d !== $R._) { d.addDependent(this); }
62 | }, this);
63 |
64 | this.dependencies = newDependencies;
65 | return this;
66 | },
67 | removeDependent: function(rFnc) {
68 | this.dependents = this.dependents.filter(function (d) { return d !== rFnc; });
69 | },
70 | addDependent: function(rFnc) {
71 | if (!this.dependents.some(function (d) { return d === rFnc; })) {
72 | this.dependents.push(rFnc);
73 | }
74 | },
75 | argumentList: function(unboundArgs) {
76 | return this.dependencies.map(function(dependency) {
77 | if (dependency === $R._) {
78 | return unboundArgs.shift();
79 | } else if (dependency._isReactive) {
80 | return dependency.get();
81 | } else {
82 | return undefined;
83 | }
84 | }).concat(unboundArgs);
85 | }
86 | };
87 |
88 | if (typeof module !== 'undefined') {
89 | module.exports = $R;
90 | } else {
91 | defaultContext.$R = $R;
92 | }
93 |
94 | //Private
95 | function topo(rootFnc) {
96 | var explored = {};
97 | function search(rFnc) {
98 | if (explored[rFnc.id]) { return []; }
99 | explored[rFnc.id] = true;
100 | return rFnc.dependents.reduce(function (acc, dep) { return acc.concat(search(dep))},[]).concat(rFnc);
101 | }
102 |
103 | return search(rootFnc).reverse();
104 | }
105 |
106 | function wrap(v) {
107 | return v && (v._isReactive || v == $R._) ? v : $R(function () {return v;});
108 | }
109 | })();
--------------------------------------------------------------------------------
/test/observable/perf/lib/reactor.js:
--------------------------------------------------------------------------------
1 | // Generated by CoffeeScript 1.7.1
2 | (function() {
3 | var CompoundError, OBSERVER, SIGNAL, dependencyStack, global,
4 | __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; },
5 | __hasProp = {}.hasOwnProperty,
6 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
7 |
8 | SIGNAL = "SIGNAL";
9 |
10 | OBSERVER = "OBSERVER";
11 |
12 | global = typeof exports !== "undefined" && exports !== null ? exports : this;
13 |
14 | dependencyStack = [];
15 |
16 | global.Signal = function(definition) {
17 | var signalCore, signalInterface;
18 | signalCore = {
19 | dependencyType: SIGNAL,
20 | definition: null,
21 | value: null,
22 | error: null,
23 | dependents: [],
24 | dependencies: [],
25 | update: function() {
26 | var dependency, dependentIndex, error, _i, _len, _ref;
27 | _ref = this.dependencies;
28 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
29 | dependency = _ref[_i];
30 | dependentIndex = dependency.dependents.indexOf(this);
31 | dependency.dependents.splice(dependentIndex, 1);
32 | }
33 | this.dependencies = [];
34 | this.error = null;
35 | if (this.definition instanceof Function) {
36 | dependencyStack.push(this);
37 | try {
38 | return this.value = this.definition();
39 | } catch (_error) {
40 | error = _error;
41 | this.error = error;
42 | throw error;
43 | } finally {
44 | dependencyStack.pop();
45 | }
46 | } else {
47 | return this.value = this.definition;
48 | }
49 | },
50 | read: function() {
51 | var dependent, signalError;
52 | dependent = dependencyStack[dependencyStack.length - 1];
53 | if (dependent != null) {
54 | if (__indexOf.call(this.dependents, dependent) < 0) {
55 | this.dependents.push(dependent);
56 | }
57 | if (__indexOf.call(dependent.dependencies, this) < 0) {
58 | dependent.dependencies.push(this);
59 | }
60 | }
61 | if (this.error) {
62 | signalError = new Error('Reading from corrupted Signal');
63 | throw signalError;
64 | } else {
65 | return this.value;
66 | }
67 | },
68 | write: function(newDefinition) {
69 | var dependencyQueue, dependent, error, errorList, errorMessage, observer, observerList, target, _i, _j, _len, _len1, _ref;
70 | this.definition = newDefinition;
71 | dependencyQueue = [this];
72 | observerList = [];
73 | errorList = [];
74 | while (dependencyQueue.length >= 1) {
75 | target = dependencyQueue.shift();
76 | try {
77 | target.update();
78 | } catch (_error) {
79 | error = _error;
80 | errorList.push(error);
81 | }
82 | _ref = target.dependents;
83 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
84 | dependent = _ref[_i];
85 | if (dependent.dependencyType === SIGNAL) {
86 | if (__indexOf.call(dependencyQueue, dependent) < 0) {
87 | dependencyQueue.push(dependent);
88 | }
89 | } else if (dependent.dependencyType === OBSERVER) {
90 | if (__indexOf.call(observerList, dependent) < 0) {
91 | observerList.push(dependent);
92 | }
93 | }
94 | }
95 | }
96 | for (_j = 0, _len1 = observerList.length; _j < _len1; _j++) {
97 | observer = observerList[_j];
98 | try {
99 | observer.update();
100 | } catch (_error) {
101 | error = _error;
102 | errorList.push(error);
103 | }
104 | }
105 | if (errorList.length === 1) {
106 | throw errorList[0];
107 | } else if (errorList.length > 1) {
108 | errorMessage = errorList.length + " errors due to signal write";
109 | throw new CompoundError(errorMessage, errorList);
110 | }
111 | return this.value;
112 | }
113 | };
114 | signalInterface = function(newDefinition) {
115 | if (arguments.length === 0) {
116 | return signalCore.read();
117 | } else {
118 | if (newDefinition instanceof Object) {
119 | signalInterface.set = function(key, value) {
120 | var output;
121 | output = newDefinition[key] = value;
122 | signalCore.write(newDefinition);
123 | return output;
124 | };
125 | } else {
126 | delete signalInterface.set;
127 | }
128 | if (newDefinition instanceof Array) {
129 | signalInterface.push = function() {
130 | var output;
131 | output = newDefinition.push.apply(newDefinition, arguments);
132 | signalCore.write(newDefinition);
133 | return output;
134 | };
135 | signalInterface.pop = function() {
136 | var output;
137 | output = newDefinition.pop.apply(newDefinition, arguments);
138 | signalCore.write(newDefinition);
139 | return output;
140 | };
141 | signalInterface.shift = function() {
142 | var output;
143 | output = newDefinition.shift.apply(newDefinition, arguments);
144 | signalCore.write(newDefinition);
145 | return output;
146 | };
147 | signalInterface.unshift = function() {
148 | var output;
149 | output = newDefinition.unshift.apply(newDefinition, arguments);
150 | signalCore.write(newDefinition);
151 | return output;
152 | };
153 | signalInterface.reverse = function() {
154 | var output;
155 | output = newDefinition.reverse.apply(newDefinition, arguments);
156 | signalCore.write(newDefinition);
157 | return output;
158 | };
159 | signalInterface.sort = function() {
160 | var output;
161 | output = newDefinition.sort.apply(newDefinition, arguments);
162 | signalCore.write(newDefinition);
163 | return output;
164 | };
165 | signalInterface.splice = function() {
166 | var output;
167 | output = newDefinition.splice.apply(newDefinition, arguments);
168 | signalCore.write(newDefinition);
169 | return output;
170 | };
171 | } else {
172 | delete signalInterface.push;
173 | delete signalInterface.pop;
174 | delete signalInterface.shift;
175 | delete signalInterface.unshift;
176 | delete signalInterface.reverse;
177 | delete signalInterface.sort;
178 | delete signalInterface.splice;
179 | }
180 | return signalCore.write(newDefinition);
181 | }
182 | };
183 | signalInterface(definition);
184 | return signalInterface;
185 | };
186 |
187 | global.Observer = function(definition) {
188 | var observerCore, observerInterface;
189 | observerCore = {
190 | dependencyType: OBSERVER,
191 | definition: null,
192 | dependencies: [],
193 | update: function() {
194 | var dependency, dependentIndex, _i, _len, _ref;
195 | _ref = this.dependencies;
196 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
197 | dependency = _ref[_i];
198 | dependentIndex = dependency.dependents.indexOf(this);
199 | dependency.dependents.splice(dependentIndex, 1);
200 | }
201 | this.dependencies = [];
202 | if (definition instanceof Function) {
203 | dependencyStack.push(this);
204 | try {
205 | return this.definition();
206 | } finally {
207 | dependencyStack.pop();
208 | }
209 | }
210 | },
211 | write: function(newdefinition) {
212 | this.definition = newdefinition;
213 | return this.update();
214 | }
215 | };
216 | observerInterface = function(newdefinition) {
217 | return write(newdefinition);
218 | };
219 | observerCore.write(definition);
220 | return observerInterface;
221 | };
222 |
223 | CompoundError = (function(_super) {
224 | __extends(CompoundError, _super);
225 |
226 | function CompoundError(message, errorArray) {
227 | var error, errorDescription, errorProperties, property, proxyError, _i, _j, _len, _len1, _ref, _ref1;
228 | this.errors = errorArray;
229 | _ref = this.errors;
230 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
231 | error = _ref[_i];
232 | errorDescription = (_ref1 = error.stack) != null ? _ref1 : error.toString();
233 | message = message + '\n' + errorDescription;
234 | }
235 | proxyError = Error.call(this, message);
236 | proxyError.name = "CompoundError";
237 | errorProperties = Object.getOwnPropertyNames(proxyError);
238 | for (_j = 0, _len1 = errorProperties.length; _j < _len1; _j++) {
239 | property = errorProperties[_j];
240 | if (proxyError.hasOwnProperty(property)) {
241 | this[property] = proxyError[property];
242 | }
243 | }
244 | return this;
245 | }
246 |
247 | return CompoundError;
248 |
249 | })(Error);
250 |
251 | global.CompoundError = CompoundError;
252 |
253 | }).call(this);
--------------------------------------------------------------------------------
/test/observable/root.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import { o, S, root } from 'sinuous/observable';
3 |
4 | test('allows subcomputations to escape their parents', function(t) {
5 | root(function() {
6 | var outerTrigger = o(null),
7 | innerTrigger = o(null),
8 | innerRuns = 0;
9 |
10 | S(function() {
11 | // register dependency to outer trigger
12 | outerTrigger();
13 | // inner computation
14 | root(function() {
15 | S(function() {
16 | // register dependency on inner trigger
17 | innerTrigger();
18 | // count total runs
19 | innerRuns++;
20 | });
21 | });
22 | });
23 |
24 | // at start, we have one inner computation, that's run once
25 | t.equal(innerRuns, 1);
26 |
27 | // trigger the outer computation, making more inners
28 | outerTrigger(null);
29 | outerTrigger(null);
30 |
31 | t.equal(innerRuns, 3);
32 |
33 | // now trigger inner signal: three orphaned computations should equal three runs
34 | innerRuns = 0;
35 | innerTrigger(null);
36 |
37 | t.equal(innerRuns, 3);
38 | t.end();
39 | });
40 | });
41 |
42 | //test("is necessary to create a toplevel computation", function () {
43 | // t.equal(() => {
44 | // S(() => 1)
45 | // }).toThrowError(/root/);
46 | //});
47 |
48 | test('does not freeze updates when used at top level', function(t) {
49 | root(() => {
50 | var s = o(1);
51 | var c = S(() => s());
52 |
53 | t.equal(c(), 1);
54 | s(2);
55 | t.equal(c(), 2);
56 | s(3);
57 | t.equal(c(), 3);
58 | t.end();
59 | });
60 | });
61 |
62 | test('persists through entire scope when used at top level', t => {
63 | root(() => {
64 | var s = o(1);
65 |
66 | S(() => s());
67 | s(2);
68 |
69 | var c2 = S(() => s());
70 | s(3);
71 |
72 | t.equal(c2(), 3);
73 | t.end();
74 | });
75 | });
76 |
--------------------------------------------------------------------------------
/test/observable/sample.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import { o, S, sample } from 'sinuous/observable';
3 |
4 | test('avoids a depdendency', function(t) {
5 | var a = o(1),
6 | b = o(2),
7 | c = o(3),
8 | d = 0;
9 |
10 | S(function() {
11 | d++;
12 | a();
13 | sample(b);
14 | c();
15 | });
16 |
17 | t.equal(d, 1);
18 |
19 | b(4);
20 |
21 | t.equal(d, 1);
22 |
23 | a(5);
24 | c(6);
25 |
26 | t.equal(d, 3);
27 | t.end();
28 | });
29 |
--------------------------------------------------------------------------------
/test/observable/transaction.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import { o, S, root, transaction } from 'sinuous/observable';
3 |
4 | test('batches all changes until end', function(t) {
5 | var d1 = o(9);
6 | var d2 = o(99);
7 |
8 | transaction(function() {
9 | d1(10);
10 | d2(100);
11 | t.equal(d1(), 9);
12 | t.equal(d2(), 99);
13 | });
14 |
15 | t.equal(d1(), 10);
16 | t.equal(d2(), 100);
17 | t.end();
18 | });
19 |
20 | test('halts propagation within its scope', function(t) {
21 | root(function() {
22 | var d1 = o(9);
23 | var d2 = o(99);
24 |
25 | var f = S(function() {
26 | return d1() + d2();
27 | });
28 |
29 | transaction(function() {
30 | d1(10);
31 | d2(100);
32 |
33 | t.equal(f(), 9 + 99);
34 | });
35 |
36 | t.equal(f(), 10 + 100);
37 | t.end();
38 | });
39 | });
40 |
41 | test('nested transaction', function(t) {
42 | var d = o(1);
43 |
44 | transaction(function() {
45 | d(2);
46 | t.equal(d(), 1);
47 |
48 | transaction(function() {
49 | d(3);
50 | t.equal(d(), 1);
51 |
52 | transaction(function() {
53 | d(4);
54 | });
55 |
56 | t.equal(d(), 1);
57 | });
58 |
59 | t.equal(d(), 1);
60 | });
61 |
62 | t.equal(d(), 4);
63 | t.end();
64 | });
65 |
--------------------------------------------------------------------------------
/test/observable/value.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import { o, S, root } from 'sinuous/observable';
3 |
4 |
5 | function value(current, eq) {
6 | const v = o(current);
7 | return function(update) {
8 | if (!arguments.length) return v();
9 | if (!(eq ? eq(update, current) : update === current)) {
10 | current = v(update);
11 | }
12 | return update;
13 | };
14 | }
15 |
16 | test("takes and returns an initial value", function (t) {
17 | t.equal(value(1)(), 1);
18 | t.end();
19 | });
20 |
21 | test("can be set by passing in a new value", function (t) {
22 | var d = value(1);
23 | d(2);
24 | t.equal(d(), 2);
25 | t.end();
26 | });
27 |
28 | test("returns value being set", function (t) {
29 | var d = value(1);
30 | t.equal(d(2), 2);
31 | t.end();
32 | });
33 |
34 | test("does not propagate if set to equal value", function (t) {
35 | root(function () {
36 | var d = value(1),
37 | e = 0,
38 | f = S(function () { d(); return ++e; });
39 |
40 | t.equal(f(), 1);
41 | d(1);
42 | t.equal(f(), 1);
43 | });
44 | t.end();
45 | });
46 |
47 | test("propagate if set to unequal value", function (t) {
48 | root(function () {
49 | var d = value(1),
50 | e = 0,
51 | f = S(function () { d(); return ++e; });
52 |
53 | t.equal(f(), 1);
54 | d(1);
55 | t.equal(f(), 1);
56 | d(2);
57 | t.equal(f(), 2);
58 | });
59 | t.end();
60 | });
61 |
62 | test("can take an equality predicate", function (t) {
63 | root(function () {
64 | var d = value([1], function (a, b) { return a[0] === b[0]; }),
65 | e = 0,
66 | f = S(function () { d(); return ++e; });
67 |
68 | t.equal(f(), 1);
69 | d([1]);
70 | t.equal(f(), 1);
71 | d([2]);
72 | t.equal(f(), 2);
73 | });
74 | t.end();
75 | });
76 |
--------------------------------------------------------------------------------
/test/sinuous.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import { o, html } from 'sinuous';
3 | import { subscribe } from 'sinuous/observable';
4 | import { map } from 'sinuous/map';
5 | import { fragInnerHTML } from './_utils.js';
6 |
7 | test('simple', function(t) {
8 | t.equal(
9 | html`
10 |
11 | `.outerHTML,
12 | ' '
13 | );
14 | t.equal(
15 | html`
16 | hello world
17 | `.outerHTML,
18 | 'hello world '
19 | );
20 | t.end();
21 | });
22 |
23 | test('returns a simple string', t => {
24 | const frag = html`
25 | a
26 | `;
27 | t.assert(frag instanceof DocumentFragment);
28 | t.assert(frag.childNodes[0] instanceof Text);
29 | t.equal(frag.childNodes[0].textContent, 'a');
30 | t.end();
31 | });
32 |
33 | test('returns a simple number', t => {
34 | const frag = html`
35 | ${9}
36 | `;
37 | t.assert(frag instanceof DocumentFragment);
38 | t.assert(frag.childNodes[0] instanceof Text);
39 | t.equal(frag.childNodes[0].textContent, '9');
40 | t.end();
41 | });
42 |
43 | test('returns a document fragment', t => {
44 | const frag = html`
45 | ${[
46 | html`
47 | Banana
48 | `,
49 | html`
50 | Apple
51 | `
52 | ]}
53 | `;
54 | t.assert(frag instanceof DocumentFragment);
55 | t.equal(frag.childNodes[0].outerHTML, 'Banana
');
56 | t.equal(frag.childNodes[1].outerHTML, 'Apple
');
57 | t.end();
58 | });
59 |
60 | test('returns a simple observable string', t => {
61 | const title = o('Banana');
62 | const frag = html`
63 | ${title}
64 | `;
65 | t.assert(frag instanceof DocumentFragment);
66 | t.assert(frag.childNodes[0] instanceof Text);
67 | t.equal(frag.childNodes[0].textContent, 'Banana');
68 | t.end();
69 | });
70 |
71 | test('component children order', t => {
72 | let order = '';
73 | const Comp = (props, ...children) => {
74 | order += 'a';
75 | return children;
76 | };
77 | const Child = () => {
78 | order += 'b';
79 | return html` `;
80 | };
81 |
82 | const result = html`
83 | <${Comp}>
84 | <${Child} />
85 | />
86 | `;
87 |
88 | t.equal(order, 'ab');
89 | t.equal(fragInnerHTML(result), ' ');
90 | t.end();
91 | });
92 |
93 | test('conditional lists without root', t => {
94 | const choice = o(1);
95 | const filler = o(0);
96 |
97 | const Spinner = () => html`
`;
98 |
99 | const Story = (index) => {
100 | const n1 = `a${index}`;
101 | const n2 = `b${index}`;
102 | const list = o();
103 |
104 | subscribe(() => {
105 | if (filler() === index) list([n1, n2]);
106 | });
107 |
108 | return html`${() => list() ? map(list, (item) => html`${item} `) : Spinner()}`;
109 | };
110 |
111 | const log = (el, ...args) => {
112 | console.warn(Array.from(el.childNodes)
113 | .map(c => `${c}${c.__g?','+c.__g:''}`).join(' — '), ...args);
114 | console.warn('');
115 | };
116 |
117 | const firstStory = Story(1);
118 |
119 | console.warn('raw story 1 element');
120 | log(firstStory);
121 | const stories = [firstStory, Story(2), Story(3)];
122 |
123 | const div = html`${() => stories[choice() - 1]}
`;
124 | document.body.appendChild(div);
125 | log(div);
126 |
127 | console.warn('story 1 - filler 1');
128 |
129 | filler(1);
130 | t.equal(div.children.length, 2);
131 | log(div);
132 |
133 | console.warn('story 2 - filler 2');
134 | choice(2);
135 | t.equal(div.children.length, 1);
136 | log(div);
137 |
138 | filler(2);
139 | t.equal(div.children[0].innerText, 'a2');
140 | t.equal(div.children.length, 2);
141 |
142 | t.end();
143 | });
144 |
145 | test('nested fragments without root', t => {
146 | const choice = o(0);
147 | const show = o(true);
148 | const show2 = o(true);
149 |
150 | const Story = (index) => {
151 | const n1 = `a${index}`;
152 | const n2 = `b${index}`;
153 | const list = o([n1, n2]);
154 | return html`${() => show() ? map(list, (item) => html`${item} `) : ''}`;
155 | };
156 |
157 | const firstStory = Story(1);
158 | const stories = [firstStory, Story(2), Story(3)];
159 |
160 | const div = html`${() => show2() && stories[choice()]}
`;
161 | document.body.appendChild(div);
162 |
163 |
164 | t.equal(div.children.length, 2);
165 | t.equal(div.children[0].innerText, 'a1');
166 |
167 | show(false);
168 | t.equal(div.children.length, 0);
169 |
170 | show(true);
171 | choice(1);
172 | t.equal(div.children[0].innerText, 'a2');
173 |
174 | t.equal(div.children.length, 2);
175 |
176 | show(false);
177 | t.equal(div.children.length, 0);
178 |
179 | show(true);
180 | choice(2);
181 |
182 | t.equal(div.children.length, 2);
183 |
184 | show2(false);
185 | t.equal(div.children.length, 0);
186 |
187 | choice(1);
188 | show(true);
189 | show2(true);
190 |
191 | t.equal(div.children.length, 2);
192 | t.equal(div.children[0].innerText, 'a2');
193 |
194 | t.end();
195 | });
196 |
--------------------------------------------------------------------------------
/test/template.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import spy from 'ispy';
3 | import { h, html } from 'sinuous';
4 | import { template, o, t } from 'sinuous/template';
5 | import { map } from 'sinuous/map';
6 | import { normalizeAttributes } from './_utils.js';
7 |
8 | test('tags return functions', function(tt) {
9 | tt.assert(typeof o() === 'function');
10 | tt.assert(typeof t() === 'function');
11 | tt.end();
12 | });
13 |
14 | test('template returns a function', function(tt) {
15 | tt.assert(typeof template(() => h('h1')) === 'function');
16 | tt.end();
17 | });
18 |
19 | test('template result returns an element', function(tt) {
20 | tt.equal(template(() => h('h1'))().firstChild.outerHTML, ' ');
21 | tt.end();
22 | });
23 |
24 | test('template result fills tags', function(tt) {
25 | tt.equal(
26 | template(() => h('h1', t('title')))({ title: 'Test' }).firstChild.outerHTML,
27 | 'Test '
28 | );
29 | tt.end();
30 | });
31 |
32 | test('template works w/ event listeners', function(tt) {
33 | const buttonClick = spy();
34 | const obj = { buttonClick };
35 | const btn = template(() =>
36 | h('button', { onclick: o('buttonClick') }, 'Click me')
37 | )(obj).firstChild;
38 |
39 | btn.click();
40 | tt.equal(buttonClick.callCount, 1, 'click called');
41 |
42 | obj.buttonClick = spy();
43 | btn.click();
44 | tt.equal(obj.buttonClick.callCount, 1, 'can change click handler via observable prop');
45 |
46 | tt.equal(buttonClick.callCount, 1, 'first handler is still clicked just once');
47 |
48 | tt.end();
49 | });
50 |
51 | test('template result fills observable tags', function(tt) {
52 | const obj = { title: 'Apple', class: 'juice' };
53 | const tmpl = template(() =>
54 | h('h1', h('span', { class: o('class') }, 'Pear'), h('span', o('title')))
55 | )(obj);
56 |
57 | tt.equal(
58 | tmpl.firstChild.children[0].outerHTML,
59 | 'Pear '
60 | );
61 | tt.equal(tmpl.firstChild.children[1].outerHTML, 'Apple ');
62 |
63 | obj.title = '⛄️';
64 | obj.class = 'mousse';
65 |
66 | tt.equal(obj.title, '⛄️');
67 | tt.equal(
68 | tmpl.firstChild.children[0].outerHTML,
69 | 'Pear '
70 | );
71 | tt.equal(tmpl.firstChild.children[1].outerHTML, '⛄️ ');
72 | tt.end();
73 | });
74 |
75 | test('template result fills tags w/ same value', function(tt) {
76 | const title = template(() => h('h1', t('title')));
77 | tt.equal(title({ title: 'Test' }).firstChild.outerHTML, 'Test ');
78 | tt.equal(title({ title: 'Test' }).firstChild.outerHTML, 'Test ');
79 | tt.end();
80 | });
81 |
82 | test('template result fills multiple observable tags w/ same key', function(tt) {
83 | const title = template(() =>
84 | h('h1', { class: o('title') }, h('b', o('title')), h('i', o('title')))
85 | );
86 | const obj = {
87 | title: ''
88 | };
89 |
90 | const rendered = title(obj);
91 | obj.title = 'banana';
92 |
93 | tt.equal(
94 | rendered.firstChild.outerHTML,
95 | 'banana banana '
96 | );
97 |
98 | tt.end();
99 | });
100 |
101 | test('template works with map', function(tt) {
102 | const Row = template(
103 | () => html`
104 |
105 | ${t('id')}
106 | ${o('label')}
107 |
108 |
109 |
113 |
114 |
115 |
116 |
117 | `
118 | );
119 |
120 | const rows = () =>
121 | [1, 2].map(id => ({
122 | id,
123 | label: `Label ${id}`
124 | }));
125 |
126 | const table = document.createElement('table');
127 | table.appendChild(map(rows, Row));
128 |
129 | tt.equal(
130 | normalizeAttributes(table.innerHTML),
131 | normalizeAttributes(
132 | `
133 | 1
134 | Label 1
135 |
136 |
137 |
138 |
139 |
140 |
141 | 2
142 | Label 2
143 |
144 |
145 |
146 |
147 | `.replace(/>[\s]+<')
148 | )
149 | );
150 |
151 | tt.end();
152 | });
153 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | import './_polyfills.js';
2 | import './h/add-node.js';
3 | import './h/hyperscript.js';
4 | import './h/svg.js';
5 | import './h/insert.js';
6 | import './h/insert-bugs.js';
7 | import './h/insert-markers.js';
8 | import './h/utils.js';
9 | import './observable/child.js';
10 | import './observable/observable.js';
11 | import './observable/S.js';
12 | import './observable/sample.js';
13 | import './observable/root.js';
14 | import './observable/dispose.js';
15 | import './observable/transaction.js';
16 | import './observable/on.js';
17 | import './observable/value.js';
18 | import './map/map.js';
19 | import './map/map-basic.js';
20 | import './map/map-fragments.js';
21 | import './map/map-objects.js';
22 | import './map/dispose.js';
23 | import './hydrate/hydrate.js';
24 | import './hydrate/svg.js';
25 | import './hydrate/selector.js';
26 | import './template.js';
27 | import './sinuous.js';
28 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "noEmit": true,
7 | "strict": true,
8 | "allowJs": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------