What You Will Learn
- Defining entities with typed fields and defaults
- CRUDD operations (Create, Read, Update, Delete, Destroy)
- Filtering with
matchexpressions and query chains - Inline editing with edit state tracking
- Session persistence for user preferences
- Internationalization (i18n) with translation files
- Dark mode with
data-themeand 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.themeandsession.langsurvive 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 & 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
<>
{t("tasks.title")}
{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}
{/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
/_flinfor the admin console
Key Patterns Recap
| Pattern | Code |
|---|---|
| Entity CRUDD | save todo / delete todo / destroy todo |
| Query chain | Todo.where(done == false).order_by("priority") |
| Aggregation | Todo.sum("estimatedHours") |
| Match filter | result = match filter { "pending" -> ... _ -> ... } |
| Typed entity param | fn toggle(task: Todo) { ... } |
| Session read | theme = session.theme || "light" |
| Session write | session.theme = "dark"; location.reload() |
| i18n | {t("tasks.title")} |
Common Mistakes
| Wrong | Right | Why |
|---|---|---|
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 {...}} inline | Store in variable, then {var} | Match cannot be used inline |
order_by(priority) | order_by("priority") | Field name must be a string |
task.length | len(tasks) | Use len() function |
format_date(task.created_at, ...) | time_format(task.created_at, "DD/MM") | Use time_format for timestamps |