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.",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"test": "ava"
|
||||
"dev": "webpack --hide-modules --watch",
|
||||
"build": "NODE_ENV=production && webpack --hide-modules",
|
||||
"test": "ava",
|
||||
"watch": "ava --watch"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -23,5 +26,40 @@
|
||||
"bugs": {
|
||||
"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