store auto-subscriptions

pull/1882/head
Rich Harris 6 years ago committed by GitHub
parent 48f1f6b4d0
commit f991a41d83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -428,6 +428,13 @@ export default class Component {
imports.push(node);
node.specifiers.forEach((specifier: Node) => {
if (specifier.local.name[0] === '$') {
this.error(specifier.local, {
code: 'illegal-declaration',
message: `The $ prefix is reserved, and cannot be used for variable and import names`
});
}
this.userVars.add(specifier.local.name);
this.imported_declarations.add(specifier.local.name);
});
@ -481,7 +488,14 @@ export default class Component {
let { scope } = createScopes(script.content);
this.module_scope = scope;
// TODO unindent
scope.declarations.forEach((node, name) => {
if (name[0] === '$') {
this.error(node, {
code: 'illegal-declaration',
message: `The $ prefix is reserved, and cannot be used for variable and import names`
});
}
});
this.extract_imports_and_exports(script.content, this.imports, this.module_exports);
remove_indentation(this.code, script.content);
@ -498,6 +512,15 @@ export default class Component {
this.instance_scope = instance_scope;
this.instance_scope_map = map;
instance_scope.declarations.forEach((node, name) => {
if (name[0] === '$') {
this.error(node, {
code: 'illegal-declaration',
message: `The $ prefix is reserved, and cannot be used for variable and import names`
});
}
});
instance_scope.declarations.forEach((node, name) => {
this.userVars.add(name);
this.declarations.push(name);
@ -790,10 +813,19 @@ export default class Component {
assignees.add(getObject(node.argument).name);
this.skip();
} else if (isReference(node, parent)) {
const { name } = getObject(node);
const object = getObject(node);
const { name } = object;
if (component.declarations.indexOf(name) !== -1) {
dependencies.add(name);
} else if (name[0] === '$') {
component.warn_if_undefined(object, null);
// cheeky hack
component.template_references.add(name);
dependencies.add(name);
}
this.skip();
}
},
@ -882,12 +914,17 @@ export default class Component {
}
warn_if_undefined(node, template_scope: TemplateScope, allow_implicit?: boolean) {
const { name } = node;
let { name } = node;
if (name[0] === '$') {
name = name.slice(1);
this.has_reactive_assignments = true;
}
if (allow_implicit && !this.instance_script) return;
if (this.instance_scope && this.instance_scope.declarations.has(name)) return;
if (this.module_scope && this.module_scope.declarations.has(name)) return;
if (template_scope.names.has(name)) return;
if (template_scope && template_scope.names.has(name)) return;
if (globalWhitelist.has(name)) return;
this.warn(node, {

@ -118,7 +118,7 @@ export default class Expression {
// conditions — it doesn't apply if the dependency is inside a
// function, and it only applies if the dependency is writable
if (component.instance_script) {
if (component.writable_declarations.has(name)) {
if (component.writable_declarations.has(name) || name[0] === '$') {
dynamic_dependencies.add(name);
}
} else {

@ -257,11 +257,15 @@ export default function dom(
return true;
});
const reactive_stores = Array.from(component.template_references).filter(n => n[0] === '$');
filtered_declarations.push(...reactive_stores);
const has_definition = (
component.javascript ||
filtered_props.length > 0 ||
component.partly_hoisted.length > 0 ||
filtered_declarations.length > 0 ||
reactive_stores.length > 0 ||
component.reactive_declarations.length > 0
);
@ -280,6 +284,14 @@ export default function dom(
: null
);
const reactive_store_subscriptions = reactive_stores.length > 0 && reactive_stores
.map(name => deindent`
let ${name};
${component.options.dev && `@validate_store(${name.slice(1)}, '${name.slice(1)}');`}
$$self.$$.on_destroy.push(${name.slice(1)}.subscribe($$value => { ${name} = $$value; $$make_dirty('${name}'); }));
`)
.join('\n\n');
if (has_definition) {
builder.addBlock(deindent`
function ${definition}(${args.join(', ')}) {
@ -287,6 +299,8 @@ export default function dom(
${component.partly_hoisted.length > 0 && component.partly_hoisted.join('\n\n')}
${reactive_store_subscriptions}
${filtered_declarations.length > 0 && `$$self.$$.get = () => (${stringifyProps(filtered_declarations)});`}
${set && `$$self.$$.set = ${set};`}

@ -36,6 +36,15 @@ export default function ssr(
user_code = `let { ${props.join(', ')} } = $$props;`
}
const reactive_stores = Array.from(component.template_references).filter(n => n[0] === '$');
const reactive_store_values = reactive_stores.map(name => {
const assignment = `const ${name} = @get_store_value(${name.slice(1)});`;
return component.options.dev
? `@validate_store(${name.slice(1)}, '${name.slice(1)}'); ${assignment}`
: assignment;
});
// TODO only do this for props with a default value
const parent_bindings = component.javascript
? component.props.map(prop => {
@ -51,6 +60,8 @@ export default function ssr(
do {
$$settled = true;
${reactive_store_values}
${component.reactive_declarations.map(d => d.snippet)}
$$rendered = \`${renderer.code}\`;
@ -59,6 +70,8 @@ export default function ssr(
return $$rendered;
`
: deindent`
${reactive_store_values}
${component.reactive_declarations.map(d => d.snippet)}
return \`${renderer.code}\`;`;

@ -97,4 +97,10 @@ export function create_ssr_component($$render) {
$$render
};
}
export function get_store_value(store) {
let value;
store.subscribe(_ => value = _)();
return value;
}

@ -55,4 +55,10 @@ export function safe_not_equal(a, b) {
export function not_equal(a, b) {
return a != a ? b == b : a !== b;
}
export function validate_store(store, name) {
if (!store || typeof store.subscribe !== 'function') {
throw new Error(`'${name}' is not a store with a 'subscribe' method`);
}
}

@ -0,0 +1,40 @@
import { writable } from '../../../../store.js';
export default {
skip: true,
props: {
things: [
writable('a'),
writable('b'),
writable('c')
]
},
html: `
<button>a</button>
<button>b</button>
<button>c</button>
`,
async test({ assert, component, target, window }) {
const buttons = target.querySelectorAll('button');
const click = new window.MouseEvent('click');
await buttons[1].dispatchEvent(click);
assert.htmlEqual(target.innerHTML, `
<button>a</button>
<button>B</button>
<button>c</button>
`);
await component.things[1].set('d');
assert.htmlEqual(target.innerHTML, `
<button>d</button>
<button>B</button>
<button>c</button>
`);
}
};

@ -0,0 +1,7 @@
<script>
export let things;
</script>
{#each things as thing}
<button on:click="{() => thing.update(n => n.toUpperCase())}">{$thing}</button>
{/each}

@ -0,0 +1,28 @@
import { writable } from '../../../../store.js';
export default {
props: {
count: writable(0)
},
html: `
<button>double 0</button>
`,
async test({ assert, component, target, window }) {
const button = target.querySelector('button');
const click = new window.MouseEvent('click');
await button.dispatchEvent(click);
assert.htmlEqual(target.innerHTML, `
<button>double 2</button>
`);
await component.count.set(42);
assert.htmlEqual(target.innerHTML, `
<button>double 84</button>
`);
}
};

@ -0,0 +1,8 @@
<script>
export let count;
let double;
$: double = $count * 2;
</script>
<button on:click="{() => count.update(n => n + 1)}">double {double}</button>

@ -0,0 +1,28 @@
import { writable } from '../../../../store.js';
export default {
props: {
count: writable(0)
},
html: `
<button>count 0</button>
`,
async test({ assert, component, target, window }) {
const button = target.querySelector('button');
const click = new window.MouseEvent('click');
await button.dispatchEvent(click);
assert.htmlEqual(target.innerHTML, `
<button>count 1</button>
`);
await component.count.set(42);
assert.htmlEqual(target.innerHTML, `
<button>count 42</button>
`);
}
};

@ -0,0 +1,5 @@
<script>
export let count;
</script>
<button on:click="{() => count.update(n => n + 1)}">count {$count}</button>

@ -0,0 +1,11 @@
export default {
compileOptions: {
dev: true
},
props: {
count: 0
},
error: `'count' is not a store with a 'subscribe' method`
};

@ -0,0 +1,5 @@
<script>
export let count;
</script>
<button on:click="{() => count.update(n => n + 1)}">count {$count}</button>

@ -0,0 +1,9 @@
import { writable } from '../../../../store.js';
export default {
props: {
count: writable(0)
},
error: `The $ prefix is reserved, and cannot be used for variable and import names`
};

@ -0,0 +1,6 @@
<script>
export let count;
let $count;
</script>
<button on:click="{() => count.update(n => n + 1)}">count {$count}</button>
Loading…
Cancel
Save