Skip to content
Mithril.js 2
Main Navigation ガイドAPI

日本語

English
简体中文
繁體中文
Español
Français
Русский
Português – Brasil
Deutsch
한국어
Italiano
Polski
Türkçe
čeština
magyar

日本語

English
简体中文
繁體中文
Español
Français
Русский
Português – Brasil
Deutsch
한국어
Italiano
Polski
Türkçe
čeština
magyar

外観

Sidebar Navigation

はじめに

インストール方法

シンプルなアプリケーション

リソース

JSX

レガシーブラウザでの ES6+ の利用

アニメーション

テスト

例

サードパーティとの統合

パスの取り扱い

主要な概念

Virtual DOM ノード

コンポーネント

ライフサイクルメソッド

Key(キー)

自動再描画システム

その他

フレームワークの比較

v1.x からの移行

v0.2.x からの移行

API

このページの内容

コンポーネント ​

構造 ​

コンポーネントは、ビューの一部をカプセル化し、コードの整理や再利用を容易にするための仕組みです。

view メソッドを持つ JavaScript オブジェクトは、Mithril.js のコンポーネントとみなされます。コンポーネントは、m() ユーティリティ関数を通じて利用できます。

javascript
// コンポーネントの定義
var Example = {
  view: function (vnode) {
    return m('div', 'Hello');
  },
};

// コンポーネントの利用
m(Example);

// 上記と同等の HTML
// <div>Hello</div>

ライフサイクルメソッド ​

コンポーネントは、仮想 DOM ノードと同様のライフサイクルメソッドを持つことができます。各ライフサイクルメソッドと view には、引数として vnode が渡されることに注意してください(onbeforeupdate には、以前の vnode も追加で渡されます)。

javascript
var ComponentWithHooks = {
  oninit: function (vnode) {
    console.log('initialized');
  },
  oncreate: function (vnode) {
    console.log('DOM created');
  },
  onbeforeupdate: function (newVnode, oldVnode) {
    return true;
  },
  onupdate: function (vnode) {
    console.log('DOM updated');
  },
  onbeforeremove: function (vnode) {
    console.log('exit animation can start');
    return new Promise(function (resolve) {
      // アニメーション完了後に resolve を呼び出す
      resolve();
    });
  },
  onremove: function (vnode) {
    console.log('removing DOM element');
  },
  view: function (vnode) {
    return 'hello';
  },
};

他の種類の仮想 DOM ノードと同様に、コンポーネントは vnode タイプとして使用される際に、追加のライフサイクルメソッドを定義できます。

javascript
function initialize(vnode) {
  console.log('initialized as vnode');
}

m(ComponentWithHooks, { oninit: initialize });

vnode のライフサイクルメソッドは、コンポーネントのメソッドをオーバーライドしません。また、その逆もありません。コンポーネントのライフサイクルメソッドは、常に vnode の対応するメソッドの後に実行されます。

vnode で独自のコールバック関数名にライフサイクルメソッド名を使用しないように注意してください。

ライフサイクルメソッドの詳細については、ライフサイクルメソッドのページを参照してください。

コンポーネントへのデータの受け渡し ​

データは、ハイパースクリプト関数で attrs オブジェクトを 2 番目のパラメータとして渡すことによって、コンポーネントインスタンスに渡すことができます。

javascript
m(Example, { name: 'Floyd' });

このデータは、コンポーネントの view またはライフサイクルメソッドで vnode.attrs を介してアクセスできます。

javascript
var Example = {
  view: function (vnode) {
    return m('div', 'Hello, ' + vnode.attrs.name);
  },
};

注意: ライフサイクルメソッドは attrs オブジェクトでも定義できるため、独自のコールバックにそれらの名前を使用することは避ける必要があります。Mithril.js 自体もそれらを呼び出すためです。それらをライフサイクルメソッドとして使用したい場合にのみ、attrs で使用してください。

ステート ​

すべての仮想 DOM ノードと同様に、コンポーネント vnode はステートを持つことができます。コンポーネントのステートは、オブジェクト指向アーキテクチャのサポートやカプセル化、関心の分離に役立ちます。

他の多くのフレームワークとは異なり、Mithril.js ではコンポーネントのステートを変更しても、自動的に再描画や DOM の更新がトリガーされるわけではないことに注意してください。代わりに、再描画は、イベントハンドラが起動したとき、m.request によって行われた HTTP リクエストが完了したとき、またはブラウザが別のルートに移動したときに実行されます。Mithril.js のコンポーネントステートメカニズムは、あくまでアプリケーションの便宜のために提供されています。

上記の条件によらないステートの変更(例:setTimeout の後)が発生した場合は、m.redraw() を使用して手動で再描画をトリガーできます。

クロージャコンポーネントステート ​

上記の例では、各コンポーネントは POJO(Plain Old JavaScript Object)として定義されています。これは、Mithril.js がそのコンポーネントのインスタンスのプロトタイプとして内部的に使用します。POJO でコンポーネントステートを使用することは可能ですが(後述)、最もクリーンで簡単なアプローチではありません。代わりに、クロージャコンポーネント** を使用することを推奨します。これは、POJO コンポーネントインスタンスを 返す ラッパー関数であり、それ自体がクローズされたスコープを持ちます。

クロージャコンポーネントを使用すると、ステートは外側の関数内で宣言された変数によって簡単に管理できます。

javascript
function ComponentWithState(initialVnode) {
  // コンポーネントステート変数。各インスタンスに固有
  var count = 0;

  // POJO コンポーネントインスタンス:vnode を返す view 関数を持つオブジェクト
  return {
    oninit: function (vnode) {
      console.log('init a closure component');
    },
    view: function (vnode) {
      return m(
        'div',
        m('p', 'Count: ' + count),
        m(
          'button',
          {
            onclick: function () {
              count += 1;
            },
          },
          'Increment count'
        )
      );
    },
  };
}

クロージャ内で宣言された関数も、そのステート変数にアクセスできます。

javascript
function ComponentWithState(initialVnode) {
  var count = 0;

  function increment() {
    count += 1;
  }

  function decrement() {
    count -= 1;
  }

  return {
    view: function (vnode) {
      return m(
        'div',
        m('p', 'Count: ' + count),
        m(
          'button',
          {
            onclick: increment,
          },
          'Increment'
        ),
        m(
          'button',
          {
            onclick: decrement,
          },
          'Decrement'
        )
      );
    },
  };
}

クロージャコンポーネントは、POJO と同じように使用されます。例:m(ComponentWithState, { passedData: ... })。

クロージャコンポーネントの大きな利点は、イベントハンドラコールバックをアタッチするときに this をバインドすることを心配する必要がないことです。実際、this はまったく使用されず、this コンテキストのあいまいさについて考慮する必要はありません。

POJO コンポーネントステート ​

一般的に、コンポーネントステートの管理にはクロージャを使用することをお勧めします。ただし、POJO でステートを管理する理由がある場合は、コンポーネントのステートは、初期化時のブループリントとして、vnode.state を介して、およびコンポーネントメソッドの this キーワードを介して、次の 3 つの方法でアクセスできます。

初期化時 ​

POJO コンポーネントの場合、コンポーネントオブジェクトは各コンポーネントインスタンスのプロトタイプであるため、コンポーネントオブジェクトで定義されたプロパティは vnode.state のプロパティとしてアクセスできます。これにより、シンプルな「ブループリント」状態の初期化が可能になります。

以下の例では、data は ComponentWithInitialState コンポーネントの vnode.state オブジェクトのプロパティになります。

javascript
var ComponentWithInitialState = {
  data: 'Initial content',
  view: function (vnode) {
    return m('div', vnode.state.data);
  },
};

m(ComponentWithInitialState);

// 上記と同等の HTML
// <div>Initial content</div>

vnode.state 経由 ​

ご覧のとおり、ステートは vnode.state プロパティを介してアクセスすることもできます。これは、すべてのライフサイクルメソッドとコンポーネントの view メソッドで使用できます。

javascript
var ComponentWithDynamicState = {
  oninit: function (vnode) {
    vnode.state.data = vnode.attrs.text;
  },
  view: function (vnode) {
    return m('div', vnode.state.data);
  },
};

m(ComponentWithDynamicState, { text: 'Hello' });

// 上記と同等の HTML
// <div>Hello</div>

this キーワード経由 ​

ステートは、this キーワードを介してアクセスすることもできます。これは、すべてのライフサイクルメソッドとコンポーネントの view メソッドで使用できます。

javascript
var ComponentUsingThis = {
  oninit: function (vnode) {
    this.data = vnode.attrs.text;
  },
  view: function (vnode) {
    return m('div', this.data);
  },
};

m(ComponentUsingThis, { text: 'Hello' });

// 上記と同等の HTML
// <div>Hello</div>

ES5 関数を使用する場合、ネストされた匿名関数内の this の値はコンポーネントインスタンスではないことに注意してください。この JavaScript の制限を回避するには、アロー関数を使用するか、サポートされていない場合は vnode.state を使用することをお勧めします。

クラス ​

ニーズに合致する場合は(オブジェクト指向プロジェクトなど)、クラスを使用してコンポーネントを作成することもできます。

javascript
class ClassComponent {
  constructor(vnode) {
    this.kind = 'class component';
  }
  view() {
    return m('div', `Hello from a ${this.kind}`);
  }
  oncreate() {
    console.log(`A ${this.kind} was created`);
  }
}

クラスコンポーネントは、ツリーをレンダリングするために、.prototype.view を介して検出される view() メソッドを定義する必要があります。

これらは、通常のコンポーネントと同じように使用できます。

javascript
// 例:m.render 経由
m.render(document.body, m(ClassComponent));

// 例:m.mount 経由
m.mount(document.body, ClassComponent);

// 例:m.route 経由
m.route(document.body, '/', {
  '/': ClassComponent,
});

// 例:コンポーネントの構成
class AnotherClassComponent {
  view() {
    return m('main', [m(ClassComponent)]);
  }
}

クラスコンポーネントステート ​

クラスを使用すると、ステートはクラスインスタンスのプロパティとメソッドによって管理でき、this を介してアクセスできます。

javascript
class ComponentWithState {
  constructor(vnode) {
    this.count = 0;
  }
  increment() {
    this.count += 1;
  }
  decrement() {
    this.count -= 1;
  }
  view() {
    return m(
      'div',
      m('p', 'Count: ', this.count),
      m(
        'button',
        {
          onclick: () => {
            this.increment();
          },
        },
        'Increment'
      ),
      m(
        'button',
        {
          onclick: () => {
            this.decrement();
          },
        },
        'Decrement'
      )
    );
  }
}

this コンテキストを正しく参照できるように、イベントハンドラコールバックにはアロー関数を使用する必要があることに注意してください。

コンポーネントの種類の混合 ​

コンポーネントの種類は自由に組み合わせることができます。クラスコンポーネントは、クロージャまたは POJO コンポーネントを子として持つことができます。

特殊な属性 ​

Mithril.js では、特定のプロパティ key に特別な意味を持たせているため、通常のコンポーネント属性ではそれらの使用を避けてください。

  • ライフサイクルメソッド:oninit、oncreate、onbeforeupdate、onupdate、onbeforeremove、および onremove
  • key:key 付きフラグメントで ID を追跡するために使用されます
  • tag:vnode を通常の属性オブジェクトや vnode 以外のオブジェクトと区別するために使用されます。

アンチパターンを避ける ​

Mithril.js は柔軟ですが、いくつかのコードパターンは推奨されません。

肥大化したコンポーネントを避ける ​

一般的に、「太った」コンポーネントとは、カスタムインスタンスメソッドを持つコンポーネントのことです。つまり、関数を vnode.state または this にアタッチすることは避けるべきです。コンポーネントインスタンスメソッドに論理的に適合し、他のコンポーネントで再利用できないロジックを持つことは非常にまれです。そのロジックが将来別のコンポーネントで必要になる可能性は比較的高いです。

そのロジックがコンポーネントステートに強く結びついている場合を除き、データレイヤーに配置されている方がコードをリファクタリングするのが簡単です。

このような太ったコンポーネントを考えてみてください。

javascript
// views/Login.js
// 避けるべきパターン
var Login = {
  username: '',
  password: '',
  setUsername: function (value) {
    this.username = value;
  },
  setPassword: function (value) {
    this.password = value;
  },
  canSubmit: function () {
    return this.username !== '' && this.password !== '';
  },
  login: function () {
    /*...*/
  },
  view: function () {
    return m('.login', [
      m('input[type=text]', {
        oninput: function (e) {
          this.setUsername(e.target.value);
        },
        value: this.username,
      }),
      m('input[type=password]', {
        oninput: function (e) {
          this.setPassword(e.target.value);
        },
        value: this.password,
      }),
      m(
        'button',
        { disabled: !this.canSubmit(), onclick: this.login },
        'Login'
      ),
    ]);
  },
};

通常、大規模なアプリケーションのコンテキストでは、上記のログインコンポーネントは、ユーザー登録およびパスワード回復用のコンポーネントと並んで存在します。ログイン画面から登録画面またはパスワード回復画面に移動するときに、メールフィールドに事前に入力できるようにしたいと想像してください(またはその逆)。ユーザーが間違ったページに入力した場合にメールを再入力する必要がないようにするためです(または、ユーザー名が見つからない場合は、ユーザーを登録フォームに誘導したい場合があります)。

すぐに、このコンポーネントから別のコンポーネントに username および password フィールドを共有することが難しいことがわかります。これは肥大化したコンポーネントがステートをカプセル化しているため、定義上、外部からのアクセスが難しくなるためです。

このコンポーネントをリファクタリングし、ステートコードをコンポーネントからアプリケーションのデータレイヤーに分離する方が理にかなっています。これは、新しいモジュールを作成するのと同じくらい簡単です。

javascript
// models/Auth.js
// 推奨パターン
var Auth = {
  username: '',
  password: '',
  setUsername: function (value) {
    Auth.username = value;
  },
  setPassword: function (value) {
    Auth.password = value;
  },
  canSubmit: function () {
    return Auth.username !== '' && Auth.password !== '';
  },
  login: function () {
    /*...*/
  },
};

module.exports = Auth;

次に、コンポーネントをクリーンアップできます。

javascript
// views/Login.js
// 推奨パターン
var Auth = require('../models/Auth');

var Login = {
  view: function () {
    return m('.login', [
      m('input[type=text]', {
        oninput: function (e) {
          Auth.setUsername(e.target.value);
        },
        value: Auth.username,
      }),
      m('input[type=password]', {
        oninput: function (e) {
          Auth.setPassword(e.target.value);
        },
        value: Auth.password,
      }),
      m(
        'button',
        {
          disabled: !Auth.canSubmit(),
          onclick: Auth.login,
        },
        'Login'
      ),
    ]);
  },
};

このようにして、Auth モジュールは認証関連のステートの信頼できる唯一の情報源になり、Register コンポーネントはこのデータに簡単にアクセスでき、必要に応じて canSubmit などのメソッドを再利用することもできます。さらに、検証コードが必要な場合(たとえば、メールフィールドの場合)、setEmail を変更するだけで済み、その変更により、メールフィールドを変更するすべてのコンポーネントでメールの検証が行われます。

おまけとして、コンポーネントのイベントハンドラのステートへの参照を保持するために .bind を使用する必要がなくなったことに注意してください。

vnode.attrs 自体を他の vnode に転送しないでください ​

インターフェイスを柔軟に保ち、実装を簡単にするために、属性を特定の child コンポーネントまたは要素に転送したい場合があります。たとえば、Bootstrap のモーダルなどです。次のように vnode の属性を転送したくなるかもしれません。

javascript
// 避けるべきパターン
var Modal = {
  // ...
  view: function (vnode) {
    return m('.modal[tabindex=-1][role=dialog]', vnode.attrs, [
      //         ここで `vnode.attrs` を転送 ^
      // ...
    ]);
  },
};

上記のように行うと、使用時に問題が生じる可能性があります。

javascript
var MyModal = {
  view: function () {
    return m(
      Modal,
      {
        // これでは2回トグルされるため、正しく表示されません
        onupdate: function (vnode) {
          if (toggle) $(vnode.dom).modal('toggle');
        },
      },
      [
        // ...
      ]
    );
  },
};

代わりに、個々の 属性を vnode に転送する必要があります。

javascript
// 推奨パターン
var Modal = {
  // ...
  view: function (vnode) {
    return m('.modal[tabindex=-1][role=dialog]', vnode.attrs.attrs, [
      //              ここで `attrs:` を転送 ^
      // ...
    ]);
  },
};

// 例
var MyModal = {
  view: function () {
    return m(Modal, {
      attrs: {
        // これにより 1 回切り替わります
        onupdate: function (vnode) {
          if (toggle) $(vnode.dom).modal('toggle');
        },
      },
      // ...
    });
  },
};

children を操作しないでください ​

コンポーネントが属性または children の適用方法について独自のルールを持っている場合は、カスタム属性の使用に切り替える必要があります。

多くの場合、コンポーネントに設定可能なタイトルと本文がある場合など、複数の children のセットを定義することが望ましいです。

この目的のために children プロパティを分割代入するのは避けてください。

javascript
// 避けるべきパターン
var Header = {
  view: function (vnode) {
    return m('.section', [
      m('.header', vnode.children[0]),
      m('.tagline', vnode.children[1]),
    ]);
  },
};

m(Header, [m('h1', 'My title'), m('h2', 'Lorem ipsum')]);

// 不自然な使用例
m(Header, [
  [m('h1', 'My title'), m('small', 'A small note')],
  m('h2', 'Lorem ipsum'),
]);

上記のコンポーネントは、children が受信したのと同じ連続した形式で出力されるという前提を破ります。実装を読まないとコンポーネントを理解するのが困難です。代わりに、属性を名前付きパラメータとして使用し、均一な child コンテンツのために children を予約します。

javascript
// 推奨パターン
var BetterHeader = {
  view: function (vnode) {
    return m('.section', [
      m('.header', vnode.attrs.title),
      m('.tagline', vnode.attrs.tagline),
    ]);
  },
};

m(BetterHeader, {
  title: m('h1', 'My title'),
  tagline: m('h2', 'Lorem ipsum'),
});

// より明確な使用例
m(BetterHeader, {
  title: [m('h1', 'My title'), m('small', 'A small note')],
  tagline: m('h2', 'Lorem ipsum'),
});

コンポーネントを静的に定義し、動的に呼び出す ​

ビュー内でコンポーネント定義を作成することを避ける ​

view メソッド内からコンポーネントを作成する場合(直接インラインで、またはそれを行う関数を呼び出すことによって)、再描画ごとにコンポーネントの異なるクローンが作成されます。コンポーネント vnode を diff する際、新しい vnode が参照するコンポーネントと古い vnode が参照するコンポーネントが厳密に等しくない場合、たとえ最終的に同じコードを実行するとしても、それらは異なるコンポーネントとみなされます。これは、ファクトリを介して動的に作成されたコンポーネントは常に最初から再作成されることを意味します。

そのため、コンポーネントを再作成することは避けるべきです。代わりに、コンポーネントを慣用的に使用してください。

javascript
// 避けるべきパターン
var ComponentFactory = function (greeting) {
  // 呼び出しごとに新しいコンポーネントを作成します
  return {
    view: function () {
      return m('div', greeting);
    },
  };
};
m.render(document.body, m(ComponentFactory('hello')));
// 2 回目の呼び出しでは、何もしないのではなく、div を最初から再作成します
m.render(document.body, m(ComponentFactory('hello')));

// 推奨パターン
var Component = {
  view: function (vnode) {
    return m('div', vnode.attrs.greeting);
  },
};
m.render(document.body, m(Component, { greeting: 'hello' }));
// 2 回目の呼び出しでは DOM は変更されません
m.render(document.body, m(Component, { greeting: 'hello' }));

ビューの外でコンポーネントインスタンスを作成することを避ける ​

逆に、同様の理由から、コンポーネントインスタンスがビューの外で作成された場合、将来の再描画ではノードの等価性チェックが実行され、スキップされます。したがって、コンポーネントインスタンスは常にビュー内で作成する必要があります。

javascript
// 避けるべきパターン
var Counter = {
  count: 0,
  view: function (vnode) {
    return m(
      'div',
      m('p', 'Count: ' + vnode.state.count),

      m(
        'button',
        {
          onclick: function () {
            vnode.state.count++;
          },
        },
        'Increase count'
      )
    );
  },
};

var counter = m(Counter);

m.mount(document.body, {
  view: function (vnode) {
    return [m('h1', 'My app'), counter];
  },
});

上記の例では、カウンタコンポーネントボタンをクリックすると、そのステートカウントが増加しますが、コンポーネントを表す vnode が同じ参照を共有するため、そのビューはトリガーされません。したがって、レンダリングプロセスではそれらが diff されません。新しい vnode が作成されるように、常にビューでコンポーネントを呼び出す必要があります。

javascript
// 推奨パターン
var Counter = {
  count: 0,
  view: function (vnode) {
    return m(
      'div',
      m('p', 'Count: ' + vnode.state.count),

      m(
        'button',
        {
          onclick: function () {
            vnode.state.count++;
          },
        },
        'Increase count'
      )
    );
  },
};

m.mount(document.body, {
  view: function (vnode) {
    return [m('h1', 'My app'), m(Counter)];
  },
});
Pager
前のページVirtual DOM ノード
次のページライフサイクルメソッド

MITライセンス の下で公開されています。

Copyright (c) 2024 Mithril Contributors

https://mithril.js.org/components.html

MITライセンス の下で公開されています。

Copyright (c) 2024 Mithril Contributors