Edit Product Block
EditProduct is a controlled product-edit form block built on MUI, with nested sections for media, product details, inventory, and submit actions.
Installation
- 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
What You Need
| Item | Why it matters |
|---|---|
data | Controlled form state. The stock shape includes title, categoryId, typeId, optionId, quantity, description, details, prefecture, city, onlineAvailability, inventoryNotes, availableFrom, availableUntil, additionalDetails, image, and tags. |
onDataChange | Receives the next data snapshot plus metadata for the changed field path (meta.name), latest value (meta.value), cause (meta.cause), and optional event (meta.event). |
errors (optional) | Field-level validation keyed by bracket-notation paths. |
onRejectAttachment (optional) | Receives rejected upload files from the image dropzone with a DropzoneFileError. |
labels (optional) | Copy for section titles, field labels, the dropzone, and the submit button. |
placeholders (optional) | Placeholder text for text and select fields. |
selectOptions (optional) | Dropdown options for category, type, option, and prefecture fields. |
tagTypes / tags (optional) | Drive the tag picker in the basic-info section. |
children (optional) | Use compound sections or a block override function. |
EditProduct does not own form state. Keep data in your app and pass updates back through onDataChange. data.image can be File, { url, type?, id? }, or null, and data.tags is an array of Tag objects.
Code Examples
- Quick Start
- Labels and Copy
- Form Errors
- Compound Components
- Block Override
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(); }} /> ); }
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(); }} /> ); }
This block does not have URL props. Override copy with labels and placeholders on the root, or replace the stock text by using the compound sections below.
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(); }} /> ); }
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> ); }
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> ); }
defaultBlockOrder is the stock order for the root sections and default input primitives. The root renderer keeps the input primitives in defaultEditProductBlocks for override access, but filters them out of the visible default order. Use function children when you want to add, remove, or reorder default blocks.
Important Props
Core Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
data | EditProductFormData ({ title, categoryId, typeId, optionId, quantity, description, details, tags, prefecture, city, onlineAvailability, inventoryNotes, availableFrom, availableUntil, additionalDetails, image } or extended Record<string, unknown>) | Yes | - | Controlled form state. |
onDataChange | (data: EditProductFormData, meta: { name: string; value: unknown; cause: one of input, change, blur, clear, reset, programmatic; event?: React.SyntheticEvent }) => void | Yes | - | Called whenever a field changes. meta.name uses bracket-notation paths. |
errors | { [fieldPath: string]: string | string[] } | No | undefined | Validation feedback keyed by the field path, including nested array paths. |
onRejectAttachment | (file: File, error: DropzoneFileError) => void | No | undefined | Called when the dropzone rejects an upload. |
tagTypes | TagType[] ({ id: string; label: string }[]) | No | undefined | Tag groups used by BasicInfo.TagsField. |
tags | Tag[] ({ id: string; typeId?: string; label: string }[]) | No | undefined | Available tag options used by BasicInfo.TagsField. |
Content Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
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 | Copy for section titles, field labels, dropzone text, image alt text, the quantity unit suffix, and the submit button. |
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 | Placeholder text for the text and select fields. |
selectOptions | { categoryOptions?: EditProductSelectOption[]; typeOptions?: EditProductSelectOption[]; optionOptions?: EditProductSelectOption[]; prefectureOptions?: EditProductSelectOption[] } | No | undefined | Dropdown options used by the select fields. |
Layout and Composition Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
component | StackProps<'form'>['component'] | No | 'form' | Root element rendered by the outer stack. |
children | BlocksOverride<typeof defaultEditProductBlocks, CustomBlocks> | No | undefined | Use JSX compound sections or a function that returns blocks and blockOrder. |
Inherited props come from StackProps<'form'> with children replaced by the block override API, so standard form/container props such as className, sx, id, and onSubmit are available.
Default UI Blocks
| Block | Built on | Notes |
|---|---|---|
EditProduct | Stack | Root form shell with responsive padding and max width. |
MainInfo | Stack | Section for the upload dropzone and title field. |
MainInfo.Dropzone | Box + Menu + MenuItem + react-dropzone | File upload surface for image; default text includes Drop the image here..., Upload product image, PNG, JPG up to 2MB, Options, Select a new file, Delete, and the alt text Selected. |
MainInfo.TitleField | TextField | Multiline title input with a character counter. Defaults to label Title and placeholder Enter title. |
BasicInfo | Stack | Product detail section with category, type, quantity, description, details, and tags. |
BasicInfo.SectionTitle | Typography | Defaults to Basic Information. |
BasicInfo.CategoryField | SelectField | Select field bound to categoryId. Defaults to label Category and placeholder Select category. |
BasicInfo.TypeField | SelectField | Select field bound to typeId. Defaults to label Type and placeholder Select type. |
BasicInfo.OptionField | SelectField | Select field bound to optionId. Defaults to label Option and placeholder Select option. |
BasicInfo.QuantityField | FormControl + OutlinedInput | Quantity input bound to quantity, with unit text. Defaults to label Quantity, placeholder Enter quantity, unit pcs. |
BasicInfo.DescriptionField | TextField | Multiline description field bound to description. Defaults to Description / Describe the product. |
BasicInfo.DetailsField | TextField | Multiline details field bound to details. Defaults to Details / Enter details. |
BasicInfo.TagsField | FormControl + FormGroup + Checkbox | Renders grouped tag checkboxes. Only renders when tagTypes and tags are provided. Defaults to Tags. |
AdditionalInfo | Stack | Inventory and location section. |
AdditionalInfo.Title | Typography | Defaults to Inventory. |
AdditionalInfo.Subtitle | Typography | Defaults to Location. |
AdditionalInfo.PrefectureField | SelectField | Select field bound to prefecture. Defaults to label Prefecture and placeholder Select prefecture. |
AdditionalInfo.CityField | TextField | Text field bound to city. Defaults to label City and placeholder Enter city. |
AdditionalInfo.OnlineAvailabilityField | FormControlLabel + Checkbox | Boolean checkbox bound to onlineAvailability. Defaults to Online Availability. |
AdditionalInfo.InventoryNotesField | TextField | Multiline notes field bound to inventoryNotes. Defaults to Inventory Notes / Notes about inventory. |
AdditionalInfo.AvailableFromField | TimePicker | Time field bound to availableFrom and stored as HH:mm. Defaults to Available From. |
AdditionalInfo.AvailableUntilField | TimePicker | Time field bound to availableUntil and stored as HH:mm. Defaults to Available Until. |
AdditionalInfo.AdditionalDetailsField | TextField | Multiline field bound to additionalDetails. Defaults to Additional Details / Enter additional details. |
Actions | Stack + Button | Submit area centered under the form. |
Actions.SubmitButton | Button | variant="contained", size="large", type="submit", with a check icon. Defaults to Submit. |
Extra field primitives
| Primitive | Main Props | Inherits | Built on | Notes |
|---|---|---|---|---|
TextField | name, label, placeholder, required | TextFieldProps plus context getValue, setValue, and errors | TextField | Controlled text input used by the stock layout. |
NumberField | name, label, placeholder, required | TextFieldProps plus context getValue, setValue, and errors | TextField | Numeric text field with type="number" and min=0. |
SelectField | name, options, placeholder, label | TextFieldProps plus context getValue, setValue, and errors | TextField | Select input that shows placeholder text when no value is selected. |
CheckboxField | name, label | FormControlLabelProps with control fixed to MUI Checkbox | FormControlLabel + Checkbox | Controlled boolean field used by OnlineAvailabilityField. |
TimeField | name, label, required, helperText | TimePickerProps plus context getValue, setValue, and errors | TimePicker | Stores time values as HH:mm strings through DateTime conversion. |
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;
};