Build a Todo App

A complete task management app with filtering, inline editing, i18n, dark mode, and aggregation queries. One language replaces your entire stack.

What You Will Learn

  • Defining entities with typed fields and defaults
  • CRUDD operations (Create, Read, Update, Delete, Destroy)
  • Filtering with match expressions and query chains
  • Inline editing with edit state tracking
  • Session persistence for user preferences
  • Internationalization (i18n) with translation files
  • Dark mode with data-theme and CSS variables
  • Using embedded components (<Card>, <Icon>)
  • Template method chains (Todo.where(done == true).count)
  • Aggregation queries (sum, avg, min, max)

Project Structure

todo-app/
├── app/
│   ├── index.flin          # Homepage (/)
│   ├── tasks.flin           # Task management (/tasks)
│   ├── stats.flin           # Statistics dashboard (/stats)
│   └── settings.flin        # User settings (/settings)
├── entities/
│   └── Todo.flin            # Todo entity definition
├── layouts/
│   └── default.flin         # App layout with nav
├── i18n/
│   ├── en.flin              # English translations
│   └── fr.flin              # French translations
└── styles/
    └── theme.css            # App styles

No components/ directory needed. All UI components are embedded in the FLIN binary.

Step 1: Define the Entity

Create entities/Todo.flin:

entity Todo {
    title: text
    description: text = ""
    done: bool = false
    priority: int = 1
    estimatedHours: float = 1.0
    category: text = "general"
    deadline: time = now
}
// Auto fields: id, created_at, updated_at, deleted_at, version
// Fields with defaults are optional when creating
// Use "text" (not "string"), "int", "float", "bool", "time"

Step 2: Create the Layout

The layout wraps every page. Create layouts/default.flin:

theme = session.theme || "light"
currentLang = session.lang || "en"

fn t(key) {
    dict = translations[currentLang]
    result = dict[key]
    return result ?? key
}

My Todo App
{children}

Key patterns:

  • Session persistence: session.theme and session.lang survive page reloads (stored in cookies)
  • Translation function: t(key) looks up the current language dictionary, falls back to the key itself with ??
  • {children}: Where page content gets injected

Step 3: Add Translation Files

Create i18n/en.flin (note: square brackets, not curly braces):

[
    "nav.stats": "Statistics",
    "nav.settings": "Settings",
    "tasks.title": "My Tasks",
    "tasks.add": "Add Task",
    "tasks.empty": "No tasks yet. Create your first one!",
    "filter.all": "All",
    "filter.pending": "Pending",
    "filter.completed": "Completed",
    "stats.total": "Total Tasks",
    "stats.completed": "Completed",
    "stats.pending": "Pending"
]

Create i18n/fr.flin:

[
    "nav.stats": "Statistiques",
    "nav.settings": "Paramètres",
    "tasks.title": "Mes Tâches",
    "tasks.add": "Ajouter",
    "tasks.empty": "Aucune tâche. Créez la première !",
    "filter.all": "Toutes",
    "filter.pending": "En cours",
    "filter.completed": "Terminées",
    "stats.total": "Total des tâches",
    "stats.completed": "Terminées",
    "stats.pending": "En cours"
]

Step 4: Build the Tasks Page

This is the core of the app. Create app/tasks.flin:

State &amp; Queries

// State
filter = session.filter || "all"
showAddForm = false
editingTaskId = 0
editTitle = ""
newTitle = ""
newCategory = "general"
newPriority = 1

// Computed queries (match expression)
filteredTodos = match filter {
    "pending" -> Todo.where(done == false)
    "completed" -> Todo.where(done == true)
    _ -> Todo.all
}

allCount = Todo.count
pendingCount = Todo.where(done == false).count
completedCount = Todo.where(done == true).count

Functions

// ALWAYS type entity params: (task: Todo)
fn addTask() {
    if newTitle != "" {
        task = Todo {
            title: newTitle,
            category: newCategory,
            priority: newPriority
        }
        save task
        newTitle = ""
        showAddForm = false
    }
}

fn toggleTask(task: Todo) {
    task.done = !task.done
    save task
}

fn deleteTask(task: Todo) {
    delete task
}

fn startEdit(task: Todo) {
    editingTaskId = task.id
    editTitle = task.title
}

fn saveEdit(task: Todo) {
    task.title = editTitle
    save task
    editingTaskId = 0
}

fn setFilter(newFilter) {
    filter = newFilter
    session.filter = newFilter
}

Template

<>
    
{allCount} {t("stats.total")} {pendingCount} {t("stats.pending")} {completedCount} {t("stats.completed")}
{if showAddForm} {/if}
{if allCount == 0}

{t("tasks.empty")}

{else} {for task in filteredTodos} {if editingTaskId == task.id}
{else}
{task.title} {task.category} | P{task.priority} | {time_format(task.created_at, "DD/MM/YYYY")}
{/if} {/for} {/if}

Step 5: Stats Page with Aggregations

Create app/stats.flin to demonstrate aggregation queries:

<>
    

{t("stats.title")}

{Todo.count} {t("stats.total")} {Todo.where(done == true).count} {t("stats.completed")} {Todo.where(done == false).count} {t("stats.pending")}

Time Tracking

{Todo.sum("estimatedHours")}h Total Hours {Todo.avg("estimatedHours")}h Avg Hours/Task

By Category

General: {Todo.where(category == "general").count} tasks
Work: {Todo.where(category == "work").count} tasks
Personal: {Todo.where(category == "personal").count} tasks

Aggregation methods work directly in templates: Todo.sum("estimatedHours"), Todo.avg("estimatedHours"). The field name is always a string.

Step 6: Settings Page

<>
    

Settings

Theme
Language

Variables theme and currentLang are defined in the layout and available on every page, because layouts and pages share one scope (FLIN uses a PHP-like include model, not isolated component state).

Running the App

flin dev path/to/todo-app

Open http://localhost:3000 and try:

  • Add a task with title, category, and priority
  • Click a task to toggle done/undone
  • Click the task text to edit inline
  • Switch between filter tabs
  • Change language to French
  • Toggle dark mode
  • Check the stats page for aggregations
  • Visit /_flin for the admin console

Key Patterns Recap

PatternCode
Entity CRUDDsave todo / delete todo / destroy todo
Query chainTodo.where(done == false).order_by("priority")
AggregationTodo.sum("estimatedHours")
Match filterresult = match filter { "pending" -> ... _ -> ... }
Typed entity paramfn toggle(task: Todo) { ... }
Session readtheme = session.theme || "light"
Session writesession.theme = "dark"; location.reload()
i18n{t("tasks.title")}

Common Mistakes

WrongRightWhy
fn del(task)fn del(task: Todo)Entity params must be typed
{ "key": "val" } in i18n[ "key": "val" ]i18n files use square brackets
{match filter {...}} inlineStore in variable, then {var}Match cannot be used inline
order_by(priority)order_by("priority")Field name must be a string
task.lengthlen(tasks)Use len() function
format_date(task.created_at, ...)time_format(task.created_at, "DD/MM")Use time_format for timestamps