スポンサーリンク

StencilでlocalStorage利用のTODOアプリを写経してみる

2019年9月27日

Stencilとは、Webコンポーネントを作成するためのツール(コンパイラ)だそうです。

何を言っているのか自分でもよく分かりませんが、特定のフレームワークに依存せずに、フォームやナビゲーションバーなどの、『再利用可能なWebページ作成用の部品』を作成するためのツールだそうです。ただ、AngularやVue.jsなどのJavaScriptフレームワークのように使用することもできます。

https://stenciljs.com/

@stencil/coreが2019年6月にVersion1.0.0になり、ようやく使用できるようになったようです。2019年6月以前のチュートリアルをそのまま写経しても、うまくいかないことがあります。そのため、Stencil.jsの解説を読むのであれば、2019年7月以降のものを選んで読むのがおすすめです。

今回は、以下のサイトを写経して、TODOアプリを作ってみたいと思います。
https://www.joshmorony.com/building-a-notepad-application-from-scratch-with-ionic-and-stencil-js/

スポンサードリンク

ソースコードとDEMOサイト

(作成中)
ソースコード


DEMOサイト

開発環境

Windows 10 Pro (1803)
VisualStudioCode 1.37.1
git version 2.20.1.windows.1
nvm-windows 1.1.7
node 12.2.0
npm 6.9.0
yarn 1.16.0

新規Stencilアプリ作成

C:/stencil/ フォルダ内に、stencil-todo-localStorage/ という名前のアプリを作成することにします。

C:/stencil/ フォルダを右クリックして、『Open with Code』をクリックして、VisualStudioCodeで開き、Ctrl+@でターミナル画面を出します。次に、以下を入力して新規stencilアプリを作成します。

npm init stencil
// ionic-pwaを選択して、Enterを押す
// Project name は stencil-todo-localStorage と入力して、Enterを押す
// Confirm? はそのままEnterを押す

以下のようになります。(この時点では、node_modules/フォルダは作成されていません。)

とりあえず、開発サーバを起動してみます。言われるがままに、以下のように入力すると、以下のようにエラーが出ます。

cd stencil-todo-localstorage
npm start

言われたとおりにやっているのにエラーが出るのでびっくりしたのですが、よく見ると、src/ フォルダしかなく、node_modules/ フォルダが無いので、以下のように入力してみます。

npm install
npm start

うまくいったようです。

開発サーバを停止するときは、"Ctrl+C" => y + Enter で停止できるようです。

Stencil.jsのionic-pwaのファイル構造

とりあえず、この時点でのファイル構造を見てみます。
(Windows のコマンドプロンプトで、 “tree /f" とすると、見ることができます。)

src/components/ フォルダには、以下の3つのフォルダあり、それぞれ4つのファイルが入っており、その中の、app-xxx.tsxファイルとapp-xxx.cssファイルがセットになって、コンポーネントを形成しています。

  1. app-root
  2. app-home
  3. app-profile

今回は、トップページは、app-homeコンポーネントが表示されています。app-homeコンポーントの中身は、

src/components/app-home/app-home.tsx

の中の、render() { return [] } の中で指定しているようです。こちらの書き方は、Ionic4 (Angular)に慣れていれば、見慣れている書き方となります。

app-rootコンポーネントは特別で、ルーティングrouting(URLの値によって、表示するコンポーネントを振り分ける)を示しているだけのようです。具体的には、以下のようになっています。

  • ”/” (トップページ)は、app-home.tsx を表示する
  • ”/profile/:name” は、app-profile.tsx を表示する
  • (URLから、@Propとしてnameを受け取っています。)

URLから、@Propとしてnameを受け取り、たとえば、<ion-title>のところで、Profile: {this.name} と記載することにより、nameの内容(今回は"ionic")を表示しています。

なんとなく、構造が分かった気になったところで、次に進みます。

ここまでのソースコード
https://github.com/adash333/stencil-todo-localstorage/tree/3cb8fd6f0a726762126908b166cc49cbdc0b70f8

必要なコンポーネント用ファイルとサービス用ファイルの作成

今回作成するのは、ホームページ(app-home component)にTODOリストが表示され、それおぞれのTODOをクリックすると、詳細ページ(detail component)が表示されるようなアプリです。

また、以下の2つのサービスも作成します。

  • todo.ts (TODOをCRUD(create, read, update, delete)するサービス)
  • storage.ts (TODOリストをlocalStorageに書き換えるサービス)

具体的には、

  1. src/components/app-profile/ フォルダをフォルダごと削除

したのち、以下の4つのファイルを作成します。(4つとも、最初は中身が空のファイルを作成しておきます。)

  1. src/components/app-detail/app-detail.tsx
  2. src/components/app-detail/app-detail.css
  3. src/services/todos.ts
  4. src/services/storage.ts

ルーティングの設定

ルーティング(URLにより、表示するページを振り分けること)の設定を行います。

src/components/app-root/app-root.tsx を編集することにより、以下のようにURLによって表示するページを振り分けます。

  • “/" または “/notes" => app-home.tsx を表示
  • “/todos/2" など => app-detail.tsx を表示(2番目のtodo)
// (変更前)
<ion-route url="/" component="app-home" />
<ion-route url="/profile/:name" component="app-profile" />

// (変更後)
<ion-route url="/" component="app-home" />
<ion-route url="/todos" component="app-home" />
<ion-route url="/todos/:id" component="app-detail" />

(変更前)

(変更後)

参考: https://ionicframework.com/jp/docs/api/router

app-home.tsxの編集

写経元サイトを参考に、トップページを編集し、TODOリストを表示と、TODOの追加をできるようにします。

src/components/app-home/app-home.tsx

(変更前)

(変更後)

Ionicの使い方については、公式Documentが参考になります。
https://ionicframework.com/jp/docs/api/toolbar

TODO追加 を実装

TODOを追加する際に、今回は、ion-alert-controller を利用してみます。そのためには、まず、
src/components/app-root/app-root.tsx
に<ion-alert-controller/ > を 追加します。

次に、
src/components/app-home/app-home.tsx
に、以下のコードを追加します。

componentDidLoad() {}

  addNote() {
    const alertCtrl = document.querySelector("ion-alert-controller");
    console.log(alertCtrl);
  }

// ion-button のところに
<ion-button onClick={() => this.addNote()}>

// ion-list のところに
<ion-list>
  {this.notes.map(note => (
    <ion-item button detail href={`/notes/${note.id}`} routerDirection="forward">
      <ion-label>{note.title}</ion-label>
    </ion-item>
          ))}
</ion-list>

今は画面が真っ白になっていますが、

  • 画面右上のボタンをクリックするとaddTodo() 関数が発動して、Alert画面でTodoを追加する
  • todos 配列(Todoリストを格納)

をこれから実装していきます。

参考: https://ionicframework.com/jp/docs/api/alert

Interfaceインターフェース の作成

Todosサービスを作成する前に、Todoの定義であるtodoインターフェースを作成します。id, title, contentとなります。

src/interfaces/note.ts (新規作成)

export interface Note {
  id: string;
  title: string;
  content: string;
}

src/services/storage.tsの作成

データをlocalStorageに保存したりするためのコードを、
src/services/storage.ts
に記載します。以下の3つの関数を定義します。

  • get(key: string)
  • set(key: string, value: any)
  • remove(key: string)

参考1:ブラウザにデータを保存するlocalStorage(ローカルストレージ)の使い方 Posted by NAGAYA on Oct 26th, 2017

参考2:JavaScriptのLocalStrage(ローカルストレージ) の使い方を現役エンジニアが解説【初心者向け】2018/2/21

src/services/todo.tsの作成

前述のservices/storage.ts を利用して、Todoリストの書き込み、削除などのサービスを、
src/services/todo.ts
に記載します。以下の6個の関数(load、saveとCRUD)を定義します。

  • load()
  • save()
  • getTodo(id)
  • createTodo(title)
  • updateTodo(Todo, content)
  • deleteTodo(Todo)

そして、最後に、全部を、

export const todosService = new todosServiceController();

としてexportしています。

app-home.tsxを修正してaddTodo()を完成させる

src/components/app-home/app-home.tsx

を以下のように編集して、Todoの追加の実装を完成させます。

// 最初に以下を追加して、State(from @stencil/core)と、todoインターフェースとtodosサービスをインポート
import { Component, State, h } from '@stencil/core';
import { Todo } from "../../interfaces/todo";
import { TodosService } from "../../services/todos";

// export class AppHome { の直後に以下を追加
  @State() todos: Todo[] = [];

  async componentDidLoad() {
    this.todos = await TodosService.load();
  }

  async addTodo() {
    const alertCtrl = document.querySelector("ion-alert-controller");

    let alert = await alertCtrl.create({
      header: "New Note",
      message: "What should the title of this todo be?",
      inputs: [
        {
          type: "text",
          name: "title"
        }
      ],
      buttons: [
        {
          text: "Cancel"
        },
        {
          text: "Save",
          handler: async data => {
            TodosService.createTodo(data.title);
            this.todos = [...(await TodosService.load())];
          }
        }
      ]
    });

    alert.present();
  }

これでやっと、Todoの追加(Titleのみ)を実装することができました。

参考: https://ionicframework.com/jp/docs/api/alert

ここまでのソースコード
https://github.com/adash333/stencil-todo-localstorage/tree/58edf280fd52fe2f3ed3e96d2f49ee81c051cb51

app-detailページの実装

app-homeページのそれぞれのTodo(のtitle)をクリックすると、対応するapp-detailページへ移動し、

Todoのtitle, detailの表示
detailの編集、削除

ができるようにします。

  • componentDidLoad() 関数の中で、初期化を行う
  • noteChanged(ev) 関数
  • deleteTodo() 関数

src/components/app-detail/app-detail.tsx

ターミナル画面に、

『@Prop() id: string; のところは、本当は、"id"という文字列は使ってほしくないので、別の文字列に変更することをお勧めします。』

と出ていますが、今回は無視します。

削除しても、リロードしないと削除されないなど、なんか微妙に挙動がおかしいです。
src/components/app-home/app-home.tsx
に、以下のコードを追加します。(理由はよくわかりません。)

public navCtrl = document.querySelector("ion-router");

この状況下でも、app-detailページでTodoを削除しても、リロードしない限りは、app-homeページで、直前に削除したTodoのTitleが表示されたままでした。リロードすると消えるので、localStorageからは削除されているようですが、app-homeページが自動ではページを更新してくれないようです。

そこで、
src/components/app-home/app-home.tsx
の15行目を以下のように変更します。

// (変更前)
this.todos = await TodosService.load();

// (変更後)
this.todos = [...(await TodosService.load())];

この変更を行っても、Todoを削除したときに、すぐにTodoリストから削除されませんでした。理由はわかりません。残念。。。

@stateとcomponentDidLoad()

State Decorator(公式ドキュメント)  によると、内容が変化する可能性のあるデータには、 @state()デコレータをつけるそうです。

@State() propertyが変更されるたびに、コンポーネントのrender関数が呼び出される(再描画される)。

とのことなのですが、今回は、app-detail.tsxでデータを削除しても、app-home.tsxに自動的に反映するためには、app-home.tsxに以下のように記載する必要があるそうです。

  @State() todos: Todo[] = [];
  public navCtrl = document.querySelector("ion-router");

  async componentDidLoad() {
    // URLが変化したら、notesをNoteServiceからloadする
    this.navCtrl.addEventListener("ionRouteDidChange", async () => {
      this.todos = [...(await TodosService.load())];
    });
  }

(変更前)

(変更後)

しかし、これでも、app-detail.tsxで、特定のTodoを削除しても、その直後のapp-home.tsxでは直前に削除したTodoが表示されたままで、更新ボタンを押すと、消えました。。。原因はわかりません。がっくしです。

Stencilの@propと@state に少しだけまとめてみましたので、もしよろしければご覧ください。

app-detail.cssの編集

app-detailのところのテキストボックスの大きさがすごく変なので、app-detail.css を編集します。

(変更前)

(変更後)

うまくいきません。Ctrl + Shift + I で見てみると、

<ion-textarea class="native-textarea sc-ion-textarea-md" ...>
</ion-textarea>

の中に、

<textarea class="native-textarea sc-ion-textarea-md" ...>
(書き込んだ内容)
</textarea class=">

が入ってしまっています。

途中。。。

不完全ですが、この時点でのソースコードとDEMOサイトを以下に記載しておきます。

ソースコードとDEMOサイト(不完全)

ソースコード
https://github.com/adash333/stencil-todo-localstorage/tree/ee509427375c887b9903eb8622ed72f537dad6c4

DEMOサイト
https://nostalgic-feynman-6f201d.netlify.com

Netlifyにデプロイする際の設定

作成中

スポンサーリンク