diff --git a/src/compile/nodes/Binding.ts b/src/compile/nodes/Binding.ts index 9f3dab38e7..49b7204c0e 100644 --- a/src/compile/nodes/Binding.ts +++ b/src/compile/nodes/Binding.ts @@ -8,13 +8,6 @@ import Block from '../render-dom/Block'; import Expression from './shared/Expression'; import { dimensions } from '../../utils/patterns'; -const readOnlyMediaAttributes = new Set([ - 'duration', - 'buffered', - 'seekable', - 'played' -]); - // TODO a lot of this element-specific stuff should live in Element — // Binding should ideally be agnostic between Element and InlineComponent @@ -54,263 +47,4 @@ export default class Binding extends Node { this.obj = obj; this.prop = prop; } - - munge( - block: Block - ) { - const node: Element = this.parent; - - const needsLock = node.name !== 'input' || !/radio|checkbox|range|color/.test(node.getStaticAttributeValue('type')); - const isReadOnly = ( - (node.isMediaNode() && readOnlyMediaAttributes.has(this.name)) || - dimensions.test(this.name) - ); - - let updateConditions: string[] = []; - - const { name } = getObject(this.value.node); - const { snippet } = this.value; - - // special case: if you have e.g. `` - // and `selected` is an object chosen with a - if (binding.name === 'group') { - const bindingGroup = getBindingGroup(component, binding.value.node); - if (type === 'checkbox') { - return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`; - } - - return `${node.var}.__value`; - } - - // - if (type === 'range' || type === 'number') { - return `@toNumber(${node.var}.${binding.name})`; - } - - if ((binding.name === 'buffered' || binding.name === 'seekable' || binding.name === 'played')) { - return `@timeRangesToArray(${node.var}.${binding.name})` - } - - // everything else - return `${node.var}.${binding.name}`; -} - -function isComputed(node: Node) { - while (node.type === 'MemberExpression') { - if (node.computed) return true; - node = node.object; - } - - return false; } diff --git a/src/compile/nodes/Element.ts b/src/compile/nodes/Element.ts index 5428f7818e..647222d0c5 100644 --- a/src/compile/nodes/Element.ts +++ b/src/compile/nodes/Element.ts @@ -683,70 +683,6 @@ function getRenderStatement( return `@createElement("${name}")`; } -const events = [ - { - eventNames: ['input'], - filter: (node: Element, name: string) => - node.name === 'textarea' || - node.name === 'input' && !/radio|checkbox|range/.test(node.getStaticAttributeValue('type')) - }, - { - eventNames: ['change'], - filter: (node: Element, name: string) => - node.name === 'select' || - node.name === 'input' && /radio|checkbox/.test(node.getStaticAttributeValue('type')) - }, - { - eventNames: ['change', 'input'], - filter: (node: Element, name: string) => - node.name === 'input' && node.getStaticAttributeValue('type') === 'range' - }, - - { - eventNames: ['resize'], - filter: (node: Element, name: string) => - dimensions.test(name) - }, - - // media events - { - eventNames: ['timeupdate'], - filter: (node: Element, name: string) => - node.isMediaNode() && - (name === 'currentTime' || name === 'played') - }, - { - eventNames: ['durationchange'], - filter: (node: Element, name: string) => - node.isMediaNode() && - name === 'duration' - }, - { - eventNames: ['play', 'pause'], - filter: (node: Element, name: string) => - node.isMediaNode() && - name === 'paused' - }, - { - eventNames: ['progress'], - filter: (node: Element, name: string) => - node.isMediaNode() && - name === 'buffered' - }, - { - eventNames: ['loadedmetadata'], - filter: (node: Element, name: string) => - node.isMediaNode() && - (name === 'buffered' || name === 'seekable') - }, - { - eventNames: ['volumechange'], - filter: (node: Element, name: string) => - node.isMediaNode() && - name === 'volume' - } -]; - function shouldHaveAttribute( node, attributes: string[], diff --git a/src/compile/nodes/InlineComponent.ts b/src/compile/nodes/InlineComponent.ts index c91dd76196..a54ef71f2f 100644 --- a/src/compile/nodes/InlineComponent.ts +++ b/src/compile/nodes/InlineComponent.ts @@ -98,456 +98,4 @@ export default class InlineComponent extends Node { this.children = mapChildren(component, this, scope, info.children); } - - init( - block: Block, - stripWhitespace: boolean, - nextSibling: Node - ) { - this.cannotUseInnerHTML(); - - if (this.expression) { - block.addDependencies(this.expression.dependencies); - } - - this.attributes.forEach(attr => { - block.addDependencies(attr.dependencies); - }); - - this.bindings.forEach(binding => { - block.addDependencies(binding.value.dependencies); - }); - - this.handlers.forEach(handler => { - block.addDependencies(handler.dependencies); - }); - - this.var = block.getUniqueName( - ( - this.name === 'svelte:self' ? this.component.name : - this.name === 'svelte:component' ? 'switch_instance' : - this.name - ).toLowerCase() - ); - - if (this.children.length) { - this._slots = new Set(['default']); - - this.children.forEach(child => { - child.init(block, stripWhitespace, nextSibling); - }); - } - - if (this.component.options.nestedTransitions) { - block.addOutro(); - } - } - - build( - block: Block, - parentNode: string, - parentNodes: string - ) { - const { component } = this; - - const name = this.var; - - const componentInitProperties = [`root: #component.root`, `store: #component.store`]; - - if (this.children.length > 0) { - const slots = Array.from(this._slots).map(name => `${quoteNameIfNecessary(name)}: @createFragment()`); - componentInitProperties.push(`slots: { ${slots.join(', ')} }`); - - this.children.forEach((child: Node) => { - child.build(block, `${this.var}._slotted.default`, 'nodes'); - }); - } - - const statements: string[] = []; - - const name_initial_data = block.getUniqueName(`${name}_initial_data`); - const name_changes = block.getUniqueName(`${name}_changes`); - let name_updating: string; - let beforecreate: string = null; - - const updates: string[] = []; - - const usesSpread = !!this.attributes.find(a => a.isSpread); - - const attributeObject = usesSpread - ? '{}' - : stringifyProps( - this.attributes.map(attr => `${quoteNameIfNecessary(attr.name)}: ${attr.getValue()}`) - ); - - if (this.attributes.length || this.bindings.length) { - componentInitProperties.push(`data: ${name_initial_data}`); - } - - if (!usesSpread && (this.attributes.filter(a => a.isDynamic).length || this.bindings.length)) { - updates.push(`var ${name_changes} = {};`); - } - - if (this.attributes.length) { - if (usesSpread) { - const levels = block.getUniqueName(`${this.var}_spread_levels`); - - const initialProps = []; - const changes = []; - - const allDependencies = new Set(); - - this.attributes.forEach(attr => { - addToSet(allDependencies, attr.dependencies); - }); - - this.attributes.forEach(attr => { - const { name, dependencies } = attr; - - const condition = dependencies.size > 0 && (dependencies.size !== allDependencies.size) - ? `(${[...dependencies].map(d => `changed.${d}`).join(' || ')})` - : null; - - if (attr.isSpread) { - const value = attr.expression.snippet; - initialProps.push(value); - - changes.push(condition ? `${condition} && ${value}` : value); - } else { - const obj = `{ ${quoteNameIfNecessary(name)}: ${attr.getValue()} }`; - initialProps.push(obj); - - changes.push(condition ? `${condition} && ${obj}` : obj); - } - }); - - block.builders.init.addBlock(deindent` - var ${levels} = [ - ${initialProps.join(',\n')} - ]; - `); - - statements.push(deindent` - for (var #i = 0; #i < ${levels}.length; #i += 1) { - ${name_initial_data} = @assign(${name_initial_data}, ${levels}[#i]); - } - `); - - const conditions = [...allDependencies].map(dep => `changed.${dep}`).join(' || '); - - updates.push(deindent` - var ${name_changes} = ${allDependencies.size === 1 ? `${conditions}` : `(${conditions})`} ? @getSpreadUpdate(${levels}, [ - ${changes.join(',\n')} - ]) : {}; - `); - } else { - this.attributes - .filter((attribute: Attribute) => attribute.isDynamic) - .forEach((attribute: Attribute) => { - if (attribute.dependencies.size > 0) { - updates.push(deindent` - if (${[...attribute.dependencies] - .map(dependency => `changed.${dependency}`) - .join(' || ')}) ${name_changes}${quotePropIfNecessary(attribute.name)} = ${attribute.getValue()}; - `); - } - }); - } - } - - if (this.bindings.length) { - component.target.hasComplexBindings = true; - - name_updating = block.alias(`${name}_updating`); - block.addVariable(name_updating, '{}'); - - let hasLocalBindings = false; - let hasStoreBindings = false; - - const builder = new CodeBuilder(); - - this.bindings.forEach((binding: Binding) => { - let { name: key } = getObject(binding.value.node); - - let setFromChild; - - if (binding.isContextual) { - const computed = isComputed(binding.value.node); - const tail = binding.value.node.type === 'MemberExpression' ? getTailSnippet(binding.value.node) : ''; - - const head = block.bindings.get(key); - - const lhs = binding.value.node.type === 'MemberExpression' - ? binding.value.snippet - : `${head}${tail} = childState${quotePropIfNecessary(binding.name)}`; - - setFromChild = deindent` - ${lhs} = childState${quotePropIfNecessary(binding.name)}; - - ${[...binding.value.dependencies] - .map((name: string) => { - const isStoreProp = name[0] === '$'; - const prop = isStoreProp ? name.slice(1) : name; - const newState = isStoreProp ? 'newStoreState' : 'newState'; - - if (isStoreProp) hasStoreBindings = true; - else hasLocalBindings = true; - - return `${newState}${quotePropIfNecessary(prop)} = ctx${quotePropIfNecessary(name)};`; - })} - `; - } - - else { - const isStoreProp = key[0] === '$'; - const prop = isStoreProp ? key.slice(1) : key; - const newState = isStoreProp ? 'newStoreState' : 'newState'; - - if (isStoreProp) hasStoreBindings = true; - else hasLocalBindings = true; - - if (binding.value.node.type === 'MemberExpression') { - setFromChild = deindent` - ${binding.value.snippet} = childState${quotePropIfNecessary(binding.name)}; - ${newState}${quotePropIfNecessary(prop)} = ctx${quotePropIfNecessary(key)}; - `; - } - - else { - setFromChild = `${newState}${quotePropIfNecessary(prop)} = childState${quotePropIfNecessary(binding.name)};`; - } - } - - statements.push(deindent` - if (${binding.value.snippet} !== void 0) { - ${name_initial_data}${quotePropIfNecessary(binding.name)} = ${binding.value.snippet}; - ${name_updating}${quotePropIfNecessary(binding.name)} = true; - }` - ); - - builder.addConditional( - `!${name_updating}${quotePropIfNecessary(binding.name)} && changed${quotePropIfNecessary(binding.name)}`, - setFromChild - ); - - updates.push(deindent` - if (!${name_updating}${quotePropIfNecessary(binding.name)} && ${[...binding.value.dependencies].map((dependency: string) => `changed.${dependency}`).join(' || ')}) { - ${name_changes}${quotePropIfNecessary(binding.name)} = ${binding.value.snippet}; - ${name_updating}${quotePropIfNecessary(binding.name)} = ${binding.value.snippet} !== void 0; - } - `); - }); - - block.maintainContext = true; // TODO put this somewhere more logical - - const initialisers = [ - hasLocalBindings && 'newState = {}', - hasStoreBindings && 'newStoreState = {}', - ].filter(Boolean).join(', '); - - // TODO use component.on('state', ...) instead of _bind - componentInitProperties.push(deindent` - _bind(changed, childState) { - var ${initialisers}; - ${builder} - ${hasStoreBindings && `#component.store.set(newStoreState);`} - ${hasLocalBindings && `#component._set(newState);`} - ${name_updating} = {}; - } - `); - - beforecreate = deindent` - #component.root._beforecreate.push(() => { - ${name}._bind({ ${this.bindings.map(b => `${quoteNameIfNecessary(b.name)}: 1`).join(', ')} }, ${name}.get()); - }); - `; - } - - this.handlers.forEach(handler => { - handler.var = block.getUniqueName(`${this.var}_${handler.name}`); // TODO this is hacky - handler.render(component, block, false); // TODO hoist when possible - if (handler.usesContext) block.maintainContext = true; // TODO is there a better place to put this? - }); - - if (this.name === 'svelte:component') { - const switch_value = block.getUniqueName('switch_value'); - const switch_props = block.getUniqueName('switch_props'); - - const { snippet } = this.expression; - - block.builders.init.addBlock(deindent` - var ${switch_value} = ${snippet}; - - function ${switch_props}(ctx) { - ${(this.attributes.length || this.bindings.length) && deindent` - var ${name_initial_data} = ${attributeObject};`} - ${statements} - return { - ${componentInitProperties.join(',\n')} - }; - } - - if (${switch_value}) { - var ${name} = new ${switch_value}(${switch_props}(ctx)); - - ${beforecreate} - } - - ${this.handlers.map(handler => deindent` - function ${handler.var}(event) { - ${handler.snippet || `#component.fire("${handler.name}", event);`} - } - - if (${name}) ${name}.on("${handler.name}", ${handler.var}); - `)} - `); - - block.builders.create.addLine( - `if (${name}) ${name}._fragment.c();` - ); - - if (parentNodes) { - block.builders.claim.addLine( - `if (${name}) ${name}._fragment.l(${parentNodes});` - ); - } - - block.builders.mount.addBlock(deindent` - if (${name}) { - ${name}._mount(${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'}); - ${this.ref && `#component.refs.${this.ref.name} = ${name};`} - } - `); - - const anchor = this.getOrCreateAnchor(block, parentNode, parentNodes); - const updateMountNode = this.getUpdateMountNode(anchor); - - if (updates.length) { - block.builders.update.addBlock(deindent` - ${updates} - `); - } - - block.builders.update.addBlock(deindent` - if (${switch_value} !== (${switch_value} = ${snippet})) { - if (${name}) { - ${this.component.options.nestedTransitions - ? deindent` - @groupOutros(); - const old_component = ${name}; - old_component._fragment.o(() => { - old_component.destroy(); - });` - : `${name}.destroy();`} - } - - if (${switch_value}) { - ${name} = new ${switch_value}(${switch_props}(ctx)); - - ${this.bindings.length > 0 && deindent` - #component.root._beforecreate.push(() => { - const changed = {}; - ${this.bindings.map(binding => deindent` - if (${binding.value.snippet} === void 0) changed.${binding.name} = 1;`)} - ${name}._bind(changed, ${name}.get()); - });`} - ${name}._fragment.c(); - - ${this.children.map(child => child.remount(name))} - ${name}._mount(${updateMountNode}, ${anchor}); - - ${this.handlers.map(handler => deindent` - ${name}.on("${handler.name}", ${handler.var}); - `)} - - ${this.ref && `#component.refs.${this.ref.name} = ${name};`} - } else { - ${name} = null; - ${this.ref && deindent` - if (#component.refs.${this.ref.name} === ${name}) { - #component.refs.${this.ref.name} = null; - }`} - } - } - `); - - if (updates.length) { - block.builders.update.addBlock(deindent` - else if (${switch_value}) { - ${name}._set(${name_changes}); - ${this.bindings.length && `${name_updating} = {};`} - } - `); - } - - block.builders.destroy.addLine(`if (${name}) ${name}.destroy(${parentNode ? '' : 'detach'});`); - } else { - const expression = this.name === 'svelte:self' - ? component.name - : `%components-${this.name}`; - - block.builders.init.addBlock(deindent` - ${(this.attributes.length || this.bindings.length) && deindent` - var ${name_initial_data} = ${attributeObject};`} - ${statements} - var ${name} = new ${expression}({ - ${componentInitProperties.join(',\n')} - }); - - ${beforecreate} - - ${this.handlers.map(handler => deindent` - ${name}.on("${handler.name}", function(event) { - ${handler.snippet || `#component.fire("${handler.name}", event);`} - }); - `)} - - ${this.ref && `#component.refs.${this.ref.name} = ${name};`} - `); - - block.builders.create.addLine(`${name}._fragment.c();`); - - if (parentNodes) { - block.builders.claim.addLine( - `${name}._fragment.l(${parentNodes});` - ); - } - - block.builders.mount.addLine( - `${name}._mount(${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'});` - ); - - if (updates.length) { - block.builders.update.addBlock(deindent` - ${updates} - ${name}._set(${name_changes}); - ${this.bindings.length && `${name_updating} = {};`} - `); - } - - block.builders.destroy.addLine(deindent` - ${name}.destroy(${parentNode ? '' : 'detach'}); - ${this.ref && `if (#component.refs.${this.ref.name} === ${name}) #component.refs.${this.ref.name} = null;`} - `); - } - - if (this.component.options.nestedTransitions) { - block.builders.outro.addLine( - `if (${name}) ${name}._fragment.o(#outrocallback);` - ); - } - } - - remount(name: string) { - return `${this.var}._mount(${name}._slotted.default, null);`; - } -} - -function isComputed(node: Node) { - while (node.type === 'MemberExpression') { - if (node.computed) return true; - node = node.object; - } - - return false; } diff --git a/src/compile/nodes/shared/Node.ts b/src/compile/nodes/shared/Node.ts index cc544c9439..ec636c9e08 100644 --- a/src/compile/nodes/shared/Node.ts +++ b/src/compile/nodes/shared/Node.ts @@ -138,26 +138,6 @@ export default class Node { if (this.parent) return this.parent.findNearest(selector); } - getOrCreateAnchor(block: Block, parentNode: string, parentNodes: string) { - // TODO use this in EachBlock and IfBlock — tricky because - // children need to be created first - const needsAnchor = this.next ? !this.next.isDomNode() : !parentNode || !this.parent.isDomNode(); - const anchor = needsAnchor - ? block.getUniqueName(`${this.var}_anchor`) - : (this.next && this.next.var) || 'null'; - - if (needsAnchor) { - block.addElement( - anchor, - `@createComment()`, - parentNodes && `@createComment()`, - parentNode - ); - } - - return anchor; - } - remount(name: string) { return `${this.var}.m(${name}._slotted.default, null);`; } diff --git a/src/compile/render-dom/wrappers/Element/Attribute.ts b/src/compile/render-dom/wrappers/Element/Attribute.ts index 1a2549ce4e..e568a877b8 100644 --- a/src/compile/render-dom/wrappers/Element/Attribute.ts +++ b/src/compile/render-dom/wrappers/Element/Attribute.ts @@ -6,6 +6,7 @@ import { stringify } from '../../../../utils/stringify'; export default class AttributeWrapper { node: Attribute; + parent: ElementWrapper; constructor(node: Attribute, parent: ElementWrapper) { this.node = node; diff --git a/src/compile/render-dom/wrappers/Element/Binding.ts b/src/compile/render-dom/wrappers/Element/Binding.ts new file mode 100644 index 0000000000..e6b8de5a0a --- /dev/null +++ b/src/compile/render-dom/wrappers/Element/Binding.ts @@ -0,0 +1,290 @@ +import Binding from '../../../nodes/Binding'; +import ElementWrapper from '.'; +import { dimensions } from '../../../../utils/patterns'; +import getObject from '../../../../utils/getObject'; +import Block from '../../Block'; + +type Handler = { + +} + +const readOnlyMediaAttributes = new Set([ + 'duration', + 'buffered', + 'seekable', + 'played' +]); + +export default class BindingWrapper { + node: Binding; + parent: ElementWrapper; + + object: string; + handler: Handler; + updateDom: string; + initialUpdate: string; + needsLock: boolean; + updateCondition: string; + isReadOnlyMediaAttribute: boolean; + + constructor(node: Binding, parent: ElementWrapper) { + this.node = node; + this.parent = parent; + + parent.cannotUseInnerHTML(); + + const needsLock = ( + parent.node.name !== 'input' || + !/radio|checkbox|range|color/.test(parent.getStaticAttributeValue('type')) + ); + + const isReadOnly = ( + (parent.isMediaNode() && readOnlyMediaAttributes.has(this.node.name)) || + dimensions.test(this.node.name) + ); + + let updateConditions: string[] = []; + + const { name } = getObject(this.node.value.node); + const { snippet } = this.node.value; + + // special case: if you have e.g. `` + // and `selected` is an object chosen with a + if (binding.name === 'group') { + const bindingGroup = getBindingGroup(component, binding.value.node); + if (type === 'checkbox') { + return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`; + } + + return `${node.var}.__value`; + } + + // + if (type === 'range' || type === 'number') { + return `@toNumber(${node.var}.${binding.name})`; + } + + if ((binding.name === 'buffered' || binding.name === 'seekable' || binding.name === 'played')) { + return `@timeRangesToArray(${node.var}.${binding.name})` + } + + // everything else + return `${node.var}.${binding.name}`; +} + +function isComputed(node: Node) { + while (node.type === 'MemberExpression') { + if (node.computed) return true; + node = node.object; + } + + return false; +} \ No newline at end of file diff --git a/src/compile/render-dom/wrappers/Element/index.ts b/src/compile/render-dom/wrappers/Element/index.ts index 4e2f2211ed..62af28bba0 100644 --- a/src/compile/render-dom/wrappers/Element/index.ts +++ b/src/compile/render-dom/wrappers/Element/index.ts @@ -14,12 +14,80 @@ import deindent from '../../../../utils/deindent'; import namespaces from '../../../../utils/namespaces'; import AttributeWrapper from './Attribute'; import StyleAttributeWrapper from './StyleAttribute'; +import { dimensions } from '../../../../utils/patterns'; +import BindingWrapper from './Binding'; + +const events = [ + { + eventNames: ['input'], + filter: (node: Element, name: string) => + node.name === 'textarea' || + node.name === 'input' && !/radio|checkbox|range/.test(node.getStaticAttributeValue('type')) + }, + { + eventNames: ['change'], + filter: (node: Element, name: string) => + node.name === 'select' || + node.name === 'input' && /radio|checkbox/.test(node.getStaticAttributeValue('type')) + }, + { + eventNames: ['change', 'input'], + filter: (node: Element, name: string) => + node.name === 'input' && node.getStaticAttributeValue('type') === 'range' + }, + + { + eventNames: ['resize'], + filter: (node: Element, name: string) => + dimensions.test(name) + }, + + // media events + { + eventNames: ['timeupdate'], + filter: (node: Element, name: string) => + node.isMediaNode() && + (name === 'currentTime' || name === 'played') + }, + { + eventNames: ['durationchange'], + filter: (node: Element, name: string) => + node.isMediaNode() && + name === 'duration' + }, + { + eventNames: ['play', 'pause'], + filter: (node: Element, name: string) => + node.isMediaNode() && + name === 'paused' + }, + { + eventNames: ['progress'], + filter: (node: Element, name: string) => + node.isMediaNode() && + name === 'buffered' + }, + { + eventNames: ['loadedmetadata'], + filter: (node: Element, name: string) => + node.isMediaNode() && + (name === 'buffered' || name === 'seekable') + }, + { + eventNames: ['volumechange'], + filter: (node: Element, name: string) => + node.isMediaNode() && + name === 'volume' + } +]; export default class ElementWrapper extends Wrapper { node: Element; fragment: FragmentWrapper; attributes: AttributeWrapper[]; + bindings: BindingWrapper[]; classDependencies: string[]; + initialUpdate: string; var: string; @@ -43,6 +111,10 @@ export default class ElementWrapper extends Wrapper { return new AttributeWrapper(attribute, this); }); + this.bindings = this.node.bindings.map(binding => { + return new BindingWrapper(binding, this); + }); + this.fragment = new FragmentWrapper(renderer, block, node.children, this, stripWhitespace, nextSibling); } @@ -62,7 +134,7 @@ export default class ElementWrapper extends Wrapper { const slot = this.node.attributes.find((attribute: Node) => attribute.name === 'slot'); const prop = slot && quotePropIfNecessary(slot.chunks[0].data); const initialMountNode = this.slotted ? - `${this.findNearest(/^InlineComponent/).var}._slotted${prop}` : // TODO this looks bonkers + `${this.node.findNearest(/^InlineComponent/).var}._slotted${prop}` : // TODO this looks bonkers parentNode; block.addVariable(node); @@ -75,7 +147,7 @@ export default class ElementWrapper extends Wrapper { if (parentNodes) { block.builders.claim.addBlock(deindent` ${node} = ${this.getClaimStatement(parentNodes)}; - var ${nodes} = @children(${this.name === 'template' ? `${node}.content` : node}); + var ${nodes} = @children(${this.node.name === 'template' ? `${node}.content` : node}); `); } else { block.builders.claim.addLine( @@ -122,16 +194,16 @@ export default class ElementWrapper extends Wrapper { } let hasHoistedEventHandlerOrBinding = ( - //(this.hasAncestor('EachBlock') && this.node.bindings.length > 0) || + //(this.hasAncestor('EachBlock') && this.bindings.length > 0) || this.node.handlers.some(handler => handler.shouldHoist) ); const eventHandlerOrBindingUsesComponent = ( - this.node.bindings.length > 0 || + this.bindings.length > 0 || this.node.handlers.some(handler => handler.usesComponent) ); const eventHandlerOrBindingUsesContext = ( - this.node.bindings.some(binding => binding.usesContext) || + this.bindings.some(binding => binding.node.usesContext) || this.node.handlers.some(handler => handler.usesContext) ); @@ -163,7 +235,7 @@ export default class ElementWrapper extends Wrapper { this.addBindings(block); this.addEventHandlers(block); - if (this.ref) this.addRef(block); + if (this.node.ref) this.addRef(block); this.addAttributes(block); this.addTransitions(block); this.addAnimation(block); @@ -199,7 +271,7 @@ export default class ElementWrapper extends Wrapper { if (isVoidElementName(wrapper.node.name)) return open + '>'; - return `${open}>${wrapper.fragment.nodes.map(toHTML).join('')}`; + return `${open}>${wrapper.fragment.nodes.map(toHTML).join('')}`; } if (renderer.options.dev) { @@ -239,19 +311,16 @@ export default class ElementWrapper extends Wrapper { : `{}`}, ${this.node.namespace === namespaces.svg ? true : false})`; } - addBindings( - block: Block - ) { - if (this.node.bindings.length === 0) return; - - if (this.name === 'select' || this.isMediaNode()) this.component.target.hasComplexBindings = true; + addBindings(block: Block) { + if (this.bindings.length === 0) return; - const needsLock = this.name !== 'input' || !/radio|checkbox|range|color/.test(this.getStaticAttributeValue('type')); + if (this.node.name === 'select' || this.isMediaNode()) { + this.renderer.hasComplexBindings = true; + } - // TODO munge in constructor - const mungedBindings = this.bindings.map(binding => binding.munge(block)); + const needsLock = this.node.name !== 'input' || !/radio|checkbox|range|color/.test(this.getStaticAttributeValue('type')); - const lock = mungedBindings.some(binding => binding.needsLock) ? + const lock = this.bindings.some(binding => binding.needsLock) ? block.getUniqueName(`${this.var}_updating`) : null; @@ -261,7 +330,7 @@ export default class ElementWrapper extends Wrapper { .map(event => { return { events: event.eventNames, - bindings: mungedBindings.filter(binding => event.filter(this, binding.name)) + bindings: this.bindings.filter(binding => event.filter(this, binding.name)) }; }) .filter(group => group.bindings.length); @@ -282,8 +351,6 @@ export default class ElementWrapper extends Wrapper { ); }); - const usesContext = group.bindings.some(binding => binding.handler.usesContext); - const usesState = group.bindings.some(binding => binding.handler.usesState); const usesStore = group.bindings.some(binding => binding.handler.usesStore); const mutations = group.bindings.map(binding => binding.handler.mutation).filter(Boolean).join('\n'); @@ -369,7 +436,7 @@ export default class ElementWrapper extends Wrapper { } }); - this.initialUpdate = mungedBindings.map(binding => binding.initialUpdate).filter(Boolean).join('\n'); + this.initialUpdate = this.bindings.map(binding => binding.initialUpdate).filter(Boolean).join('\n'); } addAttributes(block: Block) { diff --git a/src/compile/render-dom/wrappers/Fragment.ts b/src/compile/render-dom/wrappers/Fragment.ts index 74da5a1484..3171ce385c 100644 --- a/src/compile/render-dom/wrappers/Fragment.ts +++ b/src/compile/render-dom/wrappers/Fragment.ts @@ -1,6 +1,7 @@ import Wrapper from './shared/wrapper'; import EachBlock from './EachBlock'; import Element from './Element'; +import InlineComponent from './InlineComponent'; import MustacheTag from './MustacheTag'; import Text from './Text'; import Window from './Window'; @@ -14,6 +15,7 @@ const wrappers = { Comment: null, EachBlock, Element, + InlineComponent, MustacheTag, Text, Window diff --git a/src/compile/render-dom/wrappers/InlineComponent/index.ts b/src/compile/render-dom/wrappers/InlineComponent/index.ts new file mode 100644 index 0000000000..0dced0daca --- /dev/null +++ b/src/compile/render-dom/wrappers/InlineComponent/index.ts @@ -0,0 +1,478 @@ +import Wrapper from '../shared/wrapper'; +import Renderer from '../../Renderer'; +import Block from '../../Block'; +import Node from '../../../nodes/shared/Node'; +import InlineComponent from '../../../nodes/InlineComponent'; +import FragmentWrapper from '../Fragment'; +import { quoteNameIfNecessary, quotePropIfNecessary } from '../../../../utils/quoteIfNecessary'; +import stringifyProps from '../../../../utils/stringifyProps'; +import addToSet from '../../../../utils/addToSet'; +import deindent from '../../../../utils/deindent'; +import Attribute from '../../../nodes/Attribute'; +import CodeBuilder from '../../../../utils/CodeBuilder'; +import getObject from '../../../../utils/getObject'; +import Binding from '../../../nodes/Binding'; +import getTailSnippet from '../../../../utils/getTailSnippet'; + +export default class InlineComponentWrapper extends Wrapper { + var: string; + _slots: Set; // TODO lost the underscore + node: InlineComponent; + fragment: FragmentWrapper; + + constructor( + renderer: Renderer, + block: Block, + parent: Wrapper, + node: InlineComponent, + stripWhitespace: boolean, + nextSibling: Wrapper + ) { + super(renderer, block, parent, node); + + this.cannotUseInnerHTML(); + + if (this.node.expression) { + block.addDependencies(this.node.expression.dependencies); + } + + this.node.attributes.forEach(attr => { + block.addDependencies(attr.dependencies); + }); + + this.node.bindings.forEach(binding => { + block.addDependencies(binding.value.dependencies); + }); + + this.node.handlers.forEach(handler => { + block.addDependencies(handler.dependencies); + }); + + this.var = ( + this.node.name === 'svelte:self' ? renderer.component.name : + this.node.name === 'svelte:component' ? 'switch_instance' : + this.node.name + ).toLowerCase(); + + if (this.node.children.length) { + this._slots = new Set(['default']); + this.fragment = new FragmentWrapper(renderer, block, node.children, this, stripWhitespace, nextSibling); + } + + if (renderer.component.options.nestedTransitions) { + block.addOutro(); + } + } + + render( + block: Block, + parentNode: string, + parentNodes: string + ) { + const { renderer } = this; + const { component } = renderer; + + const name = this.var; + + const componentInitProperties = [ + `root: #component.root`, + `store: #component.store` + ]; + + if (this.fragment) { + const slots = Array.from(this._slots).map(name => `${quoteNameIfNecessary(name)}: @createFragment()`); + componentInitProperties.push(`slots: { ${slots.join(', ')} }`); + + this.fragment.nodes.forEach((child: Wrapper) => { + child.render(block, `${this.var}._slotted.default`, 'nodes'); + }); + } + + const statements: string[] = []; + + const name_initial_data = block.getUniqueName(`${name}_initial_data`); + const name_changes = block.getUniqueName(`${name}_changes`); + let name_updating: string; + let beforecreate: string = null; + + const updates: string[] = []; + + const usesSpread = !!this.node.attributes.find(a => a.isSpread); + + const attributeObject = usesSpread + ? '{}' + : stringifyProps( + this.node.attributes.map(attr => `${quoteNameIfNecessary(attr.name)}: ${attr.getValue()}`) + ); + + if (this.node.attributes.length || this.node.bindings.length) { + componentInitProperties.push(`data: ${name_initial_data}`); + } + + if (!usesSpread && (this.node.attributes.filter(a => a.isDynamic).length || this.node.bindings.length)) { + updates.push(`var ${name_changes} = {};`); + } + + if (this.node.attributes.length) { + if (usesSpread) { + const levels = block.getUniqueName(`${this.var}_spread_levels`); + + const initialProps = []; + const changes = []; + + const allDependencies = new Set(); + + this.node.attributes.forEach(attr => { + addToSet(allDependencies, attr.dependencies); + }); + + this.node.attributes.forEach(attr => { + const { name, dependencies } = attr; + + const condition = dependencies.size > 0 && (dependencies.size !== allDependencies.size) + ? `(${[...dependencies].map(d => `changed.${d}`).join(' || ')})` + : null; + + if (attr.isSpread) { + const value = attr.expression.snippet; + initialProps.push(value); + + changes.push(condition ? `${condition} && ${value}` : value); + } else { + const obj = `{ ${quoteNameIfNecessary(name)}: ${attr.getValue()} }`; + initialProps.push(obj); + + changes.push(condition ? `${condition} && ${obj}` : obj); + } + }); + + block.builders.init.addBlock(deindent` + var ${levels} = [ + ${initialProps.join(',\n')} + ]; + `); + + statements.push(deindent` + for (var #i = 0; #i < ${levels}.length; #i += 1) { + ${name_initial_data} = @assign(${name_initial_data}, ${levels}[#i]); + } + `); + + const conditions = [...allDependencies].map(dep => `changed.${dep}`).join(' || '); + + updates.push(deindent` + var ${name_changes} = ${allDependencies.size === 1 ? `${conditions}` : `(${conditions})`} ? @getSpreadUpdate(${levels}, [ + ${changes.join(',\n')} + ]) : {}; + `); + } else { + this.node.attributes + .filter((attribute: Attribute) => attribute.isDynamic) + .forEach((attribute: Attribute) => { + if (attribute.dependencies.size > 0) { + updates.push(deindent` + if (${[...attribute.dependencies] + .map(dependency => `changed.${dependency}`) + .join(' || ')}) ${name_changes}${quotePropIfNecessary(attribute.name)} = ${attribute.getValue()}; + `); + } + }); + } + } + + if (this.node.bindings.length) { + renderer.hasComplexBindings = true; + + name_updating = block.alias(`${name}_updating`); + block.addVariable(name_updating, '{}'); + + let hasLocalBindings = false; + let hasStoreBindings = false; + + const builder = new CodeBuilder(); + + this.node.bindings.forEach((binding: Binding) => { + let { name: key } = getObject(binding.value.node); + + let setFromChild; + + if (binding.isContextual) { + const computed = isComputed(binding.value.node); + const tail = binding.value.node.type === 'MemberExpression' ? getTailSnippet(binding.value.node) : ''; + + const head = block.bindings.get(key); + + const lhs = binding.value.node.type === 'MemberExpression' + ? binding.value.snippet + : `${head}${tail} = childState${quotePropIfNecessary(binding.name)}`; + + setFromChild = deindent` + ${lhs} = childState${quotePropIfNecessary(binding.name)}; + + ${[...binding.value.dependencies] + .map((name: string) => { + const isStoreProp = name[0] === '$'; + const prop = isStoreProp ? name.slice(1) : name; + const newState = isStoreProp ? 'newStoreState' : 'newState'; + + if (isStoreProp) hasStoreBindings = true; + else hasLocalBindings = true; + + return `${newState}${quotePropIfNecessary(prop)} = ctx${quotePropIfNecessary(name)};`; + })} + `; + } + + else { + const isStoreProp = key[0] === '$'; + const prop = isStoreProp ? key.slice(1) : key; + const newState = isStoreProp ? 'newStoreState' : 'newState'; + + if (isStoreProp) hasStoreBindings = true; + else hasLocalBindings = true; + + if (binding.value.node.type === 'MemberExpression') { + setFromChild = deindent` + ${binding.value.snippet} = childState${quotePropIfNecessary(binding.name)}; + ${newState}${quotePropIfNecessary(prop)} = ctx${quotePropIfNecessary(key)}; + `; + } + + else { + setFromChild = `${newState}${quotePropIfNecessary(prop)} = childState${quotePropIfNecessary(binding.name)};`; + } + } + + statements.push(deindent` + if (${binding.value.snippet} !== void 0) { + ${name_initial_data}${quotePropIfNecessary(binding.name)} = ${binding.value.snippet}; + ${name_updating}${quotePropIfNecessary(binding.name)} = true; + }` + ); + + builder.addConditional( + `!${name_updating}${quotePropIfNecessary(binding.name)} && changed${quotePropIfNecessary(binding.name)}`, + setFromChild + ); + + updates.push(deindent` + if (!${name_updating}${quotePropIfNecessary(binding.name)} && ${[...binding.value.dependencies].map((dependency: string) => `changed.${dependency}`).join(' || ')}) { + ${name_changes}${quotePropIfNecessary(binding.name)} = ${binding.value.snippet}; + ${name_updating}${quotePropIfNecessary(binding.name)} = ${binding.value.snippet} !== void 0; + } + `); + }); + + block.maintainContext = true; // TODO put this somewhere more logical + + const initialisers = [ + hasLocalBindings && 'newState = {}', + hasStoreBindings && 'newStoreState = {}', + ].filter(Boolean).join(', '); + + // TODO use component.on('state', ...) instead of _bind + componentInitProperties.push(deindent` + _bind(changed, childState) { + var ${initialisers}; + ${builder} + ${hasStoreBindings && `#component.store.set(newStoreState);`} + ${hasLocalBindings && `#component._set(newState);`} + ${name_updating} = {}; + } + `); + + beforecreate = deindent` + #component.root._beforecreate.push(() => { + ${name}._bind({ ${this.node.bindings.map(b => `${quoteNameIfNecessary(b.name)}: 1`).join(', ')} }, ${name}.get()); + }); + `; + } + + this.node.handlers.forEach(handler => { + handler.var = block.getUniqueName(`${this.var}_${handler.name}`); // TODO this is hacky + handler.render(component, block, false); // TODO hoist when possible + if (handler.usesContext) block.maintainContext = true; // TODO is there a better place to put this? + }); + + if (this.node.name === 'svelte:component') { + const switch_value = block.getUniqueName('switch_value'); + const switch_props = block.getUniqueName('switch_props'); + + const { snippet } = this.node.expression; + + block.builders.init.addBlock(deindent` + var ${switch_value} = ${snippet}; + + function ${switch_props}(ctx) { + ${(this.node.attributes.length || this.node.bindings.length) && deindent` + var ${name_initial_data} = ${attributeObject};`} + ${statements} + return { + ${componentInitProperties.join(',\n')} + }; + } + + if (${switch_value}) { + var ${name} = new ${switch_value}(${switch_props}(ctx)); + + ${beforecreate} + } + + ${this.node.handlers.map(handler => deindent` + function ${handler.var}(event) { + ${handler.snippet || `#component.fire("${handler.name}", event);`} + } + + if (${name}) ${name}.on("${handler.name}", ${handler.var}); + `)} + `); + + block.builders.create.addLine( + `if (${name}) ${name}._fragment.c();` + ); + + if (parentNodes) { + block.builders.claim.addLine( + `if (${name}) ${name}._fragment.l(${parentNodes});` + ); + } + + block.builders.mount.addBlock(deindent` + if (${name}) { + ${name}._mount(${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'}); + ${this.node.ref && `#component.refs.${this.node.ref.name} = ${name};`} + } + `); + + const anchor = this.getOrCreateAnchor(block, parentNode, parentNodes); + const updateMountNode = this.getUpdateMountNode(anchor); + + if (updates.length) { + block.builders.update.addBlock(deindent` + ${updates} + `); + } + + block.builders.update.addBlock(deindent` + if (${switch_value} !== (${switch_value} = ${snippet})) { + if (${name}) { + ${component.options.nestedTransitions + ? deindent` + @groupOutros(); + const old_component = ${name}; + old_component._fragment.o(() => { + old_component.destroy(); + });` + : `${name}.destroy();`} + } + + if (${switch_value}) { + ${name} = new ${switch_value}(${switch_props}(ctx)); + + ${this.node.bindings.length > 0 && deindent` + #component.root._beforecreate.push(() => { + const changed = {}; + ${this.node.bindings.map(binding => deindent` + if (${binding.value.snippet} === void 0) changed.${binding.name} = 1;`)} + ${name}._bind(changed, ${name}.get()); + });`} + ${name}._fragment.c(); + + ${this.fragment.nodes.map(child => child.remount(name))} + ${name}._mount(${updateMountNode}, ${anchor}); + + ${this.node.handlers.map(handler => deindent` + ${name}.on("${handler.name}", ${handler.var}); + `)} + + ${this.node.ref && `#component.refs.${this.node.ref.name} = ${name};`} + } else { + ${name} = null; + ${this.node.ref && deindent` + if (#component.refs.${this.node.ref.name} === ${name}) { + #component.refs.${this.node.ref.name} = null; + }`} + } + } + `); + + if (updates.length) { + block.builders.update.addBlock(deindent` + else if (${switch_value}) { + ${name}._set(${name_changes}); + ${this.node.bindings.length && `${name_updating} = {};`} + } + `); + } + + block.builders.destroy.addLine(`if (${name}) ${name}.destroy(${parentNode ? '' : 'detach'});`); + } else { + const expression = this.node.name === 'svelte:self' + ? component.name + : `%components-${this.node.name}`; + + block.builders.init.addBlock(deindent` + ${(this.node.attributes.length || this.node.bindings.length) && deindent` + var ${name_initial_data} = ${attributeObject};`} + ${statements} + var ${name} = new ${expression}({ + ${componentInitProperties.join(',\n')} + }); + + ${beforecreate} + + ${this.node.handlers.map(handler => deindent` + ${name}.on("${handler.name}", function(event) { + ${handler.snippet || `#component.fire("${handler.name}", event);`} + }); + `)} + + ${this.node.ref && `#component.refs.${this.node.ref.name} = ${name};`} + `); + + block.builders.create.addLine(`${name}._fragment.c();`); + + if (parentNodes) { + block.builders.claim.addLine( + `${name}._fragment.l(${parentNodes});` + ); + } + + block.builders.mount.addLine( + `${name}._mount(${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'});` + ); + + if (updates.length) { + block.builders.update.addBlock(deindent` + ${updates} + ${name}._set(${name_changes}); + ${this.node.bindings.length && `${name_updating} = {};`} + `); + } + + block.builders.destroy.addLine(deindent` + ${name}.destroy(${parentNode ? '' : 'detach'}); + ${this.node.ref && `if (#component.refs.${this.node.ref.name} === ${name}) #component.refs.${this.node.ref.name} = null;`} + `); + } + + if (component.options.nestedTransitions) { + block.builders.outro.addLine( + `if (${name}) ${name}._fragment.o(#outrocallback);` + ); + } + } + + remount(name: string) { + return `${this.var}._mount(${name}._slotted.default, null);`; + } +} + +function isComputed(node: Node) { + while (node.type === 'MemberExpression') { + if (node.computed) return true; + node = node.object; + } + + return false; +} \ No newline at end of file diff --git a/src/compile/render-dom/wrappers/shared/Wrapper.ts b/src/compile/render-dom/wrappers/shared/Wrapper.ts index bdb26d700c..8ed70a22d0 100644 --- a/src/compile/render-dom/wrappers/shared/Wrapper.ts +++ b/src/compile/render-dom/wrappers/shared/Wrapper.ts @@ -11,6 +11,7 @@ export default class Wrapper { prev: Wrapper | null; next: Wrapper | null; + var: string; canUseInnerHTML: boolean; constructor( @@ -33,6 +34,26 @@ export default class Wrapper { if (this.parent) this.parent.cannotUseInnerHTML(); } + getOrCreateAnchor(block: Block, parentNode: string, parentNodes: string) { + // TODO use this in EachBlock and IfBlock — tricky because + // children need to be created first + const needsAnchor = this.next ? !this.next.isDomNode() : !parentNode || !this.parent.isDomNode(); + const anchor = needsAnchor + ? block.getUniqueName(`${this.var}_anchor`) + : (this.next && this.next.var) || 'null'; + + if (needsAnchor) { + block.addElement( + anchor, + `@createComment()`, + parentNodes && `@createComment()`, + parentNode + ); + } + + return anchor; + } + getUpdateMountNode(anchor: string) { return (this.parent && this.parent.isDomNode()) ? this.parent.var