Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions Documentation/CommandForm/radio-button-field.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# RadioButtonField

`RadioButtonField` renders a single PrimeReact `RadioButton` that sets the bound command property to a specific value when selected. Use multiple `RadioButtonField` components bound to the same property to form a radio group.

## Usage

```tsx
import { CommandDialog } from '@cratis/components';
import { RadioButtonField } from '@cratis/components/CommandForm';

<CommandDialog command={MyCommand} visible={visible} onHide={() => setVisible(false)}>
<RadioButtonField<MyCommand> value={c => c.size} buttonValue="small" label="Small" />
<RadioButtonField<MyCommand> value={c => c.size} buttonValue="medium" label="Medium" />
<RadioButtonField<MyCommand> value={c => c.size} buttonValue="large" label="Large" />
</CommandDialog>
```

## Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `value` | `(instance: TCommand) => unknown` | — | **Required.** Accessor function that returns the bound property from the command instance. Pass the command type as the generic parameter for full type safety. |
| `buttonValue` | `string \| number` | — | **Required.** The value this radio button represents. When selected, the command property is set to this value. |
| `label` | `string` | — | Text displayed inline next to the radio button. |

## Behavior

- Default value is an empty string.
- The radio button is checked when the current field value equals `buttonValue`.
- Validation state is reflected via the PrimeReact `invalid` flag on the radio button.
55 changes: 55 additions & 0 deletions Documentation/CommandForm/radio-group-field.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# RadioGroupField

`RadioGroupField` renders a group of PrimeReact `RadioButton` components from an options array, allowing the user to select a single value.

## Usage

```tsx
import { CommandDialog } from '@cratis/components';
import { RadioGroupField } from '@cratis/components/CommandForm';

const sizeOptions = [
{ id: 'small', label: 'Small' },
{ id: 'medium', label: 'Medium' },
{ id: 'large', label: 'Large' },
];

<CommandDialog command={MyCommand} visible={visible} onHide={() => setVisible(false)}>
<RadioGroupField<MyCommand>
value={c => c.size}
options={sizeOptions}
optionLabel="label"
optionValue="id"
title="Size"
/>
</CommandDialog>
```

With horizontal layout:

```tsx
<RadioGroupField<MyCommand>
value={c => c.priority}
options={priorityOptions}
optionLabel="label"
optionValue="id"
title="Priority"
layout="horizontal"
/>
```

## Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `value` | `(instance: TCommand) => unknown` | — | **Required.** Accessor function that returns the bound property from the command instance. Pass the command type as the generic parameter for full type safety. |
| `options` | `Array<Record<string, unknown>>` | — | **Required.** Array of option objects. |
| `optionLabel` | `string` | — | **Required.** Key in each option object to use as the display label. |
| `optionValue` | `string` | — | **Required.** Key in each option object to use as the submitted value. |
| `layout` | `'horizontal' \| 'vertical'` | `'vertical'` | Controls whether the radio buttons are stacked vertically or laid out in a horizontal row. |

## Behavior

- Default value is an empty string.
- A radio button is checked when the current field value equals its `optionValue`.
- Validation state is reflected via the PrimeReact `invalid` flag on all radio buttons.
4 changes: 4 additions & 0 deletions Documentation/CommandForm/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
href: multi-select-field.md
- name: NumberField
href: number-field.md
- name: RadioButtonField
href: radio-button-field.md
- name: RadioGroupField
href: radio-group-field.md
- name: SliderField
href: slider-field.md
- name: TextAreaField
Expand Down
125 changes: 123 additions & 2 deletions Source/CommandForm/fields/Fields.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import {
CalendarField,
ColorPickerField,
MultiSelectField,
ChipsField
ChipsField,
RadioButtonField,
RadioGroupField
} from './index';

const meta: Meta = {
Expand Down Expand Up @@ -48,6 +50,8 @@ class FormFieldsCommand extends Command {
new PropertyDescriptor('color', String),
new PropertyDescriptor('multiSelect', Array),
new PropertyDescriptor('chips', Array),
new PropertyDescriptor('radioButton', String),
new PropertyDescriptor('radioGroup', String),
];

textInput = '';
Expand All @@ -62,6 +66,8 @@ class FormFieldsCommand extends Command {
color = '';
multiSelect: Array<string | number> = [];
chips: string[] = [];
radioButton = '';
radioGroup = '';

constructor() {
super(Object, false);
Expand All @@ -84,7 +90,9 @@ class FormFieldsCommand extends Command {
'calendarDate',
'color',
'multiSelect',
'chips'
'chips',
'radioButton',
'radioGroup'
];
}
}
Expand Down Expand Up @@ -145,6 +153,8 @@ export const AllFields: Story = {
color: '10b981',
multiSelect: ['feature1', 'feature3'],
chips: ['alpha', 'beta'],
radioButton: '',
radioGroup: '',
}}
onFieldChange={async (command, fieldName) => {
// Validate on field change
Expand Down Expand Up @@ -358,6 +368,43 @@ export const AllFields: Story = {

<StoryDivider />

<StorySection>
<h3>Radio Button</h3>

<RadioButtonField<FormFieldsCommand>
value={c => c.radioButton}
buttonValue="option1"
label="Option 1"
/>
<RadioButtonField<FormFieldsCommand>
value={c => c.radioButton}
buttonValue="option2"
label="Option 2"
/>
<RadioButtonField<FormFieldsCommand>
value={c => c.radioButton}
buttonValue="option3"
label="Option 3"
/>
</StorySection>

<StoryDivider />

<StorySection>
<h3>Radio Group</h3>

<RadioGroupField<FormFieldsCommand>
value={c => c.radioGroup}
title="Radio Group Field"
description="Radio buttons from an options array"
options={dropdownOptions}
optionValue="id"
optionLabel="name"
/>
</StorySection>

<StoryDivider />

<div style={{ marginTop: '1.5rem', display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
<button
type="submit"
Expand Down Expand Up @@ -630,3 +677,77 @@ export const ChipsFieldExample: Story = {
);
}
};

export const RadioButtonFieldExample: Story = {
render: () => {
return (
<StoryContainer size="sm" asCard>
<h2>RadioButtonField</h2>
<p>PrimeReact RadioButton for selecting a single value. Multiple RadioButtonFields bound to the same property form a radio group.</p>

<CommandForm<FormFieldsCommand>
command={FormFieldsCommand}
initialValues={{ radioButton: 'apple' }}
>
<RadioButtonField<FormFieldsCommand>
value={c => c.radioButton}
buttonValue="apple"
label="Apple"
/>
<RadioButtonField<FormFieldsCommand>
value={c => c.radioButton}
buttonValue="banana"
label="Banana"
/>
<RadioButtonField<FormFieldsCommand>
value={c => c.radioButton}
buttonValue="cherry"
label="Cherry"
/>
</CommandForm>
</StoryContainer>
);
}
};

export const RadioGroupFieldExample: Story = {
render: () => {
const options = [
{ id: 'small', name: 'Small' },
{ id: 'medium', name: 'Medium' },
{ id: 'large', name: 'Large' },
{ id: 'xl', name: 'Extra Large' },
];

return (
<StoryContainer size="sm" asCard>
<h2>RadioGroupField</h2>
<p>PrimeReact RadioButton group rendered from an options array.</p>

<CommandForm<FormFieldsCommand>
command={FormFieldsCommand}
initialValues={{ radioGroup: 'medium' }}
>
<RadioGroupField<FormFieldsCommand>
value={c => c.radioGroup}
title="T-Shirt Size"
description="Select your preferred size"
options={options}
optionValue="id"
optionLabel="name"
/>

<RadioGroupField<FormFieldsCommand>
value={c => c.radioGroup}
title="T-Shirt Size (Horizontal)"
description="Same options laid out horizontally"
options={options}
optionValue="id"
optionLabel="name"
layout="horizontal"
/>
</CommandForm>
</StoryContainer>
);
}
};
30 changes: 30 additions & 0 deletions Source/CommandForm/fields/RadioButtonField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

import { RadioButton, RadioButtonChangeEvent } from 'primereact/radiobutton';
import React from 'react';
import { asCommandFormField, WrappedFieldProps } from '@cratis/arc.react/commands';

interface RadioButtonFieldComponentProps extends WrappedFieldProps<string | number> {
label?: string;
buttonValue: string | number;
}

export const RadioButtonField = asCommandFormField<RadioButtonFieldComponentProps>(
(props) => (
<div className="flex align-items-center">
<RadioButton
value={props.buttonValue}
checked={props.value === props.buttonValue}
onChange={(e: RadioButtonChangeEvent) => props.onChange(e.value)}
onBlur={props.onBlur}
invalid={props.invalid}
/>
{props.label && <label className="ml-2">{props.label}</label>}
</div>
),
{
defaultValue: '',
extractValue: (e: unknown) => e as string | number
}
);
43 changes: 43 additions & 0 deletions Source/CommandForm/fields/RadioGroupField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

import { RadioButton, RadioButtonChangeEvent } from 'primereact/radiobutton';
import React from 'react';
import { asCommandFormField, WrappedFieldProps } from '@cratis/arc.react/commands';

interface RadioGroupFieldComponentProps extends WrappedFieldProps<string | number> {
options: Array<Record<string, unknown>>;
optionLabel: string;
optionValue: string;
layout?: 'horizontal' | 'vertical';
}

export const RadioGroupField = asCommandFormField<RadioGroupFieldComponentProps>(
(props) => {
const layout = props.layout ?? 'vertical';
return (
<div className={`flex ${layout === 'horizontal' ? 'flex-row gap-4 flex-wrap' : 'flex-column gap-2'}`}>
{props.options.map((option) => {
const optValue = option[props.optionValue] as string | number;
const optLabel = option[props.optionLabel] as string;
return (
<div key={String(optValue)} className="flex align-items-center">
<RadioButton
value={optValue}
checked={props.value === optValue}
onChange={(e: RadioButtonChangeEvent) => props.onChange(e.value)}
onBlur={props.onBlur}
invalid={props.invalid}
/>
<label className="ml-2">{optLabel}</label>
</div>
);
})}
</div>
);
},
{
defaultValue: '',
extractValue: (e: unknown) => e as string | number
}
);
2 changes: 2 additions & 0 deletions Source/CommandForm/fields/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ export * from './CalendarField';
export * from './ColorPickerField';
export * from './MultiSelectField';
export * from './ChipsField';
export * from './RadioButtonField';
export * from './RadioGroupField';
Loading