Web Componentsでmarqueeを作る
Web Componentsという仕様を知っていますか?
独自のHTML要素に近いものをJavaScriptで作ってみます.
ちなみにWeb Componentsの説明についてはMDN Web Docsにて「再利用可能なカスタム要素を作成し、その機能を他のコードから分離してウェブアプリケーションで利用できるようにします。」と書かれています.
これらの仕様と機能を使って、<marquee>要素を実装します.
<marquee>要素については以下のMDN Web Docsを参照してください.
今回作るWeb Componentsについて
今回は<marquee>
要素を作りたいのですが、Web Componentsの仕様上、要素名には-を含む必要があるため<marquee-x>
という名前で要素を作ります.
以下のようなイメージで使える要素を目指していきます.
<marquee-x speed="5"> Hello </marquee-x>
Web ComponentsはHTMLElement
を継承したClassをwindow.customElements.define('marquee-x', MarqueeX);
の記述で要素を有効化します.
class MarqueeX extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
}
}
window.customElements.define('marquee-x', MarqueeX);
タグで囲まれた要素を表示したい
タグで囲まれた要素を表示するにあたって<slot>
を利用します.
<slot>の詳細な仕様の説明は避けますが、カスタムコンポーネントで囲んだ要素をWeb Componentsの中で表示できるようになります.
<slot>要素内に<span>タグを置くことで今回はアニメーションやスタイルの適用をやりやすくします.
const slotElement = document.createElement('slot');
const wrapperElement = document.createElement('span');
wrapperElement.innerHTML = slotElement.outerHTML;
スタイルを設定したい
<slot>と対になる<template>タグを利用し<template>内に、Web Components内で使いたいHTMLタグを記述します. styleの適用先として:slotted
の導入を検討しましたが、要素内にrawTextしかない場合にスタイルの適用がされないため, <span>タグに対してテキストを当てます.
const templateElement = document.createElement('template');
templateElement.innerHTML = `
<style>
:host {
white-space: nowrap;
margin: 0 calc(50% - 50vw);
width: 100vw;
position: relative;
}
::slotted(*) {
position: absolute;
left: 100vw;
position: relative;
display: inline-block;
}
</style>
`;
```javascript: うまく動く.js
const templateElement = document.createElement('template');
templateElement.innerHTML = `
<style>
:host {
white-space: nowrap;
margin: 0 calc(50% - 50vw);
width: 100vw;
position: relative;
}
span {
position: absolute;
left: 100vw;
}
span::slotted(*) {
position: relative;
display: inline-block;
}
</style>
`;
それぞれ<slot>と<template>の詳しい仕様や解説はMDN Web Docsに説明を譲ります.
JavaScriptで動かす
<slot>で呼び出された要素を<slot>で囲み、<span>タグ経由でJavaScriptのanimate()
を呼び出します.
wrapperElement.id = 'animate-target';
...
connectedCallback() {
const animateTarget = this.shadowRoot?.getElementById('animate-target');
if (animateTarget) {
const keyframes = [{ left: '100vw' }, { left: '0' }];
const options = {
duration: window.innerWidth ? (window.innerWidth / this.speed) * 250 : 10000,
iterations: Infinity
};
animateTarget.animate(keyframes, options);
}
}
最終的なWeb Components
厳密に仕様を再現しているわけでも、レスポンシブ対応しているわけでもないですが、一旦marqueeっぽく動くオレオレ実装での独自要素ができました.
最終的なソースコードは以下の通りです.
<script>
const templateElement = document.createElement('template');
templateElement.innerHTML = `
<style>
:host {
white-space: nowrap;
margin: 0 calc(50% - 50vw);
width: 100vw;
position: relative;
}
span {
position: absolute;
left: 100vw;
}
span::slotted(*) {
position: relative;
display: inline-block;
}
</style>
`;
const slotElement = document.createElement('slot');
const wrapperElement = document.createElement('span');
wrapperElement.innerHTML = slotElement.outerHTML;
wrapperElement.id = 'animate-target';
class MarqueeX extends HTMLElement {
speed: number;
constructor() {
super();
this.attachShadow({ mode: 'open' });
if (this.shadowRoot) {
this.shadowRoot.appendChild(templateElement.content.cloneNode(true));
this.shadowRoot.appendChild(wrapperElement);
}
const speedAttribute = this.getAttribute('speed');
this.speed = speedAttribute ? parseInt(speedAttribute) : 5;
}
connectedCallback() {
const animateTarget = this.shadowRoot?.getElementById('animate-target');
if (animateTarget) {
const keyframes = [{ left: '100vw' }, { left: '0' }];
const options = {
duration: window.innerWidth ? (window.innerWidth / this.speed) * 250 : 10000,
iterations: Infinity
};
animateTarget.animate(keyframes, options);
}
}
}
window.customElements.define('marquee-x', MarqueeX);
</script>
<marquee-x speed="5"> Hello </marquee-x>
動いている様子
デモ環境
例に漏れず環境はSvelte-Kit上ですが、生のJavaScriptが動いています.
Discussion