はじめに
前書き
- 本記事は FileMaker Advent Calendar 2021 24 日目の記事です。
対象読者
- Claris FileMaker Pro 19 ユーザ
- FileMaker の WEB ビューアにおいて Web Components を触ってみたいという(奇特な)方
- Lit を CDN で触ってみることに興味のある方
- Lit に限らず FileMaker Pro における JavaScript コンポーネント管理について考えてみたい方
検証環境
- Windows 10 Pro
- FileMaker Pro 19.4.1
- lit-element 3.0.2
- インターネット接続環境
Web Components とは?
Web Components は、再利用可能なカスタム要素を作成し、ウェブアプリの中で利用するための、一連のテクノロジーです。コードの他の部分から独立した、カプセル化された機能を使って実現します。
- これだけだとナンジャイって話だと思いますが、手っ取り早く言うと
ブラウザにネイティブでサポートされているリッチな UI 実装のための技術
です。超大雑把ですが、間違っていない - 今は WEB の UI 構築というと React, Vue, Angular, Svelte その他 JavaScript フレームワークで書くぞーというのが主流ですが、そのうち Web Components や Flutter Web が来るかもしれない(し、来ないかもしれない)
- 2021/12/24 時点だと、この記事が良い感じにまとまっていると思います
Lit とは?
- Web Components をそのまま使おうとするとツラミ感が高くなるのでライブラリを使うのが一般的
- 一番定番だったような気がする Polymer の後継となる Web Components 用のライブラリが Lit です
- Polymer から Lit へ、そして Lit 1.x 系から Lit 2.0 へ、ということについては、以下の記事がよくまとまっていると思います
- TypeScript で書くのが本道ですが、今回はトランスパイルなどできない FileMaker Pro の WEB ビューア環境により CDN を利用するため JavaScript で書くことになります
この記事では何をする?
- FileMaker Pro の WEB ビューアで Lit を用いて以下の機能を実装してみます
- ToDo 管理
- 現在の時分秒のリアルタイムなカウント表示
- マウスカーソルの位置取得
準備
FileMaker ファイル作成
- ファイル名は何でも構いませんが、ここでは
lit.fmp12
という名前で作成
FileMaker テーブルとフィールドの作成と定義
outputs
- メインとなるテーブル
- 計算フィールドの中身は後で解説予定
js
- 各 JavaScript コンポーネントを格納しておくためのテーブル
includes
- 要は outputs と js の二つを繋ぐための中間テーブル
- outputs テーブルから js テーブルの中身を手軽に覗くために計算フィールドを作っておく
- もちろんテーブルオカレンスの定義をした後でないと計算フィールドは作れない
todos
- ToDo の中身を保管しておくためのテーブル
FileMaker テーブルオカレンスの定義
全体像
outputs ⇔ includes
- レコード作成許可にチェックを入れておく
outputs ⇔ todos
- こちらも同じくレコード作成許可にチェック
includes ⇔ js
- こちらは includes テーブルに計算フィールドを作るために
FileMaker レイアウトの作成
outputs
- メインとなるレイアウト
- 完成形を貼っておきますが、WEB ビューアの中身やスクリプトトリガなどは後で解説します
js
- 最低限のコードを弄るために code フィールドは大きくしておく
includes, todos
- 基本的には覗くこともほぼないので、最低限の一行レイアウトに
FileMaker スクリプトの作成
set_global_lit_element_js_path
-
$$lit_element_js_path
というグローバル変数の定義
"https://unpkg.com/lit-element/lit-element.js?module"
open
- OnLayoutEnter スクリプトトリガで実行させる用として
reload
- ウインドウ内容の再表示
outputs レイアウトへのスクリプトトリガ設定
- 先ほど作っておいた
open
スクリプトを OnLayoutEnter スクリプトトリガで設定
値一覧の作成
- 後で必要になるので、以下の通りに作成しておく
さらなる準備
情報元
- あらためて今回は公式サイトより、チュートリアルにおける ToDo リスト作成、時計表示、マウスカーソルの位置取得をベースとして少し改変したものを実装してみましょう
g_for_webview フィールド
- 全レコードに共通となる html コードとして、以下の通り入力しておきます
-
[[[js]]]
や[[[components]]]
など、他では使われ得ない記述の仕方をしておいて、後で WEB ビューア側で Substitute 関数により置換するようにします
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script type="module">
[[[js]]]
</script>
</head>
<body>
[[[components]]]
</body>
</html>
todos ポータル
-
done
フィールドには次の場合にオブジェクトを隠す
設定
IsEmpty ( outputs⇔todos◎output_id::id )
- ポータルオブジェクトには
todos_portal
という名前をつけておく
includes ポータル
- 関連レコード移動用の
⇒
やc_js_name
,c_js_code
については次の場合にオブジェクトを隠す
設定
IsEmpty ( outputs⇔includes◎output_id::id )
-
js_id
は先ほど作成した値一覧をもとにドロップダウンリストに
- また
js_id
は入力が評価されたら OnObjectSave スクリプトトリガでreload
を実行させるようにする
-
⇒
を押したら関連レコード移動させるために以下の通りfrom_outputs_to_js
というスクリプトを作成、設定- 孫リレーションまで作っておいて、スクリプトでなく単一ステップということも考えられるものの、孫リレーションの管理コストとの天秤で、今回はこのように
c_components, c_list_items の定義
- 解説を後回しにしていた計算フィールドについて、それぞれ以下のように While 関数で実装します
c_components
- インクルードするものとして選択された JavaScript の名前をベースにして、html 側で読み込むタグを生成します
c_components
While (
[
i = 0;
items = List ( outputs⇔includes◎output_id::c_js_name );
max = ValueCount ( items );
result = ""
];
i < max;
[
i = i + 1;
result = If (
not IsEmpty ( result );
result & ¶
) &
"<" & GetValue ( items ; i ) & "></" & GetValue ( items ; i ) & ">"
];
result
)
- これによって、例えば以下のようなテキストが出力されます
<clock-element></clock-element>
<todo-list></todo-list>
<mouse-position></mouse-position>
c_list_items
-
id
,text
,completed
という三つの項目を持った JSON データを作ります - todos テーブルにある関連レコードをもとに生成させます
c_list_items
While (
[
i = 0;
item_names = List ( outputs⇔todos◎output_id::name );
item_status = List ( outputs⇔todos◎output_id::done );
max = ValueCount ( item_names );
result = ""
];
i < max;
[
i = i + 1;
result = If (
not IsEmpty ( result );
result & ¶
) &
"{id: " & i & ", text: '" & GetValue ( item_names ; i ) & "', completed: " &
If (
GetValue ( item_status ; i );
"true";
"false"
) & "},"
];
result
)
- これによって、例えば以下のようなテキストが出力されます
{id: 1, text: 'To Do リストを作る', completed: false},
{id: 2, text: 'CSS を宛てる', completed: false},
WEB ビューア
- 完成形としては以下のようなコードを指定
- Let 関数内の変数処理でゴリゴリ Substitute していくというスタイル
- 要注意点としては、Substitute をかける順番で、
js
については先にやっておく必要がある- 先にやらないと
lit_element_js_path
などはjs
内に存在するため、正しく置換完了しない
- 先にやらないと
Let (
[
lit_element_js_path = $$lit_element_js_path;
js = outputs::js & ¶ &
List ( outputs⇔includes◎output_id::c_js_code );
css = outputs::css;
list_items = outputs::c_list_items;
components = outputs::c_components;
result = outputs::g_for_webview;
result = Substitute ( result ; "[[[js]]]" ; js );
result = Substitute ( result ; "[[[css]]]" ; css );
result = Substitute ( result ; "[[[lit_element_js_path]]]" ; lit_element_js_path );
result = Substitute ( result ; "[[[list_items]]]" ; list_items );
result = Substitute ( result ; "[[[components]]]" ; components )
];
result
)
css, js フィールド
css
- 各レコード単位で制御できるように、あえて
css
フィールドを一つ切っていますが、全部まとめて管理させたいという場合はグローバルフィールドにするのもアリだと思います - 中身はあくまで一例
.completed {
color: #777;
text-decoration-line: line-through;
}
.cursor-pointer {
cursor: pointer;
}
.margin-bottom-3 {
margin-bottom: 3rem;
}
.text-color-red {
color: red;
}
js
-
js
フィールドも、一応、各レコード単位で制御できるように通常のテキストフィールドに設定している - 以下の一行は必須なので、たとえば計算値自動入力のフィールド定義をしておいてもよい
import { LitElement, html, css } from "[[[lit_element_js_path]]]"
- 計算値自動入力のフィールド定義をする場合の例
"import { LitElement, html, css } from \"[[[lit_element_js_path]]]\""
FileMaker.PerformScriptWithOption 実行用のスクリプト作成
- 後で必要となるので、以下二つのスクリプトを作成しておきます
toggle_todos
- todos テーブルの
done
の状態を0
/1
に切り替えるためのスクリプト - 受け取った引数が数字となっていて、その数字に応じた行のポータルレコードを弄る
- 中身は以下の通り
add_todo_item
- WEB ビューア内で ToDo を「追加」された場合に todos テーブルへレコード作成するためのスクリプト
- 中身は以下の通り
実装
ToDo 管理
- いよいよ実装に取りかかれます。ここまで準備に次ぐ準備ができたら、後は色んな機能を追加していくことが容易になっているはずです
- ということでまずは ToDo 管理から始めます
完成形のイメージ
- 以下のような感じです
チュートリアルのソースコード
- あらためて以下にありますが、そのままは使えないので、色々改変します
ソースコードの完成形
- 以下のような感じになるので、これを js テーブルに登録しましょう
- このコードの中身を解説して欲しい人って読者の中にいるのかどうかわからないので、割愛……
class ToDoList extends LitElement {
static properties = {
listItems: {attribute: false},
hideCompleted: {},
};
static styles = css`
[[[css]]]
`;
constructor() {
super();
this.listItems = [
[[[list_items]]]
];
this.hideCompleted = false;
}
render() {
const items = this.hideCompleted
? this.listItems.filter ((item) => !item.completed) : this.listItems;
const todos = html `
<ul>
${items.map (
(item) => html `
<li class=${item.completed ? 'completed cursor-pointer' : 'cursor-pointer'}
@click=${() => this.toggleCompleted(item)}>
${item.text}
</li>`
)}
</ul>
`;
const caughtUpMessage = html `
<p class="text-color-red">全て完了!</p>
`;
const todosOrMessage = items.length > 0
? todos : caughtUpMessage;
return html `
<div class="margin-bottom-3">
<h1>To Do</h1>
<input id="newitem" aria-label="New item">
<button @click=${this.addToDo}>追加</button><br>
<label class="cursor-pointer">
<input type="checkbox"
@change=${this.setHideCompleted}
?checked=${this.hideCompleted}>
完了したものを非表示にする
</label>
${todosOrMessage}
</div>
`;
}
toggleCompleted(item) {
FileMaker.PerformScriptWithOption ("toggle_todos", item.id, 0);
this.requestUpdate();
}
setHideCompleted(e) {
this.hideCompleted = e.target.checked;
}
get input() {
return this.renderRoot?.querySelector('#newitem') ?? null;
}
addToDo() {
if ( this.input.value.length > 0 ) {
this.listItems.push({text: this.input.value, completed: false});
FileMaker.PerformScriptWithOption ("add_todo_item", this.input.value, 0);
this.input.value = '';
this.requestUpdate();
}
}
}
customElements.define('todo-list', ToDoList);
- ポイントは、以下の二行ですね。先ほど作成しておいたスクリプトを引数つきで実行させています
FileMaker.PerformScriptWithOption ("toggle_todos", item.id, 0);
FileMaker.PerformScriptWithOption ("add_todo_item", this.input.value, 0);
- name フィールドは以下の箇所で設定した
todo-list
というものを入力
customElements.define('todo-list', ToDoList);
includes ポータルへのセット
- あとは以下のようにポータルから
todo-list
を呼び出すようにすれば完成
- 最初に見せたとおり、以下のようになります
動作確認
- WEB ビューア内の
To do リストを作る
という行をクリックすると、取消線が引かれ、さらに todos ポータル内の値も変わります - 逆に、 todos ポータル内の値を直接変更すると、それに応じて WEB ビューア内の表示も変わってくれます
- 完了したものを非表示にした状態
- 全て完了
- WEB ビューア内のインプットフィールドに入力したものは、正しく FileMaker 側にもレコード作成される
- ということで ToDo 機能が実装できましたね
時間表示
- 時分秒をリアルタイムにカウント表示してくれる機能の実装へ
- 先ほどと同じことの繰り返しになってくるので、記述はざっくり省略します
- もうこの記事を書ける時間もあまり残されていないというのっぴきならない事情もある……
js テーブルへのレコード作成
- こんな感じに作りましょう。name は
clock-element
- 先ほどと比べるとあまりソースコードは弄っていません
class ClockElement extends LitElement {
static styles = css`
[[[css]]]
`;
clock = new ClockController(this, 100);
render() {
const formattedTime = timeFormat.format(this.clock.value);
return html `
<div class="margin-bottom-3">
<h1>現在時刻</h1>
<p>${formattedTime}</p>
</div>
`;
}
}
const timeFormat = new Intl.DateTimeFormat('en-US', {
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
});
class ClockController {
host;
value = new Date();
timeout;
_timerID;
constructor(host, timeout = 1000) {
(this.host = host).addController(this);
this.timeout = timeout;
}
hostConnected() {
this._timerID = setInterval(() => {
this.value = new Date();
this.host.requestUpdate();
}, this.timeout);
}
hostDisconnected() {
clearInterval(this._timerID);
this._timerID = undefined;
}
}
customElements.define('clock-element', ClockElement);
インクルードして動作確認
- どぞ!
参考情報
- 表示形式の変更については Intl.DateTimeFormat のドキュメントとして、以下参照
マウスの位置取得
- サクサクといきましょう。下準備を念入りにしただけあって、サクサクといけますでしょう?
js テーブルへのレコード作成
- こんな感じに作りましょう。name は
mouse-position
- ここのソースコードは元となっているものからほとんど弄っていません
class MousePosition extends LitElement {
mouse = new MouseController(this);
render() {
return html`
<div class="margin-bottom-3">
<h1>The mouse is at:</h1>
x: ${this.mouse.pos.x}
y: ${this.mouse.pos.y}
</div>
`;
}
}
class MouseController {
host;
pos = {x: 0, y: 0};
_onMouseMove = ({clientX, clientY}) => {
this.pos = {x: clientX, y: clientY};
this.host.requestUpdate();
};
constructor(host) {
this.host = host;
host.addController(this);
}
hostConnected() {
window.addEventListener('mousemove', this._onMouseMove);
}
hostDisconnected() {
window.removeEventListener('mousemove', this._onMouseMove);
}
}
customElements.define('mouse-position', MousePosition);
インクルードして動作確認
- どぞ!
コンポーネントを組み合わせてインクルード
ToDo + 時刻
- たとえばこんな感じですね
全部載せ
- 当たり前ですが組み合わせは自由自在で、インクルードする順番によって表示順も変わります
おわりに
次回
- ……は、恐らくないです
- 将来的に、たとえばインクルードする js に対してレコード内容に応じた引数を渡してあげることができる、とかそんな機能をつけたりすることもアリだろうと思います
*今回の実装上いたしかたないところですが、データベース側の情報変更にあわせて WEB ビューア全体を再レンダリングしているので、本来のリアクティブ性は全く無くなってしまっているので、そのあたりも課題感としては残っています。誰か後を継いでほしい
感想
- ちょっと軽い気持ちで最近の Web Components ってどうなってるんだっけー、からの、FileMaker の WEB ビューアで実装できるからやってみたらどうなるかなー、からの、コンポーネント管理について真剣に考え始めるところまで入ってしまって、気づけば最初は単一テーブルだけで簡素に作っていた(非実用的な)だけのものから、わりかし実用的なものにまで膨らんでいってしまったなー、と……
- 前日のアドカレ記事で Alpine.js について書かれていて、これはコンポーネント管理とかほとんど意識しなくてよいからとても相性は良いのだけれど、多少コンポーネント管理した方が規模感が大きくなると取り回しが効きやすいんじゃないかなーという気はしています
- とはいえ、何も Lit でやる必然性なんてどこにもなくて、たぶん Vue あたりで書いた方が、実用面では高く作れると思います
- まあ、遊び心満載な記事ということで! メリークリスマス!🎅