API機能を使って、画面遷移なしで、エントリーのフィルタリングをしよう
※この記事の内容は2021年9月に開催された a-blog cms Training Camp 2021 Autumn の中で行われた「API機能を使って、画面遷移なしで、エントリーのフィルタリングをしよう」というカスタマイズ講座の内容です。
a-blog cms ver.2.12で実装予定(2021年9月11日現在)のAPI機能をつかって画面遷移のないエントリーのフィルタリング機能を実装します。
当日のカスタマイズ講座を行った動画もありますので、よければこちらもご覧ください。
近年、Web技術の進化により、ユーザーはよりリッチな体験を求めるようになりました。
JavaScriptを使用して非同期でデータ通信を行うことで、不要な画面遷移をさせることなくページを更新することができるからです。
それに伴い、ヘッドレスCMSやSPAなどJavaScriptでデータ通信を行い、画面を表示させる実装が増えました。
API機能について
最初にa-blog cmsでAPI機能について以下のことを説明します。
- API機能でできること
- API機能を使うための初期設定
- エンドポイント
API機能でできること
API機能ができることによって以下のような事ができるようになります。
- ヘッドレスCMSとしてa-blog cmsを利用する
- 画面遷移のないコンテンツのフィルタリングやソート機能の開発
- 画面遷移のないページネーションやもっと見るボタンの実装
- Nuxt.jsやNext.jsといった静的サイトジェネレータとの連携
他にも柔軟な使用方法が考えられます。そのため、様々なニーズに答えることができる様になるといえるでしょう。
API機能を使うための初期設定
コンフィグ > 一般設定 >API設定からAPI機能についての設定を行うことができます。
API設定をクリックしていただき、APIの有効化のチェックボックスにチェックを入れていただくとa-blog cmsのAPIを使用することができるようになります。
また、この設定画面で、APIをリクエストする側のドメインの設定や、HTTPリファラー、IPアドレスによるAPI制限の設定をすることができます。
※ 同一オリジンでAPIを使用する際には Allow-Origin の設定は不要です。
また、API-KEYも記載されていますので、メモしておきましょう。APIを使用してHTTP通信をする際に必要になります。API-KEYはコンフィグセット毎に生成されるので、異なるコンフィグセットを設定しているブログが存在する場合は注意しましょう。
エンドポイント
エンドポイントとはAPIにアクセスするためのURIのことを言います。a-blog cmsでは URLコンテキストの末尾に /api/:module_id/
(:module_id
は任意のモジュールID)を追加したURIがエンドポイントになります。
例えば、a-blog cmsを https://acms.com
というURLで使用していて、summary_index
というモジュールIDを設定したモジュールの情報を取得する場合のエンドポイントは以下になります。
https://acms.com/api/summary_index/
また、a-blog cmsの特徴であるURLコンテキストを利用することができます。(ここは嬉しいポイントですよね。)
例えば、 summary_index
のモジュールIDを設定したモジュールの情報のうち、カスタムフィールド「price」の値を「60000」で登録しているかつ2ページ目の情報だけを取得したい場合は以下のようなエンドポイントになります。
https://acms.com/field/price/60000/page/2/api/summary_index/
また、API機能はモジュールIDを作成することができるすべてのモジュールに対応しています。
フィルタリング機能の実装
この記事では、API機能を使った実装の1例として、エントリーのフィルタリング機能を実装していきます。フィルタリング機能を作成するにあたって、サンプルとなるデータを用意する必要があります。この記事では、下記のスーパーヒーローの特徴というデータセットを用いて実装していきます。
事前準備
a-blog cms でAPI機能を使用する事前準備として以下の5点の作業をご自分の a-blog cms の環境で行います。
field-hero.html
でエントリーのカスタムフィールドを作成する- 上記の
heroes_info.csv
をcsvインポートの機能を用いてインポートする - コンフィグ > 一般設定 > API設定からAPI機能を有効にする
- 異なるドメインからHTTPリクエストを送信する場合は、Allow-Origin に HTTPリクエストを送信するサイトのドメインを追加して保存する
- summary_hero_index というIDで Entry_Summary のモジュールIDを作成する。その際、URLコンテキストのフィールドとページ、表示設定の「リクエストに404 Not Foundとして応答する」にチェックを付ける
field-hero.html のサンプルはこちらになります。
<h3 class="acms-admin-admin-title2">ヒーロー情報設定</h3> <table class="acms-admin-table-admin-edit"> <tr> <th>身長</th> <td> <input type="number" name="height" value="{height}" class="acms-admin-form-width-mini" /> <input type="hidden" name="field[]" value="height" /> </td> </tr> <tr> <th>体重</th> <td> <input type="number" name="weight" value="{weight}" class="acms-admin-form-width-mini" /> <input type="hidden" name="field[]" value="weight" /> </td> </tr> <tr> <th>性別</th> <td> <div class="acms-admin-form-radio"> <input type="radio" name="gender" value="Male" {gender:checked#Male} id="input-radio-gender-Male" /> <label for="input-radio-gender-Male"> <i class="acms-admin-ico-radio"></i>男性</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="gender" value="Female" {gender:checked#Female} id="input-radio-gender-Female" /> <label for="input-radio-gender-Female"> <i class="acms-admin-ico-radio"></i>女性</label> </div> <input type="hidden" name="field[]" value="gender" /> </td> </tr> <tr> <th>瞳の色</th> <td> <div class="acms-admin-form-radio"> <input type="radio" name="eye_color" value="yellow" {eye_color:checked#yellow} id="input-radio-eye_color-yellow" /> <label for="input-radio-eye_color-yellow"> <i class="acms-admin-ico-radio"></i>yellow</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="eye_color" value="blue" {eye_color:checked#blue} id="input-radio-eye_color-blue" /> <label for="input-radio-eye_color-blue"> <i class="acms-admin-ico-radio"></i>blue</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="eye_color" value="green" {eye_color:checked#green} id="input-radio-eye_color-green" /> <label for="input-radio-eye_color-green"> <i class="acms-admin-ico-radio"></i>green</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="eye_color" value="brown" {eye_color:checked#brown} id="input-radio-eye_color-brown" /> <label for="input-radio-eye_color-brown"> <i class="acms-admin-ico-radio"></i>brown</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="eye_color" value="red" {eye_color:checked#red} id="input-radio-eye_color-red" /> <label for="input-radio-eye_color-red"> <i class="acms-admin-ico-radio"></i>red</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="eye_color" value="violet" {eye_color:checked#violet} id="input-radio-eye_color-violet" /> <label for="input-radio-eye_color-violet"> <i class="acms-admin-ico-radio"></i>violet</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="eye_color" value="white" {eye_color:checked#white} id="input-radio-eye_color-white" /> <label for="input-radio-eye_color-white"> <i class="acms-admin-ico-radio"></i>white</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="eye_color" value="purple" {eye_color:checked#purple} id="input-radio-eye_color-purple" /> <label for="input-radio-eye_color-purple"> <i class="acms-admin-ico-radio"></i>purple</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="eye_color" value="black" {eye_color:checked#black} id="input-radio-eye_color-black" /> <label for="input-radio-eye_color-black"> <i class="acms-admin-ico-radio"></i>black</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="eye_color" value="grey" {eye_color:checked#grey} id="input-radio-eye_color-grey" /> <label for="input-radio-eye_color-grey"> <i class="acms-admin-ico-radio"></i>grey</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="eye_color" value="silver" {eye_color:checked#silver} id="input-radio-eye_color-silver" /> <label for="input-radio-eye_color-silver"> <i class="acms-admin-ico-radio"></i>silver</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="eye_color" value="gold" {eye_color:checked#gold} id="input-radio-eye_color-gold" /> <label for="input-radio-eye_color-gold"> <i class="acms-admin-ico-radio"></i>gold</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="eye_color" value="hazel" {eye_color:checked#hazel} id="input-radio-eye_color-hazel" /> <label for="input-radio-eye_color-hazel"> <i class="acms-admin-ico-radio"></i>hazel</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="eye_color" value="indigo" {eye_color:checked#indigo} id="input-radio-eye_color-indigo" /> <label for="input-radio-eye_color-indigo"> <i class="acms-admin-ico-radio"></i>indigo</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="eye_color" value="amber" {eye_color:checked#amber} id="input-radio-eye_color-amber" /> <label for="input-radio-eye_color-amber"> <i class="acms-admin-ico-radio"></i>amber</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="eye_color" value="bown" {eye_color:checked#bown} id="input-radio-eye_color-bown" /> <label for="input-radio-eye_color-bown"> <i class="acms-admin-ico-radio"></i>bown</label> </div> <input type="hidden" name="field[]" value="eye_color" /> </td> </tr> <tr> <th>種族</th> <td> <select name="race"> <option value=""></option> <option value="Alien" {race:selected#Alien}>Alien</option> <option value="Alpha" {race:selected#Alpha}>Alpha</option> <option value="Amazon" {race:selected#Amazon}>Amazon</option> <option value="Android" {race:selected#Android}>Android</option> <option value="Animal" {race:selected#Animal}>Animal</option> <option value="Asgardian" {race:selected#Asgardian}>Asgardian</option> <option value="Atlantean" {race:selected#Atlantean}>Atlantean</option> <option value="Bizarro" {race:selected#Bizarro}>Bizarro</option> <option value="Bolovaxian" {race:selected#Bolovaxian}>Bolovaxian</option> <option value="Clone" {race:selected#Clone}>Clone</option> <option value="Cosmic Entity" {race:selected#Cosmic Entity}>Cosmic Entity</option> <option value="Cyborg" {race:selected#Cyborg}>Cyborg</option> <option value="Czarnian" {race:selected#Czarnian}>Czarnian</option> <option value="Dathomirian Zabrak" {race:selected#Dathomirian Zabrak}>Dathomirian Zabrak</option> <option value="Demon" {race:selected#Demon}>Demon</option> <option value="Eternal" {race:selected#Eternal}>Eternal</option> <option value="Flora Colossus" {race:selected#Flora Colossus}>Flora Colossus</option> <option value="Frost Giant" {race:selected#Frost Giant}>Frost Giant</option> <option value="God" {race:selected#God }>God</option> <option value="Gorilla" {race:selected#Gorilla}>Gorilla</option> <option value="Gungan" {race:selected#Gungan}>Gungan</option> <option value="Human" {race:selected#Human}>Human</option> <option value="HumanKree" {race:selected#HumanKree}>HumanKree</option> <option value="HumanSpattoi" {race:selected#HumanSpattoi}>HumanSpattoi</option> <option value="HumanVulcan" {race:selected#HumanVulcan}>HumanVulcan</option> <option value="HumanVuldarian" {race:selected#HumanVuldarian}>HumanVuldarian</option> <option value="Icthyo Sapien" {race:selected#Icthyo Sapien}>Icthyo Sapien</option> <option value="inhuman" {race:selected#inhuman}>inhuman</option> <option value="Kaiju" {race:selected#Kaiju}>Kaiju</option> <option value="Kakarantharaian" {race:selected#Kakarantharaian}>Kakarantharaian</option> <option value="Korugaran" {race:selected#Korugaran}>Korugaran</option> <option value="Kryptonian" {race:selected#Korugaran}>Kryptonian</option> <option value="Luphomoid" {race:selected#Luphomoid}>Luphomoid</option> <option value="Maiar" {race:selected#Maiar}>Maiar</option> <option value="Martian" {race:selected#Martian}>Martian</option> <option value="Metahuman" {race:selected#Metahuman}>Metahuman</option> <option value="Mutant" {race:selected#Mutant}>Mutant</option> <option value="New Got" {race:selected#New Got}>New Got</option> <option value="Neyaphem" {race:selected#Neyaphem}>Neyaphem</option> <option value="Parademon" {race:selected#Parademon}>Parademon</option> <option value="Planet" {race:selected#Planet}>Planet</option> <option value="Rodian" {race:selected#Rodian}>Rodian</option> <option value="Saiyan" {race:selected#Saiyan}>Saiyan</option> <option value="Spartoi" {race:selected#Spartoi}>Spartoi</option> <option value="Strontian" {race:selected#Strontian}>Strontian</option> <option value="Symbiote" {race:selected#Symbiote}>Symbiote</option> <option value="Talokite" {race:selected#Talokite}>Talokite</option> <option value="Tamaranean" {race:selected#Tamaranean}>Tamaranean</option> <option value="Tamaranean" {race:selected#Tamaranean}>Ungaran</option> <option value="Vampire" {race:selected#Vampire}>Vampire</option> <option value="Xenomorph XX121" {race:selected#Xenomorph XX121}>Xenomorph XX121</option> <option value="Yautja" {race:selected#Yautja}>Yautja</option> <option value="Yoda's species" {race:selected#Yoda's species}>Yoda's species</option> <option value="ZenWhoberian" {race:selected#ZenWhoberian}>ZenWhoberian</option> <option value="Zombie" {race:selected#Zombie}>Zombie</option> </select> <input type="hidden" name="field[]" value="race" /> </td> </tr> <tr> <th>髪の毛の色</th> <td> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="hair_color[]" value="No Hair" {hair_color:checked#No Hair} id="input-checkbox-hair_color-No Hair" /> <label for="input-checkbox-hair_color-No Hair"> <i class="acms-admin-ico-checkbox"></i>No Hair</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="hair_color[]" value="Black" {hair_color:checked#Black} id="input-checkbox-hair_color-Black" /> <label for="input-checkbox-hair_color-Black"> <i class="acms-admin-ico-checkbox"></i>Black</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="hair_color[]" value="Blond" {hair_color:checked#Blond} id="input-checkbox-hair_color-Blond" /> <label for="input-checkbox-hair_color-Blond"> <i class="acms-admin-ico-checkbox"></i>Blond</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="hair_color[]" value="Brown" {hair_color:checked#Brown} id="input-checkbox-hair_color-Brown" /> <label for="input-checkbox-hair_color-Brown"> <i class="acms-admin-ico-checkbox"></i>Brown</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="hair_color[]" value="White" {hair_color:checked#White} id="input-checkbox-hair_color-White" /> <label for="input-checkbox-hair_color-White"> <i class="acms-admin-ico-checkbox"></i>White</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="hair_color[]" value="Purple" {hair_color:checked#Purple} id="input-checkbox-hair_color-Purple" /> <label for="input-checkbox-hair_color-Purple"> <i class="acms-admin-ico-checkbox"></i>Purple</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="hair_color[]" value="Orange" {hair_color:checked#Orange} id="input-checkbox-hair_color-Orange" /> <label for="input-checkbox-hair_color-Orange"> <i class="acms-admin-ico-checkbox"></i>Orange</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="hair_color[]" value="Pink" {hair_color:checked#Pink} id="input-checkbox-hair_color-Pink" /> <label for="input-checkbox-hair_color-Pink"> <i class="acms-admin-ico-checkbox"></i>Pink</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="hair_color[]" value="Red" {hair_color:checked#Red} id="input-checkbox-hair_color-Red" /> <label for="input-checkbox-hair_color-Red"> <i class="acms-admin-ico-checkbox"></i>Red</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="hair_color[]" value="Auburn" {hair_color:checked#Auburn} id="input-checkbox-hair_color-Auburn" /> <label for="input-checkbox-hair_color-Auburn"> <i class="acms-admin-ico-checkbox"></i>Auburn</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="hair_color[]" value="Strawberry Blond" {hair_color:checked#Strawberry Blond} id="input-checkbox-hair_color-Strawberry Blond" /> <label for="input-checkbox-hair_color-Strawberry Blond"> <i class="acms-admin-ico-checkbox"></i>Strawberry Blond</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="hair_color[]" value="Blue" {hair_color:checked#Blue} id="input-checkbox-hair_color-Blue" /> <label for="input-checkbox-hair_color-Blue"> <i class="acms-admin-ico-checkbox"></i>Blue</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="hair_color[]" value="Green" {hair_color:checked#Green} id="input-checkbox-hair_color-Green" /> <label for="input-checkbox-hair_color-Green"> <i class="acms-admin-ico-checkbox"></i>Green</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="hair_color[]" value="Magenta" {hair_color:checked#Magenta} id="input-checkbox-hair_color-Magenta" /> <label for="input-checkbox-hair_color-Magenta"> <i class="acms-admin-ico-checkbox"></i>Magenta</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="hair_color[]" value="Silver" {hair_color:checked#Silver} id="input-checkbox-hair_color-Silver" /> <label for="input-checkbox-hair_color-Silver"> <i class="acms-admin-ico-checkbox"></i>Silver</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="hair_color[]" value="Grey" {hair_color:checked#Grey} id="input-checkbox-hair_color-Grey" /> <label for="input-checkbox-hair_color-Grey"> <i class="acms-admin-ico-checkbox"></i>Grey</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="hair_color[]" value="Yellow" {hair_color:checked#Yellow} id="input-checkbox-hair_color-Yellow" /> <label for="input-checkbox-hair_color-Yellow"> <i class="acms-admin-ico-checkbox"></i>Yellow</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="hair_color[]" value="Gold" {hair_color:checked#Gold} id="input-checkbox-hair_color-Gold" /> <label for="input-checkbox-hair_color-Gold"> <i class="acms-admin-ico-checkbox"></i>Gold</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="hair_color[]" value="Indigo" {hair_color:checked#Indigo} id="input-checkbox-hair_color-Indigo" /> <label for="input-checkbox-hair_color-Indigo"> <i class="acms-admin-ico-checkbox"></i>Indigo</label> </div> <input type="hidden" name="field[]" value="hair_color" /> </td> </tr> <tr> <th>制作会社</th> <td> <div class="acms-admin-form-radio"> <input type="radio" name="publisher" value="Marvel Comics" {publisher:checked#Marvel Comics} id="input-radio-publisher-Marvel Comics" /> <label for="input-radio-publisher-Marvel Comics"> <i class="acms-admin-ico-radio"></i>Marvel Comics</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="publisher" value="Dark Horse Comics" {publisher:checked#Dark Horse Comics} id="input-radio-publisher-Dark Horse Comics" /> <label for="input-radio-publisher-Dark Horse Comics"> <i class="acms-admin-ico-radio"></i>Dark Horse Comics</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="publisher" value="DC Comics" {publisher:checked#DC Comics} id="input-radio-publisher-DC Comics" /> <label for="input-radio-publisher-DC Comics"> <i class="acms-admin-ico-radio"></i>DC Comics</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="publisher" value="NBC Heroes" {publisher:checked#NBC Heroes} id="input-radio-publisher-NBC Heroes" /> <label for="input-radio-publisher-NBC Heroes"> <i class="acms-admin-ico-radio"></i>NBC Heroes</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="publisher" value="Wildstorm" {publisher:checked#Wildstorm} id="input-radio-publisher-Wildstorm" /> <label for="input-radio-publisher-Wildstorm"> <i class="acms-admin-ico-radio"></i>Wildstorm</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="publisher" value="Image Comics" {publisher:checked#Image Comics} id="input-radio-publisher-Image Comics" /> <label for="input-radio-publisher-Image Comics"> <i class="acms-admin-ico-radio"></i>Image Comics</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="publisher" value="Icon Comics" {publisher:checked#Icon Comics} id="input-radio-publisher-Icon Comics" /> <label for="input-radio-publisher-Icon Comics"> <i class="acms-admin-ico-radio"></i>Icon Comics</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="publisher" value="SyFy" {publisher:checked#SyFy} id="input-radio-publisher-SyFy" /> <label for="input-radio-publisher-SyFy"> <i class="acms-admin-ico-radio"></i>SyFy</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="publisher" value="HannaBarbera" {publisher:checked#HannaBarbera} id="input-radio-publisher-HannaBarbera" /> <label for="input-radio-publisher-HannaBarbera"> <i class="acms-admin-ico-radio"></i>HannaBarbera</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="publisher" value="George Lucas" {publisher:checked#George Lucas} id="input-radio-publisher-George Lucas" /> <label for="input-radio-publisher-George Lucas"> <i class="acms-admin-ico-radio"></i>George Lucas</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="publisher" value="Team Epic TV" {publisher:checked#Team Epic TV} id="input-radio-publisher-Team Epic TV" /> <label for="input-radio-publisher-Team Epic TV"> <i class="acms-admin-ico-radio"></i>Team Epic TV</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="publisher" value="South Park" {publisher:checked#South Park} id="input-radio-publisher-South Park" /> <label for="input-radio-publisher-South Park"> <i class="acms-admin-ico-radio"></i>South Park</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="publisher" value="HarperCollins" {publisher:checked#HarperCollins} id="input-radio-publisher-HarperCollins" /> <label for="input-radio-publisher-HarperCollins"> <i class="acms-admin-ico-radio"></i>HarperCollins</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="publisher" value="ABC Studios" {publisher:checked#ABC Studios} id="input-radio-publisher-ABC Studios" /> <label for="input-radio-publisher-ABC Studios"> <i class="acms-admin-ico-radio"></i>ABC Studios</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="publisher" value="Universal Studios" {publisher:checked#Universal Studios} id="input-radio-publisher-Universal Studios" /> <label for="input-radio-publisher-Universal Studios"> <i class="acms-admin-ico-radio"></i>Universal Studios</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="publisher" value="IDW Publishing" {publisher:checked#IDW Publishing} id="input-radio-publisher-IDW Publishing" /> <label for="input-radio-publisher-IDW Publishing"> <i class="acms-admin-ico-radio"></i>IDW Publishing</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="publisher" value="Shueisha" {publisher:checked#Shueisha} id="input-radio-publisher-Shueisha" /> <label for="input-radio-publisher-Shueisha"> <i class="acms-admin-ico-radio"></i>Shueisha</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="publisher" value="Sony Pictures" {publisher:checked#Sony Pictures} id="input-radio-publisher-Sony Pictures" /> <label for="input-radio-publisher-Sony Pictures"> <i class="acms-admin-ico-radio"></i>Sony Pictures</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="publisher" value="J. K. Rowling" {publisher:checked#J. K. Rowling} id="input-radio-publisher-J. K. Rowling" /> <label for="input-radio-publisher-J. K. Rowling"> <i class="acms-admin-ico-radio"></i>J. K. Rowling</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="publisher" value="Titan Books" {publisher:checked#Titan Books} id="input-radio-publisher-Titan Books" /> <label for="input-radio-publisher-Titan Books"> <i class="acms-admin-ico-radio"></i>Titan Books</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="publisher" value="Rebellion" {publisher:checked#Rebellion} id="input-radio-publisher-Rebellion" /> <label for="input-radio-publisher-Rebellion"> <i class="acms-admin-ico-radio"></i>Rebellion</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="publisher" value="Microsoft" {publisher:checked#Microsoft} id="input-radio-publisher-Microsoft" /> <label for="input-radio-publisher-Microsoft"> <i class="acms-admin-ico-radio"></i>Microsoft</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="publisher" value="J. R. R. Tolkien" {publisher:checked#J. R. R. Tolkien} id="input-radio-publisher-J. R. R. Tolkien" /> <label for="input-radio-publisher-J. R. R. Tolkien"> <i class="acms-admin-ico-radio"></i>J. R. R. Tolkien</label> </div> <input type="hidden" name="field[]" value="publisher" /> </td> </tr> <tr> <th>肌の色</th> <td> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="skin_color[]" value="blue" {skin_color:checked#blue} id="input-checkbox-skin_color-blue" /> <label for="input-checkbox-skin_color-blue"> <i class="acms-admin-ico-checkbox"></i>blue</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="skin_color[]" value="red" {skin_color:checked#red} id="input-checkbox-skin_color-red" /> <label for="input-checkbox-skin_color-red"> <i class="acms-admin-ico-checkbox"></i>red</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="skin_color[]" value="black" {skin_color:checked#black} id="input-checkbox-skin_color-black" /> <label for="input-checkbox-skin_color-black"> <i class="acms-admin-ico-checkbox"></i>black</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="skin_color[]" value="grey" {skin_color:checked#grey} id="input-checkbox-skin_color-grey" /> <label for="input-checkbox-skin_color-grey"> <i class="acms-admin-ico-checkbox"></i>grey</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="skin_color[]" value="gold" {skin_color:checked#gold} id="input-checkbox-skin_color-gold" /> <label for="input-checkbox-skin_color-gold"> <i class="acms-admin-ico-checkbox"></i>gold</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="skin_color[]" value="green" {skin_color:checked#green} id="input-checkbox-skin_color-green" /> <label for="input-checkbox-skin_color-green"> <i class="acms-admin-ico-checkbox"></i>green</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="skin_color[]" value="white" {skin_color:checked#white} id="input-checkbox-skin_color-white" /> <label for="input-checkbox-skin_color-white"> <i class="acms-admin-ico-checkbox"></i>white</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="skin_color[]" value="pink" {skin_color:checked#pink} id="input-checkbox-skin_color-pink" /> <label for="input-checkbox-skin_color-pink"> <i class="acms-admin-ico-checkbox"></i>pink</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="skin_color[]" value="silver" {skin_color:checked#silver} id="input-checkbox-skin_color-silver" /> <label for="input-checkbox-skin_color-silver"> <i class="acms-admin-ico-checkbox"></i>silver</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="skin_color[]" value="yellow" {skin_color:checked#yellow} id="input-checkbox-skin_color-yellow" /> <label for="input-checkbox-skin_color-yellow"> <i class="acms-admin-ico-checkbox"></i>yellow</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="skin_color[]" value="purple" {skin_color:checked#purple} id="input-checkbox-skin_color-purple" /> <label for="input-checkbox-skin_color-purple"> <i class="acms-admin-ico-checkbox"></i>purple</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="skin_color[]" value="Orange" {skin_color:checked#Orange} id="input-checkbox-skin_color-Orange" /> <label for="input-checkbox-skin_color-Orange"> <i class="acms-admin-ico-checkbox"></i>Orange</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="skin_color[]" value="gray" {skin_color:checked#gray} id="input-checkbox-skin_color-gray" /> <label for="input-checkbox-skin_color-gray"> <i class="acms-admin-ico-checkbox"></i>gray</label> </div> <div class="acms-admin-form-checkbox"> <input type="checkbox" name="skin_color[]" value="bluewhite" {skin_color:checked#bluewhite} id="input-checkbox-skin_color-bluewhite" /> <label for="input-checkbox-skin_color-bluewhite"> <i class="acms-admin-ico-checkbox"></i>bluewhite</label> </div> <input type="hidden" name="field[]" value="skin_color" /> </td> </tr> <tr> <th>全体的な位置付け</th> <td> <div class="acms-admin-form-radio"> <input type="radio" name="alignment" value="good" {alignment:checked#good} id="input-radio-alignment-good" /> <label for="input-radio-alignment-good"> <i class="acms-admin-ico-radio"></i>good</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="alignment" value="bad" {alignment:checked#bad} id="input-radio-alignment-bad" /> <label for="input-radio-alignment-bad"> <i class="acms-admin-ico-radio"></i>bad</label> </div> <div class="acms-admin-form-radio"> <input type="radio" name="alignment" value="neutral" {alignment:checked#neutral} id="input-radio-alignment-neutral" /> <label for="input-radio-alignment-neutral"> <i class="acms-admin-ico-radio"></i>neutral</label> </div> <input type="hidden" name="field[]" value="alignment" /> </td> </tr> </table>
処理の流れ
実際に、JavaScriptを書いて実装していく前に、処理の流れを確認します。
この記事ではReactというJavaScriptライブラリを使用して実装していきますが、他のJavaScriptフレームワークやライブラリ、例えば、Vue.js などを利用しても、処理の流れは共通しているためとても大切な部分になってくるからです。
1. conditions
というglobal state を用意します。(global stateとは異なるコンポーネント間で共有できるデータのことになります)
conditions
には以下のようにデータをもたせます。初期値はからのオブジェクトとします。
カスタムフィールドの field
の値を key
にして、value
に field
と keywords
の key
を持つオブジェクトを持つようにします。
{
gender: {field: 'gender', keywords: ['Male']},
eye_color: {field: 'eye_color', keywords: ['yellow', 'green']},
}
2. AccordionItemがクリックされると、クリックされたAccordionItemに紐付いている field
と keyword
が conditions
に追加されます。(conditions
にすでに 同じfield
を key
にもつオブジェクトが追加されている場合は keyword
のみ追加)
例えば、AccordionItemに紐付いている `field` が `hair_color` 、 `keyword` が `Brown` だった場合、 `conditions` は下記のようになります。
{
gender: {field: 'gender', keywords: ['Male']},
eye_color: {field: 'eye_color', keywords: ['yellow', 'green']},
hair_color: {field: 'hair_color', keywords: ['Brown']},
}
3. conditions
が変更されたら、conditions
の情報を元に エンドポイントを作成し、HTTPリクエストを行い、取得したデータを元に、HeroCard
を描画します。
エンドポイントは上記の例の場合、 `https://example.com/field/gender/Male/_``and``_/eye_color/yellow/green/_``and``_/hair_color/Brown/api/:module_id/` になります。このURIに対してHTTPリクエストを行い、返ってきたレスポンスデータを画面に反映します。
4. また、クリックされたAccordionItemに紐付いている field
と keyword
がフィルタリングされている値であった場合には、その field
を key
にもつオブジェクトの keywords
から keyword
を削除します。
↓ `field` が `eye_color`、 `keyword` が `yellow` の `AccordionItem` をクリックした場合の `conditions`
{
gender: {field: 'gender', keywords: ['Male']},
eye_color: {field: 'eye_color', keywords: ['green']},
hair_color: {field: 'hair_color', keywords: ['Brown']},
}
削除する field
の keywords
が1つであった場合には オブジェクトの key
毎削除します。
↓field
が gender
、 keyword
が Male
の AccordionItem
をクリックした場合の conditions
{ eye_color: {field: 'eye_color', keywords: ['yellow', 'green']}, hair_color: {field: 'hair_color', keywords: ['Brown']}, }
実装
処理の流れは理解できましたでしょうか?処理の流れが理解できたら、実際にソースコードを書いて実装していきます。 また今回はソースコードが長くなってしまうので、githubにリポジトリを作成しました。詳細はこちらのリポジトリをみていただけると幸いです。
またこちらのリポジトリの README.md
には簡単にこの講座用のテスト環境を試すことができる手順が記載してありますので、お時間ある方はぜひお試しください。
以下の手順で再現可能です。
git clone https://github.com/appleple/api-lesson-in-a-blogcms-training-camp-2021-autumn.git
npm ci
touch .env
.env
ファイルに下記をコピペ
API_KEY = 任意のAPI-KEY
npm run build
npm start
GitHub
2021年9月11日(土)開催のa-blog cms Training Camp 2021 Autumnのカスタマイズ講座「API機能を使って、画面遷移なしで、エントリーのフィルタリングをしよう」で使用されるサンプルコード - ...
今回の講座ではいくつかの外部ライブラリを使用しています。
- @material-ui/core (スタイリング簡易化のため)
- @material-ui/icons (スタイリング簡易化のため)
- axios (HTTPリクエストを送信するため)
- react react-dom (実装のため)
以下のコマンドでインストールできます。
npm i @material-ui/core @material-ui/icons axios react react-dom
HTTPリクエストを送信する
それでは、ソースコードをポイントに絞って解説します。 まず、a-blog cms に対してHTTPリクエストを送信するソースコードをみてみましょう。
今回は axios というライブラリを使用しています。URLの末尾に /api/:module_id/
をつけることでa-blog cmsのAPIに通信できます。
// src/common.js import axios from 'axios'; // eslint-disable-next-line import/prefer-default-export export const get = (endpoint) => axios.get(`http://6zgkhz8h.ablogcms.io/field${endpoint}/api/summary_hero_index/`, { headers: { 'X-API-KEY': process.env.API_KEY }, });
ハードコーディングを回避する
上記のような API-KEY を使用するようなソースコードではAPI-KEYを直接ソースコードに書くことは望ましくありません。本番環境と開発環境では API-KEY が異なることがあったり、Githubなどでソースコードを公開する時に、API-KEYは知られたくない情報だからです。
こういった本来プログラムの中に記述すべきでない情報を、直接ソースコードに書くことをハードコーディング と呼びます。
このようなハードコーディングを解決したい場合に有効なのが環境変数です。環境変数を利用することで、API-KEY等を直接ソースコードに書かなくて済みます。
今回は、環境変数を手軽に使用するための npm ライブラリ webpack-dotenv
というライブラリを利用して 環境変数を使用します。
まず、 webpack-dotenv
を npm でインストールします。
npm i -D webpack-dotenv
使い方は簡単で webpackの設定ファイルに下記のコードを追加します。
// webpack.prod.js
const Dotenv = require('dotenv-webpack'); // ←ここを追加
module.exports = {
// other settings...
plugins: [
// other plugins...
new Dotenv(), // ←ここを追加
],
};
webpackの設定ができたら環境変数を定義するファイルをルートディレクトリ(package.json
がある場所)に作成します。コマンドで作成する方はこちら↓
touch .env
作成できたら .env
ファイルに環境変数を定義します。
// .env
API_KEY = 任意のAPI-KEY
これで設定は完了です。 プログラム内では process.env..envファイルで定義した環境変数の左辺
と記述することで環境変数を呼び出すことができます。
この場合だと、下記のようになります。
// env-test.js
console.log(process.env.API_KEY); // output: 任意のAPI-KEY
これで、API-KEYをソースコードに直接書かなくても良くなりました!!
webpack-dotenv
を使用する際の注意点として、必ず .env
ファイルを .gitignore
するのを忘れないことです。これを忘れてしまうと、Githubにプッシュした際にリポジトリに登録されてしまい、環境変数の中身がネット上に公開されてしまい、せっかく環境変数を利用した意味がなくなってしまいます。
ただこれだと、CI/CDなどを使用して、リモートリポジトリ上でプログラムを動作させたい場合に困ってしまう。そういった場合はCI/CDには環境変数を設定できる機能が備わっている場合が多いので、リモートリポジトリ上ではそちらを使用してください。
global state の定義
次にReactの標準機能である Context という機能を使用して global state を定義します。ここで conditions
と conditions
を変更する関数 setConditions
を定義しています。
// src/store.js
import React, { useState, createContext, useContext } from 'react';
const ConditionsContext = createContext({
conditions: {},
setConditions: () => {},
});
const ConditionsProvider = ({ children }) => {
const [conditions, setConditions] = useState({});
return (
<ConditionsContext.Provider value={{ conditions, setConditions }}>
{children}
</ConditionsContext.Provider>
);
};
const useConditions = () => useContext(ConditionsContext);
export { ConditionsProvider, useConditions };
これで、ConditionsProvider
コンポーネントでラップしたすべてのコンポーネントで conditions
と setConditions
を使用することができるようになりました。
フィルタリング条件の入力
global state が定義できたら、実際にフィルタリング条件の入力をおこなう src/components/AccordionItem.js
をみていきましょう。
// src/components/AccordionItem.js
// ...省略
const AccordionItem = ({ field, keyword }) => {
const classes = useStyles();
const { conditions, setConditions } = useConditions();
// フィルターされたかどうか
const [filtered, setFiltered] = useState(false);
const handleClick = () => {
const keywords = conditions[field] ? conditions[field].keywords : [];
if (!filtered) {
const added = { [field]: { field, keywords: [...keywords, keyword] } };
setConditions({ ...conditions, ...added });
} else if (conditions[field].keywords.length === 1) {
const { [field]: _, ...rest } = conditions;
// delete conditions[field];
setConditions(rest);
} else {
const deleted = {
[field]: { field, keywords: keywords.filter((k) => k !== keyword) },
};
setConditions({ ...conditions, ...deleted });
}
setFiltered(!filtered);
};
return (
<ListItem
button
className={`${classes.nested} ${filtered ? classes.isFillterd : ''}`}
onClick={handleClick}
>
<ListItemText primary={keyword} />
</ListItem>
);
};
export default AccordionItem;
簡単に解説すると、このコンポーネントは field
と keyword
という情報を持っています。例えば、field
が 'gender
'
、 keyword
が '
Male
'
だとします。
そして、10行目の const [filtered, setFiltered] = useState(false);
というところで、このコンポーネントが持っている field
と keyword
はフィルター済みかどうかという状態を定義しています。
そして、下記の handleClick
という関数内で AccordionItem
コンポーネントがクリックされたときの動作を定義しています。
const handleClick = () => {
const keywords = conditions[field] ? conditions[field].keywords : [];
if (!filtered) {
const added = { [field]: { field, keywords: [...keywords, keyword] } };
setConditions({ ...conditions, ...added });
} else if (conditions[field].keywords.length === 1) {
const { [field]: _, ...rest } = conditions;
// delete conditions[field];
setConditions(rest);
} else {
const deleted = {
[field]: { field, keywords: keywords.filter((k) => k !== keyword) },
};
setConditions({ ...conditions, ...deleted });
}
setFiltered(!filtered);
};
フィルター済みでなければ、AccordionItem
が持つ field
を key
に指定し、value
を { field: field, keywords: [ keyword ] }
で指定したプロパティを conditions
に追加しています。
フィルター済みである、かつ keywords
の配列の要素の数が1つであれば、 オブジェクトの key
毎削除しています。
フィルター済みである、かつ keywords
の配列の要素の数が1つでなければ、AccordionItem
が持つ field
を key
にもつオブジェクトの keywords
から keyword
を削除しています。
ヒーローの特徴の情報を表示する
次に、実際にヒーローの特徴の情報を表示するコンポーネントをみていきます。src/components/HeroesIndex.js
です。
// src/components/HeroesIndex.js
const HeroesIndex = () => {
const { conditions } = useConditions();
const [heroes, setHeroes] = useState([]);
const [isShowMore, setIsShowMore] = useState(true);
const [isError, setIsError] = useState(false);
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
const endpoint = Object.values(conditions)
.map((obj) => `/${obj.field}/${obj.keywords.join('/')}`)
.join('/_and_');
setIsLoaded(false);
get(endpoint).then(
({ data: { 'entry:loop': result, lastPage } }) => {
setIsError(false);
setIsLoaded(true);
setHeroes(Array.isArray(result) ? result : [result]);
setIsShowMore(!!lastPage);
},
() => {
setIsLoaded(true);
setIsError(true);
},
);
}, [conditions]);
if (isError) {
return (
<div style={style}>
<div>該当するヒーローは存在しません。</div>
</div>
);
}
if (!isLoaded) {
return (
<div style={progress}>
<CircularProgress size="80px" />
</div>
);
}
return (
<div style={style}>
{heroes.length > 0
&& heroes.map((hero) => <HeroCard hero={hero} key={hero.eid} />)}
{isShowMore ? (
<ShowMoreButton
setHeroes={setHeroes}
setIsShowMore={setIsShowMore}
text="もっと見る"
/>
) : (
''
)}
</div>
);
};
export default HeroesIndex;
簡単に解説します。 まずは、エンドポイントの作成です。↓の部分でエンドポイントを作成しています。
const endpoint = Object.values(conditions)
.map((obj) => `/${obj.field}/${obj.keywords.join('/')}`)
.join('/_and_');
次に、実際にAPIに対してHTTPリクエストを送信するところです。Reactの標準機能で useEffect
の第2引数に配列の形で、値を書いておくと、その値が更新されるたびに useEffect
関数を実行してくれます。
この場合は conditions
が更新されるごとにHTTPリクエストを送信したいので、[conditions]
と記述しています。
useEffect(() => {
// 省略...
get(endpoint).then(
({ data: { 'entry:loop': result, lastPage } }) => {
setIsError(false);
setIsLoaded(true);
setHeroes(Array.isArray(result) ? result : [result]);
setIsShowMore(!!lastPage);
},
() => {
setIsLoaded(true);
setIsError(true);
},
);
}, [conditions]);
a-blog cms の APIにHTTPリクエストを送ると↓のようなレスポンスが返ってきますが、今回必要なのは entry:loop
と lastPage
だけなので、そのデータだけ JavaScript の機能である分割代入を使って抜き出しています。
// Entry_SummaryのAPIをGETしたときのレスポンスの例
moduleField":[],
"noimage":[
{
"noImgX": 100,
"noImgY": 100
},
省略...
"entry:loop":[
{"permalink": "http://6zgkhz8h.ablogcms.io/hero/entry-751.html", "title": "Wonder Woman",…},
{"permalink": "http://6zgkhz8h.ablogcms.io/hero/entry-726.html", "title": "Violator",…},
{"permalink": "http://6zgkhz8h.ablogcms.io/hero/entry-681.html", "title": "T800",…},
省略...
],
"unit:loop":[
[],
[],
[]
],
"page:loop":[
{
"page": 1,
"pageCurAttr": " class=\"cur\"",
"glue":[]
},
{"page": 2, "glue":[], "link#front":{"url": "http://6zgkhz8h.ablogcms.io/page/2/"…},
{"page": 3, "glue":[], "link#front":{"url": "http://6zgkhz8h.ablogcms.io/page/3/"…},
{
"page": 4,
"link#front":{
"url": "http://6zgkhz8h.ablogcms.io/page/4/"
},
"link#rear":[]
}
],
"forwardLink":{
"url": "http://6zgkhz8h.ablogcms.io/page/2/",
"forwardNum": 10,
"forwardPage": 2
},
"indexUrl": "http://6zgkhz8h.ablogcms.io/hero/",
"indexBlogName": "a-blog cms",
"blogName": "a-blog cms",
"blogCode": "",
"blogUrl": "http://6zgkhz8h.ablogcms.io/",
"indexCategoryName": "ヒーロー",
"categoryName": "ヒーロー",
"categoryCode": "hero",
"categoryUrl": "http://6zgkhz8h.ablogcms.io/hero/",
"itemsAmount": 734,
"itemsFrom": 1,
"itemsTo": 10,
"lastPageUrl": "http://6zgkhz8h.ablogcms.io/page/74/",
"lastPage": 74
}
上記の JSON のデータから、entry:loop
のデータを result
という変数に入れて、HeroesIndex
コンポーネントで定義している heroes
という state
を result
で更新します。
このとき、heroes
の中身は↓のようになっています。
[
{"permalink": "http://6zgkhz8h.ablogcms.io/hero/entry-751.html", "title": "Wonder Woman",…},
{"permalink": "http://6zgkhz8h.ablogcms.io/hero/entry-726.html", "title": "Violator",…},
{"permalink": "http://6zgkhz8h.ablogcms.io/hero/entry-681.html", "title": "T800",…},
省略...
]
そして↓で下記のコードで heroes
を元に HeroCard
をレンダリングしています。
{heroes.length > 0
&& heroes.map((hero) => <HeroCard hero={hero} key={hero.eid} />)}
また、ここが a-blog cms の便利なところなのですが、管理画面 > モジュールIDの設定画面 > 表示設定で “エントリーがない場合のHTTP応答” の “リクエストに404 Not Foundとして応答する” にチェックをつけると、対象となるエントリー、つまり entry:loop
の値がない場合には404のステータスコードを返してくれるところです。
この仕様があることで JavaScriptでエラーハンドリングをすることができます。実際に講座のサンプルコード内でも以下のようにエラーハンドリングを行っています。
get(endpoint).then(
({ data: { 'entry:loop': result, lastPage } }) => {
setIsError(false);
setIsLoaded(true);
setHeroes(Array.isArray(result) ? result : [result]);
setIsShowMore(!!lastPage);
},
// ここからエラー処理
() => {
setIsLoaded(true);
setIsError(true);
},
);
エラーだった場合は以下のように ”該当するヒーローは存在しません。” という文を表示するようにしています。
if (isError) {
return (
<div style={style}>
<div>該当するヒーローは存在しません。</div>
</div>
);
}
お疲れさまでした。以上で今回のカスタマイズは終了となります。今回はポイントに絞って解説させていただきました。ソースコードの中身を詳しく知りたいという方は、繰り返しにはなりますが、↓のリポジトリをみていただけると、今回使用したソースコードがすべてアップロードされていますので、そちらを御覧ください。
GitHub
2021年9月11日(土)開催のa-blog cms Training Camp 2021 Autumnのカスタマイズ講座「API機能を使って、画面遷移なしで、エントリーのフィルタリングをしよう」で使用されるサンプルコード - ...
終わりに
実は今回の講座のデモにはAPI機能を利用した実装がもう1機能実装されています。デモを見ていただいたらわかるかと思いますが、「もっと見るボタン」の実装です。講座では解説できませんでしたが、ソースコード自体は src/components/ShowMoreButton.js にありますので、興味のある方はGithubを確認していただけると幸いです。また、自分でもっとAPI機能を使って何かを実装してみたい!という方は ヒーローの身長(height)や体重(weight)、名前のアルファベットによるソート機能、キーワード検索機能を追加で実装してみると勉強になると思います。ぜひご自分の手で実装してみてください!