0.98.2 • Published 1 month ago

vrf v0.98.2

Weekly downloads
295
License
-
Repository
github
Last release
1 month ago

Coverage Status

Live demo

Documentation

Build Setup

# install dependencies
npm install

# start playground
npm start

Vrf installation

import Vue from 'vue'
import Vrf from 'vrf'
import {translate} from './i18n' // we assume, that i18n file exports translate function with (property, modelName) => string signature

Vue.use(Vrf, {
  translate
})

Table of contents

What is vrf?

Vrf (Vue Resource Form) is a solution for quickly writing declarative user interface forms.

First of all vrf is the specification of form components. There are so many great ui frameworks with different components API, so if you want migrate from one to other you will must refactor each form and probably, it isn't what you want. Vrf provides common abstract standard for forms like pure html forms, but more powerful.

This package contains a set of descriptors for each form element that you can use to create your own implementation. If you need to add a new property/feature to some component - this is probably an occasion to think about whether it is possible to add it to the core (this can be discussed in issues). If the it can be added to the core, this will mean that it is included in the standard and all authors of other implementations will also be able to implement it. If this is not possible, the property is added only for the adapter component and will work only for this adapter.

Vrf doesn't depends on current I18n, validation and network interaction libraries. Instead, it provides interfaces for integration with any one.

Ideology

Vrf puts form at the forefront of your application, but at the same time all high-level features are provided unobtrusively and their activation is implemented explicitly. The main thesis of vrf is to stay simple while it is possible. This means, for example, that you can make the most of the built-in autoforming capabilities and not write any code, but if one day you need a more complex flow than effects can offer, you can simply turn off the auto mode and manually manipulate the form, while still taking the other advantages that vrf gives.

In other words, the complex things have the right to be complex, but simple must remain simple.

What does it look like?

It allows you to write forms in this way:

<rf-form name="User" auto>
  <rf-input name="firstName" />
  <rf-input name="lastName" />
  <rf-switch name="blocked" />
  <rf-select name="roleId" options="roles" />
  <rf-textarea name="comment" />
  <rf-submit>Save</rf-submit>
</rf-form>

Such form will load and save data without a single line of Javascript code. This is possible due to the use of the effects, which describes the general logic of working with entities in your project. If some form required very specific logic, the form can be used in a lower level mode(without "auto" flag).

Features

  • expressive syntax
  • ease of separation
  • I18n
  • Serverside validations
  • autoforms
  • nested entities
  • option disabled / readonly for entire form
  • vuex integration

Basics

Object binding

Binding to an object is the cornerstone of vrf. This concept assumes that instead of defining a v-model for a field each time, you do a binding once — entirely on the form object, and simply inform each component of the form what the name of the field to which it is attached is called. Due to this knowledge, the form can take on the tasks of internationalization and display of validations (whereas when determining the v-model for each field, you are forced to do it yourself).

<template>

<rf-form :resource="resource" :errors="errors">
  <rf-input name="title">
</rf-form>

</template>

<script>

export default {
  data(){
    return {
       resource: {
          title: ""
       },
       errors:{
         title: ['Should not be empty']
       }
    }
  }
}


</script>

Access to the resource

The form passes a reactive context to all child components (using the Provide / Inject API), so any descendant of the form (not even direct) can receive this data. This allows you to break complex forms into parts, but without the need, it is better not to use this opportunity and try to keep the entire form in one file.

There are several ways to access the resource:

  • use rf-resource component
<rf-resource v-slot="{$resource}">
  <div>{{$resource}}</div>
</rf-resource>
  • use Resource mixin
<template>

<div>
  {{$resource}}
</div>

</template>

<script>

import {Resource} from 'vrf'

export default {
   mixins: [
    Resource
   ]
}

</script>
  • implement your own component using accessible descriptors
<template>
<div>
  <input v-model="$value" />
  {{$resource}}
</div>
</template>

<script>

import {descriptors} from 'vrf'

export default {
  extends: descriptors.input
}

</script>

Where is the resource?

The resource can be in three places:

  • in the state of the parent component for the form
  • in the state of form(this happens in autoforms, or for example if you do not pass a resource prop). In this case, you can get a reference to the resource using :resource.sync prop.
  • in vuex

Expressions

The standard way of writing expressions that depdends on resource is using $resource variable from scoped slot on rf-form in main form file

<rf-form
  name="Todo"
  v-slot="{$resource}"
>
  <rf-input name="title" :disabled="$resource.id" />
</rf-form>

It won't fail if $resource is not loaded yet, because scoped slot is rendered only after $resource is loaded. All required sources are initialized using empty arrays, so using $sources reference is safe as well.

If your form is splitted into files and you need conditional rendering in the file without rf-form - you should use rf-resource component to access the resource.

Sources

Some components (for example, such as selects) require options for their work. For these purposes, the form sources property serves. It expects a hash of all the necessary options that can be accessed in specific components by name.

<template>

<rf-form :resource="todo" :sources="sources">
  <rf-select name="status" options="statuses" />
</rf-form>

</template>

<script>

export default {
  data(){
    return {
      resource: {
        status: 1
      },
      sources: {
        statuses: [
          {
            id: 1,
            title: 'pending'
          },
          {
            id: 2,
            title: 'ready'
          }
        ]
      }
    }
  }
}

</script>

Instead of a string with the name of the options, you may also pass directly an array of options(but it is used less often since vrf's strength is precisely the declarative descriptions of forms and autoforms can load sources by name).

When you use a source name with autoforms, form uses effects to load the collection for a source. Internally, this is achieved by calling the method requireSource on the form when component was mounted or options prop was updated. Then the form chooses the most effective loading strategy depending on the stage at which the method requireSource was called. You may use this method in your own components, when you need sources for their work.

Nested entities

Vrf supports work with nested entities, both single and with collections. To work with them, the rf-nested component is used, which expects a scoped slot with form components for a nested entity. Internally, rf-nested uses the rf-form the required number of times, so the use of rf-nested can be equated with the declaration of the form inside the form, which can be duplicated if necessary.

<template>

<rf-form :resource="todo">
  <rf-input name="title" />
  <rf-nested name="subtasks"> // you may specify translation-name for nested scope, by default it will be singularized name
    <template v-slot="props">
      <rf-input name="title">
      <rf-datepicker name="deadline" />
    </template>
  </rf-nested>
</rf-form>

</template>

<script>

export default {
  data(){
    return {
      resource: {
        title: '',
        subtasks: [
          {
            title: '',
            deadline: new Date
          }
        ]
      }
    }
  }
}


</script>

Autoforms

Autoforms are a special form mode in which the form within itself performs tasks of loading, saving data, forwarding validation errors, and can also perform some side effects, for example, redirecting to a page of a newly created entity.

Autoforms powered by Effects API which allows to create plugins in modular way. Due to this, it is possible to implement the flow of autoforms for the specifics of any project. You may use ready-made effects or implement your own.

Data loading control

Vrf provide some methods on rf-form allows you to manage data loading:

$refs.form.forceReload() // Completely reloading, excplicitly displayed to user

$refs.form.reloadResource() // Reload only resource without showing loaders

$refs.form.reloadResource(['messages']) // Reload only 'messages' key on resource

$refs.form.reloadSources() // Reload only sources

$refs.form.reloadRootResource(['options']) // reload root form resource, useful if nested component affects data on top level

Method reloadResource allows you to write custom components which may reload the piece of data they are responsible for.

import {descriptors} from 'vrf'

export default {
  extends: descriptors.base,
  methods: {
    ... // some logic mutating data on the server
    invalidate(){
      this.$form.reloadResource(this.name)
    }
  }
}

Actions

Vrf provides its own way to create simple buttons that activate async requests. These requests are served by effects and the received data stored in the context of the form(by analogy with a resource).

For example, this snippet renders a button that initiates POST request to /archive in a resource context.

<rf-action name="archive" />

You may change requests parameters by props

<rf-action 
  name="archive"
  method="put"
  :data="{force: true}"
  :params="{queryParameter: 1}"
/>

rf-action in adapters may handle pending status by loader showing. Moreover, you can implement your own rf-action view using activator slot

<rf-action name="archive">
  <template v-slot:activator="{on, pending, humanName}">
    <my-great-button v-on="on" :loading="pending">{{humanName}}</my-great-button>
  </template>
</rf-action>

To render the results, in simple cases you can use rf-action-result component(with slot or component).

<rf-action name="loadText" />

<rf-action-result name="loadText" component="some-component-with-data-and-or-status-props" />

<rf-action-result name="loadtext">
  <template v-slot="{data}">
    <p>{{data}}></p>
  </template>
</rf-action-result>

Or/and use event result

<rf-action name="doSomething" @result="onResult" />

If you need reload resource on result, you may use prop reload-on-result

<rf-action name="switchMode" reload-on-result />

If your action must show toast in UI by result, this can be done in the effects. For example, in REST effect $message field will be processed by effect as a message for user and it will be emitted by bashowMessage.

Run actions programmatically

You may run actions programmatically as well

<template>

<rf-form name="Todo" auto>
  ...
</rf-form>

</template>

<script>
export default {
  methods: {
    attachImage() {
      const data = new FormData()
      this.$form.executeAction('attachImage', {data, method: 'PUT'})
    }
  }
}
</script>

Bitwise fields

Sometimes you need to manage some bitwise values in your resource. There is rf-bitwise component to manage them. It has two modes - you can use this component as a wrapper for checkboxes, or use its options property(like rf-select). It supports inverted mode as well.

<template>

<rf-form :resource="todo">

  <!-- rf-bitwise as wrapper, markup mode -->
  <rf-bitwise name="flags">
    <rf-checkbox name="visible" power="0" />
    <rf-checkbox name="editable" power="1" />
    <rf-checkbox name="shareable" power="2" />
  </rf-bitwise>

  <!-- rf-bitwise renders checkboxes itself by options -->
  <rf-bitwise
    name="flags"
    :options="options"
  />
</rf-form>

</template>

<script>

export default {
  data(){
    return {
      resource: {
        flags: 0
      }
    }
  },
  computed: {
    options(){
      return [
        {
          id: 0,
          name: 'visible' // use title instead of name, if you don't need translations
        },
        {
          id: 1,
          name: 'editable'
        },
        {
          id: 2,
          name: 'shareable'
        }
      ]
    }
  }
}


</script>

</template>

Default props

You may specify default props values for some inputs globally during vrf initialization. It allows you to set up common styles for ui framework if it uses props for customization.

Vue.use(Vrf, {
  adapters: [
    VrfVuetify
  ],
  defaultProps: {
    RfInput: {
      outlined: true
    }
  }
})

v-model

In some cases you may want to use rf- controls outside of form, if you don't need form functionality, but still want to use the same elements without separating by vrf/non-vrf inputs. Regarding to this vrf inputs support v-model directive, allowing to use them in seamless way

<template>

<div>
  <p>Enter your first name</p>
  <rf-input v-model="firstName" />
</div>

</template>


<script>

export default {
  data(){
    return {
      firstName: ''
    }
  }
}

</script>

Advanced

Architecture

Vrf is all about modularity, you may customize almost any part of it. The final result is achieved due to symbiosis of the following components:

  • Core(this package) - contains all business logic of forms. It implements form based on standard html components, without any styling and it's the foundation providing APIs for other modules.

  • Adapters - implements VRF using some ui framework over link components descriptors from core. Most likely you will use vrf with some adapter.

  • Translate lambda - function with (modelProperty, modelName) -> translation signature, used for translations

  • Effects - autoforms logic and side effects

  • Autocomplete providers - components containing autocompletes logic

Effects API

Vrf uses effects to deal with auto-forms lifecycle and side effects. There are two types of effects - API and non-API effects.

API effects:

  • activated by the auto property of the rf-form
  • executed for each event in order of registration in js.use(Vrf, {effects: [...]}) until some effect returns promise(this mechanic works only on onLoad, onLoadSources, onLoadSource, onSave, onCreate and onUpdate subscriptions)
  • it's possible to choose effect by specify its name in auto property of the rf-form
  • by passing EffectExecutor to auto property you may customize autoform logic ad-hoc

non-API effects:

  • activated by the effects property of the rf-form
  • executed for each event in order of registration
  • it's possible to specify effects for current form by passing array of names to the effects property

Effect definition and using

There are type definitions for more convenient developing effects. If you create a plugin, you should export an effect factory with default options to provide simple way to add new options in the future.

// effect.ts
import {Effect} from 'vrf'

export default (options = {}) : Effect => {
  return{
    name: 'effect-name',
    api: true,
    effect({onLoad, onLoadSource, onLoadSources, onSave, }){
      onLoad((id) => Promise.resolve({}))

      onLoadSource((name) => Promise.resolve([]))

      onLoadSources((names) => Promise.resolve({}))

      onSave((resource) => Promise.resolve())
    }
  }
}


// initialization of vrf in project

import effect from './effect'

const effects = [
  effect()
]

Vue.use(Vrf, {effects})

Lifecycle

Effects are mounted after auto/effects props changing and initially after form mounting. There are two effect lifecycle events:

  • onMounted - is fired on each effect mounting
  • onUnmounted - is fired on each effect unmounting(when managing props are changed or form is destroyed). This subscription should be used to clear some stuff, for example some side event listeners.

API effects

There are some subscriptions for api effects:

  • onLoad - is fired when form loads the resource
  • onLoadSources - is fired when form loads the sources in eager way
  • onLoadSource - is fired when form loads only one source because of form.requireSource execution. It happens for example if rf-select appeared as a result of condition rendering
  • onSave - is fired when form is submitted. It is optional subscription, instead you may use more convenient onCreate and onUpdate subscriptions.
  • onCreate - is fired when form creates new resource
  • onUpdate - is fire when form updates new resource
  • onCreated - is fired when onCreate returned an id of new created resource. There is a default trap for this event, which reloads form data, but it's possible to override this behaviour by using event.stopPropagation()
  • onLoaded - is fired when data is received from backend, the same like after-load-success on the form
  • onSuccess - is fired when resource is saved successfully
  • onFailure - is fired when resource wasn't saved due to errors

Data converters

You may implement data convertation after receiving resource from api effect and before sending. For this purpose Effects API has two subscriptions:

  • onAfterLoad - is fired each time when vrf received entities from api effect(for resource and for each entity of sources)
  • onBeforeSave - is fired before resource will be saved

The listeners of these events are just mappers, which get object and return modified object. It's possible to use many converters in your application, they will be executed in the order of registrations in effects section. Converters should use api: true flag, because they should be executed always when auto is enabled.

User notifications

There is a standard way to provide user notification customization using onShowMessage subscription. So, you may use bashowMessage helper to emit message from any effect using type definitions from vrf, and any notifications effect which uses onShowMessage subscription will be able to show this notification.

Adapter API

The adapter must export the added components, it can both override components from the vrf, and add new ones(with rf- prefix).

export default {
  name: 'vrf-adapter-name',
  components: {
    RfInput
    ...
  }
}

If you need install hook, you can add it, but you should be aware that it does not receive options, since they refer to vrf. If you want options, you need to export an adapter factory instead of an adapter.

export default (options) => {
  name: 'vrf-adapter-name',
  components: {
    RfInput
    ...
  },
  install(Vue){
  }
}

One of the main things to consider when writing an adapter is that your adapter should not have a dependency vrf or a ui framework that you are wrapping(you must include them only in dev and peer dependencies). Following this rule will avoid duplication of dependencies in the final product.

For this reason, instead of importing the parent descriptor from vrf (which is only valid in the final product), you need to use the vrfParent key in your component.

<template>

<input type="text" v-model="$value" />

</template>

<script>

export default {
  vrfParent: 'input'
}

</script>

If you need a basic implementation of element from core - use $vrfParent

<template>

<button @onClick="onClick" v-if="someCondition">{{humanName}}</button>
<component :is="$vrfParent" v-else />

</template>

<script>

export default {
  vrfParent: 'action',
  computed: {
    someCondition() {
      ...
    }
  }
}

</script>

After all, add your adapter to vrf

import VrfAdapterName from './vrf-adapter-name'

Vue.use(Vrf, {
  adapters: [
    VrfAdapterName
  ]
})

Adapters

Effects

Planned

  • Clientside validations
0.98.2-alpha.4

1 month ago

0.98.2-alpha.5

1 month ago

0.98.2-alpha.3

1 month ago

0.98.2-alpha

1 month ago

0.98.2-alpha.2

1 month ago

0.98.2-alpha.1

1 month ago

0.98.1

4 months ago

0.98.2

4 months ago

0.97.0

10 months ago

0.98.0

7 months ago

0.95.0

12 months ago

0.95.0-events-order

10 months ago

0.96.0

10 months ago

0.85.0

1 year ago

0.81.0

1 year ago

0.78.0

1 year ago

0.74.0

1 year ago

0.93.0

1 year ago

0.93.1

1 year ago

0.86.0

1 year ago

0.82.0

1 year ago

0.79.0

1 year ago

0.90.0

1 year ago

0.75.0

1 year ago

0.94.0

1 year ago

0.88.0-alpha

1 year ago

0.87.0

1 year ago

0.83.0

1 year ago

0.76.0

1 year ago

0.91.0

1 year ago

0.91.1

1 year ago

0.91.2

1 year ago

0.91.3

1 year ago

0.88.0

1 year ago

0.84.0

1 year ago

0.80.0

1 year ago

0.77.0

1 year ago

0.73.0

1 year ago

0.92.0

1 year ago

0.89.0

1 year ago

0.62.0

1 year ago

0.59.0

1 year ago

0.55.0

2 years ago

0.70.0

1 year ago

0.63.0

1 year ago

0.56.0

2 years ago

0.71.0

1 year ago

0.68.0

1 year ago

0.64.0

1 year ago

0.60.1

1 year ago

0.60.0

1 year ago

0.57.0

2 years ago

0.64.0-alpha.0

1 year ago

0.72.0

1 year ago

0.69.0

1 year ago

0.61.1

1 year ago

0.65.0

1 year ago

0.61.0

1 year ago

0.58.0

1 year ago

0.66.0

1 year ago

0.52.1

2 years ago

0.53.0

2 years ago

0.54.0

2 years ago

0.51.6

2 years ago

0.51.4

2 years ago

0.51.5

2 years ago

0.51.2

2 years ago

0.51.3

2 years ago

0.51.1

2 years ago

0.52.0

2 years ago

0.51.0

2 years ago

0.50.1-effects.9

2 years ago

0.50.1-effects.5

2 years ago

0.50.1-effects.6

2 years ago

0.50.1-effects.7

2 years ago

0.50.1-effects.8

2 years ago

0.50.1-effects.1

2 years ago

0.50.1-effects.2

2 years ago

0.50.1-effects.3

2 years ago

0.50.1-effects.4

2 years ago

0.50.1-effects.0

2 years ago

0.48.0

3 years ago

0.49.0

3 years ago

0.46.0

3 years ago

0.50.0

3 years ago

0.47.0

3 years ago

0.45.0

3 years ago

0.44.0

3 years ago

0.43.1

3 years ago

0.43.0

3 years ago

0.42.1

3 years ago

0.41.0

3 years ago

0.42.0

3 years ago

0.40.2

3 years ago

0.40.3

3 years ago

0.40.0

3 years ago

0.40.1

3 years ago

0.39.1

3 years ago

0.39.0

3 years ago

0.38.4

3 years ago

0.38.2

3 years ago

0.38.1

3 years ago

0.38.0

3 years ago

0.38.3

3 years ago

0.37.0

3 years ago

0.36.4

3 years ago

0.36.3

3 years ago

0.36.2

3 years ago

0.36.7

3 years ago

0.36.6

3 years ago

0.36.5

3 years ago

0.36.0

3 years ago

0.35.5

3 years ago

0.35.4

3 years ago

0.35.3

3 years ago

0.35.6

3 years ago

0.35.2

3 years ago

0.34.1

3 years ago

0.34.0

3 years ago

0.33.5

3 years ago

0.33.4

3 years ago

0.35.1

3 years ago

0.33.3

3 years ago

0.35.0

3 years ago

0.33.2

3 years ago

0.33.1

3 years ago

0.33.0

3 years ago

0.32.1

3 years ago

0.32.0

3 years ago

0.30.0

3 years ago

0.31.0

3 years ago

0.29.1

3 years ago

0.29.0

3 years ago

0.28.2

3 years ago

0.28.1

3 years ago

0.28.0

3 years ago

0.27.0

4 years ago

0.26.0

4 years ago

0.25.0

4 years ago

0.23.0

4 years ago

0.24.0

4 years ago

0.22.0

4 years ago

0.21.0

4 years ago

0.20.0

4 years ago

0.19.0

4 years ago

0.17.0

4 years ago

0.18.0

4 years ago

0.16.0

4 years ago

0.16.1

4 years ago

0.15.0

4 years ago

0.14.0

4 years ago

0.11.0

4 years ago

0.12.0

4 years ago

0.13.0

4 years ago

0.9.3

4 years ago

0.10.0

4 years ago

0.9.2

4 years ago

0.9.0

4 years ago

0.8.1

4 years ago

0.9.1

4 years ago

0.8.0

4 years ago

0.7.4

4 years ago

0.7.3

4 years ago

0.7.2

4 years ago

0.7.1

4 years ago

0.7.0

4 years ago

0.5.0

4 years ago

0.6.1

4 years ago

0.6.0

4 years ago

0.5.1

4 years ago

0.3.5

4 years ago

0.4.1

4 years ago

0.4.0

4 years ago

0.3.4

4 years ago

0.3.3

4 years ago

0.3.0

4 years ago

0.3.2

4 years ago

0.3.1

4 years ago

0.2.2

4 years ago

0.2.1

4 years ago

0.2.0

4 years ago

0.1.2

4 years ago

0.1.0

4 years ago

0.1.1

4 years ago

0.0.6

4 years ago

0.0.5

4 years ago

0.0.4

4 years ago

0.0.3

4 years ago

0.0.2

4 years ago

0.0.1

4 years ago