@twintag/twintag-form 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.eslintrc.cjs +4 -0
  2. package/.turbo/turbo-build.log +50 -0
  3. package/CHANGELOG.md +7 -0
  4. package/dist/index.es.js +27356 -0
  5. package/dist/index.umd.js +196 -0
  6. package/dist/lib/components/Form/index.d.ts +10 -0
  7. package/dist/lib/components/Form/index.d.ts.map +1 -0
  8. package/dist/lib/components/Form/register.d.ts +13 -0
  9. package/dist/lib/components/Form/register.d.ts.map +1 -0
  10. package/dist/lib/components/Input/checkbox.d.ts +8 -0
  11. package/dist/lib/components/Input/checkbox.d.ts.map +1 -0
  12. package/dist/lib/components/Input/file.d.ts +8 -0
  13. package/dist/lib/components/Input/file.d.ts.map +1 -0
  14. package/dist/lib/components/Input/index.d.ts +8 -0
  15. package/dist/lib/components/Input/index.d.ts.map +1 -0
  16. package/dist/lib/components/Input/simple.d.ts +8 -0
  17. package/dist/lib/components/Input/simple.d.ts.map +1 -0
  18. package/dist/lib/components/Input/submit.d.ts +8 -0
  19. package/dist/lib/components/Input/submit.d.ts.map +1 -0
  20. package/dist/lib/components/Input/textarea.d.ts +8 -0
  21. package/dist/lib/components/Input/textarea.d.ts.map +1 -0
  22. package/dist/lib/components/Input/toggle.d.ts +8 -0
  23. package/dist/lib/components/Input/toggle.d.ts.map +1 -0
  24. package/dist/lib/index.d.ts +18 -0
  25. package/dist/lib/index.d.ts.map +1 -0
  26. package/dist/lib/styles.d.ts +120 -0
  27. package/dist/lib/styles.d.ts.map +1 -0
  28. package/dist/lib/types/index.d.ts +16 -0
  29. package/dist/lib/types/index.d.ts.map +1 -0
  30. package/dist/lib/webcomponent.d.ts +2 -0
  31. package/dist/lib/webcomponent.d.ts.map +1 -0
  32. package/dist/twind.config.d.ts +5 -0
  33. package/dist/twind.config.d.ts.map +1 -0
  34. package/dist/webcomponent.es.js +31725 -0
  35. package/dist/webcomponent.umd.js +196 -0
  36. package/lib/components/Form/index.tsx +86 -0
  37. package/lib/components/Form/register.tsx +70 -0
  38. package/lib/components/Input/checkbox.tsx +50 -0
  39. package/lib/components/Input/file.tsx +76 -0
  40. package/lib/components/Input/index.tsx +30 -0
  41. package/lib/components/Input/simple.tsx +41 -0
  42. package/lib/components/Input/submit.tsx +24 -0
  43. package/lib/components/Input/textarea.tsx +36 -0
  44. package/lib/components/Input/toggle.tsx +50 -0
  45. package/lib/index.tsx +173 -0
  46. package/lib/styles.ts +116 -0
  47. package/lib/types/index.ts +16 -0
  48. package/lib/webcomponent.ts +20 -0
  49. package/package.json +56 -0
  50. package/scripts/build.js +55 -0
  51. package/tailwind.config.cjs +8 -0
  52. package/tsconfig.json +13 -0
  53. package/twind.config.ts +22 -0
@@ -0,0 +1,86 @@
1
+ import type { FieldValues } from 'react-hook-form';
2
+ import type { WithChildren } from 'twintag-types/dom';
3
+
4
+ import { useEffect, useRef } from 'react';
5
+ import { useForm } from 'react-hook-form';
6
+
7
+ import { RegisterInputs } from './register';
8
+
9
+ type Props = {
10
+ options?: {
11
+ resetOnSubmit?: boolean;
12
+ };
13
+ } & WithChildren;
14
+
15
+ const Form = ({ children, options }: Props) => {
16
+ const formRef = useRef<HTMLFormElement>(null);
17
+
18
+ const {
19
+ reset,
20
+ register,
21
+ handleSubmit,
22
+ formState: { isSubmitSuccessful },
23
+ control,
24
+ } = useForm();
25
+
26
+ useEffect(() => {
27
+ if (isSubmitSuccessful && options?.resetOnSubmit) reset();
28
+ }, [isSubmitSuccessful]);
29
+
30
+ const onValid = (data: FieldValues) => {
31
+ const customSubmit = new CustomEvent('twintag-form-valid', {
32
+ detail: data,
33
+ bubbles: true,
34
+ cancelable: false,
35
+ composed: true,
36
+ });
37
+
38
+ formRef.current?.dispatchEvent(customSubmit);
39
+ };
40
+
41
+ const onInvalid = (__errors: FieldValues) => {
42
+ const customSubmit = new CustomEvent('twintag-form-invalid', {
43
+ detail: __errors,
44
+ bubbles: true,
45
+ cancelable: false,
46
+ composed: true,
47
+ });
48
+
49
+ formRef.current?.dispatchEvent(customSubmit);
50
+ };
51
+
52
+ const onValidate = () => {
53
+ handleSubmit(onValid, onInvalid)();
54
+ };
55
+
56
+ useEffect(() => {
57
+ if (formRef.current) {
58
+ formRef.current.addEventListener('twintag-form-validate', onValidate);
59
+ }
60
+
61
+ return () => {
62
+ if (formRef.current) {
63
+ formRef.current.removeEventListener(
64
+ 'twintag-form-validate',
65
+ onValidate
66
+ );
67
+ }
68
+ };
69
+ }, []);
70
+
71
+ return (
72
+ <form
73
+ ref={formRef}
74
+ className='relative font-sans'
75
+ onSubmit={handleSubmit(onValid, onInvalid)}
76
+ >
77
+ <div className='grid w-auto gap-4'>
78
+ <RegisterInputs useForm={{ register, control }}>
79
+ {children}
80
+ </RegisterInputs>
81
+ </div>
82
+ </form>
83
+ );
84
+ };
85
+
86
+ export default Form;
@@ -0,0 +1,70 @@
1
+ import type { ReactElement } from 'react';
2
+ import type { UseFormRegister, Control } from 'react-hook-form';
3
+ import type { WithChildren } from 'twintag-types/dom';
4
+
5
+ import { Children, createElement } from 'react';
6
+
7
+ import Input from '../Input';
8
+
9
+ const RegisterInputs = ({
10
+ children,
11
+ useForm,
12
+ }: {
13
+ useForm: { register: UseFormRegister<any>; control: Control };
14
+ } & WithChildren) => {
15
+ return Children.map(children as any, (child) => {
16
+ if (!child) return null;
17
+
18
+ if (child.props?.config?.items) {
19
+ return createElement(child.type, {
20
+ config: {
21
+ ...child.props.config,
22
+ items: child.props.config.items.map((item: any) => {
23
+ if (item.type === 'input') {
24
+ return {
25
+ ...item,
26
+ useForm,
27
+ };
28
+ }
29
+
30
+ return item;
31
+ }),
32
+ },
33
+ });
34
+ }
35
+
36
+ if (child.props?.children) {
37
+ return createElement(
38
+ child.type,
39
+ {
40
+ ...child.props,
41
+ },
42
+ <RegisterInputs useForm={useForm}>
43
+ {child.props.children}
44
+ </RegisterInputs>
45
+ );
46
+ }
47
+
48
+ if ((child as ReactElement).props?.config?.type === 'input') {
49
+ return createElement(child.type, {
50
+ config: {
51
+ ...child.props.config,
52
+ useForm,
53
+ },
54
+ });
55
+ }
56
+
57
+ if (child.type === Input) {
58
+ return createElement(child.type, {
59
+ ...child.props,
60
+ useForm,
61
+ });
62
+ }
63
+
64
+ return createElement(child.type, {
65
+ ...child.props,
66
+ });
67
+ });
68
+ };
69
+
70
+ export { RegisterInputs };
@@ -0,0 +1,50 @@
1
+ import type { VariantProps } from 'tailwind-variants';
2
+
3
+ import { useMemo } from 'react';
4
+ import { useController } from 'react-hook-form';
5
+ import { CheckIcon } from '@heroicons/react/24/outline';
6
+ import * as RdxCheckbox from '@radix-ui/react-checkbox';
7
+
8
+ import type { CommonProps } from '@/types';
9
+
10
+ import { checkboxStyle } from '@/styles';
11
+
12
+ type Props = CommonProps<VariantProps<typeof checkboxStyle>['variant']>;
13
+
14
+ const Checkbox = ({ useForm, label, bind, options }: Props) => {
15
+ const { container, checkbox } = useMemo(() => {
16
+ if (options?.variant) return checkboxStyle({ variant: options.variant });
17
+ return checkboxStyle();
18
+ }, []);
19
+
20
+ const {
21
+ field: { onChange, value },
22
+ } = useController({
23
+ name: bind as string,
24
+ control: useForm ? useForm.control : undefined,
25
+ defaultValue: false,
26
+ rules: {
27
+ required: options?.optionalField ? false : `${label} is required`,
28
+ },
29
+ });
30
+
31
+ return (
32
+ <label className={container()}>
33
+ <RdxCheckbox.Root
34
+ className={checkbox()}
35
+ defaultChecked={value}
36
+ onCheckedChange={(state) => {
37
+ onChange(state);
38
+ }}
39
+ >
40
+ <RdxCheckbox.Indicator className='text-black'>
41
+ <CheckIcon className='w-4' />
42
+ </RdxCheckbox.Indicator>
43
+ </RdxCheckbox.Root>
44
+
45
+ <span className='leading-none text-black'>{label as string}</span>
46
+ </label>
47
+ );
48
+ };
49
+
50
+ export default Checkbox;
@@ -0,0 +1,76 @@
1
+ import type { VariantProps } from 'tailwind-variants';
2
+
3
+ import { useMemo } from 'react';
4
+ import { useController } from 'react-hook-form';
5
+ import { CloudArrowUpIcon } from '@heroicons/react/24/outline';
6
+
7
+ import type { CommonProps } from '@/types';
8
+
9
+ import { fileStyle } from '@/styles';
10
+
11
+ type Props = CommonProps<VariantProps<typeof fileStyle>['variant']>;
12
+
13
+ const File = ({ useForm, label, bind, options }: Props) => {
14
+ const { container, span, input } = useMemo(() => {
15
+ if (options?.variant) return fileStyle({ variant: options?.variant });
16
+ return fileStyle();
17
+ }, []);
18
+
19
+ const {
20
+ field: { onChange, value },
21
+ } = useController({
22
+ name: bind as string,
23
+ control: useForm ? useForm.control : undefined,
24
+ defaultValue: null,
25
+ rules: {
26
+ required: options?.optionalField ? false : `${label} is required`,
27
+ },
28
+ });
29
+
30
+ return (
31
+ <label className={container()}>
32
+ {label ? <span className={span()}>{label as string}</span> : <></>}
33
+
34
+ <input
35
+ type='file'
36
+ multiple
37
+ capture='environment'
38
+ onChange={(e) => {
39
+ if (e.target.files) {
40
+ onChange(e.target.files);
41
+ }
42
+ }}
43
+ className='sr-only peer'
44
+ />
45
+
46
+ <span
47
+ className={`${input()} ${
48
+ value && value.length > 0
49
+ ? 'border-primary text-primary'
50
+ : 'border-slate-700/10 text-slate-400'
51
+ }`}
52
+ >
53
+ <span className='grid'>
54
+ {value && value.length > 0 ? (
55
+ Array.from(value as unknown as FileList).map((item, index) => {
56
+ return (
57
+ <span
58
+ key={index}
59
+ className='block max-w-full overflow-hidden text-ellipsis whitespace-nowrap'
60
+ >
61
+ {item.name}
62
+ </span>
63
+ );
64
+ })
65
+ ) : (
66
+ <span>Select a file</span>
67
+ )}
68
+ </span>
69
+
70
+ <CloudArrowUpIcon className='h-6 w-6 min-w-[1.5rem]' />
71
+ </span>
72
+ </label>
73
+ );
74
+ };
75
+
76
+ export default File;
@@ -0,0 +1,30 @@
1
+ import type { CommonProps } from '@/types';
2
+
3
+ import Simple from './simple';
4
+ import Textarea from './textarea';
5
+ import Toggle from './toggle';
6
+ import File from './file';
7
+ import Submit from './submit';
8
+ import Checkbox from './checkbox';
9
+
10
+ export const Inputs = new Map()
11
+ .set('simple', Simple)
12
+ .set('textarea', Textarea)
13
+ .set('toggle', Toggle)
14
+ .set('file', File)
15
+ .set('submit', Submit)
16
+ .set('checkbox', Checkbox);
17
+
18
+ type Props = {
19
+ subtype?: string;
20
+ };
21
+
22
+ const Input = ({ subtype = 'simple', ...rest }: Props) => {
23
+ const InputComponent = Inputs.get(subtype);
24
+
25
+ if (!InputComponent) return <></>;
26
+
27
+ return <InputComponent {...rest} />;
28
+ };
29
+
30
+ export default Input;
@@ -0,0 +1,41 @@
1
+ import type { VariantProps } from 'tailwind-variants';
2
+
3
+ import { useId, useMemo } from 'react';
4
+
5
+ import type { CommonProps } from '@/types';
6
+
7
+ import { simpleStyle } from '@/styles';
8
+
9
+ type Props = CommonProps<VariantProps<typeof simpleStyle>['variant']>;
10
+
11
+ const Simple = ({ useForm, label, bind, options }: Props) => {
12
+ const id = useId();
13
+
14
+ const { container, span, input } = useMemo(() => {
15
+ if (options?.variant) return simpleStyle({ variant: options.variant });
16
+ return simpleStyle();
17
+ }, []);
18
+
19
+ return (
20
+ <div className={container()}>
21
+ {label ? (
22
+ <label htmlFor={id}>
23
+ <span className={span()}>{label as string}</span>
24
+ </label>
25
+ ) : (
26
+ <></>
27
+ )}
28
+ <input
29
+ id={id}
30
+ type='text'
31
+ placeholder='Write here...'
32
+ className={input()}
33
+ {...useForm!.register(bind as string, {
34
+ required: options?.optionalField ? false : `${label} is required`,
35
+ })}
36
+ />
37
+ </div>
38
+ );
39
+ };
40
+
41
+ export default Simple;
@@ -0,0 +1,24 @@
1
+ import type { VariantProps } from 'tailwind-variants';
2
+
3
+ import { useMemo } from 'react';
4
+
5
+ import type { CommonProps } from '@/types';
6
+
7
+ import { submitStyle } from '@/styles';
8
+
9
+ type Props = CommonProps<VariantProps<typeof submitStyle>['variant']>;
10
+
11
+ const Submit = ({ label, options }: Props) => {
12
+ const style = useMemo(() => {
13
+ if (options?.variant) return submitStyle({ variant: options.variant });
14
+ return submitStyle();
15
+ }, []);
16
+
17
+ return (
18
+ <button type='submit' className={style}>
19
+ {label as string}
20
+ </button>
21
+ );
22
+ };
23
+
24
+ export default Submit;
@@ -0,0 +1,36 @@
1
+ import type { VariantProps } from 'tailwind-variants';
2
+
3
+ import { useId, useMemo } from 'react';
4
+
5
+ import type { CommonProps } from '@/types';
6
+
7
+ import { simpleStyle } from '@/styles';
8
+
9
+ type Props = CommonProps<VariantProps<typeof simpleStyle>['variant']>;
10
+
11
+ const Textarea = ({ useForm, label, bind, options }: Props) => {
12
+ const id = useId();
13
+
14
+ const { container, span, input } = useMemo(() => {
15
+ if (options?.variant) return simpleStyle({ variant: options.variant });
16
+ return simpleStyle();
17
+ }, []);
18
+
19
+ return (
20
+ <div className={container()}>
21
+ <label htmlFor={id}>
22
+ <span className={span()}>{label as string}</span>
23
+ </label>
24
+ <textarea
25
+ id={id}
26
+ className={input()}
27
+ placeholder='Write here...'
28
+ {...useForm!.register(bind as string, {
29
+ required: options?.optionalField ? false : `${label} is required`,
30
+ })}
31
+ />
32
+ </div>
33
+ );
34
+ };
35
+
36
+ export default Textarea;
@@ -0,0 +1,50 @@
1
+ import type { VariantProps } from 'tailwind-variants';
2
+
3
+ import { useMemo } from 'react';
4
+ import { useController } from 'react-hook-form';
5
+ import * as Switch from '@radix-ui/react-switch';
6
+
7
+ import type { CommonProps } from '@/types';
8
+
9
+ import { toggleStyle } from '@/styles';
10
+
11
+ type Props = CommonProps<VariantProps<typeof toggleStyle>['variant']>;
12
+
13
+ const Toggle = ({ useForm, label, bind, options }: Props) => {
14
+ const { container, span, input, thumb } = useMemo(() => {
15
+ if (options?.variant) return toggleStyle({ variant: options.variant });
16
+ return toggleStyle();
17
+ }, []);
18
+
19
+ const {
20
+ field: { onChange, value },
21
+ } = useController({
22
+ name: bind as string,
23
+ control: useForm ? useForm.control : undefined,
24
+ defaultValue: false,
25
+ rules: {
26
+ required: options?.optionalField ? false : `${label} is required`,
27
+ },
28
+ });
29
+
30
+ return (
31
+ <div className='w-full'>
32
+ <label className={container()}>
33
+ {label ? <span className={span()}>{label as string}</span> : <></>}
34
+
35
+ <Switch.Root
36
+ id={bind as string}
37
+ checked={value}
38
+ className={input()}
39
+ onCheckedChange={(state) => {
40
+ onChange(state);
41
+ }}
42
+ >
43
+ <Switch.Thumb className={thumb()} />
44
+ </Switch.Root>
45
+ </label>
46
+ </div>
47
+ );
48
+ };
49
+
50
+ export default Toggle;
package/lib/index.tsx ADDED
@@ -0,0 +1,173 @@
1
+ import type { FC, ReactNode } from 'react';
2
+
3
+ import { LabelToString } from 'twintag-types/dom';
4
+
5
+ import Input from './components/Input';
6
+ import Form from './components/Form';
7
+
8
+ type Config = {
9
+ key?: number;
10
+ type?: any;
11
+ subtype?: string;
12
+ label?: string;
13
+ items?: any[];
14
+ bind?: string;
15
+ options?: Record<string, any>;
16
+ };
17
+
18
+ type Component = {
19
+ subtype?: string;
20
+ label?: string;
21
+ bind?: string;
22
+ children?: ReactNode;
23
+ options?: Record<string, any>;
24
+ };
25
+
26
+ type Format = {
27
+ $config: Config;
28
+ $blocks?: Map<string, any>;
29
+ $containers?: Map<string, any>;
30
+ $available?: Map<string, any>;
31
+ };
32
+
33
+ const format = ({ $config, $blocks, $containers, $available }: Format) => {
34
+ const config: Config & {
35
+ block?: FC<Component>;
36
+ container?: FC<Component>;
37
+ } = { ...$config };
38
+
39
+ const typeLength = config.type.length;
40
+ const colonIndex = config.type.indexOf(':');
41
+
42
+ const typeFiltered = config.type.substring(
43
+ 0,
44
+ colonIndex > 0 ? config.type.lastIndexOf(':') : typeLength + 1
45
+ );
46
+ const subtypeFiltered =
47
+ colonIndex > 0 && colonIndex + 1 !== typeLength
48
+ ? config.type.substring(colonIndex + 1, typeLength + 1)
49
+ : undefined;
50
+
51
+ const isTypeAvailable = $available?.has(typeFiltered);
52
+
53
+ if (isTypeAvailable) {
54
+ const block = $blocks?.get(typeFiltered);
55
+ const container = $containers?.get(typeFiltered);
56
+
57
+ config.type = typeFiltered;
58
+ config.subtype = config.subtype ? config.subtype : subtypeFiltered;
59
+ config.block = block;
60
+ config.container = container;
61
+ }
62
+
63
+ if (config.label) config.label = LabelToString.parse(config.label);
64
+
65
+ if (config.items && Array.isArray(config.items))
66
+ config.items = config.items.map((item) =>
67
+ format({
68
+ $config: item,
69
+ $available,
70
+ $blocks,
71
+ $containers,
72
+ })
73
+ );
74
+
75
+ return config;
76
+ };
77
+
78
+ type Params = {
79
+ $config: Config;
80
+ $blocks?: Map<string, any>;
81
+ $containers?: Map<string, any>;
82
+ };
83
+
84
+ const useConfig = (params: Params) => {
85
+ const available = new Map([
86
+ ...new Map(params.$blocks),
87
+ ...new Map(params.$containers),
88
+ ]);
89
+
90
+ return format({
91
+ ...params,
92
+ $available: available,
93
+ });
94
+ };
95
+
96
+ const blocks = new Map().set('input', Input);
97
+ const containers = new Map().set('form', Form);
98
+
99
+ type RendererProps = {
100
+ config: Config & {
101
+ block?: FC<Component>;
102
+ container?: FC<Component>;
103
+ };
104
+ };
105
+
106
+ const FormRenderer = ({ config }: RendererProps) => {
107
+ const { key, type, subtype, label, bind, items, block, container, ...rest } =
108
+ config;
109
+
110
+ if (!type) return null;
111
+
112
+ if (container && items && Array.isArray(items)) {
113
+ const Component = container;
114
+
115
+ return (
116
+ <Component key={key} subtype={subtype} label={label} {...rest}>
117
+ {items.map((item, idx) => (
118
+ <FormRenderer
119
+ key={idx}
120
+ config={{
121
+ ...rest,
122
+ ...item,
123
+ options: {
124
+ ...item.options,
125
+ variant: rest.options?.variant,
126
+ },
127
+ }}
128
+ />
129
+ ))}
130
+ </Component>
131
+ );
132
+ }
133
+
134
+ if (block) {
135
+ const Component = block;
136
+
137
+ return (
138
+ <Component
139
+ key={key}
140
+ subtype={subtype}
141
+ label={label}
142
+ bind={bind}
143
+ {...rest}
144
+ />
145
+ );
146
+ }
147
+
148
+ return null;
149
+ };
150
+
151
+ type Props = {
152
+ config: Component[];
153
+ options?: {
154
+ variant?: string;
155
+ resetOnSubmit?: boolean;
156
+ };
157
+ };
158
+
159
+ const TwintagForm = ({ config, options }: Props) => {
160
+ const __config = useConfig({
161
+ $blocks: blocks,
162
+ $containers: containers,
163
+ $config: {
164
+ type: 'form',
165
+ items: config ? config : [],
166
+ options,
167
+ },
168
+ });
169
+
170
+ return <FormRenderer config={__config} />;
171
+ };
172
+
173
+ export default TwintagForm;