@formschema/native
    TypeScript icon, indicating that this package has built-in type declarations

    2.0.0-beta.6 • Public • Published

    FormSchema Native

    Vue component form based on JSON Schema and Native HTML

    npm Build status Test coverage

    Table of Contents

    Install

    npm install --save @formschema/native

    Demo

    formschema-demo-elementui

    Usage

    <template>
      <FormSchema :schema="schema" v-model="model" @submit.prevent="submit">
        <button type="submit">Subscribe</button>
      </FormSchema>
    </template>
    
    <script>
      import FormSchema from '@formschema/native'
      import schema from './schema/newsletter-subscription.json'
    
      export default {
        data: () => ({
          schema: schema,
          model: {}
        }),
        methods: {
          submit (e) {
            // this.model contains the valid data according your JSON Schema.
            // You can submit your model to the server here
          }
        },
        components: { FormSchema }
      }
    </script>

    Features

    Supported Keywords

    Irrelevant (ignored) Keywords

    Since FormSchema is just a form generator, some JSON Schema validation keywords are irrelevant:

    FormSchema API

    Props

    Name Type Description Default
    schema required Object The input JSON Schema object.
    v-model any Use this directive to create two-way data bindings with the component. It automatically picks the correct way to update the element based on the input type. undefined
    id String The id property of the Element interface represents the form's identifier, reflecting the id global attribute. Random unique ID
    name String The name of the form. It must be unique among the forms in a document. undefined
    bracketed-object-input-name Boolean When set to true (default), checkbox inputs and nested object inputs will * automatically include brackets at the end of their names (e.g. name="grouped-checkbox-fields[]"). Setting this property to false, disables this behaviour. true
    search Boolean Use this prop to enable search landmark role to identify a section of the page used to search the page, site, or collection of sites. false
    disabled Boolean Indicates whether the form elements are disabled or not. false
    components ComponentsLib Use this prop to overwrite the default Native HTML Elements with custom components. GLOBAL.Elements
    descriptor DescriptorInstance UI Schema Descriptor to use for rendering. {}
    validator Function The validator function to use to validate data before to emit the input event.
    Syntax
    function validator(field: GenericField): Promise<boolean>
    Parameters
    • field: GenericField The field that requests validation
    • field.id: string The input ID attribute value
    • field.name: string The input name attribute value
    • field.value: any The input value for validation
    • field.schema: JsonSchema The JSON Schema object of the input
    • field.required: boolean Boolean indicating whether or not the field is mandatory
    • field.hasChildren: boolean Boolean indicating whether or not the field has children
    • field.initialValue: any The initial input value
    • field.messages: Message[] The input value for validation
    Return value
    A promise that return true if validation success and false otherwise
    null

    Events

    Name Description
    input Fired synchronously when the value of an element is changed.
    Arguments
    • value: any

    Methods

    form()

    Get the HTML form object reference.

    Example

    <template>
      <FormSchema ref="formSchema" :schema="schema"/>
    </template>
    
    <script>
      import FormSchema from '@formschema/native'
    
      export default {
        components: { FormSchema },
        data: () => ({
          schema: { type: 'string' }
        }),
        mounted() {
          console.log(this.$refs.formSchema.form())
        }
      };
    </script>

    Syntax

    form(): HTMLFormElement | VNode | undefined

    Return value

    An HTMLFormElement object or a VNode object describing the form element object, or undefined for input JSON schema object.

    Working with Async Schema

    <template>
      <FormSchema :schema="schema"/>
    </template>
    
    <script>
      import axios from 'axios'
      import FormSchema from '@formschema/native'
    
      export default {
        components: { FormSchema },
        data: () => ({
          schema: {}
        }),
        created() {
          axios.get('/api/schema/subscription.json').then(({ data: schema }) => {
            this.schema = schema
          });
        }
      };
    </script>

    Working with Vue Router

    Load an async schema on the beforeRouterEnter hook:

    <template>
      <FormSchema :schema="schema"/>
    </template>
    
    <script>
      import axios from 'axios'
      import FormSchema from '@formschema/native'
    
      export default {
        components: { FormSchema },
        data: () => ({
          schema: {}
        }),
        beforeRouterEnter(from, to, next) {
          axios.get('/api/schema/subscription.json')
            .then(({ data: schema }) => next((vm) => vm.setSchema(schema)))
            .catch(next);
        },
        methods: {
          setSchema(schema) {
            this.schema = schema;
          }
        }
      };
    </script>

    Workind with JSON Schema $ref Pointers

    To load a JSON Schema with $ref pointers, you need to install an additional dependency to resolve them:

    import $RefParser from 'json-schema-ref-parser';
    import FormSchema from '@formschema/native';
    import schemaWithPointers from './schema/with-pointers.json';
    
    export default {
      components: { FormSchema },
      data: () => ({
        schema: {}
      }),
      created () {
        $RefParser.dereference(schemaWithPointers)
          .then((schema) => {
            // `schema` is the resolved schema that contains your entire JSON
            // Schema, including referenced files, combined into a single object
            this.schema = schema;
          });
      }
    }

    See json-schema-ref-parser documentation page for more details.

    Data Validation

    Native HTML5 Validation

    By default, FormSchema uses basic HTML5 validation by applying validation attributes on inputs. This is enough for simple schema, but you will need to dedicated JSON Schema validator if you want to validate complex schema.

    Custom Validation API

    For custom validation, you need to provide a validation function prop.

    Bellow the custom validation API:

    type MessageInfo = 0;
    type MessageSuccess = 1;
    type MessageWarining = 2;
    type MessageError = 3;
    type MessageType = MessageInfo | MessageSuccess | MessageWarining | MessageError;
    
    interface Message {
      type?: MessageType;
      text: string;
    }
    
    interface GenericField<TModel = any> {
      readonly id: string;
      readonly key: string;
      readonly name: string;
      readonly isRoot: boolean;
      readonly schema: JsonSchema;
      readonly required: boolean;
      readonly hasChildren: boolean;
      readonly initialValue: TModel;
      readonly value: TModel;
      readonly messages: Required<Message>[];
      clear(): void; // clear field
      reset(): void; // reset initial field value
      addMessage(message: string, type: MessageType = MessageError): void;
      clearMessages(recursive: boolean = false): void;
    }

    Custom Validation with AJV

    Bellow a basic example with the popular AJV validator:

    <template>
      <FormSchema v-model="model" v-bind="{ schema, validator }" @submit.prevent="onSubmit">
        <button type="submit">Submit</button>
      </FormSchema>
    </template>
    
    <script>
      import Ajv from 'ajv';
      import FormSchema from '@formschema/native';
    
      export default {
        data: () => ({
          schema: {
            type: 'object',
            properties: {
              username: {
                type: 'string',
                minLength: 5
              },
              password: {
                type: 'string',
                minLength: 6
              }
            },
            required: ['username', 'password']
          },
          model: {},
          ajv: new Ajv({ allErrors: true })
        }),
        computed: {
          validate() {
            return this.ajv.compile(this.schema);
          }
        },
        methods: {
          onSubmit({ field }) {
            if (field && this.validator(field)) {
              // validation success, submit code here
            }
          },
          validator(field) {
            // Clear all messages
            field.clearMessages(true);
    
            if (!this.validate(field.value)) {
              this.validate.errors.forEach(({ dataPath, message }) => {
                const errorField = field.hasChildren
                  ? field.getField(dataPath) || field
                  : field;
    
                /**
                 * Add a message to `errorField`.
                 * The first argument is the message string
                 * and the second one the message type:
                 *    0 - Message Info
                 *    1 - Message Success
                 *    2 - Message Warning
                 *    3 - Message Error
                 */
                errorField.addMessage(message, 3);
              });
    
              // Return `false` to cancel the `input` event
              return Promise.resolve(false);
            }
    
            // Return `true` to trigger the `input` event
            return Promise.resolve(true);
          }
        },
        components: { FormSchema }
      };
    </script>

    Disable Native HTML5 Validation

    Since FormSchema use the native HTML Form element, attributes novalidate and formvalidate can be used to disable form validation as it described in the W3C specification:

    If present, they indicate that the form is not to be validated during submission.
    The no-validate state of an element is true if the element is a submit button and the element's formnovalidate attribute is present, or if the element's form owner's novalidate attribute is present, and false otherwise.

    Example: Disable Form Validation using novalidate

    <template>
      <FormSchema v-model="model" :schema="schema" novalidate>
        <button type="submit">Submit</button>
      </FormSchema>
    </template>

    Usecase: Implement Save, Cancel and Submit

    Disable the form validation constraints could be useful when implementing a save feature to the form:

    • The user should be able to save their progress even though they haven't fully entered the data in the form
    • The user should be able to cancel the saved form data
    • The user should be able to submit form data with validation
    <template>
      <FormSchema v-model="model" :schema="schema" action="/api/blog/post" method="post">
        <input type="submit" name="submit" value="Submit">
        <input type="submit" formnovalidate name="save" value="Save">
        <input type="submit" formnovalidate name="cancel" value="Cancel">
      </FormSchema>
    </template>

    Labels Translation

    The simple way to translate labels without to change the JSON Schema file is to use a descriptor.

    Here an example with Vue I18n:

    <template>
      <FormSchema v-model="model" :schema="schema" :descriptor="descriptor"/>
    </template>
    
    <script>
      import FormSchema from '@formschema/native';
    
      export default {
        data: () => ({
          schema: {
            type: 'object',
            properties: {
              firstname: {
                type: 'string'
              },
              lastname: {
                type: 'string'
              }
            }
          },
          model: {}
        }),
        computed: {
          descriptor() {
            properties: {
              firstname: {
                label: this.$t('firstname.label'),
                helper: this.$t('firstname.helper')
              },
              lastname: {
                label: this.$t('lastname.label'),
                helper: this.$t('lastname.helper')
              }
            }
          }
        },
        // `i18n` option, setup locale info for component
        // see https://kazupon.github.io/vue-i18n/guide/component.html
        i18n: {
          messages: {
            en: {
              firstname: {
                label: 'First Name',
                helper: 'Your First Name'
              },
              lastname: {
                label: 'Last Name',
                helper: 'Your Last Name'
              }
            },
            fr: {
              firstname: {
                label: 'Prénom',
                helper: 'Votre prénom'
              },
              lastname: {
                label: 'Nom',
                helper: 'Votre nom'
              }
            }
          }
        },
        components: { FormSchema }
      };
    </script>

    Render Form Elements

    Textarea

    Add a text/* media types to a string schema to render a Textarea element.

    Example schema.json

    {
      "type": "string",
      "contentMediaType": "text/plain"
    }

    You can also use a descriptor to force the Render to use a Textarea element:

    Example descriptor.json

    {
      "kind": "textarea"
    }

    File Input

    String schemas with media types not starting with text/* are automatically render as Input File elements.

    Example schema.json

    {
      "type": "string",
      "contentMediaType": "image/png"
    }

    There is a list of MIME types officially registered by the IANA, but the set of types supported will be application and operating system dependent. Mozilla Developer Network also maintains a shorter list of MIME types that are important for the web.

    Hidden Input

    Schemas with descriptor's kind hidden are render as hidden input elements.

    Example schema.json

    {
      "type": "string"
    }

    Example descriptor.json

    {
      "kind": "hidden"
    }

    Password Input

    String schemas with a descriptor's kind password are used to render Input Password elements.

    Example schema.json

    {
      "type": "string"
    }

    Example descriptor.json

    {
      "kind": "password"
    }

    Multiple Checkbox

    To define multiple checkbox, use the JSON Schema keyword enum and uniqueItems:

    Example schema.json

    {
      "type": "array",
      "uniqueItems": true,
      "enum": {
        "type": "string",
        "enum": [
          "daily",
          "promotion"
        ]
      }
    }

    Example descriptor.json

    {
      "items": {
        "daily": {
          "label": "Receive daily updates"
        },
        "promotion": {
          "label": "Receive promotion emails"
        }
      }
    }

    Grouped Radio

    To group radio elements, use the JSON Schema keyword enum with a enum descriptor:

    Example schema.json

    {
      "type": "string",
      "enum": [
        "monday",
        "tuesday",
        "wednesday",
        "thursday",
        "friday",
        "saturday",
        "sunday"
      ]
    }

    Example descriptor.json

    {
      "kind": "enum",
      "items": {
        "monday": { "label": "Monday" },
        "tuesday": { "label": "Tuesday" },
        "wednesday": { "label": "Wednesday" },
        "thursday": { "label": "Thursday" },
        "friday": { "label": "Friday" },
        "saturday": { "label": "Saturday" },
        "sunday": { "label": "Sunday" }
      }
    }

    Select Input

    To group HTML Select element, use the JSON Schema keyword enum with a list descriptor:

    Example schema.json

    {
      "type": "string",
      "enum": [
        "monday",
        "tuesday",
        "wednesday",
        "thruday",
        "friday",
        "saturday",
        "sunday"
      ]
    }

    Example descriptor.json

    {
      "kind": "list",
      "items": {
        "monday": { "label": "Monday" },
        "tuesday": { "label": "Tuesday" },
        "wednesday": { "label": "Wednesday" },
        "thursday": { "label": "Thursday" },
        "friday": { "label": "Friday" },
        "saturday": { "label": "Saturday" },
        "sunday": { "label": "Sunday" }
      }
    }

    Array Input

    To render a array field, define your schema like:

    Example schema.json

    {
      "type": "array",
      "items": {
        "type": "string"
      }
    }

    FormSchema will render a text input by adding a button to add more inputs.

    Regex Input

    To render a regex input, define your schema like:

    Example schema.json

    {
      "type": "string",
      "pattern": "[a-e]+"
    }

    Fieldset Element

    FormSchema use a <fieldset> element to group inputs of a object JSON Schema:

    Example schema.json

    {
      "type": "object",
      "properties": {
        "firstname": {
          "type": "string"
        },
        "lastname": {
          "type": "string"
        }
      },
      "required": ["firstname"]
    }

    Use descriptor to set labels and helpers. You can also change the order of properties for the rendering:

    Example descriptor.json

    {
      "properties": {
        "firstname": {
          "label": "First Name",
          "helper": "Your first name"
        },
        "lastname": {
          "label": "Last Name",
          "helper": "Your last name"
        }
      },
      "order": ["lastname", "firstname"]
    }

    Custom Form Elements

    Elements API

    type SchemaType = 'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean' | 'null';
    type ScalarKind = 'string' | 'password' | 'number' | 'integer' | 'null' | 'boolean' | 'hidden' | 'textarea' | 'image' | 'file' | 'radio' | 'checkbox';
    type ItemKind = 'enum' | 'list';
    type FieldKind = SchemaType | ScalarKind | ItemKind;
    type ComponentsType = 'form' | 'message' | 'button' | 'helper' | FieldKind;
    type Component = string | VueComponent | VueAsyncComponent;
    
    interface IComponents {
      set(kind: ComponentsType, component: Component): void;
      get(kind: ComponentsType, fallbackComponent?: Component): Component;
    }

    Custom Elements Example

    To define custom elements, you need to use the NativeComponents class and the components prop:

    // MyCustomComponents.js
    
    // First, import the base class Components from `@formschema/native` package
    import { NativeComponents } from '@formschema/native';
    
    // Then declare your custom components as functional components
    import { InputElement } from '@/components/InputElement';
    import { StateElement } from '@/components/StateElement';
    import { ArrayElement } from '@/components/ArrayElement';
    import { FieldsetElement } from '@/components/FieldsetElement';
    import { ListElement } from '@/components/ListElement';
    import { TextareaElement } from '@/components/TextareaElement';
    import { MessageElement } from '@/components/Message';
    
    // Finaly, extend the NativeComponents class and define override it
    export class MyCustomComponents extends NativeComponents {
      constructor() {
        super();
    
        this.set('array', ArrayElement);
        this.set('boolean', StateElement);
        this.set('string', InputElement);
        this.set('password', InputElement);
        this.set('file', InputElement);
        this.set('image', InputElement);
        this.set('radio', StateElement);
        this.set('checkbox', StateElement);
        this.set('enum', FieldsetElement);
        this.set('number', InputElement);
        this.set('integer', InputElement);
        this.set('object', FieldsetElement);
        this.set('list', ListElement);
        this.set('textarea', TextareaElement);
        this.set('message', MessageElement);
      }
    }

    See the file NativeComponents.ts for an example.

    <template>
      <FormSchema v-model="model" :schema="schema" :components="components"/>
    </template>
    
    <script>
      import FormSchema from '@formschema/native'
      import { MyCustomComponents } from './MyCustomComponents'
    
      export default {
        data: () => ({
          schema: { /* ... */ },
          components: new MyCustomComponents(),
          model: {}
        }),
        components: { FormSchema }
      }
    </script>

    ElementUI Example

    Descriptor Interface

    type SchemaType = 'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean' | 'null';
    type ParserKind = SchemaType | 'enum' | 'list' | 'textarea' | 'image' | 'file' | 'password';
    type ScalarKind = 'string' | 'password' | 'number' | 'integer' | 'null' | 'boolean' | 'hidden' | 'textarea' | 'image' | 'file' | 'radio' | 'checkbox';
    type ItemKind = 'enum' | 'list';
    type FieldKind = SchemaType | ScalarKind | ItemKind;
    type ComponentsType = 'form' | 'message' | 'button' | 'helper' | FieldKind;
    type Component = string | VueComponent | AsyncVueComponent;
    
    type SetDescriptor = EnumDescriptor | ArrayDescriptor | ObjectDescriptor;
    type Descriptor = ScalarDescriptor | SetDescriptor | ListDescriptor;
    type DescriptorConstructor = (field: Field) => Descriptor;
    type DescriptorInstance = Descriptor | DescriptorConstructor;
    
    interface DescriptorDefinition<TKind extends FieldKind = FieldKind> {
      kind?: TKind;
      label?: string;
      helper?: string;
      visible?: boolean; // by default true. If false, component will be ignored on rendering
      component?: Component;
      attrs?: {
        [attr: string]: unknown;
      };
      props?: {
        [prop: string]: unknown;
      };
    }
    
    /**
     * Describe scalar types like: string, password, number, integer,
     * boolean, null, hidden field, textarea element, image and file
     * inputs, radio and checkbox elements
     */
    interface ScalarDescriptor extends DescriptorDefinition<ScalarKind> {
    }
    
    /**
     * Use to describe grouped object properties
     */
    interface ObjectGroupDescriptor extends DescriptorDefinition {
      properties: string[];
    }
    
    /**
     * Describe JSON Schema with type `object`
     */
    interface ObjectDescriptor extends DescriptorDefinition {
      layout?: Component; // default: 'fieldset'
      properties?: {
        [schemaProperty: string]: DescriptorInstance;
      };
      order?: string[];
      groups?: {
        [groupId: string]: ObjectGroupDescriptor;
      };
    }
    
    /**
     * Describe JSON Schema with key `enum`
     */
    interface ItemsDescriptor<TKind extends ItemKind> extends DescriptorDefinition<TKind> {
      items?: {
        [itemValue: string]: ScalarDescriptor;
      };
    }
    
    /**
     * Describe HTML Radio Elements
     */
    interface EnumDescriptor extends ItemsDescriptor<'enum'> {
      layout?: Component; // default: 'fieldset'
    }
    
    /**
     * Describe HTML Select Element
     */
    interface ListDescriptor extends ItemsDescriptor<'list'> {
    }
    
    /**
     * Describe buttons for array schema
     */
    interface ButtonDescriptor<T extends string, A extends Function> extends Partial<ActionButton<A>> {
      type: T;
      label: string;
      tooltip?: string;
      visible?: boolean;
      component?: Component;
    }
    
    type ActionPushTrigger = () => void;
    
    type PushButtonDescriptor = ButtonDescriptor<'push', ActionPushTrigger>;
    type MoveUpButtonDescriptor = ButtonDescriptor<'moveUp', ActionPushTrigger>;
    type MoveDownButtonDescriptor = ButtonDescriptor<'moveDown', ActionPushTrigger>;
    type DeleteButtonDescriptor = ButtonDescriptor<'delete', ActionPushTrigger>;
    type UnknownButtonDescriptor = ButtonDescriptor<string, ActionPushTrigger>;
    
    type ArrayItemButton = MoveUpButtonDescriptor
      | MoveDownButtonDescriptor
      | DeleteButtonDescriptor
      | UnknownButtonDescriptor;
    
    /**
     * Describe JSON Schema with type `array`
     */
    interface ArrayDescriptor extends DescriptorDefinition {
      layout?: Component; // default: 'fieldset'
      items?: DescriptorInstance[] | DescriptorInstance;
      pushButton: PushButtonDescriptor | null;
      buttons: ArrayItemButton[];
    }

    Usecases

    Grouping fields

    FormSchema can help you organize your form fields by grouping them. Please check ObjectDescriptor definition.

    Example schema.json

    {
      "type": "object",
      "properties": {
        "lastname": {
          "type": "string"
        },
        "firstname": {
          "type": "string"
        },
        "city": {
          "type": "string"
        }
      }
    }

    Example descriptor.json

    {
      "groups": {
        "name": {
          "label": "Your name",
          "properties": ["firstname", "lastname"]
        },
        "location": {
          "label": "Your location",
          "properties": ["city"]
        }
      }
    }
    Result without grouping
    <form id="id-form">
      <fieldset id="id-form-field">
        <div data-fs-kind="string" data-fs-type="text" data-fs-field="lastname"><label for="id-form-field-lastname"></label>
          <div data-fs-input="text"><input id="id-form-field-lastname" type="text" name="lastname"></div>
        </div>
        <div data-fs-kind="string" data-fs-type="text" data-fs-field="firstname"><label for="id-form-field-firstname"></label>
          <div data-fs-input="text"><input id="id-form-field-firstname" type="text" name="firstname"></div>
        </div>
        <div data-fs-kind="string" data-fs-type="text" data-fs-field="city"><label for="id-form-field-city"></label>
          <div data-fs-input="text"><input id="id-form-field-city" type="text" name="city"></div>
        </div>
      </fieldset>
    </form>
    Result with grouping
    <form id="id-form">
      <fieldset id="id-form-field">
        <div data-fs-group="name">
          <div data-fs-group-label="name">Your name</div>
          <div data-fs-group-nodes="2">
            <div data-fs-kind="string" data-fs-type="text" data-fs-field="firstname"><label for="id-form-field-firstname"></label>
              <div data-fs-input="text"><input id="id-form-field-firstname" type="text" name="firstname"></div>
            </div>
            <div data-fs-kind="string" data-fs-type="text" data-fs-field="lastname"><label for="id-form-field-lastname"></label>
              <div data-fs-input="text"><input id="id-form-field-lastname" type="text" name="lastname"></div>
            </div>
          </div>
        </div>
        <div data-fs-group="location">
          <div data-fs-group-label="location">Your location</div>
          <div data-fs-group-nodes="1">
            <div data-fs-kind="string" data-fs-type="text" data-fs-field="city"><label for="id-form-field-city"></label>
              <div data-fs-input="text"><input id="id-form-field-city" type="text" name="city"></div>
            </div>
          </div>
        </div>
      </fieldset>
    </form>

    Contributing

    Please see contributing guide.

    License

    Under the MIT license. See LICENSE file for more details.

    Install

    npm i @formschema/native

    DownloadsWeekly Downloads

    959

    Version

    2.0.0-beta.6

    License

    MIT

    Unpacked Size

    1.17 MB

    Total Files

    19

    Last publish

    Collaborators

    • demsking