🧠 Fase 2: Lógica em Memória¶
Objetivo: construir a lógica que permite adicionar tarefas, sem ainda usar banco de dados.
Nesta fase, o foco é compreender como o Phoenix LiveView mantém o estado da interface em tempo real, usando apenas o socket — o elo entre cliente e servidor.
🧩 O que é o "estado" no LiveView?¶
No modelo LiveView, o estado da aplicação vive no servidor. Sempre que o usuário interage com a interface (digita, clica, envia um formulário), o navegador envia um pequeno evento via WebSocket. O servidor processa o evento, atualiza o estado e devolve o HTML atualizado.
Esse "estado" é armazenado no socket, uma estrutura que representa a sessão daquele usuário. Tudo o que a tela precisa saber — tarefas, campos de formulário, filtros — é guardado ali.
Socket = "memória viva" do LiveView
assign(socket, chave: valor) ➜ grava ou atualiza o estado
@chave ➜ acessa o valor no render
💡 Conexão com o tutorial de Clojure: o socket cumpre aqui o papel que o
r/atomcumpria no Reagent — a "caixa" observada cuja mudança redesenha a interface. A diferença: lá a caixa vivia no navegador; aqui, ela vive no servidor.
⚙️ Passo 2.1: O Estado Inicial (mount/3)¶
Quando o usuário abre a página, o LiveView chama mount/3. É aqui que criamos o estado inicial — uma mini "memória RAM" para aquele cliente.
Abra o arquivo:
e substitua o mount/3 atual por:
# @impl true → indicamos que esta função faz parte do comportamento LiveView
@impl true
def mount(_params, _session, socket) do
# Nossa "memória" inicial: duas tarefas de exemplo
tasks = [
%{id: 1, title: "Comprar leite", completed: false},
%{id: 2, title: "Aprender LiveView", completed: true}
]
# assign/2 coloca dados no socket (nosso estado de interface)
socket =
assign(socket,
tasks: tasks,
new_task_title: "" # o campo de texto começa vazio
)
# {:ok, socket} → retorna o estado inicial ao LiveView
{:ok, socket}
end
🎨 Passo 2.2: Mostrando o Estado (render/1)¶
A função render/1 é o "desenhista" da interface: recebe os dados do socket (via assigns) e devolve o HTML dinâmico.
Substitua o render/1 por:
@impl true
def render(assigns) do
~H"""
<div class="w-full max-w-lg mx-auto mt-12 p-6 bg-white rounded-lg shadow-md">
<h1 class="text-3xl font-bold mb-6 text-center text-gray-800">
Minha Lista de Tarefas
</h1>
<%!-- FORMULÁRIO DE ENTRADA --%>
<form phx-submit="save_task" phx-change="update_form" class="flex gap-2 mb-6">
<input
type="text"
name="title"
value={@new_task_title}
placeholder="O que precisa ser feito?"
class="flex-grow p-2 border rounded"
autofocus
/>
<button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Adicionar
</button>
</form>
<%!-- LISTA DE TAREFAS --%>
<div class="mt-8">
<ul id="task-list">
<li :for={task <- @tasks} class="flex justify-between items-center p-3 border-b">
<span class={if task.completed, do: "line-through text-gray-500", else: "text-gray-900"}>
{task.title}
</span>
</li>
</ul>
</div>
</div>
"""
end
🧭 Entendendo os atributos "mágicos"¶
| Atributo | Função |
|---|---|
phx-submit="save_task" |
Dispara o evento "save_task" quando o formulário é enviado. |
phx-change="update_form" |
Dispara o evento "update_form" a cada tecla digitada. |
value={@new_task_title} |
Liga o campo de texto ao estado do socket. |
:for={task <- @tasks} |
Repete o <li> para cada tarefa (o "for" do HEEx). |
⚠️ Passo 2.3: O "Aha!" — o texto que desaparece¶
Salve o arquivo e recarregue a página. Você verá o formulário e as duas tarefas de exemplo.
Agora digite algo no campo. Percebeu? O texto desaparece a cada letra!
Isso acontece porque o evento "update_form" é disparado, mas ainda não existe uma função para tratá-lo (handle_event). O processo LiveView falha, o supervisor o reinicia automaticamente (é assim que a BEAM lida com erros!), e o mount/3 roda de novo — limpando o campo. Dê uma olhada no terminal do servidor: o erro está registrado lá.
Essa é uma lição dupla: (1) todo evento phx-* precisa de um handle_event correspondente; (2) quando um processo LiveView "morre", ele renasce do estado inicial.
🧩 Passo 2.4: Tratando a Digitação (update_form)¶
Vamos criar a função que captura o evento "update_form". Adicione entre o mount/3 e o render/1:
# Captura o evento de digitação no campo
@impl true
def handle_event("update_form", %{"title" => new_title}, socket) do
# Atualiza o valor do campo no estado
socket = assign(socket, new_task_title: new_title)
{:noreply, socket} # retorna o socket atualizado, sem recarregar a página
end
Salve e teste: agora o texto digitado permanece. O estado está sendo atualizado a cada tecla — e o LiveView reflete isso no HTML.
🧱 Passo 2.5: Tratando o Envio (save_task)¶
O próximo passo é realmente adicionar a nova tarefa à lista quando o usuário clicar em "Adicionar". Adicione esta função logo abaixo da anterior:
# Captura o evento de envio do formulário
@impl true
def handle_event("save_task", %{"title" => title}, socket) do
if String.trim(title) != "" do
# Cria uma nova tarefa "em memória"
new_task = %{
id: System.unique_integer([:positive]),
title: title,
completed: false
}
# Atualiza a lista de tarefas e limpa o campo
socket =
socket
|> update(:tasks, fn tasks -> tasks ++ [new_task] end)
|> assign(:new_task_title, "")
{:noreply, socket}
else
# Ignora caso o campo esteja vazio
{:noreply, socket}
end
end
| Função | Explicação |
|---|---|
System.unique_integer/1 |
Gera um ID único (suficiente para uso "em memória"). |
update/3 |
Atualiza um valor existente no socket aplicando uma função (a lista :tasks). |
assign/3 |
Redefine new_task_title para limpar o input. |
🎉 Resultado¶
Você tem um Todo List funcional, mesmo sem banco de dados! Adicione algumas tarefas e veja a lista crescer instantaneamente.
O teste da verdade: agora recarregue a página (F5). As tarefas que você adicionou sumiram — só as duas de exemplo voltaram. E se você reiniciar o servidor, o mesmo acontece.
Isso é esperado: as tarefas viviam apenas na memória daquele socket. Cada F5 abre uma conexão nova, e o mount/3 recomeça do zero. Você acabou de ganhar a motivação para a Fase 3: persistência de verdade.
O que aprendemos — o coração do LiveView:
- O estado mora no socket;
- Cada evento é tratado por um
handle_event/3; - O
render/1reflete o estado atual em HTML, sem recarregar a página.
💾 Passo 2.6: Commit¶
🧠 Pausa Didática — Entendendo o Estilo Elixir¶
Antes de continuar, vamos fazer uma pausa estratégica para compreender a estrutura e o estilo da linguagem que aparecem no nosso código. Mesmo com poucas linhas, o todo_live.ex já demonstra vários conceitos centrais: módulos, funções, imutabilidade, pattern matching e o pipe.
Este é o estado atual do nosso arquivo (confira se o seu está igual):
defmodule ElixirTodoListWeb.TodoLive do
use ElixirTodoListWeb, :live_view
@impl true
def mount(_params, _session, socket) do
tasks = [
%{id: 1, title: "Comprar leite", completed: false},
%{id: 2, title: "Aprender LiveView", completed: true}
]
socket =
assign(socket,
tasks: tasks,
new_task_title: ""
)
{:ok, socket}
end
@impl true
def handle_event("update_form", %{"title" => new_title}, socket) do
socket = assign(socket, new_task_title: new_title)
{:noreply, socket}
end
@impl true
def handle_event("save_task", %{"title" => title}, socket) do
if String.trim(title) != "" do
new_task = %{
id: System.unique_integer([:positive]),
title: title,
completed: false
}
socket =
socket
|> update(:tasks, fn tasks -> tasks ++ [new_task] end)
|> assign(:new_task_title, "")
{:noreply, socket}
else
{:noreply, socket}
end
end
@impl true
def render(assigns) do
~H"""
... (o HTML completo do Passo 2.2) ...
"""
end
end
1️⃣ Módulos — a "caixa" do código¶
Em Elixir, tudo vive dentro de um módulo (defmodule). Um módulo é como uma caixa que organiza funções relacionadas — parecido com uma classe, mas com uma diferença fundamental: 👉 módulos não possuem estado interno nem herança. Eles apenas agrupam funções. O estado vive em estruturas como o socket ou em processos — nunca no módulo em si.
2️⃣ Funções e Aridade¶
Funções são definidas com def (públicas) ou defp (privadas). O número após a barra é a aridade — quantos parâmetros a função recebe:
mount/3→ 3 parâmetros;render/1→ 1 parâmetro;handle_event/3→ 3 parâmetros.
Em Elixir, a aridade faz parte do nome da função: handle_event/2 e handle_event/3 são funções diferentes, e não sobrecargas como em Java ou C#.
3️⃣ Pattern Matching — a escolha automática¶
Observe:
def handle_event("update_form", %{"title" => new_title}, socket) do ... end
def handle_event("save_task", %{"title" => title}, socket) do ... end
Temos duas funções com o mesmo nome e aridade, e o Elixir decide qual chamar pelo padrão dos argumentos: se o primeiro argumento for "update_form", executa a primeira; se for "save_task", a segunda. Isso é o pattern matching — um dos pilares do paradigma funcional.
Em Python, escreveríamos:
def handle_event(event_name, payload, socket):
if event_name == "update_form":
...
elif event_name == "save_task":
...
Em Elixir, a própria definição da função já é o "if".
4️⃣ Tuplas — a maneira Elixir de retornar¶
Em vez de retornar valores "crus", o Elixir usa tuplas etiquetadas:
A primeira posição é sempre um átomo (:ok, :noreply, :error) descrevendo o status. Essa convenção vem do Erlang e facilita o controle de fluxo entre processos.
5️⃣ O Socket — nosso "estado vivo"¶
- O
socketé a "caixa de estado" daquela sessão; assignnão muta a caixa: cria uma nova versão dela (imutabilidade!);- Cada atualização do socket dispara uma nova renderização automática.
6️⃣ O Operador Pipe (|>) — elegância funcional¶
Lê-se: "pegue o socket, atualize as tarefas e depois limpe o campo de texto". O pipe passa o resultado de cada linha como primeiro argumento da próxima, eliminando variáveis intermediárias — como uma receita passo a passo.
📘 Recapitulando¶
| Conceito | Significado | Analogia |
|---|---|---|
defmodule |
Agrupa funções relacionadas | Classe sem estado |
def / defp |
Funções públicas / privadas | Métodos |
| Aridade | Nº de parâmetros (parte do nome) | — |
| Pattern Matching | Escolhe a função pelo padrão dos argumentos | Substitui if/switch |
Tuplas {:ok, ...} |
Retornos estruturados | return (status, valor) |
\|> (pipe) |
Encadeia transformações | Fluent interface |
socket |
Estado imutável da interface | Store/State no React |
Fim da Fase 2! 🏁
Agora estamos prontos para conectar nossa lógica em memória a um banco de dados real com Ecto.