8000 feat: multistep and tabbed forms by logaretm · Pull Request #173 · formwerkjs/formwerk · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat: multistep and tabbed forms #173

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions packages/core/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const FieldTypePrefixes = {
OTPField: 'otp',
OTPSlot: 'otps',
FileField: 'ff',
FlowSegment: 'fs',
} as const;

export const NOOP = () => {};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ interface ControlButtonProps {
disabled?: boolean;
}

export function useControlButtonProps(props: () => ControlButtonProps) {
export function useControlButtonProps(props: (el: HTMLElement | undefined) => ControlButtonProps) {
const buttonEl = shallowRef<HTMLElement>();

const buttonProps = computed(() => {
const isBtn = isButtonElement(buttonEl.value);
const { disabled, ...rest } = props();
const { disabled, ...rest } = props(buttonEl.value);

return withRefCapture(
{
Expand Down
12 changes: 10 additions & 2 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Core Composables
export * from './useTextField';
export * from './useSearchField';
export * from './useSwitch';
Expand All @@ -19,15 +20,22 @@ export * from './useFileField';
export * from './useCalendar';
export * from './usePicker';
export * from './types';
export * from './config';
export * from './useForm';
export * from './useFormField';
export * from './useFormGroup';
export * from './useFormRepeater';
export * from './useFormFlow';

// Utils
export * from './validation';
export * from './i18n/useLocale';
export { version } from './constants';
export { normalizePath } from './utils/path';

// Config
export * from './config';

// Misc
export { version } from './constants';

// Internals should export types only
export type * from './useSpinButton';
4 changes: 2 additions & 2 deletions packages/core/src/useForm/useFormActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@
unsetPath(output, path, true);
}

const result = await onSuccess(withConsumers(output), { event: e, form: e?.target as HTMLFormElement });
const result = await onSuccess(asConsumableData(output), { event: e, form: e?.target as HTMLFormElement });
isSubmitting.value = false;
wasSubmitted.value = true;

Expand Down Expand Up @@ -296,7 +296,7 @@
}

if (state.touched) {
form.setInitialTouchedPath(path, state.touched as any, opts);

Check warning on line 299 in packages/core/src/useForm/useFormActions.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
}

form.revertValues(path);
Expand Down Expand Up @@ -335,7 +335,7 @@
};
}

function withConsumers<TData extends FormObject>(data: TData): ConsumableData<TData> {
export function asConsumableData<TData extends FormObject>(data: TData): ConsumableData<TData> {
const toObject = () => data;
const toFormData = () => {
const formData = new FormData();
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/useFormFlow/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './useFormFlow';
export * from './useFlowSegment';
14 changes: 14 additions & 0 deletions packages/core/src/useFormFlow/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { InjectionKey, MaybeRefOrGetter } from 'vue';
import { GenericFormSchema } from '../types';

export interface FlowSegmentProps<TSchema extends GenericFormSchema> {
id?: string | number;
schema?: TSchema;
}

export interface FormFlowContext {
isSegmentActive: (segmentId: string) => boolean;
registerSegment: (staticId: string, userSegmentId: MaybeRefOrGetter<string | number | undefined>) => void;
}

export const FormFlowContextKey: InjectionKey<FormFlowContext> = Symbol('FormFlowContext');
107 changes: 107 additions & 0 deletions packages/core/src/useFormFlow/useFlowSegment.ts
9E81
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { computed, defineComponent, h, inject, provide, ref } from 'vue';
import { FormFlowContextKey, FlowSegmentProps } from './types';
import { useUniqId, withRefCapture } from '../utils/common';
import { FieldTypePrefixes } from '../constants';
import { useValidationProvider } from '../validation/useValidationProvider';
import { FormKey } from '../useForm';
import { GenericFormSchema } from '../types';
import { FormGroupContext, FormGroupKey } from '../useFormGroup';

export function useFlowSegment<TSchema extends GenericFormSchema>(props: FlowSegmentProps<TSchema>) {
const element = ref<HTMLElement>();
const id = useUniqId(FieldTypePrefixes.FlowSegment);
const formFlow = inject(FormFlowContextKey, null);
if (!formFlow) {
throw new Error('FormFlowSegment must be used within a FormFlow, did you forget to call `useFormFlow`?');
}

formFlow.registerSegment(id, () => props.id);
const form = inject(FormKey, null);

const { validate, onValidationDispatch, defineValidationRequest, onValidationDone, dispatchValidateDone } =
useValidationProvider({
getValues: () => form?.getValues(),
schema: props.schema,
type: 'GROUP',
});

const requestValidation = defineValidationRequest(async res => {
// Clears Errors in that path before proceeding.
form?.clearErrors();
for (const entry of res.errors) {
form?.setErrors(entry.path, entry.messages);
}

dispatchValidateDone();
});

const ctx: FormGroupContext = {
onValidationDispatch,
onValidationDone,
requestValidation,
getValidationMode: () => (props.schema ? 'schema' : 'aggregate'),
isHtmlValidationDisabled: () => false,
};

const isActive = computed(() => formFlow.isSegmentActive(id));

const segmentProps = computed(() => {
return withRefCapture(
{
'data-form-segment-id': id,
'data-form-segment-user-id': props.id,
'data-active': isActive.value ? 'true' : undefined,
},
element,
);
});

// Whenever the form is validated, only validate if the step is active.
form?.onValidationDispatch(enqueue => {
if (!isActive.value) {
return;
}

enqueue(
validate().then(result => {
return {
...result,
errors: result.errors,
};
}),
);
});

// When the form is done validating, the form group should also signal the same to its children.
form?.onValidationDone(dispatchValidateDone);

// Form steps act as a form group, but they offer no path prefixing.
provide(FormGroupKey, ctx);

return {
/**
* Reference to the step element.
*/
segmentElement: element,

/**
* Props for the step element.
*/
segmentProps,

/**
* Whether the segment is active.
*/
isActive,
};
}

export const FormFlowSegment = /*#__PURE__*/ defineComponent({
name: 'FormFlowSegment',
props: ['as', 'schema', 'id'],
setup(props, { attrs, slots }) {
const { segmentProps, isActive } = useFlowSegment(props);

return () => h(props.as || 'div', { ...attrs, ...segmentProps.value }, isActive.value ? slots.default?.() : []);
},
});
Loading
Loading
0