Skip to content

Commit 9a959c1

Browse files
logaretmgenu
andauthored
feat: granular field composables (#205)
* feat: initial API draft * revert: go back to old api and refactor description props * refactor: rename useFormField to useFieldState * feat: second draft of the api * feat: added injectable version * feat: expose injectables * feat: export useFormField from index * feat: export types from useTextField index * fix: provide FieldState in useFieldState * feat: export FieldProps interface in useFormField * chore: playground tests * feat: added validateOn prop to text control * refactor: generate uniq id on the control level * refactor: only one composable for form fields * feat: move v-model to the text control input * feat: migrate search field to use text control * fix: avoid mutating the model value directly * feat: expose label property * feat: split select into a control and field composables * feat: introduce FieldBaseProps interface for consistent field properties * feat: split switch into a control and field composables * feat: split slider into a control and field composables * feat: split the number field into a control and field composables * feat: split file field into a control and field composables * refactor: expose the label/description/error element props * fix: types * feat: split time and date fields to control composables * feat: split combox field into control and field composables * refactor: remove redundencies from label and radio groups * refactor: completely deprecate field model sync * fix: model sync * refactor: include field as control prop * feat: split otp to control and field pair * chore: changeset * refactor: all controls inject or create the field * refactor: prop normalization * refactor: remove redudant props * refactor: redudant required prop * feat: exposed ids of all fields * feat: add controlId to date and time control elements * refactor: streamline accessibility props handling in useCustomField * refactor: remove optional chaining for field * feat: expose useCustomControl * refactor: rename and unify control id name * refactor: restructure fields and control types * feat: allow label components to receive for attributes * feat: expose control id on the useFieldController * chore: update prop types * fix: bring back old code * fix: types * chore: register with devtools in `useFormField` (#214) * chore: register with devtools in `useFormField` * refactor: update registerField to accept Ref type and use * feat: introduce BuiltInControlTypes for consistent control type * chore: fix typo * fix: types and type reactivity in devtools --------- Co-authored-by: Abdelrahman Awad <[email protected]> * feat: add `blurred` state (#220) * feat: add `blurred` state * chore: touch fields `onInput` and `onChange` * chore: add ability to blur a field * feat: track blurred fields * test: update blur test to check both touched and blurred states * test: update input test to check touched state and value updates * feat: add clearBlurred method to reset blurred fields * test: add tests for handling blurred state in form * test: enhance field state tests for blurred states * refactor: remove redundant onChange handler in useTextField * feat: add blurred state to form transactions and field management * chore: rebase branch * feat: implement onBlur handler to set blurred state in useSwitchControl * feat: manage blurred state in useSlider * feat: enhance useSelectControl to manage touched and blurred states * feat: update useRadio to manage blurred state in onBlur handler * feat: manage touched and blurred states in fillSlots and onBlur handlers * feat: update useNumberControl to manage blurred state in onBlur handler * feat: update useFileControl to manage touched state in various handlers * feat: manage blurred state in date and time controls * feat: manage touched state in useComboBoxControl * feat: manage blurred state in useCheckbox and useCheckboxGroup * feat: manage blurred state in useCalendarControl * test: fix failing tests and added test cases for touched --------- Co-authored-by: Abdelrahman Awad <[email protected]> * feat: add control element reference to useCustomControl * chore: update changesets --------- Co-authored-by: Eugen Istoc <[email protected]>
1 parent bc88778 commit 9a959c1

File tree

85 files changed

+5504
-5187
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

85 files changed

+5504
-5187
lines changed

.changeset/eleven-keys-notice.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@formwerk/core': minor
3+
---
4+
5+
feat(core): each field composable now has a control composable variant to allow for more granular components

.changeset/sad-mice-travel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@formwerk/core': minor
3+
---
4+
5+
feat(core): added `isBlurred` state to fields and forms

.oxlintrc.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
"@typescript-eslint/triple-slash-reference": "error",
8686
"@typescript-eslint/explicit-function-return-type": "off",
8787
"@typescript-eslint/explicit-module-boundary-types": "off",
88+
"no-wrapper-object-types": "off",
8889
"no-console": [
8990
"error",
9091
{

packages/core/src/a11y/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './useErrorMessage';
22
export * from './useLabel';
3+
export * from './useDescription';
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { computed, MaybeRefOrGetter, shallowRef, toValue } from 'vue';
2+
import { useCaptureProps } from '../utils/common';
3+
import { AriaDescriptionProps } from '../types';
4+
5+
export function createDescriptionProps(inputId: string): AriaDescriptionProps {
6+
return {
7+
id: `${inputId}-d`,
8+
};
9+
}
10+
11+
interface CreateDescribedByInit {
12+
inputId: MaybeRefOrGetter<string>;
13+
description: MaybeRefOrGetter<string | undefined>;
14+
}
15+
16+
export function useDescription({ inputId, description }: CreateDescribedByInit) {
17+
const descriptionRef = shallowRef<HTMLElement>();
18+
const descriptionProps = useCaptureProps(() => createDescriptionProps(toValue(inputId)), descriptionRef);
19+
20+
const describedByProps = computed(() => {
21+
return {
22+
'aria-describedby': descriptionRef.value && toValue(description) ? descriptionProps.value.id : undefined,
23+
};
24+
});
25+
26+
return {
27+
describedByProps,
28+
descriptionProps,
29+
};
30+
}

packages/core/src/a11y/useLabel.spec.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { render, screen } from '@testing-library/vue';
22
import { useLabel } from './useLabel';
3-
import { shallowRef } from 'vue';
3+
import { defineComponent, shallowRef } from 'vue';
44

55
describe('label element', () => {
66
test('should render label with `for` attribute', async () => {
@@ -95,3 +95,38 @@ describe('label target (labelledBy)', () => {
9595
expect(screen.getByTestId('input')?.getAttribute('aria-labelledby')).toBe(`${labelFor}-l`);
9696
});
9797
});
98+
99+
describe('label component', () => {
100+
test('should render label component with `for` attribute', async () => {
101+
const label = 'label';
102+
const labelFor = 'input';
103+
const inputRef = shallowRef<HTMLElement>();
104+
105+
const LabelComp = defineComponent({
106+
props: ['for'],
107+
template: `
108+
<label data-testid="label" :for="$props.for">
109+
<slot />
110+
</label>
111+
`,
112+
});
113+
114+
await render({
115+
components: { LabelComp },
116+
setup: () => {
117+
return {
118+
...useLabel({ label: label, for: labelFor, targetRef: inputRef }),
119+
label,
120+
inputRef,
121+
};
122+
},
123+
template: `
124+
<LabelComp v-bind="labelProps">{{ label }}</LabelComp>
125+
<input data-testid="input" ref="inputRef" />
126+
`,
127+
});
128+
129+
expect(screen.getByTestId('label')).toHaveTextContent(label);
130+
expect(screen.getByTestId('label')?.getAttribute('for')).toBe(labelFor);
131+
});
132+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { ref } from 'vue';
2+
import { FieldState } from '../useFormField';
3+
import { useSyncModel } from './useModelSync';
4+
5+
/**
6+
* A proxy for the model value of a field, if the field is not provided, a local ref is created.
7+
*/
8+
export function useVModelProxy<T = unknown>(field: FieldState<T>) {
9+
const model = field.fieldValue ?? ref<T>();
10+
11+
useSyncModel({
12+
model,
13+
modelName: 'modelValue',
14+
onModelPropUpdated: field.setValue,
15+
});
16+
17+
return {
18+
model,
19+
setModelValue: field.setValue,
20+
};
21+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { FormField } from '../useFormField';
2+
import { StandardSchema } from './forms';
3+
4+
export interface ControlApi {
5+
getControlElement(): HTMLElement | undefined;
6+
getControlId(): string | undefined;
7+
getControlType(): string | undefined;
8+
}
9+
10+
export interface ControlProps<TValue = unknown, TInitialValue = TValue> {
11+
/**
12+
* The name of the field.
13+
*/
14+
name?: string;
15+
16+
/**
17+
* The field to use for the control. Internal usage only.
18+
*/
19+
_field?: FormField<TValue | undefined>;
20+
21+
/**
22+
* Schema for field validation.
23+
*/
24+
schema?: StandardSchema<TValue>;
25+
26+
/**
27+
* The v-model value of the field.
28+
*/
29+
modelValue?: TValue | undefined;
30+
31+
/**
32+
* The initial value of the field.
33+
*/
34+
value?: TInitialValue;
35+
36+
/**
37+
* Whether the field is disabled.
38+
*/
39+
disabled?: boolean;
40+
41+
/**
42+
* Whether the field is required.
43+
*/
44+
required?: boolean;
45+
}
46+
47+
export const BuiltInControlTypes = {
48+
Text: 'Text',
49+
Calendar: 'Calendar',
50+
Date: 'Date',
51+
Time: 'Time',
52+
File: 'File',
53+
Select: 'Select',
54+
Number: 'Number',
55+
OTP: 'OTP',
56+
Slider: 'Slider',
57+
Switch: 'Switch',
58+
Checkbox: 'Checkbox',
59+
CheckboxGroup: 'CheckboxGroup',
60+
RadioGroup: 'RadioGroup',
61+
ComboBox: 'ComboBox',
62+
Hidden: 'Hidden',
63+
Search: 'Search',
64+
Custom: 'Custom',
65+
} as const;

packages/core/src/types/forms.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ export type TouchedSchema<TForm extends FormObject> = Simplify<Schema<TForm, boo
1616

1717
export type DirtySchema<TForm extends FormObject> = Simplify<Schema<TForm, boolean>>;
1818

19+
export type BlurredSchema<TForm extends FormObject> = Simplify<Schema<TForm, boolean>>;
20+
1921
export type DisabledSchema<TForm extends FormObject> = Partial<Record<Path<TForm>, boolean>>;
2022

2123
export type ErrorsSchema<TForm extends FormObject> = Partial<Record<Path<TForm>, string[]>>;

packages/core/src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './common';
22
export * from './paths';
33
export * from './forms';
4+
export * from './controls';

0 commit comments

Comments
 (0)