document stores

pull/2179/head
Richard Harris 7 years ago
parent 4d653f2635
commit 3e8ea124ea

@ -0,0 +1,18 @@
<script>
import { count } from './stores.js';
import Incrementer from './Incrementer.svelte';
import Decrementer from './Decrementer.svelte';
import Resetter from './Resetter.svelte';
let count_value;
const unsubscribe = count.subscribe(value => {
count_value = value;
});
</script>
<h1>The count is {count_value}</h1>
<Incrementer/>
<Decrementer/>
<Resetter/>

@ -0,0 +1,11 @@
<script>
import { count } from './stores.js';
function decrement() {
// TODO decrement the count
}
</script>
<button on:click={decrement}>
-
</button>

@ -0,0 +1,11 @@
<script>
import { count } from './stores.js';
function increment() {
// TODO increment the count
}
</script>
<button on:click={increment}>
+
</button>

@ -0,0 +1,11 @@
<script>
import { count } from './stores.js';
function reset() {
// TODO reset the count
}
</script>
<button on:click={reset}>
reset
</button>

@ -0,0 +1,3 @@
import { writable } from 'svelte/store';
export const count = writable(0);

@ -0,0 +1,18 @@
<script>
import { count } from './stores.js';
import Incrementer from './Incrementer.svelte';
import Decrementer from './Decrementer.svelte';
import Resetter from './Resetter.svelte';
let count_value;
const unsubscribe = count.subscribe(value => {
count_value = value;
});
</script>
<h1>The count is {count_value}</h1>
<Incrementer/>
<Decrementer/>
<Resetter/>

@ -0,0 +1,11 @@
<script>
import { count } from './stores.js';
function decrement() {
count.update(n => n - 1);
}
</script>
<button on:click={decrement}>
-
</button>

@ -0,0 +1,11 @@
<script>
import { count } from './stores.js';
function increment() {
count.update(n => n + 1);
}
</script>
<button on:click={increment}>
+
</button>

@ -0,0 +1,11 @@
<script>
import { count } from './stores.js';
function reset() {
count.set(0);
}
</script>
<button on:click={reset}>
reset
</button>

@ -0,0 +1,3 @@
import { writable } from 'svelte/store';
export const count = writable(0);

@ -0,0 +1,27 @@
---
title: Writable stores
---
Not all application state belongs inside your application's component hierarchy. Sometimes, you'll have values that need to be accessed by multiple unrelated components, or by a regular JavaScript module.
In Svelte, we do this with *stores*. A store is simply an object with a `subscribe` method that allows interested parties to be notified whenever the store value changes. In `App.svelte`, `count` is a store, and we're setting `count_value` in the `count.subscribe` callback.
Click the `stores.js` tab to see the definition of `count`. It's a *writable* store, which means it has `set` and `update` methods in addition to `subscribe`.
Now go to the `Incrementer.svelte` tab so that we can wire up the `+` button:
```js
function increment() {
count.update(n => n + 1);
}
```
Clicking the `+` button should now update the count. Do the inverse for `Decrementer.svelte`.
Finally, in `Resetter.svelte`, implement `reset`:
```js
function reset() {
count.set(0);
}
```

@ -0,0 +1,18 @@
<script>
import { count } from './stores.js';
import Incrementer from './Incrementer.svelte';
import Decrementer from './Decrementer.svelte';
import Resetter from './Resetter.svelte';
let count_value;
const unsubscribe = count.subscribe(value => {
count_value = value;
});
</script>
<h1>The count is {count_value}</h1>
<Incrementer/>
<Decrementer/>
<Resetter/>

@ -0,0 +1,11 @@
<script>
import { count } from './stores.js';
function decrement() {
count.update(n => n - 1);
}
</script>
<button on:click={decrement}>
-
</button>

@ -0,0 +1,11 @@
<script>
import { count } from './stores.js';
function increment() {
count.update(n => n + 1);
}
</script>
<button on:click={increment}>
+
</button>

@ -0,0 +1,11 @@
<script>
import { count } from './stores.js';
function reset() {
count.set(0);
}
</script>
<button on:click={reset}>
reset
</button>

@ -0,0 +1,3 @@
import { writable } from 'svelte/store';
export const count = writable(0);

@ -0,0 +1,12 @@
<script>
import { count } from './stores.js';
import Incrementer from './Incrementer.svelte';
import Decrementer from './Decrementer.svelte';
import Resetter from './Resetter.svelte';
</script>
<h1>The count is {$count}</h1>
<Incrementer/>
<Decrementer/>
<Resetter/>

@ -0,0 +1,11 @@
<script>
import { count } from './stores.js';
function decrement() {
count.update(n => n - 1);
}
</script>
<button on:click={decrement}>
-
</button>

@ -0,0 +1,11 @@
<script>
import { count } from './stores.js';
function increment() {
count.update(n => n + 1);
}
</script>
<button on:click={increment}>
+
</button>

@ -0,0 +1,11 @@
<script>
import { count } from './stores.js';
function reset() {
count.set(0);
}
</script>
<button on:click={reset}>
reset
</button>

@ -0,0 +1,3 @@
import { writable } from 'svelte/store';
export const count = writable(0);

@ -0,0 +1,44 @@
---
title: Auto-subscriptions
---
The app in the previous example works, but there's a subtle bug — the `unsubscribe` function never gets called. If the component was instantiated and destroyed many times, this would result in a *memory leak*.
One way to fix it would be to use the `onDestroy` [lifecycle hook](tutorial/ondestroy):
```html
<script>
import { onDestroy } from 'svelte';
import { count } from './stores.js';
import Incrementer from './Incrementer.svelte';
import Decrementer from './Decrementer.svelte';
import Resetter from './Resetter.svelte';
let count_value;
const unsubscribe = count.subscribe(value => {
count_value = value;
});
onDestroy(unsubscribe);
</script>
<h1>The count is {count_value}</h1>
```
It starts to get a bit boilerplatey though, especially if your component subscribes to multiple stores. Instead, Svelte has a trick up its sleeve — you can reference a store value by prefixing the store name with `$`:
```html
<script>
import { count } from './stores.js';
import Incrementer from './Incrementer.svelte';
import Decrementer from './Decrementer.svelte';
import Resetter from './Resetter.svelte';
</script>
<h1>The count is {$count}</h1>
```
You're not limited to using `$count` inside the markup, either — you can use it anywhere in the `<script>` as well, such as in event handlers or reactive declarations.
> Any name beginning with `$` is assumed to refer to a store value. It's effectively a reserved character — don't declare your own variables with a `$` prefix.

@ -0,0 +1,12 @@
<script>
import { time } from './stores.js';
const formatter = new Intl.DateTimeFormat('en', {
hour12: true,
hour: 'numeric',
minute: '2-digit',
second: '2-digit'
});
</script>
<h1>The time is {formatter.format($time)}</h1>

@ -0,0 +1,9 @@
import { readable } from 'svelte/store';
export const time = readable(function start(set) {
// implementation goes here
return function stop() {
};
});

@ -0,0 +1,12 @@
<script>
import { time } from './stores.js';
const formatter = new Intl.DateTimeFormat('en', {
hour12: true,
hour: 'numeric',
minute: '2-digit',
second: '2-digit'
});
</script>
<h1>The time is {formatter.format($time)}</h1>

@ -0,0 +1,11 @@
import { readable } from 'svelte/store';
export const time = readable(function start(set) {
const interval = setInterval(() => {
set(new Date());
}, 1000);
return function stop() {
clearInterval(interval);
};
}, new Date());

@ -0,0 +1,19 @@
---
title: Readable stores
---
Not all stores should be writable by whoever has a reference to them. For example, you might have a store representing the mouse position or the user's geolocation, and it doesn't make sense to be able to set those values from 'outside'. For those cases, we have *readable* stores.
Click over to the `stores.js` tab. The first argument to `readable` is a `start` function that takes a `set` callback and returns a `stop` function. The `start` function is called when the store gets its first subscriber; `stop` is called when the last subscriber unsubscribes. The second (optional) argument is the initial value.
```js
export const time = readable(function start(set) {
const interval = setInterval(() => {
set(new Date());
}, 1000);
return function stop() {
clearInterval(interval);
};
}, new Date());
```

@ -0,0 +1,17 @@
<script>
import { time, elapsed } from './stores.js';
const formatter = new Intl.DateTimeFormat('en', {
hour12: true,
hour: 'numeric',
minute: '2-digit',
second: '2-digit'
});
</script>
<h1>The time is {formatter.format($time)}</h1>
<p>
This page has been open for
{$elapsed} {$elapsed === 1 ? 'second' : 'seconds'}
</p>

@ -0,0 +1,18 @@
import { readable, derive } from 'svelte/store';
export const time = readable(function start(set) {
const interval = setInterval(() => {
set(new Date());
}, 1000);
return function stop() {
clearInterval(interval);
};
}, new Date());
const start = new Date();
export const elapsed = derive(
time,
$time => {}
);

@ -0,0 +1,17 @@
<script>
import { time, elapsed } from './stores.js';
const formatter = new Intl.DateTimeFormat('en', {
hour12: true,
hour: 'numeric',
minute: '2-digit',
second: '2-digit'
});
</script>
<h1>The time is {formatter.format($time)}</h1>
<p>
This page has been open for
{$elapsed} {$elapsed === 1 ? 'second' : 'seconds'}
</p>

@ -0,0 +1,18 @@
import { readable, derive } from 'svelte/store';
export const time = readable(function start(set) {
const interval = setInterval(() => {
set(new Date());
}, 1000);
return function stop() {
clearInterval(interval);
};
}, new Date());
const start = new Date();
export const elapsed = derive(
time,
$time => Math.round(($time - start) / 1000)
);

@ -0,0 +1,14 @@
---
title: Derived stores
---
You can create a store whose value is based on the value of one or more *other* stores with `derive`. Building on our previous example, we can create a store that derives the time the page has been open:
```js
export const elapsed = derive(
time,
$time => Math.round(($time - start) / 1000)
);
```
> It's possible to derive a store from multiple inputs, and to explicitly `set` a value instead of returning it (which is useful for deriving values asynchronously). Consult the [API reference](docs/TK) for more information.

@ -0,0 +1,9 @@
<script>
import { count } from './stores.js';
</script>
<h1>The count is {$count}</h1>
<button on:click={count.increment}>+</button>
<button on:click={count.decrement}>-</button>
<button on:click={count.reset}>reset</button>

@ -0,0 +1,14 @@
import { writable } from 'svelte/store';
function createCount() {
const { subscribe, set, update } = writable(0);
return {
subscribe,
increment: () => {},
decrement: () => {},
reset: () => {}
};
}
export const count = createCount();

@ -0,0 +1,9 @@
<script>
import { count } from './stores.js';
</script>
<h1>The count is {$count}</h1>
<button on:click={count.increment}>+</button>
<button on:click={count.decrement}>-</button>
<button on:click={count.reset}>reset</button>

@ -0,0 +1,14 @@
import { writable } from 'svelte/store';
function createCount() {
const { subscribe, set, update } = writable(0);
return {
subscribe,
increment: () => update(n => n + 1),
decrement: () => update(n => n - 1),
reset: () => set(0)
};
}
export const count = createCount();

@ -0,0 +1,21 @@
---
title: Custom stores
---
As long as an object correctly implements the `subscribe` method, it's a store. Beyond that, anything goes. It's very easy, therefore, to create custom stores with domain-specific logic.
For example, the `count` store from our earlier example could include `increment`, `decrement` and `reset` methods and avoid exposing `set` and `update`:
```js
function createCount() {
const { subscribe, set, update } = writable(0);
return {
subscribe,
increment: () => update(n => n + 1),
decrement: () => update(n => n - 1),
reset: () => set(0)
};
}
```

@ -78,13 +78,21 @@ Maybe lifecycle should go first, since we're using `onMount` in the `this` demo?
## Stores ## Stores
* [ ] `writable` (and second argument?) * [x] `writable` (and second argument?)
* [ ] `$foo` * [x] `$foo`
* [ ] `readable` * [x] `readable`
* [ ] `derive` * [x] `derive`
* [ ] custom stores
* [ ] `bind:value={$foo}`
* [ ] `$foo += 1` (if we implement it) * [ ] `$foo += 1` (if we implement it)
## Motion
* [ ] `tweened`
* [ ] `spring`
## Lifecycle ## Lifecycle
* [ ] `onMount` * [ ] `onMount`

@ -26,6 +26,7 @@
let repl; let repl;
let prev; let prev;
let scrollable;
const lookup = new Map(); const lookup = new Map();
sections.forEach(section => { sections.forEach(section => {
@ -46,6 +47,9 @@
}); });
}); });
// TODO is there a non-hacky way to trigger scroll when chapter changes?
$: if (scrollable) chapter, scrollable.scrollTo(0, 0);
// TODO: this will need to be changed to the master branch, and probably should be dynamic instead of included // TODO: this will need to be changed to the master branch, and probably should be dynamic instead of included
// here statically // here statically
const tutorial_repo_link = 'https://github.com/sveltejs/svelte/tree/restructure-docs/site/content/tutorial'; const tutorial_repo_link = 'https://github.com/sveltejs/svelte/tree/restructure-docs/site/content/tutorial';
@ -226,7 +230,7 @@
<TableOfContents {sections} {slug} {selected}/> <TableOfContents {sections} {slug} {selected}/>
</div> </div>
<div class="chapter-markup"> <div class="chapter-markup" bind:this={scrollable}>
{@html chapter.html} {@html chapter.html}
<div class="controls"> <div class="controls">

@ -9,27 +9,37 @@ function get_sections() {
const sections = fs.readdirSync(`content/tutorial`) const sections = fs.readdirSync(`content/tutorial`)
.filter(dir => /^\d+/.test(dir)) .filter(dir => /^\d+/.test(dir))
.map(dir => { .map(dir => {
const meta = JSON.parse(fs.readFileSync(`content/tutorial/${dir}/meta.json`, 'utf-8')); let meta;
try {
meta = JSON.parse(fs.readFileSync(`content/tutorial/${dir}/meta.json`, 'utf-8'));
} catch (err) {
throw new Error(`Error reading metadata for ${dir}`);
}
return { return {
title: meta.title, title: meta.title,
chapters: fs.readdirSync(`content/tutorial/${dir}`) chapters: fs.readdirSync(`content/tutorial/${dir}`)
.filter(dir => /^\d+/.test(dir)) .filter(dir => /^\d+/.test(dir))
.map(tutorial => { .map(tutorial => {
const md = fs.readFileSync(`content/tutorial/${dir}/${tutorial}/text.md`, 'utf-8'); try {
const { metadata, content } = extract_frontmatter(md); const md = fs.readFileSync(`content/tutorial/${dir}/${tutorial}/text.md`, 'utf-8');
const { metadata } = extract_frontmatter(md);
const slug = tutorial.replace(/^\d+-/, ''); const slug = tutorial.replace(/^\d+-/, '');
if (slugs.has(slug)) throw new Error(`Duplicate slug: ${slug}`); if (slugs.has(slug)) throw new Error(`Duplicate slug: ${slug}`);
slugs.add(slug); slugs.add(slug);
return { return {
slug, slug,
title: metadata.title, title: metadata.title,
section_dir: dir, section_dir: dir,
chapter_dir: tutorial, chapter_dir: tutorial,
}; };
} catch (err) {
throw new Error(`Error building tutorial ${dir}/${tutorial}: ${err.message}`);
}
}) })
} }
}); });
@ -38,13 +48,23 @@ function get_sections() {
} }
export function get(req, res) { export function get(req, res) {
if (!json || process.env.NODE_ENV !== 'production') { try {
json = JSON.stringify(get_sections()); if (!json || process.env.NODE_ENV !== 'production') {
} json = JSON.stringify(get_sections());
}
res.set({ res.set({
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}); });
res.end(json); res.end(json);
} catch (err) {
res.writeHead(500, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify({
message: err.message
}));
}
} }

Loading…
Cancel
Save