lit-html v1.0.0 がつい先日リリースされたようです。とてもよいですね。
背景
lit-html ではテンプレートから DOM の生成と更新ができるので、これだけで Web アプリを作ることができそうな感じがしますが、どこを見ても LitElement を使おうというものばかりで、lit-html だけでやる例が無かったのでやってみました。
できたもの
よくあるやつです。
Todo はテキストボックスとリスト表示がある最低限の構成になってちょうどいいですね。
コンポーネントの定義
const App = () => html`
<div>
<h1>ToDo</h1>
</div>
`
単にテンプレートを html
に渡す関数です。シンプルでよいですね。
(私は React 畑からやってきたので、対比として分かりやすいようコンポーネントと言っていますが、実際にはこれはテンプレートです)
レンダリング
render(App(), document.body)
第二引数の場所に DOM がレンダリングされます。重要なこととして、これをもう一度呼んだときには DOM 全体が再生成されるのではなく変更された部分のみが更新されます。
コンポーネントのネスト
別のコンポーネントを埋め込むことが出来ます。分かりやすいですね。
const App = () => html`
<div>
<h1>ToDo</h1>
${TaskList()}
</div>
`
パラメータの渡し方
引数として渡します。よいですね。
const TaskItem = (title: string) => html`
<div class="task">
<div class="title">${title}</div>
</div>
`
配列
お決まりのやつです。
const TaskList = (tasks: Task[]) => html`
<div class="task-list">
${tasks.map(TaskItem)}
</div>
イベントハンドリング
値は .プロパティ名
で渡すことができます。""
で囲わなくてよいです。
イベントは @イベント名
で設定することができます。
issue とかに書いてあるのと記法が違ったので迷いました。
const NewTask = (
inputText: string,
onInput: (value: string) => void
) => html`
<div class="new-task">
<input
type="text"
.value=${inputText}
@input=${e => onInput(e.currentTarget.value)}
/>
</div>
`
update の仕方
state が更新されたときに render
を再び呼べばいいだけなんですが、気持ちで単一の store を作って Single State Tree らしきことをします。
const renderApp = () => render(App(), document.body)
const createStore = <T>(initialState: T) => {
let data = initialState
return (update: any = null) => {
if (update) {
data = { ...data, ...update }
renderApp()
}
return data
}
}
更新時に renderApp を呼ぶことだけが肝心で、createStore の実装は特に本稿では重要ではないです。
こういう感じで使います。
// create store with initial state
const store = createStore({ count: 0 })
// get state
const { count } = store()
// set state
store({ count: 2 })
コンポーネント全体
App がすべてのイベントをハンドリングするようにしていて煩雑な感じがしますが、特に変な部分がなくすんなり書けてよいですね。
const TaskItem = (
task: Task,
onCheck: (checked: boolean) => void,
onClickRemove: () => void
) => html`
<div class=${classMap({ task: true, done: task.done })}>
<input
type="checkbox"
@change=${e => onCheck(e.currentTarget.checked)}
.checked=${task.done}
/>
<div class="title">${task.title}</div>
<div class="remove" @click=${onClickRemove}>×</div>
</div>
`
const TaskList = (
tasks: Task[],
onCheck: (id: TaskId, checked: boolean) => void,
onClickRemove: (id: TaskId) => void
) => html`
<div class="task-list">
${tasks.map(t =>
TaskItem(t, checked => onCheck(t.id, checked), () => onClickRemove(t.id))
)}
</div>
`
const NewTask = (
inputText: string,
onInput: (value: string) => void,
onKeyPress: (e: KeyboardEvent) => void
) => html`
<div class="new-task">
<input
type="text"
.value=${inputText}
@input=${e => onInput(e.currentTarget.value)}
@keypress=${onKeyPress}
placeholder="what we have to do?"
/>
</div>
`
const App = () => html`
<div>
<h1>ToDos</h1>
${NewTask(
store().inputText,
inputText => store({ inputText }),
e => {
if (e.key === "Enter") {
const state = store()
store({
tasks: [...state.tasks, { title: state.inputText, id: uid() }],
inputText: ""
})
}
}
)}
${TaskList(
store().tasks,
(id, done) =>
store({
tasks: store().tasks.map(t => (t.id === id ? { ...t, done } : t))
}),
id => store({ tasks: store().tasks.filter(t => t.id !== id) })
)}
</div>
`
所感
デモ用途として React の代わりにお手軽に使おうと思って lit-html を選んだんですが、そんなに変な部分がないし、雑に書いてもちゃんと変更があった部分だけが再描画されてとても嬉しいです。React で何も考えず Stateless Functional Component を作っていると無駄なレンダリングが発生しまくるし、shouldUpdate を書くにも object の比較云々でいろいろ気を使うんですが、そういうのが無いのもすごく嬉しいですね。
あえて LitElement を使わないでやってみましたが、そもそも個人的に誰かが作ったベースクラスがあってそれを継承してなにか作るというのがあまり好みじゃないので、lit-html 楽しくて良いぞという感じです。
規模のあるプロジェクトで導入するにはまだググられ力が足りていない感じがしますが、JSX 入れたくない気持ちがあるときに良いと思います。
基本は lit-html だけで組んで、ある程度まとまった粒度で LitElement を使って Web Components として外に公開すると良さそうかと思いました。
小ネタ
classMap
classMap
を使って classNames 相当のことができます。
import { classMap } from "lit-html/directives/class-map"
const TaskItem = (
task: Task
) => html`
<div class=${classMap({ task: true, done: task.done })}>
<div class="title">${task.title}</div>
</div>
`
-
{ title: "foo", done: false }
が渡されたときはclass="task"
-
{ title: "foo", done: true }
が渡されたときはclass="task done"
になります。