この記事は最終更新日から1年以上が経過しています。
@programming
投稿日 2019/6/11
更新日 2022/4/27 ✏
作って覚えるカスタム要素
Custom Elements (カスタム要素) を理解する一番の近道は、実際に何かテキトーなカスタム要素を一つ作ってみることではないかと思います。そこで今回は、TODOを管理できる簡単なカスタム要素を実際に作ってみたいと思います。
- 更新 2022/04/27 古いブラウザにも対応させるpolyfillを追加。
目次
TODOカスタム要素クラス
下記のtodo-element.js
が今回作成したTODOカスタム要素の本体です。中身はHTMLElement
を継承したTodoElement
というクラスです:
todo-element.js
/**
* TODO management custom element
* @version 0.1.0
* @author 芽萌丸プログラミング部 {@link https://memo.appri.me/programming/}
*/
class TodoElement extends HTMLElement {
/** 変更監視対象属性一覧 */
static get observedAttributes() {
return ['list', 'debug'];
}
/** コンストラクタ */
constructor() {
super();
const self = this;
this.debug = self.getAttribute("debug") === "true" ? true : false;
if (self.debug) console.log("initializing...");
//
// カスタム要素内要素を定義
//
this.shadow = this.attachShadow({ mode: 'open' });
// スタイル要素
this.styleEl = document.createElement('style');
this.styleEl.textContent = `
.remove {
color: red;
padding: 4px 12px;
background: transparent;
border: 0px solid transparent;
cursor: pointer;
}
`;
this.shadow.appendChild(this.styleEl);
// アイテム入力ボックス要素
this.iptEl = document.createElement("input");
this.iptEl.onkeyup = function(e) {
const item = (self.iptEl.value) ? self.iptEl.value.trim() : null;
if (!item) {
self.btnAddEl.setAttribute("disabled", "true");
return;
} else {
self.btnAddEl.removeAttribute("disabled");
}
if (e.keyCode === 13) {
// ** Enter:
self.btnAddEl.click(e);
}
};
this.shadow.appendChild(this.iptEl);
// アイテム追加ボタン要素
this.btnAddEl = document.createElement("button");
this.btnAddEl.textContent = "+";
this.btnAddEl.setAttribute("disabled", "true");
this.btnAddEl.onclick = function() {
const item = (self.iptEl.value) ? self.iptEl.value.trim() : null;
if (!item) return;
const midx = self.getItems().findIndex(x => x === item);
if (midx >= 0) {
// 既に登録されているものは一旦削除してから追加しなおす
self.removeItem(self.ulEl.querySelectorAll("li")[midx]);
}
self.addItem(item);
};
this.shadow.appendChild(this.btnAddEl);
// アイテムリスト要素
this.ulEl = document.createElement("ul");
this.shadow.appendChild(this.ulEl);
}
connectedCallback() {
const self = this;
if (self.debug) console.log(`${self.constructor.name} element added to page.`);
}
disconnectedCallback() {
const self = this;
if (self.debug) console.log(`${self.constructor.name} element removed from page.`);
}
adoptedCallback(oldDoc, newDoc) {
const self = this;
if (self.debug) console.log(`${self.constructor.name} element moved to new page.`);
}
attributeChangedCallback(name, oldValue, newValue) {
const self = this;
if (self.debug) console.log(`${self.constructor.name} element attributes changed: "${name}" ${newValue}`);
if (name === "list") {
try {
self.addItems(JSON.parse(newValue));
} catch (ex) {
if (self.debug) console.error("'list' attribute is an invalid JSON format.", ex);
}
}
}
//
// my functions
//
getItems() {
const self = this;
const arr = [];
const els = self.ulEl.querySelectorAll("li");
for (let len = els.length, i = 0; i < len; i++) {
arr.push(els[i].childNodes[0].textContent); // text node only
}
return arr;
}
addItem(item) {
const self = this;
const liEl = document.createElement("li");
liEl.textContent = item;
const btnRemoveEl = document.createElement("button");
btnRemoveEl.textContent = "Delete";
btnRemoveEl.setAttribute("class", "remove");
btnRemoveEl.onclick = function(e) {
self.removeItem(e.currentTarget.parentElement);
};
liEl.appendChild(btnRemoveEl);
self.ulEl.prepend(liEl);
}
addItems(items) {
const self = this;
for (let len = items.length, i = 0; i < len; i++) {
self.addItem(items[i]);
}
}
removeItem(item) {
item.parentNode.removeChild(item);
}
dumpItems() {
const self = this;
return JSON.stringify(self.getItems());
}
}
// NOTE: module.exports に参照を渡しておいて browserify にも対応させておきます:
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined')
module.exports = TodoElement;
else {
window.TodoElement = TodoElement;
}
// TODOカスタム要素を "todo-element" として登録します:
if (typeof customElements !== "undefined") {
customElements.define('todo-element', TodoElement);
}
コードレビュー
作成手順
先ほどのカスタム要素の作成手順です。基本的には下記の手順でコーディングしていくだけで大抵のカスタム要素は作れるのではないでしょうか:
class TodoElement extends HTMLElement {...}
何らかのHTML要素クラスを継承してカスタム要素クラスを作成static get observedAttributes() { return ['list', 'debug']; }
値の変更を監視したい属性があればそれらの属性名を指定- コンストラクタ内で
shadow = this.attachShadow({ mode: 'open' });
で shadow DOM を取得し、そこにタグ内部で使いたい子要素を追加 - 必要なら下記のハンドラーメソッドを実装:
connectedCallback()
要素がドキュメントに挿入されたときに呼び出されますdisconnectedCallback()
要素がドキュメントから削除されたときに呼び出されますattributeChangedCallback(attr, oldVal, newVal)
要素の属性が変更、追加、削除、または置換されたときに呼び出されます (監視対象の属性に対してのみ)adoptedCallback(oldDoc, newDoc)
要素が新しい文書に採用されたときに呼び出されます
- 最後に
customElements.define('todo-element', TodoElement);
でカスタム要素を登録します - おまけ:browserifyにも対応させるために下記のコードも追加しておきました。コレは個人的な好みです:
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined')
module.exports = TodoElement;
else
window.TodoElement = TodoElement;
属性
今回のTODOカスタム要素には下記の2つの属性を持たせました:
list
属性: デフォルトのTODO一覧を設定debug
属性:true
をセットすることでコンソールにデバッグログを出力
どちらもstatic get observedAttributes() {...}
で値変更の監視対象に指定してあります。外部から属性値がセットされるとattributeChangedCallback()
が発火します。
公開メソッド
dumpItems()
などのメソッドは外部から参照できます。そのため<button onclick="alert(document.querySelector('todo-element').dumpItems());">Dump Items</button>
のように外部から呼び出すことができます。
使い方
...
<div>TODOカスタム要素のテスト</div>
<todo-element list='["aaa","bbb","ccc"]' debug="true"></todo-element>
<button onclick="alert(document.querySelector('todo-element').dumpItems());">Dump Items</button>
<script src="./todo-element.js"></script>
...
TIP: もし古いブラウザでも動かしたい場合はこちらのpolyfillをロードしてください:
<!-- TIP: 古いブラウザでも動かしたい時は[こちらのpolyfill]をロードします: -->
<!-- see: https://github.com/webcomponents/polyfills/tree/master/packages/webcomponentsjs#how-to-use -->
<script src="https://unpkg.com/@webcomponents/webcomponentsjs@2.6.0/custom-elements-es5-adapter.js"></script>
<script src="https://unpkg.com/@webcomponents/webcomponentsjs@2.6.0/webcomponents-loader.js"></script>
上記のコードはこんな感じで動きます:
TODOカスタム要素のテスト
以上となります。