There are two reasons for creating our project after designing the Model and Msg:
It forces us to think and design before we start to write code. In most cases, it leads to a better architecture.
update and especially view are better to write and test "live" because it's fun and the fast feedback loop reveals design and implementation issues quickly.
I assume you've already tried to setup a new project for the counter example. If not, open seed-quickstart repo and read its README.md.
Please, create and start a new Seed app if you want to follow the steps below.
Remove unnecessary comments from your lib.rs.
lib.rs without comments#![allow(clippy::wildcard_imports)]
use seed::{prelude::*, *};
// ------ ------
// Init
// ------ ------
fn init(_: Url, _: &mut impl Orders<Msg>) -> Model {
Model::default()
}
// ------ ------
// Model
// ------ ------
type Model = i32;
// ------ ------
// Update
// ------ ------
#[derive(Copy, Clone)]
enum Msg {
Increment,
}
fn update(msg: Msg, model: &mut Model, _: &mut impl Orders<Msg>) {
match msg {
Msg::Increment => *model += 1,
}
}
// ------ ------
// View
// ------ ------
#[allow(clippy::trivially_copy_pass_by_ref)]
fn view(model: &Model) -> Node<Msg> {
div![
"This is a counter: ",
C!["counter"],
button![model, ev(Ev::Click, |_| Msg::Increment),],
]
}
// ------ ------
// Start
// ------ ------
#[wasm_bindgen(start)]
pub fn start() {
App::start("app", init, update, view);
}
Add our designed Model and Msg. (We'll resolve compilation errors in next steps.)
Model and Msg// ------ ------
// Model
// ------ ------
struct Model {
todos: BTreeMap<Ulid, Todo>,
new_todo_title: String,
selected_todo: Option<SelectedTodo>,
filter: Filter,
base_url: Url,
}
struct Todo {
id: Ulid,
title: String,
completed: bool,
}
struct SelectedTodo {
id: Ulid,
title: String,
input_element: ElRef<web_sys::HtmlInputElement>,
}
enum Filter {
All,
Active,
Completed,
}
// ------ ------
// Update
// ------ ------
#[derive(Copy, Clone)]
enum Msg {
UrlChanged(subs::UrlChanged),
NewTodoTitleChanged(String),
// ------ Basic Todo operations ------
CreateTodo,
ToggleTodo(Ulid),
RemoveTodo(Ulid),
// ------ Bulk operations ------
CheckOrUncheckAll,
ClearCompleted,
// ------ Selection ------
SelectTodo(Option<Ulid>),
SelectedTodoTitleChanged(String),
SaveSelectedTodo,
}
Add dependency ulid into Cargo.toml.
[dependencies]
ulid = "0.3.3"
...
Import BTreeMap and Ulid into lib.rs.
use seed::{prelude::*, *};
use std::collections::BTreeMap;
use ulid::Ulid;
Remove the line
#[derive(Copy, Clone)]
from lib.rs, because subs::UrlChanged and String don't implement Copy and standalone Clone for Msg is an anti-pattern.
Remove allow attribute and div! content from view. You can write simple alternative content (e.g. "I'm a placeholder") to check in your browser that everything works once we fix all compilation errors.
// ------ ------
// View
// ------ ------
fn view(model: &Model) -> Node<Msg> {
div![
"I'm a placeholder"
]
}
Write an update skeleton with all match arms. Each arm contains log! with Msg data - it'll help us to write and test view.
update skeletonfn update(msg: Msg, model: &mut Model, _: &mut impl Orders<Msg>) {
match msg {
Msg::UrlChanged(subs::UrlChanged(url)) => {
log!("UrlChanged", url);
}
Msg::NewTodoTitleChanged(title) => {
log!("NewTodoTitleChanged", title);
}
// ------ Basic Todo operations ------
Msg::CreateTodo => {
log!("CreateTodo");
}
Msg::ToggleTodo(id) => {
log!("ToggleTodo");
}
Msg::RemoveTodo(id) => {
log!("RemoveTodo");
}
// ------ Bulk operations ------
Msg::CheckOrUncheckAll => {
log!("CheckOrUncheckAll");
}
Msg::ClearCompleted => {
log!("ClearCompleted");
}
// ------ Selection ------
Msg::SelectTodo(opt_id) => {
log!("SelectTodo", opt_id);
},
Msg::SelectedTodoTitleChanged(title) => {
log!("SelectedTodoTitleChanged", title);
},
Msg::SaveSelectedTodo => {
log!("SaveSelectedTodo");
}
}
}
Let's create the most simple Model instance in our init.
fn init(_: Url, _: &mut impl Orders<Msg>) -> Model {
Model {
todos: BTreeMap::new(),
new_todo_title: String::new(),
selected_todo: None,
filter: Filter::All,
base_url: Url::new(),
}
}
Temporarily disable compiler linters dead_code and unused_variables. They are very useful in later stages of the project however they cause a sea of warnings in the terminal now and it's annoying when you want to read compilation errors.
#![allow(clippy::wildcard_imports)]
// TODO: Remove
#![allow(dead_code, unused_variables)]
Implement a Model method add_mock_data. It'll allow us to test the view function until our update function is complete.
init and add_mock_datafn init(_: Url, _: &mut impl Orders<Msg>) -> Model {
Model {
...
}.add_mock_data()
}
...
struct Model {
...
}
// TODO: Remove
impl Model {
fn add_mock_data(mut self) -> Self {
let (id_a, id_b) = (Ulid::new(), Ulid::new());
self.todos.insert(id_a, Todo {
id: id_a,
title: "I'm todo A".to_owned(),
completed: false,
});
self.todos.insert(id_b, Todo {
id: id_b,
title: "I'm todo B".to_owned(),
completed: true,
});
self.new_todo_title = "I'm a new todo title".to_owned();
self.selected_todo = Some(SelectedTodo {
id: id_b,
title: "I'm better todo B".to_owned(),
input_element: ElRef::new(),
});
self
}
}
Make sure there aren't any errors or warnings in your terminal and you see something like I'm a placeholder in your browser.
Note: There are .to_owned() calls instead of to_string() or into() in the code above and in other chapters.
"foo".to_string() wouldn't make any sense if you don't know Rust: "Why we are casting string to string??". If you understand &str as a kind of text/string, then it makes more sense to write to_owned(). to_owned() better expresses the operation: "promoting" a string reference to the owned string. And you can accidentally introduce more expensive to_string operation when you replace a_str.to_string() with a_complex_item.to_string().
into() has similar problems like to_string but they are worse because into is even more general than to_string. For example, you have no idea what type bar has in the expression let bar = "foo".into().
Done! We are ready to write view in the next chapter!