{"id":"fission-tabs-1","tabs":[{"index":0,"label":"Leptos","value":"source"},{"index":1,"label":"Fission","value":"fission"}]}
{"id":"fission-tabs-1","index":0,"label":"Leptos","value":"source"}
#[component]
fn TodoApp() -> impl IntoView {
let todos = vec![
Todo { id: 1, title: "Write release notes".into(), done: false },
Todo { id: 2, title: "Test Android package".into(), done: true },
];
view! {
<section>
<h1>"Ship checklist"</h1>
{todos.into_iter().map(|todo| view! {
<div><span>{if todo.done { "Done" } else { "Open" }}</span><span>{todo.title}</span></div>
}).collect_view()}
</section>
}
}
{"id":"fission-tabs-1","index":1,"label":"Fission","value":"fission"}
use fission::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Todo {
pub id: u64,
pub title: String,
pub done: bool,
}
pub struct TodoApp;
impl From<TodoApp> for Widget {
fn from(_app: TodoApp) -> Widget {
let todos = vec![
Todo { id: 1, title: "Write release notes".into(), done: false },
Todo { id: 2, title: "Test Android package".into(), done: true },
];
let rows = todos.into_iter().map(|todo| {
let status = if todo.done { "Done" } else { "Open" };
Row {
gap: Some(12.0),
children: widgets![Text::new(status), Text::new(todo.title)],
..Default::default()
}
.into()
});
Container::new(Column {
gap: Some(12.0),
children: std::iter::once(Text::new("Ship checklist").size(28.0).weight(800).into())
.chain(rows)
.collect(),
..Default::default()
})
.padding_all(24.0)
.into()
}
}
{"id":"fission-tabs-1"}
{"id":"fission-tabs-2","tabs":[{"index":0,"label":"Leptos","value":"source"},{"index":1,"label":"Fission","value":"fission"}]}
{"id":"fission-tabs-2","index":0,"label":"Leptos","value":"source"}
#[component]
fn TodoApp() -> impl IntoView {
let (draft, set_draft) = create_signal(String::new());
let (todos, set_todos) = create_signal(Vec::<Todo>::new());
let add = move |_| {
let title = draft.get().trim().to_string();
if title.is_empty() { return; }
set_todos.update(|items| items.push(Todo::new(title)));
set_draft.set(String::new());
};
view! { <TodoContent draft=draft set_draft=set_draft todos=todos on:add=add /> }
}
{"id":"fission-tabs-2","index":1,"label":"Fission","value":"fission"}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TodoState {
pub draft: String,
pub todos: Vec<Todo>,
pub next_id: u64,
pub current_path: String,
}
impl Default for TodoState {
fn default() -> Self {
Self {
draft: String::new(),
todos: Vec::new(),
next_id: 1,
current_path: "/".into(),
}
}
}
impl GlobalState for TodoState {}
#[fission_reducer(SetDraft)]
fn set_draft(state: &mut TodoState, draft: String) {
state.draft = draft;
}
#[fission_reducer(AddDraftTodo)]
fn add_draft_todo(state: &mut TodoState) {
let title = state.draft.trim();
if title.is_empty() {
return;
}
state.todos.push(Todo {
id: state.next_id,
title: title.to_string(),
done: false,
});
state.next_id += 1;
state.draft.clear();
}
#[fission_reducer(ToggleTodo)]
fn toggle_todo(state: &mut TodoState, id: u64) {
if let Some(todo) = state.todos.iter_mut().find(|todo| todo.id == id) {
todo.done = !todo.done;
}
}
pub struct TodoScreen;
impl From<TodoScreen> for Widget {
fn from(_screen: TodoScreen) -> Widget {
let (ctx, view) = fission::build::current::<TodoState>();
let update_draft = with_reducer!(ctx, SetDraft(String::new()), set_draft);
let add_todo = with_reducer!(ctx, AddDraftTodo, add_draft_todo);
let rows = view.state().todos.iter().map(|todo| {
let toggle = with_reducer!(ctx, ToggleTodo(todo.id), toggle_todo);
Row {
gap: Some(12.0),
children: widgets![
Checkbox {
checked: todo.done,
on_toggle: Some(toggle),
..Default::default()
}
.semantics_identifier(format!("todo.toggle.{}", todo.id)),
Text::new(todo.title.clone()),
],
..Default::default()
}
.semantics_identifier(format!("todo.row.{}", todo.id))
.into()
});
Column {
gap: Some(16.0),
children: widgets![
Text::new("Ship checklist").size(28.0).weight(800),
Row {
gap: Some(12.0),
children: widgets![
TextInput {
id: Some(WidgetId::explicit("todo-title")),
value: view.state().draft.clone(),
label: Some("New task".into()),
placeholder: Some("Add a task".into()),
on_change: Some(update_draft),
..Default::default()
}
.semantics_identifier("todo.title"),
Button {
child: Some(Text::new("Add task").into()),
on_press: Some(add_todo),
..Default::default()
}
.semantics_identifier("todo.add"),
],
..Default::default()
},
Column { gap: Some(10.0), children: rows.collect(), ..Default::default() },
],
..Default::default()
}
.into()
}
}
{"id":"fission-tabs-2"}
{"id":"fission-tabs-3","tabs":[{"index":0,"label":"Leptos","value":"source"},{"index":1,"label":"Fission","value":"fission"}]}
{"id":"fission-tabs-3","index":0,"label":"Leptos","value":"source"}
let todos = create_resource(
|| (),
|_| async move { load_todos().await }
);
view! {
<Suspense fallback=move || view! { <p>"Loading"</p> }>
{move || todos.get().map(|items| view! { <TodoList todos=items /> })}
</Suspense>
}
{"id":"fission-tabs-3","index":1,"label":"Fission","value":"fission"}
use fission::core::{JobRef, JobSpec};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LoadTodosRequest;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ApiError {
pub message: String,
}
pub struct LoadTodosJob;
impl JobSpec for LoadTodosJob {
type Request = LoadTodosRequest;
type Ok = Vec<Todo>;
type Err = ApiError;
const NAME: &'static str = "todo.load";
}
pub const LOAD_TODOS_JOB: JobRef<LoadTodosJob> = JobRef::new(LoadTodosJob::NAME);
#[fission_reducer(LoadTodos)]
fn load_todos(state: &mut TodoState, ctx: &mut ReducerContext<TodoState>) {
state.loading = true;
ctx.effects
.app(LOAD_TODOS_JOB, LoadTodosRequest)
.on_ok(ctx.effects.bind(TodosLoaded, reduce_with!(todos_loaded)))
.on_err(ctx.effects.bind(TodosFailed, reduce_with!(todos_failed)));
}
{"id":"fission-tabs-3"}
{"id":"fission-tabs-4","tabs":[{"index":0,"label":"Leptos","value":"source"},{"index":1,"label":"Fission","value":"fission"}]}
{"id":"fission-tabs-4","index":0,"label":"Leptos","value":"source"}
view! {
<div class="rounded-xl border bg-white px-4 py-3 shadow-sm">
<input type="checkbox" prop:checked=todo.done />
<span>{todo.title}</span>
</div>
}
{"id":"fission-tabs-4","index":1,"label":"Fission","value":"fission"}
pub struct TodoRow {
pub todo: Todo,
}
impl From<TodoRow> for Widget {
fn from(row: TodoRow) -> Widget {
let (ctx, view) = fission::build::current::<TodoState>();
let tokens = &view.env().theme.tokens;
let toggle = with_reducer!(ctx, ToggleTodo(row.todo.id), toggle_todo);
Container::new(Row {
gap: Some(tokens.spacing.s),
children: widgets![
Checkbox {
checked: row.todo.done,
on_toggle: Some(toggle),
..Default::default()
}
.semantics_identifier(format!("todo.toggle.{}", row.todo.id)),
Text::new(row.todo.title)
.size(tokens.typography.font_size_base)
.color(tokens.colors.text_primary),
],
..Default::default()
})
.bg(tokens.colors.surface)
.border(tokens.colors.border, 1.0)
.border_radius(tokens.radii.large)
.padding([tokens.spacing.m, tokens.spacing.m, tokens.spacing.s, tokens.spacing.s])
.into()
}
}
{"id":"fission-tabs-4"}
{"id":"fission-tabs-5","tabs":[{"index":0,"label":"Leptos","value":"source"},{"index":1,"label":"Fission","value":"fission"}]}
{"id":"fission-tabs-5","index":0,"label":"Leptos","value":"source"}
view! {
<Router>
<Routes fallback=|| view! { <p>"Not found"</p> }>
<Route path=path!("/") view=TodoApp />
<Route path=path!("/todos/:id") view=TodoDetail />
</Routes>
</Router>
}
{"id":"fission-tabs-5","index":1,"label":"Fission","value":"fission"}
use fission::widgets::{Route, Router};
use std::sync::Arc;
#[fission_reducer(NavigateTo)]
fn navigate_to(state: &mut TodoState, path: String) {
state.current_path = path;
}
pub struct TodoRoutes;
impl From<TodoRoutes> for Widget {
fn from(_routes: TodoRoutes) -> Widget {
let (_, view) = fission::build::current::<TodoState>();
Router::<TodoState> {
current_path: view.state().current_path.clone(),
routes: vec![
Route {
path: "/".into(),
builder: Arc::new(|_, _, _| TodoScreen.into()),
},
Route {
path: "/todos/:id".into(),
builder: Arc::new(|_, _, params| TodoDetail {
id: params["id"].parse().unwrap_or(0),
}
.into()),
},
],
not_found: Some(Arc::new(|_, _, _| Text::new("Not found").into())),
}
.into()
}
}
{"id":"fission-tabs-5"}
{"id":"fission-tabs-6","tabs":[{"index":0,"label":"Leptos","value":"source"},{"index":1,"label":"Fission","value":"fission"}]}
{"id":"fission-tabs-6","index":0,"label":"Leptos","value":"source"}
// Browser-level tests usually drive rendered DOM.
page.get_by_label("New task").fill("Publish release").await?;
page.get_by_role("button").click().await?;
page.get_by_text("Publish release").wait_for().await?;
{"id":"fission-tabs-6","index":1,"label":"Fission","value":"fission"}
use fission_test_driver::{LiveTestClient, SelectorQuery};
#[test]
#[ignore]
fn user_can_add_a_todo_in_a_real_shell() {
let control_port = reserve_control_port();
let mut child = launch_todo_app(control_port);
let client = LiveTestClient::connect(control_port);
client.wait_for_ready(15_000).expect("app ready");
client
.fill_text_semantic_identifier("todo.title", "Publish release")
.expect("fill draft");
client
.tap_semantic_identifier("todo.add")
.expect("add todo");
client
.wait_for_visible(SelectorQuery::label("Publish release"), 5_000)
.expect("todo visible");
client
.screenshot(".artifacts/screenshots/todos/added.png")
.expect("screenshot");
client.quit().expect("quit app");
let _ = child.wait();
}
{"id":"fission-tabs-6"}
Leptos concept | Closest Fission concept | Important difference |
|---|---|---|
Signal | GlobalState, selector, or #[local_state] | Fission chooses state storage by ownership and lifetime. |
Resource | Resource/job/service/effect | Outside work is explicit runtime work. |
Component function | Struct plus impl From<T> for Widget | Fission avoids hidden render-time side effects. |
Leptos Router | Fission Router | Route state is shared across all targets. |
DOM hydration | Shell lowering | Fission can lower to native, web canvas, terminal, static HTML, or SSR. |