@wedgekit/render
TypeScript icon, indicating that this package has built-in type declarations

5.1.5 • Public • Published

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 anyformfield's value by including the field in anupdatedValuesobject 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.

Readme

Keywords

none

Package Sidebar

Install

npm i @wedgekit/render

Weekly Downloads

6

Version

5.1.5

License

MIT

Unpacked Size

111 kB

Total Files

4

Last publish

Collaborators

  • tprettyman
  • rnimrod
  • jquerijero
  • brent-heavican
  • msuiter
  • rerskine
  • timmy2654
  • jfiller
  • mada1113
  • kolson
  • dreadman3716