SPA (Single Page Application)
PolymerでMarkdown形式のテキストを整形して表示する簡単なSPAを作ってみます。
サーバサイドの実行は
Google Apps Script で、テキストの格納先として
Googleスプレッドシート を使用します。
※ Polymer サンプルコード (2) FizzBuzz の続きです。
- 動作確認
- Windows 10 : ○ Chrome60 Firefox54, △ Edge40, × IE11
- Mac 10.3 : ○ Chrome60 Safari10.1
- Android 7.1 : △ Chrome60
画面イメージ
デモサイト (AppsScript)
データ (Spreadsheet)
サンプルコード
Google Driveでスプレッドシートを作成し、「ツール」→「スクリプトエディタ」からApps Scriptを編集、上部メニューの公開からウェブ アプリケーションとして導入を選択して実行します。
-
コード.gsAppsScriptの本体
/**
* @OnlyCurrentDoc
*/
function doGet(e) {
return HtmlService.createTemplateFromFile('index').evaluate()
.setTitle('Polymer Simple CMS')
.addMetaTag('viewport', 'width=device-width, initial-scale=1')
}
function getPages(){
return SpreadsheetApp
.getActive()
.getSheetByName("シート1")
.getRange("A2:B20")
.getValues()
.filter(function(c){ return c[0] })
.map(function(c){ return { key:c[0], title:c[1]} })
}
function getContents(key) {
return SpreadsheetApp
.getActive()
.getSheetByName("シート1")
.getRange("A2:C20")
.getValues()
.filter(function(c){ return key == c[0] })
.pop()[2]
}
-
index.html<my-view>タグの呼出し元
<base href="https://polygit.org/components/" target="_top">
<script src="webcomponentsjs/webcomponents-loader.js"></script>
<?!= HtmlService.createHtmlOutputFromFile('my-view').getContent() ?>
<my-view></my-view>
-
my-view.html<my-view>タグを表示するHTML
<link rel="import" href="polymer/polymer-element.html">
<link rel="import" href="polymer/lib/elements/dom-repeat.html">
<link rel="import" href="app-layout/app-layout.html">
<link rel="import" href="iron-icons/iron-icons.html">
<link rel="import" href="paper-icon-button/paper-icon-button.html">
<link rel="import" href="paper-listbox/paper-listbox.html">
<link rel="import" href="paper-item/paper-item.html">
<link rel="import" href="marked-element/marked-element.html">
<dom-module id="my-view">
<template strip-whitespace>
<style>
:host {
display: block;
--app-primary-color: #29549a;
}
app-header {
background-color: var(--app-primary-color);
color: white;
}
app-header paper-icon-button {
--paper-icon-button-ink-color: white;
}
div[main-title] { margin-left: 10px; }
marked-element { margin-left: 15px; }
paper-item:not(.iron-selected) { cursor: pointer; }
</style>
<app-drawer-layout force-narrow fullbleed>
<app-drawer slot="drawer" swipe-open>
<app-header fixed>
<app-toolbar>
<paper-icon-button icon="close" drawer-toggle></paper-icon-button>
</app-toolbar>
</app-header>
<paper-listbox selected="{{key}}" attr-for-selected="key" selected-attribute="drawer-toggle">
<template is="dom-repeat" items="[[_pages]]">
<paper-item key="[[item.key]]">[[item.title]]</paper-item>
</template>
</paper-listbox>
</app-drawer>
<app-header-layout has-scrolling-region>
<app-header slot="header" reveals>
<app-toolbar>
<paper-icon-button icon="menu" drawer-toggle></paper-icon-button>
<div main-title>My app</div>
<paper-icon-button icon="delete"></paper-icon-button>
<paper-icon-button icon="search"></paper-icon-button>
<paper-icon-button icon="close"></paper-icon-button>
</app-toolbar>
</app-header>
<marked-element markdown="[[contents]]">
<div slot="markdown-html"></div>
</marked-element>
</app-header-layout>
</app-drawer-layout>
</template>
<script>
window.addEventListener('WebComponentsReady', ()=>{
class MyView extends Polymer.Element {
static get is() { return 'my-view' }
static get properties() {
return {
key: {
type: String,
value: "view1",
observer: '_keyChanged'
}
}
}
ready() {
super.ready()
google.script.run.withSuccessHandler(c=>this._pages=c).getPages()
}
_keyChanged(key) {
if (key) google.script.run.withSuccessHandler(c=>this.contents=c).getContents(key)
}
}
customElements.define(MyView.is, MyView)
})
</script>
</dom-module>
ページ表示の流れ
- メニューの
<paper-item key="view2">Page 2をクリックすると{{key}}にview2がセットされます。 -
{{key}}が更新されると_keyChanged(key)が呼ばれます。 -
コード.gsで定義したサーバサイドの関数getContents(key)が呼ばれ、view2に対応したテキストがAppsScript経由で取得されます。 - テキストを
withSuccessHandlerで受け取り、{{contents}}にセットします。 -
{{contents}}が<marked-element>によって整形されて表示されます。
各行の説明
コード.gs
/**
* @OnlyCurrentDoc
*/
@OnlyCurrentDocは、Polymerの動作とあまり関係ありませんが、AppsScriptでスプレッドシートへの権限を限定させるアノテーションです。
function doGet(e) {
return HtmlService.createTemplateFromFile('index').evaluate()
.setTitle('Polymer Simple CMS')
.addMetaTag('viewport', 'width=device-width, initial-scale=1')
}
Apps ScriptではdoGet()という決められた名前で関数を定義し、テンプレートファイル(index.html)を指定すると、その内容をブラウザで表示できます。
ただ、表示されるHTMLではタグが制限されているので、.setTitle()や.addMetaTag()でタイトルや<meta>タグを設定します。
function getPages(){
return SpreadsheetApp
.getActive()
.getSheetByName("シート1")
.getRange("A2:B20")
.getValues()
.filter(function(c){ return c[0] })
.map(function(c){ return { key:c[0], title:c[1]} })
}
function getContents(key) {
return SpreadsheetApp
.getActive()
.getSheetByName("シート1")
.getRange("A2:C20")
.getValues()
.filter(function(c){ return key == c[0] })
.pop()[2]
}
Polymerから呼び出す用の、スプレッドシートからページキーの一覧とキー文字列で内容を取得する関数を定義しています。
index.html
<base href="https://polygit.org/components/" target="_top">
<script src="webcomponentsjs/webcomponents-loader.js"></script>
ライブラリの読み込みを簡潔にする為、<base>タグで集約し、Web Componentsのポリフィルを読み込みます。
また、Apps Scriptで外部へのリンクを貼るには<base>タグにtarget="_top"を設定しておきます。
<?!= HtmlService.createHtmlOutputFromFile('my-view').getContent() ?>
<my-view></my-view>
<my-view>タグを定義したmy-view.htmlを読み込み、表示します。HTML Importsを使えればですが、AppsScriptでは外部ファイルを呼出しにくいので、テンプレートの内容を書き出すようにします(<?!= ... ?>の!(感嘆符)を入れる必要があるので注意です)。
my-view.html
<link rel="import" href="polymer/polymer-element.html">
<link rel="import" href="polymer/lib/elements/dom-repeat.html">
まずタグ(カスタム要素)を定義するのにpolymer/polymer-element.htmlを読み込みます(前回説明)。
ここで<dom-module>という定義用のタグと<dom-repeat>という繰り返し処理のタグが使えるようになります。
<link rel="import" href="app-layout/app-layout.html">
Webアプリケーション用のレイアウトライブラリであるApp Layoutを読み込みます。
<app-drawer-layout>と<app-drawer>で左メニュー、<app-header-layout>、<app-header>、<app-toolbar>で上部ツールバーを表示します。
上記は省略して書いていますが、ちゃんと書く場合は余計なタグを読み込まれないように個別指定します。
<link rel="import" href="app-layout/app-header-layout/app-header-layout.html"> <link rel="import" href="app-layout/app-drawer-layout/app-drawer-layout.html"> <link rel="import" href="app-layout/app-header/app-header.html"> <link rel="import" href="app-layout/app-toolbar/app-toolbar.html"> <link rel="import" href="app-layout/app-drawer/app-drawer.html">
<link rel="import" href="iron-icons/iron-icons.html">
<link rel="import" href="paper-icon-button/paper-icon-button.html">
<link rel="import" href="paper-listbox/paper-listbox.html">
<link rel="import" href="paper-item/paper-item.html">
iron-iconsはアイコンセットで一覧はこちらで確認できます。他、<paper-icon-button>でボタン、<paper-listbox>、<paper-item>でメニューリストを使用します。
<link rel="import" href="marked-element/marked-element.html">
Markdownで書かれたテキストを変換するのにJSライブラリのmarkdをラップしたmarked-elementを使用します。
<dom-module id="my-view">
<template strip-whitespace>
<style>
...
</style>
...
</template>
<script>
window.addEventListener('WebComponentsReady', ()=>{
class MyView extends Polymer.Element {
static get is() {
return 'my-view'
}
...
}
customElements.define(MyView.is, MyView)
})
</script>
</dom-module>
<my-view>タグを定義します。<style>タグは<template>タグの中に入れて使用します。
window.addEventListener('WebComponentsReady', ()=>{})はHTML Importを使わずChrome以外のブラウザに対応させる為に必要となります。
:host {
display: block;
--app-primary-color: #29549a;
}
app-header {
background-color: var(--app-primary-color);
color: white;
}
app-header paper-icon-button {
--paper-icon-button-ink-color: white;
}
div[main-title] { margin-left: 10px; }
marked-element { margin-left: 15px; }
paper-item:not(.iron-selected) { cursor: pointer; }
CSSのセレクタで:hostを指定すると、コンポーネント自体(my-viewタグ)へのスタイル指定となります(例えば :host { display: none; } でmy-viewタグが表示されなくなります)。
--app-primary-colorはカスタムプロパティという機能で、 CSSで変数として使えます。
<paper-icon-button>のアイコンの色を変更する--paper-icon-button-ink-colorなど、すでに定義されているものは各タグの仕様やソースをみながら利用します。
divタグやmarked-elementタグのセレクタ指定を 雑に 簡潔にしていますが、適切にコンポーネント化しておけば簡潔な指定が許容されるのはPolymerのよいところだと思います。
<app-drawer-layout force-narrow fullbleed>
<app-drawer slot="drawer" swipe-open>
<app-header fixed>
<app-toolbar>
<paper-icon-button icon="close" drawer-toggle></paper-icon-button>
</app-toolbar>
</app-header>
...
</app-drawer>
</app-drawer-layout>
左メニュー(app-drawer)を使用するには<app-drawer-layout>タグで全体を囲み、メニュー部分は<app-drawer>で囲みます。
<app-drawer slot="drawer">のslotは<app-drawer-layout>でdrawerという名前で定義された場所(slot)に入れる為に必要な指定です。
drawer-toggle属性があるタグはクリックでメニューの開閉となります。
<paper-listbox selected="{{key}}" attr-for-selected="key" selected-attribute="drawer-toggle">
<template is="dom-repeat" items="[[_pages]]">
<paper-item key="[[item.key]]">[[item.title]]</paper-item>
</template>
</paper-listbox>
左メニューの項目には<paper-listbox>を使用し、attr-for-selected="key"でクリックされた<paper-item>のkeyのデータがselected="{{key}}"にバインドされます。
selected-attribute="drawer-toggle"はメニューの項目の選択時に、<paper-item>へdrawer-toggle属性が付与され、メニューが閉じられます。
dom-repeatでは_pagesに格納されたページ一覧の情報をページ数分繰り返し処理をして表示しています。
例えば スプレッドシードのA列(キー)に
view4を入れて内容を登録すると<paper-item key="view4">新しいコンテンツ</paper-item>`が追加されて新しいページを表示することができます。
<app-header-layout has-scrolling-region>
<app-header slot="header" reveals>
<app-toolbar>
<paper-icon-button icon="menu" drawer-toggle></paper-icon-button>
<div main-title>My app</div>
<paper-icon-button icon="delete"></paper-icon-button>
<paper-icon-button icon="search"></paper-icon-button>
<paper-icon-button icon="close"></paper-icon-button>
</app-toolbar>
</app-header>
<marked-element markdown="[[contents]]">
<div slot="markdown-html"></div>
</marked-element>
</app-header-layout>
ツールバーをつくる為に、<app-header-layout>タグで全体を囲み、<app-toolbar>を<app-header>で囲みます(こちらもslot="header"が必要)。
delete、search、closeアイコンは飾りです。
<marked-element>でcontents変数に入ったMarkdownのテキストが直ちに整形されます。
static get properties() {
return {
key: {
type: String,
value: "view1",
observer: '_keyChanged'
}
}
}
ready() {
super.ready()
google.script.run.withSuccessHandler(c=>this._pages=c).getPages()
}
_keyChanged(key) {
if (key) google.script.run.withSuccessHandler(c=>this.contents=c).getContents(key)
}
<my-view>タグの属性を定義します。属性にvalueを指定すると初期値となり、observerを定義するとその値が更新されるたびに指定したメソッド_keyChanged(key)が呼び出されます。
_keyChanged(key)では google.script.run を使用してコード.gsで作成した関数 getContents(key) を呼び出し、取得したテキストをwithSuccessHandler()経由で{{contents}}にセットします。
ready()関数はカスタム要素の初期処理で使えるコールバック関数で、super.ready() を呼び出す必要があります。
最後に
コメント、編集リクエスト歓迎
Polymer サンプルコード (4) PWA [1] に続きます。
以上


