はじめに

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-cmszcode-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属性を指定することを推奨します。これにより、データの管理が明確になり、デバッグも容易になります。

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の利点

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>

動作:

例:

<!-- テンプレート -->
<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>

バリデーションルール:

動作:

注意: 複雑なバリデーション(メール形式チェック、重複チェックなど)は、バックエンドで実施することを推奨します。フロントエンドでは、requiredmaxのみをサポートしています。

バックエンドデータの参照

バックエンドから渡されたデータをテンプレート内で参照できます。動的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>

動作:

使用例:

<!-- 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>

ループ変数の参照:

ループ内での他のテンプレート構文:

ループ内では、通常のテンプレート構文({$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>

制限事項:

完全な例:

<!-- 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>

動作:

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="$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 で指定したフィールドに紐づきます。そのフィールドが空(undefinednull、空文字列、または実質的に空のrichテキスト)の場合、親要素を削除します。基本動作では空タグが残るため、親ごと消したいときだけ z-empty を使ってください。

<!-- 基本動作:空タグが残る -->
<p>{$subtitle?:サブタイトル}</p>
<!-- subtitleがundefinedの場合、<p></p>が残る -->

<!-- z-empty:親要素を削除 -->
<div z-empty="$subtitle">
  <p>{$subtitle?:サブタイトル}</p>
</div>
<!-- subtitleがundefinedの場合、<div>要素ごと削除される -->

動作:

使用例:

<!-- オプショナルフィールドで親要素を削除したい場合 -->
<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>

スロットの制限

PartDataslotsプロパティで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でのみ利用可能な機能です。

タイプとパーツ

共通パーツ、個別パーツ、特別パーツ

パーツ管理機能

パーツテンプレートの構造

{
  "id": "part-id",
  "title": "パーツタイトル",
  "description": "パーツの説明",
  "body": "<div>{$title:タイトル}</div>",
  "slots": {
    "slotName": {
      "allowedParts": ["part-id-1", "part-id-2"]
    }
  },
  "slotOnly": false
}

テンプレート記法の後から追加と自動初期化

パーツ管理で既存のパーツにテンプレート記法(フィールド)を後から追加した場合、既存のページデータ(page)にそのパーツを使用しているコンポーネントがあると、自動的に不足しているフィールドが初期化されます。

動作:

初期化されるデフォルト値:

使用例:

// 既存のパーツテンプレート
{
  "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でのみ利用可能な機能です。

画像管理機能

画像データ構造

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属性で初期設定を指定できます。

設定の構造

設定はcmsdev、および共通設定の3つのカテゴリに分離されています。

{
  "cms": {
    "allowDynamicContentInteraction": false,
    "devRightPadding": false,
    "enableContextMenu": false
  },
  "dev": {
    "showDataViewer": false,
    "enableTemplateSuggestions": false
  },
  "categoryOrder": "common"
}

設定の優先順位

  1. localStorage: ユーザーが変更した設定(最優先)
  2. config属性: 初期設定として指定された値
  3. デフォルト値: 全てfalse

CMS設定(cms)

zcode-cmszcode-editorの両方で共有される設定です。

allowDynamicContentInteraction

デフォルト: false

アコーディオン、タブ、モーダル、リンクなどの動的コンテンツの動作を有効/無効にします。

設定パネルでは「ページの動作を有効にする」として表示されます。

devRightPadding

デフォルト: false

編集パネル表示時にコンテンツの右余白を追加します。

設定パネルでは「編集パネル分の余白をつける」として表示されます。

enableContextMenu

デフォルト: false

右クリックメニューを有効にします。

設定パネルでは「右クリックメニューを有効にする」として表示されます。

Dev設定(dev)

zcode-editor専用の設定です。

showDataViewer

デフォルト: false

データビューアを表示します。

設定パネルでは「データビューアを表示」として表示されます。

enableTemplateSuggestions

デフォルト: false

テンプレート記法の予測変換を有効にします。

パーツ管理パネルのエディタで使用されます。

共通設定

zcode-cmszcode-editorの両方で使用される設定です。

categoryOrder

デフォルト: "common"

パーツ管理、画像管理、データビューア、追加パネルにおける「共通」「個別」「特別」タブの表示順序と初期選択を制御します。

設定可能な値:

この設定は以下の画面に適用されます:

設定の使用例

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

スロット

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

パラメータ

戻り値

生成されたHTML文字列

イベント

save-request

保存ボタンクリック時に発火します。event.detaildata は含まれません。保存対象のデータは 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

含まれないもの: 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');

パラメータ

戻り値

指定したパスのデータ。パスが指定されていない場合は全体のデータを返します。

setData(path: string | object, value?: any)

データを設定します。

const cms = document.getElementById('cms');

// パスを指定して値を設定
cms.setData('page.0.title', '新しいタイトル');

// オブジェクト全体を設定
cms.setData({
  page: [...],
  parts: {
    common: [...],
    individual: [...]
  }
});

セキュリティ注意: このメソッドはクライアント側から任意のデータを設定できます。開発者ツールからも呼び出し可能です。サーバー側での検証を必ず実装してください。

パラメータ

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

SlotConfig

interface SlotConfig {
  allowedParts?: string[];
  children?: ComponentData[];
}

TypeData

interface TypeData {
  id: string;
  type: string;
  description: string;
  parts: PartData[];
}

PartData

interface PartData {
  id: string;
  title: string;
  description: string;
  body: string;
  slots?: Record<string, { allowedParts?: string[] }>;
  slotOnly?: boolean;
}

ImageData

interface ImageData {
  id: string;
  name: string;
  url: string;
  mimeType?: string;
  needsUpload?: boolean;
}

データベース構成

ZeroCode.jsのデータをRDBで管理する場合の推奨データベース設計です。

テーブル構成

以下の3つのテーブルで構成されます:

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;

設計の特徴

注意事項

保存リクエスト

保存ボタンをクリックすると、save-requestイベントが発火します。

イベントの受け取り方

event.detaildata は含まれません。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-commonimages-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イベントでエラーを返してください。フロントエンドでは、requiredmaxのみをチェックします。複雑なバリデーション(メール形式チェック、重複チェックなど)はバックエンドで実施してください。

イベント形式:

// 成功時
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
          })
        );
      }
    }
  }
});

エラーの表示:

注意: requestIdsave-requestイベントのrequestIdと同じ値を使用してください。これにより、複数の保存リクエストが同時に発火した場合でも、正しいレスポンスとリクエストを関連付けることができます。

セキュリティ

重要: ZeroCode.jsはフロントエンドライブラリのため、クライアント側での完全なセキュリティ保証はできません。サーバー側での検証を必ず実装してください。

必須実装事項

1. サーバー側でのデータ検証(必須)

2. 認証・認可

3. 属性値のセキュリティ

4. パーツテンプレートの管理

実装例

※ クライアントは save-request ハンドラで cms.getData() し、targets をループして各 targetdatareq.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 });
});

画像アップロードのセキュリティ