update animate chapter

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

@ -3,6 +3,8 @@
import { crossfade } from 'svelte/transition';
const [send, receive] = crossfade({
duration: d => Math.sqrt(d * 200),
fallback(node, params) {
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform;
@ -18,17 +20,17 @@
}
});
let uid = 1;
let todos = [
{ id: 1, done: false, description: 'write some docs' },
{ id: 2, done: false, description: 'start writing JSConf talk' },
{ id: 3, done: true, description: 'buy some milk' },
{ id: 4, done: false, description: 'mow the lawn' },
{ id: 5, done: false, description: 'feed the turtle' },
{ id: 6, done: false, description: 'fix some bugs' },
{ id: uid++, done: false, description: 'write some docs' },
{ id: uid++, done: false, description: 'start writing blog post' },
{ id: uid++, done: true, description: 'buy some milk' },
{ id: uid++, done: false, description: 'mow the lawn' },
{ id: uid++, done: false, description: 'feed the turtle' },
{ id: uid++, done: false, description: 'fix some bugs' },
];
let uid = todos.length + 1;
function add(input) {
const todo = {
id: uid++,
@ -44,13 +46,42 @@
todos = todos.filter(t => t !== todo);
}
function handleKeydown(event) {
if (event.which === 13) {
add(event.target);
}
function mark(todo, done) {
todo.done = done;
remove(todo);
todos = todos.concat(todo);
}
</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>
.board {
display: grid;
@ -60,7 +91,7 @@
margin: 0 auto;
}
.new-todo {
.board > input {
font-size: 1.4em;
grid-column: 1/3;
}
@ -73,69 +104,45 @@
}
label {
line-height: 1;
padding: 0.5em;
position: relative;
line-height: 1.2;
padding: 0.5em 2.5em 0.5em 2em;
margin: 0 0 0.5em 0;
border-radius: 2px;
user-select: none;
border: 1px solid rgba(103,103,120, 0.5);
background-color: rgba(103,103,120,0.1);
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;
}
.done {
opacity: 0.3;
border: 1px solid hsl(240, 8%, 90%);
background-color:hsl(240, 8%, 98%);
}
button {
float: right;
height: 1em;
box-sizing: border-box;
padding: 0 0.5em;
line-height: 1;
background-color: transparent;
position: absolute;
top: 0;
right: 0.2em;
width: 2em;
height: 100%;
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;
color: rgb(170,30,30);
opacity: 0;
transition: opacity 0.2s;
text-indent: -9999px;
cursor: pointer;
}
label:hover button {
opacity: 1;
}
</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>
</style>

@ -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>
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) {
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform;
@ -18,17 +20,17 @@
}
});
let uid = 1;
let todos = [
{ id: 1, done: false, description: 'write some docs' },
{ id: 2, done: false, description: 'start writing JSConf talk' },
{ id: 3, done: true, description: 'buy some milk' },
{ id: 4, done: false, description: 'mow the lawn' },
{ id: 5, done: false, description: 'feed the turtle' },
{ id: 6, done: false, description: 'fix some bugs' },
{ id: uid++, done: false, description: 'write some docs' },
{ id: uid++, done: false, description: 'start writing blog post' },
{ id: uid++, done: true, description: 'buy some milk' },
{ id: uid++, done: false, description: 'mow the lawn' },
{ id: uid++, done: false, description: 'feed the turtle' },
{ id: uid++, done: false, description: 'fix some bugs' },
];
let uid = todos.length + 1;
function add(input) {
const todo = {
id: uid++,
@ -44,103 +46,110 @@
todos = todos.filter(t => t !== todo);
}
function handleKeydown(event) {
if (event.which === 13) {
addTodo(event.target);
}
function mark(todo, done) {
todo.done = done;
remove(todo);
todos = todos.concat(todo);
}
</script>
<style>
.new-todo {
font-size: 1.4em;
width: 100%;
margin: 2em 0 1em 0;
}
<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
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 {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 1em;
max-width: 36em;
margin: 0 auto;
}
.left, .right {
float: left;
width: 50%;
padding: 0 1em 0 0;
box-sizing: border-box;
.board > input {
font-size: 1.4em;
grid-column: 1/3;
}
h2 {
font-size: 2em;
font-weight: 200;
user-select: none;
margin: 0 0 0.5em 0;
}
label {
top: 0;
left: 0;
display: block;
font-size: 1em;
line-height: 1;
padding: 0.5em;
margin: 0 auto 0.5em auto;
position: relative;
line-height: 1.2;
padding: 0.5em 2.5em 0.5em 2em;
margin: 0 0 0.5em 0;
border-radius: 2px;
background-color: #eee;
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 {
background-color: rgb(180,240,100);
.done {
border: 1px solid hsl(240, 8%, 90%);
background-color:hsl(240, 8%, 98%);
}
button {
float: right;
height: 1em;
box-sizing: border-box;
padding: 0 0.5em;
line-height: 1;
background-color: transparent;
position: absolute;
top: 0;
right: 0.2em;
width: 2em;
height: 100%;
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;
color: rgb(170,30,30);
opacity: 0;
transition: opacity 0.2s;
text-indent: -9999px;
cursor: pointer;
}
label:hover button {
opacity: 1;
}
</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>
</style>

@ -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.
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>
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) {
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform;
@ -18,17 +20,17 @@
}
});
let uid = 1;
let todos = [
{ id: 1, done: false, description: 'write some docs' },
{ id: 2, done: false, description: 'start writing JSConf talk' },
{ id: 3, done: true, description: 'buy some milk' },
{ id: 4, done: false, description: 'mow the lawn' },
{ id: 5, done: false, description: 'feed the turtle' },
{ id: 6, done: false, description: 'fix some bugs' },
{ id: uid++, done: false, description: 'write some docs' },
{ id: uid++, done: false, description: 'start writing blog post' },
{ id: uid++, done: true, description: 'buy some milk' },
{ id: uid++, done: false, description: 'mow the lawn' },
{ id: uid++, done: false, description: 'feed the turtle' },
{ id: uid++, done: false, description: 'fix some bugs' },
];
let uid = todos.length + 1;
function add(input) {
const todo = {
id: uid++,
@ -44,103 +46,110 @@
todos = todos.filter(t => t !== todo);
}
function handleKeydown(event) {
if (event.which === 13) {
addTodo(event.target);
}
function mark(todo, done) {
todo.done = done;
remove(todo);
todos = todos.concat(todo);
}
</script>
<style>
.new-todo {
font-size: 1.4em;
width: 100%;
margin: 2em 0 1em 0;
}
<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
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 {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 1em;
max-width: 36em;
margin: 0 auto;
}
.left, .right {
float: left;
width: 50%;
padding: 0 1em 0 0;
box-sizing: border-box;
.board > input {
font-size: 1.4em;
grid-column: 1/3;
}
h2 {
font-size: 2em;
font-weight: 200;
user-select: none;
margin: 0 0 0.5em 0;
}
label {
top: 0;
left: 0;
display: block;
font-size: 1em;
line-height: 1;
padding: 0.5em;
margin: 0 auto 0.5em auto;
position: relative;
line-height: 1.2;
padding: 0.5em 2.5em 0.5em 2em;
margin: 0 0 0.5em 0;
border-radius: 2px;
background-color: #eee;
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 {
background-color: rgb(180,240,100);
.done {
border: 1px solid hsl(240, 8%, 90%);
background-color:hsl(240, 8%, 98%);
}
button {
float: right;
height: 1em;
box-sizing: border-box;
padding: 0 0.5em;
line-height: 1;
background-color: transparent;
position: absolute;
top: 0;
right: 0.2em;
width: 2em;
height: 100%;
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;
color: rgb(170,30,30);
opacity: 0;
transition: opacity 0.2s;
text-indent: -9999px;
cursor: pointer;
}
label:hover button {
opacity: 1;
}
</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>
</style>

@ -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>
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) {
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform;
@ -18,17 +21,17 @@
}
});
let uid = 1;
let todos = [
{ id: 1, done: false, description: 'write some docs' },
{ id: 2, done: false, description: 'start writing JSConf talk' },
{ id: 3, done: true, description: 'buy some milk' },
{ id: 4, done: false, description: 'mow the lawn' },
{ id: 5, done: false, description: 'feed the turtle' },
{ id: 6, done: false, description: 'fix some bugs' },
{ id: uid++, done: false, description: 'write some docs' },
{ id: uid++, done: false, description: 'start writing blog post' },
{ id: uid++, done: true, description: 'buy some milk' },
{ id: uid++, done: false, description: 'mow the lawn' },
{ id: uid++, done: false, description: 'feed the turtle' },
{ id: uid++, done: false, description: 'fix some bugs' },
];
let uid = todos.length + 1;
function add(input) {
const todo = {
id: uid++,
@ -44,103 +47,112 @@
todos = todos.filter(t => t !== todo);
}
function handleKeydown(event) {
if (event.which === 13) {
addTodo(event.target);
}
function mark(todo, done) {
todo.done = done;
remove(todo);
todos = todos.concat(todo);
}
</script>
<style>
.new-todo {
font-size: 1.4em;
width: 100%;
margin: 2em 0 1em 0;
}
<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
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 {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 1em;
max-width: 36em;
margin: 0 auto;
}
.left, .right {
float: left;
width: 50%;
padding: 0 1em 0 0;
box-sizing: border-box;
.board > input {
font-size: 1.4em;
grid-column: 1/3;
}
h2 {
font-size: 2em;
font-weight: 200;
user-select: none;
margin: 0 0 0.5em 0;
}
label {
top: 0;
left: 0;
display: block;
font-size: 1em;
line-height: 1;
padding: 0.5em;
margin: 0 auto 0.5em auto;
position: relative;
line-height: 1.2;
padding: 0.5em 2.5em 0.5em 2em;
margin: 0 0 0.5em 0;
border-radius: 2px;
background-color: #eee;
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 {
background-color: rgb(180,240,100);
.done {
border: 1px solid hsl(240, 8%, 90%);
background-color:hsl(240, 8%, 98%);
}
button {
float: right;
height: 1em;
box-sizing: border-box;
padding: 0 0.5em;
line-height: 1;
background-color: transparent;
position: absolute;
top: 0;
right: 0.2em;
width: 2em;
height: 100%;
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;
color: rgb(170,30,30);
opacity: 0;
transition: opacity 0.2s;
text-indent: -9999px;
cursor: pointer;
}
label:hover button {
opacity: 1;
}
</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>
</style>

@ -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
---
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