商品編集ブロック
EditProduct は、MUI 上に構築された制御された商品編集フォームブロックで、メディア、商品詳細、在庫、送信アクションのためのネストされたセクションを備えています。
インストール
- npm
- yarn
- pnpm
- bun
npm install @nodeblocks/frontend-edit-product-block
yarn add @nodeblocks/frontend-edit-product-block
pnpm add @nodeblocks/frontend-edit-product-block
bun add @nodeblocks/frontend-edit-product-block
必要なもの
| 項目 | 理由 |
|---|---|
data | 制御されたフォーム状態。標準の形には title、categoryId、typeId、optionId、quantity、description、details、prefecture、city、onlineAvailability、inventoryNotes、availableFrom、availableUntil、additionalDetails、image、tags が含まれます。 |
onDataChange | 変更されたフィールドパス(meta.name)、最新の値(meta.value)、原因(meta.cause)、任意のイベント(meta.event)に関するメタデータとともに、次のデータスナップショットを受け取ります。 |
errors (optional) | ブラケット記法のパスをキーにしたフィールドレベルのバリデーション。 |
onRejectAttachment (optional) | 画像ドロップゾーンから拒否されたアップロードファイルを DropzoneFileError とともに受け取ります。 |
labels (optional) | セクションタイトル、フィールドラベル、ドロップゾーン、送信ボタンの文言です。 |
placeholders (optional) | テキストフィールドとセレクトフィールドのプレースホルダーテキストです。 |
selectOptions (optional) | カテゴリ、種別、オプション、都道府県フィールド用のドロップダウンオプションです。 |
tagTypes / tags (optional) | basic-info セクションのタグピッカーを制御します。 |
children (optional) | 複合セクションまたはブロックのオーバーライド関数を使用します。 |
制御コンポーネント
EditProduct はフォーム状態を所有しません。data はアプリ側で保持し、更新は onDataChange 経由で渡してください。data.image は File、{ url, type?, id? }、または null にでき、data.tags は Tag オブジェクトの配列です。
コード例
- クイックスタート
- ラベルと文言
- フォームエラー
- 複合コンポーネント
- ブロックのオーバーライド
ライブエディター
function Example() { const defaultData = { title: '', categoryId: '', typeId: '', optionId: '', quantity: '', description: '', details: '', prefecture: '', city: '', onlineAvailability: false, inventoryNotes: '', availableFrom: '', availableUntil: '', additionalDetails: '', image: null, tags: [], }; const [data, setData] = React.useState(defaultData); const selectOptions = { categoryOptions: [ { value: 'electronics', label: 'Electronics' }, { value: 'clothing', label: 'Clothing' }, { value: 'home', label: 'Home & Garden' }, ], typeOptions: [ { value: 'physical', label: 'Physical' }, { value: 'digital', label: 'Digital' }, { value: 'service', label: 'Service' }, ], optionOptions: [ { value: 'standard', label: 'Standard' }, { value: 'premium', label: 'Premium' }, { value: 'custom', label: 'Custom' }, ], prefectureOptions: [ { value: 'Tokyo', label: 'Tokyo' }, { value: 'Osaka', label: 'Osaka' }, { value: 'Fukuoka', label: 'Fukuoka' }, ], }; const tagTypes = [ { id: '100', label: 'Product highlights' }, { id: '200', label: 'Materials & care' }, ]; const tags = [ { id: '101', typeId: '100', label: 'Best seller' }, { id: '102', typeId: '100', label: 'New arrival' }, { id: '201', typeId: '200', label: 'Organic / natural materials' }, { id: '202', typeId: '200', label: 'Contains recycled content' }, ]; return ( <EditProduct data={data} selectOptions={selectOptions} tagTypes={tagTypes} tags={tags} onDataChange={(nextData) => { setData(nextData); }} onSubmit={(e) => { e.preventDefault(); }} /> ); }
結果
Loading...
ライブエディター
function Example() { const defaultData = { title: '', categoryId: '', typeId: '', optionId: '', quantity: '', description: '', details: '', prefecture: '', city: '', onlineAvailability: false, inventoryNotes: '', availableFrom: '', availableUntil: '', additionalDetails: '', image: null, tags: [], }; const [data, setData] = React.useState(defaultData); return ( <EditProduct data={data} labels={{ mainInfoSectionTitle: 'Media', titleField: 'Product title', dropzoneDropHere: 'Drop the image here', dropzoneUploadImage: 'Upload product image', dropzoneSubtitle: 'PNG, JPG up to 2MB', dropzoneOptionsButton: 'Image actions', dropzoneReplaceFile: 'Replace image', dropzoneDeleteFile: 'Remove image', basicInfoSectionTitle: 'Product details', categoryField: 'Category', typeField: 'Type', optionField: 'Option', quantityField: 'Quantity', quantityUnit: 'pcs', descriptionField: 'Description', detailsField: 'Details', tagsField: 'Tags', additionalInfoTitle: 'Inventory', additionalInfoSubtitle: 'Location', prefectureField: 'Prefecture', cityField: 'City', onlineAvailabilityField: 'Online availability', inventoryNotesField: 'Inventory notes', availableFromField: 'Available from', availableUntilField: 'Available until', additionalDetailsField: 'Additional details', submitButton: 'Save changes', }} placeholders={{ titleField: 'Enter title', categoryField: 'Select category', typeField: 'Select type', optionField: 'Select option', quantityField: 'Enter quantity', descriptionField: 'Describe the product', detailsField: 'Enter details', prefectureField: 'Select prefecture', cityField: 'Enter city', inventoryNotesField: 'Notes about inventory', additionalDetailsField: 'Enter additional details', }} onDataChange={(nextData) => { setData(nextData); }} onSubmit={(e) => { e.preventDefault(); }} /> ); }
結果
Loading...
ラベルはルートにあります
このブロックには URL props はありません。コピーの上書きはルートの labels と placeholders に置くか、下の複合セクションを使って標準テキストを置き換えてください。
ライブエディター
function Example() { const defaultData = { title: '', categoryId: '', typeId: '', optionId: '', quantity: '', description: '', details: '', prefecture: '', city: '', onlineAvailability: false, inventoryNotes: '', availableFrom: '', availableUntil: '', additionalDetails: '', image: null, tags: [], }; const [data, setData] = React.useState(defaultData); const [errors, setErrors] = React.useState({}); const handleDataChange = (nextData, meta) => { const { [meta.name]: _removed, ...restErrors } = errors; let nextErrors = restErrors; // Validate required fields on blur (same pattern as storybook) if (meta.cause === 'blur') { nextErrors = { ...restErrors }; const value = nextData[meta.name]; if (value === '' || (typeof value === 'string' && !value.trim())) { nextErrors[meta.name] = 'This field is required.'; } } setErrors(nextErrors); setData(nextData); }; return ( <EditProduct data={data} errors={Object.keys(errors).length ? errors : undefined} onDataChange={handleDataChange} onSubmit={(e) => { e.preventDefault(); }} /> ); }
結果
Loading...
ライブエディター
function Example() { const defaultData = { title: '', categoryId: '', typeId: '', optionId: '', quantity: '', description: '', details: '', prefecture: '', city: '', onlineAvailability: false, inventoryNotes: '', availableFrom: '', availableUntil: '', additionalDetails: '', image: null, tags: [], }; const [data, setData] = React.useState(defaultData); const selectOptions = { categoryOptions: [ { value: 'electronics', label: 'Electronics' }, { value: 'clothing', label: 'Clothing' }, { value: 'home', label: 'Home & Garden' }, ], typeOptions: [ { value: 'physical', label: 'Physical' }, { value: 'digital', label: 'Digital' }, { value: 'service', label: 'Service' }, ], optionOptions: [ { value: 'standard', label: 'Standard' }, { value: 'premium', label: 'Premium' }, { value: 'custom', label: 'Custom' }, ], prefectureOptions: [ { value: 'Tokyo', label: 'Tokyo' }, { value: 'Osaka', label: 'Osaka' }, { value: 'Fukuoka', label: 'Fukuoka' }, ], }; const tagTypes = [ { id: '100', label: 'Product highlights' }, { id: '200', label: 'Materials & care' }, ]; const tags = [ { id: '101', typeId: '100', label: 'Best seller' }, { id: '102', typeId: '100', label: 'New arrival' }, { id: '201', typeId: '200', label: 'Organic / natural materials' }, { id: '202', typeId: '200', label: 'Contains recycled content' }, ]; return ( <EditProduct data={data} selectOptions={selectOptions} tagTypes={tagTypes} tags={tags} onDataChange={(nextData) => { setData(nextData); }} onSubmit={(e) => { e.preventDefault(); }} > <EditProduct.MainInfo> <EditProduct.MainInfo.Dropzone /> <EditProduct.MainInfo.TitleField /> </EditProduct.MainInfo> <EditProduct.BasicInfo> <EditProduct.BasicInfo.SectionTitle /> <EditProduct.BasicInfo.CategoryField /> <EditProduct.BasicInfo.TypeField /> <EditProduct.BasicInfo.OptionField /> <EditProduct.BasicInfo.QuantityField /> <EditProduct.BasicInfo.DescriptionField /> <EditProduct.BasicInfo.DetailsField /> <EditProduct.BasicInfo.TagsField /> </EditProduct.BasicInfo> <EditProduct.AdditionalInfo> <EditProduct.AdditionalInfo.Title /> <EditProduct.AdditionalInfo.Subtitle /> <EditProduct.AdditionalInfo.PrefectureField /> <EditProduct.AdditionalInfo.CityField /> <EditProduct.AdditionalInfo.OnlineAvailabilityField /> <EditProduct.AdditionalInfo.InventoryNotesField /> <EditProduct.AdditionalInfo.AvailableFromField /> <EditProduct.AdditionalInfo.AvailableUntilField /> <EditProduct.AdditionalInfo.AdditionalDetailsField /> </EditProduct.AdditionalInfo> <EditProduct.Actions> <EditProduct.Actions.SubmitButton>Save changes</EditProduct.Actions.SubmitButton> </EditProduct.Actions> </EditProduct> ); }
結果
Loading...
ライブエディター
function Example() { const defaultData = { title: '', categoryId: '', typeId: '', optionId: '', quantity: '', description: '', details: '', prefecture: '', city: '', onlineAvailability: false, inventoryNotes: '', availableFrom: '', availableUntil: '', additionalDetails: '', image: null, tags: [], }; const [data, setData] = React.useState(defaultData); return ( <EditProduct data={data} onDataChange={(nextData) => { setData(nextData); }} onSubmit={(e) => { e.preventDefault(); }} > {({ defaultBlocks, defaultBlockOrder }) => ({ blocks: { ...defaultBlocks, customNotification: ( <div style={{ padding: 12, border: '1px solid #b6d4fe', background: '#eef6ff' }}> Custom notification block added via Block Override pattern </div> ), }, blockOrder: ['customNotification', ...defaultBlockOrder], })} </EditProduct> ); }
結果
Loading...
ブロックのオーバーライドに関する注意
defaultBlockOrder は、ルートセクションと既定の入力プリミティブの標準順です。ルートレンダラーはオーバーライド用に defaultEditProductBlocks に入力プリミティブを保持していますが、表示される既定順からは除外します。デフォルトブロックを追加・削除・並べ替えしたいときは関数 child を使ってください。
重要な props
基本 props
| Prop | 型 | 必須 | 既定値 | 説明 |
|---|---|---|---|---|
data | EditProductFormData ({ title, categoryId, typeId, optionId, quantity, description, details, tags, prefecture, city, onlineAvailability, inventoryNotes, availableFrom, availableUntil, additionalDetails, image } or extended Record<string, unknown>) | Yes | - | 制御されたフォーム状態。 |
onDataChange | (data: EditProductFormData, meta: { name: string; value: unknown; cause: one of input, change, blur, clear, reset, programmatic; event?: React.SyntheticEvent }) => void | Yes | - | フィールドが変更されるたびに呼び出されます。meta.name はブラケット記法のパスを使います。 |
errors | { [fieldPath: string]: string | string[] } | No | undefined | 入れ子の配列パスを含む、フィールドパスをキーにしたバリデーションフィードバック。 |
onRejectAttachment | (file: File, error: DropzoneFileError) => void | No | undefined | ドロップゾーンがアップロードを拒否したときに呼び出されます。 |
tagTypes | TagType[] ({ id: string; label: string }[]) | No | undefined | BasicInfo.TagsField で使われるタググループ。 |
tags | Tag[] ({ id: string; typeId?: string; label: string }[]) | No | undefined | BasicInfo.TagsField で使われる利用可能なタグオプション。 |
コンテンツ props
| Prop | 型 | 必須 | 既定値 | 説明 |
|---|---|---|---|---|
labels | { mainInfoSectionTitle?: string; titleField?: string; dropzoneDropHere?: string; dropzoneUploadImage?: string; dropzoneSubtitle?: string; dropzoneImageAlt?: string; dropzoneOptionsButton?: string; dropzoneReplaceFile?: string; dropzoneDeleteFile?: string; basicInfoSectionTitle?: string; categoryField?: string; typeField?: string; optionField?: string; quantityField?: string; quantityUnit?: string; descriptionField?: string; detailsField?: string; tagsField?: string; additionalInfoTitle?: string; additionalInfoSubtitle?: string; prefectureField?: string; cityField?: string; onlineAvailabilityField?: string; inventoryNotesField?: string; availableFromField?: string; availableUntilField?: string; additionalDetailsField?: string; submitButton?: string } | No | undefined | セクションタイトル、フィールドラベル、ドロップゾーン文言、画像の alt 文言、数量単位の接尾辞、送信ボタンの文言です。 |
placeholders | { titleField?: string; categoryField?: string; typeField?: string; optionField?: string; quantityField?: string; descriptionField?: string; detailsField?: string; prefectureField?: string; cityField?: string; inventoryNotesField?: string; additionalDetailsField?: string } | No | undefined | テキストフィールドとセレクトフィールドのプレースホルダー文言です。 |
selectOptions | { categoryOptions?: EditProductSelectOption[]; typeOptions?: EditProductSelectOption[]; optionOptions?: EditProductSelectOption[]; prefectureOptions?: EditProductSelectOption[] } | No | undefined | セレクトフィールドで使われるドロップダウンオプションです。 |
レイアウトと構成 props
| Prop | 型 | 必須 | 既定値 | 説明 |
|---|---|---|---|---|
component | StackProps<'form'>['component'] | No | 'form' | 外側の Stack によって描画されるルート要素。 |
children | BlocksOverride<typeof defaultEditProductBlocks, CustomBlocks> | No | undefined | JSX の複合セクション、または blocks と blockOrder を返す関数を使用します。 |
継承される props は StackProps<'form'> 由来で、children がブロックのオーバーライド API に置き換えられているため、className、sx、id、onSubmit などの標準的な form/container props を利用できます。
デフォルト UI ブロック
| ブロック | 基盤 | 備考 |
|---|---|---|
EditProduct | Stack | レスポンシブな余白と最大幅を持つルート form シェルです。 |
MainInfo | Stack | アップロードドロップゾーンとタイトルフィールドのセクションです。 |
MainInfo.Dropzone | Box + Menu + MenuItem + react-dropzone | image 用のファイルアップロード面です。既定の文言には Drop the image here...、Upload product image、PNG, JPG up to 2MB、Options、Select a new file、Delete、alt テキスト Selected が含まれます。 |
MainInfo.TitleField | TextField | 文字数カウンター付きの複数行タイトル入力です。既定のラベルは Title、プレースホルダーは Enter title です。 |
BasicInfo | Stack | カテゴリ、種別、数量、説明、詳細、タグのための商品詳細セクションです。 |
BasicInfo.SectionTitle | Typography | 既定値は Basic Information です。 |
BasicInfo.CategoryField | SelectField | categoryId にバインドされたセレクトフィールドです。既定のラベルは Category、プレースホルダーは Select category です。 |
BasicInfo.TypeField | SelectField | typeId にバインドされたセレクトフィールドです。既定のラベルは Type、プレースホルダーは Select type です。 |
BasicInfo.OptionField | SelectField | optionId にバインドされたセレクトフィールドです。既定のラベルは Option、プレースホルダーは Select option です。 |
BasicInfo.QuantityField | FormControl + OutlinedInput | quantity にバインドされた数量入力で、単位テキスト付きです。既定のラベルは Quantity、プレースホルダーは Enter quantity、単位は pcs です。 |
BasicInfo.DescriptionField | TextField | description にバインドされた複数行説明フィールドです。既定値は Description / Describe the product です。 |
BasicInfo.DetailsField | TextField | details にバインドされた複数行詳細フィールドです。既定値は Details / Enter details です。 |
BasicInfo.TagsField | FormControl + FormGroup + Checkbox | グループ化されたタグのチェックボックスを描画します。tagTypes と tags が提供されたときだけ描画されます。既定値は Tags です。 |
AdditionalInfo | Stack | 在庫と所在地のセクションです。 |
AdditionalInfo.Title | Typography | 既定値は Inventory です。 |
AdditionalInfo.Subtitle | Typography | 既定値は Location です。 |
AdditionalInfo.PrefectureField | SelectField | prefecture にバインドされたセレクトフィールドです。既定のラベルは Prefecture、プレースホルダーは Select prefecture です。 |
AdditionalInfo.CityField | TextField | city にバインドされたテキストフィールドです。既定のラベルは City、プレースホルダーは Enter city です。 |
AdditionalInfo.OnlineAvailabilityField | FormControlLabel + Checkbox | onlineAvailability にバインドされた真偽値チェックボックスです。既定値は Online Availability です。 |
AdditionalInfo.InventoryNotesField | TextField | inventoryNotes にバインドされた複数行メモ欄です。既定値は Inventory Notes / Notes about inventory です。 |
AdditionalInfo.AvailableFromField | TimePicker | availableFrom にバインドされた時刻フィールドで、HH:mm として保存されます。既定値は Available From です。 |
AdditionalInfo.AvailableUntilField | TimePicker | availableUntil にバインドされた時刻フィールドで、HH:mm として保存されます。既定値は Available Until です。 |
AdditionalInfo.AdditionalDetailsField | TextField | additionalDetails にバインドされた複数行フィールドです。既定値は Additional Details / Enter additional details です。 |
Actions | Stack + Button | フォーム下部中央に配置される送信領域です。 |
Actions.SubmitButton | Button | variant="contained", size="large", type="submit"、チェックアイコン付きです。既定値は Submit です。 |
追加のフィールドプリミティブ
| プリミティブ | 主な props | 継承 | 基盤 | 備考 |
|---|---|---|---|---|
TextField | name, label, placeholder, required | コンテキストの getValue、setValue、errors に加えて TextFieldProps | TextField | 標準レイアウトで使われる制御されたテキスト入力です。 |
NumberField | name, label, placeholder, required | コンテキストの getValue、setValue、errors に加えて TextFieldProps | TextField | type="number" と min=0 を持つ数値テキストフィールドです。 |
SelectField | name, options, placeholder, label | コンテキストの getValue、setValue、errors に加えて TextFieldProps | TextField | 値が選択されていないときにプレースホルダーテキストを表示するセレクト入力です。 |
CheckboxField | name, label | control が MUI Checkbox に固定された FormControlLabelProps | FormControlLabel + Checkbox | OnlineAvailabilityField で使われる制御された真偽値フィールドです。 |
TimeField | name, label, required, helperText | コンテキストの getValue、setValue、errors に加えて TimePickerProps | TimePicker | DateTime 変換を通じて時刻値を HH:mm 文字列として保存します。 |
TypeScript
export type EditProductFormData =
| {
title: string;
categoryId: string;
typeId: string;
optionId: string;
quantity: string;
description: string;
details: string;
tags: Tag[];
prefecture: string;
city: string;
onlineAvailability: boolean;
inventoryNotes: string;
availableFrom: string;
availableUntil: string;
additionalDetails: string;
image: { url: string; type?: string; id?: string } | File | null;
}
| Record<string, unknown>;
export interface TagType {
id: string;
label: string;
}
export interface Tag {
id: string;
typeId?: string;
label: string;
}
export type EditProductSelectOption = {
value: string;
label: string;
};