jsapi-filters について
jsapi-filters は、埋め込み Domo コンテンツにクライアント側フィルターを追加するスキルです。再読み込みせずにフィルターを更新するホストページを開発する人向けです。
主な機能
- 01MessagePort 経由で埋め込み内容をフィルタリング
- 02フィルターの一斉送信や iframe の指定
- 03ドリルやフィルター変更イベントの購読
- 04App Studio へのコンテキストデータ送信
- 05アプリ準備完了やフレームサイズ変更の処理
活用シーン
- 01ドロップダウンや日付選択を Domo 埋め込みに接続
- 02複数の埋め込み間でフィルターを同期
- 03ドリルのフィルターを別の埋め込みに適用
- 04ユーザーや顧客のコンテキストを App Studio に渡す
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ stahura/ domo-ai-vibe-rules --skill jsapi-filtersインストール後、Claude Code で/jsapi-filtersを入力して読み込まれたか確認します。
同梱ファイルreferences/rpc-methods.md
詳細
Domo クライアントサイドフィルター(JS API)
埋め込みの読み込み後、ホストページから埋め込み Domo コンテンツを動的にフィルタリングします。埋め込みトークンに組み込まれるサーバーサイドのプログラマティックフィルターとは異なり、クライアントサイドフィルターはトークンを再取得せずに即座に更新されます。
用途:UI によるフィルタリング(ドロップダウン、日付ピッカー)、ドリルダウンへの応答、複数の埋め込み間でのフィルター同期、App Studio アプリへのコンテキストの受け渡し。サーバーサイドフィルターについては、programmatic-filters を参照してください。
仕組み
Domo の iframe は MessagePort を介してホストページと通信します(JSON-RPC 2.0)。
- Domo の iframe が
MessagePortとreferenceIdを含むmessageイベントを送信します - ホストページがポートを取得し、メッセージをリッスンします
- ホストがポート経由で RPC 呼び出し(フィルター)を送信し、Domo はイベント(ドリル、フィルター変更)を返します
セットアップ
ステップ 1:JS API リスナーを初期化する
グローバルリスナーを設定し、各 iframe の MessagePort ハンドシェイクを取得します。
// lib/jsapi.ts
const ports: Record<string, MessagePort> = {};
export interface DomoRpcMessage extends MessageEvent {
data: {
referenceId?: string;
method?: string;
params?: any;
result?: any;
error?: any;
id?: string;
jsonrpc?: string;
};
}
export const initializeJsApi = () => {
if (typeof window === 'undefined') return;
const handleMessage = (e: MessageEvent) => {
if (!e.ports || !e.ports[0]) return;
const referenceId = e.data?.referenceId;
if (!referenceId) return;
const port = e.ports[0];
port.start();
// Store the port for sending commands later
ports[referenceId] = port;
// Listen for events from this iframe
port.onmessage = (event: MessageEvent) => {
const data = event.data;
if (!data) return;
if (data.method) {
handleRpcMethod(referenceId, data.method, data.params);
}
if (data.error) {
console.error(`[JsApi] RPC error (ref: ${referenceId}):`, data.error);
}
};
};
window.addEventListener('message', handleMessage);
return () => {
window.removeEventListener('message', handleMessage);
};
};
ステップ 2:初期化処理を早めにマウントする
Domo の iframe が読み込まれる前に(アプリのルートで)マウントします。React/Next.js の場合:
// components/DomoJsApiInitializer.tsx
'use client'
import { useEffect } from 'react'
import { initializeJsApi } from '@/lib/jsapi'
export default function DomoJsApiInitializer() {
useEffect(() => {
const cleanup = initializeJsApi()
return cleanup
}, [])
return null
}
続いて、ルートレイアウトに含めます。
// app/layout.tsx
import DomoJsApiInitializer from './components/DomoJsApiInitializer'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<DomoJsApiInitializer />
{children}
</body>
</html>
)
}
ステップ 3:埋め込みの許可ドメインを設定する
Domo の Admin > Network Security > Embed Authorized Domains にホストドメインを追加します。設定しないと、iframe はハンドシェイクの message イベントを送信しません。カスタム Domo アプリの場合は、Custom Apps Authorized Domains にも追加します。
ステップ 4:埋め込みトークンを使用する(公開 URL は使用しない)
JS API は認証済みの埋め込み(トークンベースの POST フロー)でのみ動作します。公開埋め込み URL では MessagePort ハンドシェイクが開始されません。トークンのフローについては、programmatic-filters を参照してください。
フィルターの適用
保存した MessagePort 経由で /v1/filters/apply を送信します。
フィルターの構造
クライアントサイドフィルターでは operand を使用します(サーバーサイドフィルターのように operator は使用しません。これは最もよくあるバグです)。
interface ClientFilter {
column: string;
operand: string; // 'IN', 'NOT_IN', 'EQUALS', 'BETWEEN', 'CONTAINS', etc.
values: any[];
dataType?: string; // 'STRING' | 'NUMBER' | 'DATE' — disambiguates values
dataSourceId?: string; // target a specific dataset in multi-dataset embeds
}
applyFilters 関数
export const applyFilters = (
filters: Array<{ column: string; operand: string; values: any[] }> = []
) => {
Object.values(ports).forEach(port => {
port.postMessage({
id: `filter-${Date.now()}`,
jsonrpc: '2.0',
method: '/v1/filters/apply',
params: {
filters
}
});
});
};
この関数は、接続されているすべての Domo iframe にフィルターをブロードキャストします。特定の iframe を対象にするには、referenceId を使用します。
export const applyFiltersToEmbed = (
referenceId: string,
filters: Array<{ column: string; operand: string; values: any[] }> = []
) => {
const port = ports[referenceId];
if (!port) {
console.warn(`[JsApi] No port found for referenceId: ${referenceId}`);
return;
}
port.postMessage({
id: `filter-${Date.now()}`,
jsonrpc: '2.0',
method: '/v1/filters/apply',
params: { filters }
});
};
例
単一の値でフィルタリングする:
applyFilters([
{ column: 'Region', operand: 'IN', values: ['West'] }
]);
複数のフィルター(AND 条件):
applyFilters([
{ column: 'Region', operand: 'IN', values: ['West', 'East'] },
{ column: 'Year', operand: 'EQUALS', values: [2025] }
]);
すべてのフィルターをクリアする:
applyFilters([]);
React の例 — フィルタードロップダウン
'use client'
import { useState } from 'react'
import { applyFilters } from '@/lib/jsapi'
const regions = ['All', 'West', 'East', 'North', 'South']
export default function RegionFilter() {
const [selected, setSelected] = useState('All')
const handleChange = (region: string) => {
setSelected(region)
if (region === 'All') {
applyFilters([])
} else {
applyFilters([
{ column: 'Region', operand: 'IN', values: [region] }
])
}
}
return (
<select value={selected} onChange={(e) => handleChange(e.target.value)}>
{regions.map(r => (
<option key={r} value={r}>{r}</option>
))}
</select>
)
}
Domo からのイベントの処理
Domo は MessagePort 経由でイベントを返します。リスナーを適切に管理するため、登録と解除のパターンを使用します。
// Event subscription pattern
type EventCallback = (referenceId: string, params: any) => void;
const listeners: Record<string, Set<EventCallback>> = {};
export function on(event: string, callback: EventCallback): () => void {
if (!listeners[event]) listeners[event] = new Set();
listeners[event].add(callback);
return () => { listeners[event].delete(callback); };
}
次に、handleRpcMethod で登録済みリスナーにイベントを振り分けます。中央の switch で処理します。
function handleRpcMethod(referenceId: string, method: string, params: any) {
switch (method) {
case '/v1/onDrill':
handleDrill(referenceId, params);
break;
case '/v1/onFiltersChange':
handleFiltersChange(referenceId, params);
break;
case '/v1/onAppReady':
handleAppReady(referenceId, params);
break;
case '/v1/onAppData':
handleAppData(referenceId, params);
break;
case '/v1/onFrameSizeChange':
handleFrameResize(referenceId, params);
break;
default:
break;
}
}
/v1/onDrill — ドリルイベント
ユーザーがドリル可能な要素をクリックしたときに発生します。ドリルの経路を表すフィルターが含まれます。
function handleDrill(referenceId: string, params: any) {
const drillFilters = params?.filters;
// drillFilters: [{ column: 'Region', operand: 'IN', values: ['West'] }]
// Example: apply the drill filters to another embed on the page
applyFiltersToEmbed('other-embed-ref', drillFilters);
// Example: navigate to a detail page
router.push(`/details?region=${drillFilters[0]?.values[0]}`);
}
/v1/onFiltersChange — フィルター変更イベント
埋め込み内でフィルターが変更されたとき(ユーザー操作、カード間の操作、またはプログラムによる変更)に発生します。ホスト UI の同期に使用します。
function handleFiltersChange(referenceId: string, params: any) {
const currentFilters = params?.filters;
// currentFilters: [{ column: 'Region', operand: 'IN', values: ['West'] }]
// Example: update your app's filter UI to reflect what's selected in Domo
setActiveFilters(currentFilters);
// Example: sync filters to another embed
applyFiltersToEmbed('second-dashboard-ref', currentFilters);
}
/v1/onAppReady — App Studio の準備完了
App Studio アプリの読み込みが完了したときに発生します。overrideFilters=true の場合は特に重要です。このイベントの後にのみフィルターを送信してください。
function handleAppReady(referenceId: string, params: any) {
// Now safe to send filters and appData
applyFiltersToEmbed(referenceId, initialFilters);
sendAppData(referenceId, { customerId: '12345' });
}
/v1/onAppData — Domo からのアプリデータ
App Studio アプリがホストにデータを返したときに発生します。
function handleAppData(referenceId: string, params: any) {
const appData = params?.appData;
// Handle data sent from the App Studio app
console.log('App data received:', appData);
}
/v1/onFrameSizeChange — フレームのサイズ変更
埋め込みコンテンツのサイズが変更されたときに発生します。iframe コンテナーの調整に使用します。
function handleFrameResize(referenceId: string, params: any) {
const { width, height } = params || {};
const iframe = document.querySelector(`iframe[name="${referenceId}"]`);
if (iframe && height) {
(iframe as HTMLIFrameElement).style.height = `${height}px`;
}
}
App Studio アプリへのデータ送信
/v1/appData/apply を介して、任意のコンテキストを App Studio アプリに渡します。
export const sendAppData = (referenceId: string, appData: any) => {
const port = ports[referenceId];
if (!port) return;
port.postMessage({
id: `appData-${Date.now()}`,
jsonrpc: '2.0',
method: '/v1/appData/apply',
params: { appData }
});
};
埋め込みアプリは、domo.onAppData() を介してこのデータを受け取ります。ユーザー ID、顧客コンテキスト、設定などの受け渡しに使用します。
PFilter URL パラメーター
JavaScript を使わない代替手段です。URL クエリパラメーターとしてフィルターを追加します。トレードオフとして、iframe 全体が再読み込みされ、フィルターが URL に表示されて変更可能になります。
構造
?pfilters=[{"column":"ColumnName","operand":"OPERATOR","values":["val1","val2"]}]
例
https://example.domo.com/embed/pages/private/ABCDE?pfilters=[{"column":"Region","operand":"IN","values":["West","East"]},{"column":"Year","operand":"EQUALS","values":["2025"],"dataType":"NUMBER"}]
オプションのフィールド
dataSourceId— 特定のデータセットを対象にしますdataType—STRING、NUMBER、DATE
PFilters と JS API の使い分け
| PFilters | JS API | |
|---|---|---|
| 必要な設定 | なし | MessagePort の初期化 |
| 速度 | 遅い(iframe の再読み込み) | 即時(再読み込みなし) |
| セキュリティ | URL に表示され、ユーザーが変更可能 | DOM に表示され、ユーザーが変更可能 |
| 複数回の更新 | 変更のたびに iframe を再読み込み | その場で更新 |
| 最適な用途 | シンプルな単発のフィルター、リンク共有 | インタラクティブなフィルタリング、動的な UI |
セキュリティに関する警告: PFilters は URL に表示され、エンドユーザーが変更できます。セキュリティ対策にはなりません。データを分離するには、サーバーサイドのプログラムによるフィルターを使用してください。
App Studio の overrideFilters
overrideFilters=true URL パラメーターを指定すると、App Studio アプリは保存済みのデフォルトを無視し、ホストからフィルターが送信されるのを待つようになります。
https://example.domo.com/embed/pages/private/ABCDE?overrideFilters=true
動作:
overrideFilters=true— アプリはフィルターなしで読み込まれ、ホストからの送信を待ちますoverrideFilters=falseまたは省略 — アプリは保存済みのデフォルトで通常どおり読み込まれます
重要: アプリが待機状態のままにならないよう、少なくとも 1 回 /v1/filters/apply(空の [] でも可)を送信する必要があります。最初に /v1/onAppReady をリッスンしてください。
function handleAppReady(referenceId: string, params: any) {
// App is ready — send the initial filter state
applyFiltersToEmbed(referenceId, userFilters.length > 0 ? userFilters : []);
}
埋め込みコンテンツで役立つその他の URL パラメーター:
| パラメーター | 目的 |
|---|---|
transparentBackground=true | ダッシュボードの背景を透明にします |
appData=value | App Studio アプリに初期コンテキストを渡します(スペースは +) |
viewId=abc123 | 表示する App Studio のページビューを指定します |
既知の RPC メソッド
ホスト → Domo(コマンド)
| メソッド | 説明 |
|---|---|
/v1/filters/apply | 実行時フィルターを適用またはクリアします |
/v1/appData/apply | コンテキストデータを App Studio アプリに送信します |
Domo → ホスト(イベント)
| メソッド | 説明 |
|---|---|
/v1/onDrill | ユーザーがデータポイントをドリルダウンしました |
/v1/onFiltersChange | 埋め込み内のフィルターが変更されました |
/v1/onAppReady | App Studio アプリの読み込みが完了し、入力を受け付ける状態になりました |
/v1/onAppData | App Studio アプリがホストにデータを返しました |
/v1/onFrameSizeChange | 埋め込みコンテンツのサイズが変更されました |
サーバーサイドフィルターとの併用
- サーバーサイド: セキュリティ境界です。Domo によって適用され、ユーザーが回避することはできません。データの分離に使用します。
- クライアントサイド: UX を便利にするためのものです。ユーザーが変更できます。インタラクティブなフィルタリングに使用します。
クライアントサイドフィルターは、サーバーサイドで許可されたデータの範囲内で絞り込みます。サーバーサイドの制限を上書きすることはできません。
注意点とベストプラクティス
ポートが使用可能になるタイミング
ハンドシェイクが完了する前に applyFilters を呼び出すと、エラーなしで失敗します。対処方法は 2 つあります。
ダッシュボード/カード: 接続時にキューをフラッシュします。
const pendingFilters: Record<string, Array<{ column: string; operand: string; values: any[] }>> = {};
// When storing the port after handshake, flush pending filters
ports[referenceId] = port;
if (pendingFilters[referenceId]) {
applyFiltersToEmbed(referenceId, pendingFilters[referenceId]);
delete pendingFilters[referenceId];
}
App Studio アプリ: 最初に /v1/onAppReady をリッスンしてから、フィルターを送信します。overrideFilters=true を指定する場合は必須です。
operand と operator
クライアントサイドでは operand、サーバーサイドでは operator を使用します。取り違えるとエラーなしで失敗します。
referenceId のマッピング
referenceId は Domo iframe によって割り当てられるため、制御できません。複数の埋め込みでは、ハンドシェイクのタイミングとレンダリング順序を対応づけてください。単一の埋め込みでは、ブロードキャストする applyFilters() を使用してください。
Domo API の演算子のタイプミス
GREATER_THAN_EQUALS_TO が GREAT_THAN_EQUALS_TO(ER が欠落)と表記されている場合があります。比較演算子が機能しない場合は、両方の表記を試してください。
複数の iframe
各 iframe には個別のポートが割り当てられます。applyFilters はすべてにブロードキャストします。個別にフィルタリングする場合は、特定の referenceId を指定して applyFiltersToEmbed を使用してください。
パブリック埋め込みは JS API をサポートしない
MessagePort ハンドシェイクを開始するのは、認証済み(トークンベース)の埋め込みのみです。パブリック埋め込み URL では機能しません。
サポートされるオペランド
IN、NOT_IN、EQUALS、NOT_EQUALS、GREATER_THAN、GREATER_THAN_EQUALS_TO、LESS_THAN、LESS_THAN_EQUALS_TO、BETWEEN(値はちょうど 2 つ)、CONTAINS(文字列の部分一致)。
クライアントサイドでは SQL フィルターを使用できない
/v1/filters/apply がサポートするのは標準のフィルターオブジェクトのみです。SQL フィルターはサーバーサイドでのみ使用できます。
セキュリティ
クライアント側のフィルターと pfilters は UX 専用です。エンドユーザーから見えており、変更も可能です。データを分離するには、サーバー側のプログラムによるフィルターを使用してください。
TypeScript 型定義
type ClientFilterOperand =
| 'IN'
| 'NOT_IN'
| 'EQUALS'
| 'NOT_EQUALS'
| 'GREATER_THAN'
| 'GREATER_THAN_EQUALS_TO'
| 'LESS_THAN'
| 'LESS_THAN_EQUALS_TO'
| 'BETWEEN'
| 'CONTAINS';
interface ClientFilter {
column: string;
operand: ClientFilterOperand;
values: (string | number | boolean)[];
dataType?: 'STRING' | 'NUMBER' | 'DATE';
dataSourceId?: string;
}
interface DomoRpcRequest {
id: string;
jsonrpc: '2.0';
method: string;
params: Record<string, any>;
}
interface DomoRpcEvent {
method: string;
params?: Record<string, any>;
result?: any;
error?: { code: number; message: string };
}
interface DrillEventParams {
filters: ClientFilter[];
}
interface FiltersChangeParams {
filters: ClientFilter[];
}
interface FrameSizeChangeParams {
width: number;
height: number;
}
interface AppDataParams {
appData: any;
}
クイックリファレンス
RPC メソッドのリファレンスとメッセージ形式の詳細については、references/rpc-methods.md をお読みください。