Giter Club home page Giter Club logo

json-schema-form's Introduction

A Lightweight Angular JSON Schema Form Component

Quality Gate Status npm version

Goal

Features

  • Supports JSON Schema Draft 6
  • Can load referenced schemas from URLs
  • Renders compact forms
  • Supports 2-way databinding
  • Autocomplete & typeahead based on REST services (complex responses can be processed via extended JSONata)
  • CSS styling
  • Built-in validation
  • Flexible layout options (tab, table, vertical, horizontal, ...)
  • Several input widgets (file upload, date / color picker, autocomplete, ...)
  • Lightweight: < 1000 lines of code

Installation

To use the library in your project, follow these steps:

npm i @dashjoin/json-schema-form
npm i @angular/material
npm i jsonata

In your app module add:

import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { JsonSchemaFormModule } from '@dashjoin/json-schema-form';
...

@NgModule({
  ...
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    JsonSchemaFormModule,
    ...
  ],
  ...
}

Note: You need import CommonModule for nested lazy loading modules

import { CommonModule } from '@angular/common';
import { JsonSchemaFormModule } from '@dashjoin/json-schema-form';

@NgModule({
  ...
  imports: [ 
    CommonModule, 
    JsonSchemaFormModule, 
    ...
  ],
  ...
}

A small sample component:

import { Component } from '@angular/core';
import { State } from '@dashjoin/json-schema-form';
import { FormArray } from '@angular/forms';

@Component({
  selector: 'app-root',
  template: `
    <lib-json-schema-form [state]="state"></lib-json-schema-form>
  `
})
export class AppComponent {

  state: State = {
    schema: {
      type: 'array',
      items: {
        type: 'object',
        properties: {
          name: { type: 'string' },
          bday: { type: 'string', widget: 'date' }
        }
      }
    },
    value: any = [{
      name: 'Joe',
      bday: '2018-09-09T22:00:00.000Z'
    }];
    name: 'myform',

    // pick FormArray, FormGroup or FormControl for arrays, objects, or single values respectively
    control: new FormArray([])
  };

  foo() {
    // subscribe to form value change / validation or state events
    this.state.control.valueChanges.subscribe(res => {
      console.log(res);
    })
  }
}

Finally, add the material style and icons to styles.css:

@import "~@angular/material/prebuilt-themes/indigo-pink.css";
@import "https://fonts.googleapis.com/icon?family=Material+Icons";

JSON Schema Extensions

We define a couple of extensions to JSON Schema in order to define the user interface and layout of the form. Please also see the demo playground where examples of all configuration options are available.

Widget

This option specifies a specific input widget to be used. The default is a simple text field. The following options are available:

{
  "type": "string",
  "widget": "date"
}
  • select: shows a select input field with options (No free text entry is possible. Options can be loaded via rest (see below))
  • upload: the JSON property is set to the contents of an uploaded file
  • date: uses the material date picker component
  • textarea: displays a multi line textarea
  • password: input is shown as *****
  • color: shows a color picker widget
  • datetime-local, email, month, tel, time, url, week: uses the browser native input types

Custom Widgets

It is possible to create custom widgets using the following steps:

  • Create a component that extends BaseComponent. All relevant data such as the applicable subschema and the current value are passed to the component. Make sure to emit value changes via state.control. An example can be found here
  • Include the component in your @NgModule declarations
  • In the parent component, add this service to your constructor: private service: JsonSchemaFormService
  • Register your widget in ngOnInit() using this service: this.service.registerComponent('rich-text-editor', CustomComponent);
  • Include the widget in your schema: { "widget": "custom", "widgetType": "rich-text-editor" }

Autocomplete choices

The following fields control how select and autocomplete options are obtained from a REST backend:

{
  "type": "string",
  "choicesUrl": "/assets/autocomplete-simple.json",
  "choicesVerb": "GET"
}
  • choices: string array that allows defining the choices statically
  • choicesUrl: defines the REST service URL
  • choicesVerb: defines the HTTP verb to use for the REST service URL, default is POST
  • choicesUrlArgs: defines the REST service parameter. The convention is to have a single parameter. Multiple fields need to be wrapped into a single object
  • jsonata: used to transform the REST result into a string array or an array of objects with name and value fields if it is not already in that form. The transformation is expressed using JSONata
  • choicesLoad: determines whether the choices are loaded upon page load (onLoad) or upon focus (onFocus), which is the default

Autocomplete and Select Display Names and Values

If you want the option's control value (what is saved in the form) to be different than the option's display value (what is displayed in the text field), the "displayWith" option allows you to do so. The value of "displayWith" is the name under which the implementation class to perform this job was registered. The class must implement the ChoiceHandler interface. An example can be found at the end of the playground component. The registration can be done in ngOnInit() using this service: this.service.registerDisplayWith('states', new MyDisplayer()); Consider the following example:

{
  "type": "string",
  "displayWith": "localName",
  "choices": [
    "https://en.wikipedia.org/wiki/Indonesia",
    "https://en.wikipedia.org/wiki/Peru",
    "As is - no tooltip"
  ]
}

The autocomplete is configured with "localName" which is a built-in displayer. It treats options like URLs and displays the local name which is the text after the last slash, hash, colon or dot. This causes the dropdown to display "Peru" with the tooltip indicating the real value "https://en.wikipedia.org/wiki/Peru" which is written to the JSON value.

The custom implementation also enables you to exercise tight control over filtering, typeahead loading of options, and determining the display value. For an example of a typeahead implementation, see the class MyTypeAhead at the bottom of the playground component.

Layout options

Layout options determine how the input elements of arrays and objects are arranged. These options can be applied for each nesting layer (e.g. if you're entering an array of objects):

{
  "type": "array",
  "layout": "horizontal",
  "items": {
    "type": "object",
    "layout": "vertical",
    "properties": {
      "name": {
        "type": "string"
      },
      "version": {
        "type": "number"
      }
    }
  }
}
  • horizontal (default): input controls are arranged horizontally and flex-wrap if there is insufficient space
  • vertical: input controls are arranged vertically
  • tab: controls are shown in tabs (only applies to arrays and objects with additionalProperties)
  • table: controls are shown in a table with the property names being the column names (only applies to an array of objects)
  • select: array is shown as a multi-select (only applies to arrays of string)
  • Any element can be placed in an expansion panel by adding "expanded": true / false. The Boolean value indicates whether the panel is expanded by default or not

The order field allows to control the inputs of objects:

  • The order field can be a list of field names. For example "order": ["firstname", "lastname"] defines the first name input to appear before the last name, regardless of their order in the properties
  • If a property is omitted, the form does not display an input. So in the example above, an age field is not in the form even if it is listed in properties.
  • Order can also specify a 2-level hierarchy like "order": [["firstname", "lastname"], "emails"]. If a vertical layout is chosen, this displays firstname and lastname in the first row and the array of emails in the second row. The first row automatically chooses the opposite layout direction internally.

The style and class fields allow passing CSS styles and classes to the input fields. For instance, you could emphasize the input with a higher z elevation and accommodate for longer input values by increasing the default input element width:

{
  "type": "string",
  "class": [
    "mat-elevation-z2"
  ],
  "style": {
    "width": "400px"
  }
}

Please also see the definition of the Schema object.

Application Logic

In some situations, you would like to compute a field based on the contents of other fields. This can be achieved via the "compute" option. It can be placed within an object as follows:

{
  "type": "object",
  "properties": { "first": {"type": "string"}, "last": { "type": "string" }, "salutation": { "type": "string", "readOnly": true } },
  "computed": {
    "salutation": '"Dear " & first & " " & last & "," & $context("var")'
  }
}

In this example, any change to the first or last fields trigger a change in salutation which is displayed as a read only form field. The expression defining the salutation value is expressed in JSONata (https://jsonata.org/). The custom function $context allows the host application to reference data which was set via this.service.setContext(key, value).

Validation and Submitting

Some JSON Schema constructs like "pattern" or "required" allow validating an object against the schema. The result of this validation is displayed on the UI but it is also propagated to the parent component via the "error" output variable. Error contains the first validation error message or null if the form is valid. The following example shows how this information can be used to deactivate form submission:

<lib-json-schema-form [(value)]="value" [schema]="schema" [label]="schema.title" (errorChange)="error=$event">
</lib-json-schema-form>
<button [disabled]="error !== null" (click)="submit()">Submit</button>

Note that not all JSON schema validation constructs are supported. Also, arrays and additional property objects do not propagate the information and the invalid value is undefined.

Unsupported JSON Schema properties

We support JSON Schema Draft 6 with these exceptions:

  • patternProperties: allows defining a property type depending on the property name. You can work around this using additionalProperties.
  • const: allows defining a value to be constant. Work around this using default and /or enum with a single option.
  • Combining schemas (oneOf, anyOf, not, allOf): this allows giving multiple options (schemas) for a property. These constructs make a lot of sense for validation but are hard to apply in the context of a form and therefore, they are not supported.
  • contains: specifies that an array must contain one instance of a given type. As with the schema combination constructs, this makes sense for validation for not for forms.

Referenced Schemas

In order to foster reuse, schemas are often made available on the web. In this case, you can use JSON schema's $ref mechanism to have the browser load the schema as follows:

<lib-json-schema-form [schema]="{$ref:'https://raw.githubusercontent.com/riskine/ontology/master/schemas/core/profession.json'}">

The URL can also be relative to the form's URL:

<lib-json-schema-form [schema]="{$ref:'schema.json'}">

If you do not want the schema to be downloaded, you can also manually provide referenced schemas via the root schema:

{
  ...
  referenced: {
    'http://example.org/': { $id: 'http://example.org/', ... },
    'urn:myschema': { $id: 'urn:myschema', ... },
  }
}

Structure of this repository

The repository contains:

Contribute

We welcome contributions. If you are interested in contributing to Dashjoin, let us know! You'll get to know an open-minded and motivated team working together to build the next generation platform.

json-schema-form's People

Contributors

aeberhart avatar dependabot[bot] avatar eberhaal avatar sibiraj-s avatar uw4 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

json-schema-form's Issues

a11y bug array

In the editor, with default layout, for type array there is an a11y problem.

When hovering one input field by mouse, the delete button appears, but when using only the keyboard for navigation, it is not possible to delete an entry - the delete button is not visible and can't be reached.

required should be compliant to the latest json schema spec

{
  "type": "object",
  "required": [
    "name"
  ],
  "properties": {
    "name": {
      "type": "string"
    },
    "version": {
      "type": "number"
    }
  }
}


rather than

{
  "type": "object",
  "properties": {
    "name": {
      "type": "string", "required"=true
    },
    "version": {
      "type": "number"
    }
  }
}

support resolving external $ref

So far, $ref only supports json pointer references in the schema document (root schema passed to the component).
Consider these examples:

https://raw.githubusercontent.com/riskine/ontology/master/schemas/core/damage.json
https://raw.githubusercontent.com/riskine/ontology/master/schemas/core/definitions.json

Definitions.json contains basic defs which are referenced in damage.json using
"$ref": "definitions.json#/money"

It should be possible to provide the component with a resolver implementation that is able to load schemata based on their $id. In this specific example, a schema "https://ontology.riskine.com/X" can be read from "https://raw.githubusercontent.com/riskine/ontology/master/schemas/core/X". Note that the spec explicitly states that $id does not have to be "downloadable".

One resolver could be a HTTP download resolver. Another one could be a simple map: {id, schema}.

It should be possible to use the form simply by providing a $ref to an external schema:

<lib-json-schema-form [(value)]="value" [schema]="{'$ref': 'http://....'}" resolver="url">

Support additionalProperties

In order to edit json with arbitrary keys we need to support additionalProperties.

Internally, can be treated like an array of key value objects

readonly components

define a custom json schema attribute that causes the form to only display and not edit a value

array edit with tab

Select the "array" example and press + a couple of times.
enter a value and press TAB.
The same value shows up in the next input field but not in the JSON value.

autocomplete caching

select "simpleGet" example and enter text field.
select "jsonPath" example and enter text field.
The dropdown options are the same but there should be only 4 options since the complex json same is shorter.

allow hiding null properties

There can be cases, where several properties are available but most of them are null / undefined. An example is editing JSON schema itself. title, description, and many other properties are available and clutter the form. Should be possible to define a layout where these can be expanded via a ... button

propagate validation errors

there should be a boolean output variable that indicates whether the form is valid or not.
This would allow the caller to attach the disabled prop to the form directly:

invalid: boolean

<lib-json-schema-form (invalid)="invalid" ....
<button (click)="..." [disabled]="invalid">

features missing to reach draft-06 conformity

Will not support

  • patternProperties, like additionalProperties: fairly complicated to implement since the schema for the subcomponent changes once the key is edited
  • const: provides no value for an editor
  • Combining schemas (oneOf, anyOf, not, allOf): impossible to do for a form generator

allow to specify an object template

allow for computed keys on objects:

{
computed: { y: "prefix-${x}" } ]
properties: { x : { type: string } }
}

entering 1 as the value for x would set y to prefix-1

include styling parameters in schema

similar to layout. candidates are:

  • width
  • height / font size

.mat-form-field {
height: 1.5em !important;
font-size: 0.8em !important;
}

  • textarea rows / cols (can get rid of largetextarea widget)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.