nisshiee.org

Yewのタスク管理アプリをコンポーネント化してみる(1)

2019-01-24

今日、ヌーラボさんのブログにて、こんな記事が公開されていた。

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もコンポーネント化してみよう。