There are hard-coded filter links and corresponding urls in our app:
impl From<Url> for Filter {
fn from(mut url: Url) -> Self {
match url.remaining_hash_path_parts().as_slice() {
["active"] => Self::Active,
["completed"] => Self::Completed,
_ => Self::All,
}
}
}
...
fn view_filters(selected_filter: Filter) -> Node<Msg> {
ul![C!["filters"],
Filter::iter().map(|filter| {
let (link, title) = match filter {
Filter::All => ("#/", "All"),
Filter::Active => ("#/active", "Active"),
Filter::Completed => ("#/completed", "Completed"),
};
...
const
path partsDuplicated literal strings is the one of the worst thing the developer may encounter in an unfamiliar code-base. Let's DRY them.
const STORAGE_KEY: &str = "todos-seed";
// ------ Url path parts ------
const ACTIVE: &str = "active";
const COMPLETED: &str = "completed";
...
impl From<Url> for Filter {
...
[ACTIVE] => Self::Active,
[COMPLETED] => Self::Completed,
_ => Self::All,
...
fn view_filters(selected_filter: Filter) -> Node<Msg> {
...
let (path, title) = match filter {
Filter::All => ("", "All"),
Filter::Active => (ACTIVE, "Active"),
Filter::Completed => (COMPLETED, "Completed"),
};
li![
a![C![IF!(filter == selected_filter => "selected")],
attrs!{At::Href => format!("#/{}", path)},
title,
],
...
Note: It would be easy now to switch to hashbang routing (don't modify your app code, please). Specs:
... The following routes should be implemented:
#/
(all - default),#/active
and#/completed
(#!/
is also allowed). ...
impl From<Url> for Filter {
fn from(mut url: Url) -> Self {
match url.remaining_hash_path_parts().as_slice() {
["!", rest @ ..] => {
match rest {
[ACTIVE] => Self::Active,
[COMPLETED] => Self::Completed,
_ => Self::All,
}
}
_ => Self::All,
}
}
}
...
fn view_filters(selected_filter: Filter) -> Node<Msg> {
...
attrs!{At::Href => format!("#!/{}", path)},
...
The routing code and links are now good enough.
However it's not a standard way how to create links in Seed apps. Once you have a larger app with nested paths and pages, you don't want to know parent path parts - the only interesting ones are path parts related to the particular page.
Example:
/admin/statistics/report/daily
/admin/statistics/report/weekly
admin
, statistics
, report
report
page are daily
and weekly
.Yes, you can try to rely on browser base path handling and use relative paths (e.g. /admin/statistics/report/
+ daily
= /admin/statistics/report/daily
). However it's not very reliable, especially once you decide to deploy the app to non-root server path and had to explicitly set the base path (you'll learn about base path in other chapters). And there is no way how to do it for our hash-based paths.
Let's rewrite it to the standard form and then discuss it.
// ----------------- A) -----------------
fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
...
Model {
base_url: url.to_hash_base_url(),
...
filter: Filter::from(url),
}
}
...
// ----------------- B) -----------------
struct Model {
base_url: Url,
...
filter: Filter,
}
...
// ----------------- C) -----------------
// ------ ------
// Urls
// ------ ------
struct_urls!();
impl<'a> Urls<'a> {
pub fn home(self) -> Url {
self.base_url()
}
pub fn active(self) -> Url {
self.base_url().add_hash_path_part(ACTIVE)
}
pub fn completed(self) -> Url {
self.base_url().add_hash_path_part(COMPLETED)
}
}
// ------ ------
// Update
// ------ ------
// ----------------- D) -----------------
fn view(model: &Model) -> Vec<Node<Msg>> {
...
view_footer(&model.todos, model.filter, &model.base_url),
...
fn view_footer(todos: &BTreeMap<Ulid, Todo>, selected_filter: Filter, base_url: &Url) -> Node<Msg> {
...
view_filters(selected_filter, base_url),
...
// ----------------- E) -----------------
fn view_filters(selected_filter: Filter, base_url: &Url) -> Node<Msg> {
ul![C!["filters"],
Filter::iter().map(|filter| {
let urls = Urls::new(base_url);
let (url, title) = match filter {
Filter::All => (urls.home(), "All"),
Filter::Active => (urls.active(), "Active"),
Filter::Completed => (urls.completed(), "Completed"),
};
li![
a![C![IF!(filter == selected_filter => "selected")],
attrs!{At::Href => url},
title,
],
]
...
We've moved field base_url
at the top because:
url
, so it has to be placed above filter: Filter::from(url)
because Filter::from
consumes url
(i.e. takes ownership).Url
method to_hash_base_url()
deletes all path parts with index >= next_hash_path_part_index
in the cloned url. In our case it removes all path parts because next_hash_path_part_index
is always set to 0 in url
in init
.
to_hash_base_url()
returns the cloned url that will be used as a base url for our links as you'll see later.
Model
field ordering has been changed to mirror our new field ordering in init
.
This is the link building itself. We'll talk about it in a standalone section 3. struct_urls!
below.
We just pass base_url
through view
functions as necessary.
The most interesting parts in this block are:
let urls = Urls::new(base_url);
urls.home()
At::Href => url
We don't have to write error-prone string links and be careful to add symbols like #
anymore - links are created by typed methods now.
We don't have to think about parent path parts - it allows us to move the code into sub-modules without problems.
It would be easy to switch from hash-based routing to the standard one.
struct_urls!
Let's look at the content of block C) again:
struct_urls!();
impl<'a> Urls<'a> {
pub fn home(self) -> Url {
self.base_url()
}
pub fn active(self) -> Url {
self.base_url().add_hash_path_part(ACTIVE)
}
pub fn completed(self) -> Url {
self.base_url().add_hash_path_part(COMPLETED)
}
}
struct_urls!()
doesn't do anything fancy - it just hides the code to improve readability. You can copy-paste the code from the macro definition, fix paths and it would look like this:
pub struct Urls<'a> {
base_url: std::borrow::Cow<'a, Url>,
}
impl<'a> Urls<'a> {
pub fn new(base_url: impl Into<std::borrow::Cow<'a, Url>>) -> Self {
Self {
base_url: base_url.into(),
}
}
pub fn base_url(self) -> Url {
self.base_url.into_owned()
}
}
impl<'a> Urls<'a> {
pub fn home(self) -> Url {
self.base_url()
}
pub fn active(self) -> Url {
self.base_url().add_hash_path_part(ACTIVE)
}
pub fn completed(self) -> Url {
self.base_url().add_hash_path_part(COMPLETED)
}
}
There are Cows and lifetimes to improve the performance during chaining (i.e. building links from multiple path parts in nested modules). Example from pages example:
struct_urls!();
impl<'a> Urls<'a> {
...
pub fn admin_urls(self) -> page::admin::Urls<'a> {
page::admin::Urls::new(self.base_url().add_path_part(ADMIN))
}
}
In that example we pass an owned Url
into page::admin::Urls::new
, however we can still pass base_url
reference into page::admin:Urls::new
from the inside of page::admin
module (like we did in our TodoMVC example).
Note: You don't have to use struct_urls!()
if you don't want to - it's just your helper. However you'll probably appreciate it while you are writing more complex apps.
Nice! TodoMVC has a scalable routing and link building, let's finish our app in the next chapter.