LoginSignup
2
0

More than 1 year has passed since last update.

FileMaker の WEB ビューアで Lit を用いて Web Components

Last updated at Posted at 2021-12-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 が来るかもしれない(し、来ないかもしれない)

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

  • メインとなるテーブル
  • 計算フィールドの中身は後で解説予定

image.png

js

  • 各 JavaScript コンポーネントを格納しておくためのテーブル

image.png

includes

  • 要は outputs と js の二つを繋ぐための中間テーブル
  • outputs テーブルから js テーブルの中身を手軽に覗くために計算フィールドを作っておく
    • もちろんテーブルオカレンスの定義をした後でないと計算フィールドは作れない

image.png

todos

  • ToDo の中身を保管しておくためのテーブル

image.png

FileMaker テーブルオカレンスの定義

全体像

image.png

outputs ⇔ includes

  • レコード作成許可にチェックを入れておく

image.png

outputs ⇔ todos

  • こちらも同じくレコード作成許可にチェック

image.png

includes ⇔ js

  • こちらは includes テーブルに計算フィールドを作るために

image.png

FileMaker レイアウトの作成

outputs

  • メインとなるレイアウト
  • 完成形を貼っておきますが、WEB ビューアの中身やスクリプトトリガなどは後で解説します

image.png

js

  • 最低限のコードを弄るために code フィールドは大きくしておく

image.png

includes, todos

  • 基本的には覗くこともほぼないので、最低限の一行レイアウトに

image.png
image.png

FileMaker スクリプトの作成

set_global_lit_element_js_path

  • $$lit_element_js_path というグローバル変数の定義
"https://unpkg.com/lit-element/lit-element.js?module"

image.png

open

  • OnLayoutEnter スクリプトトリガで実行させる用として

image.png

reload

  • ウインドウ内容の再表示

image.png

outputs レイアウトへのスクリプトトリガ設定

  • 先ほど作っておいた open スクリプトを OnLayoutEnter スクリプトトリガで設定

image.png

値一覧の作成

  • 後で必要になるので、以下の通りに作成しておく

image.png

さらなる準備

情報元

  • あらためて今回は公式サイトより、チュートリアルにおける 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 )

image.png

  • ポータルオブジェクトには todos_portal という名前をつけておく

image.png

includes ポータル

  • 関連レコード移動用の c_js_name, c_js_code については 次の場合にオブジェクトを隠す 設定
IsEmpty ( outputs⇔includes◎output_id::id )

image.png

  • js_id は先ほど作成した値一覧をもとにドロップダウンリストに

image.png

  • また js_id は入力が評価されたら OnObjectSave スクリプトトリガで reload を実行させるようにする

image.png

  • を押したら関連レコード移動させるために以下の通り from_outputs_to_js というスクリプトを作成、設定
    • 孫リレーションまで作っておいて、スクリプトでなく単一ステップということも考えられるものの、孫リレーションの管理コストとの天秤で、今回はこのように

image.png

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]]]\""

image.png

FileMaker.PerformScriptWithOption 実行用のスクリプト作成

  • 後で必要となるので、以下二つのスクリプトを作成しておきます

toggle_todos

  • todos テーブルの done の状態を 0 / 1 に切り替えるためのスクリプト
  • 受け取った引数が数字となっていて、その数字に応じた行のポータルレコードを弄る
  • 中身は以下の通り

image.png

add_todo_item

  • WEB ビューア内で ToDo を「追加」された場合に todos テーブルへレコード作成するためのスクリプト
  • 中身は以下の通り

image.png

実装

ToDo 管理

  • いよいよ実装に取りかかれます。ここまで準備に次ぐ準備ができたら、後は色んな機能を追加していくことが容易になっているはずです
  • ということでまずは ToDo 管理から始めます

完成形のイメージ

  • 以下のような感じです

image.png

チュートリアルのソースコード

  • あらためて以下にありますが、そのままは使えないので、色々改変します

ソースコードの完成形

  • 以下のような感じになるので、これを 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);

image.png

includes ポータルへのセット

  • あとは以下のようにポータルから todo-list を呼び出すようにすれば完成

image.png

  • 最初に見せたとおり、以下のようになります

image.png

動作確認

  • WEB ビューア内の To do リストを作る という行をクリックすると、取消線が引かれ、さらに todos ポータル内の値も変わります
  • 逆に、 todos ポータル内の値を直接変更すると、それに応じて WEB ビューア内の表示も変わってくれます

image.png

  • 完了したものを非表示にした状態

image.png

  • 全て完了

image.png

  • WEB ビューア内のインプットフィールドに入力したものは、正しく FileMaker 側にもレコード作成される

image.png
image.png

  • ということで ToDo 機能が実装できましたね

時間表示

  • 時分秒をリアルタイムにカウント表示してくれる機能の実装へ
  • 先ほどと同じことの繰り返しになってくるので、記述はざっくり省略します
    • もうこの記事を書ける時間もあまり残されていないというのっぴきならない事情もある……

js テーブルへのレコード作成

  • こんな感じに作りましょう。name は clock-element
  • 先ほどと比べるとあまりソースコードは弄っていません

image.png

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);

インクルードして動作確認

  • どぞ!

image.png

参考情報

  • 表示形式の変更については Intl.DateTimeFormat のドキュメントとして、以下参照

マウスの位置取得

  • サクサクといきましょう。下準備を念入りにしただけあって、サクサクといけますでしょう?

js テーブルへのレコード作成

  • こんな感じに作りましょう。name は mouse-position
  • ここのソースコードは元となっているものからほとんど弄っていません

image.png

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);

インクルードして動作確認

  • どぞ!

image.png

コンポーネントを組み合わせてインクルード

ToDo + 時刻

  • たとえばこんな感じですね

image.png

全部載せ

  • 当たり前ですが組み合わせは自由自在で、インクルードする順番によって表示順も変わります

image.png

おわりに

次回

  • ……は、恐らくないです
  • 将来的に、たとえばインクルードする js に対してレコード内容に応じた引数を渡してあげることができる、とかそんな機能をつけたりすることもアリだろうと思います *今回の実装上いたしかたないところですが、データベース側の情報変更にあわせて WEB ビューア全体を再レンダリングしているので、本来のリアクティブ性は全く無くなってしまっているので、そのあたりも課題感としては残っています。誰か後を継いでほしい

感想

  • ちょっと軽い気持ちで最近の Web Components ってどうなってるんだっけー、からの、FileMaker の WEB ビューアで実装できるからやってみたらどうなるかなー、からの、コンポーネント管理について真剣に考え始めるところまで入ってしまって、気づけば最初は単一テーブルだけで簡素に作っていた(非実用的な)だけのものから、わりかし実用的なものにまで膨らんでいってしまったなー、と……
  • 前日のアドカレ記事で Alpine.js について書かれていて、これはコンポーネント管理とかほとんど意識しなくてよいからとても相性は良いのだけれど、多少コンポーネント管理した方が規模感が大きくなると取り回しが効きやすいんじゃないかなーという気はしています
  • とはいえ、何も Lit でやる必然性なんてどこにもなくて、たぶん Vue あたりで書いた方が、実用面では高く作れると思います
  • まあ、遊び心満載な記事ということで! メリークリスマス!🎅

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0