Implement with block construct

pull/4601/head
Timothy Johnson 6 years ago
parent a1b0295fc3
commit d99359a03a

@ -0,0 +1,86 @@
import Expression from './shared/Expression';
import map_children from './shared/map_children';
import TemplateScope from './shared/TemplateScope';
import AbstractBlock from './shared/AbstractBlock';
import { x } from 'code-red';
import { Node, Identifier, RestElement } from 'estree';
interface Context {
key: Identifier;
name?: string;
modifier: (node: Node) => Node;
}
function unpack_destructuring(contexts: Context[], node: Node, modifier: (node: Node) => Node) {
if (!node) return;
if (node.type === 'Identifier' || (node as any).type === 'RestIdentifier') { // TODO is this right? not RestElement?
contexts.push({
key: node as Identifier,
modifier
});
} else if (node.type === 'ArrayPattern') {
node.elements.forEach((element, i) => {
if (element && (element as any).type === 'RestIdentifier') {
unpack_destructuring(contexts, element, node => x`${modifier(node)}.slice(${i})` as Node);
} else {
unpack_destructuring(contexts, element, node => x`${modifier(node)}[${i}]` as Node);
}
});
} else if (node.type === 'ObjectPattern') {
const used_properties = [];
node.properties.forEach((property, i) => {
if ((property as any).kind === 'rest') { // TODO is this right?
const replacement: RestElement = {
type: 'RestElement',
argument: property.key as Identifier
};
node.properties[i] = replacement as any;
unpack_destructuring(
contexts,
property.value,
node => x`@object_without_properties(${modifier(node)}, [${used_properties}])` as Node
);
} else {
used_properties.push(x`"${(property.key as Identifier).name}"`);
unpack_destructuring(contexts, property.value, node => x`${modifier(node)}.${(property.key as Identifier).name}` as Node);
}
});
}
}
export default class WithBlock extends AbstractBlock {
type: 'WithBlock';
expression: Expression;
context_node: Node;
context: string;
scope: TemplateScope;
contexts: Context[];
has_binding = false;
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.expression = new Expression(component, this, scope, info.expression);
this.context = info.context.name || 'with'; // TODO this is used to facilitate binding; currently fails with destructuring
this.context_node = info.context;
this.scope = scope.child();
this.contexts = [];
unpack_destructuring(this.contexts, info.context, node => node);
this.contexts.forEach(context => {
this.scope.add(context.key.name, this.expression.dependencies, this);
});
this.children = map_children(component, this, this.scope, info.children);
this.warn_if_empty_block();
}
}

@ -29,6 +29,7 @@ import ThenBlock from './ThenBlock';
import Title from './Title'; import Title from './Title';
import Transition from './Transition'; import Transition from './Transition';
import Window from './Window'; import Window from './Window';
import WithBlock from './WithBlock';
// note: to write less types each of types in union below should have type defined as literal // note: to write less types each of types in union below should have type defined as literal
// https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions // https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions
@ -61,4 +62,5 @@ export type INode = Action
| ThenBlock | ThenBlock
| Title | Title
| Transition | Transition
| Window; | Window
| WithBlock;

@ -14,6 +14,7 @@ import Slot from '../Slot';
import Text from '../Text'; import Text from '../Text';
import Title from '../Title'; import Title from '../Title';
import Window from '../Window'; import Window from '../Window';
import WithBlock from '../WithBlock';
import { TemplateNode } from '../../../interfaces'; import { TemplateNode } from '../../../interfaces';
export type Children = ReturnType<typeof map_children>; export type Children = ReturnType<typeof map_children>;
@ -36,6 +37,7 @@ function get_constructor(type) {
case 'Text': return Text; case 'Text': return Text;
case 'Title': return Title; case 'Title': return Title;
case 'Window': return Window; case 'Window': return Window;
case 'WithBlock': return WithBlock;
default: throw new Error(`Not implemented: ${type}`); default: throw new Error(`Not implemented: ${type}`);
} }
} }

@ -13,10 +13,10 @@ export interface BlockOptions {
key?: Identifier; key?: Identifier;
bindings?: Map<string, { bindings?: Map<string, {
object: Identifier; object: Identifier;
property: Identifier; property?: Identifier;
snippet: Node; snippet: Node;
store: string; store: string;
tail: Node; tail?: Node;
modifier: (node: Node) => Node; modifier: (node: Node) => Node;
}>; }>;
dependencies?: Set<string>; dependencies?: Set<string>;
@ -38,10 +38,10 @@ export default class Block {
bindings: Map<string, { bindings: Map<string, {
object: Identifier; object: Identifier;
property: Identifier; property?: Identifier;
snippet: Node; snippet: Node;
store: string; store: string;
tail: Node; tail?: Node;
modifier: (node: Node) => Node; modifier: (node: Node) => Node;
}>; }>;

@ -280,11 +280,14 @@ function get_event_handler(
const { object, property, modifier, store } = context; const { object, property, modifier, store } = context;
if (lhs.type === 'Identifier') { if (lhs.type === 'Identifier') {
lhs = modifier(x`${object}[${property}]`);
contextual_dependencies.add(object.name); contextual_dependencies.add(object.name);
if (property === undefined) {
lhs = modifier(b`${object}`[0]);
} else {
lhs = modifier(x`${object}[${property}]`);
contextual_dependencies.add(property.name); contextual_dependencies.add(property.name);
} }
}
if (store) { if (store) {
set_store = b`${store}.set(${`$${store}`});`; set_store = b`${store}.set(${`$${store}`});`;

@ -13,6 +13,7 @@ import Slot from './Slot';
import Text from './Text'; import Text from './Text';
import Title from './Title'; import Title from './Title';
import Window from './Window'; import Window from './Window';
import WithBlock from './WithBlock';
import { INode } from '../../nodes/interfaces'; import { INode } from '../../nodes/interfaces';
import Renderer from '../Renderer'; import Renderer from '../Renderer';
import Block from '../Block'; import Block from '../Block';
@ -36,7 +37,8 @@ const wrappers = {
Slot, Slot,
Text, Text,
Title, Title,
Window Window,
WithBlock
}; };
function trimmable_at(child: INode, next_sibling: Wrapper): boolean { function trimmable_at(child: INode, next_sibling: Wrapper): boolean {

@ -346,7 +346,9 @@ export default class InlineComponentWrapper extends Wrapper {
const { name } = binding.expression.node; const { name } = binding.expression.node;
const { object, property, snippet } = block.bindings.get(name); const { object, property, snippet } = block.bindings.get(name);
lhs = snippet; lhs = snippet;
contextual_dependencies.push(object.name, property.name); contextual_dependencies.push(object.name);
if (property !== undefined)
contextual_dependencies.push(property.name);
} }
const params = [x`#value`]; const params = [x`#value`];

@ -0,0 +1,183 @@
import Renderer from '../Renderer';
import Block from '../Block';
import Wrapper from './shared/Wrapper';
import create_debugging_comment from './shared/create_debugging_comment';
import FragmentWrapper from './Fragment';
import { b, x } from 'code-red';
import WithBlock from '../../nodes/WithBlock';
import { Node, Identifier } from 'estree';
export default class WithBlockWrapper extends Wrapper {
block: Block;
node: WithBlock;
fragment: FragmentWrapper;
vars: {
create_with_block: Identifier;
with_block_value: Identifier;
with_block: Identifier;
get_with_context: Identifier;
}
dependencies: Set<string>;
var: Identifier = { type: 'Identifier', name: 'with' };
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: WithBlock,
strip_whitespace: boolean,
next_sibling: Wrapper
) {
super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
block.add_dependencies(node.expression.dependencies);
this.node.contexts.forEach(context => {
renderer.add_to_context(context.key.name, true);
});
this.block = block.child({
comment: create_debugging_comment(this.node, this.renderer.component),
name: renderer.component.get_unique_name('create_with_block'),
type: 'with',
// @ts-ignore todo: probably error
key: node.key as string,
bindings: new Map(block.bindings)
});
const with_block_value = renderer.component.get_unique_name(`${this.var.name}_value`);
renderer.add_to_context(with_block_value.name, true);
this.vars = {
create_with_block: this.block.name,
with_block_value,
with_block: block.get_unique_name(`${this.var.name}_block`),
get_with_context: renderer.component.get_unique_name(`get_${this.var.name}_context`),
};
const store =
node.expression.node.type === 'Identifier' &&
node.expression.node.name[0] === '$'
? node.expression.node.name.slice(1)
: null;
node.contexts.forEach(prop => {
this.block.bindings.set(prop.key.name, {
object: with_block_value,
modifier: prop.modifier,
snippet: prop.modifier(x`${with_block_value}` as Node),
store
});
});
renderer.blocks.push(this.block);
this.fragment = new FragmentWrapper(renderer, this.block, node.children, this, strip_whitespace, next_sibling);
block.add_dependencies(this.block.dependencies);
}
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
if (this.fragment.nodes.length === 0) return;
const { renderer } = this;
const {
create_with_block,
with_block,
with_block_value,
get_with_context
} = this.vars;
const needs_anchor = this.next
? !this.next.is_dom_node() :
!parent_node || !this.parent.is_dom_node();
const context_props = this.node.contexts.map(prop => b`child_ctx[${renderer.context_lookup.get(prop.key.name).index}] = ${prop.modifier(x`value`)};`);
if (this.node.has_binding) context_props.push(b`child_ctx[${renderer.context_lookup.get(with_block_value.name).index}] = value;`);
const snippet = this.node.expression.manipulate(block);
block.chunks.init.push(b`let ${with_block_value} = ${snippet};`);
renderer.blocks.push(b`
function ${get_with_context}(#ctx, value) {
const child_ctx = #ctx.slice();
${context_props}
return child_ctx;
}
`);
const initial_anchor_node: Identifier = { type: 'Identifier', name: parent_node ? 'null' : 'anchor' };
const initial_mount_node: Identifier = parent_node || { type: 'Identifier', name: '#target' };
const update_anchor_node = needs_anchor
? block.get_unique_name(`${this.var.name}_anchor`)
: (this.next && this.next.var) || { type: 'Identifier', name: 'null' };
const update_mount_node: Identifier = this.get_update_mount_node((update_anchor_node as Identifier));
const all_dependencies = new Set(this.block.dependencies); // TODO should be dynamic deps only
this.node.expression.dynamic_dependencies().forEach((dependency: string) => {
all_dependencies.add(dependency);
});
this.dependencies = all_dependencies;
block.chunks.init.push(b`
let ${with_block} = ${create_with_block}(${get_with_context}(#ctx, ${with_block_value}));
`);
block.chunks.create.push(b`
${with_block}.c();
`);
if (parent_nodes && this.renderer.options.hydratable) {
block.chunks.claim.push(b`
${with_block}.l(${parent_nodes});
`);
}
block.chunks.mount.push(b`
${with_block}.m(${initial_mount_node}, ${initial_anchor_node});
`);
if (this.dependencies.size) {
const update = this.block.has_update_method
? b`
if (${with_block}) {
${with_block}.p(child_ctx, #dirty);
} else {
${with_block} = ${create_with_block}(child_ctx);
${with_block}.c();
${with_block}.m(${update_mount_node}, ${update_anchor_node});
}`
: b`
if (!${with_block}) {
${with_block} = ${create_with_block}(child_ctx);
${with_block}.c();
${with_block}.m(${update_mount_node}, ${update_anchor_node});
}`;
block.chunks.update.push(b`
if (${block.renderer.dirty(Array.from(all_dependencies))}) {
${with_block_value} = ${snippet};
const child_ctx = ${get_with_context}(#ctx, ${with_block_value});
${update}
}
`);
}
block.chunks.destroy.push(b`${with_block}.d(detaching);`);
if (needs_anchor) {
block.add_element(
update_anchor_node as Identifier,
x`@empty()`,
parent_nodes && x`@empty()`,
parent_node
);
}
this.fragment.render(this.block, null, x`#nodes` as Identifier);
}
}

@ -11,6 +11,7 @@ import Slot from './handlers/Slot';
import Tag from './handlers/Tag'; import Tag from './handlers/Tag';
import Text from './handlers/Text'; import Text from './handlers/Text';
import Title from './handlers/Title'; import Title from './handlers/Title';
import WithBlock from './handlers/WithBlock';
import { AppendTarget, CompileOptions } from '../../interfaces'; import { AppendTarget, CompileOptions } from '../../interfaces';
import { INode } from '../nodes/interfaces'; import { INode } from '../nodes/interfaces';
import { Expression, TemplateLiteral, Identifier } from 'estree'; import { Expression, TemplateLiteral, Identifier } from 'estree';
@ -36,7 +37,8 @@ const handlers: Record<string, Handler> = {
Slot, Slot,
Text, Text,
Title, Title,
Window: noop Window: noop,
WithBlock
}; };
export interface RenderOptions extends CompileOptions{ export interface RenderOptions extends CompileOptions{

@ -0,0 +1,12 @@
import Renderer, { RenderOptions } from '../Renderer';
import WithBlock from '../../nodes/WithBlock';
import { x } from 'code-red';
export default function(node: WithBlock, renderer: Renderer, options: RenderOptions) {
const args = [node.context_node];
renderer.push();
renderer.render(node.children, options);
const result = renderer.pop();
renderer.add_expression(x`((${args}) => ${result})(${node.expression.node})`);
}

@ -38,7 +38,7 @@ export default function mustache(parser: Parser) {
parser.allow_whitespace(); parser.allow_whitespace();
// {/if}, {/each} or {/await} // {/if}, {/each}, {/await}, or {/with}
if (parser.eat('/')) { if (parser.eat('/')) {
let block = parser.current(); let block = parser.current();
let expected; let expected;
@ -63,6 +63,8 @@ export default function mustache(parser: Parser) {
expected = 'each'; expected = 'each';
} else if (block.type === 'AwaitBlock') { } else if (block.type === 'AwaitBlock') {
expected = 'await'; expected = 'await';
} else if (block.type === 'WithBlock') {
expected = 'with';
} else { } else {
parser.error({ parser.error({
code: `unexpected-block-close`, code: `unexpected-block-close`,
@ -212,7 +214,7 @@ export default function mustache(parser: Parser) {
await_block[is_then ? 'then' : 'catch'] = new_block; await_block[is_then ? 'then' : 'catch'] = new_block;
parser.stack.push(new_block); parser.stack.push(new_block);
} else if (parser.eat('#')) { } else if (parser.eat('#')) {
// {#if foo}, {#each foo} or {#await foo} // {#if foo}, {#each foo}, {#await foo}, or {#with foo}
let type; let type;
if (parser.eat('if')) { if (parser.eat('if')) {
@ -221,10 +223,12 @@ export default function mustache(parser: Parser) {
type = 'EachBlock'; type = 'EachBlock';
} else if (parser.eat('await')) { } else if (parser.eat('await')) {
type = 'AwaitBlock'; type = 'AwaitBlock';
} else if (parser.eat('with')) {
type = 'WithBlock';
} else { } else {
parser.error({ parser.error({
code: `expected-block-type`, code: `expected-block-type`,
message: `Expected if, each or await` message: `Expected if, each, await, or with`
}); });
} }
@ -272,8 +276,8 @@ export default function mustache(parser: Parser) {
parser.allow_whitespace(); parser.allow_whitespace();
// {#each} blocks must declare a context {#each list as item} // {#each} and {#with} blocks must declare a context {#each list as item}
if (type === 'EachBlock') { if (type === 'EachBlock' || type === 'WithBlock') {
parser.eat('as', true); parser.eat('as', true);
parser.require_whitespace(); parser.require_whitespace();

@ -0,0 +1,45 @@
export default {
props: {
people: { name: { first: 'Doctor', last: 'Who' } },
},
html: `
<input>
<input>
<p>Doctor Who</p>
`,
ssrHtml: `
<input value=Doctor>
<input value=Who>
<p>Doctor Who</p>
`,
async test({ assert, component, target, window }) {
const inputs = target.querySelectorAll('input');
inputs[1].value = 'Oz';
await inputs[1].dispatchEvent(new window.Event('input'));
const { people } = component;
assert.deepEqual(people, {
name: { first: 'Doctor', last: 'Oz' }
});
assert.htmlEqual(target.innerHTML, `
<input>
<input>
<p>Doctor Oz</p>
`);
people.name.first = 'Frank';
component.people = people;
assert.htmlEqual(target.innerHTML, `
<input>
<input>
<p>Frank Oz</p>
`);
},
};

@ -0,0 +1,9 @@
<script>
export let people;
</script>
{#with people as { name: { first: f, last: l } } }
<input bind:value={f}>
<input bind:value={l}>
<p>{f} {l}</p>
{/with}

@ -0,0 +1,16 @@
export default {
html: `
<button>1</button>
`,
async test({ assert, target, window, }) {
const btn = target.querySelector('button');
const clickEvent = new window.MouseEvent('click');
await btn.dispatchEvent(clickEvent);
assert.htmlEqual(target.innerHTML, `
<button>2</button>
`);
}
};

@ -0,0 +1,7 @@
<script>
let a = '1'
</script>
{#with a as b }
<button on:click={e => a = '2'}>{b}</button>
{/with}
Loading…
Cancel
Save