はじめに
ZeroCode.jsは、フレームワーク非依存のWeb ComponentsベースのCMSエディターライブラリです。Vue.jsで実装されており、カスタムHTMLテンプレート構文を使用して動的なコンテンツ管理を提供します。
クイックスタート
ZeroCode.jsをすぐに使い始めるための基本的な手順です。
インストール
npm install zerocodejs
ZeroCode.jsは内部でVue 3を使用しています。npm 7以降では、peer
dependenciesが自動的にインストールされるため、npm install zerocodejsだけでVueも一緒にインストールされます。
注意:
npm 6以前を使用している場合は、明示的にnpm install zerocodejs vueを実行してください。
基本的な使用例
CDNを使用した最も簡単な例(HTMLファイルをブラウザで開くだけで動作):
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://unpkg.com/zerocodejs/dist/zerocodejs.css">
</head>
<body>
<zcode-editor></zcode-editor>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://unpkg.com/zerocodejs/dist/zerocode.umd.js"></script>
</body>
</html>
npmを使用した例:
<zcode-editor></zcode-editor>
<script type="module">
import 'zerocodejs';
</script>
zcode-editorはエンジニア・デザイナー向けの管理画面で、パーツ管理・画像管理・データビューアが利用できます。はじめての方におすすめです。
zcode-cmsはエンドユーザー向けの管理画面で、編集・追加・削除・並べ替えのみが利用できます。
複数インスタンス対応
ZeroCode.jsは、同じページに複数のzcode-cmsやzcode-editorインスタンスを配置することができます。各インスタンスは独立したデータを管理し、互いに影響を与えません。
<!-- インスタンス1 -->
<zcode-cms id="cms-1">
<link slot="css" rel="stylesheet" href="/css/common.css" />
</zcode-cms>
<!-- インスタンス2 -->
<zcode-cms id="cms-2">
<link slot="css" rel="stylesheet" href="/css/common.css" />
</zcode-cms>
<script type="module">
import 'zerocodejs';
// 各インスタンスに独立したデータを設定
const cms1 = document.getElementById('cms-1');
cms1.setAttribute('page', JSON.stringify([...]));
cms1.setAttribute('parts-common', JSON.stringify([...]));
const cms2 = document.getElementById('cms-2');
cms2.setAttribute('page', JSON.stringify([...]));
cms2.setAttribute('parts-common', JSON.stringify([...]));
</script>
動作:
-
独立したデータ管理:
各インスタンスは
id属性で識別され、独立したデータを管理します - セッションストレージの分離: セッションストレージはインスタンスIDごとに分離され、データが混在することはありません
- イベントリスナーの分離: 各インスタンスのイベントリスナーは独立して動作します
-
IDの自動生成:
id属性が指定されていない場合、自動的に一意のIDが生成されます
注意:
複数インスタンスを使用する場合は、各インスタンスに一意のid属性を指定することを推奨します。これにより、データの管理が明確になり、デバッグも容易になります。
CDN経由で使用する場合
CDN経由で使用する場合は、Vueを先に読み込む必要があります。
<!-- Vueを先に読み込む -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- ZeroCode.jsを読み込む -->
<script src="https://unpkg.com/zerocodejs/dist/zerocode.umd.js"></script>
<link rel="stylesheet" href="https://unpkg.com/zerocodejs/dist/style.css">
<zcode-cms id="cms">
<link slot="css" rel="stylesheet" href="/css/common.css" />
</zcode-cms>
<script>
const cms = document.getElementById('cms');
cms.setAttribute('page', JSON.stringify([]));
cms.setAttribute('parts-common', JSON.stringify([]));
cms.setAttribute('parts-individual', JSON.stringify([]));
cms.setAttribute('parts-special', JSON.stringify([]));
cms.setAttribute('images-common', JSON.stringify([]));
cms.setAttribute('images-individual', JSON.stringify([]));
cms.setAttribute('images-special', JSON.stringify([]));
</script>
Shadow DOM
ZeroCode.jsは、デフォルトでShadow DOMを使用してCSS/JSを完全に分離します。これにより、呼び出し側のCSSやJavaScriptとの競合を防ぎ、副作用のない安全な統合を実現します。
Shadow DOMの利点
- CSS分離: ZeroCode.jsのスタイルが呼び出し側のCSSに影響を与えません
- JavaScript分離: Shadow DOM内のスクリプトが外部のJavaScriptと競合しません
- セキュリティ: 外部からのスタイルやスクリプトの干渉を防ぎます
Shadow DOMの無効化
use-shadow-dom="false"属性を指定することで、Shadow DOMを無効化できます(Light
DOMモード)。
<zcode-cms id="cms" use-shadow-dom="false">
<link slot="css" rel="stylesheet" href="/css/common.css" />
</zcode-cms>
注意: Shadow DOMを無効化すると、CSSやJavaScriptの競合が発生する可能性があります。通常はShadow DOMを使用することを推奨します。
Shadow DOM内でのjQuery使用
ZeroCode.jsは、Shadow DOM内でjQueryを使用する場合に自動的に拡張を行います。これにより、Shadow DOM内の要素に対してjQueryのセレクターやイベントハンドラーが正常に動作します。
テンプレート記法
ZeroCode.jsでは、カスタムHTMLテンプレート構文を使用して動的なコンテンツを定義します。
フィールド記法
テキストフィールド
{$fieldName:defaultValue}
単一行のテキスト入力フィールドとして表示されます。
<h1>{$title:タイトル}</h1>
テキストエリアフィールド
{$fieldName:defaultValue:textarea}
複数行のテキスト入力フィールドとして表示されます。
<p>{$description:説明文:textarea}</p>
リッチテキストフィールド
{$fieldName:defaultValue:rich}
リッチテキストエディター(TipTap)として表示されます。HTMLタグを含むテキストを編集できます。
<div>{$content:本文:rich}</div>
画像フィールド
{$fieldName:defaultValue:image}
画像選択フィールドとして表示されます。画像管理から画像を選択できます。
<img src="{$image:default.jpg:image}" alt="画像" />
グループ化されたフィールド
フィールド名にドット(.)を使用して、フィールドをグループ化できます。
{$fieldName.groupName:defaultValue}
例:
<div>
<h2>{$hero.title:ヒーロータイトル}</h2>
<p>{$hero.description:ヒーロー説明}</p>
</div>
グループ化されたフィールドは、編集パネルでグループとして表示され、整理された編集が可能です。
オプショナルフィールド(空入力制御)
フィールド名の後に?を追加することで、オプショナルフィールドとして定義できます。ユーザーが何も入力しなかった場合、フィールドの値はundefinedになります。
{$fieldName?:defaultValue}
{$fieldName?:defaultValue:rich}
{$fieldName?:defaultValue:image}
{$fieldName.groupName?:defaultValue}
例:
<div class="section">
<div class="section__title">{$title:タイトル(必須)}</div>
<div class="section__subtitle">{$subtitle?:サブタイトル(オプション)}</div>
<img src="{$optional_image?:default.jpg:image}" alt="{$optional_alt?:画像の説明}" />
</div>
動作:
-
ユーザーが何も入力しなかった場合、フィールドの値は
undefinedになります(デフォルト値は使用されません) - 編集パネルでは、オプショナルフィールドは空欄として表示されます
-
属性値がオプショナルフィールドのみで構成されていて、値が
undefined(または空)の場合、その属性自体がHTMLから削除されます -
基本動作では、オプショナルフィールドが空の場合でも親要素(タグ)は残ります(例:
<p></p>)。親要素ごと削除したい場合は、z-empty属性を使用してください(詳細は「条件分岐」セクションの「z-empty 属性」を参照)
例:
<!-- テンプレート -->
<img src="{$optional_image?:default.jpg:image}" alt="{$optional_alt?:画像の説明}" />
<!-- optional_imageとoptional_altが両方undefinedの場合 -->
<img />
<!-- optional_imageに値がある場合 -->
<img src="image.jpg" alt="画像の説明" />
親要素を削除したい場合:
オプショナルフィールドが空の場合、基本動作では親要素(タグ)が残ります(例:
<p></p>)。親要素ごと削除したい場合は、z-empty属性を使用してください。
<!-- 基本動作:空タグが残る -->
<p>{$subtitle?:サブタイトル}</p>
<!-- subtitleがundefinedの場合、<p></p>が残る -->
<!-- z-emptyを使用:親要素を削除 -->
<div z-empty="$subtitle">
<p>{$subtitle?:サブタイトル}</p>
</div>
<!-- subtitleがundefinedの場合、<div>要素ごと削除される -->
注意: 属性値全体がオプショナルフィールドのみで構成されている場合のみ属性が削除されます。他のテキストが含まれている場合は削除されません。
補足:
z-empty属性の詳細については、「条件分岐」セクションの「z-empty
属性」を参照してください。
バリデーション記法
フィールドにバリデーションルールを追加できます。フロントエンドでの同期バリデーションと、バックエンドでの非同期バリデーションの両方をサポートしています。
{$fieldName:defaultValue:required}
{$fieldName:defaultValue:max=100}
{$fieldName:defaultValue:required:max=50}
{$fieldName:defaultValue:readonly}
{$fieldName:defaultValue:disabled}
例:
<div class="form">
<input type="text" name="title" value="{$title:タイトル:required:max=100}" />
<input type="email" name="email" value="{$email:メールアドレス:required}" />
<textarea name="description">{$description:説明:max=500}</textarea>
<input type="text" name="readonly_field" value="{$readonly_field:読み取り専用:readonly}" />
<input type="text" name="disabled_field" value="{$disabled_field:無効化:disabled}" />
</div>
バリデーションルール:
-
:required- 必須フィールド。空の場合はエラーメッセージが表示されます。 -
:max=N- 最大文字数制限(Nは数値)。指定した文字数を超えるとエラーメッセージが表示されます。 -
:readonly- 読み取り専用フィールド。編集できませんが、値は送信されます。 :disabled- 無効化フィールド。編集できず、値も送信されません。
動作:
-
フロントエンドバリデーション(同期):
requiredとmaxは、ユーザーが入力中にリアルタイムでチェックされます。エラーがある場合は、フィールドの下にエラーメッセージが表示されます。 -
バックエンドバリデーション(非同期):
保存ボタンをクリックすると、
save-requestイベントが発火します。バックエンドでバリデーションを行い、エラーがある場合はsave-resultイベントでエラーを返してください。詳細は「保存リクエスト」セクションを参照してください。 - バリデーションエラーは、編集パネルが開いている場合は該当フィールドに表示され、編集パネルが閉じている場合は画面上部にエラーバナーが表示されます。
注意:
複雑なバリデーション(メール形式チェック、重複チェックなど)は、バックエンドで実施することを推奨します。フロントエンドでは、requiredとmaxのみをサポートしています。
バックエンドデータの参照
バックエンドから渡されたデータをテンプレート内で参照できます。動的URLや共通で使用するデータ(店舗ID、ユーザー情報など)を表示する際に便利です。
{@fieldName}
{@items[0].name}
{@items.length}
/shop/{shop_id}/products
例:
<div class="shop-header">
<h1>{@title}</h1>
<a href="{@url}">店舗詳細</a>
<p>アイテム数: {@items.length}</p>
<p>最初のアイテム: {@items[0].name}</p>
<a href="/shop/{shop_id}/products">商品一覧</a>
</div>
動作:
- 基本的なデータ参照:
{@fieldName}でバックエンドデータを参照 - ネストされたオブジェクト:
{@user.name}のようにドット記法で参照 - 配列の参照:
{@items[0]}で配列の要素を参照 - 配列のlength:
{@items.length}で配列の長さを取得 - URL内プレースホルダー:
/shop/{shop_id}/のようにURL内に直接記述可能
使用例:
<!-- Web Componentにバックエンドデータを渡す -->
<zcode-cms
backend-data='{"title":"A店舗","url":"/shop/123/","shop_id":"123","items":[{"name":"商品1"}]}'
>
<!-- テンプレート内で {@title}, {@url}, {@items[0].name} などが使用可能 -->
</zcode-cms>
注意: バックエンドデータは信頼できるソースからのみ使用してください。存在しないパスを参照した場合は空文字列が返されます。
z-for ループ記法
バックエンドデータの配列をループ表示するには、z-for属性を使用します。現在はバックエンドデータの配列のみをサポートしています。
<div z-for="item in {@items}">
<!-- ループ内のコンテンツ -->
</div>
構文:
z-for="変数名 in {@配列パス}"
例:
<!-- 店舗一覧をループ表示 -->
<div class="shop-list">
<div z-for="shop in {@shops}" class="shop-item">
<h2>{shop.name}</h2>
<p>{shop.description}</p>
<a href="{shop.url}">詳細を見る</a>
</div>
</div>
<!-- 商品一覧をループ表示 -->
<div class="product-list">
<div z-for="product in {@products}" class="product-item">
<h3>{product.name}</h3>
<p>価格: {product.price}円</p>
<p>カテゴリ: {product.category}</p>
</div>
</div>
ループ変数の参照:
-
{変数名.プロパティ名}: ループ変数のプロパティを参照(例:{shop.name}) -
{変数名.ネストされた.プロパティ}: ネストされたプロパティを参照(例:{shop.address.city}) -
{変数名}: ループ変数自体を参照(オブジェクトの場合はJSON文字列として表示)
ループ内での他のテンプレート構文:
ループ内では、通常のテンプレート構文({$field}、{@data}など)も使用できます。
<div z-for="shop in {@shops}" class="shop-item">
<h2>{shop.name}</h2>
<p>{$description:説明文}</p>
<a href="/shop/{@shop_id}/{shop.id}/">詳細</a>
</div>
制限事項:
- 現在はバックエンドデータの配列のみをサポート(コンポーネントデータの配列は非対応)
- ネストループは非対応
-
インデックス変数は非対応(
z-for="(item, index) in {@items}"のような構文は使用不可) - 空の配列の場合は、ループ要素全体が削除されます
完全な例:
<!-- HTML -->
<zcode-cms id="cms" backend-data='{"shops": [{"id": "1", "name": "A店舗", "url": "/shop/1/"}, {"id": "2", "name": "B店舗", "url": "/shop/2/"}]}'>
<link slot="css" rel="stylesheet" href="/css/common.css" />
</zcode-cms>
<!-- パーツテンプレート -->
<div class="section">
<div class="section__head">
<div class="section__title">{$title:店舗一覧}</div>
</div>
<div class="section__contents">
<div class="section__items">
<div z-for="shop in {@shops}" class="shop-item">
<h2>{shop.name}</h2>
<a href="{shop.url}">詳細を見る</a>
</div>
</div>
</div>
</div>
注意: z-forループ記法は、バックエンドデータの配列を表示するためのシンプルな実装です。将来的には、コンポーネントデータの配列やネストループなどの機能が追加される予定です。
選択肢記法
ラジオボタン(単一選択)
($fieldName:option1|option2|option3)
パイプ(|)で区切られたオプションから1つを選択します。
<div>($color:red|blue|green)</div>
チェックボックス(複数選択)
($fieldName:option1,option2,option3)
カンマ(,)で区切られたオプションから複数を選択できます。
<div>($tags:tag1,tag2,tag3)</div>
セレクトボックス(単一選択)
($fieldName@:option1|option2|option3)
アットマーク(@)とパイプ(|)で区切られたオプションから1つを選択します。
<div>($size@:S|M|L)</div>
セレクトボックス(複数選択)
($fieldName@:option1,option2,option3)
アットマーク(@)とカンマ(,)で区切られたオプションから複数を選択できます。
<div>($categories@:cat1,cat2,cat3)</div>
条件分岐
z-if は表示するかしないかを決めます。fieldName に紐づく形ではありません(fieldName に紐づくのは z-empty)。属性値に指定したキーの真偽で、要素を表示または削除します。
<element z-if="showContent">
<!-- 条件が真の場合に表示 -->
</element>
<div z-if="showContent">
<p>コンテンツが表示されます</p>
</div>
動作:
- 指定キーの値が真(
true、非空文字列など)の場合、要素を表示 -
指定キーの値が偽(
false、null、空文字列、0)の場合は要素を削除 - 指定キーが存在しない(
undefined)場合は表示として扱う
z-tag 属性(タグ名の動的変更)
<element z-tag="$tagName:h1|h2|h3">
<!-- タグ名を動的に変更 -->
</element>
HTMLタグ名を動的に変更できます。テンプレートで書いたタグ名がデフォルト値として使用されます。
<!-- 見出しタグを動的に変更(h2をデフォルト) -->
<h2 z-tag="$headingTag:h1|h2|h3" class="title">{$title:タイトル}</h2>
<!-- headingTagが"h1"の場合 → <h1 class="title">タイトル</h1> -->
<!-- headingTagが"h2"の場合 → <h2 class="title">タイトル</h2> -->
<!-- 選択肢を指定しない場合(全量表示) -->
<div z-tag="$containerTag" class="container">
{$content:コンテンツ}
</div>
動作:
-
デフォルト値: テンプレートで書いたタグ名(例:
<h2>)がデフォルト値として使用されます -
選択肢の指定:
z-tag="$tagName:h1|h2|h3"のように、パイプ(|)で区切って選択肢を指定できます - 全量表示: 選択肢を指定しない場合、すべての有効なタグが選択肢として表示されます
-
属性の保持:
タグ名が変更されても、
class、idなどの属性は保持されます - 子要素の保持: タグ名が変更されても、子要素は保持されます
対応タグ:
-
見出し:
h1,h2,h3,h4,h5,h6 - コンテナ:
div,p,span - リスト:
li,ul,ol -
セマンティック:
section,article,aside,nav,header,footer,main -
その他:
figure,figcaption,blockquote,pre,code -
テーブル:
table,thead,tbody,tr,th,td
使用例:
<!-- 見出しタグを動的に変更 -->
<h2 z-tag="$headingTag:h1|h2|h3" class="title">{$title:タイトル}</h2>
<!-- リストアイテムのタグを変更 -->
<li z-tag="$itemTag:li|div|span" class="list-item">{$item:項目}</li>
<!-- コンテナタグを変更 -->
<div z-tag="$containerTag:div|section|article" class="container">
{$content:コンテンツ}
</div>
注意: テンプレートで書いたタグ名が選択肢に含まれていない場合、開発環境では警告が表示され、選択肢の最初の値がデフォルト値として使用されます。
z-empty 属性(fieldName に紐づく・空なら親要素削除)
<element z-empty="$fieldName">
<!-- フィールドが空の場合、要素ごと削除 -->
</element>
z-empty は
$fieldName
で指定したフィールドに紐づきます。そのフィールドが空(undefined、null、空文字列、または実質的に空のrichテキスト)の場合、親要素を削除します。基本動作では空タグが残るため、親ごと消したいときだけ
z-empty を使ってください。
<!-- 基本動作:空タグが残る -->
<p>{$subtitle?:サブタイトル}</p>
<!-- subtitleがundefinedの場合、<p></p>が残る -->
<!-- z-empty:親要素を削除 -->
<div z-empty="$subtitle">
<p>{$subtitle?:サブタイトル}</p>
</div>
<!-- subtitleがundefinedの場合、<div>要素ごと削除される -->
動作:
- フィールドが
undefined、null、空文字列の場合、要素を削除 -
richテキストが実質的に空(
<p></p>、<p> </p>など)の場合も要素を削除 - フィールドに値がある場合は、要素を表示
使用例:
<!-- オプショナルフィールドで親要素を削除したい場合 -->
<div z-empty="$subtitle">
<p>{$subtitle?:サブタイトル}</p>
</div>
<!-- richテキストが空の場合も削除 -->
<div z-empty="$content">
<div>{$content?:デフォルトコンテンツ:rich}</div>
</div>
<!-- textareaフィールドでも使用可能 -->
<div z-empty="$description">
<div>{$description?:説明文:textarea}</div>
</div>
注意:
fieldName に紐づくのは z-empty です。z-if は表示 on/off
のみで、field には紐づきません。
スロット(ネスト構造)
<element z-slot="slotName">
<!-- スロットコンテンツ -->
</element>
スロットを使用して、パーツ内に他のパーツをネストできます。
<div class="features">
<div z-slot="items">
<!-- ここに子パーツが追加されます -->
</div>
</div>
スロットの制限
PartDataのslotsプロパティでallowedPartsを指定することで、スロットに追加可能なパーツを制限できます。
{
"id": "feature-list",
"title": "機能一覧",
"body": "<div z-slot=\"items\"></div>",
"slots": {
"items": {
"allowedParts": ["feature-item-1", "feature-item-2"]
}
}
}
セキュリティ注意: テンプレート構文で属性値にユーザー入力を設定する場合、基本的なエスケープ処理とURL検証が適用されますが、サーバー側での検証を必ず実装してください。
編集モード
ZeroCode.jsでは、4つの編集モードが利用できます。
edit(編集モード)
既存のコンポーネントを編集するモードです。
- コンポーネントのフィールドを編集
- 親要素への移動(「親要素を選択」ボタン)
- 同じパーツをクリックした場合はパネルを閉じる
add(追加モード)
新しいコンポーネントを追加するモードです。
- パーツタイプを選択して追加
- 既存コンポーネントをコピーして追加(表示側で選択したパーツをコピー)
- 追加位置の選択(前に追加/後に追加)
- 親要素への移動(「親要素を選択」ボタン)
- パーツ追加を続けるオプション
delete(削除モード)
コンポーネントを削除するモードです。
- 削除確認
- 親要素への移動(「親要素を選択」ボタン)
reorder(並べ替えモード)
コンポーネントの順序を変更するモードです。
- 移動元を選択
- 移動先をクリックして並べ替え
- 親要素への移動(「親要素を選択」ボタン)
親要素選択
各編集モードで使用可能な親要素選択機能です。
- 「親要素を選択」ボタンで親要素に移動
- 親要素がない場合はボタンが非表示になる
- 移動時に自動的にスクロールして移動先を表示
パーツ管理
パーツ管理は、zcode-editorでのみ利用可能な機能です。
タイプとパーツ
- タイプ(Type): パーツのグループ。複数のパーツを含む
- パーツ(Part): 実際のコンポーネントテンプレート
共通パーツ、個別パーツ、特別パーツ
- 共通パーツ: すべてのページで使用可能なパーツ
- 個別パーツ: 特定のページでのみ使用可能なパーツ
- 特別パーツ: 動的ページ(動的ルートやURLパラメータに応じて生成されるページ)で使用可能なパーツ
パーツ管理機能
- タイプの作成・編集・削除
- パーツの作成・編集・削除
- タイプ間の並べ替え
- パーツのプレビュー表示
- Monaco Editorによるコード編集
- テンプレート記法の予測変換(オプション)
パーツテンプレートの構造
{
"id": "part-id",
"title": "パーツタイトル",
"description": "パーツの説明",
"body": "<div>{$title:タイトル}</div>",
"slots": {
"slotName": {
"allowedParts": ["part-id-1", "part-id-2"]
}
},
"slotOnly": false
}
テンプレート記法の後から追加と自動初期化
パーツ管理で既存のパーツにテンプレート記法(フィールド)を後から追加した場合、既存のページデータ(page)にそのパーツを使用しているコンポーネントがあると、自動的に不足しているフィールドが初期化されます。
動作:
- データ読み込み時の自動初期化: ページデータが読み込まれる際に、すべてのコンポーネント(スロット内も含む)に対して自動的に初期化処理が実行されます
-
通常フィールドの初期化:
テンプレートに定義されている通常フィールド(
{$field:default})で、コンポーネントデータに存在しない(undefined)場合、デフォルト値で初期化されます -
オプショナルフィールドの扱い:
オプショナルフィールド(
{$field?:default})は初期化されず、undefinedのまま残ります - 再帰的な処理: スロット内の子コンポーネントも再帰的に処理され、すべての階層で初期化が行われます
初期化されるデフォルト値:
-
テキストフィールド(
{$field:default:text}): テンプレートで指定されたデフォルト値、または空文字列 -
テキストエリア(
{$field:default:textarea}): テンプレートで指定されたデフォルト値、または空文字列 -
リッチテキスト(
{$field:default:rich}): デフォルト値がある場合は<p>デフォルト値</p>、ない場合は<p></p> -
画像フィールド(
{$field:default:image}): テンプレートで指定されたデフォルト値、または空文字列 -
ラジオボタン(
($field:option1|option2)): 最初の選択肢 -
セレクトボックス(
($field@:option1|option2)): 最初の選択肢 -
チェックボックス(
($field:option1,option2)): 空配列[] -
複数選択セレクト(
($field@:option1,option2)): 空配列[] -
ブール値(
z-ifで使用される場合など):true
使用例:
// 既存のパーツテンプレート
{
"id": "hero-part",
"title": "ヒーローセクション",
"body": "<div>{$title:タイトル}</div>"
}
// 後からテンプレート記法を追加
{
"id": "hero-part",
"title": "ヒーローセクション",
"body": "<div>{$title:タイトル}</div><div>{$subtitle:サブタイトル}</div>"
}
// 既存のページデータ(初期化前)
{
"id": "hero-1",
"part_id": "hero-part",
"title": "既存のタイトル"
// subtitleフィールドが存在しない
}
// データ読み込み後の自動初期化(初期化後)
{
"id": "hero-1",
"part_id": "hero-part",
"title": "既存のタイトル",
"subtitle": "サブタイトル" // 自動的に追加・初期化される
}
オプショナルフィールドの例:
// パーツテンプレートにオプショナルフィールドを追加
{
"id": "hero-part",
"title": "ヒーローセクション",
"body": "<div>{$title:タイトル}</div><div>{$subtitle?:サブタイトル}</div>"
}
// 既存のページデータ(初期化後)
{
"id": "hero-1",
"part_id": "hero-part",
"title": "既存のタイトル"
// subtitleはundefinedのまま(初期化されない)
}
オプショナルフィールドで親要素を削除したい場合:
オプショナルフィールドが空の場合、基本動作では親要素(タグ)が残ります。親要素ごと削除したい場合は、z-empty属性を使用してください。
// パーツテンプレート(z-emptyを使用)
{
"id": "hero-part",
"title": "ヒーローセクション",
"body": "<div>{$title:タイトル}</div><div z-empty=\"$subtitle\"><p>{$subtitle?:サブタイトル}</p></div>"
}
// 既存のページデータ(subtitleがundefinedの場合)
{
"id": "hero-1",
"part_id": "hero-part",
"title": "既存のタイトル"
// subtitleはundefinedのまま
}
// レンダリング結果:subtitleがundefinedの場合、<div z-empty="$subtitle">要素ごと削除される
<div>既存のタイトル</div>
<!-- subtitleのdiv要素は表示されない -->
注意:
この初期化処理は、データ読み込み時(page属性が設定された時点)に自動的に実行されます。パーツテンプレートを編集した後、ページデータを再読み込みすると、新しいフィールドが自動的に初期化されます。
補足: テンプレート記法を削除した場合、ページデータにはフィールドが残りますが、パーツテンプレートには存在しないため、表示上は非表示になります。不要になったフィールドは、手動でページデータから削除するか、バックエンドで一括削除する処理を実装してください。
画像管理
画像管理は、zcode-editorでのみ利用可能な機能です。
画像管理機能
- 画像のアップロード
- 画像の編集(ID、名前、URL)
- 画像の削除
- 共通画像と個別画像の管理
画像データ構造
interface ImageData {
id: string;
name: string;
url: string;
mimeType?: string;
needsUpload?: boolean;
}
mimeType
画像のMIMEタイプ(例: image/jpeg,
image/png)。base64画像の場合に設定されます。
needsUpload
trueの場合、バックエンドで画像のアップロード処理が必要です。通常、base64画像はneedsUpload: trueとして保存されます。
画像選択モーダル
編集パネルから画像を選択する際に表示されるモーダルです。
- 共通画像と個別画像の切り替え
- 画像のプレビュー表示
- 画像の追加・削除
設定オプション
ZeroCode.jsでは、config属性で初期設定を指定できます。
設定の構造
設定はcms、dev、および共通設定の3つのカテゴリに分離されています。
{
"cms": {
"allowDynamicContentInteraction": false,
"devRightPadding": false,
"enableContextMenu": false
},
"dev": {
"showDataViewer": false,
"enableTemplateSuggestions": false
},
"categoryOrder": "common"
}
設定の優先順位
- localStorage: ユーザーが変更した設定(最優先)
- config属性: 初期設定として指定された値
- デフォルト値: 全て
false
CMS設定(cms)
zcode-cmsとzcode-editorの両方で共有される設定です。
allowDynamicContentInteraction
デフォルト: false
アコーディオン、タブ、モーダル、リンクなどの動的コンテンツの動作を有効/無効にします。
設定パネルでは「ページの動作を有効にする」として表示されます。
devRightPadding
デフォルト: false
編集パネル表示時にコンテンツの右余白を追加します。
設定パネルでは「編集パネル分の余白をつける」として表示されます。
enableContextMenu
デフォルト: false
右クリックメニューを有効にします。
設定パネルでは「右クリックメニューを有効にする」として表示されます。
Dev設定(dev)
zcode-editor専用の設定です。
showDataViewer
デフォルト: false
データビューアを表示します。
設定パネルでは「データビューアを表示」として表示されます。
enableTemplateSuggestions
デフォルト: false
テンプレート記法の予測変換を有効にします。
パーツ管理パネルのエディタで使用されます。
共通設定
zcode-cmsとzcode-editorの両方で使用される設定です。
categoryOrder
デフォルト: "common"
パーツ管理、画像管理、データビューア、追加パネルにおける「共通」「個別」「特別」タブの表示順序と初期選択を制御します。
設定可能な値:
-
"common": 共通タブを先に表示し、初期状態で共通タブを選択(デフォルト) "individual": 個別タブを先に表示し、初期状態で個別タブを選択-
"special": 特別タブを先に表示し、初期状態で特別タブを選択(特別データが存在する場合のみ表示)
この設定は以下の画面に適用されます:
- パーツ管理タブ(
zcode-editorのみ) - 画像管理タブ(
zcode-editorのみ) - データビューアタブ(
zcode-editorのみ、パーツ/画像表示時) - 追加パネル(
zcode-cmsとzcode-editorの両方)
設定の使用例
<zcode-cms
config='{"cms": {"allowDynamicContentInteraction": true, "devRightPadding": true, "enableContextMenu": true}, "categoryOrder": "individual"}'
></zcode-cms>
<zcode-editor
config='{"cms": {"allowDynamicContentInteraction": true}, "dev": {"showDataViewer": true}, "categoryOrder": "individual"}'
></zcode-editor>
画像選択モーダルで追加・削除ボタンを表示するカテゴリを指定するには
imageModalActions を使用します(未指定時は追加・削除とも非表示)。
保存対象(targets)も同様に、add または delete が true のカテゴリのみに限定されます。
config='{"cms":{"imageModalActions":{"special":{"add":true,"delete":true}}}}'
または、JavaScript変数で指定することもできます:
const cmsConfig = {
cms: {
allowDynamicContentInteraction: true,
devRightPadding: true,
enableContextMenu: true,
imageModalActions: { special: { add: true, delete: true } }
},
categoryOrder: 'individual'
};
const cmsElement = document.getElementById('cms');
cmsElement.setAttribute('config', JSON.stringify(cmsConfig));
APIリファレンス
zcode-cms
ユーザー向け管理画面のWebコンポーネント。
属性
| 属性名 | 型 | 説明 |
|---|---|---|
page |
string | ページデータ(JSON文字列) |
parts-common |
string | 共通パーツデータ(JSON文字列) |
parts-individual |
string | 個別パーツデータ(JSON文字列) |
parts-special |
string | 特別パーツデータ(JSON文字列) |
images-common |
string | 共通画像データ(JSON文字列) |
images-individual |
string | 個別画像データ(JSON文字列) |
images-special |
string | 特別画像データ(JSON文字列) |
config |
string | 初期設定データ(JSON文字列) |
use-shadow-dom |
string | Shadow DOMを使用するか('true' | 'false'、デフォルト: 'true') |
スロット
css: CSSファイルを指定script: JavaScriptファイルを指定
zcode-editor
エンジニア・デザイナー向け管理画面のWebコンポーネント。
ZeroCodeCMSの機能に加えて、パーツ管理・画像管理・データビューアが利用できます。
属性
zcode-cmsの属性に加えて、以下の属性が利用できます:
| 属性名 | 型 | 説明 |
|---|---|---|
enable-parts-manager |
string | パーツ管理を有効にするか(デフォルト: 'true') |
enable-images-manager |
string | 画像管理を有効にするか(デフォルト: 'true') |
renderToHtml()
サーバーサイドレンダリング用のHTML文字列生成関数。
import { renderToHtml } from 'zerocodejs';
const html = renderToHtml(data, {
enableEditorAttributes: false
});
パラメータ
data: ZeroCodeData形式のデータ-
options.enableEditorAttributes: 編集用属性を有効にするか(デフォルト:false)
戻り値
生成されたHTML文字列
イベント
save-request
保存ボタンクリック時に発火します。event.detail に
data は含まれません。保存対象のデータは
cms.getData() で取得してください。
cms.addEventListener('save-request', (event) => {
const { requestId, source, targets, timestamp } = event.detail;
const data = cms.getData();
for (const target of targets) {
// target ごとに data から必要な部分を切り出してサーバーへ送る
}
});
event.detail
requestId: リクエストID(save-resultで対応付けに使用)source: 送信元('cms'または'editor')-
targets: 保存対象の配列('page','parts-common','parts-individual','parts-special','images-common','images-individual','images-special','parts-common-css','parts-individual-css','parts-special-css'のいずれか) timestamp: タイムスタンプ
含まれないもの: target(単数)・data。データは
cms.getData() で取得する。
zcode-dom-updated
DOMが更新されたときに発火します。動的コンテンツの初期化などに使用できます。
window.addEventListener('zcode-dom-updated', () => {
// DOM更新後の処理
initializeAccordion();
});
メソッド
getData(path?: string)
データを取得します。
const cms = document.getElementById('cms');
// 全体のデータを取得
const allData = cms.getData();
// 特定のパスのデータを取得
const pageData = cms.getData('page');
const firstComponent = cms.getData('page.0');
const title = cms.getData('page.0.title');
パラメータ
-
path(オプション): データのパス(例:'page','page.0','page.0.title')
戻り値
指定したパスのデータ。パスが指定されていない場合は全体のデータを返します。
setData(path: string | object, value?: any)
データを設定します。
const cms = document.getElementById('cms');
// パスを指定して値を設定
cms.setData('page.0.title', '新しいタイトル');
// オブジェクト全体を設定
cms.setData({
page: [...],
parts: {
common: [...],
individual: [...]
}
});
セキュリティ注意: このメソッドはクライアント側から任意のデータを設定できます。開発者ツールからも呼び出し可能です。サーバー側での検証を必ず実装してください。
パラメータ
-
path: データのパス(文字列の場合)またはデータオブジェクト全体(オブジェクトの場合) value(オプション): 設定する値(pathが文字列の場合)
allowDynamicContentInteraction(プロパティ)
動的コンテンツの動作を有効/無効にします(getter/setter)。
const cms = document.getElementById('cms');
// 値を取得
const isEnabled = cms.allowDynamicContentInteraction;
// 値を設定
cms.allowDynamicContentInteraction = true;
データ構造
ZeroCode.jsで使用するデータ構造の説明です。
ZeroCodeData
interface ZeroCodeData {
page: ComponentData[];
parts: {
common: TypeData[];
individual: TypeData[];
special: TypeData[];
};
images: {
common: ImageData[];
individual: ImageData[];
special: ImageData[];
};
}
ComponentData
interface ComponentData {
id: string;
part_id: string;
[key: string]: any;
slots?: Record<string, ComponentData[] | SlotConfig>;
}
id: コンポーネントの一意のIDpart_id: パーツID(タイトル変更時も紐付けが維持される)-
[key: string]: any: フィールドの値(テンプレート記法で定義されたフィールド) slots: スロットの子コンポーネント
SlotConfig
interface SlotConfig {
allowedParts?: string[];
children?: ComponentData[];
}
allowedParts: 許可されるパーツIDの配列-
children: 子コンポーネントの配列(ComponentData[]としても使用可能)
TypeData
interface TypeData {
id: string;
type: string;
description: string;
parts: PartData[];
}
id: タイプID(タイプ変更時も紐付けが維持される)type: タイプ名description: タイプの説明parts: パーツの配列
PartData
interface PartData {
id: string;
title: string;
description: string;
body: string;
slots?: Record<string, { allowedParts?: string[] }>;
slotOnly?: boolean;
}
id: パーツID(タイトル変更時も紐付けが維持される)title: パーツタイトルdescription: パーツの説明body: パーツのテンプレート(HTML文字列)slots: スロットの設定slotOnly: スロット専用パーツかどうか
ImageData
interface ImageData {
id: string;
name: string;
url: string;
mimeType?: string;
needsUpload?: boolean;
}
id: 画像の一意のIDname: 画像名-
url: 画像のURL(base64データの場合はdata:image/...形式) -
mimeType: MIMEタイプ(例:image/jpeg,image/png) -
needsUpload: アップロードが必要かどうか(trueの場合、バックエンドでアップロード処理が必要)
データベース構成
ZeroCode.jsのデータをRDBで管理する場合の推奨データベース設計です。
テーブル構成
以下の3つのテーブルで構成されます:
- zcode_common_parts: 共通パーツ管理(店舗に依存しない)
- zcode_common_images: 共通画像管理(店舗に依存しない)
- zcode_individual: 店舗ごとのページ・個別パーツ・個別画像
zcode_common_parts(共通パーツ)
すべての店舗で共有される共通パーツを管理します。
CREATE TABLE zcode_common_parts (
id VARCHAR(50) PRIMARY KEY,
type VARCHAR(50) NOT NULL,
description TEXT,
parts JSON NOT NULL,
version INT NOT NULL DEFAULT 1,
is_published BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
| カラム名 | 型 | NULL | 説明 |
|---|---|---|---|
id |
VARCHAR(50) | NO | パーツタイプID(PK) |
type |
VARCHAR(50) | NO | パーツタイプ(例: hero, features) |
description |
TEXT | YES | タイプの説明 |
parts |
JSON | NO | パーツ配列(PartData[]形式) |
version |
INT | NO | バージョン番号(楽観的ロック用) |
is_published |
BOOLEAN | NO | 公開フラグ |
created_at |
TIMESTAMP | NO | 作成日時 |
updated_at |
TIMESTAMP | NO | 更新日時 |
zcode_common_images(共通画像)
すべての店舗で共有される共通画像を管理します。
CREATE TABLE zcode_common_images (
id VARCHAR(50) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
url TEXT NOT NULL,
mime_type VARCHAR(50) NOT NULL,
needs_upload BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
| カラム名 | 型 | NULL | 説明 |
|---|---|---|---|
id |
VARCHAR(50) | NO | 画像ID(PK) |
name |
VARCHAR(255) | NO | 画像名 |
url |
TEXT | NO | 画像URL(base64の場合はdata:image/...形式) |
mime_type |
VARCHAR(50) | NO | MIMEタイプ(例: image/jpeg, image/png) |
needs_upload |
BOOLEAN | NO |
アップロード要否(trueの場合、バックエンドでアップロード処理が必要)
|
created_at |
TIMESTAMP | NO | 作成日時 |
updated_at |
TIMESTAMP | NO | 更新日時 |
zcode_individual(店舗ごとの個別データ)
店舗ごとのページ、個別パーツ、個別画像を管理します。
CREATE TABLE zcode_individual (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
data_type ENUM('page', 'parts', 'images') NOT NULL,
content JSON NOT NULL,
version INT NOT NULL DEFAULT 1,
status ENUM('draft', 'published', 'archived') NOT NULL DEFAULT 'draft',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_type_status (user_id, data_type, status)
);
| カラム名 | 型 | NULL | 説明 |
|---|---|---|---|
id |
BIGINT | NO | レコードID(PK, AUTO_INCREMENT) |
user_id |
BIGINT | NO | 店舗ID(外部キー) |
data_type |
ENUM | NO | データ種別(page, parts, images) |
content |
JSON | NO |
データ内容(ComponentData[], TypeData[],
ImageData[]形式)
|
version |
INT | NO | バージョン番号(楽観的ロック用) |
status |
ENUM | NO |
ステータス(draft: 下書き, published: 公開,
archived: アーカイブ)
|
created_at |
TIMESTAMP | NO | 作成日時 |
updated_at |
TIMESTAMP | NO | 更新日時 |
データ例
zcode_individual のデータ例
-- 店舗1(user_id=1)のページデータ(下書き)
INSERT INTO zcode_individual (user_id, data_type, content, status) VALUES (
1,
'page',
'[{"id": "hero-1", "part_id": "zcode-part-1", "title": "店舗1のタイトル", ...}]',
'draft'
);
-- 店舗1(user_id=1)のページデータ(公開版)
INSERT INTO zcode_individual (user_id, data_type, content, status) VALUES (
1,
'page',
'[{"id": "hero-1", "part_id": "zcode-part-1", "title": "店舗1のタイトル", ...}]',
'published'
);
-- 店舗1(user_id=1)の個別パーツデータ
INSERT INTO zcode_individual (user_id, data_type, content, status) VALUES (
1,
'parts',
'[{"id": "zcode-part-12", "type": "cta", "description": "店舗専用のCTA", ...}]',
'published'
);
-- 店舗1(user_id=1)の個別画像データ
INSERT INTO zcode_individual (user_id, data_type, content, status) VALUES (
1,
'images',
'[{"id": "img-ind-1", "name": "店舗専用画像", "url": "/images/store1-hero.jpg", ...}]',
'published'
);
クエリ例
店舗の公開ページデータを取得
SELECT content
FROM zcode_individual
WHERE user_id = 1
AND data_type = 'page'
AND status = 'published'
LIMIT 1;
店舗の下書きページデータを取得
SELECT content
FROM zcode_individual
WHERE user_id = 1
AND data_type = 'page'
AND status = 'draft'
LIMIT 1;
共通パーツを取得
SELECT parts
FROM zcode_common_parts
WHERE is_published = TRUE;
設計の特徴
- 共通データの一元管理: 共通パーツと共通画像は1箇所で管理され、すべての店舗で共有されます
- 個別データの統合管理: 店舗ごとのページ、個別パーツ、個別画像を1つのテーブルで管理します
- ステータス管理: 下書き・公開・アーカイブを1テーブルで管理できます(1店舗あたり最大9レコード: 3種別 × 3ステータス)
-
楽観的ロック:
versionカラムを使用して同時更新の競合を防ぎます -
スケーラビリティ:
200店舗規模でも問題なく動作します(インデックス
idx_user_type_statusが有効)
注意事項
-
contentカラムはJSON型またはLONGTEXT型を使用してください(データ量が多い場合) -
MySQLの場合、
JSON型は最大1GBですが、実用的には数MB程度を目安にしてください - PostgreSQLの場合、
JSONB型を使用すると検索パフォーマンスが向上します - 更新時は
versionカラムをチェックして楽観的ロックを実装してください
保存リクエスト
保存ボタンをクリックすると、save-requestイベントが発火します。
イベントの受け取り方
event.detail に data は含まれません。cms.getData()
で取得してください。
const cms = document.getElementById('cms');
cms.addEventListener('save-request', (event) => {
const { requestId, source, targets, timestamp } = event.detail;
const data = cms.getData();
for (const target of targets) {
fetch('/api/zero-code/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ target, source, data, requestId, timestamp })
});
}
});
保存ターゲットの仕様
save-requestイベントのtargets配列には、現在のタブやモードに応じて複数のターゲットが含まれる場合があります。
| タブ/モード | 含まれるターゲット | 説明 |
|---|---|---|
| ページ管理(編集モード) |
imageModalActions から算出。page + (add または delete が true の画像カテゴリ)。未指定時は ['page'] のみ
|
ページデータと画像データを保存。ページCSSは含まれない(パーツ管理でのみ編集可能) |
| パーツ管理 |
['parts-common', 'parts-common-css']、['parts-individual', 'parts-individual-css']、または['parts-special', 'parts-special-css']
|
パーツデータとページCSSを保存(パーツ編集時にページCSSも編集可能なため) |
| 画像管理 |
['images-common']、['images-individual']、または['images-special']
|
画像データのみを保存 |
| データビューアー | 選択中のタブとカテゴリに応じて決定 | 現在表示中のデータに対応するターゲット |
画像のアップロード
targets配列にimages-common、images-individual、またはimages-specialが含まれる場合、needsUpload: trueの画像をアップロード処理してください。data はハンドラ先頭で
cms.getData() したもの。
for (const target of targets) {
if (target.startsWith('images-')) {
const images = target === 'images-common'
? data.images?.common || []
: target === 'images-individual'
? data.images?.individual || []
: data.images?.special || [];
const imagesToUpload = images.filter(img => img.needsUpload === true);
for (const image of imagesToUpload) {
// base64データをデコード
const base64Data = image.url.replace(/^data:image\/\w+;base64,/, '');
const buffer = Buffer.from(base64Data, 'base64');
// S3などにアップロード
// アップロード後、image.urlを更新し、needsUploadをfalseに設定
}
}
}
バリデーションエラーの返却
バックエンドでバリデーションを行い、エラーがある場合はsave-resultイベントでエラーを返してください。フロントエンドでは、requiredとmaxのみをチェックします。複雑なバリデーション(メール形式チェック、重複チェックなど)はバックエンドで実施してください。
イベント形式:
// 成功時
cms.dispatchEvent(
new CustomEvent('save-result', {
detail: {
requestId: 'req-1234567890-abc', // save-request の requestId
target: 'page', // この回の保存対象(targets の要素のいずれか)
ok: true,
errors: []
},
bubbles: true,
composed: true
})
);
// エラー時
cms.dispatchEvent(
new CustomEvent('save-result', {
detail: {
requestId: 'req-1234567890-abc', // save-request の requestId
target: 'page', // この回の保存対象(targets の要素のいずれか)
ok: false,
errors: [
{
path: 'page.0', // コンポーネントのパス(オプション、指定しない場合はすべてのコンポーネントに適用)
field: 'email', // フィールド名
message: 'メールアドレスの形式が正しくありません', // エラーメッセージ
code: 'FORMAT' // エラーコード(オプション)
}
]
},
bubbles: true,
composed: true
})
);
実装例(Node.js/Express):
cms.addEventListener('save-request', async (event) => {
const { requestId, source, targets, timestamp } = event.detail;
const data = cms.getData();
for (const target of targets) {
try {
// バリデーション
const errors = [];
if (target === 'page') {
// ページデータのバリデーション
const pageData = data.page || [];
for (let i = 0; i < pageData.length; i++) {
const component = pageData[i];
// メール形式チェック(例)
if (component.email && !component.email.includes('@')) {
errors.push({
path: `page.${i}`,
field: 'email',
message: 'メールアドレスの形式が正しくありません',
code: 'FORMAT'
});
}
// 重複チェック(例)
if (component.title && await isTitleDuplicate(component.title)) {
errors.push({
path: `page.${i}`,
field: 'title',
message: 'このタイトルは既に使用されています',
code: 'DUPLICATE'
});
}
}
} else if (target === 'parts-common-css' || target === 'parts-individual-css' || target === 'parts-special-css') {
// CSSのバリデーション(例)
const css = target === 'parts-common-css' ? data.css?.common :
target === 'parts-individual-css' ? data.css?.individual :
data.css?.special || '';
if (css.length > 10000) {
errors.push({
field: target,
message: 'CSSのサイズが大きすぎます',
code: 'SIZE_LIMIT'
});
}
} else if (target.startsWith('images-')) {
// 画像データのアップロード処理
const images = target === 'images-common'
? data.images?.common || []
: target === 'images-individual'
? data.images?.individual || []
: data.images?.special || [];
for (const image of images) {
if (image.needsUpload) {
// 画像をアップロード
const uploadedUrl = await uploadImage(image);
image.url = uploadedUrl;
image.needsUpload = false;
}
}
}
if (errors.length > 0) {
// エラーがある場合
cms.dispatchEvent(
new CustomEvent('save-result', {
detail: {
requestId,
target,
ok: false,
errors
},
bubbles: true,
composed: true
})
);
continue; // 次のターゲットへ
}
// データベースに保存
await saveToDatabase(target, data);
// 成功時
cms.dispatchEvent(
new CustomEvent('save-result', {
detail: {
requestId,
target,
ok: true,
errors: []
},
bubbles: true,
composed: true
})
);
} catch (error) {
// エラー時
cms.dispatchEvent(
new CustomEvent('save-result', {
detail: {
requestId,
target,
ok: false,
errors: [
{
field: 'general',
message: '保存に失敗しました',
code: 'SAVE_FAILED'
}
]
},
bubbles: true,
composed: true
})
);
}
}
}
});
エラーの表示:
-
編集パネルが開いている場合:
エラーは該当フィールドの下に表示されます。
pathが指定されている場合、そのパスのコンポーネントのフィールドにのみ表示されます。 - 編集パネルが閉じている場合: エラーは画面上部にエラーバナーとして表示されます。編集パネルを開くと、該当フィールドに詳細エラーが表示されます。
注意:
requestIdはsave-requestイベントのrequestIdと同じ値を使用してください。これにより、複数の保存リクエストが同時に発火した場合でも、正しいレスポンスとリクエストを関連付けることができます。
セキュリティ
重要: ZeroCode.jsはフロントエンドライブラリのため、クライアント側での完全なセキュリティ保証はできません。サーバー側での検証を必ず実装してください。
必須実装事項
1. サーバー側でのデータ検証(必須)
- すべてのデータをサーバー側で検証してください
-
パーツテンプレート(
part.body)が信頼できるソースからのみ来ることを確認してください -
save-requestイベントのsourceフィールドを確認し、CMSからのパーツデータ保存を拒否してください
2. 認証・認可
- パーツデータの変更は認証されたユーザーのみ許可してください
- ロールベースアクセス制御を実装してください
- CMS(
source: 'cms')からのパーツデータ保存を拒否してください
3. 属性値のセキュリティ
-
URL属性(
href,src,action)の検証を実装してください -
style属性にユーザー入力を直接設定する場合は、サーバー側での検証を推奨します - 基本的なエスケープ処理が適用されますが、サーバー側での追加検証を推奨します
4. パーツテンプレートの管理
- パーツテンプレートは信頼できるソースからのみ受け入れてください
- サーバー側でテンプレートの妥当性を検証してください
実装例
※ クライアントは save-request ハンドラで cms.getData() し、targets
をループして各 target と data を
req.body に含めて送る。
// Node.js/Express の例(サーバー側)
app.post('/api/zero-code/save', authenticate, (req, res) => {
const { target, source, data } = req.body;
if (source === 'cms' && target.startsWith('parts-')) {
return res.status(403).json({
error: 'CMSからのパーツ保存は拒否されました'
});
}
// データ構造の検証
if (!validateDataStructure(data)) {
return res.status(400).json({
error: '無効なデータです'
});
}
// パーツテンプレートの検証
if (target.startsWith('parts-')) {
if (!validatePartTemplate(data)) {
return res.status(400).json({
error: '無効なテンプレートです'
});
}
}
// 画像アップロード処理
if (target.startsWith('images-')) {
const imagesToUpload = data.filter(img => img.needsUpload === true);
for (const image of imagesToUpload) {
// アップロード処理
}
}
// 保存処理
// ...
res.json({ success: true });
});
画像アップロードのセキュリティ
needsUpload: trueの画像のみアップロード処理を行ってください- MIMEタイプ(
mimeType)の検証を実装してください - ファイルサイズの制限を設定してください
- ファイル名のサニタイズを実装してください