Build the basics – a Todo application

To start off our Todo application, let's go ahead and utilize the template that we already have. Now, in most Todo applications, we want to be able to do the following things:

  • Add
  • Remove/mark complete
  • Update

So what we have is a basic CRUD application without any server operations. Let's go ahead and write the Svelte HTML that we would expect for this application:

<script>
import { createEventDispatcher } from 'svelte';
export let completed;
export let num;
export let description;

const dispatch = createEventDispatcher();
</script>
<style>
.completed {
text-decoration: line-through;
}
</style>
<li class:completed>
Task {num}: {description}
<input type="checkbox" bind:checked={completed} />
<button on:click="{() => dispatch('remove', null)}">Remove</button>
</li>

We have split our Todo application into a Todo component and the general application. The Todo element will hold all of our logic for the completion and deletion of the element. As we can see from the preceding example, we are doing the following things:

  • We expose the number this task is and the description.
  • We have a completed property that is hidden from the main application.
  • We have a class for styling a completed item.
  • The list element with the completion variable is bound to the complete class.
  • The num and description properties are tied to information.
  • A checkbox is added for when we complete an item.
  • And there's a button that will tell our application what we want to be removed.

That is quite a bit to digest, but when we put it all together, we will see that this holds most of the logic for an individual Todo item. Now, we need to add all of the logic for our application. It should look something like the following:

<script>
import Todo from './Todo.svelte';

let newTodoText = '';
const Todos = new Set();

function addTodo() {
const todo = new Todo({
target: document.querySelector('#main'),
props: {
num : Todos.size,
description : newTodoText
}
});
newTodoText = '';
todo.$on('remove', () => {
Todos.delete(todo);
todo.$destroy();
});
Todos.add(todo);
}
</script>
<style></style>
<h1>Todo Application!</h1>
<ul id="main">
</ul>
<button on:click={addTodo}>Add Todo</button>
<input type="text" bind:value={newTodoText} />

We first import the Todo that we created before. We then have newTodoText as a property bound to our input text. Then, we create a set to store all of our Todos. Next, we create an addTodo method that will be bound to the click event of our Add Todo button. This will create a new Todo, binding the element to our unordered list and setting the properties to our set size and input text respectively. We reset the Todo text, and add a remove listener to destroy the Todo and also remove it from our set. Finally, we add it to our set.

We now have a basic Todo application! All of this logic should be fairly straightforward. Let's add some additional features as we had in a previous chapter. We will add the following things to our Todo application to make it a bit more robust and useful:

  • Have due dates associated with each Todo
  • Keep a count of all the Todos
  • Create filters that will filter based on overdue, completed, and all
  • Transitions based on the filters and the addition of each Todo

First, let's add a due date to our Todo application. We will add a new exported field inside our Todo.svelte file called dueDate, and we will also add it to our template like the following:

//inside of script tag
export let dueDate;

//part of the template
<li class:completed>
Task {num}: {description} - Due on {dueDate}
<input type="checkbox" bind:checked={completed} />
<button on:click="{() => dispatch('remove', null)}">Remove</button>
</li>

Then, inside of our App.svelte file, we will add a date control and make sure that when we add our Todo to the list, we also make sure that we put this field back in. This should look like the following:

//inside of the script tag
let newTodoDate = null;
function addTodo() {
const todo = new Todo({
target: document.querySelector('#main'),
props: {
num : Todos.size + 1,
dueDate : newTodoDate,
description : newTodoText
}
});
newTodoText = '';
newTodoDate = null;
todo.$on('remove', () => {
Todos.delete(todo);
todo.$destroy();
});
Todos.add(todo);
}

//part of the template
<input type="date" bind:value={newTodoDate} />

We now have a fully functioning due date system. Next, we will add the number of current Todos to our application. This is as simple as binding some text in a span to the size of our set, as shown in the following code:

//inside of script tag
let currSize = 0;
function addTodo() {
const todo = new Todo({
// code removed for readability
});
todo.$on('remove', () => {
Todos.delete(todo);
currSize = Todos.size;
todo.$destroy();
});
Todos.add(todo);
currSize = Todos.size;
}

//part of the template
<h1>Todo Application! <span> Current number of Todos: {currSize}</span></h1>

Alright, now we want to be able to do something with all of the dates and completed states. Let's add some filters so we can remove Todos that do not fit our criteria. We will be adding the completed and overdue filters. We are going to make these checkboxes since an item can be both overdue and completed at the same time:

//inside of script tag
let completed = false;
let overdue = false;

//part of the template
<label><input type="checkbox" bind:checked={completed}
on:change={handleFilter}/>Completed</label>
<label><input type="checkbox" bind:checked={overdue}
on:change={handleFilter}/>Overdue</label>

Our handle filter logic should look something like the following:

function handleHide(item) {
const currDate = Date.now();
if( completed && overdue ) {
item.hidden = !item.completed || new Date(item.dueDate).getTime() < currDate;
return;
}
if( completed ) {
item.hidden = !item.completed;
return;
}
if( overdue ) {
item.hidden = new Date(item.dueDate).getTime() < currDate;
return;
}
item.hidden = false;
}

function handleFilter() {
for(const item of Todos) {
handleHide(item);
}
}

We also need to make sure that we have the same hide logic for any new Todo items:

const todo = new Todo({
target: document.querySelector('#main'),
props: {
num : Todos.size + 1,
dueDate : newTodoDate,
description : newTodoText
}
});
handleHide(todo);

Finally, our Todo.svelte component should look like the following:

<svelte:options accessors={true} />
<script>
import { createEventDispatcher } from 'svelte';

export let num;
export let description;
export let dueDate;
export let hidden = false;
export let completed = false;

const dispatch = createEventDispatcher();
</script>
<style>
.completed {
text-decoration: line-through;
}
.hidden {
display : none;
}
</style>
<li class:completed class:hidden>
Task {num}: {description} - Due on {dueDate}
<input type="checkbox" bind:checked={completed} />
<button on:click="{() => dispatch('remove', null)}">Remove</button>
</li>

Most of this should look familiar, except for the top portion. There are special tags that we can add to Svelte files that allow us access to certain properties, such as the following:

  • <svelte:window> gives us access to the window events.
  • <svelte:body> gives us access to the body events.
  • <svelte:head> gives us access to the head of the document.
  • <svelte:component> gives us access to ourselves as a DOM element.
  • <svelete:self> allows us to contain ourselves (for recursive structures such as trees).
  • <svelte:options> allows us to add compiler options to our component.

In this case, we want our parent component to be able to access our properties through getters/setters, so we set the accessors option to true. This is how we are able to change our hidden property inside of the App.svelte file and allows us to get properties that are on each Todo.

Finally, let's add in some fade in and out transitions. Svelte comes with some nice animations when we add/remove elements. The one that we are going to use is the fade animation. So, our Todo.svelte file will now have the following added:

//inside of script tag
import { fade } form 'svelte/transition';

//part of template
{#if !hidden}
<li in:fade out:fade class:completed>
Task {num}: {description} - Due on {dueDate}
<input type="checkbox" bind:checked={completed} />
<button on:click="{() => dispatch('remove', null)}">Remove</button>
</li>
{/if}

The special syntax is for conditional DOM addition/subtraction. The same way we can add/remove children with the DOM API, Svelte is doing the same. Next, we can see that we added the in:fade and out:fade directives to the list elements. Now, when the element is added or removed from the DOM, it will fade in and out.

We now have a fairly functional Todo application. We have filtering logic, Todos that are tied to due dates, and even a bit of animation. The next step is to clean up the code a bit. We can do this with the stores built into Svelte.

Stores are a way of sharing state without having to do some of the trickery that we have had to use in our application (we opened up the accessor system when we probably should not have). The shared state between our Todos and our main application is the overdue and completed filters. Each Todo should most likely be in control of this property, but we are currently utilizing the accessor option and all of the filtering is done in our main application. With a writable store, we no longer have to do that.

First, we write a stores.js file like the following:

import { writable } from 'svelte/store';

export const overdue = writable(false);
export const completed = writable(false);

Next, we update our App.svelte file to not target the hidden property in the Todos, and we bind the checked properties of our checkbox inputs to the stores like the following:

//inside of script tag
import { completed, overdue } from './stores.js';

//part of the template
<label><input type="checkbox" bind:checked={$completed} />Completed</label>
<label><input type="checkbox" bind:checked={$overdue} />Overdue</label>

The dollar sign in front of our stores means that these are stores and not variables in our scripts. It allows us to update and subscribe to the stores without having to unsubscribe from them on destroy. Finally, we can update our Todo.svelte file to look like the following:

<script>
import { overdue, completed } from './stores.js';
import { createEventDispatcher, onDestroy } from 'svelte';
import { fade } from 'svelte/transition';

export let num;
export let description;
export let dueDate;
let _completed = false;

const dispatch = createEventDispatcher();
</script>
<style>
.completed {
text-decoration: line-through;
}
</style>
{#if
!(
($completed && !_completed) ||
($overdue && new Date(dueDate).getTime() >= Date.now())
)
}
<li in:fade out:fade class:_completed>
Task {num}: {description} - Due on {dueDate}
<input type="checkbox" bind:checked={_completed} />
<button on:click="{() => dispatch('remove', null)}">Remove</button>
</li>
{/if}

We have added the overdue and completed stores to our system. You may have noticed that we got rid of the compiler option at the top of the file. We then link our #if condition to these stores. We have now put the responsibility of hiding the Todos based on filters on the Todos themselves while also removing quite a bit of code. It should start to become obvious that there are many ways that we can build applications in Svelte and maintain quite a bit of control over our application.

Before moving onto the next application, go ahead and look at the bundled JavaScript and CSS along with adding new features to the application. Next, we are going to look at building a weather application and getting data from a server for this information.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.225.11.98