Skip to content

Commit b5102f4

Browse files
committed
Add spread -- rough idea
1 parent b3fa965 commit b5102f4

File tree

14 files changed

+343
-5
lines changed

14 files changed

+343
-5
lines changed

src/generators/Generator.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,10 @@ export default class Generator {
827827
if (node.type === 'Component' && node.name === ':Component') {
828828
node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
829829
}
830+
831+
if (node.type === 'Spread') {
832+
node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
833+
}
830834
},
831835

832836
leave(node: Node, parent: Node) {

src/generators/nodes/Component.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ export default class Component extends Node {
4848
}
4949
});
5050

51+
if (this.spread) {
52+
block.addDependencies(this.spread.metadata.dependencies);
53+
}
54+
5155
this.var = block.getUniqueName(
5256
(
5357
this.name === ':Self' ? this.generator.name :
@@ -76,6 +80,7 @@ export default class Component extends Node {
7680
const name = this.var;
7781

7882
const componentInitProperties = [`root: #component.root`];
83+
let componentInitialData = null;
7984

8085
if (this.children.length > 0) {
8186
const slots = Array.from(this._slots).map(name => `${quoteIfNecessary(name, generator.legacy)}: @createFragment()`);
@@ -224,7 +229,7 @@ export default class Component extends Node {
224229
}
225230
});
226231

227-
componentInitProperties.push(`data: ${name_initial_data}`);
232+
componentInitialData = name_initial_data;
228233

229234
const initialisers = [
230235
'state = #component.get()',
@@ -248,10 +253,21 @@ export default class Component extends Node {
248253
});
249254
`;
250255
} else if (initialProps.length) {
251-
componentInitProperties.push(`data: ${initialPropString}`);
256+
componentInitialData = initialPropString;
252257
}
253258
}
254259

260+
if (this.spread) {
261+
const initialData = this.spread.renderForComponent(block, updates);
262+
componentInitialData = componentInitialData ?
263+
`@assign({}, ${initialData}, ${componentInitialData})` :
264+
initialData;
265+
}
266+
267+
if (componentInitialData) {
268+
componentInitProperties.push(`data: ${componentInitialData}`);
269+
}
270+
255271
const isDynamicComponent = this.name === ':Component';
256272

257273
const switch_vars = isDynamicComponent && {
@@ -551,4 +567,4 @@ function isComputed(node: Node) {
551567
}
552568

553569
return false;
554-
}
570+
}

src/generators/nodes/Element.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,10 @@ export default class Element extends Node {
138138
component._slots.add(slot);
139139
}
140140

141+
if (this.spread) {
142+
block.addDependencies(this.spread.metadata.dependencies);
143+
}
144+
141145
this.var = block.getUniqueName(
142146
this.name.replace(/[^a-zA-Z0-9_$]/g, '_')
143147
);
@@ -240,6 +244,10 @@ export default class Element extends Node {
240244
attribute.render(block);
241245
});
242246

247+
if (this.spread) {
248+
this.spread.renderForElement(block);
249+
}
250+
243251
// event handlers
244252
let eventHandlerUsesComponent = false;
245253

src/generators/nodes/Spread.ts

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import deindent from '../../utils/deindent';
2+
import { DomGenerator } from '../dom/index';
3+
import Node from './shared/Node';
4+
import Element from './Element';
5+
import Block from '../dom/Block';
6+
7+
export default class Spread {
8+
type: 'Spread';
9+
start: number;
10+
end: number;
11+
12+
generator: DomGenerator;
13+
parent: Element;
14+
expression: Node;
15+
16+
metadata: {
17+
dependencies: string[];
18+
snippet: string;
19+
};
20+
21+
constructor({
22+
generator,
23+
expression,
24+
parent
25+
}: {
26+
generator: DomGenerator,
27+
expression: Node,
28+
parent: Element
29+
}) {
30+
this.type = 'Spread';
31+
this.generator = generator;
32+
this.parent = parent;
33+
34+
this.expression = expression;
35+
}
36+
37+
renderForElement(block: Block) {
38+
const node = this.parent;
39+
40+
const { expression } = this;
41+
const { indexes } = block.contextualise(expression);
42+
const { dependencies, snippet } = this.metadata;
43+
44+
const value = snippet;
45+
46+
const hasChangeableIndex = Array.from(indexes).some(index => block.changeableIndexes.get(index));
47+
48+
const shouldCache = (
49+
expression.type !== 'Identifier' ||
50+
block.contexts.has(expression.name) ||
51+
hasChangeableIndex
52+
);
53+
54+
const last = shouldCache && block.getUniqueName(`${node.var}_spread_value`);
55+
56+
if (shouldCache) block.addVariable(last);
57+
58+
const init = shouldCache ? `${last} = ${value}` : value;
59+
60+
const activeKeys = block.getUniqueName(`${node.var}_spread_keys`);
61+
block.addVariable(activeKeys, '{}');
62+
63+
const changes = block.getUniqueName(`${node.var}_spread_changes`);
64+
65+
block.builders.hydrate.addBlock(deindent`
66+
var ${changes} = ${init};
67+
for (var key in ${changes}) {
68+
@setAttribute(${node.var}, key, ${changes}[key]);
69+
${activeKeys}[key] = true;
70+
}
71+
`);
72+
73+
if (dependencies.length || hasChangeableIndex) {
74+
const changedCheck = (
75+
( block.hasOutroMethod ? `#outroing || ` : '' ) +
76+
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
77+
);
78+
79+
const updateCachedValue = `${last} !== (${last} = ${value})`;
80+
81+
const condition = shouldCache ?
82+
( dependencies.length ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue ) :
83+
changedCheck;
84+
85+
const oldKeys = block.getUniqueName(`${node.var}_spread_keys_old`);
86+
87+
const updater = deindent`
88+
var ${oldKeys} = ${activeKeys};
89+
${activeKeys} = {};
90+
91+
var ${changes} = ${shouldCache ? last : value};
92+
for (var key in ${changes}) {
93+
${activeKeys}[key] = true;
94+
delete ${oldKeys}[key];
95+
@setAttribute(${node.var}, key, ${changes}[key]);
96+
}
97+
98+
for (var key in ${oldKeys}) {
99+
@removeAttribute(${node.var}, key);
100+
}
101+
`;
102+
103+
block.builders.update.addConditional(
104+
condition,
105+
updater
106+
);
107+
}
108+
}
109+
110+
renderForComponent(block: Block, updates: string[]) {
111+
const node = this.parent;
112+
113+
const { expression } = this;
114+
const { indexes } = block.contextualise(expression);
115+
const { dependencies, snippet } = this.metadata;
116+
117+
const value = snippet;
118+
119+
const hasChangeableIndex = Array.from(indexes).some(index => block.changeableIndexes.get(index));
120+
121+
const shouldCache = (
122+
expression.type !== 'Identifier' ||
123+
block.contexts.has(expression.name) ||
124+
hasChangeableIndex
125+
);
126+
127+
const last = shouldCache && block.getUniqueName(`${node.var}_spread_value`);
128+
129+
if (shouldCache) block.addVariable(last);
130+
131+
const init = shouldCache ? `${last} = ${value}` : value;
132+
133+
const activeKeys = block.getUniqueName(`${node.var}_spread_keys`);
134+
block.addVariable(activeKeys, '{}');
135+
136+
const changes = block.getUniqueName(`${node.var}_spread_changes`);
137+
138+
if (dependencies.length || hasChangeableIndex) {
139+
const changedCheck = (
140+
( block.hasOutroMethod ? `#outroing || ` : '' ) +
141+
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
142+
);
143+
144+
const updateCachedValue = `${last} !== (${last} = ${value})`;
145+
146+
const condition = shouldCache ?
147+
( dependencies.length ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue ) :
148+
changedCheck;
149+
150+
const oldKeys = block.getUniqueName(`${node.var}_spread_keys_old`);
151+
152+
updates.push(deindent`
153+
if (${condition}) {
154+
var ${oldKeys} = ${activeKeys};
155+
${activeKeys} = {};
156+
157+
var ${changes} = ${shouldCache ? last : value};
158+
for (var key in ${changes}) {
159+
${activeKeys}[key] = true;
160+
delete ${oldKeys}[key];
161+
${node.var}_changes[key] = ${changes}[key];
162+
}
163+
164+
for (var key in ${oldKeys}) {
165+
${node.var}_changes[key] = undefined;
166+
}
167+
}
168+
`);
169+
}
170+
171+
return value;
172+
}
173+
}

src/generators/nodes/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import PendingBlock from './PendingBlock';
1717
import RawMustacheTag from './RawMustacheTag';
1818
import Ref from './Ref';
1919
import Slot from './Slot';
20+
import Spread from './Spread';
2021
import Text from './Text';
2122
import ThenBlock from './ThenBlock';
2223
import Title from './Title';
@@ -42,11 +43,12 @@ const nodes: Record<string, any> = {
4243
RawMustacheTag,
4344
Ref,
4445
Slot,
46+
Spread,
4547
Text,
4648
ThenBlock,
4749
Title,
4850
Transition,
4951
Window
5052
};
5153

52-
export default nodes;
54+
export default nodes;

src/parse/state/tag.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ export default function tag(parser: Parser) {
178178
parser.allowWhitespace();
179179
}
180180

181+
element.spread = readSpread(parser);
182+
181183
const uniqueNames = new Set();
182184

183185
let attribute;
@@ -384,3 +386,27 @@ function readSequence(parser: Parser, done: () => boolean) {
384386

385387
parser.error(`Unexpected end of input`);
386388
}
389+
390+
function readSpread(parser: Parser) {
391+
const start = parser.index;
392+
393+
if (parser.eat('{{...')) {
394+
const expression = readExpression(parser);
395+
parser.allowWhitespace();
396+
397+
if (!parser.eat('}}')) {
398+
parser.error(`Expected }}`);
399+
}
400+
401+
parser.allowWhitespace();
402+
403+
return {
404+
start,
405+
end: parser.index,
406+
type: 'Spread',
407+
expression,
408+
};
409+
}
410+
411+
return null;
412+
}

src/shared/dom.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ export function setAttribute(node, attribute, value) {
8585
node.setAttribute(attribute, value);
8686
}
8787

88+
export function removeAttribute(node, attribute) {
89+
node.removeAttribute(attribute);
90+
}
91+
8892
export function setXlinkAttribute(node, attribute, value) {
8993
node.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value);
9094
}
@@ -177,4 +181,4 @@ export function selectMultipleValue(select) {
177181
return [].map.call(select.querySelectorAll(':checked'), function(option) {
178182
return option.__value;
179183
});
180-
}
184+
}

test/parser/samples/spread/input.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div {{...props}}></div>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"hash": "phg0l6",
3+
"html": {
4+
"start": 0,
5+
"end": 24,
6+
"type": "Fragment",
7+
"children": [
8+
{
9+
"start": 0,
10+
"end": 24,
11+
"type": "Element",
12+
"name": "div",
13+
"attributes": [],
14+
"children": [],
15+
"spread": {
16+
"start": 5,
17+
"end": 17,
18+
"type": "Spread",
19+
"expression": {
20+
"type": "Identifier",
21+
"start": 10,
22+
"end": 15,
23+
"name": "props"
24+
}
25+
}
26+
}
27+
]
28+
},
29+
"css": null,
30+
"js": null
31+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<p>foo: {{foo}}</p>
2+
<p>baz: {{baz}} ({{typeof baz}})</p>
3+
<p>qux: {{qux}}</p>
4+
<p>quux: {{quux}}</p>

0 commit comments

Comments
 (0)