5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

FileMakerAdvent Calendar 2021

Day 25

Alpine.jsを使ったWEBビューワ開発

Last updated at Posted at 2021-12-23

FileMaker で WebViewer を使うなら Alpine.js + Tailwindcssからの続きです。

##Alpineの話をする前にwebビューワでの開発の流れについて少々

dataAPI ありきでの開発

ver19 から登場した "FileMaker Data APIを実行"スクリプトステップのおかげで、常に一定の型の JSON でレコード情報を取得できるようになりました。
この"一定の型"というのが重要であり、戻り値の response.data 以下にレコードが配列で収まっているというのが保証されているわけです。
なので自分は何も考えずにresponse.data 以下をそのまま webviewer に渡すようにしています。FM 側で変にデータを整形せず、そのまま渡す。JSON の解析は JavaScript の方が高速かつ簡単ですので変に FM で弄らない方が楽です。
JavaScript にこの決まった型の JSON を渡す事で、汎用的なコードで受ければいいことになります。
Alpine.js を触ればわかるのですが、この dataAPI の戻り値は簡単に Alpine.jsのx-data にセットでき、すぐに動くものができるというのがこのコンビの素晴らしいところです。
また、この決まった型の JSON を渡せるということは、FileMaker を全く知らない web 界隈の人にも仕事を任せる事ができます。


コードエディタ(IDE) とのやりとりは常に一方通行で。

話はちょっと変わりますが、FileMaker での開発は、そのスクリプトエディタ上での開発となりますが、webビューワの世界だけはそことは切り離す事ができます。
ここは各人各様のやり方があるのでしょうが、ほぼ VSCODE のような IDE で開発したものを貼り付けて行っていると思います。
しかし、この IDE と FileMaker との開発中のデータのやり取りが意外と面倒で、お互い連携しているわけではないので、必ず一方通行にしておかないと非常に煩雑になります。ケアレスミスが多発するのです。
実はこの部分が一番厄介というか気をつける部分だと私的には常々思っていました。

私の場合、以下のような流れで一方通行を行っています。

  1. 極力"FileMaker Data APIを実行"スクリプトステップの値 を利用してweb ビューワにデータを渡す。
  2. IDE(VSCODE)での開発中は開発モード状態にしておく(変数で明示)
  3. data.jsファイルを別途作成し、その中にdataAPIの戻り値をそのままモックデータとして貼り付ける。
  4. 最後にタスクランナー(Gulp)を利用して html, css, JavaScript を一枚の html ファイルにまとめ、それを常に一方通行で FM に貼り付ける。
  5. webビューワ内のコードは常に Gitで管理し、 GitHub にプッシュして差分管理

以下にかいつまんで説明します。

### IDEでの開発中は開発モード状態にしておく

2)の部分は以下のような1行のコードを予め挟んで置きます。これを後述するタスクランナーでまとめる時に"dev"を"prod"に置換してFileMakerに渡す時は const mode = "prod" になるようにしておきます。

html
<script>
  const mode = "dev"
</script>

モードを分ける理由は、例えばFileMaker.PerformScriptという関数は開発(ブラウザでの)中は使えないのでエラーになります。開発中だけその部分をコメントアウトしておくのがよくあるパターンだと思うのですが、うっかりコメントアウトを外し忘れとかよくありますので。
こうしておけば条件分岐でdevモードの時はログに出力、prodモードの時はFileMaker.PerformScriptを実行になりますので、不要なミスを避けられます。
それ以外にも開発環境とFileMaker環境との差異は色々と出てきますので、環境分岐は一方通行実現に必須です。

###dataAPIの戻り値をそのままモックデータとして使う。

3)についてはdataAPIの戻り値をデータビューワーでコピーし、data.jsファイルに以下のように貼り付けます。

js
const data = [] //この配列の中にresponse.data以下をペーストしておく。

前回記事での内容で言いますと、

js
const data = [
   {
    fieldData: {
      employee_age: '61',
      employee_name: 'Tiger Nixon',
      employee_salary: '320800',
      id: '1',
      profile_image: 'https://randomuser.me/api/portraits/men/1.jpg',
    },
    modId: '0',
    portalData: {},
    recordId: '2',
  },

<以下略>

   ]

という感じになります。これをAlpine.jsに渡す時に、例えば以下のようなconvDataという関数を用意しておき、

script.js
const convData = (data) => {
  return data.map( e => e.fieldData)
}

この関数にモックデータを渡すとfieldData以下のみを渡す事ができます。前回記事ではFileMaker側でこの処理を行いましたが、JavaScriptで書くとたったこれだけで済みます。この例は階層が浅いですが、深い階層やポータルデータの処理など、複雑な処理もこの段階で全てやっておきます。

###タスクランナー Gulp.jsでファイルをまとめる。

タスクランナーとは、ぶっちゃけて言うと便利ツールなんですが、色々あります。今ではバンドラーという呼び名でwebpackviteなどが有名ですが、とても高機能で設定方法を覚えるのも大変です。webビューワではそこまでの機能は大袈裟で不要です。そしてAlpine.jsはコンパイル(ブラウザが読めるようにコード変換する)の必要がありません。
なので2)で述べたIDEでの開発環境と本番環境としてのFileMakerとの差異を埋めるだけ、主に文字の置換だけで十分なので、JavaScriptで記述できるGulp.jsの方がベターだと思います。

gulpfile.jsの設定の中身はたったの10行程度です。

gulpfile.js
const gulp = require('gulp')
const replace = require('gulp-replace')
const inlinesource = require('gulp-inline-source')

gulp.task('fm', () => {
  return gulp
    .src('./public/index.html') //変換元のindex.html
    .pipe(replace('<script src="../src/data.js"></script>', '<script>const data = "___DATA___"</script>')) //モックデータの入ったdata.jsをFileMakerからのdataと置換できるようにしておく。
    .pipe(replace('dev', 'prod')) // devモードからprodモードへ
    .pipe(inlinesource()) // inlineプロパティを付与したhtmlタグの中身をインライン文字列で置換
    .pipe(gulp.dest('./filemaker')) //変換後のディレクトリ。この中にindex.htmlファイルが出来、それをそのままFMにペーストする。
})

プラグインとして文字列置換のgulp-replace、リンク先のソースコードを埋め込むgulp-inline-sourceを利用しているのがミソです。
このgulp-inline-sourceが非常に便利で、ソースコードを埋め込みたい箇所に下記のように"inline"と記述しておくと、そのsrcに指定したソースコードをそのまま埋め込んでくれます。

index.html
<link rel="stylesheet" href='styles.css' inline></style>
<中略>
<script src="../node_modules/alpinejs/dist/cdn.min.js" inline></script>

Alpine.jsのminifyしたコードを埋め込む処理、それとtailwindcssが作成したstyleシートをinlineとして埋め込んでいます。
Alpine.jsは僅か33kbですし、tailwindcssが作成した必要なだけのcssも僅かです。
前回記事ではCDN経由で読み込ませていましたが、通常はwebビューワごとに埋め込んいます。こうしておけばオフラインでも使えますので。


話がだいぶ逸れましたが、快適なwebビューワ開発には必須なので記しました。

Alpine.jsに話題を戻します。

前回の記事の題材をカスタマイズしてみます。

前回記事で扱ったサンプルデータを少し改造してみました。
salaryでソートするボタンを付け、更にカードをクリックするとFileMakerのスクリプトを叩くようにしてみました。

image.png

index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link
      href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css"
      rel="stylesheet"
    />
    <script
      src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.min.js"
      defer
    ></script>
    <title>WebViewerDocument</title>
  </head>
  <body>
    <div class="container pt-8 mx-auto" x-data="alpineData" x-init="init()">
      <button
        class="btn bg-blue-300 text-white px-2 py-1 mb-2 rounded-md"
        x-on:click="toggleFlag"
      >
        ソートボタン
      </button>
      <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
        <template x-for="item in sortCard" :key="item.id">
          <div
            x-on:click="showCard(item.id)"
            class="
              flex
              items-center
              shadow
              hover:bg-indigo-100 hover:shadow-lg hover:rounded
              transition
              duration-150
              ease-in-out
              transform
              hover:scale-105
              p-3
            "
          >
            <img
              class="w-10 h-10 rounded-full mr-4"
              :src="`${item.profile_image}`"
            />
            <div class="text-sm">
              <p
                class="text-gray-900 leading-none"
                x-text="item.employee_name + ' (' + item.employee_age + ')'"
              ></p>
              <p
                class="text-gray-600"
                x-text="'$'+item.employee_salary/100"
              ></p>
            </div>
          </div>
        </template>
      </div>
    </div>
    <script>
      const mode = "dev";
    </script>

    <script src="data.js"></script>
    <script src="script.js"></script>
  </body>
</html>

大きな変更点は

index.html
<div class="container pt-8 mx-auto" x-data="alpineData" x-init="init()">

x-dataにscript.js内にあるalpineDataというオブジェクトデータを渡し、x-initで初期化をおこなっています。
そしてボタンを追加し、ボタンには
x-on:click="toggleFlag"
という記述をし、alpineDataが持つtoggleFlagメソッドを起動するようにしています。これによってsortFlgの値が反転し、自動的にsortCard()メソッドが反応し、カードの並びが変化します。

index.html
<template x-for="item in sortCard" :key="item.id">

レコードをループさせるこの部分 x-for="item in sortCard" も 前回はレコードデータそのもの(この場合はdata)を回していましたが、sortCardというメソッドの戻り値を回すようにしています。

更に、カード本体にも
x-on:click="showCard(item.id)"
という記述を加え、カードをクリックするとこちらもalpineDataが持つshowCardメソッドにemployee_idが渡り、FileMakerのshowCardというスクリプト(単純にそのidのレコードを開くだけ)を叩くようにしています。

このshowCardメソッドはdoScriptという関数を叩いていますが、そのdoScriptはFileMaker.PerformScriptWithOptionをラップしているだけで、modeがdevの時はコンソールログに引数を表示、prodの時はFileMakerスクリプトを叩きます。上述した環境による分岐です。

script.js
// Alpine.jsに渡す変数
const alpineData = {
  data: '',   // x-dataに渡す値
  // 初期化メソッド 必要なデータのみをdataにセットする
  init() {
    this.data = convData(data)
  },
  sortFlg: false,    //ソートフラグ
 // フラグ値を反転させるメソッド
  toggleFlag() {
    this.sortFlg = !this.sortFlg
  },
  // sortFlgの値に応じてdataをソートさせる関数
  get sortCard() {
    if (this.data) {
      if (this.sortFlg) {
        return this.data.sort((a, b) => a.employee_salary - b.employee_salary)
      } else {
        return this.data.sort((a, b) => b.employee_salary - a.employee_salary)
      }
    }
  },
  // FileMakerのスクリプトを叩く関数
  showCard(id) {
    doScript('showCard', id, 0)
  },
}

// =======================================================

// FMのdataAPIから受け取ったJSONからfieldDataのみを取得する関数
const convData = (data) => {
  return data.map((e) => e.fieldData)
}

// FMのスクリプトを叩く関数
const doScript = (scriptName, args, optionNum) => {
  if (!optionNum) optionNum = 0
  args = JSON.stringify(args)
  if (mode != 'prod') {
    console.log(args)
  } else {
    FileMaker.PerformScriptWithOption(scriptName, args, optionNum)
  }
}

data.js
const data = [
  {
    fieldData: {
      employee_age: '61',
      employee_name: 'Tiger Nixon',
      employee_salary: '320800',
      id: '1',
      profile_image: 'https://randomuser.me/api/portraits/men/1.jpg',
    },
    modId: '0',
    portalData: {},
    recordId: '2',
  },
  {
    fieldData: {
      employee_age: '63',
      employee_name: 'Garrett Winters',
      employee_salary: '170750',
      id: '2',
      profile_image: 'https://randomuser.me/api/portraits/men/2.jpg',
    },
    modId: '0',
    portalData: {},
    recordId: '3',
  },

<以下略>

]

data.jsファイルの内容は省略していますが、上記3つのファイルを一つのフォルダに入れてindex.htmlファイルを開けばすぐに試すことができます。

うまく伝える事が難しいのですが、こんな感じでx-dataに渡すオブジェクトに色んなメソッドを追加していく事で色んなUIが実現します。この後、JavaScriptのfilter(),include()などですぐに検索機能も作れるのがわかると思います。

逆にFileMakarからこのalpineData内のメソッドを叩く事はできないのですが、Alpine.store()というグローバルステートを作れるので、そちらにFileMakerから叩くスクリプトを記述できます。
この辺はまたいつか。

気をつけるのは、調子に乗って肥大化してしまわないよう、ほどほどにしておく事です。主役はFileMakerでAlpine.jsはあくまでもサポート役です。

ディベロッパーツールとしてchrome機能拡張があります。これは絶対あった方がいいです。
Alpine.js devtools

追記: Vue.jsの作者Evan Youさん、このAlpine.jsより更に軽量(Alpineの半分だとか)のpetite-vueの開発に着手されています。まだアルファ版ですが、ビルド無しで手軽に使えるそうです。とても楽しみですね。


上記サンプルデータはGitHubのリポジトリに公開しております。
公開データは上述した一方通行の設定ファイルなども含んでいますので、node.jsをインストールした上で使用して下さい。詳細はGitのREADME.mdに書いております。

5
4
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
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?