@oarepo/data-editor

A library for editing of rendered JSON data.

Usage no npm install needed!

<script type="module">
  import oarepoDataEditor from 'https://cdn.skypack.dev/@oarepo/data-editor';
</script>

README

@oarepo/data-editor

A library for editing of rendered JSON data.

travis build stat npm version

Installation

@oarepo/data-editor requires @oarepo/data-renderer to be installed as well.

yarn add @oarepo/data-editor
yarn add @oarepo/data-renderer

To register/configure the library, add a new boot file to quasar (or main.js for vue-cli projects):

import DataEditor from '@oarepo/data-editor'
import DataRenderer from '@oarepo/data-renderer'

export default async ({ Vue, store, router }) => {
    Vue.use(DataEditor, {})
    Vue.use(DataRenderer, {})
 }

Usage

To use data editor, add data-editor to template. data-editor acceptsrecord, options, layout and pathLayouts and renders interface to edit received data with buttons for editing, addition and removal based on data type. Dialog components and default values can be passed to data-editor.

Record

record contains json data to be passed to data-editor.

  • simple object: record: { object: { a: 1 } }
  • simple array: record: { array: [1, 2] }
  • complex array: record: { array: [{ a: 1 }}, { b: 2 }] }
  • empty record: record: {}

Options

options may consist of schema, extraProps.

  • schema of rendered data (inline, block, table, flex), defaults to table
    • table options: { schema: table }
    • inline options: { schema: inline }
  • extraProps (submit, cancel method for data editor)
    • options: { extraProps: { submit: { submit: this.submit, cancel: this.cancel } } }

Layout

layout can be used to define the layout of an empty record, to associate dialog components or default values with props, to define children.

  • basic layout: layout: [{ 'path': 'a', 'label': 'a' }]
  • layout with dialog component: layout: [{ 'path': 'a', 'label': 'a', dialogComponent: DialogComponent, dynamic: true }]
  • layout with default value: layout: [{ 'path': 'a', 'label': 'a', defaultValue: () => 1, dynamic: true }]
  • layout with default value as a function: layout: [{ 'path': 'a', 'label': 'a', defaultValue: defaultValue, dynamic: true }]

pathLayouts can be used to associate dialog components or default values with objects or arrays regardless of their position within the layout. * simple default value options: { pathLayouts: { simpleArray: defaultValue: () => 1 } } * complex default value options: { pathLayouts: { complexArray: defaultValue: () => { a: 1 } } }

If there is a complex value in layout and not in data, then this complex value must be created by clicking displayed button before it and its content can be edited.

layout and pathLayouts are described in more detail here: https://github.com/oarepo/data-renderer/

Dialog component

Specifies dialog component to be used with data editor, e.g. dialog to add a new object with default property to array, dialog to enter property and value to be added to object etc.

Examples

Examples are located at /src/components:

simple object

Example of a simple object. Src at /src/components/SimpleEdit.vue:

<template lang="pug">
data-editor(:record="record" :options="options" :layout="layout")
</template>

<script>
import Vue from 'vue'

export default {
  name: 'simple-edit',
  data: function () {
    return {
      record: {
        Contact: {
          Phone: '+420123123123',
          Email: 'mary.black@gmail.com'
        }
      },
      options: {
        schema: 'table',
        extraProps: {
          submit: this.submit,
          cancel: this.cancel
        }
      },
      layout: {
        showEmpty: true
      }
    }
  },
  methods: {
    submit ({ context, prop, value, op }) {
      isNaN(value)
      if (op === 'add') {
        Vue.set(context, prop, value)
      }
      if (op === 'replace') {
        if (context[prop] === undefined) {
          Vue.set(context, prop, value)
        } else {
          context[prop] = value
        }
      }
    },
    cancel (props) {
      console.log('cancelling')
    }
  }
}
</script>

Simple array

Example of a simple array. Src at /src/components/SimpleArray.vue:

<template lang="pug">
data-editor(:record="record" :options="options" :layout="layout")
</template>

<script>
import Vue from 'vue'

export default {
  name: 'array-edit',
  data: function () {
    return {
      record: {
        keywords: ['first keyword', 'second keyword']
      },
      options: {
        schema: 'table',
        extraProps: {
          submit: this.submit,
          cancel: this.cancel
        }
      },
      layout: {
        children: [
          {
            prop: 'keywords',
            label: {
              label: 'keywordArray'
            },
            item: {
              label: {
                label: 'keyword'
              }
            }
          }
        ]
      }
    }
  },
  methods: {
    submit ({ path, context, prop, value, op, pathValues }) {
      if (op === 'add') {
        if (Array.isArray(context)) {
          context.push(value)
        } else {
          context[prop] = value
        }
      }
      if (op === 'replace') {
        Vue.set(context, prop, value)
      }
      if (op === 'remove') {
        if (Array.isArray(context)) {
          context.splice(prop, 1)
        } else {
          delete context[prop]
        }
      }
    },
    cancel ({ props }) {
      console.log('cancelling')
    }
  }
}
</script>

Complex array with default value

Example of a complex array with default value for newly added items. Src at /src/components/SimpleArray.vue:

<template lang="pug">
data-editor(:record="record" :options="options" :layout="layout")
</template>

<script>
export default {
  name: 'default-value-complex-array-edit',
  data: function () {
    return {
      record: {
        contact: [{ email: 1 }, { phone: '+420123123123' }]
      },
      options: {
        schema: 'table',
        extraProps: {
          submit: this.submit,
          cancel: this.cancel
        }
      },
      layout: {
        children: [{
          prop: 'contact',
          additionalProps: { defaultValue: () => ({ phone: '+420123123124' }) }
        }]
      }
    }
  },
  methods: {
    submit ({ path, context, prop, value, op, pathValues }) {
      if (op === 'add') {
        if (Array.isArray(context)) {
          context.push(value)
        } else {
          context[prop] = value
        }
      }
      if (op === 'replace') {
        context[prop] = value
      }
      if (op === 'remove') {
        if (Array.isArray(context)) {
          context.splice(prop, 1)
        } else {
          delete context[prop]
        }
      }
    },
    cancel ({ props }) {
      console.log('cancelling')
    }
  }
}
</script>

Complex array with dialog

Example of a complex array with dialog for addition of new items. Src at /src/components/ComplexArrayDialogEdit.vue:

<template lang="pug">
data-editor(:record="record" :options="options" :layout="layout")
</template>

<script>
import Vue from 'vue'
import DialogComponent from './DialogComponent'

export default {
  name: 'complex-array-dialog-edit',
  data: function () {
    return {
      record: {
        contact: [{ phone: '+420123123124' }, { email: 'mary.black@gmail.com' }]
      },
      options: {
        schema: 'table',
        extraProps: {
          submit: this.submit,
          cancel: this.cancel,
          dialogComponent: DialogComponent
        }
      },
      layout: {
        children: [
          {
            prop: 'contact'
          }
        ]
      }
    }
  },
  methods: {
    submit ({ path, context, prop, value, op, pathValues }) {
      if (op === 'add') {
        if (Array.isArray(context)) {
          context.push(value)
        } else {
          Vue.set(context, prop, value)
        }
      }
      if (op === 'replace') {
        context[prop] = value
      }
      if (op === 'remove') {
        if (Array.isArray(context)) {
          context.splice(prop, 1)
        } else {
          delete context[prop]
        }
      }
    },
    cancel ({ props }) {
      console.log('cancelling')
    }
  }
}
</script>

Record with dialog component and no data

Example of an empty record with layout and dialog. Src at /src/components/NonExistingObjectDialogEdit.vue:

<template lang="pug">
data-editor(:record="record" :options="options" :layout="layout")
</template>

<script>
import Vue from 'vue'
import DialogWithPropertyComponent from './DialogWithPropertyComponent'

export default {
  name: 'non-existing-object-dialog-edit',
  data: function () {
    return {
      record: {},
      options: {
        schema: 'table',
        extraProps: {
          submit: this.submit,
          cancel: this.cancel
        }
      },
      layout: {
        showEmpty: true,
        children: [
          {
            prop: 'object',
            additionalProps: { dialogComponent: DialogWithPropertyComponent },
            children: []
          }
        ]
      }
    }
  },
  methods: {
    submit ({ path, context, prop, value, op, pathValues }) {
      if (op === 'add') {
        if (Array.isArray(context)) {
          context.push(value)
        } else {
          Vue.set(context, prop, value)
        }
      }
      if (op === 'replace') {
        context[prop] = value
      }
    },
    cancel ({ props }) {
      console.log('cancelling')
    }
  }
}
</script>

Example of a dialog component. Src at /src/components/DialogComponent.vue:

<template lang="pug">
q-dialog(ref="dialog" @hide="onDialogHide")
 q-card
   q-card-section
     q-form(ref="form")
       q-input(label="value" v-model="value")
   q-card-actions(align="right")
     q-btn(color="primary" type="submit" label="OK" @click="onOKClick")
     q-btn(color="primary" label="Cancel" @click="onCancelClick")
</template>

<script>
export default {
 name: 'dialog-component',
 data: function () {
   return {
     value: null
   }
 },
 props: {
   initialValue: Object
 },
 mounted () {
   if (this.initialValue) {
     this.value = this.initialValue
   }
 },
 methods: {
   show () {
     this.$refs.dialog.show()
   },
   hide () {
     this.$refs.dialog.hide()
   },
   onDialogHide () {
     this.$emit('hide')
   },
   async onOKClick () {
     if (await this.$refs.form.validate()) {
       this.$emit('ok', { phone: this.value })
       this.hide()
     }
   },
   onCancelClick () {
     this.hide()
   }
 }
}
</script>

Empty object with dialog component and default value function

Example of object with default value as a function and dialog component. Src at /src/components/AdditionalPropsEdit.vue:

<template lang="pug">
data-editor(:record="record" :options="options" :layout="layout")
</template>

<script>
import DialogWithPropertyComponent from './DialogWithPropertyComponent'
import Vue from 'vue'

function defaultValue ({ context, layout }) {
  for (const prop of 'abcdefghijklmnopqrstuvwxyz'.split('')) {
    if (context[layout.prop][prop] === undefined) {
      return { prop: prop, value: 'keyword' }
    }
  }
}

export default {
  name: 'additional-props-edit',
  data: function () {
    return {
      record: {
        creator: { name: 'Mary Black' },
        contact: { phone: '+420123123124' },
        keywords: {}
      },
      layout: {
        children: [
          { prop: 'creator', additionalProps: { dialogComponent: DialogWithPropertyComponent } },
          { prop: 'contact', additionalProps: { dialogComponent: DialogWithPropertyComponent } },
          { prop: 'keywords', additionalProps: { defaultValue: defaultValue } }]
      },
      options: {
        schema: 'table',
        extraProps: {
          submit: this.submit,
          cancel: this.cancel
        }
      }
    }
  },
  methods: {
    submit ({ path, context, prop, value, op, pathValues }) {
      if (op === 'add') {
        if (Array.isArray(context)) {
          context.push(value)
        } else {
          Vue.set(context, prop, value)
        }
      }
      if (op === 'replace') {
        context[prop] = value
      }
      if (op === 'remove') {
        if (Array.isArray(context)) {
          context.splice(prop, 1)
        } else {
          delete context[prop]
        }
      }
    },
    cancel ({ props }) {
      console.log('cancelling')
    }
  }
}
</script>

Record as tree

Example of a record as tree with a complex default value for addition of new items. Src at /src/components/TreeEdit.vue:

<template lang="pug">
data-editor(:record="record" :options="options" :layout="layout" :path-layouts="pathLayouts")
</template>

<script>
import Vue from 'vue'

export default {
  name: 'tree-edit',
  data: function () {
    return {
      record: {
        object: [
          { creator: 'Mary Black' },
          {
            contact: [
              { phone: '+420123123123' },
              { email: ['mary.black@gmail.com'] }]
          }]
      },
      options: {
        schema: 'table',
        extraProps: {
          submit: this.submit,
          cancel: this.cancel
        }
      },
      layout: {
        children: [
          {
            prop: 'object',
            additionalProps: { defaultValue: () => ({ keywords: ['first keyword', 'second keyword'] }) }
          }
        ]
      }
    }
  },
  methods: {
    submit ({ path, context, prop, value, op, pathValues }) {
      if (op === 'add') {
        if (Array.isArray(context)) {
          context.push(value)
        } else {
          Vue.set(context, prop, value)
        }
      }
      if (op === 'replace') {
        context[prop] = value
      }
      if (op === 'remove') {
        if (Array.isArray(context)) {
          context.splice(prop, 1)
        } else {
          delete context[prop]
        }
      }
    },
    cancel ({ props }) {
      console.log('cancelling')
    }
  }
}
</script>