レイアウトのライブラリ化

前の記事の続きですが、もう少し具体的にレイアウトを行うためのカスタムタグを作成し、ライブラリ化してみました。
将来ありそうなパターンとしてページの構成部品(全体のレイアウト、ページヘッダ、リンクリスト、フッタ、など)ごとにカスタムタグを作成しています。

具体例

まずは具体例を示します。
下のリンクをクリックしてください。

具体例を表示

本文のソースコードは次の様になります。

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>Page Sample</title>
	<link rel="import" href="layout1.html">
	<style>
		#HeaderTitle {
			padding: 3em 0;
			background-color: darkgray;
			text-align: center;
		}

		#HeaderTitle > span {
			font-size: 300%;
			font-weight: bolder;
		}
	</style>
</head>
<body>
	<page-frame>
		<page-header slot="header">
			<div id="HeaderTitle" slot="header-title"><span>ヘッダタイトル</span></div>
			<div id="HeaderLogo" slot="header-logo">
				ロゴ<br>
				です
			</div>
		</page-header>
		<page-menu slot="menu">
			<a slot="menu-list" href="#">menu1</a>
			<a slot="menu-list" href="#">menu2</a>
			<a slot="menu-list" href="#">menu3</a>
		</page-menu>
		<page-content slot="contents">
			<h1 slot="title">コンテンツ</h1>
			<div slot="body">
				<p>
					ページの内容・・・<br>
					・・・<br>
				</p>
				<page-content slot="contents">
					<h2 slot="title">サブコンテンツ1</h2>
					<div slot="body">
						サブコンテンツ1の内容・・・<br>
						・・・<br>
						・・・<br>
					</div>
				</page-content>
				<page-content slot="contents">
					<h2 slot="title">サブコンテンツ2</h2>
					<div slot="body">
						サブコンテンツ2の内容・・・<br>
						・・・<br>
						・・・<br>
					</div>
				</page-content>
			</div>
		</page-content>
		<page-footer slot="footer">
			<a slot="left" href="#">Top Page</a>
			<span slot="center">ページフッタ</span>
			<a slot="right" href="#">▲</a>
		</page-footer>
	</page-frame>
</body>
</html>

どうでしょうか。非常にシンプルだと思います。
レイアウトするために必要な<div>が全然ありません。

ちなみにテンプレートは次の通りです。

<!-- ページ全体 -->
<template id="page-frame">
	<div id="outer">
		<div id="area1">
			<slot name="header"></slot>
		</div>
		<div id="area2">
			<div id="area21">
				<slot name="menu"></slot>
			</div>
			<div id="area22">
				<slot name="contents"></slot>
			</div>
		</div>
		<div id="area3">
			<slot name="footer"></slot>
		</div>
	</div>
	<style>
	#outer {
		border: solid black 1px;
	}

	#area2 {
		display: table;
		table-layout: fixed;
		width: 100%;
		border-top: solid black 1px;
		border-bottom: solid black 1px;
	}

	#area21 {
		display: table-cell;
		width: 15em;
		border-right: solid black 1px;
		vertical-align: top;
	}

	#area22 {
		display: table-cell;
	}
	</style>
</template>

<!-- ヘッダ -->
<template id="page-header">
	<div id="outer">
		<div id="area1">
			<slot name="header-title"></slot>
		</div>
		<div id="area2">
			<slot name="header-logo"></slot>
		</div>
	</div>
	<style>
	#outer {
		position: relative;
	}

	#area2 {
		position: absolute;
		top:0;
		left:0;
	}
	</style>
</template>

<!-- メニュー -->
<template id="page-menu">
	<div id="outer">
		<slot name="menu-list"></slot>
	</div>
	<style>
	::slotted(a) {
		display: block;
		border-bottom: solid gray 1px;
		padding: 0.25em 0.5em;
		color: gray;
		text-decoration: none;
	}

	::slotted(a:hover){
		background-color: lightgray;
		color: black;
	}
	</style>
</template>

<!-- コンテンツ -->
<template id="page-content">
	<div id="outer">
		<slot name="title"></slot>
		<slot name="body" id="content-body"></slot>
	</div>
	<style>
		#outer {
			margin: 0.25em 0.5em;
		}

		#content-body {
			padding: 0.5em 1em;
		}
	</style>
</template>

<!-- フッタ -->
<template id="page-footer">
	<div id="outer">
		<div id="footer-left"><slot name="left"></slot></div>
		<div id="footer-center"><slot name="center"></slot></div>
		<div id="footer-right"><slot name="right"></slot></div>
	</div>
	<style>
	#outer {
		display: table;
		table-layout: fixed;
		width: 100%;
	}

	#footer-left {
		display: table-cell;
		width: 30%;
	}

	#footer-center {
		display: table-cell;
		text-align: center;
	}

	#footer-right {
		display: table-cell;
		text-align: right;
		width: 30%;
	}
	</style>
</template>

<script>
(function(){
	const doc = document.currentScript.ownerDocument;
	const tmpls = doc.getElementsByTagName("template");
	for(let i=0; i<tmpls.length; i++){
		const cls = class extends HTMLElement{
			constructor(){
				super();
			}

			connectedCallback(){
				const sr = this.attachShadow({mode: "open"});
				const tmpl = doc.getElementById(tmpls[i].id);
				const clone = document.importNode(tmpl.content, true);
				sr.appendChild(clone);
			}
		};
		customElements.define(tmpls[i].id, cls);
	}
})();
</script>

モノシリックにしてみる

先ほどの例はヘッダやメニュー、フッタをそれぞれのカスタムタグで定義していましたが、実際のWebページではこれらはほとんどのページで同じ構成になります。
ならば、それらを含めてモノシリックにした方が実用的なのではないかと考えてやってみたのが次です。
下のリンクをクリックすると具体例が表示されます(表示されるレイアウトは先ほどのと同じです)。

具体例を表示

本文のソースコードは次の様になります。

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>Page Sample</title>
	<link rel="import" href="layout2.html">
</head>
<body>
	<page-frame>
		<page-content slot="contents">
			<h1 slot="title">コンテンツ</h1>
			<div slot="body">
				<p>
					ページの内容・・・<br>
					・・・<br>
				</p>
				<page-content slot="contents">
					<h2 slot="title">サブコンテンツ1</h2>
					<div slot="body">
						サブコンテンツ1の内容・・・<br>
						・・・<br>
						・・・<br>
					</div>
				</page-content>
				<page-content slot="contents">
					<h2 slot="title">サブコンテンツ2</h2>
					<div slot="body">
						サブコンテンツ2の内容・・・<br>
						・・・<br>
						・・・<br>
					</div>
				</page-content>
			</div>
		</page-content>
	</page-frame>
</body>
</html>

もはやほとんど本文しか書かれていません。
しかし、HTMLとしてはこれがあるべき姿ではないかと思います。
ドキュメントにとって重要なのはそのコンテンツであってレイアウトではありません。この様なマークアップならレイアウトをどうするかということに煩わされることなくコンテンツに集中することができます。

テンプレートは次の通りです。

<!-- ページ全体 -->
<template id="page-frame">
	<div id="outer">
		<div id="area1">
			<div id="header-outer">
				<div id="HeaderTitle"><span>ヘッダタイトル</span></div>
				<div id="HeaderLogo">
					ロゴ<br>
					です
				</div>
			</div>
		</div>
		<div id="area2">
			<div id="area21">
				<a class="menu-list" href="#">menu1</a>
				<a class="menu-list" href="#">menu2</a>
				<a class="menu-list" href="#">menu3</a>
			</div>
			<div id="area22">
				<slot name="contents"></slot>
			</div>
		</div>
		<div id="area3">
			<div id="footer-outer">
				<div id="footer-left">
					<a href="#">Top Page</a>
				</div>
				<div id="footer-center">
					<span slot="center">ページフッタ</span>
				</div>
				<div id="footer-right">
					<a slot="right" href="#">▲</a>
				</div>
			</div>
		</div>
	</div>
	<style>
	/* 全体のレイアウト */
	#outer {
		border: solid black 1px;
	}

	#area2 {
		display: table;
		table-layout: fixed;
		width: 100%;
		border-top: solid black 1px;
		border-bottom: solid black 1px;
	}

	#area21 {
		display: table-cell;
		width: 15em;
		border-right: solid black 1px;
		vertical-align: top;
	}

	#area22 {
		display: table-cell;
	}

	/* ヘッダ */
	#header-outer {
		position: relative;
	}

	#HeaderTitle {
		padding: 3em 0;
		background-color: darkgray;
		text-align: center;
	}

	#HeaderTitle > span {
		font-size: 300%;
		font-weight: bolder;
	}

	#HeaderLogo {
		position: absolute;
		top: 0;
		left: 0;
	}

	/* メニュー */
	a.menu-list {
		display: block;
		border-bottom: solid gray 1px;
		padding: 0.25em 0.5em;
		color: gray;
		text-decoration: none;
	}

	a.menu-list:hover {
		background-color: lightgray;
		color: black;
	}

	/* フッタ */
	#footer-outer {
		display: table;
		table-layout: fixed;
		width: 100%;
	}

	#footer-left {
		display: table-cell;
		width: 30%;
	}

	#footer-center {
		display: table-cell;
		text-align: center;
	}

	#footer-right {
		display: table-cell;
		text-align: right;
		width: 30%;
	}
	</style>
</template>

<!-- コンテンツ -->
<template id="page-content">
	<div id="outer">
		<slot name="title"></slot>
		<slot name="body" id="content-body"></slot>
	</div>
	<style>
		#outer {
			margin: 0.25em 0.5em;
		}

		#content-body {
			padding: 0.5em 1em;
		}
	</style>
</template>

<script>
(function(){
	const doc = document.currentScript.ownerDocument;
	const tmpls = doc.getElementsByTagName("template");
	for(let i=0; i<tmpls.length; i++){
		const cls = class extends HTMLElement{
			constructor(){
				super();
			}

			connectedCallback(){
				const sr = this.attachShadow({mode: "open"});
				const tmpl = doc.getElementById(tmpls[i].id);
				const clone = document.importNode(tmpl.content, true);
				sr.appendChild(clone);
			}
		};
		customElements.define(tmpls[i].id, cls);
	}
})();
</script>

モノシリックにしてみる2

先ほどの例はレイアウト用のカスタムタグを書き直していましたが、最初の例で作成した部品を再利用してみます。
まぁ、見た目は同じです。

具体例を表示

本文のソースコードは次の様になります。
ページ全体を表すタグが<page-frame>から<page-frame-mono>に変わっただけです。

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>Page Sample</title>
	<link rel="import" href="layout-mono.html">
</head>
<body>
	<page-frame-mono>
		<page-content slot="contents">
			<h1 slot="title">コンテンツ</h1>
			<div slot="body">
				<p>
					ページの内容・・・<br>
					・・・<br>
				</p>
				<page-content slot="contents">
					<h2 slot="title">サブコンテンツ1</h2>
					<div slot="body">
						サブコンテンツ1の内容・・・<br>
						・・・<br>
						・・・<br>
					</div>
				</page-content>
				<page-content slot="contents">
					<h2 slot="title">サブコンテンツ2</h2>
					<div slot="body">
						サブコンテンツ2の内容・・・<br>
						・・・<br>
						・・・<br>
					</div>
				</page-content>
			</div>
		</page-content>
	</page-frame-mono>
</body>
</html>

カスタムタグのHTMLファイルは次の様になります。
内部で<page-frame>を使っているのでモノシリックにした時に同じ名前を使うことができません。なので、<page-frame-mono>という名前にしています。

<link rel="import" href="layout1.html"><!-- templateタグ内はダメ -->
<template id="page-frame-mono">
	<page-frame>
		<page-header slot="header">
			<div id="HeaderTitle" slot="header-title"><span>ヘッダタイトル</span></div>
			<div id="HeaderLogo" slot="header-logo">
				ロゴ<br>
				です
			</div>
		</page-header>
		<page-menu slot="menu">
			<a slot="menu-list" href="#">menu1</a>
			<a slot="menu-list" href="#">menu2</a>
			<a slot="menu-list" href="#">menu3</a>
		</page-menu>
		<!--
			contents部分はpage-frameにもあるが、直接page-frameのslotに内容を反映することはできない。
			なのでpage-frameを使用する側にもslotを用意する必要がある
		-->
		<div slot="contents">
			<slot name="contents"></slot>
		</div>
		<page-footer slot="footer">
			<a slot="left" href="#">Top Page</a>
			<span slot="center">ページフッタ</span>
			<a slot="right" href="#">▲</a>
		</page-footer>
	</page-frame>
	<style>
		#HeaderTitle {
			padding: 3em 0;
			background-color: darkgray;
			text-align: center;
		}

		#HeaderTitle > span {
			font-size: 300%;
			font-weight: bolder;
		}
	</style>
</template>

<script>
(function(){
	const doc = document.currentScript.ownerDocument;
	const tmpls = doc.getElementsByTagName("template");
	for(let i=0; i<tmpls.length; i++){
		const cls = class extends HTMLElement{
			constructor(){
				super();
			}

			connectedCallback(){
				const sr = this.attachShadow({mode: "open"});
				const tmpl = doc.getElementById(tmpls[i].id);
				const clone = document.importNode(tmpl.content, true);
				sr.appendChild(clone);
			}
		};
		customElements.define(tmpls[i].id, cls);
	}
})();
</script>

この様に別のカスタムタグを再利用する場合、2つほど注意点があります。

一つは、<link ref="import" href="〜〜〜">を書く場所です。
<template>内に書くことはできません。
なのでサンプルコードの様にファイルの先頭に書くことになります。

もう一つは親になるテンプレートで作った<slot>をそのまま使うことができないということです。
この例ではページの本文部分は固定では無いので<div id="area21">の中は<slot>にする必要があります。<page-frame>のこの部分は元々<slot>があるのですが、再利用している方でも<slot>を記述しています(20行目)。
これが無いと本文で<page-frame-mono>の子要素として記述した内容が反映されません。

これは<template>がオブジェクト指向で言う所の継承ではなく、包含の関係になっているためです。
(これを書いていて気づいたのですが多分継承でも作れます。この例でいうと<page-frame-mono>をclass PageFrameMono extends customElement.get("page-frame") {}みたいにしてゴリゴリやるとできそうです。多分メリットは無いです)

まとめ

レイアウトをテンプレート化することで本文のマークアップが非常にシンプルになります。
将来的には次の様になるのではないかと思っています。

  1. ページの部品ごとにいくつかのデザインのカスタムタグが用意されたライブラリが公開される様なる
  2. それを使ってモノシリックなページレイアウトを作成する
  3. モノシリックなページレイアウトのカスタムタグを使ってページを作成する

この様になればページのレイアウトとコンテンツは非常に疎結合になり、分業しやすくなります。
また、カスタムタグの名前や属性に変更がなければテンプレートを差し替えるだけでレイアウト変更可能です(本文中の図の色合いなどの調整は残るでしょうが)。
これは私にはHTMLのマークアップにとって理想的な姿に思えます。