Skip to content

Commit

Permalink
feat(react-formio): add support Choicesjs and React-select layout to …
Browse files Browse the repository at this point in the history
…InputTags
  • Loading branch information
Romakita committed Jan 25, 2025
1 parent 6d83862 commit 388eac8
Show file tree
Hide file tree
Showing 20 changed files with 486 additions and 154 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export type FormControlProps<
Attributes extends HTMLAttributes<HTMLElement> = InputHTMLAttributes<HTMLInputElement>
> = BaseFormControlProps<Value> & Omit<Attributes, "onChange" | "value" | "size">;

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

export function FormControl<Value = unknown>({
Expand All @@ -48,7 +48,7 @@ export function FormControl<Value = unknown>({
"-with-before": !!before,
"-with-after": !!after
},
size && `form-group-${size}`,
size && `-size-${size}`,
className
)}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { InputHTMLAttributes } from "react";

import type { FormControlProps } from "../form-control/FormControl";

export interface InputTagsProps<Data = string> extends FormControlProps<Data[], InputHTMLAttributes<HTMLInputElement>> {
layout?: "html5" | "react" | "choicesjs";
delimiter?: string;
customProperties?: Record<string, any>;
}
64 changes: 21 additions & 43 deletions packages/react-formio/src/molecules/forms/input-tags/InputTags.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,28 @@
import Choices from "@formio/choices.js";
import uniq from "lodash/uniq";
import { useEffect, useRef } from "react";
import { ComponentType } from "react";

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

export interface InputTagsProps<T = any> extends Omit<FormControlProps, "description" | "prefix" | "suffix"> {
value?: T;
onChange?: (name: string, value: T) => void;
placeholder?: string;

[key: string]: any;
}

export function InputTags({ name, value = [], label, onChange, required, description, prefix, suffix, ...props }: InputTagsProps) {
const ref: any = useRef();

useEffect(() => {
const instance = new Choices(ref.current, {
delimiter: ",",
editItems: true,
removeItemButton: true
});

instance.setValue([].concat(value, []));

instance.passedElement.element.addEventListener("addItem", (event: any) => {
onChange && onChange(name, uniq(value.concat(event.detail.value)));
});

instance.passedElement.element.addEventListener("removeItem", (event: any) => {
onChange &&
onChange(
name,
value.filter((v: string) => v !== event.detail.value)
);
});

return () => {
instance.destroy();
};
}, []);
export function InputTags<Data = string>(props: InputTagsProps) {
const { name, id = name, label, required, description, before, after, size, className, layout = "choicesjs", ...otherProps } = props;

const FormControl = getComponent<typeof DefaultFormControl>("FormControl");
const Component = getComponent<ComponentType<InputTagsProps<Data>>>([`InputTags.${layout}`, "Input"]);
console.log("VALUE", props.value);
return (
<FormControl name={name} label={label} required={required} description={description} prefix={prefix} suffix={suffix}>
<input ref={ref} type='text' {...props} id={name} required={required} />
<FormControl
id={id}
name={name}
label={label}
required={required}
description={description}
before={before}
after={after}
size={size}
className={className}
>
<Component {...(otherProps as any)} id={id} name={name} required={required} />
</FormControl>
);
}
Expand Down
6 changes: 6 additions & 0 deletions packages/react-formio/src/molecules/forms/input-tags/all.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import "../form-control/FormControl";
import "./components/ChoicesTags";
import "./components/ReactTags";
import "../input-text/InputText";
export * from "./InputTags";
export * from "./InputTags.interface";
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
import "../all";

import type { Meta, StoryObj } from "@storybook/react";

import { iconClass } from "../../../utils/iconClass";
import { useValue } from "../../__fixtures__/useValue.hook";
import { InputTags } from "./InputTags";
import { iconClass } from "../../../../utils/iconClass";
import { useValue } from "../../../__fixtures__/useValue.hook";
import { InputTags } from "../InputTags";

/**
* The InputTags component enables users to create new options in the text field.
*
* ```tsx
* import {InputTags} from "@tsed/react-formio/molecules/forms/input-tags/all";
*
* or
*
* import "@tsed/react-formio/molecules/forms/input-tags/components/ChoicesTags";
* import "@tsed/react-formio/molecules/forms/input-tags/components/ReactTags";
* import "@tsed/react-formio/molecules/forms/input-text/InputText";
* import {InputTags} from "@tsed/react-formio/molecules/forms/input-tags/InputTags";
*
* ```
*/
export default {
title: "forms/InputTags",
title: "forms/InputTags/ChoicesJs",
component: InputTags,
argTypes: {
label: {
Expand All @@ -29,14 +41,21 @@ export default {
placeholder: {
control: "text"
},
choices: {
control: "object"
},
description: {
control: "text"
},
layout: {
control: "select",
options: ["choicesjs", "react"]
},
onChange: {
action: "onChange"
}
},
parameters: {},
args: {
layout: "choicesjs"
},
tags: ["autodocs"]
} satisfies Meta<typeof InputTags>;

Expand All @@ -52,10 +71,20 @@ export const Usage: Story = {
}
};

export const WithPrefix: Story = {
export const WithSizeOption: Story = {
args: {
name: "name",
label: "Label",
value: ["test"],
size: "small",
placeholder: "Placeholder"
}
};

export const AppendBefore: Story = {
render(args) {
// eslint-disable-next-line react-hooks/rules-of-hooks
return <InputTags prefix={<i className={iconClass(undefined, "calendar")} />} {...useValue(args)} />;
return <InputTags before={<i className={iconClass(undefined, "calendar")} />} {...useValue(args)} />;
},
args: {
label: "Label",
Expand All @@ -66,10 +95,10 @@ export const WithPrefix: Story = {
}
};

export const WithSuffix: Story = {
export const AppendAfter: Story = {
render(args) {
// eslint-disable-next-line react-hooks/rules-of-hooks
return <InputTags suffix={<i className={iconClass(undefined, "calendar")} />} {...useValue(args)} />;
return <InputTags after={<i className={iconClass(undefined, "calendar")} />} {...useValue(args)} />;
},
args: {
label: "Label",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import Choices from "@formio/choices.js";
import { useEffect, useRef } from "react";
import { useDebouncedCallback } from "use-debounce";

import { registerComponent } from "../../../../registries/components";
import { cleanFormControlProps } from "../../form-control/FormControl";
import type { InputTagsProps } from "../InputTags.interface";

export function useChoiceTags<Data = string>(props: InputTagsProps<Data>) {
const { value, onChange, name = "", delimiter, customProperties, ...otherProps } = props;
const ref = useRef<HTMLInputElement | null>(null);
const instanceRef = useRef<Choices | null>(null);

const onAdd = useDebouncedCallback((add: Data) => {
const values = ((value || []) as Data[]).concat(add);

onChange?.(name, [...values]);
}, 100);

const onDelete = useDebouncedCallback((remove: Data) => {
const values = (value || []).filter((v) => v !== remove);

onChange?.(name, [...values]);
});

useEffect(() => {
if (ref.current) {
const instance = new Choices(ref.current!, {
duplicateItemsAllowed: false,
...customProperties,
delimiter,
editItems: true,
removeItemButton: true
});

instance.setValue((value || []) as string[]);

instanceRef.current = instance;

instance.passedElement.element.addEventListener("addItem", (event: { detail: { value: unknown } }) => {
onAdd(event.detail.value as Data);
});

instance.passedElement.element.addEventListener("removeItem", (event: { detail: { value: unknown } }) => {
onDelete(event.detail.value as Data);
});
}

return () => {
if (instanceRef.current) {
instanceRef.current.destroy();
}
};
}, [delimiter]);

Check warning on line 54 in packages/react-formio/src/molecules/forms/input-tags/components/ChoicesTags.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has missing dependencies: 'customProperties', 'onAdd', 'onDelete', and 'value'. Either include them or remove the dependency array

return {
otherProps: {
...otherProps,
name
},
ref,
instanceRef
};
}

export function ChoicesTags<Data = string>(props: InputTagsProps<Data>) {
const { ref, otherProps } = useChoiceTags<Data>(props);

return <input type='text' {...cleanFormControlProps(otherProps)} ref={ref} />;
}

registerComponent("InputTags.choicesjs", ChoicesTags);
Loading

0 comments on commit 388eac8

Please sign in to comment.