molgenis / molgenis-ui-form

Library for generating HTML web forms
GNU Lesser General Public License v3.0
0 stars 10 forks source link

molgenis-ui-form

codecov Known Vulnerabilities Commitizen friendly

Library for generating HTML web forms

Changelog

Details changes for each release generated in the CHANGELOG.md. You can view the changes on GitHub as well (https://github.com/molgenis/molgenis-ui-form/releases)

Usage

Install

# Add library using yarn.
yarn add @molgenis/molgenis-ui-form

Use

<template>
  <form-component
    id="example-form"
    :formFields="formFields"
    :initialFormData="formData"
    :formState="formState"
    :options="options"
    @valueChange="onValueChanged"
    @addOptionRequest="handleAddOptionRequest"
  >
  </form-component>
</template>

<script>
// Import form component
import { FormComponent } from '@molgenis/molgenis-ui-form'

// Import EntityToFormMapper
import { EntityToFormMapper } from '@molgenis/molgenis-ui-form'

// Import all form component styles
import '../node_modules/@molgenis/molgenis-ui-form/dist/static/css/molgenis-ui-form.css'

export default {
  name: 'ExampleComponent',
  data() {
    return {
      formFields: [],
      formData: {},
      formState: {},
      options: {
        showEyeButton: true
      }
    }
  },
  methods: {
    handleAddOptionRequest(addNewItemFunction) {
      // Handle the request to add a new item here
      const item = {
        id: 'new_item',
        label: 'New Item',
        value: 'new_item'
      }

      addNewItemFunction(item)
    },

    onValueChanged(formData) {
      // Do something with the updated formData
      console.log(formData)
    }
  },
  created() {
    // Generate a form from a MOLGENIS v2 API response
    // Or create fields based on the specs (Form specifications)
    const form = EntityToFormMapper.generateForm(metadata, items[0])
    this.formFields = form.formFields
    this.formData = form.formData
  },
  components: {
    FormComponent
  }
}
</script>

Note: Whatever you pass to the FormComponent as formData object, the FormComponent makes a local copy with Object.assign({}, formData).

If you want to react to data input, use the @valueChanged event.

valueChanged event

When data in the form is changed, the form fires a valueChanged event. This event exposes two arguments, formData and isFormValid.

formData is a key value object, where the field ID is the key, and the data filled in by the user is the value. isFormValid is a boolean telling you if there are any invalid fields in the form.

An example handler is shown below

methods: {
  onValueChanged (formData) {
    console.log(formData) // all the data currently in the form
  }
}
addOptionRequest event

To allow the user to add new options to a select list the handleAddOptionRequest should be a function with the following properties:

The option object passed to the completedFunction should at least have the following fields

Options

The FormComponent object can be configured via an options property. If no options object is supplied the defaults are used.

Option name Default Description
showEyeButton True Toggle the visibility of the Eye button.
allowAddingOptions False When set to true users are show a interface item to add options to a (multi)select.
inputDebounceTime 500 Time (in milliseconds) between input updates needs to pass before update event is fired.

Form specifications

Example on how to create fields and data objects for using the forms.

const fields = [
  {
    type: 'text',
    id: 'example-text-field',
    label: 'Example text field',
    description: 'This field shows how to create a form field from scratch',
    required: (formData) => true,
    disabled: false,
    readOnly: false,
    visible: (formData) => true,
    validate: (formData) => true
  }
]

const data = {
  'example-text-field': 'example value'
}

Type support

We support most HTML input types like number, text, and email. Below is a list of supported types.

type renders
radios A list of radio buttons
single-select A Vue select dropdown which supports asynchronous and synchronous option lists
multi-select A Vue Multiselect dropdown which supports asynchronous and synchronous option lists
number A HTML5 number input
integer A HTML5 number input, with step size set to 1 and may not include a fraction and must lie between --2147483648 and 2147483647
long A HTML5 number input, Long may not include a fraction and should fit into javascript number type.
decimal A HTML5 number input, may include a fraction
text-area A textarea HTML element
date A Vue Flatpickr Date component
date-time A Vue Flatpickr Date component with 'enableTime = true'
checkboxes A list of checkboxes
text A HTML5 text input
hyperlink A HTML5 text, only valid uri is allowed
email A HTML5 text email
password A HTML5 password input
file A HTML5 file input
field-group A type that is used to nest other inputs

Option field example

Fields that render lists of options like radio buttons, checkboxes, and select dropdowns have an additional option parameter. This option parameter should always contain a function returning a promise.

This makes it usable for both synchronous and asynchronous rendering of option lists.

const fields = [
  {
    type: 'field-group',
    id: 'example-field-group',
    label: 'Example field group',
    description: 'This field shows how to create a group of fields',
    required: (formData) => true,
    disabled: false,
    readOnly: false,
    visible: (formData) => true,
    validate: (formData) => true,
    options: () => {
      const options = [
        {
          id: '1',
          label: 'Option 1',
          value: '1'
        },
        {
          id: '2',
          label: 'Option 2',
          value: '2'
        }
      ]

      return Promise.resolve(options)
    },
    isAddOptionAllowed: () => Promise.resolve(true)
  }
]

The isAddOptionAllowed function indicated if the user should be allowed to add options

Field group example

A field group can be used to group a set of fields. You can specify a list of fields under a field-group via the children parameter.

const fields = [
  {
    type: 'field-group',
    id: 'example-field-group',
    label: 'Example field group',
    description: 'This field shows how to create a group of fields',
    required: (formData) => true,
    disabled: false,
    readOnly: false,
    visible: (formData) => true,
    validate: (formData) => true,
    children: [
      {
        type: 'text',
        id: 'example-field-group',
        label: 'Example text field',
        description: 'This field shows how to create a form field from scratch',
        required: (formData) => true,
        disabled: false,
        readOnly: false,
        visible: (formData) => true,
        validate: (formData) => true
      }
    ]
  }
]

// Note that the field group itself does not have data
const data = {
  'example-text-field': 'example value'
}

Range

A number fields valid input range can be restricted by supplying a range object The range object can have a min property, a max property or both. for example

{
  id: 'example-number-with-range',
  type: 'integer' | 'long',
  range: {
    min: 0,
    max: 13
  }
}

Required, visible, and validate

As you might have noticed in the above examples, required, visible, and validate are functions returning a boolean. The reason for this is that you might want to validate a field based on the input of another field.

Or show / hide a field once another field has a certain value.

const fields = [
  {
    type: 'text',
    id: 'example-text-field',
    label: 'Example text field',
    description: 'This field controls the visibility of example-number-field',
    required: (formData) => true,
    disabled: false,
    readOnly: false,
    visible: (formData) => true,
    validate: (formData) => true
  },
  {
    type: 'integer',
    id: 'example-integer-field',
    label: 'Example integer field',
    description:
      'This field is shown if "example-text-field" contains the text "show"',
    required: (formData) => true,
    disabled: false,
    readOnly: false,
    visible: (formData) => {
      return formData['example-text-field'] === 'show'
    },
    validate: (formData) => true
  }
]

Unique field value validation

Fields of type string, number, integer, long, decimal or radio may include a function that validated value uniqueness. The function should return a promise that resolves to a boolean indicating if the value is unique in some context.

Example:

unique: (proposedValue, [context]) => {
  return new Promise((resolve, reject) => {
    api.get('some-api-call-that-checks-proposed-value').then((result) => {
      resolve(result)
    })
  })
}

Validation message localization

Validation messages may be localized via the use of the @molgenis/molgenis-i18n-js plugin.

Usage:

Vue.use(i18n, {
  lng: 'en',
  fallbackLng: 'en',
  namespace: ['some-namespace', ui-form],
  callback () {
    /* eslint-disable no-new */
    new Vue({
      el: '#form-demo',
      template: '<FormDemo/>',
      components: { FormDemo }
    })
  }
})

If no 'ui-form' namespace is set on the supplied Vue instance the default (English) messages are used.

message keys and defaults
Key Default message Additional Info
ui-form:form_required_field 'This field is required'
ui-form:form_validation_failed 'Validation failed'
ui-form:form_not_a_valid_number 'Not a valid number'
ui-form:form_not_a_valid_hyperlink 'Not a valid Hyperlink'
ui-form:form_not_a_valid_email 'Not a valid email'
ui-form:form_not_within_range 'Value is outside of range' min, max is added as: ' ($min - $max)'
ui-form:form_below_min_value 'Value is below allowed value' min value is added as: ' $min'
ui-form:form_above_max_value 'Value is above allowed value max value is added as: ' $max'

Entity mapper options

The EntityToFormMapper.generateForm function takes a optional options param. The options param is a object that can contain the following properties:

Invalid validation expression handling

Erroneous validation expressions in the visibleExpression and nullableExpression metadata fields will fallback to the respective visible and nillable values. A broken validationExpression will fall back to being true.

Description fields

The description will automatically make URL's clickable.

You can create a (read more) button by using an enter in the description field, which will display the remaining text after the enter.

You are able to create a information tooltip by using the following tag inside of the description field {i}My information icon tooltip{/i}

Break lines with {br} for example a description like this: {br}• list item 1{br}• list item 2{br}• list item 3{br}

Will render a list like this:
• List item 1
• List item 2
• List item 3

Development

The general guidelines and setup of the development environment are described here.

Build setup

# install dependencies
yarn install

# serve with hot reload at localhost:8080
yarn run dev

# build for production with minification
yarn run build

# build for production and view the bundle analyzer report
yarn run build --report

# run unit tests
yarn run unit

# run continuous unit tests
yarn run unitwatch

# run e2e tests
yarn run e2e

# run all tests
yarn test

How to commit

We use conventional commits to generate changelogs and release notes. Please check: https://www.conventionalcommits.org/

Example

git commit file.ext -m "fix(file.ext): fixes something"

How to publish

Each time a PR is merged a release will be done to NPM. The CHANGELOG.md and GitHub release will be ammended.

The version of the package is based upon convential commits. Check: http://commitizen.github.io/cz-cli/.

Tests

To develop tests please acknowledge the following guidelines.

End-to-End test

Please make sure you add the name of the specific test in the test. This is needed to see the test-name in Saucelabs. Example

browser.options.desiredCapabilities.name = 'Example testname'

Integration with vue-form

We use the vue-form library for validation.

The component tree mingles elements from vue-form and molgenis-ui-form like this:

<FormComponent :formState :initialFormData>
  <VueForm :state="formState">
    <FormFieldComponent :formState :formData :field>
      <TypedFieldComponent v-model="formData[field.id]">
        <Validate :state :debounce>
          <input v-model="localValue">
          <FormFieldMessages>

The TypedFieldComponent creates a localValue data element and binds it to a type-specific input inside a Validate component. The TypedFieldComponent fires @Input events whenever localValue changes. These @Input events are bound to formData[field.id]

Event handling

vue-form debounces the validation until input ceases. The FormFieldComponent listens for formState[field.id].$pending to become false to make sure that validation is done and the fieldState is up to date. Then it emits a @dataChange event to the FormComponent and the FormComponent emits @valueChange.