Config

Plugins are simply functions that help to create a configuration object that is passed into Formatic, so first let's talk about the config.

Almost all of Formatic's behavior is passed in via the config property. If you pass in no config, then Formatic uses it's own default config plugin to create a config for you. To change Formatic's behavior, you simply pass in a config object with different methods.

Passing in no config:

React.render(<Formatic fields={fields} />, document.body);

Is equivalent to this:

const config = Formatic.createConfig();

React.render(<Formatic config={config} fields={fields} />, document.body);

A Simple Plugin Example

Plugins are just functions that help in the creation of a config. Here's a simple plugin that will will use the key instead of the label of a field if the label is not present.

const plugin = () => {
  return {
    fieldLabel: field => {
      if (!field.label) {
        return field.key;
      }
      return field.label;
    },
  };
};

Note that plugin functions receive the config as a parameter, so you can delegate to other methods on the config. Let's "humanize" our key by calling the config.humanize method on the config.

const plugin = config => {
  return {
    fieldLabel: field => {
      if (!field.label) {
        if (field.key) {
          return config.humanize(field.key);
        }
      }
      return field.label;
    },
  };
};

Also note that at the point in time config is passed in, it's had all previous plugins applied. So you can save any existing methods for wrapping. Here, we'll delegate back to the original fieldLabel method.

const plugin = config => {
  // Need to save off existing function so we can delegate to it.
  const fieldLabel = config.fieldLabel;

  return {
    // This will become the new method for the next plugin if there is one or
    // ultimately the method available on the config if nothing overrides it.
    fieldLabel: field => {
      if (!field.label) {
        if (field.key) {
          return config.humanize(field.key);
        }
      }
      // Delegate to the function originally passed in above.
      return fieldLabel(field);
    },
  };
};

Using Plugins

To use a plugin, just pass it in to Formatic.createConfig.

const config = Formatic.createConfig(plugin);

React.render(<Formatic config={config} fields={fields} />, document.body);

You can pass in multiple plugins. If multiple plugins define the same method, the config will get the method from the last plugin. As shown above though, each plugin's method can delegate to an earlier plugin's method.

const config = Formatic.createConfig(pluginA, pluginB, pluginC);

React.render(<Formatic config={config} fields={fields} />, document.body);

Adding Field Types

To add a new field type, you can use the `FieldContainer` component to create the field component, and you point to it with a plugin.

import React from 'react';
import Formatic, { FieldContainer } from 'formatic';

const config = Formatic.createConfig(() => ({
  createElement_Upper: props => (
    <FieldContainer {...props}>
      {({ onChangeValue, onFocus, onBlur }) => (
        <input
          onBlur={onBlur}
          onChange={event => onChangeValue(event.target.value.toUpperCase())}
          onFocus={onFocus}
          value={props.field.value}
        />
      )}
    </FieldContainer>
  ),
}));

const fields = [{ type: 'upper', key: 'name' }];

class App extends React.Component {
  state = { name: '' };

  onChange = newValue => {
    // newValue.name will always be upper-cased
    this.setState(newValue);
  };

  render() {
    return (
      <Formatic
        config={config}
        fields={fields}
        onChange={this.onChange}
        value={this.state}
      />
    );
  }
}

React.render(<App />, document.getElementById('some-element'));

Plugin API

createElement( name, props, children) )

You can override the rendering of every component using the createElement method. This is useful for wrapping components or injecting props into many components based on the names of those components.

createElement_* ( props, children) )

You can override the rendering of a specific component using the createElement_ methods. This way, you can change the props or completely replace that component with a custom component.

See Adding Field Types above for an example.

renderTag( tagName, tagProps, metaProps, children )

The renderTag method allows you to override the rendering of any element of any component. This is useful for adding custom styling to components.

Here's a simple example where we add a custom style attribute.

const plugin = config => {
  const { renderTag } = config;
  return {
    renderTag: (Tag, tagProps, metaProps, ...children) => {
      const { typeName, parentTypeName, elementName } = metaProps;
      // The `typeName` is the name of the component that is rendering this
      // element. `elementName` is the identifier for the specific tag.
      if (typeName === 'Password' && elementName === 'PasswordInput') {
        tagProps = {
          ...tagProps,
          style: { color: 'red' },
        };
      }
      // Nested children of a type have a `parentTypeName`.
      if (parentTypeName === 'Array' && elementName === 'RemoveItem') {
        tagProps = {
          ...tagProps,
          style: { color: 'red' },
        };
      }
      // Pass through other tags to the original renderTag method.
      return renderTag(Tag, tagProps, metaProps, ...children);
    },
  };
};

Ultimately, the default renderTag will callReact.createElement, so you have complete control over rendering. That does mean you have to handle the children properly though. If you render children, and it happens to be an array, then you will likely get a key warning.

Here's an example using Emotion for CSS in various ways and proper handling of children.

import React from 'react';
/** @jsx jsx */
import { css, jsx } from '@emotion/core';
import styled from '@emotion/styled';

const labelCss = css(`
  border: 1px solid black;
`);

const RemoveItemButton = styled.button`
  color: red;
`;

const passwordCss = css(`
  color: red;
`);

// A helper function to render children inside other tags.
const renderChildren = children => {
  if (children.length > 1) {
    // Spread the children so we don't get key warnings.
    return React.createElement(React.Fragment, {}, ...children);
  }
  return children;
};

const plugin = config => {
  const { renderTag } = config;
  return {
    renderTag: (Tag, tagProps, metaProps, ...children) => {
      const { typeName, parentTypeName, elementName } = metaProps;
      if (typeName === 'Fields' && elementName === 'Label') {
        return (
          <Tag css={labelCss} {...tagProps}>
            {renderChildren(children)}
          </Tag>
        );
      }
      if (typeName === 'Password' && elementName === 'PasswordInput') {
        // We know there are no children here, so we can ignore them.
        return <Tag css={passwordCss} {...tagProps} />;
      }
      if (parentTypeName === 'Array' && elementName === 'RemoveItem') {
        // Pass through to original renderTag which will then call
        // React.createElement. That will in turn handle children correctly.
        return renderTag(RemoveItemButton, tagProps, ...children);
      }
      // Pass through other tags to the original renderTag method.
      return renderTag(Tag, tagProps, metaProps, ...children);
    },
  };
};

Lots of other plugin methods can be found in the default config.