Skip to content

Instantly share code, notes, and snippets.

@nileshtrivedi
Created June 11, 2025 11:57
Show Gist options
  • Save nileshtrivedi/02de3a7f697b14765a3fa04a4f74875d to your computer and use it in GitHub Desktop.
Save nileshtrivedi/02de3a7f697b14765a3fa04a4f74875d to your computer and use it in GitHub Desktop.
single-file todo with ash without auth
Mix.install([
{:ash, "~> 3.0"},
{:phoenix_playground, "== 0.1.6"},
], consolidate_protocols: false)
defmodule Helpdesk do
end
defmodule Helpdesk.Support do
use Ash.Domain, validate_config_inclusion?: false
resources do
resource Helpdesk.Support.Todo
end
end
defmodule Helpdesk.Support.Todo do
# This turns this module into a resource
use Ash.Resource, domain: Helpdesk.Support, data_layer: Ash.DataLayer.Ets
actions do
defaults [:read, :destroy]
create :add do
accept [:task, :user_id]
change set_attribute(:status, "pending")
end
update :toggle do
accept []
change atomic_update(:status, expr(if status == :pending, do: :done, else: :pending))
end
end
attributes do
uuid_primary_key :id
attribute :user_id, :string do
allow_nil? false
public? true
end
attribute :task, :string do
allow_nil? false
public? true
end
attribute :status, :string do
default "pending"
allow_nil? false
public? true
end
validations do
validate attribute_in(:status, ["pending", "done"])
end
end
end
defmodule TodoLive do
use Phoenix.LiveView
require Ash.Query
def get_user_todos(user_id) do
Helpdesk.Support.Todo
|> Ash.Query.filter(user_id == ^user_id)
|> Ash.read!()
end
def mount(_params, _session, socket) do
user_id = "foobar"
todos = get_user_todos(user_id)
{:ok, assign(socket, todos: todos, user_id: user_id, new_todo_name: "drink water")}
end
def handle_event("create_todo", %{"name" => name}, socket) do
if name == "" do
{:noreply, socket}
else
# Create new todo with current user's ID
Helpdesk.Support.Todo
|> Ash.Changeset.for_create(:add, %{task: String.trim(name), user_id: "foobar"})
|> Ash.create!()
# Update socket with new list of todos
socket =
socket
|> assign(:todos, get_user_todos(socket.assigns.user_id))
|> assign(:new_todo_name, "")
{:noreply, socket}
end
end
def handle_event("toggle_todo", %{"id" => id}, socket) do
# Find the todo and ensure it belongs to current user
todo = Helpdesk.Support.Todo
|> Ash.Query.filter(user_id == ^socket.assigns.user_id)
|> Ash.Query.filter(id == ^id)
|> Ash.read_one!()
# Update todo
{:ok, _todo} =
todo
|> Ash.Changeset.for_update(:toggle)
|> Ash.update!()
# Refresh todos
{:noreply, assign(socket, :todos, get_user_todos(socket.assigns.user_id))}
end
def handle_event("delete_todo", %{"id" => id}, socket) do
# Delete todo, ensuring it belongs to current user
todo = Helpdesk.Support.Todo
|> Ash.Query.filter(user_id == ^socket.assigns.user_id)
|> Ash.Query.filter(id == ^id)
|> Ash.read_one!()
todo
|> Ash.Changeset.for_destroy(:destroy)
|> Ash.destroy!()
# Refresh todos
{:noreply, assign(socket, :todos, get_user_todos(socket.assigns.user_id))}
end
def render(assigns) do
~H"""
<script src="https://cdn.tailwindcss.com?plugins=forms"></script>
<div class="max-w-md mx-auto mt-10 p-6 bg-gray-200 rounded-lg shadow-md">
<h1 class="text-2xl font-bold mb-4">Todo List for <%= @user_id %></h1>
<form phx-submit="create_todo" class="mb-4 flex">
<input
type="text"
name="name"
value={@new_todo_name}
placeholder="Enter a new todo"
class="flex-grow mr-2 px-2 py-1 border rounded"
/>
<button
type="submit"
class="bg-blue-500 text-white px-4 py-1 rounded hover:bg-blue-600"
>
Add Todo
</button>
</form>
<ul>
<%= for todo <- @todos do %>
<li class="flex items-center justify-between py-2 border-b">
<label class="flex items-center gap-2">
<input
type="checkbox"
phx-click="toggle_todo"
phx-value-id={todo.id}
checked={todo.status == "done"}
class="mr-2"
/>
<span class={if todo.status == "done", do: "line-through text-gray-500", else: ""}>
<%= todo.task %>
</span>
<span>by <%= todo.user_id %></span>
</label>
<button
phx-click="delete_todo"
phx-value-id={todo.id}
class="text-red-500 hover:text-red-700"
>
</button>
</li>
<% end %>
</ul>
</div>
"""
end
end
defmodule Demo.Router do
use Phoenix.Router
import Phoenix.LiveView.Router
pipeline :browser do
plug :accepts, ["html"]
plug :put_secret_key_base
plug :fetch_session
plug :put_root_layout, html: {PhoenixPlayground.Layout, :root}
plug :put_secure_browser_headers
end
scope "/" do
pipe_through :browser
live "/", TodoLive
end
def put_secret_key_base(conn, _) do
put_in conn.secret_key_base, System.get_env("SECRET_KEY_BASE")
end
end
defmodule Demo.Endpoint do
use Phoenix.Endpoint, otp_app: :phoenix_playground
plug Plug.Logger
socket "/live", Phoenix.LiveView.Socket
plug Plug.Static, from: {:phoenix, "priv/static"}, at: "/assets/phoenix"
plug Plug.Static, from: {:phoenix_live_view, "priv/static"}, at: "/assets/phoenix_live_view"
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader, reloader: &PhoenixPlayground.CodeReloader.reload/2
plug Demo.Router
end
PhoenixPlayground.start(
open_browser: false,
live_reload: true,
child_specs: [],
# endpoint: Demo.Endpoint,
# endpoint_options: [debug_errors: true]
plug: Demo.Router
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment