update animate chapter

pull/2432/head
Rich Harris 6 years ago
parent 2a0c36e7c8
commit bb10298f2c

@ -3,6 +3,8 @@
import { crossfade } from 'svelte/transition'; import { crossfade } from 'svelte/transition';
const [send, receive] = crossfade({ const [send, receive] = crossfade({
duration: d => Math.sqrt(d * 200),
fallback(node, params) { fallback(node, params) {
const style = getComputedStyle(node); const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform; const transform = style.transform === 'none' ? '' : style.transform;
@ -18,17 +20,17 @@
} }
}); });
let uid = 1;
let todos = [ let todos = [
{ id: 1, done: false, description: 'write some docs' }, { id: uid++, done: false, description: 'write some docs' },
{ id: 2, done: false, description: 'start writing JSConf talk' }, { id: uid++, done: false, description: 'start writing blog post' },
{ id: 3, done: true, description: 'buy some milk' }, { id: uid++, done: true, description: 'buy some milk' },
{ id: 4, done: false, description: 'mow the lawn' }, { id: uid++, done: false, description: 'mow the lawn' },
{ id: 5, done: false, description: 'feed the turtle' }, { id: uid++, done: false, description: 'feed the turtle' },
{ id: 6, done: false, description: 'fix some bugs' }, { id: uid++, done: false, description: 'fix some bugs' },
]; ];
let uid = todos.length + 1;
function add(input) { function add(input) {
const todo = { const todo = {
id: uid++, id: uid++,
@ -44,13 +46,42 @@
todos = todos.filter(t => t !== todo); todos = todos.filter(t => t !== todo);
} }
function handleKeydown(event) { function mark(todo, done) {
if (event.which === 13) { todo.done = done;
add(event.target); remove(todo);
} todos = todos.concat(todo);
} }
</script> </script>
<div class='board'>
<input
placeholder="what needs to be done?"
on:keydown={e => e.which === 13 && add(e.target)}
>
<div class='left'>
<h2>todo</h2>
{#each todos.filter(t => !t.done) as todo (todo.id)}
<label>
<input type=checkbox on:change={() => mark(todo, true)}>
{todo.description}
<button on:click="{() => remove(todo)}">remove</button>
</label>
{/each}
</div>
<div class='right'>
<h2>done</h2>
{#each todos.filter(t => t.done) as todo (todo.id)}
<label class="done">
<input type=checkbox checked on:change={() => mark(todo, false)}>
{todo.description}
<button on:click="{() => remove(todo)}">remove</button>
</label>
{/each}
</div>
</div>
<style> <style>
.board { .board {
display: grid; display: grid;
@ -60,7 +91,7 @@
margin: 0 auto; margin: 0 auto;
} }
.new-todo { .board > input {
font-size: 1.4em; font-size: 1.4em;
grid-column: 1/3; grid-column: 1/3;
} }
@ -73,69 +104,45 @@
} }
label { label {
line-height: 1; position: relative;
padding: 0.5em; line-height: 1.2;
padding: 0.5em 2.5em 0.5em 2em;
margin: 0 0 0.5em 0; margin: 0 0 0.5em 0;
border-radius: 2px; border-radius: 2px;
user-select: none; user-select: none;
border: 1px solid rgba(103,103,120, 0.5); border: 1px solid hsl(240, 8%, 70%);
background-color: rgba(103,103,120,0.1); background-color:hsl(240, 8%, 93%);
color: #333; color: #333;
} }
input { margin: 0 } input[type="checkbox"] {
position: absolute;
left: 0.5em;
top: 0.6em;
margin: 0;
}
.done { .done {
opacity: 0.3; border: 1px solid hsl(240, 8%, 90%);
background-color:hsl(240, 8%, 98%);
} }
button { button {
float: right; position: absolute;
height: 1em; top: 0;
box-sizing: border-box; right: 0.2em;
padding: 0 0.5em; width: 2em;
line-height: 1; height: 100%;
background-color: transparent; background: no-repeat 50% 50% url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23676778' d='M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M17,7H14.5L13.5,6H10.5L9.5,7H7V9H17V7M9,18H15A1,1 0 0,0 16,17V10H8V17A1,1 0 0,0 9,18Z'%3E%3C/path%3E%3C/svg%3E");
background-size: 1.4em 1.4em;
border: none; border: none;
color: rgb(170,30,30);
opacity: 0; opacity: 0;
transition: opacity 0.2s; transition: opacity 0.2s;
text-indent: -9999px;
cursor: pointer;
} }
label:hover button { label:hover button {
opacity: 1; opacity: 1;
} }
</style> </style>
<div class='board'>
<input class="new-todo" placeholder="what needs to be done?" on:keydown={handleKeydown}>
<div class='left'>
<h2>todo</h2>
{#each todos.filter(t => !t.done) as todo (todo.id)}
<label
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
>
<input type=checkbox bind:checked={todo.done}>
{todo.description}
<button on:click="{() => remove(todo)}">x</button>
</label>
{/each}
</div>
<div class='right'>
<h2>done</h2>
{#each todos.filter(t => t.done) as todo (todo.id)}
<label
class="done"
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
>
<input type=checkbox bind:checked={todo.done}>
{todo.description}
<button on:click="{() => remove(todo)}">x</button>
</label>
{/each}
</div>
</div>

@ -1,65 +0,0 @@
import { quintOut } from 'svelte/easing';
export default function crossfade({ send, receive, fallback }) {
let requested = new Map();
let provided = new Map();
function crossfade(from, node) {
const to = node.getBoundingClientRect();
const dx = from.left - to.left;
const dy = from.top - to.top;
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform;
return {
duration: 400,
easing: quintOut,
css: (t, u) => `
opacity: ${t};
transform: ${transform} translate(${u * dx}px,${u * dy}px);
`
};
}
return {
send(node, params) {
provided.set(params.key, {
rect: node.getBoundingClientRect()
});
return () => {
if (requested.has(params.key)) {
const { rect } = requested.get(params.key);
requested.delete(params.key);
return crossfade(rect, node);
}
// if the node is disappearing altogether
// (i.e. wasn't claimed by the other list)
// then we need to supply an outro
provided.delete(params.key);
return fallback(node, params);
};
},
receive(node, params) {
requested.set(params.key, {
rect: node.getBoundingClientRect()
});
return () => {
if (provided.has(params.key)) {
const { rect } = provided.get(params.key);
provided.delete(params.key);
return crossfade(rect, node);
}
requested.delete(params.key);
return fallback(node, params);
};
}
};
}

@ -1,8 +1,10 @@
<script> <script>
import { quintOut } from 'svelte/easing'; import { quintOut } from 'svelte/easing';
import crossfade from './crossfade.js'; // TODO put this in svelte/transition! import { crossfade } from 'svelte/transition';
const [send, receive] = crossfade({
duration: d => Math.sqrt(d * 200),
const { send, receive } = crossfade({
fallback(node, params) { fallback(node, params) {
const style = getComputedStyle(node); const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform; const transform = style.transform === 'none' ? '' : style.transform;
@ -18,17 +20,17 @@
} }
}); });
let uid = 1;
let todos = [ let todos = [
{ id: 1, done: false, description: 'write some docs' }, { id: uid++, done: false, description: 'write some docs' },
{ id: 2, done: false, description: 'start writing JSConf talk' }, { id: uid++, done: false, description: 'start writing blog post' },
{ id: 3, done: true, description: 'buy some milk' }, { id: uid++, done: true, description: 'buy some milk' },
{ id: 4, done: false, description: 'mow the lawn' }, { id: uid++, done: false, description: 'mow the lawn' },
{ id: 5, done: false, description: 'feed the turtle' }, { id: uid++, done: false, description: 'feed the turtle' },
{ id: 6, done: false, description: 'fix some bugs' }, { id: uid++, done: false, description: 'fix some bugs' },
]; ];
let uid = todos.length + 1;
function add(input) { function add(input) {
const todo = { const todo = {
id: uid++, id: uid++,
@ -44,103 +46,110 @@
todos = todos.filter(t => t !== todo); todos = todos.filter(t => t !== todo);
} }
function handleKeydown(event) { function mark(todo, done) {
if (event.which === 13) { todo.done = done;
addTodo(event.target); remove(todo);
} todos = todos.concat(todo);
} }
</script> </script>
<style> <div class='board'>
.new-todo { <input
font-size: 1.4em; placeholder="what needs to be done?"
width: 100%; on:keydown={e => e.which === 13 && add(e.target)}
margin: 2em 0 1em 0; >
}
<div class='left'>
<h2>todo</h2>
{#each todos.filter(t => !t.done) as todo (todo.id)}
<label
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
>
<input type=checkbox on:change={() => mark(todo, true)}>
{todo.description}
<button on:click="{() => remove(todo)}">remove</button>
</label>
{/each}
</div>
<div class='right'>
<h2>done</h2>
{#each todos.filter(t => t.done) as todo (todo.id)}
<label
class="done"
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
>
<input type=checkbox checked on:change={() => mark(todo, false)}>
{todo.description}
<button on:click="{() => remove(todo)}">remove</button>
</label>
{/each}
</div>
</div>
<style>
.board { .board {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 1em;
max-width: 36em; max-width: 36em;
margin: 0 auto; margin: 0 auto;
} }
.left, .right { .board > input {
float: left; font-size: 1.4em;
width: 50%; grid-column: 1/3;
padding: 0 1em 0 0;
box-sizing: border-box;
} }
h2 { h2 {
font-size: 2em; font-size: 2em;
font-weight: 200; font-weight: 200;
user-select: none; user-select: none;
margin: 0 0 0.5em 0;
} }
label { label {
top: 0; position: relative;
left: 0; line-height: 1.2;
display: block; padding: 0.5em 2.5em 0.5em 2em;
font-size: 1em; margin: 0 0 0.5em 0;
line-height: 1;
padding: 0.5em;
margin: 0 auto 0.5em auto;
border-radius: 2px; border-radius: 2px;
background-color: #eee;
user-select: none; user-select: none;
border: 1px solid hsl(240, 8%, 70%);
background-color:hsl(240, 8%, 93%);
color: #333;
} }
input { margin: 0 } input[type="checkbox"] {
position: absolute;
left: 0.5em;
top: 0.6em;
margin: 0;
}
.right label { .done {
background-color: rgb(180,240,100); border: 1px solid hsl(240, 8%, 90%);
background-color:hsl(240, 8%, 98%);
} }
button { button {
float: right; position: absolute;
height: 1em; top: 0;
box-sizing: border-box; right: 0.2em;
padding: 0 0.5em; width: 2em;
line-height: 1; height: 100%;
background-color: transparent; background: no-repeat 50% 50% url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23676778' d='M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M17,7H14.5L13.5,6H10.5L9.5,7H7V9H17V7M9,18H15A1,1 0 0,0 16,17V10H8V17A1,1 0 0,0 9,18Z'%3E%3C/path%3E%3C/svg%3E");
background-size: 1.4em 1.4em;
border: none; border: none;
color: rgb(170,30,30);
opacity: 0; opacity: 0;
transition: opacity 0.2s; transition: opacity 0.2s;
text-indent: -9999px;
cursor: pointer;
} }
label:hover button { label:hover button {
opacity: 1; opacity: 1;
} }
</style> </style>
<div class='board'>
<input class="new-todo" placeholder="what needs to be done?" on:enter={add}>
<div class='left'>
<h2>todo</h2>
{#each todos.filter(t => !t.done) as todo (todo.id)}
<label
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
>
<input type=checkbox bind:checked={todo.done}>
{todo.description}
<button on:click="{() => remove(todo)}">x</button>
</label>
{/each}
</div>
<div class='right'>
<h2>done</h2>
{#each todos.filter(t => t.done) as todo (todo.id)}
<label
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
>
<input type=checkbox bind:checked={todo.done}>
{todo.description}
<button on:click="{() => remove(todo)}">x</button>
</label>
{/each}
</div>
</div>

@ -1,65 +0,0 @@
import { quintOut } from 'svelte/easing';
export default function crossfade({ send, receive, fallback }) {
let requested = new Map();
let provided = new Map();
function crossfade(from, node) {
const to = node.getBoundingClientRect();
const dx = from.left - to.left;
const dy = from.top - to.top;
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform;
return {
duration: 400,
easing: quintOut,
css: (t, u) => `
opacity: ${t};
transform: ${transform} translate(${u * dx}px,${u * dy}px);
`
};
}
return {
send(node, params) {
provided.set(params.key, {
rect: node.getBoundingClientRect()
});
return () => {
if (requested.has(params.key)) {
const { rect } = requested.get(params.key);
requested.delete(params.key);
return crossfade(rect, node);
}
// if the node is disappearing altogether
// (i.e. wasn't claimed by the other list)
// then we need to supply an outro
provided.delete(params.key);
return fallback(node, params);
};
},
receive(node, params) {
requested.set(params.key, {
rect: node.getBoundingClientRect()
});
return () => {
if (provided.has(params.key)) {
const { rect } = provided.get(params.key);
provided.delete(params.key);
return crossfade(rect, node);
}
requested.delete(params.key);
return fallback(node, params);
};
}
};
}

@ -4,4 +4,27 @@ title: Deferred transitions
A particularly powerful feature of Svelte's transition engine is the ability to *defer* transitions, so that they can be coordinated between multiple elements. A particularly powerful feature of Svelte's transition engine is the ability to *defer* transitions, so that they can be coordinated between multiple elements.
TODO https://github.com/sveltejs/svelte/issues/2159 Take this pair of todo lists, in which toggling a todo sends it to the opposite list. In the real world, objects don't behave like that — instead of disappearing and reappearing in another place, they move through a series of intermediate positions. Using motion can go a long way towards helping users understand what's happening in your app.
We can achieve this effect using the `crossfade` function, which creates a pair of transitions called `send` and `receive`. When an element is 'sent', it looks for a corresponding element being 'received', and generates a transition that transforms the element to its counterpart's position and fades it out. When an element is 'received', the reverse happens. If there is no counterpart, the `fallback` transition is used.
Find the `<label>` element on line 65, and add the `send` and `receive` transitions:
```html
<label
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
>
```
Do the same for the next `<label>` element:
```html
<label
class="done"
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
>
```
Now, when you toggle items, they move smoothly to their new location. The non-transitioning items still jump around awkwardly — we can fix that in the next chapter.

@ -1,8 +1,10 @@
<script> <script>
import { quintOut } from 'svelte/easing'; import { quintOut } from 'svelte/easing';
import crossfade from './crossfade.js'; // TODO put this in svelte/transition! import { crossfade } from 'svelte/transition';
const [send, receive] = crossfade({
duration: d => Math.sqrt(d * 200),
const { send, receive } = crossfade({
fallback(node, params) { fallback(node, params) {
const style = getComputedStyle(node); const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform; const transform = style.transform === 'none' ? '' : style.transform;
@ -18,17 +20,17 @@
} }
}); });
let uid = 1;
let todos = [ let todos = [
{ id: 1, done: false, description: 'write some docs' }, { id: uid++, done: false, description: 'write some docs' },
{ id: 2, done: false, description: 'start writing JSConf talk' }, { id: uid++, done: false, description: 'start writing blog post' },
{ id: 3, done: true, description: 'buy some milk' }, { id: uid++, done: true, description: 'buy some milk' },
{ id: 4, done: false, description: 'mow the lawn' }, { id: uid++, done: false, description: 'mow the lawn' },
{ id: 5, done: false, description: 'feed the turtle' }, { id: uid++, done: false, description: 'feed the turtle' },
{ id: 6, done: false, description: 'fix some bugs' }, { id: uid++, done: false, description: 'fix some bugs' },
]; ];
let uid = todos.length + 1;
function add(input) { function add(input) {
const todo = { const todo = {
id: uid++, id: uid++,
@ -44,103 +46,110 @@
todos = todos.filter(t => t !== todo); todos = todos.filter(t => t !== todo);
} }
function handleKeydown(event) { function mark(todo, done) {
if (event.which === 13) { todo.done = done;
addTodo(event.target); remove(todo);
} todos = todos.concat(todo);
} }
</script> </script>
<style> <div class='board'>
.new-todo { <input
font-size: 1.4em; placeholder="what needs to be done?"
width: 100%; on:keydown={e => e.which === 13 && add(e.target)}
margin: 2em 0 1em 0; >
}
<div class='left'>
<h2>todo</h2>
{#each todos.filter(t => !t.done) as todo (todo.id)}
<label
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
>
<input type=checkbox on:change={() => mark(todo, true)}>
{todo.description}
<button on:click="{() => remove(todo)}">remove</button>
</label>
{/each}
</div>
<div class='right'>
<h2>done</h2>
{#each todos.filter(t => t.done) as todo (todo.id)}
<label
class="done"
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
>
<input type=checkbox checked on:change={() => mark(todo, false)}>
{todo.description}
<button on:click="{() => remove(todo)}">remove</button>
</label>
{/each}
</div>
</div>
<style>
.board { .board {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 1em;
max-width: 36em; max-width: 36em;
margin: 0 auto; margin: 0 auto;
} }
.left, .right { .board > input {
float: left; font-size: 1.4em;
width: 50%; grid-column: 1/3;
padding: 0 1em 0 0;
box-sizing: border-box;
} }
h2 { h2 {
font-size: 2em; font-size: 2em;
font-weight: 200; font-weight: 200;
user-select: none; user-select: none;
margin: 0 0 0.5em 0;
} }
label { label {
top: 0; position: relative;
left: 0; line-height: 1.2;
display: block; padding: 0.5em 2.5em 0.5em 2em;
font-size: 1em; margin: 0 0 0.5em 0;
line-height: 1;
padding: 0.5em;
margin: 0 auto 0.5em auto;
border-radius: 2px; border-radius: 2px;
background-color: #eee;
user-select: none; user-select: none;
border: 1px solid hsl(240, 8%, 70%);
background-color:hsl(240, 8%, 93%);
color: #333;
} }
input { margin: 0 } input[type="checkbox"] {
position: absolute;
left: 0.5em;
top: 0.6em;
margin: 0;
}
.right label { .done {
background-color: rgb(180,240,100); border: 1px solid hsl(240, 8%, 90%);
background-color:hsl(240, 8%, 98%);
} }
button { button {
float: right; position: absolute;
height: 1em; top: 0;
box-sizing: border-box; right: 0.2em;
padding: 0 0.5em; width: 2em;
line-height: 1; height: 100%;
background-color: transparent; background: no-repeat 50% 50% url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23676778' d='M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M17,7H14.5L13.5,6H10.5L9.5,7H7V9H17V7M9,18H15A1,1 0 0,0 16,17V10H8V17A1,1 0 0,0 9,18Z'%3E%3C/path%3E%3C/svg%3E");
background-size: 1.4em 1.4em;
border: none; border: none;
color: rgb(170,30,30);
opacity: 0; opacity: 0;
transition: opacity 0.2s; transition: opacity 0.2s;
text-indent: -9999px;
cursor: pointer;
} }
label:hover button { label:hover button {
opacity: 1; opacity: 1;
} }
</style> </style>
<div class='board'>
<input class="new-todo" placeholder="what needs to be done?" on:enter={add}>
<div class='left'>
<h2>todo</h2>
{#each todos.filter(t => !t.done) as todo (todo.id)}
<label
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
>
<input type=checkbox bind:checked={todo.done}>
{todo.description}
<button on:click="{() => remove(todo)}">x</button>
</label>
{/each}
</div>
<div class='right'>
<h2>done</h2>
{#each todos.filter(t => t.done) as todo (todo.id)}
<label
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
>
<input type=checkbox bind:checked={todo.done}>
{todo.description}
<button on:click="{() => remove(todo)}">x</button>
</label>
{/each}
</div>
</div>

@ -1,65 +0,0 @@
import { quintOut } from 'svelte/easing';
export default function crossfade({ send, receive, fallback }) {
let requested = new Map();
let provided = new Map();
function crossfade(from, node) {
const to = node.getBoundingClientRect();
const dx = from.left - to.left;
const dy = from.top - to.top;
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform;
return {
duration: 400,
easing: quintOut,
css: (t, u) => `
opacity: ${t};
transform: ${transform} translate(${u * dx}px,${u * dy}px);
`
};
}
return {
send(node, params) {
provided.set(params.key, {
rect: node.getBoundingClientRect()
});
return () => {
if (requested.has(params.key)) {
const { rect } = requested.get(params.key);
requested.delete(params.key);
return crossfade(rect, node);
}
// if the node is disappearing altogether
// (i.e. wasn't claimed by the other list)
// then we need to supply an outro
provided.delete(params.key);
return fallback(node, params);
};
},
receive(node, params) {
requested.set(params.key, {
rect: node.getBoundingClientRect()
});
return () => {
if (provided.has(params.key)) {
const { rect } = provided.get(params.key);
provided.delete(params.key);
return crossfade(rect, node);
}
requested.delete(params.key);
return fallback(node, params);
};
}
};
}

@ -1,8 +1,11 @@
<script> <script>
import { quintOut } from 'svelte/easing'; import { quintOut } from 'svelte/easing';
import crossfade from './crossfade.js'; // TODO put this in svelte/transition! import { crossfade } from 'svelte/transition';
import { flip } from 'svelte/animate';
const [send, receive] = crossfade({
duration: d => Math.sqrt(d * 200),
const { send, receive } = crossfade({
fallback(node, params) { fallback(node, params) {
const style = getComputedStyle(node); const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform; const transform = style.transform === 'none' ? '' : style.transform;
@ -18,17 +21,17 @@
} }
}); });
let uid = 1;
let todos = [ let todos = [
{ id: 1, done: false, description: 'write some docs' }, { id: uid++, done: false, description: 'write some docs' },
{ id: 2, done: false, description: 'start writing JSConf talk' }, { id: uid++, done: false, description: 'start writing blog post' },
{ id: 3, done: true, description: 'buy some milk' }, { id: uid++, done: true, description: 'buy some milk' },
{ id: 4, done: false, description: 'mow the lawn' }, { id: uid++, done: false, description: 'mow the lawn' },
{ id: 5, done: false, description: 'feed the turtle' }, { id: uid++, done: false, description: 'feed the turtle' },
{ id: 6, done: false, description: 'fix some bugs' }, { id: uid++, done: false, description: 'fix some bugs' },
]; ];
let uid = todos.length + 1;
function add(input) { function add(input) {
const todo = { const todo = {
id: uid++, id: uid++,
@ -44,103 +47,112 @@
todos = todos.filter(t => t !== todo); todos = todos.filter(t => t !== todo);
} }
function handleKeydown(event) { function mark(todo, done) {
if (event.which === 13) { todo.done = done;
addTodo(event.target); remove(todo);
} todos = todos.concat(todo);
} }
</script> </script>
<style> <div class='board'>
.new-todo { <input
font-size: 1.4em; placeholder="what needs to be done?"
width: 100%; on:keydown={e => e.which === 13 && add(e.target)}
margin: 2em 0 1em 0; >
}
<div class='left'>
<h2>todo</h2>
{#each todos.filter(t => !t.done) as todo (todo.id)}
<label
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
animate:flip
>
<input type=checkbox on:change={() => mark(todo, true)}>
{todo.description}
<button on:click="{() => remove(todo)}">remove</button>
</label>
{/each}
</div>
<div class='right'>
<h2>done</h2>
{#each todos.filter(t => t.done) as todo (todo.id)}
<label
class="done"
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
animate:flip
>
<input type=checkbox checked on:change={() => mark(todo, false)}>
{todo.description}
<button on:click="{() => remove(todo)}">remove</button>
</label>
{/each}
</div>
</div>
<style>
.board { .board {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 1em;
max-width: 36em; max-width: 36em;
margin: 0 auto; margin: 0 auto;
} }
.left, .right { .board > input {
float: left; font-size: 1.4em;
width: 50%; grid-column: 1/3;
padding: 0 1em 0 0;
box-sizing: border-box;
} }
h2 { h2 {
font-size: 2em; font-size: 2em;
font-weight: 200; font-weight: 200;
user-select: none; user-select: none;
margin: 0 0 0.5em 0;
} }
label { label {
top: 0; position: relative;
left: 0; line-height: 1.2;
display: block; padding: 0.5em 2.5em 0.5em 2em;
font-size: 1em; margin: 0 0 0.5em 0;
line-height: 1;
padding: 0.5em;
margin: 0 auto 0.5em auto;
border-radius: 2px; border-radius: 2px;
background-color: #eee;
user-select: none; user-select: none;
border: 1px solid hsl(240, 8%, 70%);
background-color:hsl(240, 8%, 93%);
color: #333;
} }
input { margin: 0 } input[type="checkbox"] {
position: absolute;
left: 0.5em;
top: 0.6em;
margin: 0;
}
.right label { .done {
background-color: rgb(180,240,100); border: 1px solid hsl(240, 8%, 90%);
background-color:hsl(240, 8%, 98%);
} }
button { button {
float: right; position: absolute;
height: 1em; top: 0;
box-sizing: border-box; right: 0.2em;
padding: 0 0.5em; width: 2em;
line-height: 1; height: 100%;
background-color: transparent; background: no-repeat 50% 50% url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23676778' d='M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M17,7H14.5L13.5,6H10.5L9.5,7H7V9H17V7M9,18H15A1,1 0 0,0 16,17V10H8V17A1,1 0 0,0 9,18Z'%3E%3C/path%3E%3C/svg%3E");
background-size: 1.4em 1.4em;
border: none; border: none;
color: rgb(170,30,30);
opacity: 0; opacity: 0;
transition: opacity 0.2s; transition: opacity 0.2s;
text-indent: -9999px;
cursor: pointer;
} }
label:hover button { label:hover button {
opacity: 1; opacity: 1;
} }
</style> </style>
<div class='board'>
<input class="new-todo" placeholder="what needs to be done?" on:enter={add}>
<div class='left'>
<h2>todo</h2>
{#each todos.filter(t => !t.done) as todo (todo.id)}
<label
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
>
<input type=checkbox bind:checked={todo.done}>
{todo.description}
<button on:click="{() => remove(todo)}">x</button>
</label>
{/each}
</div>
<div class='right'>
<h2>done</h2>
{#each todos.filter(t => t.done) as todo (todo.id)}
<label
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
>
<input type=checkbox bind:checked={todo.done}>
{todo.description}
<button on:click="{() => remove(todo)}">x</button>
</label>
{/each}
</div>
</div>

@ -1,65 +0,0 @@
import { quintOut } from 'svelte/easing';
export default function crossfade({ send, receive, fallback }) {
let requested = new Map();
let provided = new Map();
function crossfade(from, node) {
const to = node.getBoundingClientRect();
const dx = from.left - to.left;
const dy = from.top - to.top;
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform;
return {
duration: 400,
easing: quintOut,
css: (t, u) => `
opacity: ${t};
transform: ${transform} translate(${u * dx}px,${u * dy}px);
`
};
}
return {
send(node, params) {
provided.set(params.key, {
rect: node.getBoundingClientRect()
});
return () => {
if (requested.has(params.key)) {
const { rect } = requested.get(params.key);
requested.delete(params.key);
return crossfade(rect, node);
}
// if the node is disappearing altogether
// (i.e. wasn't claimed by the other list)
// then we need to supply an outro
provided.delete(params.key);
return fallback(node, params);
};
},
receive(node, params) {
requested.set(params.key, {
rect: node.getBoundingClientRect()
});
return () => {
if (provided.has(params.key)) {
const { rect } = provided.get(params.key);
provided.delete(params.key);
return crossfade(rect, node);
}
requested.delete(params.key);
return fallback(node, params);
};
}
};
}

@ -2,4 +2,36 @@
title: The animate directive title: The animate directive
--- ---
TODO fix https://github.com/sveltejs/svelte/issues/2159 before working on `animate` In the [previous chapter](tutorial/deferred-transitions), we used deferred transitions to create the illusion of motion as elements move from one todo list to the other.
To complete the illusion, we also need to apply motion to the elements that *aren't* transitioning. For this, we use the `animate` directive.
First, import the `flip` function — flip stands for ['First, Last, Invert, Play'](https://aerotwist.com/blog/flip-your-animations/) — from `svelte/animate`:
```js
import { flip } from 'svelte/animate';
```
Then add it to the `<label>` elements:
```html
<label
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
animate:flip
>
```
The movement is a little slow in this case, so we can add a `duration` parameter:
```html
<label
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
animate:flip="{{duration: 200}}"
>
```
> `duration` can also be a `d => milliseconds` function, where `d` is the number of pixels the element has to travel
Note that all the transitions and animations are being applied with CSS, rather than JavaScript, meaning they won't block (or be blocked by) the main thread.
Loading…
Cancel
Save