feat: delimiter separated lists (#819)
This commit is contained in:
parent
66b0d7992d
commit
85db6b4f8d
@ -410,6 +410,16 @@ collections:
|
||||
- label: Description
|
||||
name: description
|
||||
widget: text
|
||||
- name: comma_separated_list
|
||||
label: Comma Separated List
|
||||
widget: list
|
||||
- name: delimiter_separated_list
|
||||
label: Custom Delimiter (Semicolon) Separated List
|
||||
widget: list
|
||||
delimiter: ';'
|
||||
default:
|
||||
- 'tag-1'
|
||||
- 'tag-2'
|
||||
- name: string_list
|
||||
label: String List
|
||||
widget: list
|
||||
|
@ -655,6 +655,7 @@ export interface ListField<EF extends BaseField = UnknownField> extends BaseFiel
|
||||
add_to_top?: boolean;
|
||||
types?: ObjectField<EF>[];
|
||||
type_key?: string;
|
||||
delimiter?: string;
|
||||
}
|
||||
|
||||
export interface MapField extends BaseField {
|
||||
|
69
packages/core/src/widgets/list/DelimitedListControl.tsx
Normal file
69
packages/core/src/widgets/list/DelimitedListControl.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import Field from '@staticcms/core/components/common/field/Field';
|
||||
import TextField from '@staticcms/core/components/common/text-field/TextField';
|
||||
import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
|
||||
|
||||
import type { ListField, ValueOrNestedValue, WidgetControlProps } from '@staticcms/core/interface';
|
||||
import type { ChangeEvent, FC } from 'react';
|
||||
|
||||
const DelimitedListControl: FC<WidgetControlProps<ValueOrNestedValue[], ListField>> = ({
|
||||
field,
|
||||
label,
|
||||
disabled,
|
||||
duplicate,
|
||||
value,
|
||||
errors,
|
||||
forSingleList,
|
||||
controlled,
|
||||
onChange,
|
||||
}) => {
|
||||
const delimiter = useMemo(() => field.delimiter ?? ',', [field.delimiter]);
|
||||
|
||||
const rawValue = useMemo(() => (value ?? []).join(delimiter), [delimiter, value]);
|
||||
|
||||
const [internalRawValue, setInternalValue] = useState(rawValue);
|
||||
const internalValue = useMemo(
|
||||
() => (controlled || duplicate ? rawValue : internalRawValue),
|
||||
[controlled, duplicate, rawValue, internalRawValue],
|
||||
);
|
||||
const debouncedInternalValue = useDebounce(internalValue, 200);
|
||||
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const handleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
const newRawValue = event.target.value;
|
||||
setInternalValue(newRawValue);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (rawValue === debouncedInternalValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newValue = debouncedInternalValue.split(delimiter).map(v => v.trim());
|
||||
onChange(newValue);
|
||||
}, [debouncedInternalValue, delimiter, onChange, rawValue]);
|
||||
|
||||
return (
|
||||
<Field
|
||||
inputRef={ref}
|
||||
label={label}
|
||||
errors={errors}
|
||||
hint={field.hint}
|
||||
forSingleList={forSingleList}
|
||||
cursor="text"
|
||||
disabled={disabled}
|
||||
>
|
||||
<TextField
|
||||
type="text"
|
||||
inputRef={ref}
|
||||
value={internalValue}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
|
||||
export default DelimitedListControl;
|
@ -13,6 +13,7 @@ import useHasChildErrors from '@staticcms/core/lib/hooks/useHasChildErrors';
|
||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import ListFieldWrapper from './components/ListFieldWrapper';
|
||||
import ListItem from './components/ListItem';
|
||||
import DelimitedListControl from './DelimitedListControl';
|
||||
import { resolveFieldKeyType, TYPES_KEY } from './typedListHelpers';
|
||||
|
||||
import type { DragEndEvent } from '@dnd-kit/core';
|
||||
@ -40,7 +41,7 @@ interface SortableItemProps {
|
||||
id: string;
|
||||
item: ValueOrNestedValue;
|
||||
index: number;
|
||||
valueType: ListValueType;
|
||||
valueType: ListValueType.MIXED | ListValueType.MULTIPLE;
|
||||
handleRemove: (index: number, event: MouseEvent) => void;
|
||||
entry: Entry<ObjectValue>;
|
||||
field: ListField;
|
||||
@ -125,6 +126,7 @@ const SortableItem: FC<SortableItemProps> = ({
|
||||
export enum ListValueType {
|
||||
MULTIPLE,
|
||||
MIXED,
|
||||
DELIMITED,
|
||||
}
|
||||
|
||||
function getFieldsDefault(
|
||||
@ -175,23 +177,25 @@ function getFieldsDefault(
|
||||
}, initialValue as ObjectValue);
|
||||
}
|
||||
|
||||
const ListControl: FC<WidgetControlProps<ValueOrNestedValue[], ListField>> = ({
|
||||
entry,
|
||||
field,
|
||||
fieldsErrors,
|
||||
submitted,
|
||||
disabled,
|
||||
duplicate,
|
||||
hidden,
|
||||
locale,
|
||||
path,
|
||||
value,
|
||||
i18n,
|
||||
errors,
|
||||
forSingleList,
|
||||
onChange,
|
||||
t,
|
||||
}) => {
|
||||
const ListControl: FC<WidgetControlProps<ValueOrNestedValue[], ListField>> = props => {
|
||||
const {
|
||||
entry,
|
||||
field,
|
||||
fieldsErrors,
|
||||
submitted,
|
||||
disabled,
|
||||
duplicate,
|
||||
hidden,
|
||||
locale,
|
||||
path,
|
||||
value,
|
||||
i18n,
|
||||
errors,
|
||||
forSingleList,
|
||||
onChange,
|
||||
t,
|
||||
} = props;
|
||||
|
||||
const internalValue = useMemo(() => value ?? [], [value]);
|
||||
const [keys, setKeys] = useState(Array.from({ length: internalValue.length }, () => uuid()));
|
||||
|
||||
@ -201,7 +205,7 @@ const ListControl: FC<WidgetControlProps<ValueOrNestedValue[], ListField>> = ({
|
||||
} else if ('types' in field) {
|
||||
return ListValueType.MIXED;
|
||||
} else {
|
||||
return null;
|
||||
return ListValueType.DELIMITED;
|
||||
}
|
||||
}, [field]);
|
||||
|
||||
@ -295,16 +299,16 @@ const ListControl: FC<WidgetControlProps<ValueOrNestedValue[], ListField>> = ({
|
||||
|
||||
const hasChildErrors = useHasChildErrors(path, fieldsErrors, i18n, false);
|
||||
|
||||
if (valueType === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = field.label ?? field.name;
|
||||
const labelSingular = field.label_singular ? field.label_singular : field.label ?? field.name;
|
||||
const listLabel = internalValue.length === 1 ? labelSingular : label;
|
||||
|
||||
const types = field[TYPES_KEY];
|
||||
|
||||
if (valueType === ListValueType.DELIMITED) {
|
||||
return <DelimitedListControl {...props} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key="list-widget">
|
||||
<ListFieldWrapper
|
||||
|
@ -1,25 +1,58 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { ListField, ValueOrNestedValue, WidgetPreviewProps } from '@staticcms/core/interface';
|
||||
import type { FC } from 'react';
|
||||
import { isNotNullish } from '@staticcms/core/lib/util/null.util';
|
||||
|
||||
const ListPreview: FC<WidgetPreviewProps<ValueOrNestedValue[], ListField>> = ({ field, value }) => {
|
||||
if (field.fields && field.fields.length === 1) {
|
||||
import type { ListField, ValueOrNestedValue, WidgetPreviewProps } from '@staticcms/core/interface';
|
||||
import type { FC, ReactNode } from 'react';
|
||||
|
||||
function renderNestedList(
|
||||
value: ValueOrNestedValue[] | ValueOrNestedValue | null | undefined,
|
||||
): ReactNode {
|
||||
if (Array.isArray(value)) {
|
||||
return (
|
||||
<div>
|
||||
<label>
|
||||
<strong>{field.name}:</strong>
|
||||
</label>
|
||||
<ul style={{ marginTop: 0 }}>
|
||||
{value?.map(item => (
|
||||
<li key={String(item)}>{String(item)}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<ul style={{ marginTop: 0 }}>
|
||||
{value.map((item, index) => (
|
||||
<li key={index}>{renderNestedList(item)}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
return <div>{field.renderedFields ?? null}</div>;
|
||||
if (isNotNullish(value) && typeof value === 'object') {
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString();
|
||||
}
|
||||
|
||||
return Object.keys(value).map((key, index) => (
|
||||
<div key={index}>
|
||||
<strong>{key}:</strong> {renderNestedList(value[key])}
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
const ListPreview: FC<WidgetPreviewProps<ValueOrNestedValue[], ListField>> = ({ field, value }) => {
|
||||
return (
|
||||
<div style={{ marginTop: '12px' }}>
|
||||
<label>
|
||||
<strong>{field.label ?? field.name}:</strong>
|
||||
</label>
|
||||
{(field.fields &&
|
||||
field.fields.length === 1 &&
|
||||
!['object', 'list'].includes(field.fields[0].widget)) ||
|
||||
(!field.fields && !field.types) ? (
|
||||
<ul style={{ marginTop: 0 }}>
|
||||
{value?.map((item, index) => (
|
||||
<li key={index}>{String(item)}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
renderNestedList(value)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListPreview;
|
||||
|
@ -15,6 +15,11 @@ import type { ListField, ValueOrNestedValue } from '@staticcms/core/interface';
|
||||
|
||||
jest.unmock('uuid');
|
||||
|
||||
const delimitedListField: ListField = {
|
||||
widget: 'list',
|
||||
name: 'delimited',
|
||||
};
|
||||
|
||||
const singletonListField: ListField = {
|
||||
widget: 'list',
|
||||
name: 'singleton',
|
||||
@ -441,6 +446,80 @@ describe(ListControl.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('delimited list', () => {
|
||||
it('renders text input instead of normal list setup', async () => {
|
||||
const { queryByTestId, getByTestId } = renderControl({
|
||||
field: delimitedListField,
|
||||
value: ['Value 1', 'Value 2'],
|
||||
});
|
||||
|
||||
expect(queryByTestId('list-widget-children')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('list-expand-button')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('list-add')).not.toBeInTheDocument();
|
||||
|
||||
const itemOne = queryByTestId('object-control-0');
|
||||
expect(itemOne).not.toBeInTheDocument();
|
||||
const itemTwo = queryByTestId('object-control-1');
|
||||
expect(itemTwo).not.toBeInTheDocument();
|
||||
|
||||
const inputWrapper = getByTestId('text-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
expect(input).toBeVisible();
|
||||
expect(input).toHaveValue('Value 1,Value 2');
|
||||
});
|
||||
|
||||
it('renders empty', async () => {
|
||||
const { getByTestId } = await renderControl({
|
||||
field: delimitedListField,
|
||||
value: [],
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('text-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
expect(input).toBeVisible();
|
||||
expect(input).toHaveValue('');
|
||||
});
|
||||
|
||||
it('ignores collapsed value', async () => {
|
||||
const { getByTestId } = await renderControl({
|
||||
field: { ...delimitedListField, collapsed: true },
|
||||
value: ['Value 1', 'Value 2'],
|
||||
});
|
||||
|
||||
const inputWrapper = getByTestId('text-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
expect(input).toBeVisible();
|
||||
});
|
||||
|
||||
it('trims input before outputting new value', async () => {
|
||||
const {
|
||||
getByTestId,
|
||||
props: { onChange },
|
||||
} = await renderControl({ field: delimitedListField, value: ['Value 1', 'Value 2'] });
|
||||
|
||||
const inputWrapper = getByTestId('text-input');
|
||||
const input = inputWrapper.getElementsByTagName('input')[0];
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.type(input, ', I am a new value with extra whitespace! ');
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenCalledTimes(0);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
expect(onChange).toHaveBeenCalledWith([
|
||||
'Value 1',
|
||||
'Value 2',
|
||||
'I am a new value with extra whitespace!',
|
||||
]);
|
||||
expect(input).toHaveValue(
|
||||
'Value 1,Value 2, I am a new value with extra whitespace! ',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('singleton list', () => {
|
||||
it('renders open by default', async () => {
|
||||
await renderListControl({ value: ['Value 1', 'Value 2'] });
|
||||
|
@ -75,7 +75,7 @@ interface ListItemProps
|
||||
| 'value'
|
||||
| 'i18n'
|
||||
> {
|
||||
valueType: ListValueType;
|
||||
valueType: ListValueType.MIXED | ListValueType.MULTIPLE;
|
||||
index: number;
|
||||
id: string;
|
||||
listeners: SyntheticListenerMap | undefined;
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import Field from '@staticcms/core/components/common/field/Field';
|
||||
import TextField from '@staticcms/core/components/common/text-field/TextField';
|
||||
import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
|
||||
|
||||
import type { StringOrTextField, WidgetControlProps } from '@staticcms/core/interface';
|
||||
import type { ChangeEvent, FC } from 'react';
|
||||
@ -17,20 +18,27 @@ const StringControl: FC<WidgetControlProps<string, StringOrTextField>> = ({
|
||||
controlled,
|
||||
onChange,
|
||||
}) => {
|
||||
const [internalRawValue, setInternalValue] = useState(value ?? '');
|
||||
const rawValue = useMemo(() => value ?? '', [value]);
|
||||
const [internalRawValue, setInternalValue] = useState(rawValue);
|
||||
const internalValue = useMemo(
|
||||
() => (controlled || duplicate ? value ?? '' : internalRawValue),
|
||||
[controlled, duplicate, value, internalRawValue],
|
||||
() => (controlled || duplicate ? rawValue : internalRawValue),
|
||||
[controlled, duplicate, rawValue, internalRawValue],
|
||||
);
|
||||
const debouncedInternalValue = useDebounce(internalValue, 200);
|
||||
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
setInternalValue(event.target.value);
|
||||
onChange(event.target.value);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
const handleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
setInternalValue(event.target.value);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (rawValue === debouncedInternalValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(debouncedInternalValue);
|
||||
}, [debouncedInternalValue, onChange, rawValue]);
|
||||
|
||||
return (
|
||||
<Field
|
||||
|
@ -2,8 +2,8 @@
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import '@testing-library/jest-dom';
|
||||
import { act, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { act } from '@testing-library/react';
|
||||
|
||||
import { mockStringField } from '@staticcms/test/data/fields.mock';
|
||||
import { createWidgetControlHarness } from '@staticcms/test/harnesses/widget.harness';
|
||||
@ -84,7 +84,12 @@ describe(StringControl.name, () => {
|
||||
await userEvent.type(input, 'I am some text');
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith('I am some text');
|
||||
expect(onChange).toHaveBeenCalledTimes(0);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
expect(onChange).toHaveBeenLastCalledWith('I am some text');
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error', async () => {
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import Field from '@staticcms/core/components/common/field/Field';
|
||||
import TextArea from '@staticcms/core/components/common/text-field/TextArea';
|
||||
import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
|
||||
|
||||
import type { StringOrTextField, WidgetControlProps } from '@staticcms/core/interface';
|
||||
import type { ChangeEvent, FC } from 'react';
|
||||
@ -17,20 +18,27 @@ const TextControl: FC<WidgetControlProps<string, StringOrTextField>> = ({
|
||||
forSingleList,
|
||||
onChange,
|
||||
}) => {
|
||||
const [internalRawValue, setInternalValue] = useState(value ?? '');
|
||||
const rawValue = useMemo(() => value ?? '', [value]);
|
||||
const [internalRawValue, setInternalValue] = useState(rawValue);
|
||||
const internalValue = useMemo(
|
||||
() => (duplicate ? value ?? '' : internalRawValue),
|
||||
[internalRawValue, duplicate, value],
|
||||
() => (duplicate ? rawValue : internalRawValue),
|
||||
[internalRawValue, duplicate, rawValue],
|
||||
);
|
||||
const debouncedInternalValue = useDebounce(internalValue, 200);
|
||||
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
setInternalValue(event.target.value);
|
||||
onChange(event.target.value);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
const handleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
setInternalValue(event.target.value);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (rawValue === debouncedInternalValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(debouncedInternalValue);
|
||||
}, [debouncedInternalValue, onChange, rawValue]);
|
||||
|
||||
return (
|
||||
<Field
|
||||
|
@ -3,7 +3,7 @@
|
||||
*/
|
||||
import '@testing-library/jest-dom';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { act } from '@testing-library/react';
|
||||
import { act, waitFor } from '@testing-library/react';
|
||||
|
||||
import { mockTextField } from '@staticcms/test/data/fields.mock';
|
||||
import { createWidgetControlHarness } from '@staticcms/test/harnesses/widget.harness';
|
||||
@ -84,7 +84,12 @@ describe(TextControl.name, () => {
|
||||
await userEvent.type(input, 'I am some text');
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith('I am some text');
|
||||
expect(onChange).toHaveBeenCalledTimes(0);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
expect(onChange).toHaveBeenLastCalledWith('I am some text');
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error', async () => {
|
||||
|
@ -82,6 +82,7 @@ collections:
|
||||
create: true
|
||||
editor:
|
||||
frame: false
|
||||
size: half
|
||||
fields:
|
||||
- label: Question
|
||||
name: title
|
||||
@ -409,6 +410,16 @@ collections:
|
||||
- label: Description
|
||||
name: description
|
||||
widget: text
|
||||
- name: comma_separated_list
|
||||
label: Comma Separated List
|
||||
widget: list
|
||||
- name: delimiter_separated_list
|
||||
label: Custom Delimiter (Semicolon) Separated List
|
||||
widget: list
|
||||
delimiter: ';'
|
||||
default:
|
||||
- 'tag-1'
|
||||
- 'tag-2'
|
||||
- name: string_list
|
||||
label: String List
|
||||
widget: list
|
||||
|
@ -5,7 +5,7 @@ weight: 17
|
||||
---
|
||||
|
||||
- **Name:** `list`
|
||||
- **UI:** The list widget contains a repeatable child widget, with controls for adding, deleting, and re-ordering the repeated widgets.
|
||||
- **UI:** The list widget contains a repeatable child widget, with controls for adding, deleting, and re-ordering the repeated widgets. If no 'fields' or 'types' are provided, the list widget defaults to a text input for entering comma-separated values.
|
||||
- **Data type:** List of widget values
|
||||
|
||||
The list widget allows you to create a repeatable item in the UI which saves as a list of widget values. You can choose any widget as a child of a list widget—even other lists.
|
||||
@ -14,19 +14,20 @@ The list widget allows you to create a repeatable item in the UI which saves as
|
||||
|
||||
For common options, see [Common widget options](/docs/widgets#common-widget-options).
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| -------------- | ---------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| default | string | `[ <default from the child fields> ]` | _Optional_. The default values for fields. Also accepts an array of items |
|
||||
| allow_add | boolean | `true` | _Optional_. `false` - Hides the button to add additional items |
|
||||
| collapsed | boolean | `true` | _Optional_. `true` - The list and entries collapse by default. |
|
||||
| summary | string | | _Optional_. The label displayed on collapsed entries. _Ignored for single field lists._ |
|
||||
| label_singular | string | `label` | _Optional_. The text to show on the add button |
|
||||
| fields | list of widgets | [] | _Optional_. A nested list of multiple widget fields to be included in each repeatable iteration |
|
||||
| min | number | | _Optional_. Minimum number of items in the list |
|
||||
| max | number | | _Optional_. Maximum number of items in the list |
|
||||
| add_to_top | boolean | `false` | _Optional_. <ul><li>`true` - New entries will be added to the top of the list</li><li>`false` - New entries will be added to the bottom of the list</li></ul> |
|
||||
| types | list of object widgets | | _Optional_. A nested list of object widgets representing the available types for items in the list. Takes priority over `fields`. |
|
||||
| type_key | string | `'type'` | _Optional_. The name of the individual field that will be added to every item in list representing the name of the object widget that item belongs to. Ignored if `types` is not defined |
|
||||
| Name | Type | Default | Description |
|
||||
| -------------- | ---------------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| default | string | `[ <default from the child fields> ]` | _Optional_. The default values for fields. Also accepts an array of items |
|
||||
| allow_add | boolean | `true` | _Optional_. `false` - Hides the button to add additional items. Ignored if both `fields` and `types` are not defined |
|
||||
| collapsed | boolean | `true` | _Optional_. `true` - The list and entries collapse by default. Ignored if both `fields` and `types` are not defined |
|
||||
| summary | string | | _Optional_. The label displayed on collapsed entries. _Ignored for single field lists._ |
|
||||
| label_singular | string | `label` | _Optional_. The text to show on the add button |
|
||||
| fields | list of widgets | [] | _Optional_. A nested list of multiple widget fields to be included in each repeatable iteration |
|
||||
| min | number | | _Optional_. Minimum number of items in the list |
|
||||
| max | number | | _Optional_. Maximum number of items in the list |
|
||||
| add_to_top | boolean | `false` | _Optional_. <ul><li>`true` - New entries will be added to the top of the list</li><li>`false` - New entries will be added to the bottom of the list</li></ul>Ignored if both `fields` and `types` are not defined |
|
||||
| types | list of object widgets | | _Optional_. A nested list of object widgets representing the available types for items in the list. Takes priority over `fields`. |
|
||||
| type_key | string | `'type'` | _Optional_. The name of the individual field that will be added to every item in list representing the name of the object widget that item belongs to. Ignored if `types` is not defined |
|
||||
| delimiter | string | `','` | _Optional_. The delimiter to use when entering a delimiter separated list. Ignored if `fields` or `types` are defined |
|
||||
|
||||
## Examples
|
||||
|
||||
@ -406,6 +407,32 @@ fields: [
|
||||
|
||||
</CodeTabs>
|
||||
|
||||
### Delimiter Separated List
|
||||
|
||||
<CodeTabs>
|
||||
```yaml
|
||||
name: tags
|
||||
label: Tags
|
||||
widget: list
|
||||
delimiter: ';' # Default: ','
|
||||
default:
|
||||
- 'tag-1'
|
||||
- 'tag-2'
|
||||
```
|
||||
|
||||
```js
|
||||
name: 'tags',
|
||||
label: 'Tags',
|
||||
widget: 'list',
|
||||
delimiter: ';', // Default: ','
|
||||
default: [
|
||||
'tag-1',
|
||||
'tag-2'
|
||||
]
|
||||
```
|
||||
|
||||
</CodeTabs>
|
||||
|
||||
### Singleton List (List of Strings)
|
||||
|
||||
<CodeTabs>
|
||||
|
Loading…
x
Reference in New Issue
Block a user