diff --git a/src/compile/Component.ts b/src/compile/Component.ts index 9b3603ea04..fbe4d483db 100644 --- a/src/compile/Component.ts +++ b/src/compile/Component.ts @@ -138,7 +138,6 @@ export default class Component { code: MagicString; - bindingGroups: string[]; indirectDependencies: Map>; expectedProperties: Set; refs: Set; @@ -197,7 +196,6 @@ export default class Component { this.refs = new Set(); this.refCallees = []; - this.bindingGroups = []; this.indirectDependencies = new Map(); this.file = options.filename && ( diff --git a/src/compile/nodes/IfBlock.ts b/src/compile/nodes/IfBlock.ts index 6a6c9b034e..795568e9cd 100644 --- a/src/compile/nodes/IfBlock.ts +++ b/src/compile/nodes/IfBlock.ts @@ -7,16 +7,6 @@ import createDebuggingComment from '../../utils/createDebuggingComment'; import Expression from './shared/Expression'; import mapChildren from './shared/mapChildren'; -function isElseIf(node: ElseBlock) { - return ( - node && node.children.length === 1 && node.children[0].type === 'IfBlock' - ); -} - -function isElseBranch(branch) { - return branch.block && !branch.condition; -} - export default class IfBlock extends Node { type: 'IfBlock'; expression: Expression; @@ -38,82 +28,6 @@ export default class IfBlock extends Node { this.warnIfEmptyBlock(); } - init( - block: Block, - stripWhitespace: boolean, - nextSibling: Node - ) { - const { component } = this; - - this.cannotUseInnerHTML(); - - const blocks: Block[] = []; - let dynamic = false; - let hasIntros = false; - let hasOutros = false; - - function attachBlocks(node: IfBlock) { - node.var = block.getUniqueName(`if_block`); - - block.addDependencies(node.expression.dependencies); - - node.block = block.child({ - comment: createDebuggingComment(node, component), - name: component.getUniqueName(`create_if_block`), - }); - - blocks.push(node.block); - node.initChildren(node.block, stripWhitespace, nextSibling); - - if (node.block.dependencies.size > 0) { - dynamic = true; - block.addDependencies(node.block.dependencies); - } - - if (node.block.hasIntros) hasIntros = true; - if (node.block.hasOutros) hasOutros = true; - - if (isElseIf(node.else)) { - attachBlocks(node.else.children[0]); - } else if (node.else) { - node.else.block = block.child({ - comment: createDebuggingComment(node.else, component), - name: component.getUniqueName(`create_if_block`), - }); - - blocks.push(node.else.block); - node.else.initChildren( - node.else.block, - stripWhitespace, - nextSibling - ); - - if (node.else.block.dependencies.size > 0) { - dynamic = true; - block.addDependencies(node.else.block.dependencies); - } - - if (node.else.block.hasIntros) hasIntros = true; - if (node.else.block.hasOutros) hasOutros = true; - } - } - - attachBlocks(this); - - if (component.options.nestedTransitions) { - if (hasIntros) block.addIntro(); - if (hasOutros) block.addOutro(); - } - - blocks.forEach(block => { - block.hasUpdateMethod = dynamic; - block.hasIntroMethod = hasIntros; - block.hasOutroMethod = hasOutros; - }); - - component.target.blocks.push(...blocks); - } - build( block: Block, parentNode: string, diff --git a/src/compile/render-dom/Renderer.ts b/src/compile/render-dom/Renderer.ts index da1c31302e..97c2696b01 100644 --- a/src/compile/render-dom/Renderer.ts +++ b/src/compile/render-dom/Renderer.ts @@ -11,6 +11,7 @@ export default class Renderer { readonly: Set; slots: Set; metaBindings: string[]; + bindingGroups: string[]; block: Block; fragment: FragmentWrapper; @@ -35,6 +36,8 @@ export default class Renderer { // initial values for e.g. window.innerWidth, if there's a meta tag this.metaBindings = []; + this.bindingGroups = []; + // main block this.block = new Block({ component, diff --git a/src/compile/render-dom/index.ts b/src/compile/render-dom/index.ts index 31226f4561..2e07b51386 100644 --- a/src/compile/render-dom/index.ts +++ b/src/compile/render-dom/index.ts @@ -166,8 +166,8 @@ export default function dom( return `if (${conditions.join(' && ')}) console.warn("${message}");` })} - ${component.bindingGroups.length && - `this._bindingGroups = [${Array(component.bindingGroups.length).fill('[]').join(', ')}];`} + ${renderer.bindingGroups.length && + `this._bindingGroups = [${Array(renderer.bindingGroups.length).fill('[]').join(', ')}];`} this._intro = ${component.options.skipIntroByDefault ? '!!options.intro' : 'true'}; ${templateProperties.onstate && `this._handlers.state = [%onstate];`} diff --git a/src/compile/render-dom/wrappers/Element/Binding.ts b/src/compile/render-dom/wrappers/Element/Binding.ts deleted file mode 100644 index e6b8de5a0a..0000000000 --- a/src/compile/render-dom/wrappers/Element/Binding.ts +++ /dev/null @@ -1,290 +0,0 @@ -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/Binding/Binding.ts b/src/compile/render-dom/wrappers/Element/Binding/Binding.ts new file mode 100644 index 0000000000..1cc9d37ee9 --- /dev/null +++ b/src/compile/render-dom/wrappers/Element/Binding/Binding.ts @@ -0,0 +1,360 @@ +import ElementWrapper from '..'; +import Block from '../../../Block'; +import deindent from '../../../../../utils/deindent'; +import Binding from '../../../../nodes/Binding'; +import getObject from '../../../../../utils/getObject'; +import Renderer from '../../../Renderer'; +import getTailSnippet from '../../../../../utils/getTailSnippet'; +import { Node } from '../../../../../interfaces'; +import Element from '../../../../nodes/Element'; +import flattenReference from '../../../../../utils/flattenReference'; +import { dimensions } from '../../../../../utils/patterns'; + +// TODO this should live in a specific binding +const readOnlyMediaAttributes = new Set([ + 'duration', + 'buffered', + 'seekable', + 'played' +]); + +export default class BindingWrapper { + element: ElementWrapper; + binding: Binding; + events: string[]; + + usesStore: boolean; + needsLock: boolean; + lock: string; + mutations: string[]; + props: Set; + storeProps: Set; + + object: string; + // handler: Handler; + updateDom: string; + initialUpdate: string; + updateCondition: string; + isReadOnly: boolean; + isReadOnlyMediaAttribute: boolean; + + constructor(element: ElementWrapper, binding: Binding) { + this.element = element; + this.binding = binding; + + element.cannotUseInnerHTML(); + + this.isReadOnly = false; + this.needsLock = false; + this.events = []; + } + + fromDom() { + throw new Error(`TODO implement in subclass`); + + // ` + // and `selected` is an object chosen with a +// if (binding.name === 'group') { +// const bindingGroup = getBindingGroup(renderer, 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 getDomUpdater( + node: Element, + binding: Binding, + snippet: string +) { + if (binding.isReadOnlyMediaAttribute()) { + return null; + } + + if (node.name === 'select') { + return node.getStaticAttributeValue('multiple') === true ? + `@selectOptions(${node.var}, ${snippet})` : + `@selectOption(${node.var}, ${snippet})`; + } + + if (binding.name === 'group') { + const type = node.getStaticAttributeValue('type'); + + const condition = type === 'checkbox' + ? `~${snippet}.indexOf(${node.var}.__value)` + : `${node.var}.__value === ${snippet}`; + + return `${node.var}.checked = ${condition};` + } + + return `${node.var}.${binding.name} = ${snippet};`; +} \ No newline at end of file diff --git a/src/compile/render-dom/wrappers/Element/Binding/InputRadioGroupBinding.ts b/src/compile/render-dom/wrappers/Element/Binding/InputRadioGroupBinding.ts new file mode 100644 index 0000000000..8fce43f674 --- /dev/null +++ b/src/compile/render-dom/wrappers/Element/Binding/InputRadioGroupBinding.ts @@ -0,0 +1,84 @@ +import Binding from '../../../../nodes/Binding'; +import Element from '../../../../nodes/Element'; +import ElementWrapper from '..'; +import Block from '../../../Block'; +import Renderer from '../../../Renderer'; +import flattenReference from '../../../../../utils/flattenReference'; +import { Node } from '../../../../../interfaces'; +import BindingWrapper from './Binding'; + +export default class InputRadioGroupBinding extends BindingWrapper { + element: ElementWrapper; + node: Binding; + + static filter( + node: Element, + binding_lookup: Record, + type: string + ) { + if (node.name === 'input' && binding_lookup.group) { + return type === 'radio'; + } + } + + constructor( + element: ElementWrapper, + binding_lookup: Record + ) { + super(element, binding_lookup.group); + this.events = ['change']; + } + + fromDom() { + const bindingGroup = getBindingGroup(this.element.renderer, this.binding.value.node); + if (this.element.node.getStaticAttributeValue('type') === 'checkbox') { + return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`; + } + + return `${this.element.var}.__value`; + } + + toDom() { + const type = this.element.node.getStaticAttributeValue('type'); + + const condition = type === 'checkbox' + ? `~${this.binding.value.snippet}.indexOf(${this.element.var}.__value)` + : `${this.element.var}.__value === ${this.binding.value.snippet}`; + + return `${this.element.var}.checked = ${condition};` + } + + render(block: Block) { + const bindingGroup = getBindingGroup( + this.element.renderer, + this.binding.value.node + ); + + block.builders.hydrate.addLine( + `#component._bindingGroups[${bindingGroup}].push(${this.element.var});` + ); + + block.builders.destroy.addLine( + `#component._bindingGroups[${bindingGroup}].splice(#component._bindingGroups[${bindingGroup}].indexOf(${this.element.var}), 1);` + ); + + super.render(block); + + // this.renderHandler(block, 'TODO'); + } +} + +function getBindingGroup(renderer: Renderer, value: Node) { + const { parts } = flattenReference(value); // TODO handle cases involving computed member expressions + const keypath = parts.join('.'); + + // TODO handle contextual bindings — `keypath` should include unique ID of + // each block that provides context + let index = renderer.bindingGroups.indexOf(keypath); + if (index === -1) { + index = renderer.bindingGroups.length; + renderer.bindingGroups.push(keypath); + } + + return index; +} \ No newline at end of file diff --git a/src/compile/render-dom/wrappers/Element/Binding/InputTextBinding.ts b/src/compile/render-dom/wrappers/Element/Binding/InputTextBinding.ts new file mode 100644 index 0000000000..63e3fb0f06 --- /dev/null +++ b/src/compile/render-dom/wrappers/Element/Binding/InputTextBinding.ts @@ -0,0 +1,32 @@ +import Binding from '../../../../nodes/Binding'; +import Element from '../../../../nodes/Element'; +import ElementWrapper from '..'; +import Block from '../../../Block'; +import BindingWrapper from './Binding'; + +export default class InputTextBinding extends BindingWrapper { + static filter( + node: Element, + binding_lookup: Record, + type: string + ) { + if (node.name === 'textarea' && binding_lookup.value) { + return true; + } + + if (node.name === 'input' && binding_lookup.value) { + return !/radio|checkbox|range/.test(type); + } + } + + constructor( + element: ElementWrapper, + binding_lookup: Record + ) { + super(element); + } + + render(block: Block) { + + } +} \ No newline at end of file diff --git a/src/compile/render-dom/wrappers/Element/UNUSED-Binding.ts b/src/compile/render-dom/wrappers/Element/UNUSED-Binding.ts new file mode 100644 index 0000000000..25b96533e0 --- /dev/null +++ b/src/compile/render-dom/wrappers/Element/UNUSED-Binding.ts @@ -0,0 +1,120 @@ +import Binding from '../../../nodes/Binding'; +import ElementWrapper from '.'; +import { dimensions } from '../../../../utils/patterns'; +import getObject from '../../../../utils/getObject'; +import Block from '../../Block'; + +type Handler = { + +} + +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} +{/if} \ No newline at end of file