# Manage a task list

Adding and removing repeated items.

Source: https://withastro-utils.github.io/docs/examples/todo/

Complete the [Forms setup](https://withastro-utils.github.io/docs/guides/forms/getting-started/index.md) first, or use the configuration, middleware, and layout files in the sidebar. Install `@astro-utils/forms` and `@astrojs/node` in an existing Astro project.

## Try the interaction

Add a task, then remove it. Removal does not require a valid value in the new-task field.

### src/layouts/Layout.astro

```astro title="src/layouts/Layout.astro"
---
import { WebForms } from '@astro-utils/forms/forms.js';
---
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width" />
        <title><slot name="title" /></title>
    </head>
    <body>
        <WebForms>
            <slot />
        </WebForms>
    </body>
</html>
```

### src/pages/todo.astro

```astro title="src/pages/todo.astro"
---
import { BButton, BInput, Bind, BindForm } from '@astro-utils/forms/forms.js';

import Layout from '../layouts/Layout.astro';

type TaskForm = {
    newTask: string;
    message: string;
};

type TodoTask = {
    title: string;
    id: string;
}

const bind = Bind<TaskForm>({ newTask: '', message: '' });
const tasks: TodoTask[] = Astro.locals.session.tasks ??= [{ id: 'limits', title: 'Review upload limits' }, { id: 'notes', title: 'Publish migration notes' }];

async function addTask() {
    tasks.push({title: bind.newTask, id: crypto.randomUUID()});

    bind.message = `${bind.newTask} added.`;
    bind.newTask = '';
}

async function removeTask(id: string, title: string) {
    const index = tasks.findIndex(task => task.id === id);
    if (index === -1) {
        bind.message = 'This task has already been removed.';
        return;
    }
    tasks.splice(index, 1);

    bind.message = `${title} removed.`;
}
---

<Layout>
<span slot="title">Manage a task list</span>
<BindForm {bind} omitState={['message']}> <!-- omitState = ensure this info will be removed in the next action -->
    {bind.message && <p role="status">{bind.message}</p>}

    <label for="newTask">New task</label>
    <BInput id="newTask" name="newTask" minlength={2} maxlength={80} required />
    <BButton onClick={addTask} whenFormOK>Add task</BButton>

    <ul>
        {tasks.map(task => (
            <li>
                <span>{task.title}</span>
                <BButton onClick={() => removeTask(task.id, task.title)}>Remove</BButton>
            </li>
        ))}
    </ul>
</BindForm>
</Layout>
```

### src/middleware.ts

```ts title="src/middleware.ts"
import forms from '@astro-utils/forms';

export const onRequest = forms();
```

### astro.config.mjs

```js title="astro.config.mjs"
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
import forms from '@astro-utils/forms/dist/integration.js';

export default defineConfig({
    output: 'server',
    adapter: node({ mode: 'standalone' }),
    integrations: [forms]
});
```

## How it works

The runnable source stores a small list in the signed session cookie. It survives page reloads while that session remains valid; it is not shared between devices. Keep the list small or move it to application storage. The removal handler checks that the item still exists before changing the list.

## Continue

Learn about [stable keys and reusable components](https://withastro-utils.github.io/docs/guides/forms/loops-and-components/index.md).
