Initial commit of the version 2 rewrite
This commit is contained in:
parent
d3a1aea436
commit
27bd2bda26
18
.babelrc
18
.babelrc
@ -1,18 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
"stage-2",
|
||||
["env", {
|
||||
"browsers": "ie >= 11"
|
||||
}]
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"transform-runtime", {
|
||||
"helpers": false,
|
||||
"polyfill": false,
|
||||
"regenerator": true,
|
||||
"moduleName": "babel-runtime"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
1
.cache/04/3a4f461d9fc91ccb6102017dbce1c9.json
Normal file
1
.cache/04/3a4f461d9fc91ccb6102017dbce1c9.json
Normal file
@ -0,0 +1 @@
|
||||
{"id":"FormulateInput.js","dependencies":[{"name":"/Users/justinschroeder/Projects/vue-formulate/package.json","includedInParent":true,"mtime":1570029289731}],"generated":{"js":"var template;\ntemplate += '<div class=\"formulate-input\"></div>';\nmodule.exports = {\n template: template\n};"},"sourceMaps":{"js":{"mappings":[{"generated":{"line":1,"column":0},"source":"FormulateInput.js","original":{"line":1,"column":0}},{"name":"template","generated":{"line":1,"column":4},"source":"FormulateInput.js","original":{"line":1,"column":4}},{"generated":{"line":1,"column":12},"source":"FormulateInput.js","original":{"line":1,"column":0}},{"name":"template","generated":{"line":2,"column":0},"source":"FormulateInput.js","original":{"line":2,"column":0}},{"generated":{"line":2,"column":8},"source":"FormulateInput.js","original":{"line":2,"column":8}},{"generated":{"line":2,"column":12},"source":"FormulateInput.js","original":{"line":2,"column":12}},{"generated":{"line":2,"column":49},"source":"FormulateInput.js","original":{"line":2,"column":0}},{"name":"module","generated":{"line":3,"column":0},"source":"FormulateInput.js","original":{"line":4,"column":0}},{"generated":{"line":3,"column":6},"source":"FormulateInput.js","original":{"line":4,"column":6}},{"name":"exports","generated":{"line":3,"column":7},"source":"FormulateInput.js","original":{"line":4,"column":7}},{"generated":{"line":3,"column":14},"source":"FormulateInput.js","original":{"line":4,"column":0}},{"generated":{"line":3,"column":17},"source":"FormulateInput.js","original":{"line":4,"column":17}},{"name":"template","generated":{"line":4,"column":0},"source":"FormulateInput.js","original":{"line":5,"column":2}},{"name":"template","generated":{"line":4,"column":2},"source":"FormulateInput.js","original":{"line":5,"column":2}},{"generated":{"line":4,"column":10},"source":"FormulateInput.js","original":{"line":5,"column":10}},{"name":"template","generated":{"line":4,"column":12},"source":"FormulateInput.js","original":{"line":5,"column":12}},{"generated":{"line":5,"column":0},"source":"FormulateInput.js","original":{"line":4,"column":17}},{"generated":{"line":5,"column":1},"source":"FormulateInput.js","original":{"line":4,"column":0}}],"sources":{"FormulateInput.js":"var template\ntemplate += '<div class=\"formulate-input\"></div>'\n\nmodule.exports = {\n template: template\n}\n"},"lineCount":null}},"error":null,"hash":"22dfb709434432cfadc29ec640064600","cacheData":{"env":{}}}
|
1
.cache/05/caa36fd98cf97e55fa7bfd3719dbdd.json
Normal file
1
.cache/05/caa36fd98cf97e55fa7bfd3719dbdd.json
Normal file
File diff suppressed because one or more lines are too long
1
.cache/12/735df070acd1ce86c5aed445df4bf1.json
Normal file
1
.cache/12/735df070acd1ce86c5aed445df4bf1.json
Normal file
@ -0,0 +1 @@
|
||||
{"id":"../node_modules/isobject/index.js","dependencies":[{"name":"/Users/justinschroeder/Projects/vue-formulate/package.json","includedInParent":true,"mtime":1570029289731},{"name":"/Users/justinschroeder/Projects/vue-formulate/node_modules/isobject/package.json","includedInParent":true,"mtime":1570025459931}],"generated":{"js":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = isObject;\n\n/*!\n * isobject <https://github.com/jonschlinkert/isobject>\n *\n * Copyright (c) 2014-2017, Jon Schlinkert.\n * Released under the MIT License.\n */\nfunction isObject(val) {\n return val != null && typeof val === 'object' && Array.isArray(val) === false;\n}\n\n;"},"sourceMaps":{"js":{"mappings":[{"generated":{"line":8,"column":0},"source":"../node_modules/isobject/index.js","original":{"line":1,"column":0}},{"generated":{"line":14,"column":0},"source":"../node_modules/isobject/index.js","original":{"line":8,"column":15}},{"name":"isObject","generated":{"line":14,"column":9},"source":"../node_modules/isobject/index.js","original":{"line":8,"column":24}},{"generated":{"line":14,"column":17},"source":"../node_modules/isobject/index.js","original":{"line":8,"column":15}},{"name":"val","generated":{"line":14,"column":18},"source":"../node_modules/isobject/index.js","original":{"line":8,"column":33}},{"generated":{"line":14,"column":21},"source":"../node_modules/isobject/index.js","original":{"line":8,"column":15}},{"generated":{"line":14,"column":23},"source":"../node_modules/isobject/index.js","original":{"line":8,"column":38}},{"generated":{"line":15,"column":0},"source":"../node_modules/isobject/index.js","original":{"line":9,"column":2}},{"name":"val","generated":{"line":15,"column":9},"source":"../node_modules/isobject/index.js","original":{"line":9,"column":9}},{"generated":{"line":15,"column":12},"source":"../node_modules/isobject/index.js","original":{"line":9,"column":12}},{"generated":{"line":15,"column":16},"source":"../node_modules/isobject/index.js","original":{"line":9,"column":16}},{"generated":{"line":15,"column":20},"source":"../node_modules/isobject/index.js","original":{"line":9,"column":9}},{"generated":{"line":15,"column":24},"source":"../node_modules/isobject/index.js","original":{"line":9,"column":24}},{"name":"val","generated":{"line":15,"column":31},"source":"../node_modules/isobject/index.js","original":{"line":9,"column":31}},{"generated":{"line":15,"column":34},"source":"../node_modules/isobject/index.js","original":{"line":9,"column":24}},{"generated":{"line":15,"column":39},"source":"../node_modules/isobject/index.js","original":{"line":9,"column":39}},{"generated":{"line":15,"column":47},"source":"../node_modules/isobject/index.js","original":{"line":9,"column":9}},{"name":"Array","generated":{"line":15,"column":51},"source":"../node_modules/isobject/index.js","original":{"line":9,"column":51}},{"generated":{"line":15,"column":56},"source":"../node_modules/isobject/index.js","original":{"line":9,"column":56}},{"name":"isArray","generated":{"line":15,"column":57},"source":"../node_modules/isobject/index.js","original":{"line":9,"column":57}},{"generated":{"line":15,"column":64},"source":"../node_modules/isobject/index.js","original":{"line":9,"column":51}},{"name":"val","generated":{"line":15,"column":65},"source":"../node_modules/isobject/index.js","original":{"line":9,"column":65}},{"generated":{"line":15,"column":68},"source":"../node_modules/isobject/index.js","original":{"line":9,"column":51}},{"generated":{"line":15,"column":74},"source":"../node_modules/isobject/index.js","original":{"line":9,"column":74}},{"generated":{"line":15,"column":79},"source":"../node_modules/isobject/index.js","original":{"line":9,"column":2}},{"generated":{"line":16,"column":0},"source":"../node_modules/isobject/index.js","original":{"line":10,"column":1}},{"generated":{"line":18,"column":0},"source":"../node_modules/isobject/index.js","original":{"line":10,"column":1}}],"sources":{"../node_modules/isobject/index.js":"/*!\n * isobject <https://github.com/jonschlinkert/isobject>\n *\n * Copyright (c) 2014-2017, Jon Schlinkert.\n * Released under the MIT License.\n */\n\nexport default function isObject(val) {\n return val != null && typeof val === 'object' && Array.isArray(val) === false;\n};\n"},"lineCount":null}},"error":null,"hash":"6f384709c2688dd9587aa95ad5f2aadc","cacheData":{"env":{}}}
|
1
.cache/1e/78c811898ee87048ab5f0f61513488.json
Normal file
1
.cache/1e/78c811898ee87048ab5f0f61513488.json
Normal file
File diff suppressed because one or more lines are too long
1
.cache/68/46c7a8bab070f5c3cc5044dd457250.json
Normal file
1
.cache/68/46c7a8bab070f5c3cc5044dd457250.json
Normal file
File diff suppressed because one or more lines are too long
1
.cache/79/174177a894587743e66f3567d8a6a4.json
Normal file
1
.cache/79/174177a894587743e66f3567d8a6a4.json
Normal file
@ -0,0 +1 @@
|
||||
{"id":"2368","dependencies":[{"name":"/Users/justinschroeder/Projects/vue-formulate/package.json","includedInParent":true,"mtime":1570038445966},{"name":"/Users/justinschroeder/Projects/vue-formulate/node_modules/isobject/package.json","includedInParent":true,"mtime":1570025459931}],"generated":{"js":"\"use strict\";function e(e){return null!=e&&\"object\"==typeof e&&!1===Array.isArray(e)}Object.defineProperty(exports,\"__esModule\",{value:!0}),exports.default=e;"},"sourceMaps":{"js":{"mappings":[{"source":"../node_modules/isobject/index.js","original":{"line":10,"column":1},"generated":{"line":1,"column":0}},{"source":"../node_modules/isobject/index.js","original":{"line":8,"column":15},"generated":{"line":1,"column":13}},{"source":"../node_modules/isobject/index.js","name":"isObject","original":{"line":8,"column":24},"generated":{"line":1,"column":22}},{"source":"../node_modules/isobject/index.js","name":"val","original":{"line":8,"column":33},"generated":{"line":1,"column":24}},{"source":"../node_modules/isobject/index.js","name":"val","original":{"line":9,"column":9},"generated":{"line":1,"column":27}},{"source":"../node_modules/isobject/index.js","original":{"line":9,"column":16},"generated":{"line":1,"column":34}},{"source":"../node_modules/isobject/index.js","name":"val","original":{"line":9,"column":9},"generated":{"line":1,"column":40}},{"source":"../node_modules/isobject/index.js","original":{"line":9,"column":39},"generated":{"line":1,"column":43}},{"source":"../node_modules/isobject/index.js","name":"val","original":{"line":9,"column":31},"generated":{"line":1,"column":60}},{"source":"../node_modules/isobject/index.js","original":{"line":9,"column":74},"generated":{"line":1,"column":64}},{"source":"../node_modules/isobject/index.js","name":"Array","original":{"line":9,"column":51},"generated":{"line":1,"column":68}},{"source":"../node_modules/isobject/index.js","name":"isArray","original":{"line":9,"column":57},"generated":{"line":1,"column":74}},{"source":"../node_modules/isobject/index.js","name":"val","original":{"line":9,"column":65},"generated":{"line":1,"column":82}},{"source":"../node_modules/isobject/index.js","original":{"line":10,"column":1},"generated":{"line":1,"column":85}},{"source":"../node_modules/isobject/index.js","original":{"line":10,"column":1},"generated":{"line":1,"column":92}},{"source":"../node_modules/isobject/index.js","original":{"line":10,"column":1},"generated":{"line":1,"column":107}},{"source":"../node_modules/isobject/index.js","original":{"line":10,"column":1},"generated":{"line":1,"column":115}},{"source":"../node_modules/isobject/index.js","original":{"line":10,"column":1},"generated":{"line":1,"column":128}},{"source":"../node_modules/isobject/index.js","original":{"line":10,"column":1},"generated":{"line":1,"column":129}},{"source":"../node_modules/isobject/index.js","original":{"line":10,"column":1},"generated":{"line":1,"column":136}},{"source":"../node_modules/isobject/index.js","original":{"line":10,"column":1},"generated":{"line":1,"column":140}},{"source":"../node_modules/isobject/index.js","original":{"line":10,"column":1},"generated":{"line":1,"column":148}},{"source":"../node_modules/isobject/index.js","original":{"line":10,"column":1},"generated":{"line":1,"column":156}}],"sources":{"../node_modules/isobject/index.js":"/*!\n * isobject <https://github.com/jonschlinkert/isobject>\n *\n * Copyright (c) 2014-2017, Jon Schlinkert.\n * Released under the MIT License.\n */\n\nexport default function isObject(val) {\n return val != null && typeof val === 'object' && Array.isArray(val) === false;\n};\n"},"lineCount":null}},"error":null,"hash":"f6e2f7389bba4628636f1d49740fe136","cacheData":{"env":{}}}
|
1
.cache/dc/b972228de3c56ec1eaff4844cbdf48.json
Normal file
1
.cache/dc/b972228de3c56ec1eaff4844cbdf48.json
Normal file
@ -0,0 +1 @@
|
||||
{"id":"vLuo","dependencies":[],"generated":{"js":"\n(function(){var t=exports.default||module.exports;\"function\"==typeof t&&(t=t.options),Object.assign(t,{render:function(){var t=this.$createElement;return(this._self._c||t)(\"div\",{staticClass:\"formulate-input\"},[this._v(\" Formulate input!\\n\")])},staticRenderFns:[],_compiled:!0,_scopeId:null,functional:void 0});})();"},"sourceMaps":null,"error":null,"hash":"376c05db7b07c17660e31aef0b2213fa","cacheData":{}}
|
1
.cache/df/ed18e086c94069c456e7803ef4a863.json
Normal file
1
.cache/df/ed18e086c94069c456e7803ef4a863.json
Normal file
File diff suppressed because one or more lines are too long
1
.cache/e6/435a82638fb431be5676d3df0ad545.json
Normal file
1
.cache/e6/435a82638fb431be5676d3df0ad545.json
Normal file
File diff suppressed because one or more lines are too long
1
.cache/f1/bfa45b351fd450a7372dcfb2e94a6c.json
Normal file
1
.cache/f1/bfa45b351fd450a7372dcfb2e94a6c.json
Normal file
@ -0,0 +1 @@
|
||||
{"id":"y3nB","dependencies":[{"name":"/Users/justinschroeder/Projects/vue-formulate/package.json","includedInParent":true,"mtime":1570038445966}],"generated":{"js":"var e;e+='<div class=\"formulate-input\">hi!</div>',module.exports={template:e};"},"sourceMaps":{"js":{"mappings":[{"source":"FormulateInput.js","original":{"line":1,"column":0},"generated":{"line":1,"column":0}},{"source":"FormulateInput.js","name":"template","original":{"line":1,"column":4},"generated":{"line":1,"column":4}},{"source":"FormulateInput.js","name":"template","original":{"line":2,"column":0},"generated":{"line":1,"column":6}},{"source":"FormulateInput.js","original":{"line":2,"column":12},"generated":{"line":1,"column":9}},{"source":"FormulateInput.js","name":"module","original":{"line":4,"column":0},"generated":{"line":1,"column":50}},{"source":"FormulateInput.js","name":"exports","original":{"line":4,"column":7},"generated":{"line":1,"column":57}},{"source":"FormulateInput.js","original":{"line":4,"column":17},"generated":{"line":1,"column":65}},{"source":"FormulateInput.js","name":"template","original":{"line":5,"column":2},"generated":{"line":1,"column":66}},{"source":"FormulateInput.js","name":"template","original":{"line":5,"column":12},"generated":{"line":1,"column":75}}],"sources":{"FormulateInput.js":"var template\ntemplate += '<div class=\"formulate-input\">hi!</div>'\n\nmodule.exports = {\n template: template\n}\n"},"lineCount":null}},"error":null,"hash":"fc879b8fcc134381138418e21efeb502","cacheData":{"env":{}}}
|
@ -1 +1,2 @@
|
||||
dist/*
|
||||
test/*
|
||||
|
26
.eslintrc.js
26
.eslintrc.js
@ -1,22 +1,22 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
node: true
|
||||
},
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint'
|
||||
parser: 'babel-eslint',
|
||||
sourceType: 'module'
|
||||
},
|
||||
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
|
||||
extends: [
|
||||
'standard',
|
||||
// https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
|
||||
// consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
|
||||
'plugin:vue/recommended',
|
||||
],
|
||||
// required to lint *.vue files
|
||||
plugins: [
|
||||
'vue'
|
||||
'plugin:vue/recommended'
|
||||
],
|
||||
env: {
|
||||
browser: true,
|
||||
},
|
||||
// add your custom rules here
|
||||
rules: {}
|
||||
'rules': {
|
||||
// allow paren-less arrow functions
|
||||
'arrow-parens': 0,
|
||||
// allow debugger during development
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
|
||||
}
|
||||
}
|
||||
|
227
README.md
227
README.md
@ -1,228 +1,5 @@
|
||||
# Vue Formulate
|
||||
---------------
|
||||
[![Build Status](https://travis-ci.org/wearebraid/vue-formulate.svg?branch=master)](https://travis-ci.org/wearebraid/vue-formulate)
|
||||
[![Current Version](https://img.shields.io/npm/v/vue-formulate.svg)](https://www.npmjs.com/package/vue-formulate)
|
||||
[![License](https://img.shields.io/github/license/wearebraid/vue-formulate.svg)](https://github.com/wearebraid/vue-formulate/blob/master/LICENSE.txt)
|
||||
[![Build Status](https://travis-ci.com/wearebraid/vue-formulate-next.svg?token=4eHp5aiDcHwjrb1T8zpy&branch=version-2)](https://travis-ci.com/wearebraid/vue-formulate-next)
|
||||
|
||||
### What is it?
|
||||
|
||||
Vue Formulate is a [Vue](https://vuejs.org/) plugin that exposes an elegant
|
||||
mechanism for building and validating forms with a centralized data store.
|
||||
|
||||
### Show and tell
|
||||
|
||||
You'll find an easy to use example, in [the example directory](https://github.com/wearebraid/vue-formulate/tree/master/example)
|
||||
as well as a live demo available at: [demo.vueformulate.com](https://demo.vueformulate.com).
|
||||
|
||||
### Get Started
|
||||
|
||||
#### Download
|
||||
First download the `vue-formulate` package from npm:
|
||||
|
||||
```sh
|
||||
npm install vue-formulate
|
||||
```
|
||||
|
||||
If you want to use ES6 features in your project
|
||||
(and this readme assumes you do), then you'll also
|
||||
need Babel:
|
||||
|
||||
```sh
|
||||
babel-preset-env
|
||||
babel-preset-stage-2
|
||||
```
|
||||
|
||||
Many Vue/Vuex projects require Babel’s `stage-2` preset.
|
||||
Best practice is to include a `.babelrc` in the project
|
||||
root:
|
||||
|
||||
```sh
|
||||
{
|
||||
"presets": [
|
||||
["env", { "modules": false }],
|
||||
"stage-2"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
#### Installation
|
||||
|
||||
Install `vue-formulate` like any other vue plugin:
|
||||
|
||||
```js
|
||||
import Vue from 'vue'
|
||||
import formulate from 'vue-formulate'
|
||||
|
||||
Vue.use(formulate)
|
||||
```
|
||||
#### Vuex
|
||||
`vue-formulate` needs to be linked to your vuex store. Vuex can be
|
||||
configured as a single root store, or as namespaced modules and `vue-formualte`
|
||||
can work with either setup.
|
||||
|
||||
**Vuex Module**
|
||||
|
||||
```js
|
||||
import {formulateModule} from 'vue-formulate'
|
||||
|
||||
export default formulateModule('namespace')
|
||||
```
|
||||
|
||||
Using a namespaced vuex module is the recommended installation method. Just be
|
||||
sure to replace `'namespace'` with the namespace of your vuex module.
|
||||
|
||||
Additionally, when using a vuex namespace, you _must_ also pass the namespace
|
||||
in the Vue plugin installation call:
|
||||
|
||||
```js
|
||||
Vue.use(formulate, {vuexModule: 'namespace'})
|
||||
```
|
||||
|
||||
Alternatively, you can install `vue-formulate`'s store elements to your vuex
|
||||
root store:
|
||||
|
||||
**Root Store**
|
||||
|
||||
```js
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import {formulateState, formulateGetters, formulateMutations} from 'vue-formulate'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
const state = () => ({
|
||||
// your own state data can live next to vue-formulate's data
|
||||
// Note: formulateState is a curried function.
|
||||
your: 'data',
|
||||
...formulateState()()
|
||||
})
|
||||
|
||||
const getters = {
|
||||
// Your own getters can live next to vue-formulate's getters
|
||||
yourGetter (state) {
|
||||
return state.your
|
||||
},
|
||||
...formulateGetters()
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
// Your own mutations can live next to vue-formulate's mutations
|
||||
setYour (state, payload) {
|
||||
state.your = payload
|
||||
},
|
||||
...formulateMutations()
|
||||
}
|
||||
|
||||
export default new Vuex.Store({
|
||||
state,
|
||||
getters,
|
||||
mutations
|
||||
})
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
`vue-formulate` automatically registers two components `formulate` and
|
||||
`formulate-element`. These two elements are able to address most of your form
|
||||
building needs. Here's a simple example:
|
||||
|
||||
```html
|
||||
<formulate name="registration">
|
||||
<formulate-element
|
||||
name="email"
|
||||
type="email"
|
||||
/>
|
||||
...more formulate-elements
|
||||
</formulate>
|
||||
```
|
||||
|
||||
You can think of `<formulate>` elements a little bit like traditional
|
||||
`<form>` tags. You _must_ wrap your `formulate-element` components
|
||||
in a `<formulate>` component. The `formulate` component has a single
|
||||
required prop `name` which creates the form’s key in the vuex store.
|
||||
|
||||
All `formulate-element` components nested inside a `<formulate>`
|
||||
component will automatically commit mutations directly to the
|
||||
store. The store becomes a live representation of all your form’s
|
||||
values.
|
||||
|
||||
The `formulate-element` component is a powerful component which handles field
|
||||
generation.
|
||||
|
||||
### Validation Rules
|
||||
|
||||
There are several built-in validation methods and you can easily add your own as well.
|
||||
|
||||
Rule | Arguments
|
||||
----------|---------------
|
||||
required | *none*
|
||||
email | *none*
|
||||
confirmed | confirmation field
|
||||
number | *none*
|
||||
|
||||
You can add as many validation rules as you want to each `formulate-element`,
|
||||
simply chain your rules with pipes `|'. Additional arguments can be passed to
|
||||
validation rules by using parenthesis after the rule name:
|
||||
|
||||
```
|
||||
validation="required|confirmed(confirmation_field)"
|
||||
```
|
||||
|
||||
The field label used in built-in validation methods is the `validation-label`
|
||||
attribute on your `formulate-element`. If no `validation-label` is found then
|
||||
the `label` attribute is used, and if no `label` attribute is found it will
|
||||
fall back to the field’s `name` attribute (which is required).
|
||||
|
||||
#### Custom Validation Rules
|
||||
|
||||
Validation rules are easy to write! They're just simple functions that are
|
||||
always passed at least one argument, an object containing the `field` name,
|
||||
`value` of the field, validation `label`, `error` function to generate an error
|
||||
message, and an object containing all the `values` for the entire form.
|
||||
|
||||
Additionally, validation rules can pass an unlimited number of extra arguments.
|
||||
These arguments are passed as the 2nd-nth arguments to the validation rule.
|
||||
Their values are parsed from the optional parenthesis in the validation
|
||||
attribute on the `formulate-element`.
|
||||
|
||||
```html
|
||||
<formulate-element
|
||||
type="password"
|
||||
name="password"
|
||||
label="Password"
|
||||
validation="confirmed(password_confirmation_field)"
|
||||
/>
|
||||
```
|
||||
|
||||
Validation rules should return an error message string if they failed, or
|
||||
`false` if the input data is valid.
|
||||
|
||||
Adding your own validation rules is easy. Just pass an additional object
|
||||
of rule functions in the plugin’s installation call:
|
||||
|
||||
```js
|
||||
Vue.use(formulate, {
|
||||
rules: {
|
||||
isPizza ({field, value, error, values, label}) {
|
||||
return value === 'pizza' ? false : `label is not pizza.`
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Styling
|
||||
|
||||
Absolutely zero styles are included so feel free to write your own! The
|
||||
`form-element` components have a wrapper `div` that receives the following
|
||||
classes:
|
||||
|
||||
```
|
||||
formulate-element
|
||||
formulate-element--has-value
|
||||
formulate-element--has-errors
|
||||
```
|
||||
|
||||
### Full Documentation
|
||||
|
||||
There are many more options available, more documentation coming soon.
|
||||
Documentation development is taking place on the [vueformulate.com](https://github.com/wearebraid/vueformulate.com/tree/version-2) repository.
|
||||
|
13
babel.config.js
Normal file
13
babel.config.js
Normal file
@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
test: {
|
||||
presets: [
|
||||
[ '@babel/preset-env', {
|
||||
targets: {
|
||||
node: 'current'
|
||||
}
|
||||
} ]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
24
build/rollup.config.js
Normal file
24
build/rollup.config.js
Normal file
@ -0,0 +1,24 @@
|
||||
import commonjs from 'rollup-plugin-commonjs' // Convert CommonJS modules to ES6
|
||||
import vue from 'rollup-plugin-vue' // Handle .vue SFC files
|
||||
import buble from 'rollup-plugin-buble' // Transpile/polyfill with reasonable browser support
|
||||
|
||||
export default {
|
||||
input: 'src/Formulate.js', // Path relative to package.json
|
||||
output: {
|
||||
name: 'Formulate',
|
||||
exports: 'named',
|
||||
globals: {
|
||||
'is-plain-object': 'isPlainObject',
|
||||
'nanoid': 'nanoid'
|
||||
}
|
||||
},
|
||||
external: ['is-plain-object', 'nanoid'],
|
||||
plugins: [
|
||||
commonjs(),
|
||||
vue({
|
||||
css: true, // Dynamically inject css as a <style> tag
|
||||
compileTemplate: true // Explicitly convert template to render function
|
||||
}),
|
||||
buble() // Transpile to ES5
|
||||
]
|
||||
}
|
1308
dist/formulate.esm.js
vendored
Normal file
1308
dist/formulate.esm.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1315
dist/formulate.min.js
vendored
Normal file
1315
dist/formulate.min.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
dist/formulate.min.js.map
vendored
Normal file
1
dist/formulate.min.js.map
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../node_modules/isobject/index.js","../node_modules/is-plain-object/index.js","Formulate.js"],"names":["isObject","val","Array","isArray","isObjectObject","o","Object","prototype","toString","call","isPlainObject","ctor","prot","constructor","hasOwnProperty","require","FormulateInput","Formulate","defaults","components","install","Vue","options","componentName","$formulate","extend","component","base","extendWith","merged","key","prop","module","exports"],"mappings":";AASC,aAFc,SAASA,EAASC,GACxBA,OAAO,MAAPA,GAA8B,iBAARA,IAA2C,IAAvBC,MAAMC,QAAQF,GAChE,OAAA,eAAA,QAAA,aAAA,CAAA,OAAA,IAAA,QAAA,QAAA;;ACyBA,aAAA,OAAA,eAAA,QAAA,aAAA,CAAA,OAAA,IAAA,QAAA,QAAA,EA3BD,IAAA,EAAA,EAAA,QAAA,aA2BC,SAAA,EAAA,GAAA,OAAA,GAAA,EAAA,WAAA,EAAA,CAAA,QAAA,GAzBD,SAASG,EAAeC,GACf,OAAgB,KAAhB,EAASA,EAAAA,SAAAA,IAC2B,oBAAtCC,OAAOC,UAAUC,SAASC,KAAKJ,GAGvB,SAASK,EAAcL,GAChCM,IAAAA,EAAKC,EAELR,OAAsB,IAAtBA,EAAeC,KAIC,mBADpBM,EAAON,EAAEQ,gBAKoB,IAAzBT,EADJQ,EAAOD,EAAKJ,aAIiC,IAAzCK,EAAKE,eAAe;;;;;AC5B1B,IAAIJ,EAAgBK,QAAQ,mBACxBC,EAAiBD,QAAQ,wBAKzBE,EAAY,WACTC,KAAAA,SAAW,CACdC,WAAY,CAEVH,eAAgBA,KAQtBC,EAAUV,UAAUa,QAAU,SAAUC,EAAKC,GAGtC,IAAA,IAAIC,KAFTF,EAAId,UAAUiB,WAAa,KACtBF,KAAAA,QAAU,KAAKG,OAAO,KAAKP,SAAUI,GAAW,IAC3B,KAAKA,QAAQH,WACrCE,EAAIK,UAAUH,EAAe,KAAKD,QAAQH,WAAWI,KASzDN,EAAUV,UAAUkB,OAAS,SAAUE,EAAMC,GACvCC,IAAAA,EAAS,GACR,IAAA,IAAIC,KAAOH,EACVC,EAAWd,eAAegB,GAC5BD,EAAOC,GAAOpB,EAAckB,EAAWE,KAASpB,EAAciB,EAAKG,IAC/D,KAAKL,OAAOE,EAAKG,GAAMF,EAAWE,IAClCF,EAAWE,GAEfD,EAAOC,GAAOH,EAAKG,GAGlB,IAAA,IAAIC,KAAQH,EACVC,EAAOf,eAAeiB,KACzBF,EAAOE,GAAQH,EAAWG,IAGvBF,OAAAA,GAGTG,OAAOC,QAAU,IAAIhB","file":"formulate.min.js","sourceRoot":"../src","sourcesContent":["/*!\n * isobject <https://github.com/jonschlinkert/isobject>\n *\n * Copyright (c) 2014-2017, Jon Schlinkert.\n * Released under the MIT License.\n */\n\nexport default function isObject(val) {\n return val != null && typeof val === 'object' && Array.isArray(val) === false;\n};\n","/*!\n * is-plain-object <https://github.com/jonschlinkert/is-plain-object>\n *\n * Copyright (c) 2014-2017, Jon Schlinkert.\n * Released under the MIT License.\n */\n\nimport isObject from 'isobject';\n\nfunction isObjectObject(o) {\n return isObject(o) === true\n && Object.prototype.toString.call(o) === '[object Object]';\n}\n\nexport default function isPlainObject(o) {\n var ctor,prot;\n\n if (isObjectObject(o) === false) return false;\n\n // If has modified constructor\n ctor = o.constructor;\n if (typeof ctor !== 'function') return false;\n\n // If has modified prototype\n prot = ctor.prototype;\n if (isObjectObject(prot) === false) return false;\n\n // If constructor does not have an Object-specific method\n if (prot.hasOwnProperty('isPrototypeOf') === false) {\n return false;\n }\n\n // Most likely a plain Object\n return true;\n};\n","var isPlainObject = require('is-plain-object')\nvar FormulateInput = require('./FormulateInput.vue')\n\n/**\n * The base formulate libary.\n */\nvar Formulate = function () {\n this.defaults = {\n components: {\n // FormulateForm: FormulateForm,\n FormulateInput: FormulateInput\n }\n }\n}\n\n/**\n * Install vue formulate, and register it’s components.\n */\nFormulate.prototype.install = function (Vue, options) {\n Vue.prototype.$formulate = this\n this.options = this.extend(this.defaults, options || {})\n for (var componentName in this.options.components) {\n Vue.component(componentName, this.options.components[componentName])\n }\n}\n\n/**\n * Create a new object by copying properties of base and extendWith.\n * @param {Object} base\n * @param {Object} extendWith\n */\nFormulate.prototype.extend = function (base, extendWith) {\n var merged = {}\n for (var key in base) {\n if (extendWith.hasOwnProperty(key)) {\n merged[key] = isPlainObject(extendWith[key]) && isPlainObject(base[key])\n ? this.extend(base[key], extendWith[key])\n : extendWith[key]\n } else {\n merged[key] = base[key]\n }\n }\n for (var prop in extendWith) {\n if (!merged.hasOwnProperty(prop)) {\n merged[prop] = extendWith[prop]\n }\n }\n return merged\n}\n\nmodule.exports = new Formulate()\n"]}
|
1318
dist/formulate.umd.js
vendored
Normal file
1318
dist/formulate.umd.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
43
dist/index.js
vendored
43
dist/index.js
vendored
File diff suppressed because one or more lines are too long
140
dist/snow.css
vendored
Normal file
140
dist/snow.css
vendored
Normal file
@ -0,0 +1,140 @@
|
||||
.formulate-input {
|
||||
margin-bottom: 2em; }
|
||||
.formulate-input .formulate-input-label {
|
||||
display: block;
|
||||
line-height: 1.5;
|
||||
font-size: .9em;
|
||||
font-weight: 600;
|
||||
margin-bottom: .1em; }
|
||||
.formulate-input .formulate-input-element {
|
||||
max-width: 20em;
|
||||
margin-bottom: .1em; }
|
||||
.formulate-input .formulate-input-help {
|
||||
color: #6d6d6d;
|
||||
font-size: .7em;
|
||||
font-weight: 300;
|
||||
line-height: 1.5;
|
||||
margin-bottom: .25em; }
|
||||
.formulate-input .formulate-input-group-item {
|
||||
margin-bottom: .5em; }
|
||||
.formulate-input:last-child {
|
||||
margin-bottom: 0; }
|
||||
.formulate-input[data-classification='text'] input {
|
||||
appearance: none;
|
||||
border-radius: .3em;
|
||||
border: 1px solid #cecece;
|
||||
box-sizing: border-box;
|
||||
background-color: transparent;
|
||||
font-size: .9em;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
padding: .75em;
|
||||
display: block;
|
||||
width: 100%;
|
||||
font-weight: 400; }
|
||||
.formulate-input[data-classification='text'] input::placeholder {
|
||||
color: #a8a8a8; }
|
||||
.formulate-input[data-classification='text'] input:focus {
|
||||
outline: 0;
|
||||
border: 1px solid #41b883; }
|
||||
.formulate-input[data-classification='textarea'] textarea {
|
||||
appearance: none;
|
||||
border-radius: .3em;
|
||||
border: 1px solid #cecece;
|
||||
box-sizing: border-box;
|
||||
background-color: transparent;
|
||||
font-size: .9em;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
padding: .75em;
|
||||
display: block;
|
||||
width: 100%;
|
||||
font-weight: 400; }
|
||||
.formulate-input[data-classification='textarea'] textarea::placeholder {
|
||||
color: #a8a8a8; }
|
||||
.formulate-input[data-classification='textarea'] textarea:focus {
|
||||
outline: 0;
|
||||
border: 1px solid #41b883; }
|
||||
.formulate-input[data-classification='select'] .formulate-input-element {
|
||||
position: relative; }
|
||||
.formulate-input[data-classification='select'] .formulate-input-element::before {
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: .3em solid transparent;
|
||||
border-top-color: #cecece;
|
||||
border-bottom-width: 0;
|
||||
top: 50%;
|
||||
margin-top: -.1em;
|
||||
right: 1em;
|
||||
position: absolute; }
|
||||
.formulate-input[data-classification='select'] select {
|
||||
appearance: none;
|
||||
border-radius: .3em;
|
||||
border: 1px solid #cecece;
|
||||
box-sizing: border-box;
|
||||
background-color: transparent;
|
||||
font-size: .9em;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
padding: .75em;
|
||||
display: block;
|
||||
width: 100%;
|
||||
font-weight: 400;
|
||||
padding-right: 2em; }
|
||||
.formulate-input[data-classification='select'] select::placeholder {
|
||||
color: #a8a8a8; }
|
||||
.formulate-input[data-classification='select'] select:focus {
|
||||
outline: 0;
|
||||
border: 1px solid #41b883; }
|
||||
.formulate-input[data-classification='select'] select[data-placeholder-selected] {
|
||||
color: #a8a8a8; }
|
||||
.formulate-input[data-classification='box'] .formulate-input-wrapper {
|
||||
display: flex;
|
||||
align-items: center; }
|
||||
.formulate-input[data-classification='box'] .formulate-input-element {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center; }
|
||||
.formulate-input[data-classification='box'] .formulate-input-element input {
|
||||
position: absolute;
|
||||
left: -999px; }
|
||||
.formulate-input[data-classification='box'] .formulate-input-element-decorator {
|
||||
display: block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
border-radius: .25em;
|
||||
border: 1px solid #cecece;
|
||||
position: relative; }
|
||||
.formulate-input[data-classification='box'] .formulate-input-element-decorator::before {
|
||||
content: '';
|
||||
display: block;
|
||||
background-size: contain;
|
||||
background-position: right;
|
||||
width: calc(100% - .125em);
|
||||
height: calc(100% - .125em);
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
top: .0625em;
|
||||
left: .0625em; }
|
||||
.formulate-input[data-classification='box'] .formulate-input-element[data-type="radio"] .formulate-input-element-decorator {
|
||||
border-radius: 1em; }
|
||||
.formulate-input[data-classification='box'] .formulate-input-element[data-type="radio"] .formulate-input-element-decorator::before {
|
||||
border-radius: 1em;
|
||||
width: calc(100% - .5em);
|
||||
height: calc(100% - .5em);
|
||||
top: .25em;
|
||||
left: .25em; }
|
||||
.formulate-input[data-classification='box'] .formulate-input-element input[type="checkbox"]:checked ~ .formulate-input-element-decorator {
|
||||
border-color: #41b883; }
|
||||
.formulate-input[data-classification='box'] .formulate-input-element input[type="checkbox"]:checked ~ .formulate-input-element-decorator::before {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="%2341b883"><path d="M8.76,56.2c-6.38-6.34,3.26-16,9.64-9.69L38,65.88,80.56,23.29c6.38-6.38,16.07,3.32,9.69,9.69L42.84,80.37a6.83,6.83,0,0,1-9.65,0Z"/></svg>'); }
|
||||
.formulate-input[data-classification='box'] .formulate-input-element input[type="radio"]:checked ~ .formulate-input-element-decorator {
|
||||
border-color: #41b883; }
|
||||
.formulate-input[data-classification='box'] .formulate-input-element input[type="radio"]:checked ~ .formulate-input-element-decorator::before {
|
||||
background-color: #41b883; }
|
||||
.formulate-input[data-classification='box'] .formulate-input-element input:focus ~ .formulate-input-element-decorator {
|
||||
border-color: #41b883; }
|
||||
.formulate-input[data-classification='box'] .formulate-input-label--after {
|
||||
margin-left: .5em; }
|
||||
.formulate-input[data-classification='box'] .formulate-input-label--before {
|
||||
margin-right: .5em; }
|
||||
.formulate-input[data-classification="group"] > .formulate-input-wrapper > .formulate-input-label {
|
||||
margin-bottom: .5em; }
|
2
dist/snow.min.css
vendored
Normal file
2
dist/snow.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
12905
package-lock.json
generated
12905
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
88
package.json
88
package.json
@ -1,13 +1,21 @@
|
||||
{
|
||||
"name": "vue-formulate",
|
||||
"version": "0.13.0",
|
||||
"description": "The easiest way to build forms in Vue with validation and vuex support.",
|
||||
"main": "dist/index.js",
|
||||
"version": "2.0.0-alpha.0",
|
||||
"description": "The easiest way to build forms in Vue.",
|
||||
"main": "dist/formulate.umd.js",
|
||||
"module": "dist/formulate.esm.js",
|
||||
"unpkg": "dist/formulate.min.js",
|
||||
"browser": {
|
||||
"./sfc": "src/Formulate.js"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "webpack --hide-modules --watch",
|
||||
"build": "NODE_ENV=production && webpack -p --hide-modules",
|
||||
"test": "ava",
|
||||
"watch": "ava --watch"
|
||||
"build": "npm run build:umd & npm run build:es & npm run build:unpkg & npm run build:css",
|
||||
"build:css": "node-sass themes/snow/snow.scss dist/snow.css && postcss --use autoprefixer -b '> 2%' < dist/snow.css | postcss --use cssnano > dist/snow.min.css",
|
||||
"build:umd": "rollup --config build/rollup.config.js --format umd --file dist/formulate.umd.js",
|
||||
"build:es": "rollup --config build/rollup.config.js --format es --file dist/formulate.esm.js",
|
||||
"build:unpkg": "rollup --config build/rollup.config.js --format iife --file dist/formulate.min.js",
|
||||
"test": "NODE_ENV=test jest --config test/jest.conf.js",
|
||||
"test:watch": "NODE_ENV=test jest --config test/jest.conf.js --watch"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -26,40 +34,42 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/wearebraid/vue-formulate/issues"
|
||||
},
|
||||
"homepage": "https://github.com/wearebraid/vue-formulate#readme",
|
||||
"homepage": "https://www.vueformulate.com",
|
||||
"devDependencies": {
|
||||
"ava": "^0.25.0",
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-eslint": "^8.2.1",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-preset-stage-2": "^6.24.1",
|
||||
"eslint": "^4.16.0",
|
||||
"eslint-config-standard": "^11.0.0-beta.0",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"eslint-plugin-node": "^5.2.1",
|
||||
"eslint-plugin-promise": "^3.6.0",
|
||||
"eslint-plugin-standard": "^3.0.1",
|
||||
"eslint-plugin-vue": "^4.2.0",
|
||||
"vue-loader": "^13.7.0",
|
||||
"vue-template-compiler": "^2.5.13",
|
||||
"webpack": "^3.10.0"
|
||||
},
|
||||
"ava": {
|
||||
"require": [
|
||||
"babel-register",
|
||||
"babel-polyfill"
|
||||
]
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
"env"
|
||||
]
|
||||
"@babel/core": "^7.6.2",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.6.0",
|
||||
"@babel/preset-env": "^7.6.2",
|
||||
"@vue/component-compiler-utils": "^3.0.0",
|
||||
"@vue/test-utils": "^1.0.0-beta.29",
|
||||
"autoprefixer": "^9.6.1",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-jest": "^24.9.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-standard": "^12.0.0",
|
||||
"eslint-plugin-import": "^2.16.0",
|
||||
"eslint-plugin-node": "^8.0.1",
|
||||
"eslint-plugin-promise": "^4.1.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"eslint-plugin-vue": "^5.2.3",
|
||||
"jest": "^24.9.0",
|
||||
"jest-vue-preprocessor": "^1.5.0",
|
||||
"node-sass": "^4.12.0",
|
||||
"postcss": "^7.0.18",
|
||||
"postcss-cli": "^6.1.3",
|
||||
"rollup": "^1.22.0",
|
||||
"rollup-plugin-buble": "^0.19.8",
|
||||
"rollup-plugin-commonjs": "^10.1.0",
|
||||
"rollup-plugin-vue": "^5.0.1",
|
||||
"vue": "^2.6.10",
|
||||
"vue-jest": "^3.0.5",
|
||||
"vue-template-compiler": "^2.6.10",
|
||||
"vue-template-es2015-compiler": "^1.9.1",
|
||||
"watch": "^1.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"clone-deep": "^3.0.1",
|
||||
"shortid": "^2.2.8"
|
||||
"is-plain-object": "^3.0.0",
|
||||
"nanoid": "^2.1.1"
|
||||
}
|
||||
}
|
||||
|
5
src/FormulateForm.vue
Normal file
5
src/FormulateForm.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<form>
|
||||
<slot />
|
||||
</form>
|
||||
</template>
|
105
src/FormulateInput.vue
Normal file
105
src/FormulateInput.vue
Normal file
@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div
|
||||
class="formulate-input"
|
||||
:data-classification="classification"
|
||||
:data-type="type"
|
||||
>
|
||||
<div class="formulate-input-wrapper">
|
||||
<slot
|
||||
v-if="context.label && context.labelPosition === 'before'"
|
||||
name="label"
|
||||
v-bind="context"
|
||||
>
|
||||
<label
|
||||
class="formulate-input-label formulate-input-label--before"
|
||||
:for="id"
|
||||
v-text="context.label"
|
||||
/>
|
||||
</slot>
|
||||
<slot v-bind="context">
|
||||
<component
|
||||
:is="context.component"
|
||||
:context="context"
|
||||
/>
|
||||
</slot>
|
||||
<slot
|
||||
v-if="context.label && context.labelPosition === 'after'"
|
||||
name="label"
|
||||
v-bind="context.label"
|
||||
>
|
||||
<label
|
||||
class="formulate-input-label formulate-input-label--after"
|
||||
:for="id"
|
||||
v-text="context.label"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
<div
|
||||
v-if="help"
|
||||
class="formulate-input-help"
|
||||
v-text="help"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import context from './libs/context'
|
||||
import nanoid from 'nanoid'
|
||||
|
||||
export default {
|
||||
name: 'FormulateInput',
|
||||
inheritAttrs: false,
|
||||
model: {
|
||||
prop: 'formulateValue',
|
||||
event: 'input'
|
||||
},
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
formulateValue: {
|
||||
type: [String, Number, Object, Boolean, Array],
|
||||
default: false
|
||||
},
|
||||
value: {
|
||||
type: [String, Number, Object, Boolean, Array],
|
||||
default: false
|
||||
},
|
||||
options: {
|
||||
type: [Object, Array, Boolean],
|
||||
default: false
|
||||
},
|
||||
optionGroups: {
|
||||
type: [Object, Boolean],
|
||||
default: false
|
||||
},
|
||||
id: {
|
||||
type: [String, Boolean, Number],
|
||||
default: () => nanoid(9)
|
||||
},
|
||||
label: {
|
||||
type: [String, Boolean],
|
||||
default: false
|
||||
},
|
||||
labelPosition: {
|
||||
type: [String, Boolean],
|
||||
default: false
|
||||
},
|
||||
help: {
|
||||
type: [String, Boolean],
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
context,
|
||||
classification () {
|
||||
const classification = this.$formulate.classify(this.type)
|
||||
return (classification === 'box' && this.options) ? 'group' : classification
|
||||
},
|
||||
component () {
|
||||
return this.$formulate.component(this.type)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
40
src/FormulateInputGroup.vue
Normal file
40
src/FormulateInputGroup.vue
Normal file
@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="formulate-input-group">
|
||||
<component
|
||||
:is="optionContext.component"
|
||||
v-for="optionContext in optionsWithContext"
|
||||
:key="optionContext.id"
|
||||
v-model="context.model"
|
||||
v-bind="optionContext"
|
||||
class="formulate-input-group-item"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FormulateInputGroup',
|
||||
props: {
|
||||
context: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
options () {
|
||||
return this.context.options || []
|
||||
},
|
||||
optionsWithContext () {
|
||||
const { options, labelPosition, attributes, ...context } = this.context
|
||||
return this.options.map(option => this.groupItemContext(context, option))
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
groupItemContext (...args) {
|
||||
return Object.assign({}, ...args, {
|
||||
component: 'FormulateInput'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
25
src/FormulateInputMixin.js
Normal file
25
src/FormulateInputMixin.js
Normal file
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Default base for input components.
|
||||
*/
|
||||
export default {
|
||||
props: {
|
||||
context: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
type () {
|
||||
return this.context.type
|
||||
},
|
||||
id () {
|
||||
return this.context.id
|
||||
},
|
||||
attributes () {
|
||||
return this.context.attributes || {}
|
||||
},
|
||||
hasValue () {
|
||||
return !!this.context.model
|
||||
}
|
||||
}
|
||||
}
|
165
src/formulate.js
165
src/formulate.js
@ -1,115 +1,90 @@
|
||||
import FormulateGroup from './components/Formulate'
|
||||
import FormulateElement from './components/FormulateElement'
|
||||
import DefaultRules from './rules'
|
||||
import DefaultErrors from './errors'
|
||||
|
||||
import library from './libs/library'
|
||||
import isPlainObject from 'is-plain-object'
|
||||
import FormulateInput from './FormulateInput.vue'
|
||||
import FormulateForm from './FormulateForm.vue'
|
||||
import FormulateInputGroup from './FormulateInputGroup.vue'
|
||||
import FormulateInputBox from './inputs/FormulateInputBox.vue'
|
||||
import FormulateInputText from './inputs/FormulateInputText.vue'
|
||||
import FormulateInputSelect from './inputs/FormulateInputSelect.vue'
|
||||
import FormulateInputTextArea from './inputs/FormulateInputTextArea.vue'
|
||||
/**
|
||||
* The base formulate library.
|
||||
*/
|
||||
class Formulate {
|
||||
/**
|
||||
* Initialize vue-formulate.
|
||||
* Instantiate our base options.
|
||||
*/
|
||||
constructor () {
|
||||
this.defaultOptions = {
|
||||
registerComponents: true,
|
||||
tags: {
|
||||
Formulate: 'formulate',
|
||||
FormulateElement: 'formulate-element'
|
||||
this.defaults = {
|
||||
components: {
|
||||
FormulateForm,
|
||||
FormulateInput,
|
||||
FormulateInputBox,
|
||||
FormulateInputText,
|
||||
FormulateInputGroup,
|
||||
FormulateInputSelect,
|
||||
FormulateInputTextArea
|
||||
},
|
||||
errors: {},
|
||||
rules: {},
|
||||
vuexModule: false
|
||||
library
|
||||
}
|
||||
this.errors = DefaultErrors
|
||||
this.rules = DefaultRules
|
||||
}
|
||||
|
||||
/**
|
||||
* Install vue-formulate as an instance of Vue.
|
||||
* @param {Vue} Vue
|
||||
* Install vue formulate, and register it’s components.
|
||||
*/
|
||||
install (Vue, options = {}) {
|
||||
install (Vue, options) {
|
||||
Vue.prototype.$formulate = this
|
||||
options = Object.assign(this.defaultOptions, options)
|
||||
if (options.registerComponents) {
|
||||
Vue.component(options.tags.Formulate, FormulateGroup)
|
||||
Vue.component(options.tags.FormulateElement, FormulateElement)
|
||||
this.options = this.extend(this.defaults, options || {})
|
||||
for (var componentName in this.options.components) {
|
||||
Vue.component(componentName, this.options.components[componentName])
|
||||
}
|
||||
if (options.errors) {
|
||||
this.errors = Object.assign(this.errors, options.errors)
|
||||
}
|
||||
if (options.rules) {
|
||||
this.rules = Object.assign(this.rules, options.rules)
|
||||
}
|
||||
this.options = options
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string of rules parse them out to relevant pieces/parts
|
||||
* @param {string} rulesString
|
||||
* Create a new object by copying properties of base and extendWith.
|
||||
* @param {Object} base
|
||||
* @param {Object} extendWith
|
||||
*/
|
||||
parseRules (rulesString) {
|
||||
return rulesString.split('|')
|
||||
.map(rule => rule.trim())
|
||||
.map(rule => rule.match(/([a-zA-Z0-9]+)\((.*)?\)/) || [null, rule, ''])
|
||||
.map(([ruleString, rule, args]) => Object.assign({}, {rule}, args ? {
|
||||
args: args.split(',').map(arg => arg.trim())
|
||||
} : {args: []}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the function that generates a validation error message for a given
|
||||
* validation rule.
|
||||
* @param {string} rule
|
||||
*/
|
||||
errorFactory (rule) {
|
||||
return this.errors[rule] ? this.errors[rule] : this.errors['default']
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a particular field, value, validation rules, and form values
|
||||
* perform asynchronous field validation.
|
||||
* @param {Object} validatee
|
||||
* @param {string} rulesString
|
||||
* @param {Object} values
|
||||
*/
|
||||
async validationErrors ({field, value, label}, rulesString, values) {
|
||||
return rulesString ? Promise.all(
|
||||
this.parseRules(rulesString)
|
||||
.map(({rule, args}) => {
|
||||
if (typeof this.rules[rule] !== 'function') {
|
||||
throw new Error(`Validation rule is invalid: ${rule}`)
|
||||
}
|
||||
return this.rules[rule]({field, value, label, error: this.errorFactory(rule), values}, ...args)
|
||||
})
|
||||
).then(responses => responses.reduce((errors, error) => {
|
||||
return error ? (Array.isArray(errors) ? errors.concat(error) : [error]) : errors
|
||||
}, false)) : false
|
||||
}
|
||||
}
|
||||
const formulate = new Formulate()
|
||||
export default formulate
|
||||
export * from './store'
|
||||
|
||||
/**
|
||||
* Mapper to allow bindings to the vuex store for custom fields.
|
||||
* @param {Object} definitions
|
||||
*/
|
||||
export const mapModels = (definitions) => {
|
||||
const models = {}
|
||||
for (let mapTo in definitions) {
|
||||
let [form, field] = definitions[mapTo].split('/')
|
||||
models[mapTo] = {
|
||||
set (value) {
|
||||
let m = formulate.options.vuexModule ? `${formulate.options.vuexModule}/` : ''
|
||||
this.$store.commit(`${m}setFieldValue`, {form, field, value})
|
||||
},
|
||||
get () {
|
||||
let m = formulate.options.vuexModule ? `${formulate.options.vuexModule}/` : ''
|
||||
if (this.$store.getters[`${m}formValues`][form]) {
|
||||
return this.$store.getters[`${m}formValues`][form][field]
|
||||
}
|
||||
return ''
|
||||
extend (base, extendWith) {
|
||||
var merged = {}
|
||||
for (var key in base) {
|
||||
if (extendWith.hasOwnProperty(key)) {
|
||||
merged[key] = isPlainObject(extendWith[key]) && isPlainObject(base[key])
|
||||
? this.extend(base[key], extendWith[key])
|
||||
: extendWith[key]
|
||||
} else {
|
||||
merged[key] = base[key]
|
||||
}
|
||||
}
|
||||
for (var prop in extendWith) {
|
||||
if (!merged.hasOwnProperty(prop)) {
|
||||
merged[prop] = extendWith[prop]
|
||||
}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine what "class" of input this element is given the "type".
|
||||
* @param {string} type
|
||||
*/
|
||||
classify (type) {
|
||||
if (this.options.library.hasOwnProperty(type)) {
|
||||
return this.options.library[type].classification
|
||||
}
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine what type of component to render given the "type".
|
||||
* @param {string} type
|
||||
*/
|
||||
component (type) {
|
||||
if (this.options.library.hasOwnProperty(type)) {
|
||||
return this.options.library[type].component
|
||||
}
|
||||
return false
|
||||
}
|
||||
return models
|
||||
}
|
||||
|
||||
export default new Formulate()
|
||||
|
26
src/inputs/FormulateInputBox.vue
Normal file
26
src/inputs/FormulateInputBox.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div
|
||||
class="formulate-input-element formulate-input-element--box"
|
||||
:data-type="context.type"
|
||||
>
|
||||
<input
|
||||
v-model="context.model"
|
||||
:type="type"
|
||||
:value="context.value"
|
||||
v-bind="attributes"
|
||||
>
|
||||
<label
|
||||
class="formulate-input-element-decorator"
|
||||
:for="id"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormulateInputMixin from '../FormulateInputMixin'
|
||||
|
||||
export default {
|
||||
name: 'FormulateInputBox',
|
||||
mixins: [FormulateInputMixin]
|
||||
}
|
||||
</script>
|
68
src/inputs/FormulateInputSelect.vue
Normal file
68
src/inputs/FormulateInputSelect.vue
Normal file
@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div
|
||||
class="formulate-input-element formulate-input-element--select"
|
||||
>
|
||||
<select
|
||||
v-model="context.model"
|
||||
v-bind="attributes"
|
||||
:data-placeholder-selected="placeholderSelected"
|
||||
>
|
||||
<option
|
||||
v-if="context.placeholder"
|
||||
value=""
|
||||
disabled
|
||||
:selected="!hasValue"
|
||||
>
|
||||
{{ context.placeholder }}
|
||||
</option>
|
||||
<template
|
||||
v-if="!optionGroups"
|
||||
>
|
||||
<option
|
||||
v-for="option in options"
|
||||
:key="option.id"
|
||||
:value="option.value"
|
||||
v-bind="option.attributes || {}"
|
||||
v-text="option.label"
|
||||
/>
|
||||
</template>
|
||||
<template
|
||||
v-else
|
||||
>
|
||||
<optgroup
|
||||
v-for="(subOptions, label) in optionGroups"
|
||||
:key="label"
|
||||
:label="label"
|
||||
>
|
||||
<option
|
||||
v-for="option in subOptions"
|
||||
:key="option.id"
|
||||
:value="option.value"
|
||||
v-bind="option.attributes || {}"
|
||||
v-text="option.label"
|
||||
/>
|
||||
</optgroup>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormulateInputMixin from '../FormulateInputMixin'
|
||||
|
||||
export default {
|
||||
name: 'FormulateInputSelect',
|
||||
mixins: [FormulateInputMixin],
|
||||
computed: {
|
||||
options () {
|
||||
return this.context.options || {}
|
||||
},
|
||||
optionGroups () {
|
||||
return this.context.optionGroups || false
|
||||
},
|
||||
placeholderSelected () {
|
||||
return !!(!this.hasValue && this.context.attributes.placeholder)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
20
src/inputs/FormulateInputText.vue
Normal file
20
src/inputs/FormulateInputText.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div
|
||||
class="formulate-input-element formulate-input-element--textarea"
|
||||
>
|
||||
<input
|
||||
v-model="context.model"
|
||||
:type="type"
|
||||
v-bind="attributes"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormulateInputMixin from '../FormulateInputMixin'
|
||||
|
||||
export default {
|
||||
name: 'FormulateInputText',
|
||||
mixins: [FormulateInputMixin]
|
||||
}
|
||||
</script>
|
19
src/inputs/FormulateInputTextArea.vue
Normal file
19
src/inputs/FormulateInputTextArea.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div
|
||||
class="formulate-input-element formulate-input-element--textarea"
|
||||
>
|
||||
<textarea
|
||||
v-model="context.model"
|
||||
v-bind="attributes"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormulateInputMixin from '../FormulateInputMixin'
|
||||
|
||||
export default {
|
||||
name: 'FormulateInputTextArea',
|
||||
mixins: [FormulateInputMixin]
|
||||
}
|
||||
</script>
|
118
src/libs/context.js
Normal file
118
src/libs/context.js
Normal file
@ -0,0 +1,118 @@
|
||||
import nanoid from 'nanoid'
|
||||
import { reduce, map } from './utils'
|
||||
|
||||
/**
|
||||
* For a single instance of an input, export all of the context needed to fully
|
||||
* render that element.
|
||||
* @return {object}
|
||||
*/
|
||||
export default function context () {
|
||||
return defineModel.call(this, Object.assign({
|
||||
type: this.type,
|
||||
value: this.value,
|
||||
classification: this.classification,
|
||||
component: this.component,
|
||||
id: this.id,
|
||||
label: this.label,
|
||||
labelPosition: labelPosition.call(this),
|
||||
attributes: attributeReducer.call(this, this.$attrs)
|
||||
}, typeContext.call(this)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Given (this.type), return an object to merge with the context
|
||||
* @return {object}
|
||||
*/
|
||||
function typeContext () {
|
||||
switch (this.classification) {
|
||||
case 'select':
|
||||
return {
|
||||
options: createOptionList(this.options),
|
||||
optionGroups: this.optionGroups ? map(this.optionGroups, (k, v) => createOptionList(v)) : false,
|
||||
placeholder: this.$attrs.placeholder || false
|
||||
}
|
||||
case 'group':
|
||||
if (this.options) {
|
||||
return {
|
||||
options: createOptionList(this.options),
|
||||
component: 'FormulateInputGroup'
|
||||
}
|
||||
}
|
||||
break
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reducer for attributes that will be applied to each core input element.
|
||||
* @return {object}
|
||||
*/
|
||||
function attributeReducer (attributes = {}) {
|
||||
if (this.id) {
|
||||
attributes.id = this.id
|
||||
}
|
||||
return attributes
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the a best-guess location for the label (before or after).
|
||||
* @return {string} before|after
|
||||
*/
|
||||
function labelPosition () {
|
||||
if (this.labelPosition) {
|
||||
return this.labelPosition
|
||||
}
|
||||
switch (this.classification) {
|
||||
case 'box':
|
||||
return 'after'
|
||||
default:
|
||||
return 'before'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an object or array of options, create an array of objects with label,
|
||||
* value, and id.
|
||||
* @param {array|object}
|
||||
* @return {array}
|
||||
*/
|
||||
function createOptionList (options) {
|
||||
if (!Array.isArray(options)) {
|
||||
return reduce(options, (options, value, label) => options.concat({ value, label, id: nanoid(15) }), [])
|
||||
} else if (Array.isArray(options) && !options.length) {
|
||||
return [{ value: this.name, label: (this.label || this.name), id: nanoid(15) }]
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a getter/setter model factory
|
||||
* @return object
|
||||
*/
|
||||
function defineModel (context) {
|
||||
return Object.defineProperty(context, 'model', {
|
||||
get: modelGetter.bind(this),
|
||||
set: modelSetter.bind(this)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value from a model.
|
||||
**/
|
||||
function modelGetter () {
|
||||
if (this.type === 'checkbox' && !Array.isArray(this.formulateValue) && this.options) {
|
||||
return []
|
||||
}
|
||||
if (this.formulateValue === false) {
|
||||
return ''
|
||||
}
|
||||
return this.formulateValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value from a model.
|
||||
**/
|
||||
function modelSetter (value) {
|
||||
this.$emit('input', value)
|
||||
}
|
95
src/libs/library.js
Normal file
95
src/libs/library.js
Normal file
@ -0,0 +1,95 @@
|
||||
export default {
|
||||
// === SINGLE LINE TEXT STYLE INPUTS
|
||||
text: {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
email: {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
number: {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
color: {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
date: {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
hidden: {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
month: {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
password: {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
range: {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
search: {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
tel: {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
time: {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
url: {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
week: {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
'datetime-local': {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
|
||||
// === MULTI LINE TEXT INPUTS
|
||||
textarea: {
|
||||
classification: 'textarea',
|
||||
component: 'FormulateInputTextArea'
|
||||
},
|
||||
|
||||
// === BOX STYLE INPUTS
|
||||
checkbox: {
|
||||
classification: 'box',
|
||||
component: 'FormulateInputBox'
|
||||
},
|
||||
radio: {
|
||||
classification: 'box',
|
||||
component: 'FormulateInputBox'
|
||||
},
|
||||
|
||||
// === BUTTON STYLE INPUTS
|
||||
submit: {
|
||||
classification: 'button',
|
||||
component: 'FormulateInputButton'
|
||||
},
|
||||
button: {
|
||||
classification: 'button',
|
||||
component: 'FormulateInputButton'
|
||||
},
|
||||
|
||||
// === SELECT STYLE INPUTS
|
||||
select: {
|
||||
classification: 'select',
|
||||
component: 'FormulateInputSelect'
|
||||
}
|
||||
}
|
41
src/libs/utils.js
Normal file
41
src/libs/utils.js
Normal file
@ -0,0 +1,41 @@
|
||||
|
||||
/**
|
||||
* Function to map over an object.
|
||||
* @param {Object} obj An object to map over
|
||||
* @param {Function} callback
|
||||
*/
|
||||
export function map (original, callback) {
|
||||
const obj = {}
|
||||
for (let key in original) {
|
||||
obj[key] = callback(key, original[key])
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to filter an object's properties
|
||||
* @param {Object} original
|
||||
* @param {Function} callback
|
||||
*/
|
||||
export function filter (original, callback) {
|
||||
let obj = {}
|
||||
for (let key in original) {
|
||||
if (callback(key, original[key])) {
|
||||
obj[key] = original[key]
|
||||
}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to reduce an object's properties
|
||||
* @param {Object} original
|
||||
* @param {Function} callback
|
||||
* @param {*} accumulator
|
||||
*/
|
||||
export function reduce (original, callback, accumulator) {
|
||||
for (let key in original) {
|
||||
accumulator = callback(accumulator, key, original[key])
|
||||
}
|
||||
return accumulator
|
||||
}
|
115
src~v1/formulate.js
Normal file
115
src~v1/formulate.js
Normal file
@ -0,0 +1,115 @@
|
||||
import FormulateGroup from './components/Formulate'
|
||||
import FormulateElement from './components/FormulateElement'
|
||||
import DefaultRules from './rules'
|
||||
import DefaultErrors from './errors'
|
||||
|
||||
class Formulate {
|
||||
/**
|
||||
* Initialize vue-formulate.
|
||||
*/
|
||||
constructor () {
|
||||
this.defaultOptions = {
|
||||
registerComponents: true,
|
||||
tags: {
|
||||
Formulate: 'formulate',
|
||||
FormulateElement: 'formulate-element'
|
||||
},
|
||||
errors: {},
|
||||
rules: {},
|
||||
vuexModule: false
|
||||
}
|
||||
this.errors = DefaultErrors
|
||||
this.rules = DefaultRules
|
||||
}
|
||||
|
||||
/**
|
||||
* Install vue-formulate as an instance of Vue.
|
||||
* @param {Vue} Vue
|
||||
*/
|
||||
install (Vue, options = {}) {
|
||||
Vue.prototype.$formulate = this
|
||||
options = Object.assign(this.defaultOptions, options)
|
||||
if (options.registerComponents) {
|
||||
Vue.component(options.tags.Formulate, FormulateGroup)
|
||||
Vue.component(options.tags.FormulateElement, FormulateElement)
|
||||
}
|
||||
if (options.errors) {
|
||||
this.errors = Object.assign(this.errors, options.errors)
|
||||
}
|
||||
if (options.rules) {
|
||||
this.rules = Object.assign(this.rules, options.rules)
|
||||
}
|
||||
this.options = options
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string of rules parse them out to relevant pieces/parts
|
||||
* @param {string} rulesString
|
||||
*/
|
||||
parseRules (rulesString) {
|
||||
return rulesString.split('|')
|
||||
.map(rule => rule.trim())
|
||||
.map(rule => rule.match(/([a-zA-Z0-9]+)\((.*)?\)/) || [null, rule, ''])
|
||||
.map(([ruleString, rule, args]) => Object.assign({}, {rule}, args ? {
|
||||
args: args.split(',').map(arg => arg.trim())
|
||||
} : {args: []}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the function that generates a validation error message for a given
|
||||
* validation rule.
|
||||
* @param {string} rule
|
||||
*/
|
||||
errorFactory (rule) {
|
||||
return this.errors[rule] ? this.errors[rule] : this.errors['default']
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a particular field, value, validation rules, and form values
|
||||
* perform asynchronous field validation.
|
||||
* @param {Object} validatee
|
||||
* @param {string} rulesString
|
||||
* @param {Object} values
|
||||
*/
|
||||
async validationErrors ({field, value, label}, rulesString, values) {
|
||||
return rulesString ? Promise.all(
|
||||
this.parseRules(rulesString)
|
||||
.map(({rule, args}) => {
|
||||
if (typeof this.rules[rule] !== 'function') {
|
||||
throw new Error(`Validation rule is invalid: ${rule}`)
|
||||
}
|
||||
return this.rules[rule]({field, value, label, error: this.errorFactory(rule), values}, ...args)
|
||||
})
|
||||
).then(responses => responses.reduce((errors, error) => {
|
||||
return error ? (Array.isArray(errors) ? errors.concat(error) : [error]) : errors
|
||||
}, false)) : false
|
||||
}
|
||||
}
|
||||
const formulate = new Formulate()
|
||||
export default formulate
|
||||
export * from './store'
|
||||
|
||||
/**
|
||||
* Mapper to allow bindings to the vuex store for custom fields.
|
||||
* @param {Object} definitions
|
||||
*/
|
||||
export const mapModels = (definitions) => {
|
||||
const models = {}
|
||||
for (let mapTo in definitions) {
|
||||
let [form, field] = definitions[mapTo].split('/')
|
||||
models[mapTo] = {
|
||||
set (value) {
|
||||
let m = formulate.options.vuexModule ? `${formulate.options.vuexModule}/` : ''
|
||||
this.$store.commit(`${m}setFieldValue`, {form, field, value})
|
||||
},
|
||||
get () {
|
||||
let m = formulate.options.vuexModule ? `${formulate.options.vuexModule}/` : ''
|
||||
if (this.$store.getters[`${m}formValues`][form]) {
|
||||
return this.$store.getters[`${m}formValues`][form][field]
|
||||
}
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}
|
||||
return models
|
||||
}
|
67
test/Formulate.test.js
Normal file
67
test/Formulate.test.js
Normal file
@ -0,0 +1,67 @@
|
||||
import Formulate from '../dist/formulate.esm.js'
|
||||
|
||||
test('can extend simple object', () => {
|
||||
let a = {
|
||||
optionA: true,
|
||||
optionB: '1234'
|
||||
}
|
||||
let b = {
|
||||
optionA: false
|
||||
}
|
||||
expect(Formulate.extend(a, b)).toEqual({
|
||||
optionA: false,
|
||||
optionB: '1234'
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
test('can extend recursively', () => {
|
||||
let a = {
|
||||
optionA: true,
|
||||
optionC: {
|
||||
first: '123',
|
||||
third: {
|
||||
a: 'b'
|
||||
}
|
||||
},
|
||||
optionB: '1234'
|
||||
}
|
||||
let b = {
|
||||
optionB: '567',
|
||||
optionC: {
|
||||
first: '1234',
|
||||
second: '789',
|
||||
}
|
||||
}
|
||||
expect(Formulate.extend(a, b)).toEqual({
|
||||
optionA: true,
|
||||
optionC: {
|
||||
first: '1234',
|
||||
third: {
|
||||
a: 'b'
|
||||
},
|
||||
second: '789'
|
||||
},
|
||||
optionB: '567'
|
||||
})
|
||||
})
|
||||
|
||||
test('installs on vue instance', () => {
|
||||
const components = [
|
||||
'FormulateForm',
|
||||
'FormulateInput',
|
||||
'FormulateInputBox',
|
||||
'FormulateInputText',
|
||||
'FormulateInputGroup',
|
||||
'FormulateInputSelect',
|
||||
'FormulateInputTextArea'
|
||||
]
|
||||
const registry = []
|
||||
function Vue () {}
|
||||
Vue.component = function (name, instance) {
|
||||
registry.push(name)
|
||||
}
|
||||
Formulate.install(Vue, { extended: true })
|
||||
expect(Vue.prototype.$formulate).toBe(Formulate)
|
||||
expect(registry).toEqual(components)
|
||||
})
|
44
test/FormulateInputBox.test.js
Normal file
44
test/FormulateInputBox.test.js
Normal file
@ -0,0 +1,44 @@
|
||||
import Vue from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Formulate from '../dist/formulate.esm.js'
|
||||
import FormulateInput from '../src/FormulateInput.vue'
|
||||
import FormulateInputBox from '../src/inputs/FormulateInputBox.vue'
|
||||
import FormulateInputGroup from '../src/FormulateInputGroup.vue'
|
||||
|
||||
Vue.use(Formulate)
|
||||
|
||||
test('type "checkbox" renders a box element', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox' } })
|
||||
expect(wrapper.contains(FormulateInputBox)).toBe(true)
|
||||
})
|
||||
|
||||
test('type "radio" renders a box element', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'radio' } })
|
||||
expect(wrapper.contains(FormulateInputBox)).toBe(true)
|
||||
})
|
||||
|
||||
test('box inputs properly process options object in context library', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'} } })
|
||||
expect(Array.isArray(wrapper.vm.context.options)).toBe(true)
|
||||
})
|
||||
|
||||
test('type "checkbox" with options renders a group', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'} } })
|
||||
expect(wrapper.contains(FormulateInputGroup)).toBe(true)
|
||||
})
|
||||
|
||||
test('type "radio" with options renders a group', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'} } })
|
||||
expect(wrapper.contains(FormulateInputGroup)).toBe(true)
|
||||
})
|
||||
|
||||
|
||||
test('labelPosition of type "checkbox" defaults to after', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox' } })
|
||||
expect(wrapper.vm.context.labelPosition).toBe('after')
|
||||
})
|
||||
|
||||
test('labelPosition of type "checkbox" with options defaults to before', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'}}})
|
||||
expect(wrapper.vm.context.labelPosition).toBe('before')
|
||||
})
|
106
test/FormulateInputText.test.js
Normal file
106
test/FormulateInputText.test.js
Normal file
@ -0,0 +1,106 @@
|
||||
import Vue from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Formulate from '../dist/formulate.esm.js'
|
||||
import FormulateInput from '../src/FormulateInput.vue'
|
||||
import FormulateInputText from '../src/inputs/FormulateInputText.vue'
|
||||
|
||||
Vue.use(Formulate)
|
||||
|
||||
/**
|
||||
* Test each type of text element
|
||||
*/
|
||||
|
||||
test('type "text" renders a text input', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'text' } })
|
||||
expect(wrapper.contains(FormulateInputText)).toBe(true)
|
||||
})
|
||||
|
||||
test('type "search" renders a text input', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'search' } })
|
||||
expect(wrapper.contains(FormulateInputText)).toBe(true)
|
||||
})
|
||||
|
||||
test('type "email" renders a text input', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'email' } })
|
||||
expect(wrapper.contains(FormulateInputText)).toBe(true)
|
||||
})
|
||||
|
||||
test('type "number" renders a text input', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'number' } })
|
||||
expect(wrapper.contains(FormulateInputText)).toBe(true)
|
||||
})
|
||||
|
||||
test('type "color" renders a text input', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'color' } })
|
||||
expect(wrapper.contains(FormulateInputText)).toBe(true)
|
||||
})
|
||||
|
||||
test('type "date" renders a text input', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'date' } })
|
||||
expect(wrapper.contains(FormulateInputText)).toBe(true)
|
||||
})
|
||||
|
||||
test('type "month" renders a text input', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'month' } })
|
||||
expect(wrapper.contains(FormulateInputText)).toBe(true)
|
||||
})
|
||||
|
||||
test('type "password" renders a text input', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'password' } })
|
||||
expect(wrapper.contains(FormulateInputText)).toBe(true)
|
||||
})
|
||||
|
||||
test('type "range" renders a text input', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'range' } })
|
||||
expect(wrapper.contains(FormulateInputText)).toBe(true)
|
||||
})
|
||||
|
||||
test('type "tel" renders a text input', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'tel' } })
|
||||
expect(wrapper.contains(FormulateInputText)).toBe(true)
|
||||
})
|
||||
|
||||
test('type "time" renders a text input', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'time' } })
|
||||
expect(wrapper.contains(FormulateInputText)).toBe(true)
|
||||
})
|
||||
|
||||
test('type "url" renders a text input', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'url' } })
|
||||
expect(wrapper.contains(FormulateInputText)).toBe(true)
|
||||
})
|
||||
|
||||
test('type "week" renders a text input', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'week' } })
|
||||
expect(wrapper.contains(FormulateInputText)).toBe(true)
|
||||
})
|
||||
|
||||
/**
|
||||
* Test functionality to text inputs
|
||||
*/
|
||||
|
||||
test('text inputs automatically have id assigned', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'text' } })
|
||||
expect(wrapper.vm.context).toHaveProperty('id')
|
||||
expect(wrapper.find(`input[id="${wrapper.vm.id}"]`).exists()).toBe(true)
|
||||
})
|
||||
|
||||
test('text inputs dont have labels', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'text' } })
|
||||
expect(wrapper.find('label').exists()).toBe(false)
|
||||
})
|
||||
|
||||
test('text inputs can have labels', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'text', label: 'Field label' } })
|
||||
expect(wrapper.find(`label[for="${wrapper.vm.id}"]`).exists()).toBe(true)
|
||||
})
|
||||
|
||||
test('text inputs dont have help text', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'text' } })
|
||||
expect(wrapper.find(`.formulate-input-help`).exists()).toBe(false)
|
||||
})
|
||||
|
||||
test('text inputs dont have help text', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'text', help: 'This is some help text' } })
|
||||
expect(wrapper.find(`.formulate-input-help`).exists()).toBe(true)
|
||||
})
|
17
test/jest.conf.js
Normal file
17
test/jest.conf.js
Normal file
@ -0,0 +1,17 @@
|
||||
const path = require('path')
|
||||
|
||||
module.exports = {
|
||||
rootDir: path.resolve(__dirname, '../'),
|
||||
moduleFileExtensions: [
|
||||
'js',
|
||||
'json',
|
||||
'vue'
|
||||
],
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/src/$1'
|
||||
},
|
||||
transform: {
|
||||
'^.+\\.js$': '<rootDir>/node_modules/babel-jest',
|
||||
'.*\\.(vue)$': '<rootDir>/node_modules/jest-vue-preprocessor'
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
import test from 'ava'
|
||||
import formulate from '../dist'
|
||||
import VueMock from './mocks/VueMock'
|
||||
|
||||
test('checks plugin registration', async t => {
|
||||
let components = []
|
||||
VueMock.component = (name, object) => components.push({name, object})
|
||||
formulate.install(VueMock)
|
||||
t.truthy(VueMock.prototype.$formulate)
|
||||
t.is(2, components.length)
|
||||
})
|
||||
|
||||
test('parses single rule', async t => {
|
||||
let rules = formulate.parseRules('required')
|
||||
t.deepEqual([{rule: 'required', args: []}], rules)
|
||||
})
|
||||
|
||||
test('parses multiple rules and trims', async t => {
|
||||
let rules = formulate.parseRules('email |required')
|
||||
t.deepEqual([{rule: 'email', args: []}, {rule: 'required', args: []}], rules)
|
||||
})
|
||||
|
||||
test('parses rule arguments', async t => {
|
||||
let rules = formulate.parseRules('required(Name)|equals(confirm_password, Your Password)')
|
||||
t.deepEqual([
|
||||
{rule: 'required', args: ['Name']},
|
||||
{rule: 'equals', args: ['confirm_password', 'Your Password']}
|
||||
], rules)
|
||||
})
|
||||
|
||||
test('tests single validation error', async t => {
|
||||
t.is(1, (await formulate.validationErrors({field: 'email', value: ''}, 'required')).length)
|
||||
})
|
||||
|
||||
test('tests multiple validation errors', async t => {
|
||||
t.is(2, (await formulate.validationErrors({field: 'email', value: 'pastaparty'}, 'email|confirmed', {
|
||||
email_confirmation: 'pizzaparty'
|
||||
})).length)
|
||||
})
|
||||
|
||||
test('tests empty validation string', async t => {
|
||||
t.is(false, await formulate.validationErrors({field: 'email', value: 'pastaparty'}, false))
|
||||
})
|
||||
|
||||
test('can extend rules and errors', async t => {
|
||||
formulate.install(VueMock, {
|
||||
rules: {
|
||||
isPizza: ({field, value, error}, label) => value === 'pizza' ? false : error({field, value})
|
||||
},
|
||||
errors: {
|
||||
isPizza: ({field, value}) => `${field} is not a pizza`
|
||||
}
|
||||
})
|
||||
t.deepEqual(['pepperoni is not a pizza'], await formulate.validationErrors({field: 'pepperoni', value: 'meat'}, 'isPizza'))
|
||||
})
|
@ -1,4 +0,0 @@
|
||||
class VueMock {
|
||||
}
|
||||
|
||||
export default VueMock
|
@ -1,90 +0,0 @@
|
||||
import test from 'ava'
|
||||
import f from '../dist'
|
||||
|
||||
const rules = f.rules
|
||||
const error = ({field, value, label}) => {
|
||||
return `${field}${label}`
|
||||
}
|
||||
|
||||
test('test required rule failure', async t => {
|
||||
let v = await rules.required({field: 'name', value: '', error, label: 'xyz'})
|
||||
t.is('string', typeof v)
|
||||
t.is('namexyz', v)
|
||||
})
|
||||
|
||||
test('test number no failure on empty', async t => {
|
||||
let v = await rules.number({field: 'name', value: '', error, label: 'xyz'})
|
||||
t.is(false, v)
|
||||
})
|
||||
|
||||
test('test number failure not number', async t => {
|
||||
let v = await rules.number({field: 'name', value: 't', error, label: 'xyz'})
|
||||
t.is('string', typeof v)
|
||||
t.is('namexyz', v)
|
||||
})
|
||||
|
||||
test('test number is typeof number', async t => {
|
||||
let v = await rules.number({field: 'name', value: '3', error, label: 'xyz'})
|
||||
t.is(false, v)
|
||||
})
|
||||
|
||||
test('test required rule empty array failure', async t => {
|
||||
let v = await rules.required({field: 'name', value: [], error, label: 'xyz'})
|
||||
t.is('namexyz', v)
|
||||
})
|
||||
|
||||
test('test required rule passes', async t => {
|
||||
t.is(false, await rules.required({field: 'name', value: 'Justin'}))
|
||||
})
|
||||
|
||||
test('test email rule with valid email', async t => {
|
||||
t.is(false, await rules.email({field: 'email', value: 'valid@example.com'}))
|
||||
})
|
||||
|
||||
test('test email rule with invalid email', async t => {
|
||||
t.is('email123', await rules.email({field: 'email', value: 'invalid@example', error, label: '123'}))
|
||||
})
|
||||
|
||||
test('test email with empty email', async t => {
|
||||
t.is(false, await rules.email({field: 'email', value: '', error}))
|
||||
})
|
||||
|
||||
test('test confirmed passes', async t => {
|
||||
t.is(false, await rules.confirmed({
|
||||
field: 'password',
|
||||
value: 'password',
|
||||
label: '123',
|
||||
error,
|
||||
values: {password_confirmation: 'password'}
|
||||
}))
|
||||
})
|
||||
|
||||
test('test confirmed passes custom field', async t => {
|
||||
t.is(false, await rules.confirmed({
|
||||
field: 'password',
|
||||
value: 'password',
|
||||
label: '123',
|
||||
error,
|
||||
values: {customfield: 'password'}
|
||||
}, 'customfield'))
|
||||
})
|
||||
|
||||
test('test confirmed fails', async t => {
|
||||
t.is('password123', await rules.confirmed({
|
||||
field: 'password',
|
||||
value: 'password',
|
||||
label: '123',
|
||||
error,
|
||||
values: {password_confirmation: 'pAssword'}
|
||||
}))
|
||||
})
|
||||
|
||||
test('test empty confirmed passes', async t => {
|
||||
t.is(false, await rules.confirmed({
|
||||
field: 'password',
|
||||
value: '',
|
||||
label: '123',
|
||||
error,
|
||||
values: {password_confirmation: ''}
|
||||
}))
|
||||
})
|
@ -1,197 +0,0 @@
|
||||
import test from 'ava'
|
||||
import {formulateState, formulateGetters, formulateMutations} from '../dist'
|
||||
|
||||
test('initial store state', async t => {
|
||||
t.deepEqual({
|
||||
values: {},
|
||||
errors: {},
|
||||
validationErrors: {},
|
||||
meta: {}
|
||||
}, formulateState()())
|
||||
})
|
||||
|
||||
test('extended initial store state', async t => {
|
||||
t.deepEqual({
|
||||
values: {},
|
||||
errors: {},
|
||||
validationErrors: {},
|
||||
meta: {},
|
||||
additionalParam: 'test'
|
||||
}, formulateState({
|
||||
additionalParam: 'test'
|
||||
})())
|
||||
})
|
||||
|
||||
test('validationErrors getter', async t => {
|
||||
let state = {
|
||||
validationErrors: {
|
||||
form: {
|
||||
field: ['This is an error']
|
||||
}
|
||||
}
|
||||
}
|
||||
t.is(formulateGetters().formValidationErrors(state), state.validationErrors)
|
||||
})
|
||||
|
||||
test('errors getter', async t => {
|
||||
let state = {
|
||||
errors: {
|
||||
form: {
|
||||
field: ['This is an error', 'second error']
|
||||
}
|
||||
}
|
||||
}
|
||||
t.is(formulateGetters().formErrors(state), state.errors)
|
||||
})
|
||||
|
||||
test('values getter', async t => {
|
||||
let state = {
|
||||
values: {
|
||||
form: {
|
||||
name: 'Johan',
|
||||
field: 'Guttenberg'
|
||||
}
|
||||
}
|
||||
}
|
||||
t.is(formulateGetters().formValues(state), state.values)
|
||||
})
|
||||
|
||||
test('formMeta getter', async t => {
|
||||
let state = {
|
||||
meta: {
|
||||
form: {
|
||||
name: {name: 'name', label: 'Name'},
|
||||
flail: {name: 'email', label: 'Email'}
|
||||
}
|
||||
}
|
||||
}
|
||||
t.deepEqual(formulateGetters().formMeta(state), {
|
||||
form: [
|
||||
{name: 'name', label: 'Name'},
|
||||
{name: 'email', label: 'Email'}
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
test('form has errors', async t => {
|
||||
let state = {
|
||||
errors: {
|
||||
form: {
|
||||
field: ['This is an error', 'second error']
|
||||
}
|
||||
}
|
||||
}
|
||||
t.is(true, formulateGetters().hasErrors(state).form)
|
||||
})
|
||||
|
||||
test('form has empty errors', async t => {
|
||||
let state = {
|
||||
errors: {
|
||||
form: {
|
||||
field: [],
|
||||
other: []
|
||||
}
|
||||
}
|
||||
}
|
||||
t.is(false, formulateGetters().hasErrors(state).form)
|
||||
})
|
||||
|
||||
test('form has no errors', async t => {
|
||||
let state = {
|
||||
errors: {
|
||||
form: {}
|
||||
}
|
||||
}
|
||||
t.is(false, formulateGetters().hasErrors(state).form)
|
||||
})
|
||||
|
||||
test('adds a new field value', async t => {
|
||||
let state = {values: {}}
|
||||
formulateMutations().setFieldValue(state, {
|
||||
form: 'form',
|
||||
field: 'name',
|
||||
value: 'test name'
|
||||
})
|
||||
t.deepEqual(state.values, {form: {name: 'test name'}})
|
||||
})
|
||||
|
||||
test('adds an existing field value', async t => {
|
||||
let state = {values: {form: {name: 'old name'}}}
|
||||
formulateMutations().setFieldValue(state, {
|
||||
form: 'form',
|
||||
field: 'name',
|
||||
value: 'new name'
|
||||
})
|
||||
t.deepEqual(state.values, {form: {name: 'new name'}})
|
||||
})
|
||||
|
||||
test('adds an error to new field', async t => {
|
||||
let state = {errors: {}}
|
||||
formulateMutations().setFieldErrors(state, {
|
||||
form: 'form',
|
||||
field: 'name',
|
||||
errors: ['i dislike this field']
|
||||
})
|
||||
t.deepEqual(state.errors, {form: {name: ['i dislike this field']}})
|
||||
})
|
||||
|
||||
test('adds an error to existing field', async t => {
|
||||
let state = {errors: {form: {name: ['i like this field']}}}
|
||||
formulateMutations().setFieldErrors(state, {
|
||||
form: 'form',
|
||||
field: 'name',
|
||||
errors: ['i dislike this field']
|
||||
})
|
||||
t.deepEqual(state.errors, {form: {name: ['i dislike this field']}})
|
||||
})
|
||||
|
||||
test('adds a validationError to new field', async t => {
|
||||
let state = {validationErrors: {}}
|
||||
formulateMutations().setFieldValidationErrors(state, {
|
||||
form: 'form',
|
||||
field: 'name',
|
||||
errors: ['i dislike this field']
|
||||
})
|
||||
t.deepEqual(state.validationErrors, {form: {name: ['i dislike this field']}})
|
||||
})
|
||||
|
||||
test('adds a validationError to existing field', async t => {
|
||||
let state = {validationErrors: {form: {name: ['i like this field']}}}
|
||||
formulateMutations().setFieldValidationErrors(state, {
|
||||
form: 'form',
|
||||
field: 'name',
|
||||
errors: ['i dislike this field']
|
||||
})
|
||||
t.deepEqual(state.validationErrors, {form: {name: ['i dislike this field']}})
|
||||
})
|
||||
|
||||
test('reset a form', async t => {
|
||||
let state = {values: {login: {username: 'testuser', password: 'secret'}}}
|
||||
formulateMutations().resetForm(state, 'login')
|
||||
t.deepEqual(state.values, {login: {username: undefined, password: undefined}})
|
||||
})
|
||||
|
||||
test('wont mutate undefined form on reset', async t => {
|
||||
let state = {values: {login: {username: 'testuser', password: 'secret'}}}
|
||||
formulateMutations().resetForm(state, 'register')
|
||||
t.deepEqual(state.values, {login: {username: 'testuser', password: 'secret'}})
|
||||
})
|
||||
|
||||
test('removeField deletes values from store', async t => {
|
||||
let state = {
|
||||
values: {
|
||||
register: {
|
||||
username: 'testuser',
|
||||
password: 'secret'
|
||||
}
|
||||
}
|
||||
}
|
||||
formulateMutations().removeField(state, {form: 'register', field: 'password'})
|
||||
t.deepEqual({
|
||||
values: {
|
||||
register: {
|
||||
username: 'testuser'
|
||||
}
|
||||
}
|
||||
}, state)
|
||||
})
|
183
themes/snow/_inputs.scss
Normal file
183
themes/snow/_inputs.scss
Normal file
@ -0,0 +1,183 @@
|
||||
// Formulate inputs
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
.formulate-input {
|
||||
margin-bottom: 2em;
|
||||
|
||||
.formulate-input-label {
|
||||
display: block;
|
||||
line-height: 1.5;
|
||||
font-size: .9em;
|
||||
font-weight: 600;
|
||||
margin-bottom: .1em;
|
||||
}
|
||||
|
||||
.formulate-input-element {
|
||||
max-width: 20em;
|
||||
margin-bottom: .1em;
|
||||
}
|
||||
|
||||
.formulate-input-help {
|
||||
color: $formulate-gray-ddd;
|
||||
font-size: .7em;
|
||||
font-weight: 300;
|
||||
line-height: 1.5;
|
||||
margin-bottom: .25em;
|
||||
}
|
||||
|
||||
.formulate-input-group-item {
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
// Text inputs
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
&[data-classification='text'] {
|
||||
input {
|
||||
@include baseinput;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Textarea inputs
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
&[data-classification='textarea'] {
|
||||
textarea {
|
||||
@include baseinput;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Select lists
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
&[data-classification='select'] {
|
||||
.formulate-input-element {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
@include down-arrow;
|
||||
top: 50%;
|
||||
margin-top: -.1em;
|
||||
right: 1em;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
@include baseinput;
|
||||
padding-right: 2em;
|
||||
|
||||
&[data-placeholder-selected] {
|
||||
color: $formulate-gray-dd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Box inputs
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
&[data-classification='box'] {
|
||||
.formulate-input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.formulate-input-element {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
left: -999px;
|
||||
}
|
||||
|
||||
&-decorator {
|
||||
display: block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
border-radius: .25em;
|
||||
border: 1px solid $formulate-gray-d;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
background-size: contain;
|
||||
background-position: right;
|
||||
width: calc(100% - .125em);
|
||||
height: calc(100% - .125em);
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
top: .0625em;
|
||||
left: .0625em;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&[data-type="radio"] {
|
||||
.formulate-input-element-decorator {
|
||||
border-radius: 1em;
|
||||
|
||||
&::before {
|
||||
border-radius: 1em;
|
||||
width: calc(100% - .5em);
|
||||
height: calc(100% - .5em);
|
||||
top: .25em;
|
||||
left: .25em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked {
|
||||
& ~ .formulate-input-element-decorator {
|
||||
border-color: $formulate-green;
|
||||
|
||||
&::before {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="%23#{str-slice("#{$formulate-green}", 2)}"><path d="M8.76,56.2c-6.38-6.34,3.26-16,9.64-9.69L38,65.88,80.56,23.29c6.38-6.38,16.07,3.32,9.69,9.69L42.84,80.37a6.83,6.83,0,0,1-9.65,0Z"/></svg>');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input[type="radio"]:checked {
|
||||
& ~ .formulate-input-element-decorator {
|
||||
border-color: $formulate-green;
|
||||
|
||||
&::before {
|
||||
background-color: $formulate-green;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input:focus {
|
||||
& ~ .formulate-input-element-decorator {
|
||||
border-color: $formulate-green;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.formulate-input-label--after {
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.formulate-input-label--before {
|
||||
margin-right: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-classification="group"] {
|
||||
& > .formulate-input-wrapper {
|
||||
& > .formulate-input-label {
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
56
themes/snow/_variables.scss
Normal file
56
themes/snow/_variables.scss
Normal file
@ -0,0 +1,56 @@
|
||||
// Snow theme colors
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
$formulate-font-stack: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
|
||||
$formulate-gray: #efefef;
|
||||
$formulate-gray-d: #cecece;
|
||||
$formulate-gray-dd: #a8a8a8;
|
||||
$formulate-gray-ddd: #6d6d6d;
|
||||
$formulate-gray-dddd: #2e2e2e;
|
||||
|
||||
$formulate-dark: #000000;
|
||||
|
||||
$formulate-blue-l: #f3f4f4;
|
||||
|
||||
$formulate-green: #41b883;
|
||||
|
||||
$formulate-yellow-d: #6b5900;
|
||||
$formulate-yellow: #e6c000;
|
||||
$formulate-yellow-l: #fff8d2;
|
||||
|
||||
|
||||
|
||||
// Mixins
|
||||
// -----------------------------------------------------------------------------
|
||||
@mixin baseinput {
|
||||
appearance: none;
|
||||
border-radius: .3em;
|
||||
border: 1px solid $formulate-gray-d;
|
||||
box-sizing: border-box;
|
||||
background-color: transparent;
|
||||
font-size: .9em;
|
||||
font-family: $formulate-font-stack;
|
||||
padding: .75em;
|
||||
display: block;
|
||||
width: 100%;
|
||||
font-weight: 400;
|
||||
|
||||
&::placeholder {
|
||||
color: $formulate-gray-dd;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
border: 1px solid $formulate-green;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin down-arrow {
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: .3em solid transparent;
|
||||
border-top-color: $formulate-gray-d;
|
||||
border-bottom-width: 0;
|
||||
}
|
2
themes/snow/snow.scss
Normal file
2
themes/snow/snow.scss
Normal file
@ -0,0 +1,2 @@
|
||||
@import 'variables';
|
||||
@import 'inputs';
|
@ -1,40 +0,0 @@
|
||||
const webpack = require('webpack')
|
||||
const path = require('path')
|
||||
|
||||
module.exports = {
|
||||
entry: path.resolve(`${__dirname}/src/formulate.js`),
|
||||
output: {
|
||||
path: path.resolve(`${__dirname}/dist/`),
|
||||
filename: 'index.js',
|
||||
libraryTarget: 'umd',
|
||||
library: 'vue-formulate',
|
||||
umdNamedDefine: true
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.vue']
|
||||
},
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader',
|
||||
options: {}
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
include: [path.resolve(__dirname, 'src')]
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
minimize: true,
|
||||
sourceMap: false,
|
||||
mangle: true,
|
||||
compress: {
|
||||
warnings: false
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user