FormSchema Native
Vue component form based on JSON Schema and Native HTML
Table of Contents
Install
npm install --save @formschema/native
Demo
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) {
}
},
components: { FormSchema }
}
</script>
Features
Supported Keywords
- type is only supported string value. Array type definition is not supported.
- enum is used to render multiple choices input
- maximum, exclusiveMaximum, minimum and exclusiveMinimum are used to render numeric fields
- multipleOf is used to render the input attribute
step
- maxLength, minLength, pattern and const are used to render string fields
- items, additionalItems, maxItems, minItems and uniqueItems are used to render array fields
- dependencies is used to implement Property dependencies and Schema dependencies
- contentEncoding
- contentMediaType
- When
contentMediaType
is equal to text/*
, the HTML element <textarea/>
is used for rendering - When
contentMediaType
is not equal to text/*
, the HTML element <input/>
with attributes { type: file, accept: contentMediaType }
is used for rendering
- title is used to render the input label
- description is used to render the input description
- default is used to define the default input value
- readOnly is used to render a field as an read-only input
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 validationfield.id: string The input ID attribute valuefield.name: string The input name attribute valuefield.value: any The input value for validationfield.schema: JsonSchema The JSON Schema object of the inputfield.required: boolean Boolean indicating whether or not the field is mandatoryfield.hasChildren: boolean Boolean indicating whether or not the field has childrenfield.initialValue: any The initial input valuefield.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
|
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) => {
this.schema = schema;
});
}
}
See json-schema-ref-parser's documentation page
for more details.
Workind with allOf
keyword
To load a JSON Schema containing allOf
keyword, you need to install an
additional dependency json-schema-merge-allof
to help FormSchema to work properly:
import mergeAllOf from 'json-schema-merge-allof';
import FormSchema from '@formschema/native';
import schemaWithAllof from './schema/with-allof.json';
export default {
components: { FormSchema },
data: () => ({
schema: mergeAllOf(schemaWithAllof)
})
}
See json-schema-merge-allof's 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;
reset(): void;
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)) {
}
},
validator(field) {
field.clearMessages(true);
if (!this.validate(field.value)) {
this.validate.errors.forEach(({ dataPath, message }) => {
const errorField = field.hasChildren
? field.getField(dataPath) || field
: field;
errorField.addMessage(message, 3);
});
return Promise.resolve(false);
}
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: {
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:
import { NativeComponents } from '@formschema/native';
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';
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>
VueMaterial Example
<template>
<FormSchema v-model="model" :schema="schema" :components="components"/>
</template>
<script>
import FormSchema, { NativeComponents } from '@formschema/native'
import createComponents from '@formschema/native/elements/VueMaterial'
export default {
data: () => ({
schema: { },
components: createComponents(NativeComponents),
model: {}
}),
components: { FormSchema }
}
</script>
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;
component?: Component;
attrs?: {
[attr: string]: unknown;
};
props?: {
[prop: string]: unknown;
};
}
interface ScalarDescriptor extends DescriptorDefinition<ScalarKind> {
}
interface ObjectGroupDescriptor extends DescriptorDefinition {
properties: string[];
}
interface ObjectDescriptor extends DescriptorDefinition {
layout?: Component;
properties?: {
[schemaProperty: string]: DescriptorInstance;
};
order?: string[];
groups?: {
[groupId: string]: ObjectGroupDescriptor;
};
}
interface ItemsDescriptor<TKind extends ItemKind> extends DescriptorDefinition<TKind> {
items?: {
[itemValue: string]: ScalarDescriptor;
};
}
interface EnumDescriptor extends ItemsDescriptor<'enum'> {
layout?: Component;
}
interface ListDescriptor extends ItemsDescriptor<'list'> {
}
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;
interface ArrayDescriptor extends DescriptorDefinition {
layout?: Component;
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.