商品作成ブロック
CreateProduct は MUI ベースの制御型商品作成フォームです。
インストール
- npm
- yarn
- pnpm
- bun
npm install @nodeblocks/frontend-create-product-block
yarn add @nodeblocks/frontend-create-product-block
pnpm add @nodeblocks/frontend-create-product-block
bun add @nodeblocks/frontend-create-product-block
必要なもの
| 項目 | 理由 |
|---|---|
data | フォーム状態の単一の信頼源。title, categoryId, quantity, tags, prefecture, image などのフィールドを含みます |
onDataChange | 検証、分析、または副作用のための更新済み状態とメタデータを受け取ります |
errors (optional) | ブラケット記法のパスをキーにしたフィールドエラー |
labels (optional) | セクションタイトル、フィールドラベル、ボタン文言、ドロップゾーンメッセージ、単位サフィックスのルート文言 |
placeholders (optional) | テキスト、数値、セレクトフィールドのルートプレースホルダー文言 |
selectOptions (optional) | カテゴリ、種別、オプション、都道府県フィールドのドロップダウン選択肢 |
tagTypes and tags (optional) | BasicInfo.TagsField のタグセクションを有効にします |
onRejectAttachment (optional) | 画像ドロップゾーンがファイルを拒否したときに呼び出されます |
CreateProduct はフォーム状態を保持しません。状態はアプリ側で管理し、data に渡してください。デフォルトの形には title, categoryId, typeId, optionId, quantity, description, details, tags, prefecture, city, onlineAvailability, inventoryNotes, availableFrom, availableUntil, additionalDetails, image が含まれます。
コード例
- クイックスタート
- ラベルとコピー
- フォームエラー
- コンパウンドコンポーネント
- ブロックオーバーライド
function Example() { const defaultData = { title: '', categoryId: '', typeId: '', optionId: '', quantity: '', description: '', details: '', tags: [], prefecture: '', city: '', onlineAvailability: false, inventoryNotes: '', availableFrom: '', availableUntil: '', additionalDetails: '', image: null, }; const [data, setData] = React.useState(defaultData); const prefectureOptions = [ { value: 'Hokkaido', label: 'Hokkaido' }, { value: 'Tokyo', label: 'Tokyo' }, { value: 'Osaka', label: 'Osaka' }, { value: 'Okinawa', label: 'Okinawa' }, ]; return ( <CreateProduct data={data} onDataChange={setData} selectOptions={{ categoryOptions: [ { value: 'electronics', label: 'Electronics' }, { value: 'clothing', label: 'Clothing' }, ], typeOptions: [ { value: 'physical', label: 'Physical' }, { value: 'digital', label: 'Digital' }, ], optionOptions: [ { value: 'standard', label: 'Standard' }, { value: 'premium', label: 'Premium' }, ], prefectureOptions, }} tagTypes={[ { id: '100', label: 'Highlights' }, { id: '200', label: 'Shipping' }, ]} tags={[ { id: '101', typeId: '100', label: 'Best seller' }, { id: '201', typeId: '200', label: 'Ships from Japan' }, ]} onSubmit={(event) => { event.preventDefault(); setData((current) => ({ ...current })); }} /> ); }
function Example() { const defaultData = { title: '', categoryId: '', typeId: '', optionId: '', quantity: '', description: '', details: '', tags: [], prefecture: '', city: '', onlineAvailability: false, inventoryNotes: '', availableFrom: '', availableUntil: '', additionalDetails: '', image: null, }; const [data, setData] = React.useState(defaultData); const prefectureOptions = [ { value: 'Hokkaido', label: 'Hokkaido' }, { value: 'Tokyo', label: 'Tokyo' }, { value: 'Osaka', label: 'Osaka' }, { value: 'Okinawa', label: 'Okinawa' }, ]; return ( <CreateProduct data={data} onDataChange={setData} labels={{ titleField: 'Product title', dropzoneUploadImage: 'Upload a product image', dropzoneSubtitle: 'PNG or JPG, up to 2MB', dropzoneOptionsButton: 'Image options', dropzoneReplaceFile: 'Replace image', dropzoneDeleteFile: 'Remove image', basicInfoSectionTitle: '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: 'Available online', inventoryNotesField: 'Inventory notes', availableFromField: 'Available from', availableUntilField: 'Available until', additionalDetailsField: 'Additional details', submitButton: 'Create product', }} placeholders={{ titleField: 'Enter a product title', categoryField: 'Select a category', typeField: 'Select a type', optionField: 'Select an 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', }} selectOptions={{ categoryOptions: [ { value: 'electronics', label: 'Electronics' }, { value: 'clothing', label: 'Clothing' }, ], typeOptions: [ { value: 'physical', label: 'Physical' }, { value: 'digital', label: 'Digital' }, ], optionOptions: [ { value: 'standard', label: 'Standard' }, { value: 'premium', label: 'Premium' }, ], prefectureOptions, }} onSubmit={(event) => { event.preventDefault(); setData((current) => ({ ...current })); }} /> ); }
同じ文言をフォーム全体に適用したい場合は、ルートにセクション文言とフィールド文言を渡してください。レイアウト固有の文言が必要な場合に限り、画像ドロップゾーン文言、フィールドラベル、送信ラベルを入れ子のサブコンポーネントで上書きしてください。
function Example() { const defaultData = { title: '', categoryId: '', typeId: '', optionId: '', quantity: '', description: '', details: '', tags: [], prefecture: '', city: '', onlineAvailability: false, inventoryNotes: '', availableFrom: '', availableUntil: '', additionalDetails: '', image: null, }; const [data, setData] = React.useState(defaultData); const prefectureOptions = [ { value: 'Hokkaido', label: 'Hokkaido' }, { value: 'Tokyo', label: 'Tokyo' }, { value: 'Osaka', label: 'Osaka' }, { value: 'Okinawa', label: 'Okinawa' }, ]; const [errors, setErrors] = React.useState({}); const handleDataChange = (nextData, meta) => { const { [meta.name]: _removed, ...restErrors } = errors; const nextErrors = { ...restErrors }; // Validate required fields on blur (same pattern as storybook) if (meta.cause === 'blur') { const value = nextData[meta.name]; if (value === '' || value == null || (Array.isArray(value) && value.length === 0)) { nextErrors[meta.name] = 'This field is required.'; } } setErrors(nextErrors); setData(nextData); }; return ( <CreateProduct data={data} errors={Object.keys(errors).length ? errors : undefined} onDataChange={handleDataChange} selectOptions={{ categoryOptions: [ { value: 'electronics', label: 'Electronics' }, { value: 'clothing', label: 'Clothing' }, ], typeOptions: [ { value: 'physical', label: 'Physical' }, { value: 'digital', label: 'Digital' }, ], optionOptions: [ { value: 'standard', label: 'Standard' }, { value: 'premium', label: 'Premium' }, ], prefectureOptions, }} tagTypes={[ { id: '100', label: 'Highlights' }, { id: '200', label: 'Shipping' }, ]} tags={[ { id: '101', typeId: '100', label: 'Best seller' }, { id: '201', typeId: '200', label: 'Ships from Japan' }, ]} onSubmit={(event) => { event.preventDefault(); setData((current) => ({ ...current })); }} /> ); }
function Example() { const defaultData = { title: '', categoryId: '', typeId: '', optionId: '', quantity: '', description: '', details: '', tags: [], prefecture: '', city: '', onlineAvailability: false, inventoryNotes: '', availableFrom: '', availableUntil: '', additionalDetails: '', image: null, }; const [data, setData] = React.useState(defaultData); const prefectureOptions = [ { value: 'Hokkaido', label: 'Hokkaido' }, { value: 'Tokyo', label: 'Tokyo' }, { value: 'Osaka', label: 'Osaka' }, { value: 'Okinawa', label: 'Okinawa' }, ]; return ( <CreateProduct data={data} onDataChange={setData} selectOptions={{ categoryOptions: [ { value: 'electronics', label: 'Electronics' }, { value: 'clothing', label: 'Clothing' }, ], typeOptions: [ { value: 'physical', label: 'Physical' }, { value: 'digital', label: 'Digital' }, ], optionOptions: [ { value: 'standard', label: 'Standard' }, { value: 'premium', label: 'Premium' }, ], prefectureOptions, }} tagTypes={[ { id: '100', label: 'Highlights' }, { id: '200', label: 'Shipping' }, ]} tags={[ { id: '101', typeId: '100', label: 'Best seller' }, { id: '201', typeId: '200', label: 'Ships from Japan' }, ]} onSubmit={(event) => { event.preventDefault(); setData((current) => ({ ...current })); }} > <CreateProduct.MainInfo> <CreateProduct.MainInfo.Dropzone /> <CreateProduct.MainInfo.TitleField /> </CreateProduct.MainInfo> <CreateProduct.BasicInfo> <CreateProduct.BasicInfo.SectionTitle /> <CreateProduct.BasicInfo.CategoryField /> <CreateProduct.BasicInfo.TypeField /> <CreateProduct.BasicInfo.OptionField /> <CreateProduct.BasicInfo.QuantityField /> <CreateProduct.BasicInfo.DescriptionField /> <CreateProduct.BasicInfo.DetailsField /> <CreateProduct.BasicInfo.TagsField /> </CreateProduct.BasicInfo> <CreateProduct.AdditionalInfo> <CreateProduct.AdditionalInfo.Title /> <CreateProduct.AdditionalInfo.Subtitle /> <CreateProduct.AdditionalInfo.PrefectureField /> <CreateProduct.AdditionalInfo.CityField /> <CreateProduct.AdditionalInfo.OnlineAvailabilityField /> <CreateProduct.AdditionalInfo.InventoryNotesField /> <CreateProduct.AdditionalInfo.AdditionalDetailsField /> </CreateProduct.AdditionalInfo> <CreateProduct.Actions> <CreateProduct.Actions.SubmitButton>Create Product</CreateProduct.Actions.SubmitButton> </CreateProduct.Actions> </CreateProduct> ); }
function Example() { const defaultData = { title: '', categoryId: '', typeId: '', optionId: '', quantity: '', description: '', details: '', tags: [], prefecture: '', city: '', onlineAvailability: false, inventoryNotes: '', availableFrom: '', availableUntil: '', additionalDetails: '', image: null, }; const [data, setData] = React.useState(defaultData); const prefectureOptions = [ { value: 'Hokkaido', label: 'Hokkaido' }, { value: 'Tokyo', label: 'Tokyo' }, { value: 'Osaka', label: 'Osaka' }, { value: 'Okinawa', label: 'Okinawa' }, ]; return ( <CreateProduct data={data} onDataChange={setData} selectOptions={{ categoryOptions: [ { value: 'electronics', label: 'Electronics' }, { value: 'clothing', label: 'Clothing' }, ], typeOptions: [ { value: 'physical', label: 'Physical' }, { value: 'digital', label: 'Digital' }, ], optionOptions: [ { value: 'standard', label: 'Standard' }, { value: 'premium', label: 'Premium' }, ], prefectureOptions, }} tagTypes={[ { id: '100', label: 'Highlights' }, { id: '200', label: 'Shipping' }, ]} tags={[ { id: '101', typeId: '100', label: 'Best seller' }, { id: '201', typeId: '200', label: 'Ships from Japan' }, ]} onSubmit={(event) => { event.preventDefault(); setData((current) => ({ ...current })); }} > {({ defaultBlocks, defaultBlockOrder }) => ({ blocks: { ...defaultBlocks, customNotification: ( <div style={{ width: '100%', border: '1px solid #cddcff', borderRadius: 8, padding: 12, background: '#eef4ff', fontSize: 14, }} > Custom notification block added via block override </div> ), }, blockOrder: ['customNotification', ...defaultBlockOrder], })} </CreateProduct> ); }
ブロックオーバーライドを使うタイミング
順序を変更したい場合、デフォルト UI ブロックを置き換えたい場合、または共有状態の処理を維持したままカスタムコンテンツを挿入したい場合にオーバーライドを使います。デフォルトの描画順は mainInfo, basicInfo, additionalInfo, actions で、その後にカスタムフロー向けの分離された入力プリミティブ textField, numberField, timeField, checkboxField, selectField が続きます。
重要なプロパティ
コアプロパティ
| プロパティ | 型 | 必須 | デフォルト | 説明 |
|---|---|---|---|---|
data | CreateProductFormData ({ title, categoryId, typeId, optionId, quantity, description, details, tags, prefecture, city, onlineAvailability, inventoryNotes, availableFrom, availableUntil, additionalDetails, image } or extended Record<string, unknown>) | Yes | - | 制御されたフォームデータオブジェクト |
onDataChange | (data, meta) => void | Yes | - | 更新時に呼び出されます。meta には name, value, cause(input, change, blur, clear, reset, programmatic)、および任意の event が含まれます |
errors | { [fieldName: string]: string | string[] } | No | undefined | ブラケット記法のパスをキーにしたフィールドエラー such as title, categoryId, image, or nested custom paths |
labels | { mainInfoSectionTitle?, titleField?, dropzoneDropHere?, dropzoneUploadImage?, dropzoneSubtitle?, dropzoneImageAlt?, dropzoneOptionsButton?, dropzoneReplaceFile?, dropzoneDeleteFile?, basicInfoSectionTitle?, categoryField?, typeField?, optionField?, quantityField?, quantityUnit?, descriptionField?, detailsField?, tagsField?, additionalInfoTitle?, additionalInfoSubtitle?, prefectureField?, cityField?, onlineAvailabilityField?, inventoryNotesField?, availableFromField?, availableUntilField?, additionalDetailsField?, submitButton? } | No | mainInfoSectionTitle: 'Create a Product', titleField: 'Title', dropzoneDropHere: 'Drop the image here...', dropzoneUploadImage: 'Upload product image', dropzoneSubtitle: 'PNG, JPG up to 2MB', dropzoneImageAlt: 'Selected', dropzoneOptionsButton: 'Options', dropzoneReplaceFile: 'Select a new file', dropzoneDeleteFile: 'Delete', basicInfoSectionTitle: 'Basic Information', 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: 'Submit' | タイトル、各セクション、フィールドラベル、ドロップゾーン文言、送信ボタンのルート文言 |
placeholders | { titleField?, categoryField?, typeField?, optionField?, quantityField?, descriptionField?, detailsField?, prefectureField?, cityField?, inventoryNotesField?, additionalDetailsField? } | No | titleField: 'Enter title', categoryField: 'Select category', typeField: 'Select type', optionField: 'Select option', quantityField: 'Enter quantity', descriptionField: 'Describe the product', detailsField: 'Enter details', prefectureField: 'Enter prefecture', cityField: 'Enter city', inventoryNotesField: 'Notes about inventory', additionalDetailsField: 'Enter additional details' | テキスト、数値、セレクトフィールドのルートプレースホルダー文言 |
selectOptions | { categoryOptions?, typeOptions?, optionOptions?, prefectureOptions? } | No | undefined | セレクトフィールド用のドロップダウン選択肢セット。省略されたグループは空リストとして描画されます |
tagTypes | TagType[] | No | undefined | BasicInfo.TagsField でのグループ表示を有効にします |
tags | Tag[] | No | undefined | BasicInfo.TagsField に表示される利用可能なタグ |
onRejectAttachment | (file: File, error: DropzoneFileError) => void | No | undefined | ドロップゾーンが画像ファイルを拒否したときに呼び出されます |
コンテンツプロパティ
labels, placeholders, selectOptions, tagTypes, tags 以外のルート専用コンテンツプロパティは公開されていません。
レイアウトと構成のプロパティ
| プロパティ | 型 | 必須 | デフォルト | 説明 |
|---|---|---|---|---|
component | StackProps<'form'>['component'] | No | 'form' | ルートコンテナ要素 |
children | ReactNode or block override function | No | undefined | コンパウンドコンポーネント用の JSX children を渡すか、ブロックをオーバーライドする関数を渡します |
className | string | No | undefined | ルートコンテナの追加クラス名 |
sx | StackProps<'form'>['sx'] | No | undefined | ルートコンテナ用の MUI sx スタイリング |
children を除く StackProps<'form'> の props を継承します。ルートはデフォルトで mainInfo, basicInfo, additionalInfo, actions を描画します。内部の入力プリミティブ TextField, NumberField, TimeField, CheckboxField, SelectField はカスタムレイアウト用に個別にエクスポートされます。
デフォルト UI ブロック
| ブロック | MUI ベースコンポーネント | 備考 |
|---|---|---|
CreateProduct | Stack + LocalizationProvider | ルートプロバイダー兼レイアウトコンテナ。component="form" がデフォルトで、中央寄せの内容と固定の最大幅を備えます |
MainInfo | Stack | 画像アップロードとタイトルのための上部セクション |
MainInfo.SectionTitle | Typography | デフォルト文言は 商品を作成 です |
MainInfo.Dropzone | Box + Button + Menu + Typography | name="image"、デフォルトのドロップメッセージは 画像をここにドロップ...、アップロード案内は 商品画像をアップロード、サブタイトルは PNG、JPG、最大2MB、オプションボタンは オプション です |
MainInfo.TitleField | TextField | name="title"、デフォルトラベルは タイトル、デフォルトプレースホルダーは タイトルを入力、最大長は 100 です |
BasicInfo | Stack | カテゴリ、数量、説明、詳細、タグのための中間セクション |
BasicInfo.SectionTitle | Typography | デフォルト文言は 基本情報 です |
BasicInfo.CategoryField | SelectField | name="categoryId"、デフォルトラベルは カテゴリ、デフォルトプレースホルダーは カテゴリを選択 です |
BasicInfo.TypeField | SelectField | name="typeId"、デフォルトラベルは 種別、デフォルトプレースホルダーは 種別を選択 です |
BasicInfo.OptionField | SelectField | name="optionId"、デフォルトラベルは オプション、デフォルトプレースホルダーは オプションを選択 です |
BasicInfo.QuantityField | FormControl + FormLabel + OutlinedInput | name="quantity"、デフォルトラベルは 数量、デフォルトプレースホルダーは 数量を入力、単位サフィックスは pcs です |
BasicInfo.DescriptionField | TextField | name="description"、デフォルトラベルは 説明、デフォルトプレースホルダーは 商品の説明を入力、複数行入力です |
BasicInfo.DetailsField | TextField | name="details"、デフォルトラベルは 詳細、デフォルトプレースホルダーは 詳細を入力、複数行入力です |
BasicInfo.TagsField | FormControl + FormLabel + FormGroup + Checkbox | name="tags"、デフォルトラベルは タグ。tagTypes と tags の両方が指定された場合のみ描画されます |
AdditionalInfo | Stack | 在庫と公開可用性のための下部セクション |
AdditionalInfo.Title | Typography | デフォルト文言は 在庫 です |
AdditionalInfo.Subtitle | Typography | デフォルト文言は 所在地 です |
AdditionalInfo.PrefectureField | SelectField | name="prefecture"、デフォルトラベルは 都道府県、デフォルトプレースホルダーは 都道府県を選択 です |
AdditionalInfo.CityField | TextField | name="city"、デフォルトラベルは 市区町村、デフォルトプレースホルダーは 市区町村を入力 です |
AdditionalInfo.OnlineAvailabilityField | FormControlLabel + Checkbox | name="onlineAvailability"、デフォルトラベルは オンラインで利用可能 です |
AdditionalInfo.InventoryNotesField | TextField | name="inventoryNotes"、デフォルトラベルは 在庫メモ、デフォルトプレースホルダーは 在庫に関するメモ、複数行入力です |
AdditionalInfo.AvailableFromField | TimePicker | name="availableFrom"、デフォルトラベルは 公開開始、保存形式は HH:mm です |
AdditionalInfo.AvailableUntilField | TimePicker | name="availableUntil"、デフォルトラベルは 公開終了、保存形式は HH:mm です |
AdditionalInfo.AdditionalDetailsField | TextField | name="additionalDetails"、デフォルトラベルは 追加詳細、デフォルトプレースホルダーは 追加詳細を入力、複数行入力です |
Actions | Stack | ページ中央に配置された下部アクション行 |
Actions.SubmitButton | Button | デフォルト文言は 送信 で、先頭にチェックアイコンが付きます |
追加フィールドプリミティブ
| プリミティブ | MUI ベースコンポーネント | 備考 |
|---|---|---|
TextField | TextField | 共有の制御テキストプリミティブ。CreateProduct.TextField としてエクスポートされます |
NumberField | TextField | 共有の制御数値プリミティブ。CreateProduct.NumberField としてエクスポートされます |
TimeField | TimePicker | 共有の制御時刻プリミティブ。CreateProduct.TimeField としてエクスポートされ、値は HH:mm 形式で保存されます |
CheckboxField | FormControlLabel + Checkbox | 共有の制御チェックボックスプリミティブ。CreateProduct.CheckboxField としてエクスポートされます |
SelectField | TextField | 共有の制御セレクトプリミティブ。CreateProduct.SelectField としてエクスポートされます |
TypeScript
export type CreateProductFormData =
| {
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>;