Yewのタスク管理アプリをコンポーネント化してみる(1)
今日、ヌーラボさんのブログにて、こんな記事が公開されていた。
Rust + Yew = WebAssembly でかんばんライクなタスク管理アプリを作ってみました。 - ヌーラボTechBlog
これを見て、「そういえば最近Rust書いてないから書きたいなー」とか「wasmもそろそろ触ってみるかー」とか思って、触ってみることにした。
とにかくこちらの記事がとても丁寧に書かれていて、Rust初心者でもコピペしていけばだいたい動かせるはず。
興味がある方はぜひ写経してみてほしい。
で、一旦動かすいわゆる「Getting Started」としてはとてもクオリティが高いのだが、できあがったコードは1つのコンポーネントに全てのViewが詰め込まれている。
(一応、Viewの構築を関数化して共用しているものの、コンポーネントにはなっていない。)
ReactやVueなどを触ったことがあれば、この画面を見たときに、「このようにコンポーネント化されていてほしい」と思うはずだ。
というわけで、コンポーネント化してみた。
今日は、黄色で囲った「Task」の部分をコンポーネント化してみたのでその部分について書く。
書いたコード
■ task.rs
use yew::prelude::*;
#[derive(PartialEq, Clone, Default)]
pub struct Task {
pub name: String,
pub assignee: String,
pub mandays: u32,
pub status: u32,
}
pub struct Model {
task: Task,
onchange: Option<Callback<Task>>,
}
impl Model {
fn increase_status(&mut self) {
if self.task.status < 3 {
self.task.status = self.task.status + 1;
self.dispatch_change();
}
}
fn decrease_status(&mut self) {
if self.task.status > 1 {
self.task.status = self.task.status - 1;
self.dispatch_change();
}
}
fn dispatch_change(&mut self) {
if let Some(ref onchange) = self.onchange {
onchange.emit(self.task.clone());
}
}
}
#[derive(PartialEq, Clone, Default)]
pub struct Props {
pub task: Task,
pub onchange: Option<Callback<Task>>,
}
pub enum Msg {
IncreaseStatus,
DecreaseStatus,
}
impl Component for Model {
type Message = Msg;
type Properties = Props;
fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self {
Model {
task: props.task,
onchange: props.onchange,
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::IncreaseStatus => {
self.increase_status();
}
Msg::DecreaseStatus => {
self.decrease_status();
}
}
false
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.task = props.task;
self.onchange = props.onchange;
true
}
}
impl Renderable<Model> for Model {
fn view(&self) -> Html<Self> {
html! {
<div class="card",>
<div class="card-content",>
{ format!("{}", &self.task.name) }
</div>
<footer class="card-footer",>
<div class="card-footer-item",>
{ &self.task.assignee }
</div>
<div class="card-footer-item",>
{ format!("{} 人日", &self.task.mandays) }
</div>
</footer>
<footer class="card-footer",>
<a class="card-footer-item", onclick=|_| Msg::DecreaseStatus,>{ "◀︎" }</a>
<a class="card-footer-item", onclick=|_| Msg::IncreaseStatus,>{ "▶︎︎" }</a>
</footer>
</div>
}
}
}
struct Task
の部分は、元記事にもあったものを持ってきただけ。
別モジュールに切り出しているのでpub
属性をつけてあるのと、Propertiesのいち要素にするためにいくつかderiveしている。
struct Model
はあとでimpl Component
する主体。
意味合い的にはViewModel
とかにした方が責務に合ってるんじゃないか?とか思ったりもしているが、一応本家の命名に合わせている。
ちなみに余談だが、「このModel
構造体を作らずにimpl Component for Task
でTask構造体をコンポーネント化できないか」という仮説を試してみた。
結論としてはダメで、何故かと言うと、onchange
など、Propertiesで与えられる付加的な属性を持つ場所が別途必要だから。
impl Model
の実装は元記事のimpl State
から持ってきている。元記事の「全入り1コンポーネント」で実装してしまうととにかくこの関数群が増え続けてしまうが、適切にコンポーネント化できると、関数群がパッケージ化されて見通しが良くなる。
元記事のincrease_status
関数は引数にidx
というビジネスロジックではなくデータ構造に依存したものを受け取ってしまっているが、コンポーネント化したらidx
引数が不要になっているのがわかると思う。
■ main.rs
mod task;
use task::Task;
fn view_column(status: u32, status_text: &str, tasks: &Vec<Task>) -> Html<Model> {
let view_task = |(idx, task)| {
html! {
<task::Model: task=task, onchange=move |t| Msg::UpdateTask(idx, t),/>
}
};
html! {
<div class=format!("column status-{}", status),>
<div class="tags has-addons",>
<span class="tag",>{ status_text }</span>
<span class="tag is-dark",>{ tasks.iter().filter(|e| e.status == status).count() } </span>
</div>
{ for tasks.iter().enumerate().filter(|t| t.1.status == status).map(view_task) }
</div>
}
}
で、Taskコンポーネントを使う側。
元記事のview_task
関数がなくなり、Taskコンポーネントを埋め込むだけに変わっている。Reactを書いたことがあればとてもイメージしやすいと思う。
おわりに
というわけで、部品をコンポーネント化し、コンポーネントをまたいでデータやイベントのやりとりをするコードを書いてみた。そんなに複雑なことはしていないという理由もあるが、比較的きれいにまとまったと思う。
というわけで次はColumnとHeaderもコンポーネント化してみよう。