Skip to content

Commit

Permalink
refactor: cleanup and simplify data structure
Browse files Browse the repository at this point in the history
  • Loading branch information
edmundhung committed Nov 12, 2023
1 parent 68b6447 commit 78c9ce5
Show file tree
Hide file tree
Showing 12 changed files with 126 additions and 132 deletions.
81 changes: 37 additions & 44 deletions packages/conform-dom/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
formatPaths,
getFormData,
getPaths,
isSubpath,
isPrefix,
isPlainObject,
setValue,
} from './formdata';
Expand Down Expand Up @@ -61,27 +61,23 @@ export type Constraint = {
pattern?: string;
};

export type FormMetadata = {
defaultValue: Record<string, unknown>;
constraint: Record<string, Constraint>;
};

export type FormState = {
key: Record<string, string>;
validated: Record<string, boolean>;
valid: Record<string, boolean>;
dirty: Record<string, boolean>;
};

export interface FormContext {
metadata: FormMetadata;
export type FormContext = {
defaultValue: Record<string, unknown>;
initialValue: Record<string, unknown>;
value: Record<string, unknown>;
error: Record<string, string[]>;
constraint: Record<string, Constraint>;
state: FormState;
}
};

export interface FormOptions<Type> {
export type FormOptions<Type> = {
defaultValue?: DefaultValue<Type>;
constraint?: Record<string, Constraint>;
lastResult?: SubmissionResult;
Expand All @@ -92,12 +88,12 @@ export interface FormOptions<Type> {
submitter: HTMLInputElement | HTMLButtonElement | null;
formData: FormData;
}) => Submission<Type>;
}
};

export type SubscriptionSubject = {
[key in
| 'error'
| 'defaultValue'
| 'initialValue'
| 'value'
| 'key'
| 'validated'
Expand All @@ -106,11 +102,11 @@ export type SubscriptionSubject = {
};

export type SubscriptionScope = {
parent?: string[];
prefix?: string[];
name?: string[];
};

export interface Form<Type extends Record<string, unknown> = any> {
export type Form<Type extends Record<string, unknown> = any> = {
id: string;
submit(event: SubmitEvent): {
formData: FormData;
Expand All @@ -130,7 +126,7 @@ export interface Form<Type extends Record<string, unknown> = any> {
): () => void;
getContext(): FormContext;
getSerializedState(): string;
}
};

export const VALIDATION_UNDEFINED = '__undefined__';

Expand All @@ -154,30 +150,33 @@ export function createForm<Type extends Record<string, unknown> = any>(
}

function initializeFormContext(): FormContext {
const metadata: FormMetadata = initializeMetadata(options);
const defaultValue = flatten(options.defaultValue);
const value = options.lastResult?.initialValue
? flatten(options.lastResult.initialValue)
: metadata.defaultValue;
: defaultValue;
const error = options.lastResult?.error ?? {};

return {
metadata,
constraint: options.constraint ?? {},
defaultValue,
initialValue: value,
value,
error,
state: {
validated: options.lastResult?.state?.validated ?? {},
key: createKeyProxy(
options.lastResult?.state?.key ?? getDefaultKey(metadata),
options.lastResult?.state?.key ?? getDefaultKey(defaultValue),
),
valid: createValidProxy(error),
dirty: createDirtyProxy(metadata.defaultValue, value),
dirty: createDirtyProxy(defaultValue, value),
},
};
}

function getDefaultKey(metadata: FormMetadata): Record<string, string> {
return Object.entries(metadata.defaultValue).reduce<Record<string, string>>(
function getDefaultKey(
defaultValue: Record<string, unknown>,
): Record<string, string> {
return Object.entries(defaultValue).reduce<Record<string, string>>(
(result, [key, value]) => {
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
Expand Down Expand Up @@ -250,13 +249,6 @@ export function createForm<Type extends Record<string, unknown> = any>(
);
}

function initializeMetadata(options: FormOptions<Type>): FormMetadata {
return {
defaultValue: flatten(options.defaultValue),
constraint: options.constraint ?? {},
};
}

function shouldNotify<Type>(config: {
prev: Record<string, Type>;
next: Record<string, Type>;
Expand All @@ -265,10 +257,10 @@ export function createForm<Type extends Record<string, unknown> = any>(
scope?: SubscriptionScope;
}): boolean {
if (config.scope) {
const parents = config.scope.parent ?? [];
const prefixes = config.scope.prefix ?? [];
const names = config.scope.name ?? [];
const list =
parents.length === 0
prefixes.length === 0
? names
: Array.from(
new Set([
Expand All @@ -279,9 +271,9 @@ export function createForm<Type extends Record<string, unknown> = any>(

for (const name of list) {
if (
parents.length === 0 ||
prefixes.length === 0 ||
names.includes(name) ||
parents.some((parent) => isSubpath(name, parent))
prefixes.some((prefix) => isPrefix(name, prefix))
) {
config.cache[name] ??= config.compareFn(
config.prev[name],
Expand All @@ -302,7 +294,7 @@ export function createForm<Type extends Record<string, unknown> = any>(
const diff: Record<keyof SubscriptionSubject, Record<string, boolean>> = {
value: {},
error: {},
defaultValue: {},
initialValue: {},
key: {},
validated: {},
valid: {},
Expand Down Expand Up @@ -331,8 +323,8 @@ export function createForm<Type extends Record<string, unknown> = any>(
next: next.initialValue,
compareFn: (prev, next) =>
JSON.stringify(prev) !== JSON.stringify(next),
cache: diff.defaultValue,
scope: subject.defaultValue,
cache: diff.initialValue,
scope: subject.initialValue,
}) ||
shouldNotify({
prev: prev.state.key,
Expand Down Expand Up @@ -510,7 +502,7 @@ export function createForm<Type extends Record<string, unknown> = any>(
value,
state: {
...context.state,
dirty: createDirtyProxy(context.metadata.defaultValue, value),
dirty: createDirtyProxy(context.defaultValue, value),
},
});
} else {
Expand Down Expand Up @@ -543,18 +535,19 @@ export function createForm<Type extends Record<string, unknown> = any>(
return;
}

const metadata = initializeMetadata(latestOptions);
const defaultValue = flatten(latestOptions.defaultValue);

updateContext({
metadata,
initialValue: metadata.defaultValue,
value: metadata.defaultValue,
constraint: latestOptions.constraint ?? {},
defaultValue,
initialValue: defaultValue,
value: defaultValue,
error: {},
state: {
validated: {},
key: createKeyProxy(getDefaultKey(metadata)),
key: createKeyProxy(getDefaultKey(defaultValue)),
valid: createValidProxy({}),
dirty: createDirtyProxy(metadata.defaultValue, metadata.defaultValue),
dirty: createDirtyProxy({}, {}),
},
});
}
Expand Down Expand Up @@ -622,7 +615,7 @@ export function createForm<Type extends Record<string, unknown> = any>(
validated: result.state?.validated ?? {},
key,
valid: createValidProxy(error),
dirty: createDirtyProxy(context.metadata.defaultValue, value),
dirty: createDirtyProxy(context.defaultValue, value),
},
});

Expand Down
10 changes: 5 additions & 5 deletions packages/conform-dom/formdata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,15 @@ export function formatPaths(paths: Array<string | number>): string {
}

/**
* Check if a name is a subpath of a parent name
* Check if a name match the prefix paths
*/
export function isSubpath(name: string, parent: string) {
export function isPrefix(name: string, prefix: string) {
const paths = getPaths(name);
const parentPaths = getPaths(parent);
const prefixPaths = getPaths(prefix);

return (
paths.length >= parentPaths.length &&
parentPaths.every((path, index) => paths[index] === path)
paths.length >= prefixPaths.length &&
prefixPaths.every((path, index) => paths[index] === path)
);
}

Expand Down
3 changes: 1 addition & 2 deletions packages/conform-dom/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ export {
type UnionKeyof,
type UnionKeyType,
type Constraint,
type FormMetadata,
type FormState,
type FieldName,
type DefaultValue,
Expand All @@ -27,4 +26,4 @@ export {
requestIntent,
parse,
} from './submission';
export { getPaths, formatPaths, isSubpath } from './formdata';
export { getPaths, formatPaths, isPrefix } from './formdata';
4 changes: 2 additions & 2 deletions packages/conform-dom/submission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ export type SubmissionState = {
validated: Record<string, boolean>;
};

export interface SubmissionContext<Value> {
export type SubmissionContext<Value> = {
intent: string | null;
initialValue: Record<string, unknown>;
value: Value | null;
error: Record<string, string[]>;
state: SubmissionState;
}
};

export type Submission<Output> =
| {
Expand Down
33 changes: 16 additions & 17 deletions packages/conform-react/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
type SubscriptionSubject,
formatPaths,
getPaths,
isSubpath,
isPrefix,
STATE,
} from '@conform-to/dom';
import {
Expand All @@ -29,10 +29,10 @@ export type BaseMetadata<Type> = {
id: string;
errorId: string;
descriptionId: string;
defaultValue: DefaultValue<Type>;
initialValue: DefaultValue<Type>;
value: DefaultValue<Type>;
error: string[] | undefined;
allError: Record<string, string[]>;
errors: string[] | undefined;
allErrors: Record<string, string[]>;
allValid: boolean;
valid: boolean;
dirty: boolean;
Expand Down Expand Up @@ -142,7 +142,6 @@ export function getBaseMetadata<Type>(
): BaseMetadata<Type> {
const name = options.name ?? '';
const id = name ? `${formId}-${name}` : formId;
const error = context.error[name];
const updateSubject = (
subject: keyof SubscriptionSubject,
scope: keyof SubscriptionScope,
Expand All @@ -160,8 +159,9 @@ export function getBaseMetadata<Type>(
id,
errorId: `${id}-error`,
descriptionId: `${id}-description`,
defaultValue: context.initialValue[name] as DefaultValue<Type>,
initialValue: context.initialValue[name] as DefaultValue<Type>,
value: context.value[name] as DefaultValue<Type>,
errors: context.error[name],
get key() {
return context.state.key[name];
},
Expand All @@ -179,46 +179,45 @@ export function getBaseMetadata<Type>(
}

for (const key of Object.keys(context.error)) {
if (isSubpath(key, name) && !context.state.valid[key]) {
if (isPrefix(key, name) && !context.state.valid[key]) {
return false;
}
}

return true;
},
get allError() {
get allErrors() {
if (name === '') {
return context.error;
}

const result: Record<string, string[]> = {};

for (const [key, errors] of Object.entries(context.error)) {
if (isSubpath(key, name)) {
if (isPrefix(key, name)) {
result[key] = errors;
}
}

return result;
},
error,
},
{
get(target, key, receiver) {
switch (key) {
case 'key':
case 'error':
case 'defaultValue':
case 'errors':
case 'initialValue':
case 'value':
case 'valid':
case 'dirty':
updateSubject(key, 'name');
updateSubject(key === 'errors' ? 'error' : key, 'name');
break;
case 'allError':
updateSubject('error', 'parent');
case 'allErrors':
updateSubject('error', 'prefix');
break;
case 'allValid':
updateSubject('valid', 'parent');
updateSubject('valid', 'prefix');
break;
}

Expand Down Expand Up @@ -254,7 +253,7 @@ export function getFieldMetadata<Type>(
case 'name':
return name;
case 'constraint':
return context.metadata.constraint[name];
return context.constraint[name];
}

return Reflect.get(target, key, receiver);
Expand Down
Loading

0 comments on commit 78c9ce5

Please sign in to comment.