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_data
fn 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!