@wedgekit/render
v5.1.5
Published
## Purpose
Downloads
188
Maintainers
Keywords
Readme
Wedgekit Render
Purpose
Wedgekit Render allows the quick creation of forms which are design system compliant. Wedgekit Render conforms to the latest Metadata Spec
Conventions
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
Basic Implementation
import Render from '@wedgekit/render';
const ExamplePage = () => <Render settings={settings} onSubmit={callback} />;
❗ NOTE: In order to make the render-engine display properly, the consuming code SHOULD wrap the render component in a container with position: relative
On Breaking Changes
There will come times when @wedgekit/render
will introduce a breaking change in order to keep in line with the metadata specification. However, it is the commitment of the WedgeKit maintainers that metadata can be migrated to be compliant with the latest spec. To that end, WedgeKit Render will not publish a breaking change until such time as a corresponding migration script has been added to @wedgekit/migrations.
Props
settings
Required
A valid metadata object. See metadata spec for details.
onSubmit
Required
A callback function to be fired when the submit
button is clicked. The callback receives all field values and the form api, then returns a tuple containing any field errors and any field value updates.
(fieldValues: { fieldName: value }) => [{ fieldName: errorString }, { fieldName: updatedValue }];
An onSubmit callback MUST return a tuple of two objects i.e. errors
and updatedValues
.
An onSubmit callback can add errors to any form
field by including the field in the errors
object.
An onSubmit callback can change any form
fields value by including the field in the updatedValues
object.
Example
const onSubmit = (fieldValues) => {
const { hasSidekick, sidekickName } = fieldValues;
const errors = {
sidekickName:
sidekickName && sidekickName.length < 2
? 'Sidekick name must be at least 2 characters'
: undefined,
};
const updatedValues = {
hasSidekick: !!sidekickName,
};
return [errors, updatedValues];
};
<Render settings={settings} onSubmit={onSubmit} />;
Async Example
const onSubmit = (formValues) =>
new Promise((resolve, reject) => {
if (submitIsValidAndSuccessful) {
resolve([{}, {}]);
} else if (submitIsValidAndSuccessfulAndNeedsToUpdateSomething) {
resolve([{}, updatedValues]);
} else if (submitIsInvalid) {
resolve([errors, {}]);
} else if (submitIsInvalidANDNeedsToUpdateSomething) {
resolve([errors, updatedVales]);
} else if (theRequestBlewUp) {
reject(Error);
}
});
actionCallbacks
An object mapping action.button
or action.icon
fields to callback functions. Each action button/icon in the metadata SHOULD have a corresponding entry in actionCallbacks
that will be executed when the button/icon is clicked. Each actionCallback
will receive all field values, the [form state, and the original event that triggered the action and returns any updated field values.
{
[fieldName]: (fieldValues: [values], state: [formState], e: SyntheticEvent) =>
void |
{[fieldName]: updatedValue} |
Promise<void | {[fieldName]: updatedValue}>
}
An actionCallback SHOULD return an updatedValues
object.
An actionCallback can change any form
field's value by including the field in the updatedValues
object.
Example
const actionCallbacks = {
"backButton": (formValues, formState, ) => {
if (!!formState.dirty) {
handleBackButton(e);
}
return {}
}
"formValuesAction": () => {
return {updatedThroughActionButton: true}
}
}
crossReferenceFields
An object mapping advanced.xref
fields to react components. Each advanced.xref
SHOULD have a corresponding entry in crossReferenceFields
that will be referenced when populating render engine.
{ [fieldName]: ({formState, setFormValue}) => ReactComponent}
A cross referenced component can view the form's state by referencing props.formState
.
A cross referenced component can change anyform
field's value by including the field in anupdatedValues
object passed to props.setFormValues
.
const crossRefComponent = ({ formState, setFormValue }) => {
setFormValue({ [fieldName]: updatedValue });
return <Component />;
};
❗❗ IMPORTANT ❗❗
To prevent useless rerenders whenever any field value changes, a cross referenced component SHOULD be memoized and have only necessary field values as dependencies.
Example
const FormStateComponent = (props) =>
props.formState.dirty ? <DirtyComponent /> : <CleanComponent />;
const SetFieldValuesComponent = (props) => (
<Button
onClick={() => {
props.setFieldValues({ updatedThroughXRef: true });
}}
/>
);
const compareDependentFields = (watchedField) => (prevProps, nextProps) => {
return prevProps.formState.values[watchedField] === nextProps.formState.values[watchedField]
}
const crossReferenceFields = {
formStateField: React.memo(FormStateComponent, compareDependentFields("watchedField1")),
setFieldValuesField: React.memo(SetFieldValuesComponent, compareDependentFields("watchedField2")))
};
❗ ❗ IMPORTANT ❗❗
Defining a functional component inline, especially in the render function, may have unintended consequences for renders and state management. Since a new function is created on every render, React is unable to properly compare the new function to the old function, thus causing a re-render every time. To avoid this, it is recommended that all state management and dependent code be handled outside of the render method. Consider the example below:
{
'exampleComponent': () => (<ExampleComponent value={someValueDerivedFromFetch} />)
}
In this situation, any change in state, especially from someValueDerivedFromFetch
, would cause the entire render method to run again. Consequently, since React could not equate the old inline function to the new, exampleComponent
would be unmounted and remounted. Alternatively you could define a component ExampleComponentWrapper
:
const ExampleComponentWrapper = () => {
const [value, setValue] = useState({});
useEffect(() => {
fetch('https://example.com/api/v1/people.json')
.then((d) => d.json())
.then((d) => setValue(d));
}, []);
return <ExampleComponent value={value} />;
};
And reference the wrapper component in crossReferenceFields
.
{
'exampleComponent': ExampleComponentWrapper
}
externalOptions
A mapping of form
fields to arrays of metadata field options. External options MAY be applied to form.switch-group
, form.multiselect
, form.select
, form.switchiepoo
, and form.list-order
fields.
{
[fieldName]: OptionsArray | () => OptionsArray | async () => OptionsArray>
}
OptionsArray
Array{label: string, id: string}
externalValues
A mapping of form
fields to updated field values.
{ [fieldName]: updatedValue }
formStateObserver
A callback function that will be called when the form state changes. A formStateObserver
callback will receive the form state.
(formState: FormState) => void
locators
A mapping of advanced.locator
fields to locator contexts. Every advanced.locator
field in the metadata MUST have a corresponding locator context in locators. A locator context MUST contain the following top-level members:
| Name | Description |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| component | The React component for the locator modal. In most cases this will be the locator from @wedgekit/locators
|
| fields | A map of form
fields to GraphQL fields returned from a locator query. Fields present will be updated with the corresponding GraphQL value when the locator query is succesful |
| autoSearch | A function which builds the autoSearch value which the locator will use when opening. It takes the value of the searchField
as it's single argument. |
| locatorTypes | An array of Locator Query names. These are provided by back end devs. |
| searchField | The meta-data form
field which should be passed to the autoSearch |
| title | The sentence cased label on the button to open the locator. |
Example:
import Locator from '@wedgekit/locator';
{
"locatorButton": {
component: Locator,
fields: {
dependentMetadataField: 'graphQLFieldName',
},
autoSearch: (value: string) => ({
searchPattern: value,
fieldName: `backendFieldName`,
dataType: `CHARACTER`,
searchBy: `Backend Field Label`
searchUsing: `Backend search method`
}),
locatorTypes: ['Locator Query Name'],
searchField: 'metadataReferencedField',
title: 'Locator Button'
}
}
longSubmit
A boolean which indicates whether the submit callback is expected to take a considerable amount of time. If true, the user will be shown a modal on top of the form while it submits. This is only relevant if the onSubmit function is async.
scrollRef
If used inside of a fixed height container, the ref of the containing element SHOULD be passed to render engine in the scrollRef
prop. This specifies which container to scroll when a form field needs to be brought into view. Default is the window object.
submittingHeader
A string to configure the header of the submitting modal on a long submit. This will only work if longSubmit is true and the onSubmit callback is async.
submittingMessage
A string which will be displayed to the user when the form is being submitted. This will only work if the onSubmit function is async.
submittingTimer
A number indicating the number of seconds the onSubmit callback is expected to take. It will determine the starting point of the countdown timer on the submitting modal. This will only work if longSubmit is true and the onSubmit callback is async.
validators
Validators are functions which are used to validate a field value. They should be used only for this purpose and no other purpose (Matt is making me write this).
The validator prop is a mapping of form
fields to validation functions or a tuple containing a validation function and an array of dependent fields. Each validation function receives the field value, name, and the form state as arguments, and returns undefined
for successful or a string for an error.
{ fieldName: | [ValidationFunction, [string]] }
Validation Function
(value, name, data) => ?string | Promise
Validation functions may be synchronous or async. Async functions should only be used if the field value needs to be checked against backend data. Validators will not run on form that is clean or clean since the last submit.
The validation function should return undefined if the value is valid, or an error message if the value is invalid. If the validation function is async, the promise should be resolved with either undefined or the error message. Rejections SHOULD be used only when the validation function failed to complete in some way.
Example
{
underFifty: (value: number, name: string, formState) =>
value > 50 ? 'This number can not exceed 50' : undefined;
}
❗ NOTE: In certain very special cases you can apply for a special dispensation to change form values in the course of validation. This is one of the render engine's most sacred rites and it not to be undertaken lightly. If your cause is deemed just, you may return a tuple containing the validation (undefined or an error) in the first position and a map of the changed values in the second.
{
branchCounty: (value: number, name: string, formState) =>
value = "Saunders" ? ['There is no Branch in this county', branchState: "Nebraksa"] : undefined;
}
❗ ❗IMPORTANT ❗❗
Field level validators SHOULD be pure-ish functions. These are functions whose result will be consistent when passed the same value. If this cannot be reasonably guaranteed, the validation SHOULD be moved to the onSubmit function.
✅ A validator to ensure a date does not fall on a weekend. The same date would always produce the same result.
✅ A validator to ensure a branch ID is valid. Technically this would require a backend call and someone could change the branch on the backend in between validations but this is unlikely in the context of our customers' day to day operations.
✅ A validator to ensure a date is after another date on the form. Because both values are passed to the validation function this will work as intended.
❌ A validator to ensure that a time falls within the next 20 minutes. If the user ran it once and walked away for 30 minutes, the same time would have a different result.
❌ A validator to make sure a resource (for example a truck) is free for use. Someone could very well reserve that resource before the user submits the form.
Dependent Fields
The array of dependent fields consists of fields which should be (re-)validated when this field changes.