この翻訳は不完全です。英語から この記事を翻訳 してください。

In this article we explore the exciting new possibilities of web components for web app developers and how Mozilla's Brick and X-Tag libraries can facilitate their use. First we'll use Brick to rapidly prototype a simple application. Then, we'll build a custom web component using X-Tag.

Web Components が解決する課題

アプリケーションのプラットフォーム、という点において、ウェブには課題があります。HTMLという、文書をかんたんにマークアップすることができ、そして意味を与えることができる言語は、アプリケーションを組み上げるには、充分な要素がありません。HTML5 の仕様には、かなり多くの新しい要素が追加されましたが、しかし、それらのサポートはブラウザごとに不完全で、他のプラットフォームにはないウィジェットがたくさんあります。例えば、Flex や iOS が提供する独特なものです。結果として、開発者は、メニューバー、スライダーコントロール、カレンダーといったものを、セマンティックでない HTML (よくあるのは <div> 要素) を用いて、JavaScript でインタラクティブにし、CSS で見た目を整えて、それぞれが独自の「ウィジェット」をつくっています。

これはうまい方針転換ですが、しかしこれは、ブラウザにすでにある機能を改良するかわりに、その機能のうえに何かを乗せているということです。別の言い方をすると、ブラウザはHTMLを表示するのに、少なくとも1秒間に60フレームを描画する、という大変な仕事をしています。その上に、ブラウザの知らないところで、私たちが独自のウィジェット機能を追加し、アニメーションさせ、表示を変更したとします。ブラウザのパフォーマンスが絶えずやりくりされているところに、私たち独自のコードを乗せるわけです。これは、インタフェースを遅くし、電池の消耗と、画面のチラつきをもたらします。

技術について

最初に、関係する技術の概要を紹介します。

Brick: 選り抜かれた Web Components

Mozilla Brick は、モジュールと再利用しやすい UI コンポーネントの組み合わせです. そのコンポーネントは、アダプティブで、レスポンシブなアプリケーションになるように設計されています。そのコンポーネントを選ぶことは、モバイルファーストという点でもとても良いことです。モバイルファーストの考え方であれば、どんな大きさのウェブベースのアプリケーションも開発できるようになります。この考え方とその設計パターンは、さまざまな大きさのデバイスに対応することができます。Brick コンポーネントはモバイルアプリだけのものではありません。それは最新のアプリのためのものです。

Brick はライブラリのようなものですが、厳密には、選り抜かれた Web components のコレクションと考えるべきものです。

Brick のコンポーネントのコレクションは、 <と>で囲まれたふつうの HTML の文で、特別なことのないふつうの要素と同じように CSS でスタイルづけすることができます。 Brick コンポーネントは、それに情報を受け渡すためのマイクロAPIを備えています。これらのコンポーネントは、いわば積み木です。あなたは「レゴ」はお好きですか? よろしい。それであれば、Brick のことも好きになることでしょう。

Brick Web Components は、X-Tag カスタム要素の polyfill を用いて、つくられています。

X-Tagとは?

X-Tag とは、ブラウザで、Web Components を動作するようにする、いくつかの(そしていずれはすべての)機能を polyfill するライブラリです。一般に、X-Tag はカスタム要素の生成物を polyfilling します。カスタム要素では、ふつうの HTML 文法と、要素ごとのAPIを用いて、DOM を拡張することができます。

Brick のコンポーネントを使うとき、X-Tag ライブラリを用いてつくられた Web Components を使うことになります。Brick は X-Tag の core ライブラリを含んでいます。そのため、もし Brick を含めているならば、あなた自身のカスタム要素をつくろうとするときにも、X-Tag をインクルードする必要はありません。X-Tag のすべての機能は、すでに使える状態にあります。

デモ

このデモプロジェクトファイルをダウンロードしてください。最初に使うのは、simple-app-with-bricks フォルダの中身です。

Bricks をアプリのなかで使う

<x-appbar>、<x-deck> と、<x-card> を用いて、シンプルなスケルトンのアプリをつくってみましょう。x-appbar は、アプリのための、きれいなヘッダーバーです。そして、 x-cards は、変化に応じてビューを切り替えてくれる x-deck の子要素にあたります。

最初に、骨子となる HTML 文書をつくるところから始め、それからアプリケーション固有のコード (以下の例にある app.css と app.js )とともに、Brick の CSS と JS を含めていきましょう。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <link rel="stylesheet" type="text/css" href="css/brick.min.css">
    <link rel="stylesheet" type="text/css" href="css/app.css">
    <title>Simple - Brick Demo</title>
  </head>
  <body>
    <!--- Some Brick components will go here -->
    <script type="text/javascript" src="js/brick.min.js"></script>
    <script type="text/javascript" src="js/app.js"></script>
  </body>
  </html>

ここに Brick の要素を追加していきます。

<x-appbar id="bar">
  <header>Simple Brick App</header>
  <button id="view-prev">Previous View</button>
  <button id="view-next">Next View</button>
</x-appbar>

最後に、x-appbar のあとに、x-cards を 子要素として持つ x-deck を追加していきましょう。x-cards には、好きなコンテンツを入れてかまいません。

<!-- Place your x-deck directly after your x-appbar -->
<x-deck id="views">
  <x-card>
    <h1>View 1</h1>
    <p>Hello, world!</p>
  </x-card>
  <x-card>
    <h1>Pick a Date</h1>
    <p>&lt;x-datepicker&gt;s are a polyfill for &lt;input type="date"&gt;</p>
    <x-datepicker></x-datepicker>
    <p>Just here to show you another tag in action!</p>
  </x-card>
  <x-card>
    <h1>A Random Cat To Spice Things Up</h1>
    <!-- Fetches from the Lorem Pixel placeholder content service -->
    <img src="http://lorempixel.com/300/300/cats">
  </x-card>
</x-deck>

すでに、シンプルなアプリケーションの構造が、ほとんどできあがりました。あと必要なのは、少しの CSS と JavaScript を結びつけることです。まず、JavaScriptは次のシンプルなものです:

document.addEventListener('DOMComponentsLoaded', function() {
  // Run any code here that depends on Brick components being loaded first
  // Very similar to jQuery's document.ready()

  // Grab the x-deck and the two buttons found in our x-appbar and assign to local vars
  var deck = document.getElementById("views"),
  nextButton = document.getElementById("view-next"),
  prevButton = document.getElementById("view-prev");

  // Add event listeners so that when we click the buttons, our views transition between one another
  prevButton.addEventListener("click", function(){ deck.previousCard(); });
  nextButton.addEventListener("click", function(){ deck.nextCard(); });
});

A mobile app showing a simple calendar.

仕上がりかけのアプリケーションのために、次のシンプルなCSSを足します:

html, body {
  margin: 0;
  padding: 0;
  font-family: sans-serif;
  height: 100%;
}

h1 {
  font-size: 100%;
}

x-deck > x-card {
  background: #eee;
  padding: 0.6em
}

ジャジャーン! 少ない行数の、ふつうのマークアップと、ちょっとした調整で、ひとつの HTML 文書のなかに、複数のビューをもつアプリのスケルトンが、つくれました。開発ツールでマークアップを確認すると、ふつうの HTML 要素に並んで、Brick のカスタム要素がうまく存在しているのがわかるでしょう。それらは、これまでのいわゆる HTML と同じように、開発ツールで確認し、あつかうことができます。

a screenshot of x-tags living alongside one another in the firefox dev tools

それでは、X-Tag を使ってカスタム要素をつくる方法を学んでいきましょう。

Bricks を自作する: X-Tag でカスタム要素をつくる

Let's say we have a mobile application in which the user takes an action that results in a blocking task. Maybe the application is waiting for an external service. The program's next instruction to complete the user-initiated task depends on the data from the server, so unfortunately we have to wait. For the sake of our purposes here, let's pretend we can't modify our program too much and assume an entrenched architecture — maybe we can't do much else other than communicate to the user until we find a way to deal with the blocking better. We have to do the best with what we have.

We will create a custom modal spinner that will tell the user they need to wait for a little while. It's important to give your users feedback on what's happening in your app when they don't get to complete their task in a timely manner: a frustrated or confused user might give up on using your app.

If you are following along with the example, you will want to switch to the x-status-hud folder inside of the demo materials now.

自作のカスタム要素を登録する

X-Tag relies on several different events to detect and upgrade elements to custom elements. X-Tag will work whether the element was present in the original source document, added by setting the innerHTML property, or created dynamically via document.createElement. You should take a look at the Helpers section of the X-Tag documentation as it covers various functions that will allow you to work with your custom elements just like vanilla ones.

The first thing that we need to do is register our custom element with X-Tag so that X-Tag knows what to do if and when it encounters our custom element. We do that by calling xtag.register:

xtag.register('x-status-hud', {
  // We will give our tag custom behavior here for our status indicating spinner
});

Important: All custom element names must contain a hyphen. Why is this? The idea here is that there are no standard HTML elements with a hyphen in them, so by keeping to this rule we can ensure that we don't trample existing namespaces and cause collisions. You do not have to prefix with x-: this is just a convention used for components created with X-Tag in the Brick ecosystem. Once upon a time in the early days of the W3C specification for custom elements, it was speculated that all custom elements would have an x- prefix; this restriction was relaxed in later versions of the specification, meaning that <bacon-eggs> and <adorable-kitten> are both perfectly valid names. Choose a name that describes what your element is or does.

If we wanted to, we could choose to set what HTML element is being used as our base element before upgrading. We can also set a specific prototype for our element if we want to involve functionality from a different element. You can declare these as follows:

xtag.register('x-superinput', {
  extends: 'input',
  prototype: Object.create(HTMLInputElement.prototype)
});

The element we are building doesn't require these properties to be set explicitly, but they are worth mentioning because they will be useful to you when you write more advanced components and want a specific level of control over them.

要素のライフサイクル

Custom elements have events that fire at certain times during their lifetime. Events are fired when an element is created, inserted into the DOM, removed from the DOM, and when attributes are set. You can take advantage of none or all of these events.

lifecycle:{
  created: function(){
    // fired once at the time a component
    // is initially created or parsed
  },
  inserted: function(){
    // fired each time a component
    // is inserted into the DOM
  },
  removed: function(){
    // fired each time an element
    // is removed from DOM
  },
  attributeChanged: function(){
    // fired when attributes are set
  }

Our element is going to use the created event. When this event fires, our code will add some child elements.

xtag.register('x-status-hud', {
  lifecycle: {
    created: function(){
        this.xtag.textEl = document.createElement('strong');

        this.xtag.spinnerContainer = document.createElement('div');
        this.xtag.spinner = document.createElement('div');

        this.xtag.spinnerContainer.className = 'spinner';

        this.xtag.spinnerContainer.appendChild(this.xtag.spinner);
        this.appendChild(this.xtag.spinnerContainer);
        this.appendChild(this.xtag.textEl);
    }
  }
  // More configuration of our element will follow here
});

カスタムメソッドを追加する

We need to have control over when we show or hide our status HUD. To do that, we need to add some methods to our component. A simple toggle() may suffice for some use cases, but let's throw in individual hide() and show() functions too:

xtag.register('x-status-hud', {
  lifecycle: {
    created: function(){
      this.xtag.textEl = document.createElement('strong');

      this.xtag.spinnerContainer = document.createElement('div');
      this.xtag.spinner = document.createElement('div');

      this.xtag.spinnerContainer.className = 'spinner';

      this.xtag.spinnerContainer.appendChild(this.xtag.spinner);
      this.appendChild(this.xtag.spinnerContainer);
      this.appendChild(this.xtag.textEl);
    }
  },

  methods: {
    toggle: function(){
      this.visible = this.visible ? false : true;
    },

    show: function (){
      this.visible = true;
    },

    hide: function (){
      this.visible = false;
    }
  }

カスタムアクセサを追加する

Properties on custom elements don't have to map to an attribute. This is by design because some setters could be very complex and not have a sensible attribute equivalent. If you would like an attribute and property to be linked, you have to pass in an empty object literal to the attribute. In the example below, this has been done for the label attribute:

xtag.register('x-status-hud', {
  lifecycle: {
    created: function(){
      this.xtag.textEl = document.createElement('strong');

      this.xtag.spinnerContainer = document.createElement('div');
      this.xtag.spinner = document.createElement('div');

      this.xtag.spinnerContainer.className = 'spinner';

      this.xtag.spinnerContainer.appendChild(this.xtag.spinner);
      this.appendChild(this.xtag.spinnerContainer);
      this.appendChild(this.xtag.textEl);
    }
  },

  methods: {
    toggle: function(){
      this.visible = this.visible ? false : true;
    },

    show: function (){
      this.visible = true;
    },

    hide: function (){
      this.visible = false;
    }
  },

  accessors: {
    visible: {
      attribute: { boolean: true }
    },

    label: {
      attribute: {},

      set: function(text) {
        this.xtag.textEl.innerHTML = text;
      }
    }
  }
}); // End tag declaration

If the difference between attributes and properties is unclear to you, Stack Overflow provides a good answer. Although the question being asked is about something else entirely (jQuery), the top answer has a great explanation that will help you understand the relationship between attributes and properties.

完成したコンポーネント

When we write code that depends on custom elements having been loaded already, we add an event listener that fires when the components have finished loading. This is sort of like jQuery's document.ready.

<script type="text/javascript">
document.addEventListener('DOMComponentsLoaded', function(){
  // Any HUD customizations should be done here.
  // We just pop up the HUD here to show you it works!
  var testHUD = document.getElementById("test");
  testHUD.label = "Please Wait...";
  testHUD.show();
}, false);
</script>

A custom HTML component showing a spinner widget with the message please wait

And there you have it. We've created a simple modular, reusable widget for our client-side code.

もっと改善してみよう

Our widget is a good starting point, but is it really finished? There are a number of ways in which we can improve this element:

  1. Have the element recalculate its size when the attributeChanged event is fired and have the component resize to fit the label as it is updated rather than truncate the label with an ellipsis.
  2. Let the developer set an image, such as an animated GIF, in place of the CSS spinner to customize the user experience further.
  3. Have a progress bar instead of a spinner to give the user some additional information about task progress.

Use your creativity to come up with a small set of practical features and improvements beyond these as an exercise on your own.

参考

ドキュメントのタグと貢献者

 このページの貢献者: storywriter
 最終更新者: storywriter,