Skip to content

Commit 6c6f640

Browse files
committed
feat(react-formio): add support Choicesjs and React-select layout to InputTags
1 parent d948768 commit 6c6f640

20 files changed

+486
-154
lines changed

packages/react-formio/src/molecules/forms/form-control/FormControl.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ export type FormControlProps<
2222
Attributes extends HTMLAttributes<HTMLElement> = InputHTMLAttributes<HTMLInputElement>
2323
> = BaseFormControlProps<Value> & Omit<Attributes, "onChange" | "value" | "size">;
2424

25-
export function cleanFormControlProps(props: FormControlProps): any {
26-
return omit(props, ["label", "description", "prefix", "suffix", "size", "shadow"]);
25+
export function cleanFormControlProps(props: FormControlProps, omitted: string[] = []): any {
26+
return omit(props, ["label", "description", "prefix", "suffix", "size", "shadow", ...omitted]);
2727
}
2828

2929
export function FormControl<Value = unknown>({
@@ -48,7 +48,7 @@ export function FormControl<Value = unknown>({
4848
"-with-before": !!before,
4949
"-with-after": !!after
5050
},
51-
size && `form-group-${size}`,
51+
size && `-size-${size}`,
5252
className
5353
)}
5454
>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { InputHTMLAttributes } from "react";
2+
3+
import type { FormControlProps } from "../form-control/FormControl";
4+
5+
export interface InputTagsProps<Data = string> extends FormControlProps<Data[], InputHTMLAttributes<HTMLInputElement>> {
6+
layout?: "html5" | "react" | "choicesjs";
7+
delimiter?: string;
8+
customProperties?: Record<string, any>;
9+
}

packages/react-formio/src/molecules/forms/input-tags/InputTags.tsx

+21-43
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,28 @@
1-
import Choices from "@formio/choices.js";
2-
import uniq from "lodash/uniq";
3-
import { useEffect, useRef } from "react";
1+
import { ComponentType } from "react";
42

5-
import { registerComponent } from "../../../registries/components";
6-
import { FormControl, FormControlProps } from "../form-control/FormControl";
3+
import { getComponent, registerComponent } from "../../../registries/components";
4+
import { type FormControl as DefaultFormControl } from "../form-control/FormControl";
5+
import type { InputTagsProps } from "./InputTags.interface";
76

8-
export interface InputTagsProps<T = any> extends Omit<FormControlProps, "description" | "prefix" | "suffix"> {
9-
value?: T;
10-
onChange?: (name: string, value: T) => void;
11-
placeholder?: string;
12-
13-
[key: string]: any;
14-
}
15-
16-
export function InputTags({ name, value = [], label, onChange, required, description, prefix, suffix, ...props }: InputTagsProps) {
17-
const ref: any = useRef();
18-
19-
useEffect(() => {
20-
const instance = new Choices(ref.current, {
21-
delimiter: ",",
22-
editItems: true,
23-
removeItemButton: true
24-
});
25-
26-
instance.setValue([].concat(value, []));
27-
28-
instance.passedElement.element.addEventListener("addItem", (event: any) => {
29-
onChange && onChange(name, uniq(value.concat(event.detail.value)));
30-
});
31-
32-
instance.passedElement.element.addEventListener("removeItem", (event: any) => {
33-
onChange &&
34-
onChange(
35-
name,
36-
value.filter((v: string) => v !== event.detail.value)
37-
);
38-
});
39-
40-
return () => {
41-
instance.destroy();
42-
};
43-
}, []);
7+
export function InputTags<Data = string>(props: InputTagsProps) {
8+
const { name, id = name, label, required, description, before, after, size, className, layout = "choicesjs", ...otherProps } = props;
449

10+
const FormControl = getComponent<typeof DefaultFormControl>("FormControl");
11+
const Component = getComponent<ComponentType<InputTagsProps<Data>>>([`InputTags.${layout}`, "Input"]);
12+
console.log("VALUE", props.value);
4513
return (
46-
<FormControl name={name} label={label} required={required} description={description} prefix={prefix} suffix={suffix}>
47-
<input ref={ref} type='text' {...props} id={name} required={required} />
14+
<FormControl
15+
id={id}
16+
name={name}
17+
label={label}
18+
required={required}
19+
description={description}
20+
before={before}
21+
after={after}
22+
size={size}
23+
className={className}
24+
>
25+
<Component {...(otherProps as any)} id={id} name={name} required={required} />
4826
</FormControl>
4927
);
5028
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import "../form-control/FormControl";
2+
import "./components/ChoicesTags";
3+
import "./components/ReactTags";
4+
import "../input-text/InputText";
5+
export * from "./InputTags";
6+
export * from "./InputTags.interface";

packages/react-formio/src/molecules/forms/input-tags/InputTags.stories.tsx packages/react-formio/src/molecules/forms/input-tags/components/ChoicesTags.stories.tsx

+40-11
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
1+
import "../all";
2+
13
import type { Meta, StoryObj } from "@storybook/react";
24

3-
import { iconClass } from "../../../utils/iconClass";
4-
import { useValue } from "../../__fixtures__/useValue.hook";
5-
import { InputTags } from "./InputTags";
5+
import { iconClass } from "../../../../utils/iconClass";
6+
import { useValue } from "../../../__fixtures__/useValue.hook";
7+
import { InputTags } from "../InputTags";
68

79
/**
10+
* The InputTags component enables users to create new options in the text field.
11+
*
812
* ```tsx
13+
* import {InputTags} from "@tsed/react-formio/molecules/forms/input-tags/all";
14+
*
15+
* or
16+
*
17+
* import "@tsed/react-formio/molecules/forms/input-tags/components/ChoicesTags";
18+
* import "@tsed/react-formio/molecules/forms/input-tags/components/ReactTags";
19+
* import "@tsed/react-formio/molecules/forms/input-text/InputText";
920
* import {InputTags} from "@tsed/react-formio/molecules/forms/input-tags/InputTags";
21+
*
1022
* ```
1123
*/
1224
export default {
13-
title: "forms/InputTags",
25+
title: "forms/InputTags/ChoicesJs",
1426
component: InputTags,
1527
argTypes: {
1628
label: {
@@ -29,14 +41,21 @@ export default {
2941
placeholder: {
3042
control: "text"
3143
},
32-
choices: {
33-
control: "object"
34-
},
3544
description: {
3645
control: "text"
46+
},
47+
layout: {
48+
control: "select",
49+
options: ["choicesjs", "react"]
50+
},
51+
onChange: {
52+
action: "onChange"
3753
}
3854
},
3955
parameters: {},
56+
args: {
57+
layout: "choicesjs"
58+
},
4059
tags: ["autodocs"]
4160
} satisfies Meta<typeof InputTags>;
4261

@@ -52,10 +71,20 @@ export const Usage: Story = {
5271
}
5372
};
5473

55-
export const WithPrefix: Story = {
74+
export const WithSizeOption: Story = {
75+
args: {
76+
name: "name",
77+
label: "Label",
78+
value: ["test"],
79+
size: "small",
80+
placeholder: "Placeholder"
81+
}
82+
};
83+
84+
export const AppendBefore: Story = {
5685
render(args) {
5786
// eslint-disable-next-line react-hooks/rules-of-hooks
58-
return <InputTags prefix={<i className={iconClass(undefined, "calendar")} />} {...useValue(args)} />;
87+
return <InputTags before={<i className={iconClass(undefined, "calendar")} />} {...useValue(args)} />;
5988
},
6089
args: {
6190
label: "Label",
@@ -66,10 +95,10 @@ export const WithPrefix: Story = {
6695
}
6796
};
6897

69-
export const WithSuffix: Story = {
98+
export const AppendAfter: Story = {
7099
render(args) {
71100
// eslint-disable-next-line react-hooks/rules-of-hooks
72-
return <InputTags suffix={<i className={iconClass(undefined, "calendar")} />} {...useValue(args)} />;
101+
return <InputTags after={<i className={iconClass(undefined, "calendar")} />} {...useValue(args)} />;
73102
},
74103
args: {
75104
label: "Label",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import Choices from "@formio/choices.js";
2+
import { useEffect, useRef } from "react";
3+
import { useDebouncedCallback } from "use-debounce";
4+
5+
import { registerComponent } from "../../../../registries/components";
6+
import { cleanFormControlProps } from "../../form-control/FormControl";
7+
import type { InputTagsProps } from "../InputTags.interface";
8+
9+
export function useChoiceTags<Data = string>(props: InputTagsProps<Data>) {
10+
const { value, onChange, name = "", delimiter, customProperties, ...otherProps } = props;
11+
const ref = useRef<HTMLInputElement | null>(null);
12+
const instanceRef = useRef<Choices | null>(null);
13+
14+
const onAdd = useDebouncedCallback((add: Data) => {
15+
const values = ((value || []) as Data[]).concat(add);
16+
17+
onChange?.(name, [...values]);
18+
}, 100);
19+
20+
const onDelete = useDebouncedCallback((remove: Data) => {
21+
const values = (value || []).filter((v) => v !== remove);
22+
23+
onChange?.(name, [...values]);
24+
});
25+
26+
useEffect(() => {
27+
if (ref.current) {
28+
const instance = new Choices(ref.current!, {
29+
duplicateItemsAllowed: false,
30+
...customProperties,
31+
delimiter,
32+
editItems: true,
33+
removeItemButton: true
34+
});
35+
36+
instance.setValue((value || []) as string[]);
37+
38+
instanceRef.current = instance;
39+
40+
instance.passedElement.element.addEventListener("addItem", (event: { detail: { value: unknown } }) => {
41+
onAdd(event.detail.value as Data);
42+
});
43+
44+
instance.passedElement.element.addEventListener("removeItem", (event: { detail: { value: unknown } }) => {
45+
onDelete(event.detail.value as Data);
46+
});
47+
}
48+
49+
return () => {
50+
if (instanceRef.current) {
51+
instanceRef.current.destroy();
52+
}
53+
};
54+
}, [delimiter]);
55+
56+
return {
57+
otherProps: {
58+
...otherProps,
59+
name
60+
},
61+
ref,
62+
instanceRef
63+
};
64+
}
65+
66+
export function ChoicesTags<Data = string>(props: InputTagsProps<Data>) {
67+
const { ref, otherProps } = useChoiceTags<Data>(props);
68+
69+
return <input type='text' {...cleanFormControlProps(otherProps)} ref={ref} />;
70+
}
71+
72+
registerComponent("InputTags.choicesjs", ChoicesTags);

0 commit comments

Comments
 (0)