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
6 changes: 6 additions & 0 deletions .changeset/new-trees-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@ensembleui/react-kitchen-sink": patch
"@ensembleui/react-runtime": patch
---

support item-template in MultiSelect widget
34 changes: 23 additions & 11 deletions apps/kitchen-sink/src/ensemble/screens/forms.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -225,11 +225,17 @@ View:
- MultiSelect:
id: multiselectoptions2
label: Choose multiple
data:
- label: Option 1
value: option1
- label: Option 2
value: option2
item-template:
data: ${getData.body.users}
name: user
value: ${user.email}
template:
Column:
children:
- Text:
text: ${user.firstName} ${user.lastName}
- Text:
text: ${user.email}
- Date:
htmlAttributes:
data-testid: date0-test
Expand Down Expand Up @@ -414,12 +420,18 @@ View:
- MultiSelect:
id: initial_multiselectoptions2
label: Choose multiple
value: option2
data:
- label: Option 1
value: option1
- label: Option 2
value: option2
value: ${[getData.body.users[0].email, getData.body.users[1].email]}
item-template:
data: ${getData.body.users}
name: user
value: ${user.email}
template:
Column:
children:
- Text:
text: ${user.firstName} ${user.lastName}
- Text:
text: ${user.email}
# - Date:
# id: initail_date
# label: Date
Expand Down
4 changes: 3 additions & 1 deletion apps/kitchen-sink/src/ensemble/screens/home.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@ View:
labelKey: name
valueKey: email
data: ${ensemble.storage.get('products')}
value: ${ensemble.storage.get('selectedProducts') ?? null}
onSearch:
executeCode: |
ensemble.invokeAPI('getProducts', { search: search }).then((res) => {
Expand All @@ -473,7 +474,8 @@ View:
console.log("onSearch values: ", search);
onChange:
executeCode: |
console.log("onChange values: ", search);
console.log("onChange values: ", value, option);
ensemble.storage.set('selectedProducts', option);

Global:
scriptName: test.js
Expand Down
103 changes: 83 additions & 20 deletions packages/runtime/src/widgets/Form/MultiSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,29 @@ import React, {
useRef,
useState,
} from "react";
import type { Expression, EnsembleAction } from "@ensembleui/react-framework";
import type {
Expression,
EnsembleAction,
CustomScope,
} from "@ensembleui/react-framework";
import {
CustomScopeProvider,
defaultScreenContext,
evaluate,
unwrapWidget,
useRegisterBindings,
useTemplateData,
} from "@ensembleui/react-framework";
import { PlusCircleOutlined } from "@ant-design/icons";
import { Select as SelectComponent, Space, Form } from "antd";
import { Select as SelectComponent, Space, Form, Select } from "antd";
import {
get,
isArray,
isEmpty,
isEqual,
isObject,
isString,
omit,
toNumber,
} from "lodash-es";
import { useDebounce } from "react-use";
Expand All @@ -29,6 +37,7 @@ import { useEnsembleAction } from "../../runtime/hooks/useEnsembleAction";
import type {
EnsembleWidgetProps,
EnsembleWidgetStyles,
HasItemTemplate,
} from "../../shared/types";
import { EnsembleRuntime } from "../../runtime";
import { getComponentStyles } from "../../shared/styles";
Expand Down Expand Up @@ -75,10 +84,12 @@ export type MultiSelectProps = {
maxTagTextLength: Expression<number>;
notFoundContent?: Expression<string> | { [key: string]: unknown };
} & EnsembleWidgetProps<MultiSelectStyles> &
FormInputProps<object[] | string[]>;
HasItemTemplate & {
"item-template"?: { value: Expression<string> };
} & FormInputProps<object[] | string[]>;

const MultiSelect: React.FC<MultiSelectProps> = (props) => {
const { data, ...rest } = props;
const { data, "item-template": itemTemplate, ...rest } = props;
const [options, setOptions] = useState<MultiSelectOption[]>([]);
const [newOption, setNewOption] = useState("");
const [selectedValues, setSelectedValues] = useState<MultiSelectOption[]>();
Expand All @@ -89,6 +100,10 @@ const MultiSelect: React.FC<MultiSelectProps> = (props) => {
const onSearchAction = useEnsembleAction(props.onSearch);

const { rawData } = useTemplateData({ data });
const { namedData } = useTemplateData({
data: itemTemplate?.data,
name: itemTemplate?.name,
});
const { id, rootRef, values } = useRegisterBindings(
{ ...rest, initialValue: props.value, selectedValues, options, widgetName },
props.id,
Expand All @@ -103,24 +118,35 @@ const MultiSelect: React.FC<MultiSelectProps> = (props) => {

// check and load initial values
useEffect(() => {
// compare previous initial value with current render initial value
// deep compare the value of the binding to tell when it has changed because it's an array
if (
!isEqual(prevInitialValue.current, values?.initialValue) &&
isArray(values?.initialValue)
) {
prevInitialValue.current = values?.initialValue || [];
const initialValue = values?.initialValue.map((item) =>
isObject(item)
? {
...(item as { [key: string]: unknown }),
label: get(item, values.labelKey || "label") as string,
value: get(item, values.valueKey || "value") as string,
}
: item,
);
const initialValue = values?.initialValue.map((item) => {
if (!isObject(item)) return item;

const label: unknown = get(item, values.labelKey || "label");
return {
...item,
// When item-template is used, we explicitly convert `null` label to `undefined`.
// This is crucial for rendering template of item-template.
// If no item-template is used, we allow `null` to pass through, as React will correctly render it as nothing.
label: itemTemplate?.name
? ((label ?? undefined) as string | undefined)
: (label as string | null),
value: get(item, values.valueKey || "value") as string,
};
});
setSelectedValues(initialValue as MultiSelectOption[]);
}
}, [values?.initialValue]);
}, [
itemTemplate?.name,
values?.initialValue,
values?.labelKey,
values?.valueKey,
]);

// load data and items
useEffect(() => {
Expand Down Expand Up @@ -156,6 +182,34 @@ const MultiSelect: React.FC<MultiSelectProps> = (props) => {
setOptions(tempOptions);
}, [rawData, values?.labelKey, values?.valueKey, values?.items]);

const renderOptions = useMemo(() => {
if (isObject(itemTemplate) && !isEmpty(namedData)) {
const tempOptions = namedData.map((item: unknown) => {
const value = evaluate<string | number>(
defaultScreenContext,
itemTemplate.value,
{
[itemTemplate.name]: get(item, itemTemplate.name) as unknown,
},
);
return (
<Select.Option
className={`${values?.id || ""}_option`}
key={value}
value={value}
>
<CustomScopeProvider value={item as CustomScope}>
{EnsembleRuntime.render([itemTemplate.template])}
</CustomScopeProvider>
</Select.Option>
);
});
return tempOptions;
}

return [];
}, [itemTemplate, namedData, values?.id]);

// handle form instance
const formInstance = Form.useFormInstance();
useEffect(() => {
Expand Down Expand Up @@ -211,8 +265,13 @@ const MultiSelect: React.FC<MultiSelectProps> = (props) => {
option?: MultiSelectOption | MultiSelectOption[],
): void => {
setSelectedValues(value);
if (action) onChangeCallback({ value, option: option ?? [] });
else onItemSelectCallback(value);

if (action) {
const plainOption = isArray(option)
? option.map((op) => omit(op, "children"))
: omit(option, "children");
onChangeCallback({ value, option: plainOption });
} else onItemSelectCallback(value);

if (newOption) {
setOptions([
Expand Down Expand Up @@ -288,6 +347,9 @@ const MultiSelect: React.FC<MultiSelectProps> = (props) => {
.${id}_input .ant-select-selector {
${getComponentStyles("multiSelect", values?.styles, true, true) as string}
}
.${id}_input .ant-select-selector .ant-select-selection-item {
height: auto !important;
}
.ant-select-item.ant-select-item-option.${id}_option[aria-selected="true"]
{
${
Expand Down Expand Up @@ -359,7 +421,6 @@ const MultiSelect: React.FC<MultiSelectProps> = (props) => {
dropdownStyle={values?.styles}
filterOption={props.onSearch ? false : handleFilterOption}
id={values?.id}
labelRender={labelRender}
maxCount={values?.maxCount ? toNumber(values.maxCount) : undefined}
maxTagCount={
values?.maxTagCount as number | "responsive" | undefined
Expand All @@ -374,7 +435,7 @@ const MultiSelect: React.FC<MultiSelectProps> = (props) => {
onChange={handleChange}
onSearch={handleSearch} // required for display new custom option with Dropdown element
optionFilterProp="children"
options={options}
{...(options.length > 0 ? { options, labelRender } : {})}
placeholder={
values?.hintText ? (
<span style={{ ...values.hintStyle }}>{values.hintText}</span>
Expand All @@ -383,7 +444,9 @@ const MultiSelect: React.FC<MultiSelectProps> = (props) => {
)
}
value={values?.selectedValues}
/>
>
{renderOptions}
</SelectComponent>
</EnsembleFormItem>
</div>
</>
Expand Down
Loading