Attribute Selection Block
The AttributeSelection Component is a fully customizable and accessible attribute selection form built with React and TypeScript. It provides a complete interface for attribute selection with form validation, error handling, and modern design patterns.
🚀 Installation
npm install @nodeblocks/frontend-attribute-selection-block@0.2.0
📖 Usage
import {AttributeSelection} from '@nodeblocks/frontend-attribute-selection-block';
- Basic Usage
- Advanced Usage
function BasicAttributeSelection() { const [submittedData, setSubmittedData] = useState(null); const handleSubmit = (data) => { console.log('Form submitted:', data); setSubmittedData(data); }; const handleCancel = (reset) => { console.log('Form cancelled'); reset(); setSubmittedData(null); }; const handleChange = (_setError, getValues) => { console.log('Form changed:', getValues()); }; return ( <div> <AttributeSelection onSubmit={handleSubmit} onCancel={handleCancel} onChange={handleChange} defaultValues={{admin: false, editor: true, viewer: false}} > <AttributeSelection.Title>Select User Roles</AttributeSelection.Title> <AttributeSelection.Subtitle>Choose the roles to assign</AttributeSelection.Subtitle> <AttributeSelection.FieldName>Roles</AttributeSelection.FieldName> <AttributeSelection.Checkbox name="admin" label="Administrator" value="admin" /> <AttributeSelection.Checkbox name="editor" label="Editor" value="editor" /> <AttributeSelection.Checkbox name="viewer" label="Viewer" value="viewer" /> <AttributeSelection.SubmitButton>Save Roles</AttributeSelection.SubmitButton> <AttributeSelection.CancelButton>Cancel</AttributeSelection.CancelButton> </AttributeSelection> {submittedData && ( <div style={{marginTop: '16px', padding: '12px', background: '#f0f0f0', borderRadius: '8px'}}> <strong>Submitted Data:</strong> <pre>{JSON.stringify(submittedData, null, 2)}</pre> </div> )} </div> ); }
function AdvancedAttributeSelection() { const [submittedData, setSubmittedData] = useState<SkillFormData | null>(null); const [formError, setFormError] = useState<string | null>(null); const handleSubmit = (data: SkillFormData) => { const selectedSkills = Object.entries(data).filter(([, value]) => value); if (selectedSkills.length < 2) { setFormError('Please select at least 2 skills'); return; } setFormError(null); setSubmittedData(data); console.log('Skills saved:', data); }; const handleCancel = (reset) => { reset(); setSubmittedData(null); setFormError(null); }; const handleChange = (_setError, getValues) => { const values = getValues(); const selectedCount = Object.values(values).filter(Boolean).length; console.log('Selected skills count:', selectedCount); }; return ( <AttributeSelection onSubmit={handleSubmit} onCancel={handleCancel} onChange={handleChange} defaultValues={{ javascript: true, typescript: true, react: false, nodejs: false, python: false, }} sx={{ maxWidth: 500, mx: 'auto', p: 3, bgcolor: '#ffffff', borderRadius: 3, boxShadow: '0 4px 20px rgba(0,0,0,0.08)', }} > {({defaultBlocks}) => { return { blocks: { ...defaultBlocks, title: ( <div> <div style={{ padding: '16px', background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', borderRadius: '12px 12px 0 0', marginBottom: '16px', }} > <div style={{fontSize: '20px', fontWeight: '700', color: '#ffffff', marginBottom: '4px'}}> Developer Skills Assessment </div> <div style={{fontSize: '14px', color: 'rgba(255,255,255,0.85)'}}> Select all skills you are proficient in (minimum 2) </div> </div> </div> ), subtitle: ( <AttributeSelection.Subtitle sx={{display: 'none'}}>Select your skills</AttributeSelection.Subtitle> ), fieldName: ( <AttributeSelection.FieldName sx={{fontWeight: '600', color: '#374151', mb: 2}}> Programming Languages & Frameworks </AttributeSelection.FieldName> ), // Each checkbox should be its own block with the name as the key javascript: ( <AttributeSelection.Checkbox name="javascript" label="JavaScript" value="javascript" sx={{ '& .MuiFormControlLabel-label': {fontWeight: '500'}, '& .Mui-checked': {color: '#6366f1'}, mb: 1, }} /> ), typescript: ( <AttributeSelection.Checkbox name="typescript" label="TypeScript" value="typescript" sx={{ '& .MuiFormControlLabel-label': {fontWeight: '500'}, '& .Mui-checked': {color: '#6366f1'}, mb: 1, }} /> ), react: ( <AttributeSelection.Checkbox name="react" label="React" value="react" sx={{ '& .MuiFormControlLabel-label': {fontWeight: '500'}, '& .Mui-checked': {color: '#6366f1'}, mb: 1, }} /> ), nodejs: ( <AttributeSelection.Checkbox name="nodejs" label="Node.js" value="nodejs" sx={{ '& .MuiFormControlLabel-label': {fontWeight: '500'}, '& .Mui-checked': {color: '#6366f1'}, mb: 1, }} /> ), python: ( <AttributeSelection.Checkbox name="python" label="Python" value="python" sx={{ '& .MuiFormControlLabel-label': {fontWeight: '500'}, '& .Mui-checked': {color: '#6366f1'}, mb: 1, }} /> ), submitButton: ( <AttributeSelection.SubmitButton sx={{ mt: 2, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', '&:hover': { background: 'linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%)', }, }} > Save Skills </AttributeSelection.SubmitButton> ), cancelButton: ( <div> <AttributeSelection.CancelButton sx={{ mt: 1, color: '#6b7280', }} > Reset </AttributeSelection.CancelButton> <div style={{marginTop: '16px', textAlign: 'center'}}> {formError && ( <div style={{ padding: '8px 16px', background: '#fee2e2', color: '#dc2626', borderRadius: '8px', marginBottom: '12px', fontSize: '14px', }} > {formError} </div> )} {submittedData && ( <div style={{ padding: '8px 16px', background: '#dcfce7', color: '#16a34a', borderRadius: '8px', fontSize: '14px', }} > Skills saved successfully! </div> )} </div> </div> ), }, // Update blockOrder to include individual checkbox blocks instead of 'checkbox' blockOrder: [ 'title', 'subtitle', 'fieldName', 'javascript', 'typescript', 'react', 'nodejs', 'python', 'submitButton', 'cancelButton', ], }; }} </AttributeSelection> ); }
🔧 Props Reference
Main Component Props
The component accepts two type parameters: AttributeSelection<T, B> where:
T- Form data type (must extendRecord<string, unknown>with index signature)B- Blocks type (Record<string, never>for simple usage,Record<string, ReactNode>for block override)
| Prop | Type | Default | Description |
|---|---|---|---|
onSubmit | (data: T) => void | Required | Callback function triggered when form is submitted with valid data |
onChange | (setError: UseFormSetError<T>, getValues: UseFormGetValues<T>) => void | Required | Callback function triggered when form values change |
onCancel | (reset: UseFormReset<T>) => void | Required | Callback function triggered when cancel button is clicked |
defaultValues | DefaultValues<T> | undefined | Default form values to populate fields on initial render |
className | string | undefined | Additional CSS class name for styling the form container |
children | BlocksOverride | undefined | Custom blocks override for form components |
Note: The main component extends MUI StackProps and renders as a form element using component="form". Default styling includes a Stack layout.
Sub-Components
The AttributeSelection component provides several sub-components. All sub-components receive their default values from the main component's context and can override these values through props.
AttributeSelection.Title
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | "職種" | Title text content for the form header |
variant | TypographyProps['variant'] | "h4" | MUI Typography variant |
component | ElementType | "h2" | HTML element to render |
className | string | undefined | Additional CSS class name for styling |
Note: This component inherits MUI Typography props. Default styling includes textAlign: 'center'.
AttributeSelection.Subtitle
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | "ご自身の職種を選択してください" | Subtitle text content providing form instructions |
variant | TypographyProps['variant'] | "body2" | MUI Typography variant |
className | string | undefined | Additional CSS class name for styling |
Note: This component inherits MUI Typography props. Default styling includes fontWeight: 600.
AttributeSelection.FieldName
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | "役割" | Field name text content displayed above form inputs |
variant | TypographyProps['variant'] | "body2" | MUI Typography variant |
className | string | undefined | Additional CSS class name for styling |
Note: This component inherits MUI Typography props. Default styling includes fontWeight: 600.
AttributeSelection.Checkbox
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | "" | Field name used for form data and validation |
label | string | "" | Label text displayed next to the checkbox |
value | string | "" | Value identifier for the checkbox option |
className | string | undefined | Additional CSS class name for styling |
Note: This component inherits MUI FormControlLabel props. Uses react-hook-form Controller for form integration. Default styling includes bordered container with rounded corners. The default checked state is controlled via the defaultValues prop on the main component.
AttributeSelection.SubmitButton
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | "次へ" | Button text content |
variant | ButtonProps['variant'] | "contained" | MUI Button variant |
size | ButtonProps['size'] | "large" | MUI Button size |
fullWidth | boolean | true | Whether button spans full width |
type | "submit" | "button" | "reset" | "submit" | HTML button type |
disabled | boolean | !formState.isValid | Disabled when form is invalid |
className | string | undefined | Additional CSS class name for styling |
Note: This component inherits MUI Button props. Automatically disabled when form validation fails.
AttributeSelection.CancelButton
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | "戻る" | Button text content |
variant | ButtonProps['variant'] | "outlined" | MUI Button variant |
size | ButtonProps['size'] | "large" | MUI Button size |
fullWidth | boolean | true | Whether button spans full width |
type | "submit" | "button" | "reset" | "button" | HTML button type |
className | string | undefined | Additional CSS class name for styling |
Note: This component inherits MUI Button props (except onClick). Calls onCancel(reset) when clicked.
🎨 Configuration examples
Custom Title Styling
<AttributeSelection.Title
variant="h3"
sx={{
color: 'primary.main',
fontWeight: 700,
textAlign: 'left'
}}
/>
Custom Checkbox Styling
<AttributeSelection.Checkbox
name="option1"
label="Custom Option"
value="option1"
sx={{
border: '2px solid #3b82f6',
borderRadius: '12px',
'&:hover': { backgroundColor: '#f0f9ff' }
}}
/>
Custom Button Styling
<AttributeSelection.SubmitButton
variant="contained"
sx={{
background: 'linear-gradient(135deg, #0f172a 0%, #1e293b 100%)',
borderRadius: '12px',
py: 1.5
}}
>
Continue
</AttributeSelection.SubmitButton>
Using Block Override Pattern
<AttributeSelection {...props}>
{({defaultBlocks, defaultBlockOrder}) => {
return {
blocks: {
...defaultBlocks,
customInfo: <Alert severity="info">Select at least one option</Alert>,
},
blockOrder: ['title', 'subtitle', 'customInfo', 'fieldName', 'checkbox', 'submitButton', 'cancelButton']
};
}}
</AttributeSelection>
🔧 TypeScript Support
Full TypeScript support with comprehensive type definitions:
import {AttributeSelection} from '@nodeblocks/frontend-attribute-selection-block';
import {StackProps} from '@mui/material';
import {DefaultValues, UseFormGetValues, UseFormReset, UseFormSetError} from 'react-hook-form';
import {ReactNode} from 'react';
// Form data interface must include index signature for react-hook-form compatibility
interface CustomAttributeFormData {
[key: string]: unknown;
role?: boolean;
department?: boolean;
experience?: boolean;
skills?: boolean;
}
// Main component props interface with two type parameters:
// T - Form data type
// B - Blocks type (Record<string, never> for simple usage, Record<string, ReactNode> for block override)
interface AttributeSelectionProps<T extends Record<string, unknown>, B extends Record<string, ReactNode>>
extends Omit<StackProps, 'onSubmit' | 'onChange' | 'children'> {
onSubmit: (data: T) => void;
onCancel: (reset: UseFormReset<T>) => void;
onChange: (setError: UseFormSetError<T>, getValues: UseFormGetValues<T>) => void;
defaultValues?: DefaultValues<T>;
children?: BlocksOverride;
}
// Type-safe submit handler
const handleSubmit = (formData: CustomAttributeFormData): void => {
console.log(formData.role, formData.department, formData.experience);
};
// Type-safe change handler with validation
const handleChange = (
_setError: UseFormSetError<CustomAttributeFormData>,
getValues: UseFormGetValues<CustomAttributeFormData>,
): void => {
const values = getValues();
const hasSelection = values.role || values.department || values.experience || values.skills;
if (!hasSelection) {
_setError('role', {message: 'Please select at least one option'});
}
};
// Type-safe cancel handler
const handleCancel = (reset: UseFormReset<CustomAttributeFormData>): void => {
reset();
console.log('Form cancelled');
};
// Simple usage with two type parameters
const SimpleAttributeSelectionForm = () => {
return (
<AttributeSelection<CustomAttributeFormData, Record<string, never>>
onSubmit={handleSubmit}
onChange={handleChange}
onCancel={handleCancel}
defaultValues={{role: false, department: false, experience: false, skills: false}}
>
<AttributeSelection.Title>Select Your Attributes</AttributeSelection.Title>
<AttributeSelection.Subtitle>Choose the options that apply to you</AttributeSelection.Subtitle>
<AttributeSelection.FieldName>Professional Information</AttributeSelection.FieldName>
<AttributeSelection.Checkbox name="role" label="Management Role" value="role" />
<AttributeSelection.Checkbox name="department" label="Technical Department" value="department" />
<AttributeSelection.Checkbox name="experience" label="5+ Years Experience" value="experience" />
<AttributeSelection.Checkbox name="skills" label="Leadership Skills" value="skills" />
<AttributeSelection.SubmitButton>Continue</AttributeSelection.SubmitButton>
<AttributeSelection.CancelButton>Go Back</AttributeSelection.CancelButton>
</AttributeSelection>
);
};
// Block override pattern with ReactNode blocks type
const AdvancedAttributeSelectionForm = () => {
return (
<AttributeSelection<CustomAttributeFormData, Record<string, ReactNode>>
onSubmit={handleSubmit}
onChange={handleChange}
onCancel={handleCancel}
defaultValues={{role: false, department: false, experience: false, skills: false}}
>
{({defaultBlocks, defaultBlockOrder}) => {
return {
blocks: {
...defaultBlocks,
title: (
<AttributeSelection.Title sx={{color: 'primary.main'}}>
Custom Title
</AttributeSelection.Title>
),
},
blockOrder: defaultBlockOrder,
};
}}
</AttributeSelection>
);
};
📝 Notes
- The root component uses MUI's
Stackwithcomponent="form"to create a semantic form element - Form state management is handled by
react-hook-formwithmode: 'onBlur'for validation - The
onChangecallback is triggered whenever form values change, allowing real-time validation SubmitButtonis automatically disabled whenformState.isValidisfalseCancelButtoncalls theonCancelcallback with the formresetfunction as argument- Default block order:
title,subtitle,fieldName,checkbox,submitButton,cancelButton - All sub-components support the MUI
sxprop for inline styling - Checkboxes use
react-hook-formController for form state integration - The component supports two generic type parameters:
<FormData, BlocksType>whereFormDatadefines form values andBlocksTypecontrols the blocks structure - Form data interfaces must include
[key: string]: unknownindex signature for react-hook-form compatibility - Use
Record<string, never>as the second type parameter for simple usage, andRecord<string, ReactNode>for block override pattern - CSS classes follow BEM naming:
nbb-attribute-selection,nbb-attribute-selection-title, etc.
Built with ❤️ using React, TypeScript, and MUI.