diff --git a/src/compile/Component.ts b/src/compile/Component.ts
index 36730ed09e..e2b7011622 100644
--- a/src/compile/Component.ts
+++ b/src/compile/Component.ts
@@ -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, {
diff --git a/src/compile/nodes/shared/Expression.ts b/src/compile/nodes/shared/Expression.ts
index c7a3d5de91..e3e2fb975e 100644
--- a/src/compile/nodes/shared/Expression.ts
+++ b/src/compile/nodes/shared/Expression.ts
@@ -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 {
diff --git a/src/compile/render-dom/index.ts b/src/compile/render-dom/index.ts
index ffc0634e02..305505df24 100644
--- a/src/compile/render-dom/index.ts
+++ b/src/compile/render-dom/index.ts
@@ -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};`}
diff --git a/src/compile/render-ssr/index.ts b/src/compile/render-ssr/index.ts
index 28d47475a5..51353d82f0 100644
--- a/src/compile/render-ssr/index.ts
+++ b/src/compile/render-ssr/index.ts
@@ -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}\`;`;
diff --git a/src/internal/ssr.js b/src/internal/ssr.js
index a6eb13107b..170f841ec6 100644
--- a/src/internal/ssr.js
+++ b/src/internal/ssr.js
@@ -97,4 +97,10 @@ export function create_ssr_component($$render) {
$$render
};
+}
+
+export function get_store_value(store) {
+ let value;
+ store.subscribe(_ => value = _)();
+ return value;
}
\ No newline at end of file
diff --git a/src/internal/utils.js b/src/internal/utils.js
index 55feb743e2..420a306508 100644
--- a/src/internal/utils.js
+++ b/src/internal/utils.js
@@ -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`);
+ }
}
\ No newline at end of file
diff --git a/test/runtime/samples/store-auto-subscribe-in-each/_config.js b/test/runtime/samples/store-auto-subscribe-in-each/_config.js
new file mode 100644
index 0000000000..969f9a9b74
--- /dev/null
+++ b/test/runtime/samples/store-auto-subscribe-in-each/_config.js
@@ -0,0 +1,40 @@
+import { writable } from '../../../../store.js';
+
+export default {
+ skip: true,
+
+ props: {
+ things: [
+ writable('a'),
+ writable('b'),
+ writable('c')
+ ]
+ },
+
+ html: `
+
+
+
+ `,
+
+ 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, `
+
+
+
+ `);
+
+ await component.things[1].set('d');
+
+ assert.htmlEqual(target.innerHTML, `
+
+
+
+ `);
+ }
+};
\ No newline at end of file
diff --git a/test/runtime/samples/store-auto-subscribe-in-each/main.html b/test/runtime/samples/store-auto-subscribe-in-each/main.html
new file mode 100644
index 0000000000..baf120838c
--- /dev/null
+++ b/test/runtime/samples/store-auto-subscribe-in-each/main.html
@@ -0,0 +1,7 @@
+
+
+{#each things as thing}
+
+{/each}
\ No newline at end of file
diff --git a/test/runtime/samples/store-auto-subscribe-in-reactive-declaration/_config.js b/test/runtime/samples/store-auto-subscribe-in-reactive-declaration/_config.js
new file mode 100644
index 0000000000..b2a5a8dba7
--- /dev/null
+++ b/test/runtime/samples/store-auto-subscribe-in-reactive-declaration/_config.js
@@ -0,0 +1,28 @@
+import { writable } from '../../../../store.js';
+
+export default {
+ props: {
+ count: writable(0)
+ },
+
+ html: `
+
+ `,
+
+ 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, `
+
+ `);
+
+ await component.count.set(42);
+
+ assert.htmlEqual(target.innerHTML, `
+
+ `);
+ }
+};
\ No newline at end of file
diff --git a/test/runtime/samples/store-auto-subscribe-in-reactive-declaration/main.html b/test/runtime/samples/store-auto-subscribe-in-reactive-declaration/main.html
new file mode 100644
index 0000000000..3233d38d2d
--- /dev/null
+++ b/test/runtime/samples/store-auto-subscribe-in-reactive-declaration/main.html
@@ -0,0 +1,8 @@
+
+
+
\ No newline at end of file
diff --git a/test/runtime/samples/store-auto-subscribe/_config.js b/test/runtime/samples/store-auto-subscribe/_config.js
new file mode 100644
index 0000000000..b8203f2a68
--- /dev/null
+++ b/test/runtime/samples/store-auto-subscribe/_config.js
@@ -0,0 +1,28 @@
+import { writable } from '../../../../store.js';
+
+export default {
+ props: {
+ count: writable(0)
+ },
+
+ html: `
+
+ `,
+
+ 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, `
+
+ `);
+
+ await component.count.set(42);
+
+ assert.htmlEqual(target.innerHTML, `
+
+ `);
+ }
+};
\ No newline at end of file
diff --git a/test/runtime/samples/store-auto-subscribe/main.html b/test/runtime/samples/store-auto-subscribe/main.html
new file mode 100644
index 0000000000..849de85acf
--- /dev/null
+++ b/test/runtime/samples/store-auto-subscribe/main.html
@@ -0,0 +1,5 @@
+
+
+
\ No newline at end of file
diff --git a/test/runtime/samples/store-dev-mode-error/_config.js b/test/runtime/samples/store-dev-mode-error/_config.js
new file mode 100644
index 0000000000..70b24337fd
--- /dev/null
+++ b/test/runtime/samples/store-dev-mode-error/_config.js
@@ -0,0 +1,11 @@
+export default {
+ compileOptions: {
+ dev: true
+ },
+
+ props: {
+ count: 0
+ },
+
+ error: `'count' is not a store with a 'subscribe' method`
+};
\ No newline at end of file
diff --git a/test/runtime/samples/store-dev-mode-error/main.html b/test/runtime/samples/store-dev-mode-error/main.html
new file mode 100644
index 0000000000..849de85acf
--- /dev/null
+++ b/test/runtime/samples/store-dev-mode-error/main.html
@@ -0,0 +1,5 @@
+
+
+
\ No newline at end of file
diff --git a/test/runtime/samples/store-prevent-user-declarations/_config.js b/test/runtime/samples/store-prevent-user-declarations/_config.js
new file mode 100644
index 0000000000..312348eba4
--- /dev/null
+++ b/test/runtime/samples/store-prevent-user-declarations/_config.js
@@ -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`
+};
\ No newline at end of file
diff --git a/test/runtime/samples/store-prevent-user-declarations/main.html b/test/runtime/samples/store-prevent-user-declarations/main.html
new file mode 100644
index 0000000000..78bed1d520
--- /dev/null
+++ b/test/runtime/samples/store-prevent-user-declarations/main.html
@@ -0,0 +1,6 @@
+
+
+
\ No newline at end of file