巷で話題のnue.jsを試してみた(2) v0.1.1でのコンポーネントレンダリング

先日書いた [巷で話題のnue.jsを試してみた](https://qiita.com/haruyan_hopemucci/items/93d4556afb76b0c964b9) の続きです。

[create-nue](https://github.com/nuejs/create-nue)のソースコードを読んで、前回の記事で完成できなかった複数コンポーネントのレンダリングができるようになったので後悔、もとい公開しようと思います。

## TodoリストとTodo入力を作ってみる

実装例としてよくあるTodoリストを作ってみます。

### Todoデータ管理コンポーネント

[Sharing code between components](https://nuejs.org/docs/nuejs/reactive-components.html#sharing-code-between-components)の章に記載されているように、コンポーネント間でコードの共有を使った方法を試してみます。

以下のようなTodoデータ管理コンポーネントを作成します。

```js:todo.js
class TodoItem {
  constructor(title) {
    this.title = title
    this.isDone = false
  }
}

export const todoItems = [new TodoItem('sample todo')]

export function addTodoItem(title) {
  todoItems.push(new TodoItem(title))
}
```
Todoの中身を保持する`todoItems`という変数と、Todoを追加する`addTodoItem`関数を外部公開するコンポーネントです。TodoItemクラスは内部でしか使用しません。
この実装はチュートリアルのカート実装に内容を合わせています。

### Todoコンポーネント

上記データコンポーネントを共有するようにした、レンダリング用のコンポーネントを作成します。

```html:templates/TodoComponents.nue.html
<script>
  import { todoItems, addTodoItem } from "./todo.js";
</script>

<div @name="todo-list">
  <p :for="item in items">
    <span>{ item.title }</span>
    <spna>{ item.isDone ? '完了' : '進行中' }</spna>
  </p>
  <script>
    constructor(){
      this.items = todoItems
      console.debug('constractor todo-list')
    }
  </script>
</div>

<div @name="input-todo">
  <input type="text" name="title" ref="inputTodo" />
  <button @click="onClickInputTodo">Add Todo</button>
  <script>
    constructor(){
      console.debug('constractor input-todo')
    }
    onClickInputTodo() {
      addTodoItem(this.$refs.inputTodo.value);
    }
  </script>
</div>
```

ファイルの先頭にscriptタグを書き、コード共有したいjsファイルをimportします。

続いて2つのコンポーネント `todo-list`と`input-todo`を定義しています。

コンポーネント名はv0.1.1現在ではケバブケースまたはスネークケースでしか記載できないようです。キャメルケース、パスカルケースでは記載できません。なぜなら後段のレンダリングでタグ名は全部lowercase変換されてしまうからです。前回の記事ではそこでハマっていました。

コード共有でimportした変数は、一旦コンポーネントの`this`にバインドしないと`:for`ディレクティブでアクセスできないようです。要注意点。

Todo入力コンポーネントで新しいTodoを追加する処理では、`$refs`を利用してinput要素の中身を取得しています。refの使い方はReactよりも易しそうです。

### ページの作成

コンポーネントを配置するページを作成します。

```html:templates/AppTodo.nue.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>TodoApp</title>
  </head>
  <body>
    <main @name="app-todo">
      <h1>Todo</h1>
      <h2>Input Todo</h2>
      <input-todo />
      <h2>List</h2>
      <todo-list />
      <script type="module" src="./app_todo.js"></script>
    </main>
  </body>
</html>
```

Todoコンポーネントファイルにて、`@name`で定義した`input-todo`と`todo-list`をタグにして単純に配置しているだけです。
また、`app_todo.js`というjsファイルはクライアントレンダリング用のスクリプトです。
中身はあとで紹介します。

### render.jsの作成

サーバーサイドでページファイルをレンダリングするための`render.js`を作成します。
中身は`create-nue`を丸ごとパクって固有名詞だけ変更しています。

```js:render.js
import { parse, render } from 'nuejs-core'
import { promises as fs } from 'node:fs'

const read = async (name, dir = 'templates') => await fs.readFile(dir + '/' + name, 'utf-8')
// read dependencies (server-side components)
const page = await read('AppTodo.nue.html')

const html = render(page)

// write index.html
await fs.writeFile('./www/todo.html', html)

console.log('wrote', 'www/todo.html')
```
`AppTodo.nue.html`をレンダリングして`www/todo.html`に書き出しています。
`renderFile`使ってないなぁ。
レンダリング対象が増えてきたらフォルダをglabして出力するrender.jsを書いた方が良さそう。
なお`render.js`は`create-nue`ではnpmのコマンドに組み込まれています。

### コンポーネントのコンパイル

Todoコンポーネントのコンパイルを行う`compile.js`を用意します。

```js:compile.js
import { compileFile } from 'nuejs-core'

await compileFile('templates/TodoComponents.nue.html','www/lib/TodoComponents.js')
```
`compile.js`も`create-nue`ではnpmのコマンドに組み込まれていました。npmから実行可能です。

### クライアントレンダリング用スクリプトの作成

ページファイルを作成したときに最後に出てきた`app_todo.js`を作成します。

```js:www/app_todo.js
import createApp from './nue.js'
import { lib } from './lib/TodoComponents.js'

// Mount <nue-island/> nodes as a Web Component
class NueIsland extends HTMLElement {
  connectedCallback() {
    const App = lib.find(el => el.name == this.getAttribute('island'))
    if (App) createApp(App, {}, lib).mount(this)
  }
}

customElements.define('nue-island', NueIsland)
```

`create-nue`の`setup.js`からほぼそのまま失敬してきました。
コンパイルで生成する`./lib/TodoComponents.js`をimportし、`nue-island`というカスタム要素を登録するスクリプトみたいです。これは[Hybrid components](https://nuejs.org/docs/nuejs/isomorphic-components.html#hybrid)で紹介されている内容と同等のものと思われます。

### レンダリングとコンパイル

いよいよレンダリングとコンパイルを実行します。

```
haruyan@haruyan-mac nue-demo % node render.js && node compile.js 
wrote www/todo.html
```

エラーなく完了しました。
レンダリングした`todo.html`はこんな感じに変換されています。

```html:www/todo.html
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TodoApp</title>
  </head>
  <body>
    <main>
      <h1>Todo</h1>
      <h2>Input Todo</h2>
      <nue-island island="input-todo"></nue-island>
      <h2>List</h2>
      <nue-island island="todo-list"></nue-island>
      <script type="module" src="./app_todo.js"></script>
    </main>
  </body>
</html>
```

`<input-todo/>`や`<todo-list/>`だった箇所が`<nue-island>`タグに変換されています。このカスタム要素と中に書いてある`island`属性を利用してコンポーネントのレンダリング対象を判断するものと推測されます。

これで表示に必要なファイルは全て作成完了です。

### 実際にブラウザで表示してみる

実際の表示結果がこちらです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653626/aca1943a-d841-ebf6-869b-8e9bd4ee36d5.png)

Todo入力欄、Todoリスト欄がそれぞれ表示されています。`todoItems`初期化時に1件だけサンプルTodoを入れているのでそれがそのまま表示されています。

Todoを追加したらしっかりList側に反映されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653626/c8d05a13-3b01-6d25-b0fe-c30954791446.png)

## 異なるコンポーネント定義ファイル間で状態の共有ができるかどうか

Vueでいうところの`Vuex`、Reactでいうところの`recoil`で実現されているコンポーネント間の状態共有について試してみます。
現状では狭い範囲で「状態のコンポーネント間共有」はできています。ドキュメント通り同じファイルに別々のコンポーネントを定義し、そのトップレベルにscriptタグで状態を管理するjsファイルをimportすることで共有を実現しています。

では異なるコンポーネント定義ファイルで状態の共有はできるのでしょうか？

既存のコンポーネントとは別にコンポーネントファイルを作成してみます。

```js:templates/RichTodoComponents.nue.html
<script>
  import { todoItems, addTodoItem } from "./todo.js";
</script>

<div @name="rich-todo-list">
  <p :for="item in items">
    { item.title } は { item.isDone ? '完了' : '進行中' } です。
  </p>
  <script>
    constructor(){
      this.items = todoItems
      console.debug('constractor rich-todo-list')
    }
  </script>
</div>
```
Todoの表示が文章になったTodoListです。どこがリッチなんだとかツッコミは受け付けません。
ファイル冒頭に先ほど作った`TodoComponents`と同様のscriptタグを設置します。

このコンポーネントをページに追加します。

```html:templates/AppTodo.nue.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>TodoApp</title>
  </head>
  <body>
    <main @name="app-todo">
      <h1>Todo</h1>
      <h2>Input Todo</h2>
      <input-todo />
      <h2>List</h2>
      <todo-list />
      <h2>Rich List</h2>
      <rich-todo-list />
      <script type="module" src="./app_todo.js"></script>
    </main>
  </body>
</html>
```

`<rich-todo-list>`を追加しました。

最後に新しく作ったコンポーネントをコンパイルするようにします。

```js:compile.js
import { compileFile } from 'nuejs-core'
await compileFile('templates/TodoComponents.nue.html', 'www/lib/TodoComponents.js')
await compileFile('templates/RichTodoComponents.nue.html', 'www/lib/RichTodoComponents.js')
```

ファイルの修正が完了したら `npm render.js && npm compile.js`でレンダリングとコンパイルを実行します。

初期表示はこのようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653626/0b96dbfd-c05c-950e-1e35-6cfc7d371803.png)

Input Todo から Todoを追加するとList側にもRich List側にも追加が反映されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653626/92b4a267-e223-2fa4-313f-9c5a33e83580.png)

ブラ○ト、やるな!

コンポーネント間のデータ共有はVueやReactよりもかなり簡単にできそうです。簡単にできすぎて心配になるくらいです。チーム開発では闇雲に共有データを壊されないように共有データコンポーネントをしっかり設計する必要がありそうです。それに注意さえすればかなり良いんじゃないでしょうか。

## 終わりに

とりあえず「複数コンポーネントの描画ができました」の報告でした。
今回学んだことは、

- 作成するコンポーネント名はケバブケースで書かないといけない。Vueみたいにパスカルケースでコンポーネント名を書くとハマるぞ！公式ドキュメントはそんなこと教えてくれないぞ！
- 複数のコンポーネントで状態共有ができる。状態共有コンポーネントをファイルの先頭でimportするだけで済むぞ！

といったところです。

今回学習用に作成したコードをgithubに公開します。よろしければ参考にしてください。
https://github.com/haruyan-hopemucci/nue-demo