Skip to content

Commit d3b6e4b

Browse files
Copilottanepiper
andcommitted
TypeScript conversion Phase 1: Core types and foundational modules
Converted 4 foundational files from .mjs to .mts: - Created src/lib/shared/types.mts - Core TypeScript interfaces and types - Converted src/lib/shared/stores.mts - Store creation with proper typing - Converted src/lib/shared/fields.mts - Form element utilities with type guards - Converted src/lib/form/errors.mts - Validation with proper types Key improvements: - Replaced all JSDoc with native TypeScript types - Added proper generics and type constraints - Created FormulaStores, BeakerStores, FieldValidity interfaces - Added type guards for runtime checks - Avoided 'any' type, using 'unknown' with proper narrowing - All functions properly typed with parameters and return types Remaining files to convert (24): - 10 more source .mjs files - 14 test .spec.mjs files - index.mjs root file Next phase will convert remaining form modules (event, init, extract, etc.) Co-authored-by: tanepiper <376930+tanepiper@users.noreply.github.com>
1 parent ce82332 commit d3b6e4b

File tree

4 files changed

+360
-0
lines changed

4 files changed

+360
-0
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import type { FormElement } from '../shared/fields.mjs';
2+
import type { FieldValidity, ValidatorFn, ValidationMessages } from '../shared/types.mjs';
3+
4+
interface ValidationCheckOptions {
5+
messages?: ValidationMessages;
6+
validators?: Record<string, Record<string, ValidatorFn>>;
7+
}
8+
9+
/**
10+
* Extracts validity errors from the element and merges with custom errors.
11+
*/
12+
function extractErrors(el: FormElement, custom?: Record<string, boolean>): Record<string, boolean> {
13+
const output: Record<string, boolean> = {};
14+
for (const key in el.validity) {
15+
if (key !== 'valid' && el.validity[key as keyof ValidityState]) {
16+
output[key] = true;
17+
}
18+
}
19+
return { ...output, ...custom };
20+
}
21+
22+
/**
23+
* Gets the result of any custom validations available on the fields.
24+
*/
25+
function getCustomValidations(
26+
value: unknown,
27+
values: Record<string, unknown>,
28+
validations: Record<string, ValidatorFn> = {}
29+
): [Record<string, string>, Record<string, boolean>] {
30+
const messages: Record<string, string> = {};
31+
const errors: Record<string, boolean> = {};
32+
33+
Object.entries(validations).forEach(([key, validation]) => {
34+
const message = validation(value, values);
35+
if (message !== null) {
36+
messages[key] = message;
37+
errors[key] = true;
38+
}
39+
});
40+
41+
return [messages, errors];
42+
}
43+
44+
/**
45+
* Creates a validation checker for an element group
46+
*/
47+
export function createValidationChecker(
48+
inputGroup: string,
49+
elementGroup: FormElement[],
50+
values: Record<string, unknown>,
51+
options?: ValidationCheckOptions
52+
): (el: FormElement, elValue: unknown) => FieldValidity {
53+
return (el: FormElement, elValue: unknown): FieldValidity => {
54+
// Reset the validity
55+
elementGroup.forEach((groupEl) => {
56+
groupEl.setCustomValidity('');
57+
groupEl.removeAttribute('data-formula-invalid');
58+
});
59+
60+
// If there are no options, just return the current error
61+
if (!options) {
62+
const valid = el.checkValidity();
63+
if (!valid) {
64+
el.setAttribute('data-formula-invalid', 'true');
65+
}
66+
return {
67+
valid,
68+
invalid: !valid,
69+
message: el.validationMessage,
70+
errors: extractErrors(el),
71+
};
72+
}
73+
74+
// Check for any custom messages in the options or dataset
75+
const customMessages: Record<string, string> = {
76+
...options?.messages?.[inputGroup],
77+
...(el.dataset as Record<string, string>),
78+
};
79+
80+
// Check for any custom validations
81+
const [messages, customErrors] = getCustomValidations(
82+
elValue,
83+
values,
84+
options?.validators?.[inputGroup]
85+
);
86+
87+
const errors = extractErrors(el, customErrors);
88+
const errorKeys = Object.keys(errors);
89+
90+
if (el.checkValidity()) {
91+
if (errorKeys.length > 0) {
92+
el.setCustomValidity(messages[errorKeys[0]]);
93+
}
94+
} else {
95+
if (customMessages[errorKeys[0]]) {
96+
el.setCustomValidity(customMessages[errorKeys[0]]);
97+
}
98+
}
99+
100+
// Recheck validity and show any messages
101+
const valid = el.checkValidity();
102+
if (!valid) {
103+
el.setAttribute('data-formula-invalid', 'true');
104+
}
105+
106+
return {
107+
valid,
108+
invalid: !valid,
109+
message: el.validationMessage,
110+
errors,
111+
};
112+
};
113+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* A form element that can be an input, select or text area
3+
*/
4+
export type FormElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
5+
6+
/**
7+
* Type guard to check if an element is a form element
8+
*/
9+
function isFormElement(el: Element): el is FormElement {
10+
return 'checkValidity' in el && typeof (el as HTMLInputElement).checkValidity === 'function';
11+
}
12+
13+
/**
14+
* Extract all fields from the form that are valid inputs with `name` property that are not part of a form group
15+
*/
16+
export function getFormFields(rootEl: HTMLElement): FormElement[] {
17+
const nodeList = rootEl.querySelectorAll('*[name]:not([data-in-group])');
18+
return Array.from(nodeList).filter(isFormElement);
19+
}
20+
21+
/**
22+
* Extract all fields from a group that are valid inputs with `name` property
23+
*/
24+
export function getGroupFields(rootEl: HTMLElement): FormElement[] {
25+
const nodeList = rootEl.querySelectorAll('*[name]');
26+
return Array.from(nodeList).filter(isFormElement);
27+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { atom, map } from 'nanostores';
2+
import type {
3+
FormulaStores,
4+
BeakerStores,
5+
FieldValidity,
6+
EnrichFields,
7+
FormValidatorFn,
8+
} from './types.mjs';
9+
10+
interface FormulaOptions {
11+
defaultValues?: Record<string, unknown>;
12+
enrich?: EnrichFields;
13+
formValidators?: Record<string, FormValidatorFn>;
14+
}
15+
16+
interface BeakerOptions extends Omit<FormulaOptions, 'defaultValues'> {
17+
defaultValues?: Record<string, unknown>[];
18+
}
19+
20+
interface InitialState {
21+
initialValues: Record<string, unknown>;
22+
initialKeys: string[];
23+
initialFieldState: Record<string, boolean>;
24+
initialValidity: Record<string, FieldValidity>;
25+
initialFormValidity: Record<string, string>;
26+
initialEnrichment: Record<string, Record<string, unknown>>;
27+
}
28+
29+
/**
30+
* Generate initial state for specified key set
31+
*/
32+
function generateInitialState<T>(
33+
keys: string[],
34+
initialState: Record<string, unknown>,
35+
stateGenerator: (key: string, initialState: Record<string, unknown>) => T
36+
): Record<string, T> {
37+
return keys.reduce((state, key) => {
38+
return { ...state, [key]: stateGenerator(key, initialState) };
39+
}, {} as Record<string, T>);
40+
}
41+
42+
/**
43+
* Function to create initial state values for the store using any passed default values
44+
*/
45+
function createFirstState(
46+
options?: FormulaOptions,
47+
initialData?: Record<string, unknown>
48+
): InitialState {
49+
const initialValues = { ...options?.defaultValues, ...initialData };
50+
const initialKeys = Object.keys(initialValues);
51+
52+
const initialFieldState = generateInitialState(initialKeys, initialValues, () => false);
53+
const initialValidity = generateInitialState(initialKeys, initialValues, (): FieldValidity => ({
54+
valid: true,
55+
invalid: false,
56+
message: '',
57+
errors: {},
58+
}));
59+
const initialFormValidity = generateInitialState(
60+
Object.keys(options?.formValidators || {}),
61+
initialValues,
62+
() => ''
63+
);
64+
65+
const initialEnrichment = Object.entries(options?.enrich || {}).reduce(
66+
(value, [key, fns]) => {
67+
return {
68+
...value,
69+
[key]: Object.entries(fns).reduce(
70+
(v, [k, fn]) => ({
71+
...v,
72+
[k]: options?.defaultValues?.[key] ? fn(options?.defaultValues?.[key]) : undefined,
73+
}),
74+
{} as Record<string, unknown>
75+
),
76+
};
77+
},
78+
{} as Record<string, Record<string, unknown>>
79+
);
80+
81+
return {
82+
initialValues,
83+
initialKeys,
84+
initialFieldState,
85+
initialValidity,
86+
initialFormValidity,
87+
initialEnrichment,
88+
};
89+
}
90+
91+
/**
92+
* Create the stores for the form instance
93+
*/
94+
export function createFormStores(
95+
options?: FormulaOptions,
96+
initialData?: Record<string, unknown>
97+
): FormulaStores {
98+
const initialStoreState = createFirstState(options, initialData);
99+
return {
100+
formValues: map(initialStoreState.initialValues),
101+
submitValues: map({}),
102+
initialValues: map(initialStoreState.initialValues),
103+
touched: map(initialStoreState.initialFieldState),
104+
dirty: map(initialStoreState.initialFieldState),
105+
errors: map(initialStoreState.initialValidity),
106+
formValidity: map(initialStoreState.initialFormValidity),
107+
formValid: atom(false),
108+
formReady: atom(false),
109+
enrichment: map(initialStoreState.initialEnrichment),
110+
};
111+
}
112+
113+
/**
114+
* Create a group store which contains arrays of form store values
115+
*/
116+
export function createGroupStores(options?: BeakerOptions): BeakerStores {
117+
const defaultValues = options?.defaultValues || [];
118+
const { defaultValues: _, ...restOptions } = options || {};
119+
120+
const eachState = defaultValues.map((defaultValue) =>
121+
createFirstState({ ...restOptions, defaultValues: defaultValue })
122+
);
123+
124+
const combineStates = <K extends keyof InitialState>(property: K): InitialState[K][] =>
125+
eachState.reduce((accumulator, currentState) => {
126+
return [...accumulator, currentState[property]];
127+
}, [] as InitialState[K][]);
128+
129+
const initialValues = combineStates('initialValues');
130+
const initialFieldState = combineStates('initialFieldState');
131+
const initialValidity = combineStates('initialValidity');
132+
const initialEnrichment = combineStates('initialEnrichment');
133+
const initialFormValidity = combineStates('initialFormValidity');
134+
135+
return {
136+
formValues: atom(initialValues),
137+
submitValues: atom([]),
138+
initialValues: atom(initialValues),
139+
touched: atom(initialFieldState),
140+
dirty: atom(initialFieldState),
141+
errors: atom(initialValidity),
142+
formValidity: atom(initialFormValidity),
143+
formValid: atom(false),
144+
formReady: atom(false),
145+
enrichment: atom(initialEnrichment),
146+
};
147+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import type { Atom, MapStore } from 'nanostores';
2+
3+
/**
4+
* A set of stores used by Formula to store the current state
5+
*/
6+
export interface FormulaStores {
7+
formValues: MapStore<Record<string, unknown>>;
8+
submitValues: MapStore<Record<string, unknown>>;
9+
initialValues: MapStore<Record<string, unknown>>;
10+
touched: MapStore<Record<string, boolean>>;
11+
dirty: MapStore<Record<string, boolean>>;
12+
errors: MapStore<Record<string, FieldValidity>>;
13+
formValidity: MapStore<Record<string, string>>;
14+
enrichment: MapStore<Record<string, Record<string, unknown>>>;
15+
formValid: Atom<boolean>;
16+
formReady: Atom<boolean>;
17+
}
18+
19+
/**
20+
* A set of stores used by Beaker to store the current state
21+
*/
22+
export interface BeakerStores {
23+
formValues: Atom<Record<string, unknown>[]>;
24+
submitValues: Atom<Record<string, unknown>[]>;
25+
initialValues: Atom<Record<string, unknown>[]>;
26+
touched: Atom<Record<string, boolean>[]>;
27+
dirty: Atom<Record<string, boolean>[]>;
28+
errors: Atom<Record<string, FieldValidity>[]>;
29+
formValidity: Atom<Record<string, string>[]>;
30+
enrichment: Atom<Record<string, Record<string, unknown>>[]>;
31+
formValid: Atom<boolean>;
32+
formReady: Atom<boolean>;
33+
}
34+
35+
/**
36+
* Validity information for a field
37+
*/
38+
export interface FieldValidity {
39+
valid: boolean;
40+
invalid: boolean;
41+
message: string;
42+
errors: Record<string, string>;
43+
}
44+
45+
/**
46+
* Validation function that returns error message or null
47+
*/
48+
export type ValidatorFn = (value: unknown, values: Record<string, unknown>) => string | null;
49+
50+
/**
51+
* Validation rules for fields
52+
*/
53+
export type ValidationRules = Record<string, Record<string, ValidatorFn>>;
54+
55+
/**
56+
* Enrichment function that transforms a value
57+
*/
58+
export type EnricherFn = (value: unknown) => unknown;
59+
60+
/**
61+
* Enrichment configuration for fields
62+
*/
63+
export type EnrichFields = Record<string, Record<string, EnricherFn>>;
64+
65+
/**
66+
* Form validator function
67+
*/
68+
export type FormValidatorFn = (values: Record<string, unknown>) => string;
69+
70+
/**
71+
* Custom messages for validation errors
72+
*/
73+
export type ValidationMessages = Record<string, Record<string, string>>;

0 commit comments

Comments
 (0)