Skip to content

[WIP] Spread attributes and properties #1248

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 1, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/generators/Generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,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) {
Expand Down
22 changes: 19 additions & 3 deletions src/generators/nodes/Component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 :
Expand Down Expand Up @@ -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()`);
Expand Down Expand Up @@ -224,7 +229,7 @@ export default class Component extends Node {
}
});

componentInitProperties.push(`data: ${name_initial_data}`);
componentInitialData = name_initial_data;

const initialisers = [
'state = #component.get()',
Expand All @@ -248,10 +253,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 && {
Expand Down Expand Up @@ -551,4 +567,4 @@ function isComputed(node: Node) {
}

return false;
}
}
8 changes: 8 additions & 0 deletions src/generators/nodes/Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,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, '_')
);
Expand Down Expand Up @@ -240,6 +244,10 @@ export default class Element extends Node {
attribute.render(block);
});

if (this.spread) {
this.spread.renderForElement(block);
}

// event handlers
let eventHandlerUsesComponent = false;

Expand Down
200 changes: 200 additions & 0 deletions src/generators/nodes/Spread.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
4 changes: 3 additions & 1 deletion src/generators/nodes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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';
Expand All @@ -42,11 +43,12 @@ const nodes: Record<string, any> = {
RawMustacheTag,
Ref,
Slot,
Spread,
Text,
ThenBlock,
Title,
Transition,
Window
};

export default nodes;
export default nodes;
26 changes: 26 additions & 0 deletions src/parse/state/tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ export default function tag(parser: Parser) {
parser.allowWhitespace();
}

element.spread = readSpread(parser);

const uniqueNames = new Set();

let attribute;
Expand Down Expand Up @@ -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;
}
6 changes: 5 additions & 1 deletion src/shared/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -177,4 +181,4 @@ export function selectMultipleValue(select) {
return [].map.call(select.querySelectorAll(':checked'), function(option) {
return option.__value;
});
}
}
1 change: 1 addition & 0 deletions test/parser/samples/spread/input.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div {{...props}}></div>
Loading