Skip to content

Commit

Permalink
A very complex issue that involved validation validating with icorrec…
Browse files Browse the repository at this point in the history
…t form values in unique cases see changelog
  • Loading branch information
joepuzzo committed May 9, 2024
1 parent 09e36fb commit 5883406
Show file tree
Hide file tree
Showing 19 changed files with 824 additions and 94 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 4.60.0 ( May 9th, 2024)

### Fixed

- A very complex issue that involved validation validating with icorrect form values in unique cases where a field "a" would depend on the value of field "b" and field "b" comes after field "a". This issue is so hard to simply describe here that instead I made two docs pages to talk about them with examples haha see [here](https://teslamotors.github.io/informed/examples/after-relevance-bug)

## 4.59.1 ( May 7th, 2024)

### Added
Expand Down
8 changes: 4 additions & 4 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export type MultistepState = {
export type ArrayFieldState = {
fields: any[];
name: string;
}
};

export type ArrayFieldApi = {
add: (amount?: number) => void;
Expand Down Expand Up @@ -403,7 +403,7 @@ export function useConditional(params: {
native: boolean;
}): unknown;

export function useArrayField(params: {
export function useArrayField(params: {
name: string;
initialValue: any;
defaultValue: any;
Expand All @@ -429,8 +429,8 @@ declare function FormFields({
onlyValidateSchema
}: {
/** Json Schema */
schema: any,
onlyValidateSchema: boolean
schema: any;
onlyValidateSchema?: boolean;
}): JSX.Element[];

declare function ArrayField({
Expand Down
41 changes: 37 additions & 4 deletions src/FormController.js
Original file line number Diff line number Diff line change
Expand Up @@ -926,8 +926,8 @@ export class FormController {
}

deregister(name) {
debug('De-Register', name);
if (this.fieldsMap.get(name)) {
debug('De-Register', name);
this.fieldsMap.delete(name);
this.emit('field', name);
}
Expand Down Expand Up @@ -1234,7 +1234,9 @@ export class FormController {
}

reset(options = {}) {
debug('Resetting Form');
debug(
'----------------------------- Resetting Form -----------------------------'
);

// There are cases where we dont want to blow away all the form values
if (this.options.current.resetOnlyOnscreen) {
Expand Down Expand Up @@ -1272,11 +1274,42 @@ export class FormController {
memory: {}
};

// Two itterations, one for array fields then a second one for non array fields
// Why? an array field reset will blow away any fields under it anyways :) so why waste time calling reset

// STEP1: Array field reset
this.fieldsMap.forEach(fieldMeta => {
if (fieldMeta.current.arrayField) {
debug(`Resetting the array field, ${fieldMeta.current.name}`);
fieldMeta.current.fieldApi.reset({ resetValue: resetValues });
}
});

// STEP2: Reset all the other fields
this.fieldsMap.forEach(fieldMeta => {
debug(`Resetting the field, ${fieldMeta.current.name}`);
fieldMeta.current.fieldApi.reset({ resetValue: resetValues });
if (!fieldMeta.current.arrayField) {
debug(`Resetting the field, ${fieldMeta.current.name}`);
fieldMeta.current.fieldApi.reset({ resetValue: resetValues });
}
});

// Need to peform validation at the end where all values have now been reset
// This fixed an issue, so now validateOnMount is consistant with first render
this.fieldsMap.forEach(fieldMeta => {
if (fieldMeta.current.validateOnMount) {
debug(
`Re-validating the field, ${
fieldMeta.current.name
} due to a reset and validateOnMount`
);
fieldMeta.current.fieldApi.validate();
}
});

debug(
'----------------------------- END Resetting Form -----------------------------'
);

this.emit('reset');
}

Expand Down
46 changes: 34 additions & 12 deletions src/hooks/useArrayField.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export const useArrayField = ({
// Gook onto the form api
const formApi = useFormApi();

// For knowing if we are performing reset
const resetRef = useRef(false);

// Map will store all fields by name
// Key => name
// Val => fieldMetaRef
Expand Down Expand Up @@ -149,23 +152,24 @@ export const useArrayField = ({

const reset = () => {
// First wipe the existing state
// Array fields are unique.. because reset will create new keys everything below gets wiped
// So, we can start by simply wiping out the state below here ( same thing we do at form level reset )
// ^^ By this I mean in form level reset we first wipe the form state :) so we can do same thing here!
// Array fields are unique.. because reset will create new keys every field will get unmounted
// So, we can start by simply wiping out the keys below here ( same thing we do at form level reset )
// this will result in all fields performing their cleanup rutines
logger(`------------ ${name} Array Field Reset Start ------------`);
// Performing reset so we set the flag
resetRef.current = true;
// Remove array field
formController.remove(name);
// When resetting we reset to the users initial value not the one tracked by this hook
// Build new initial values
const initVals =
initialValueRef.current ||
formController.getInitialValue(name) ||
defaultValueRef.current ||
[];

// Set our initial values back to what the user set at beginning
setInitialValues(initVals);
// Build a new set of keys because everything is new !!!
const resetKeys = initVals ? initVals.map(() => uuidv4()) : [];
// Finally set that shit !
setKeys(resetKeys);
// Clear out keys ( we wait until all fields have deregistered before resetting )
setKeys([]);
};

const clear = () => {
Expand Down Expand Up @@ -247,9 +251,27 @@ export const useArrayField = ({
fieldsMap.set(n, m);
formController.register(n, m);
},
deregister: (n, m) => {
deregister: n => {
formController.deregister(n);
// Remove from our map
fieldsMap.delete(n);
formController.deregister(n, m);
// On last deregister we finally complete
// console.log(`${name}-WTF1`, fieldsMap.size);
// console.log(`${name}-WTF2`, resetRef.current);
// NOTE: I originally tried to put the below logic inside of remove
// However this cuases issues because deregister is called with the correct name where remove may have old name
// Example [ 0, 1, 2 ] if we remove 1 then 2 moves to 1s place
if (!fieldsMap.size && resetRef.current) {
// V important we flag that we are done performing reset as all fields have deregistered
resetRef.current = false;
// For debug logging we show when complete
logger(`------------ ${name} Array Field Reset End ------------`);
const initVals = getInitialValues();
// Build a new set of keys because everything is new !!!
const resetKeys = initVals ? initVals.map(() => uuidv4()) : [];
// Finally set that shit !
setKeys(resetKeys);
}
},
getInitialValue: fieldName => {
// If we are getting initial value and its for this field return that
Expand All @@ -269,7 +291,7 @@ export const useArrayField = ({
if (modifiedFieldName === name) {
const path = fieldName.replace(name, '');
const v = ObjectMap.get(getInitialValues(), path);
logger(`Resetting ${path} to ${v}`);
logger(`Getting initial value for ${path} which is ${v}`);
return v;
}
return formController.getInitialValue(fieldName);
Expand Down
10 changes: 5 additions & 5 deletions src/hooks/useField.js
Original file line number Diff line number Diff line change
Expand Up @@ -361,11 +361,6 @@ export const useField = ({
// [userInitialValue, defaultValue]
// );

useFieldSubscription('field-value', validateWhen, target => {
logger(`revalidating for ${metaRef.current.name} because of ${target}`);
formController.validateField(metaRef.current.name);
});

useFieldSubscription(
'field-value',
[name],
Expand All @@ -377,6 +372,11 @@ export const useField = ({
false // No scope as we are already scoped
);

useFieldSubscription('field-value', validateWhen, target => {
logger(`revalidating for ${metaRef.current.name} because of ${target}`);
formController.validateField(metaRef.current.name);
});

useUpdateEffect(() => {
logger(`revalidating for ${metaRef.current.name} because of deps change`);
formController.validateField(metaRef.current.name);
Expand Down
10 changes: 10 additions & 0 deletions src/hooks/useFieldSubscription.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,21 @@ export const useFieldSubscription = (
};

formController.emitter.on(event, listener);
if (scopedFields.length) {
debug(
`Adding subscription on event ${event}, subscribing to events from ${scopedFields}`
);
}

// When name changes we always force an update!
// forceUpdate();

return () => {
if (scopedFields.length) {
debug(
`Removing subscription on event ${event}, un-subscribing to events from ${scopedFields}`
);
}
formController.emitter.removeListener(event, listener);
};
},
Expand Down
4 changes: 3 additions & 1 deletion vitedocs/Info.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ export const Info = ({ children, type = 'informative' }) => {
<div className="info-box">
<Well>
<Flex gap="size-100">
<InfoOutline size="S" color={type} />
<span>
<InfoOutline size="S" color={type} />
</span>
<div className="info">{children}</div>
</Flex>
</Well>
Expand Down
3 changes: 3 additions & 0 deletions vitedocs/Nav/ApiReferenceNav.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ export const ApiReferenceNav = () => {
<NavLink href="/api-reference/arrays-array-field-clear" exact>
ArrayField Clear
</NavLink>
<NavLink href="/api-reference/arrays-nested-array-field" exact>
Nesetd ArrayField
</NavLink>
<h3>Schema</h3>
<NavLink href="/api-reference/schema-intro" exact>
Intro
Expand Down
11 changes: 8 additions & 3 deletions vitedocs/Nav/ExamplesNav.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export const ExamplesNav = () => {
<h3>Arrays</h3>
<NavLink href="/examples/array-field">Array Field</NavLink>
<NavLink href="/examples/array-field-unique">Unique Validation</NavLink>
{/* <NavLink href="/examples/huge-array-field">Huge Array Field</NavLink> */}
<h3>Validation</h3>
<NavLink href="/examples/async-validation">Async Validation</NavLink>
<NavLink href="/examples/paired-validation">Paired Validation</NavLink>
Expand Down Expand Up @@ -70,11 +69,17 @@ export const ExamplesNav = () => {
</NavLink>
<h3>Hidden</h3>
<NavLink href="/examples/hidden-field">Hidden Field</NavLink>
{/* <h3>Huge Forms</h3> */}
{/* <NavLink href="/examples/huge-form">Huge Form</NavLink> */}
<h3>Huge Forms</h3>
<NavLink href="/examples/huge-form">Huge Form</NavLink>
<NavLink href="/examples/huge-array-field">Huge Array Field</NavLink>
<h3>Cool</h3>
<NavLink href="/examples/elon-musk">Elon Musk</NavLink>
<NavLink href="/examples/new-product">New Product</NavLink>
<h3>Fixed Bugs</h3>
<NavLink href="/examples/after-relevance-bug">After Render Bug</NavLink>
<NavLink href="/examples/after-relevance-bug-array">
After Render Bug in Array
</NavLink>
</ul>
);
};
107 changes: 107 additions & 0 deletions vitedocs/Pages/ApiReference/Arrays/NestedArrrayField/Example.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { Debug, ArrayField, useFormApi } from 'informed';
import { useRef } from 'react';
import { Form, Button, Input } from 'YourComponents';

const initialValues = {
friends: [
{
name: 'Matt',
age: '28',
siblings: [{ name: 'Justin ' }, { name: 'Nicole' }]
},
{
name: 'Hope',
age: '23',
siblings: [{ name: 'Paige ' }, { name: 'Maren' }]
}
]
};

const Siblings = () => {
return (
<ArrayField name="siblings">
{({ add }) => {
return (
<>
<Button
onClick={add}
type="button"
variant="accent"
style="outline">
Add Sibling
</Button>
<ArrayField.Items>
{({ remove, name }) => (
<>
<h4>{name}</h4>
<Input name="name" label="Name" required />
<Button type="button" onClick={remove} variant="negative">
Remove Sibling
</Button>
</>
)}
</ArrayField.Items>
</>
);
}}
</ArrayField>
);
};

const ResetButton = () => {
const formApi = useFormApi();
return (
<Button
type="button"
onClick={() => {
formApi.reset();
}}>
Reset Form
</Button>
);
};

const Example = () => {
const arrayFieldApiRef = useRef();

return (
<Form initialValues={initialValues}>
<ResetButton />
<br />
<ArrayField name="friends" arrayFieldApiRef={arrayFieldApiRef}>
{({ add }) => {
return (
<>
<Button
onClick={add}
type="button"
variant="accent"
style="outline">
Add Friend
</Button>
<ArrayField.Items>
{({ remove, name }) => (
<>
<h4>{name}</h4>
<Input name="name" label="Name" required />
<Input name="age" label="Age" type="number" />
<div style={{ padding: '10px', paddingLeft: '30px' }}>
<Siblings />
</div>
<Button type="button" onClick={remove} variant="negative">
Remove Friend
</Button>
</>
)}
</ArrayField.Items>
</>
);
}}
</ArrayField>
<Button type="submit">submit</Button>
<Debug values modified dirty />
</Form>
);
};

export default Example;
Loading

0 comments on commit 5883406

Please sign in to comment.