Initial stable functionality
This commit is contained in:
parent
cac53b1ad2
commit
a705eced91
18
.babelrc
Normal file
18
.babelrc
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
"stage-2",
|
||||||
|
["env", {
|
||||||
|
"browsers": "ie >= 11"
|
||||||
|
}]
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"transform-runtime", {
|
||||||
|
"helpers": false,
|
||||||
|
"polyfill": false,
|
||||||
|
"regenerator": true,
|
||||||
|
"moduleName": "babel-runtime"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
16
.editorconfig
Normal file
16
.editorconfig
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.scss]
|
||||||
|
indent_size = 2
|
1
.eslintignore
Normal file
1
.eslintignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
dist/*
|
22
.eslintrc.js
Normal file
22
.eslintrc.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
node: true
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
parser: 'babel-eslint'
|
||||||
|
},
|
||||||
|
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'
|
||||||
|
],
|
||||||
|
// add your custom rules here
|
||||||
|
rules: {}
|
||||||
|
}
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
node_modules
|
43
dist/index.js
vendored
Normal file
43
dist/index.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7377
package-lock.json
generated
Normal file
7377
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
42
package.json
42
package.json
@ -4,7 +4,10 @@
|
|||||||
"description": "The easiest way to build forms in Vue with validation and vuex support.",
|
"description": "The easiest way to build forms in Vue with validation and vuex support.",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "ava"
|
"dev": "webpack --hide-modules --watch",
|
||||||
|
"build": "NODE_ENV=production && webpack --hide-modules",
|
||||||
|
"test": "ava",
|
||||||
|
"watch": "ava --watch"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -23,5 +26,40 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/wearebraid/vue-formulate/issues"
|
"url": "https://github.com/wearebraid/vue-formulate/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/wearebraid/vue-formulate#readme"
|
"homepage": "https://github.com/wearebraid/vue-formulate#readme",
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"clone-deep": "^3.0.1",
|
||||||
|
"shortid": "^2.2.8"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
106
src/components/Formulate.vue
Normal file
106
src/components/Formulate.vue
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<template>
|
||||||
|
<form
|
||||||
|
@submit.prevent="submit"
|
||||||
|
class="formulate-element"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {equals} from '../utils'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
type: [String, Boolean],
|
||||||
|
default: function () {
|
||||||
|
return this.$formulate.options.vuexModule
|
||||||
|
}
|
||||||
|
},
|
||||||
|
initial: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
parentIdentifier: 'vue-formulate-wrapper-element'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
m () {
|
||||||
|
return `${this.module ? this.module + '/' : ''}`
|
||||||
|
},
|
||||||
|
hasErrors () {
|
||||||
|
return this.$store.getters[`${this.m}hasErrors`][this.name] || false
|
||||||
|
},
|
||||||
|
values () {
|
||||||
|
return this.$store.getters[`${this.m}formValues`][this.name] || {}
|
||||||
|
},
|
||||||
|
errors () {
|
||||||
|
return this.$store.getters[`${this.m}formErrors`][this.name] || {}
|
||||||
|
},
|
||||||
|
validationErrors () {
|
||||||
|
return this.$store.getters[`${this.m}formValidationErrors`][this.name] || {}
|
||||||
|
},
|
||||||
|
fields () {
|
||||||
|
return this.$formulate.fields(this.$vnode)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.hydrate(this.initial)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
hydrate (values) {
|
||||||
|
for (let field of this.fields) {
|
||||||
|
this.$store.commit(`${this.m}setFieldValue`, {
|
||||||
|
field: field.name,
|
||||||
|
value: values[field.name],
|
||||||
|
form: this.name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.updateFormValidation()
|
||||||
|
},
|
||||||
|
update (change) {
|
||||||
|
this.$store.commit(`${this.m}setFieldValue`, Object.assign(change, {
|
||||||
|
form: this.name
|
||||||
|
}))
|
||||||
|
this.updateFormValidation()
|
||||||
|
},
|
||||||
|
updateFieldErrors (change) {
|
||||||
|
this.$store.commit(`${this.m}setFieldErrors`, Object.assign(change, {
|
||||||
|
form: this.name
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
updateFieldValidationErrors (change) {
|
||||||
|
this.$store.commit(`${this.m}setFieldValidationErrors`, Object.assign(change, {
|
||||||
|
form: this.name
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
async validateField ({field, validation}) {
|
||||||
|
let errors = await this.$formulate.validationErrors({
|
||||||
|
field,
|
||||||
|
value: this.values[field]
|
||||||
|
}, validation, this.values)
|
||||||
|
if (!equals(errors, (this.validationErrors[field] || []))) {
|
||||||
|
this.updateFieldValidationErrors({field, errors})
|
||||||
|
}
|
||||||
|
return errors
|
||||||
|
},
|
||||||
|
updateFormValidation () {
|
||||||
|
this.fields.map(async field => this.validateField({
|
||||||
|
field: field.name,
|
||||||
|
validation: field.validation
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
submit () {
|
||||||
|
alert('submitting form')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
153
src/components/FormulateElement.vue
Normal file
153
src/components/FormulateElement.vue
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
<template>
|
||||||
|
<div class="formulate-element">
|
||||||
|
<div class="formulate-element-input-wrapper">
|
||||||
|
<!-- TEXT STYLE INPUTS -->
|
||||||
|
<label
|
||||||
|
:for="id"
|
||||||
|
v-text="label"
|
||||||
|
v-if="label && isTextInput"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
ref="input"
|
||||||
|
:type="type"
|
||||||
|
:name="name"
|
||||||
|
:id="id"
|
||||||
|
v-model="val"
|
||||||
|
v-if="isTextInput"
|
||||||
|
>
|
||||||
|
<!-- BUTTON INPUTS -->
|
||||||
|
<button
|
||||||
|
:type="type"
|
||||||
|
v-text="label || name"
|
||||||
|
v-if="isButtonInput"
|
||||||
|
:disabled="type === 'submit' && form.hasErrors"
|
||||||
|
/>
|
||||||
|
<!-- SELECT INPUTS -->
|
||||||
|
|
||||||
|
<!-- CHECKBOX INPUTS -->
|
||||||
|
|
||||||
|
<!-- RADIO INPUTS -->
|
||||||
|
|
||||||
|
<!-- CUSTOM SLOT INPUTS -->
|
||||||
|
<slot v-if="hasCustomInput" />
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
class="formulate-errors"
|
||||||
|
v-if="localAndValidationErrors.length"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="error in localAndValidationErrors"
|
||||||
|
v-text="error"
|
||||||
|
:key="error"
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {inputTypes} from '../utils'
|
||||||
|
import shortid from 'shortid'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
type: {
|
||||||
|
type: [String, Boolean],
|
||||||
|
default: 'text'
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
initial: {
|
||||||
|
type: [String, Boolean],
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
validation: {
|
||||||
|
type: [String, Boolean],
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: [String, Boolean],
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
type: [String],
|
||||||
|
default: () => shortid.generate()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
hasCustomInput () {
|
||||||
|
return (this.$slots.default && this.$slots.default.length)
|
||||||
|
},
|
||||||
|
isTextInput () {
|
||||||
|
return !this.hasCustomInput && inputTypes.text.includes(this.type)
|
||||||
|
},
|
||||||
|
isButtonInput () {
|
||||||
|
return !this.hasCustomInput && inputTypes.button.includes(this.type)
|
||||||
|
},
|
||||||
|
isListInput () {
|
||||||
|
return !this.hasCustomInput && inputTypes.list.includes(this.type)
|
||||||
|
},
|
||||||
|
form () {
|
||||||
|
let parent = this.$parent
|
||||||
|
while (parent && parent.$data && parent.$data.parentIdentifier !== 'vue-formulate-wrapper-element') {
|
||||||
|
parent = parent.$parent
|
||||||
|
}
|
||||||
|
if (!parent.$data || parent.$data.parentIdentifier !== 'vue-formulate-wrapper-element') {
|
||||||
|
throw new Error('FormulateElement has no FormulateWrapper element')
|
||||||
|
}
|
||||||
|
return parent
|
||||||
|
},
|
||||||
|
values () {
|
||||||
|
return this.form.values
|
||||||
|
},
|
||||||
|
value () {
|
||||||
|
return this.values[this.name]
|
||||||
|
},
|
||||||
|
module () {
|
||||||
|
return this.form.$props['module']
|
||||||
|
},
|
||||||
|
formName () {
|
||||||
|
return this.form.$props['name']
|
||||||
|
},
|
||||||
|
validationErrors () {
|
||||||
|
return this.form.validationErrors[this.name] || []
|
||||||
|
},
|
||||||
|
storeErrors () {
|
||||||
|
return this.form.errors[this.name] || []
|
||||||
|
},
|
||||||
|
localAndValidationErrors () {
|
||||||
|
return this.errors.concat(this.validationErrors)
|
||||||
|
},
|
||||||
|
attributes () {
|
||||||
|
return this.$props
|
||||||
|
},
|
||||||
|
val: {
|
||||||
|
set (value) {
|
||||||
|
this.form.update({field: this.name, value})
|
||||||
|
this.$refs.input.value = value
|
||||||
|
},
|
||||||
|
get () {
|
||||||
|
return this.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
errors () {
|
||||||
|
this.form.updateFieldErrors({
|
||||||
|
field: this.name,
|
||||||
|
errors: this.localAndValidationErrors
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
if (this.initial !== false) {
|
||||||
|
this.form.hydrate({[this.name]: this.initial})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
6
src/errors.js
Normal file
6
src/errors.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
required: ({field, value}, label) => `${label || field} is required`,
|
||||||
|
email: ({field, value}, label) => `${label || 'Email address'} is invalid.`,
|
||||||
|
confirmed: ({field, value}, label) => `${label || field} does not match the confirmation field.`,
|
||||||
|
default: ({field, value}) => `The ${field} field is invalid.`
|
||||||
|
}
|
132
src/formulate.js
Normal file
132
src/formulate.js
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
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']
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively find all instance of FormulateElement inside a given vnode.
|
||||||
|
*/
|
||||||
|
fields (vnode) {
|
||||||
|
let fields = []
|
||||||
|
let children = false
|
||||||
|
if (vnode && vnode.componentOptions && vnode.componentOptions.children && vnode.componentOptions.children.length) {
|
||||||
|
children = vnode.componentOptions.children
|
||||||
|
} else if (vnode && vnode.children && vnode.children.length) {
|
||||||
|
children = vnode.children
|
||||||
|
}
|
||||||
|
if (children) {
|
||||||
|
fields = fields.concat(children.reduce((names, child) => {
|
||||||
|
if (child.componentOptions && child.componentOptions.tag === this.options.tags.FormulateElement) {
|
||||||
|
names.push(child.componentOptions.propsData)
|
||||||
|
}
|
||||||
|
return names.concat(this.fields(child))
|
||||||
|
}, []))
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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}, rulesString, values) {
|
||||||
|
return rulesString ? Promise.all(
|
||||||
|
this.parseRules(rulesString)
|
||||||
|
.map(({rule, args}) => this.rules[rule]({field, value, 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
|
||||||
|
}
|
32
src/rules.js
Normal file
32
src/rules.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* Validate a required field.
|
||||||
|
* @param {Object} field
|
||||||
|
* @param {string} label
|
||||||
|
*/
|
||||||
|
async required ({field, value, error}, label) {
|
||||||
|
return (!value) ? error(...arguments) : false
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate email addresses
|
||||||
|
* @param {Object} field
|
||||||
|
* @param {string} label
|
||||||
|
*/
|
||||||
|
async email ({field, value, error}, label) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
var re = /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/;
|
||||||
|
return (value && !re.test(value.toLowerCase())) ? error(...arguments) : false
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a particular field is matches another field in the form.
|
||||||
|
* @param {Object} field
|
||||||
|
* @param {string} label
|
||||||
|
* @param {string} confirmField (uses `${field}_confirmation` by default)
|
||||||
|
*/
|
||||||
|
async confirmed ({field, value, error, values}, label, confirmField) {
|
||||||
|
confirmField = confirmField || `${field}_confirmation`
|
||||||
|
return (value && value !== values[confirmField]) ? error(...arguments) : false
|
||||||
|
}
|
||||||
|
}
|
75
src/store.js
Normal file
75
src/store.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import {map, reduce} from './utils'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Curried function for creating the formState
|
||||||
|
* @param {Object} options
|
||||||
|
*/
|
||||||
|
export const formulateState = (options = {}) => () => Object.assign({
|
||||||
|
values: {},
|
||||||
|
errors: {},
|
||||||
|
validationErrors: {}
|
||||||
|
}, options)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function for creating the formGetters
|
||||||
|
* @param {string} module
|
||||||
|
* @param {Object} getters
|
||||||
|
*/
|
||||||
|
export const formulateGetters = (moduleName = '', getters = {}) => Object.assign({
|
||||||
|
formValues (state) {
|
||||||
|
return state.values
|
||||||
|
},
|
||||||
|
formErrors (state) {
|
||||||
|
return state.errors
|
||||||
|
},
|
||||||
|
formValidationErrors (state) {
|
||||||
|
return state.validationErrors
|
||||||
|
},
|
||||||
|
hasErrors (state) {
|
||||||
|
return map(state.errors, (form, errors) => {
|
||||||
|
return reduce(errors, (hasErrors, field, errors) => hasErrors || !!errors.length, false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, getters)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function for creating the formActions
|
||||||
|
* @param {string} moduleName
|
||||||
|
* @param {Object} actions
|
||||||
|
*/
|
||||||
|
export const formulateActions = (moduleName = '', actions = {}) => Object.assign({
|
||||||
|
}, actions)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function for creating the formMutations
|
||||||
|
* @param {Object} mutations
|
||||||
|
*/
|
||||||
|
export const formulateMutations = (mutations = {}) => Object.assign({
|
||||||
|
setFieldValue (state, {form, field, value}) {
|
||||||
|
state.values = Object.assign({}, state.values, {
|
||||||
|
[form]: Object.assign({}, state.values[form] || {}, {[field]: value})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setFieldErrors (state, {form, field, errors}) {
|
||||||
|
state.errors = Object.assign({}, state.errors, {
|
||||||
|
[form]: Object.assign({}, state.errors[form] || {}, {[field]: errors})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setFieldValidationErrors (state, {form, field, errors}) {
|
||||||
|
state.validationErrors = Object.assign({}, state.validationErrors, {
|
||||||
|
[form]: Object.assign({}, state.validationErrors[form] || {}, {[field]: errors})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, mutations)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function for exposing a full vuex module.
|
||||||
|
* @param {string} moduleName
|
||||||
|
* @param {Object} validation
|
||||||
|
*/
|
||||||
|
export const formulateModule = (moduleName) => ({
|
||||||
|
state: formulateState(),
|
||||||
|
getters: formulateGetters(moduleName),
|
||||||
|
actions: formulateActions(moduleName),
|
||||||
|
mutations: formulateMutations()
|
||||||
|
})
|
89
src/utils.js
Normal file
89
src/utils.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import cloneDeep from 'clone-deep'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare the equality of two arrays.
|
||||||
|
* @param {Array} arr1
|
||||||
|
* @param {Array} arr2
|
||||||
|
*/
|
||||||
|
export function equals (arr1, arr2) {
|
||||||
|
var length = arr1.length
|
||||||
|
if (length !== arr2.length) return false
|
||||||
|
for (var i = 0; i < length; i++) {
|
||||||
|
if (arr1[i] !== arr2[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to map over an object.
|
||||||
|
* @param {Object} obj An object to map over
|
||||||
|
* @param {Function} callback
|
||||||
|
*/
|
||||||
|
export function map (original, callback) {
|
||||||
|
let obj = cloneDeep(original)
|
||||||
|
for (let key in obj) {
|
||||||
|
obj[key] = callback(key, obj[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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comprehensive list of input types supported.
|
||||||
|
*/
|
||||||
|
export const inputTypes = {
|
||||||
|
text: [
|
||||||
|
'text',
|
||||||
|
'email',
|
||||||
|
'number',
|
||||||
|
'color',
|
||||||
|
'date',
|
||||||
|
'datetime-local',
|
||||||
|
'hidden',
|
||||||
|
'month',
|
||||||
|
'password',
|
||||||
|
'radio',
|
||||||
|
'range',
|
||||||
|
'search',
|
||||||
|
'tel',
|
||||||
|
'time',
|
||||||
|
'url',
|
||||||
|
'week'
|
||||||
|
],
|
||||||
|
button: [
|
||||||
|
'submit',
|
||||||
|
'button'
|
||||||
|
],
|
||||||
|
list: [
|
||||||
|
'select'
|
||||||
|
]
|
||||||
|
}
|
43
tests/formulate.test.js
Normal file
43
tests/formulate.test.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
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))
|
||||||
|
})
|
4
tests/mocks/VueMock.js
Normal file
4
tests/mocks/VueMock.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
class VueMock {
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VueMock
|
65
tests/rules.test.js
Normal file
65
tests/rules.test.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
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}, 'xyz')
|
||||||
|
t.is('string', typeof v)
|
||||||
|
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}, '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',
|
||||||
|
error,
|
||||||
|
values: {password_confirmation: 'password'}
|
||||||
|
}, '123'))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test confirmed passes custom field', async t => {
|
||||||
|
t.is(false, await rules.confirmed({
|
||||||
|
field: 'password',
|
||||||
|
value: 'password',
|
||||||
|
error,
|
||||||
|
values: {customfield: 'password'}
|
||||||
|
}, '123', 'customfield'))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test confirmed fails', async t => {
|
||||||
|
t.is('password123', await rules.confirmed({
|
||||||
|
field: 'password',
|
||||||
|
value: 'password',
|
||||||
|
error,
|
||||||
|
values: {password_confirmation: 'pAssword'}
|
||||||
|
}, '123'))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test empty confirmed passes', async t => {
|
||||||
|
t.is(false, await rules.confirmed({
|
||||||
|
field: 'password',
|
||||||
|
value: '',
|
||||||
|
error,
|
||||||
|
values: {password_confirmation: ''}
|
||||||
|
}, '123'))
|
||||||
|
})
|
40
webpack.config.js
Normal file
40
webpack.config.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
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