A fresh way to create simple or complex forms – without programming!
Try it yourself in a Live Demo!
vue-dd-form aims to reduce the of time creating forms, while keeping the flexibility and data-model persistence. This project is a first implementation of the Described-Data approach.
Described-Data is a concept inspired by the JSON Schema. Basically, each node in the data set is assigned a view (input), which it then represents on the UI. In practise, vue-dd-form only needs your data and descriptions to render itself!
“I use a JSON Schema, what is the difference?”
npm install --save vue-dd-form
import ddForm from 'vue-dd-form';
components: {
ddForm,
}
<dd-form
:data="exampleDataSet"
:descriptions="exampleDescriptions"
@submit="submit">
</dd-form>
…inside your data()
tag
exampleDataSet: {},
exampleDescriptions: {
"name": { view: 'text', label: 'Full name'}
}
Output of this simple example looks like this:
Your JS Data is described by another JS Object targeting all nodes which should be rendered on the screen. Imagine a data model of a IMDB movie:
{
author: {
name: "J. Cameron",
birth: "2020-09-08T12:11:03.332Z",
active: true
}
title: "Titanic",
description: "Lorem ipsum ...",
language: "English",
genres: ["Action", "Drama"]
}
…the descriptions could look something like this:
{
author: { view: "group", label: "Author" },
author.name: { view: "text", label: "Name of author" },
author.birth: { view: "datetime", label: "Date of birth" },
author.active: { view: "tick", label: "Is the author still active?" },
title: { view: "text", label: "Movie title" },
description: { view: "area", "label": "Description" },
genres: { view: "checkbox", "label: "Genres", options: ["Action", "Comedy", "Drama"] },
language: { view: "select", label: "Original language", "options: ["English", "Spanish", "Korean"] }
}
Description paths
So as you can see nodes are targeted by a dotted notation. Each description object has a required value of view
, which specifies, to which UI element should the node be rendered. Currently there is 11 basic view types, though you can specify your own (see Custom views).
If you want to describe every array’s child, you can use the wildcard path. Look closely at this data set:
{
programme: [
// Day 1
{
header: 'Friday',
subheader: '25th September 2020',
items: [
// Item 1
{
title: 'Arrival',
subtitle: 'please be on time',
time: '9AM'
},
// Item 2
{
title: 'Workshop 1',
subtitle: 'hosted by John Doe',
time: '11AM'
}
// Item 3 ...
},
// Day 2 ...
],
}
The wildcard paths can be leveraged like this:
programme: { view: 'collection' },
programme[*]: { view: 'group', label: 'Day no. {_index}' }, // more about the {_index} at Dynamic values
programme[*].header: { type: 'text', label: 'Title' },
programme[*].subheader: { type: 'text', label: 'Short description' },
programme[*].items: { type: 'collection', label: 'Sessions' },
programme[*].items[*]: { type: 'group' },
programme[*].items[*].title: { type: 'text', label: 'Session name' },
programme[*].items[*].subtitle: { type: 'area', label: 'About' },
programme[*].items[*].time: { type: 'text', label: 'Time of start' },
Any view type can have a String in description value. If the string is found in provided functions
set, given function gets executed. The function also receives the child’s path and value as a parameter.
{
view: 'group'
hidden: '_groupHidden'
}
…while in JS code
<dd-form
...
:functions="myFunctions"
>
</dd-form>
data() {
return {
myFunctions: {
'_groupHidden': this.groupHidden,
'_groupIndex': this.groupIndex,
},
};
},
methods: {
groupHidden({path, value}) {
return value.item < 0;
},
groupIndex({path}) {
return this.getIndex(path);
}
},
Methods can be also put inline to the string. For these cases, the dynamic value needs to be wrapped inside the brackets.
{
view: 'group'
label: 'Day no. {_groupIndex}'
}
Check the demo’s source code to see it in use.
The order of descriptions defines the order of the views on the UI. It is also needed to describe first the Object and then it’s sub-nodes.
There is 11 basic view types included in this package:
Serves as Array container and expects to have multiple inner children. Collection shows just an add-child button by default. It is only an abstraction and has no visual representation.
Usage
{
view: 'collection',
}
Possible value types
(Array): Consumes only array
Arguments
label
(String): Shows title on the top of the viewclass
(String or Array): View’s assigned classwrapper
(String): Name of the view’s wrapperhidden
(Boolean): Hides or shows the viewdraggable
(Boolean): Allows or disallows children draggingdeletable
(Boolean): Allows or disallows children deleteaddable
(Boolean): Allows or disallows to add childrenServes as Object container and expects to have nested children. It is only an abstraction and has no visual representation.
Usage
{
view: 'group',
}
Possible value types
(Object) or null
: Consumes only object
Arguments
label
(String): Shows title on the top of the viewclass
(String or Array): View’s assigned classwrapper
(String): Name of the view’s wrapperhidden
(Boolean): Shows or hides the viewappend
(Object): Specifies the UI invisible appendant data to be merged with the object (triggered while adding new item to array)Acts as a basic single-line text field.
Usage
{
view: 'text',
}
Possible value types
String
: Consumes only String
Arguments
label
(String): Shows title on the top of the viewclass
(String or Array): View’s assigned classhidden
(Boolean): Hides or shows the viewwrapper
(String): Name of the view’s wrapperActs as a drow-down box.
Usage
{
view: 'select',
}
Possible value types
String or null
: Consumes String or null
Arguments
label
(String): Shows title on the top of the view
class
(String or Array): View’s assigned class
hidden
(Boolean): Hides or shows the view
wrapper
(String): Name of the view’s wrapper
options
(Array): Field option values
Plain mode
["Dog", "Cat"]
Value-text mode
[
{ text: "Dog", value: "doggo" },
{ text: "Cat", value: "caterpillar" }
]
Acts as a checkbox set.
Usage
{
view: 'checkbox',
}
Possible value types
Array
: Consumes Array of strings
Arguments
label
(String): Shows title on the top of the view
class
(String or Array): View’s assigned class
hidden
(Boolean): Hides or shows the view
wrapper
(String): Name of the view’s wrapper
options
(Array): Field option values
Plain mode
["Dog", "Cat"]
Value-text mode
[
{ text: "Dog", value: "doggo" },
{ text: "Cat", value: "caterpillar" }
]
Acts as a counter box.
Usage
{
view: 'counter',
}
Possible value types
Number
: Consumes a Number
Arguments
label
(String): Shows title on the top of the viewclass
(String or Array): View’s assigned classhidden
(Boolean): Hides or shows the viewwrapper
(String): Name of the view’s wrappermin
(Number): Minimum value to selectmax
(Number): Maximum value to selectActs as a date-time selection field.
Usage
{
view: 'datetime',
}
Possible value types
Date or String
: Consumes a Date, String or Timestamp (Firebase) but always returns Date
Arguments
label
(String): Shows title on the top of the viewclass
(String or Array): View’s assigned classhidden
(Boolean): Hides or shows the viewwrapper
(String): Name of the view’s wrappermin
(String)*: Minimum date valuemax
(String)*: Maximum date value* String in a Date format
Acts as a box for file uploads and renders into preview box, if consumable by web (image, video, …).
Usage
{
view: 'upload',
}
Possible value types
String
: Consumes a String
Arguments
label
(String): Shows title on the top of the viewclass
(String or Array): View’s assigned classendpoint
(String): API endpoint URL*payload
(Object or String): API payload data*hidden
(Boolean): Hides or shows the viewwrapper
(String): Name of the view’s wrapper* Endpoint URL receives a POST call with FormData (image
and payload
)
** ⚠️ Note from author** File upload functionality is heavily domain-oriented, thus I’d advise you to clone the ViewUpload.vue, modify it by your needs and use it as a Custom view.
Acts as a multi-line text field with optional WYSIWYG editor.
Usage
{
view: 'area',
}
Possible value types
String
: Consumes only String
Arguments
label
(String): Shows title on the top of the viewclass
(String or Array): View’s assigned classhidden
(Boolean): Hides or shows the viewwrapper
(String): Name of the view’s wrapperhtml
(Boolean): Whether or not to allow WYSIWYG featuresActs as a radio button set.
Usage
{
view: 'radio',
}
Possible value types
String
: Consumes a String or null
Arguments
label
(String): Shows title on the top of the view
class
(String or Array): View’s assigned class
hidden
(Boolean): Hides or shows the view
wrapper
(String): Name of the view’s wrapper
options
(Array): Field option values
Plain mode
["Dog", "Cat"]
Value-text mode
[
{ text: "Dog", value: "doggo" },
{ text: "Cat", value: "caterpillar" }
]
Acts as a single checkbox.
Usage
{
view: 'tick',
}
Possible value types
Boolean
: Consumes a Boolean
Arguments
label
(String): Shows title on the top of the viewclass
(String or Array): View’s assigned classhidden
(Boolean): Hides or shows the viewwrapper
(String): Name of the view’s wrapperAny custom view can be inserted via :views
property. Custom views are superior to the default ones, so text
can be overwritten by a custom one.
Usage:
pass a Object of components to :views
property in dd-form
component will receive path, value and description
properties
the component must emit a @change event with value and path
properties
customViews: {
'text': customTextView,
'generator': generatorView,
}
import customTextView from 'path/to/custom-view';
…and bind the data in dd-form
<dd-form
:views="customViews",
...
></dd-form>
Check the demo’s source code to see it in use.
Some sub-elements can be replaced too: button-add
, button-remove
, button-submit
, headline
.
customViews: {
'collection.button-add': customButtonAdd,
'collection.headline': customHeadline,
'collection.button-remove': customButtonRemove,
'group.headline': customHeadline,
'group.button-submit': customButtonSubmit,
}
vue-dd-form provides a :lang
property, which can rewrite the default language wording.
<dd-form
:lang="customLang",
...
></dd-form>
…and the lang object
customLang: {
add: 'Add item',
remove: 'Remove item',
save: 'Save data'
}
Views can be wrapped inside separate boxes (DIVs) so we can position them in any way. Look at example data:
{
programme: [
{
header: 'Friday',
subheader: '25th September 2020',
items: [
...
],
}
If we wanted, for instance, to have header and sub-header on the left side of the screen and items on the right, the descriptions would look as follows:
programme[*].header: { type: 'text' },
programme[*].subheader: { type: 'text', wrapper: 'left' },
programme[*].items: { type: 'collection', wrapper: 'left' },
In result HTML, the views would look like this
<div class="left">
<!-- header div -->
<div>...</div>
<!-- subheader div-->
<div>...</div>
</div>
<div class="right">
<!-- items div-->
<div>...</div>
</div>
Afterwards, only thing to be defined are the CSS classes. Check the demo’s source code to see it in use.
@change
fired after any value change, emits path, value changed & current data set
change({ path, value, data }) {
// eslint-disable-next-line no-console
console.log(path, value); // ex. => 'programme[*].header', 'Friday'
console.log(data); // ex. => { ... }
},
@submit
fired after form submit, emits data value
submit({ data }) {
// eslint-disable-next-line no-console
console.log(data); // ex. => { ... }
},
@add
fired after a item is added to a collection, emits path, value added & current data set
add({ path, value, data }) {
// eslint-disable-next-line no-console
console.log(path, value); // ex. => 'programme[*].options', { header: 'Friday', ... }
console.log(data); // ex. => { ... }
},
@remove
fired after a item is added to a collection, emits path, value removed, index & current data set
remove({ path, value, data, index }) {
// eslint-disable-next-line no-console
console.log(path, value); // ex. => 'programme[*].options', { header: 'Friday', ... }
console.log(data); // ex. => { ... }
console.log(index); // ex. => 1
},
Form can be styled in verious ways depending on the depth of the adjustment:
col--1-3
(sets width to 1/3 of the available space)col--2-3
(sets width to 2/3 of the available space)col--1-2
(sets width to one half of the available space)col--1-1
(sets full-width)row
(makes the div’s direction inline)row--boxed
(makes the div’s direction inline & puts it in a shadowed box)row-responsive
(makes the div’s direction inline, but wraps on mobile)view
view—root
view—branch
view—leaf
view__container
view__container—removable
view__wrapper—removable
button
button—add
button—remove
button—submit
headline
There are many useful things which are still not yet implemented (validation, CDN availability etc.). I’d be very thankful for any contributions! Described-Data would work the best if it was multi-platform so if you feel like cooperating (React etc.), hit me up!
Author: marekmensa
Source Code: https://github.com/marekmensa/vue-dd-form
#vue #vuejs #javascript