merge master -> mrkishi-spreadh

pull/1289/head
Rich-Harris 7 years ago
commit d0c696bb2b

@ -845,6 +845,10 @@ export default class Generator {
if (node.type === 'Component' && node.name === ':Component') {
node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
}
if (node.type === 'Spread') {
node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
}
},
leave(node: Node, parent: Node) {

@ -48,6 +48,10 @@ export default class Component extends Node {
}
});
if (this.spread) {
block.addDependencies(this.spread.metadata.dependencies);
}
this.var = block.getUniqueName(
(
this.name === ':Self' ? this.generator.name :
@ -76,6 +80,7 @@ export default class Component extends Node {
const name = this.var;
const componentInitProperties = [`root: #component.root`];
let componentInitialData = null;
if (this.children.length > 0) {
const slots = Array.from(this._slots).map(name => `${quoteIfNecessary(name, generator.legacy)}: @createFragment()`);
@ -225,7 +230,7 @@ export default class Component extends Node {
}
});
componentInitProperties.push(`data: ${name_initial_data}`);
componentInitialData = name_initial_data;
const initialisers = [
'state = #component.get()',
@ -249,10 +254,21 @@ export default class Component extends Node {
});
`;
} else if (initialProps.length) {
componentInitProperties.push(`data: ${initialPropString}`);
componentInitialData = initialPropString;
}
}
if (this.spread) {
const initialData = this.spread.renderForComponent(block, updates);
componentInitialData = componentInitialData ?
`@assign({}, ${initialData}, ${componentInitialData})` :
initialData;
}
if (componentInitialData) {
componentInitProperties.push(`data: ${componentInitialData}`);
}
const isDynamicComponent = this.name === ':Component';
const switch_vars = isDynamicComponent && {
@ -555,4 +571,4 @@ function isComputed(node: Node) {
}
return false;
}
}

@ -141,6 +141,10 @@ export default class Element extends Node {
component._slots.add(slot);
}
if (this.spread) {
block.addDependencies(this.spread.metadata.dependencies);
}
this.var = block.getUniqueName(
this.name.replace(/[^a-zA-Z0-9_$]/g, '_')
);
@ -244,6 +248,10 @@ export default class Element extends Node {
this.addTransitions(block);
this.addActions(block);
if (this.spread) {
this.spread.renderForElement(block);
}
if (allUsedContexts.size || eventHandlerUsesComponent) {
const initialProps: string[] = [];
const updates: string[] = [];

@ -0,0 +1,200 @@
import deindent from '../../utils/deindent';
import { DomGenerator } from '../dom/index';
import Node from './shared/Node';
import Element from './Element';
import Block from '../dom/Block';
export default class Spread {
type: 'Spread';
start: number;
end: number;
generator: DomGenerator;
parent: Element;
expression: Node;
metadata: {
dependencies: string[];
snippet: string;
};
constructor({
generator,
expression,
parent
}: {
generator: DomGenerator,
expression: Node,
parent: Element
}) {
this.type = 'Spread';
this.generator = generator;
this.parent = parent;
this.expression = expression;
}
renderForElement(block: Block) {
const node = this.parent;
const { expression } = this;
const { indexes } = block.contextualise(expression);
const { dependencies, snippet } = this.metadata;
const value = snippet;
const hasChangeableIndex = Array.from(indexes).some(index => block.changeableIndexes.get(index));
const shouldCache = (
expression.type !== 'Identifier' ||
block.contexts.has(expression.name) ||
hasChangeableIndex
);
const last = shouldCache && block.getUniqueName(`${node.var}_spread_value`);
if (shouldCache) block.addVariable(last);
const init = shouldCache ? `${last} = ${value}` : value;
const activeKeys = block.getUniqueName(`${node.var}_spread_keys`);
block.addVariable(activeKeys, '{}');
const changes = block.getUniqueName(`${node.var}_spread_changes`);
const hasNamedAttributes = node.attributes.length;
const namedAttributes = block.getUniqueName(`${node.var}_attributes`);
if (hasNamedAttributes) {
block.builders.init.addBlock(deindent`
var ${namedAttributes} = [${node.attributes.map(attr => `'${attr.name}'`).join(', ')}];
`)
}
block.builders.hydrate.addBlock(deindent`
var ${changes} = ${init};
for (var key in ${changes}) {
${hasNamedAttributes ? `if (${namedAttributes}.indexOf(key) !== -1) continue;` : ''}
@setAttribute(${node.var}, key, ${changes}[key]);
${activeKeys}[key] = true;
}
`);
if (dependencies.length || hasChangeableIndex) {
const changedCheck = (
( block.hasOutroMethod ? `#outroing || ` : '' ) +
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
);
const updateCachedValue = `${last} !== (${last} = ${value})`;
const condition = shouldCache ?
( dependencies.length ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue ) :
changedCheck;
const oldKeys = block.getUniqueName(`${node.var}_spread_keys_old`);
const updater = deindent`
var ${oldKeys} = ${activeKeys};
${activeKeys} = {};
var ${changes} = ${shouldCache ? last : value};
for (var key in ${changes}) {
${hasNamedAttributes ? `if (${namedAttributes}.indexOf(key) !== -1) continue;` : ''}
@setAttribute(${node.var}, key, ${changes}[key]);
${activeKeys}[key] = true;
delete ${oldKeys}[key];
}
for (var key in ${oldKeys}) {
@removeAttribute(${node.var}, key);
}
`;
block.builders.update.addConditional(
condition,
updater
);
}
}
renderForComponent(block: Block, updates: string[]) {
const node = this.parent;
const { expression } = this;
const { indexes } = block.contextualise(expression);
const { dependencies, snippet } = this.metadata;
const value = snippet;
const hasChangeableIndex = Array.from(indexes).some(index => block.changeableIndexes.get(index));
const shouldCache = (
expression.type !== 'Identifier' ||
block.contexts.has(expression.name) ||
hasChangeableIndex
);
const last = shouldCache && block.getUniqueName(`${node.var}_spread_value`);
if (shouldCache) block.addVariable(last);
const init = shouldCache ? `${last} = ${value}` : value;
const activeKeys = block.getUniqueName(`${node.var}_spread_keys`);
block.addVariable(activeKeys, '{}');
const changes = block.getUniqueName(`${node.var}_spread_changes`);
const hasNamedAttributes = node.attributes.length;
const namedAttributes = block.getUniqueName(`${node.var}_attributes`);
if (hasNamedAttributes) {
block.builders.init.addBlock(deindent`
var ${namedAttributes} = [${node.attributes.map(attr => `'${attr.name}'`).join(', ')}];
`)
}
if (dependencies.length || hasChangeableIndex) {
const changedCheck = (
( block.hasOutroMethod ? `#outroing || ` : '' ) +
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
);
const updateCachedValue = `${last} !== (${last} = ${value})`;
const condition = shouldCache ?
( dependencies.length ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue ) :
changedCheck;
const oldKeys = block.getUniqueName(`${node.var}_spread_keys_old`);
updates.push(deindent`
if (${condition}) {
var ${oldKeys} = ${activeKeys};
${activeKeys} = {};
var ${changes} = ${shouldCache ? last : value};
for (var key in ${changes}) {
${hasNamedAttributes ? `if (${namedAttributes}.indexOf(key) !== -1) continue;` : ''}
${node.var}_changes[key] = ${changes}[key];
${activeKeys}[key] = true;
delete ${oldKeys}[key];
}
for (var key in ${oldKeys}) {
${node.var}_changes[key] = undefined;
}
}
`);
}
return value;
}
}

@ -18,6 +18,7 @@ import PendingBlock from './PendingBlock';
import RawMustacheTag from './RawMustacheTag';
import Ref from './Ref';
import Slot from './Slot';
import Spread from './Spread';
import Text from './Text';
import ThenBlock from './ThenBlock';
import Title from './Title';
@ -44,6 +45,7 @@ const nodes: Record<string, any> = {
RawMustacheTag,
Ref,
Slot,
Spread,
Text,
ThenBlock,
Title,
@ -51,4 +53,4 @@ const nodes: Record<string, any> = {
Window
};
export default nodes;
export default nodes;

@ -178,6 +178,8 @@ export default function tag(parser: Parser) {
parser.allowWhitespace();
}
element.spread = readSpread(parser);
const uniqueNames = new Set();
let attribute;
@ -384,3 +386,27 @@ function readSequence(parser: Parser, done: () => boolean) {
parser.error(`Unexpected end of input`);
}
function readSpread(parser: Parser) {
const start = parser.index;
if (parser.eat('{{...')) {
const expression = readExpression(parser);
parser.allowWhitespace();
if (!parser.eat('}}')) {
parser.error(`Expected }}`);
}
parser.allowWhitespace();
return {
start,
end: parser.index,
type: 'Spread',
expression,
};
}
return null;
}

@ -85,6 +85,10 @@ export function setAttribute(node, attribute, value) {
node.setAttribute(attribute, value);
}
export function removeAttribute(node, attribute) {
node.removeAttribute(attribute);
}
export function setXlinkAttribute(node, attribute, value) {
node.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value);
}
@ -177,4 +181,4 @@ export function selectMultipleValue(select) {
return [].map.call(select.querySelectorAll(':checked'), function(option) {
return option.__value;
});
}
}

@ -0,0 +1 @@
<div {{...props}}></div>

@ -0,0 +1,31 @@
{
"hash": "phg0l6",
"html": {
"start": 0,
"end": 24,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 24,
"type": "Element",
"name": "div",
"attributes": [],
"children": [],
"spread": {
"start": 5,
"end": 17,
"type": "Spread",
"expression": {
"type": "Identifier",
"start": 10,
"end": 15,
"name": "props"
}
}
}
]
},
"css": null,
"js": null
}

@ -0,0 +1,4 @@
<p>foo: {{foo}}</p>
<p>baz: {{baz}} ({{typeof baz}})</p>
<p>qux: {{qux}}</p>
<p>quux: {{quux}}</p>

@ -0,0 +1,27 @@
export default {
solo: true,
data: {
props: {
foo: 'lol',
baz: 40 + 2,
qux: `this is a ${'piece of'} string`,
quux: 'core'
}
},
html: `<div><p>foo: lol</p>\n<p>baz: 42 (number)</p>\n<p>qux: named</p>\n<p>quux: core</p></div>`,
test ( assert, component, target ) {
component.set({
props: {
foo: 'wut',
baz: 40 + 3,
qux: `this is a ${'rather boring'} string`,
quux: 'heart'
}
});
assert.equal( target.innerHTML, `<div><p>foo: wut</p>\n<p>baz: 43 (number)</p>\n<p>qux: named</p>\n<p>quux: heart</p></div>` );
}
};

@ -0,0 +1,11 @@
<div>
<Widget {{...props}} qux="named"/>
</div>
<script>
import Widget from './Widget.html';
export default {
components: { Widget }
};
</script>

@ -0,0 +1,21 @@
export default {
solo: true,
html: `<div data-named="value" data-foo="bar">red</div>`,
test ( assert, component, target ) {
const div = target.querySelector( 'div' );
assert.equal( div.dataset.foo, 'bar' );
assert.equal( div.dataset.named, 'value' );
component.set({ color: 'blue', props: { 'data-foo': 'baz', 'data-named': 'qux' } });
assert.equal( target.innerHTML, `<div data-named="value" data-foo="baz">blue</div>` );
assert.equal( div.dataset.foo, 'baz' );
assert.equal( div.dataset.named, 'value' );
component.set({ color: 'blue', props: {} });
assert.equal( target.innerHTML, `<div data-named="value">blue</div>` );
assert.equal( div.dataset.foo, undefined );
}
};

@ -0,0 +1,13 @@
<div {{...props}} data-named="value">{{color}}</div>
<script>
export default {
data: () => ({
color: 'red',
props: {
'data-foo': 'bar',
'data-named': 'qux'
}
})
};
</script>
Loading…
Cancel
Save