Merge pull request #6 from cmath10/proposal-refactor
Typescript & storybook & build
This commit is contained in:
commit
d59df138a2
@ -1,4 +1,5 @@
|
||||
dist/*
|
||||
test/*
|
||||
coverage/*
|
||||
/.cache
|
||||
coverage/*
|
||||
dist/*
|
||||
storybook/config/*
|
||||
test/*
|
||||
|
80
.eslintrc.js
80
.eslintrc.js
@ -1,22 +1,62 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint',
|
||||
sourceType: 'module'
|
||||
},
|
||||
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
|
||||
extends: [
|
||||
'standard',
|
||||
'plugin:vue/recommended'
|
||||
],
|
||||
env: {
|
||||
browser: true,
|
||||
},
|
||||
// add your custom rules here
|
||||
'rules': {
|
||||
// allow paren-less arrow functions
|
||||
'arrow-parens': 0,
|
||||
// allow debugger during development
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
|
||||
}
|
||||
root: true,
|
||||
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser',
|
||||
sourceType: 'module',
|
||||
},
|
||||
|
||||
plugins: ['@typescript-eslint'],
|
||||
|
||||
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
|
||||
extends: [
|
||||
'standard',
|
||||
'@vue/standard',
|
||||
'@vue/typescript',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:vue/recommended',
|
||||
],
|
||||
|
||||
env: {
|
||||
browser: true,
|
||||
},
|
||||
|
||||
rules: {
|
||||
'@typescript-eslint/ban-ts-ignore': 'off', // @TODO
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off', // @TODO
|
||||
'@typescript-eslint/no-unused-vars': ['error'], // @TODO
|
||||
'arrow-parens': 0,
|
||||
'comma-dangle': ['error', 'only-multiline'],
|
||||
'indent': ['error', 4, { SwitchCase: 1 }],
|
||||
'max-depth': ['error', 3],
|
||||
'max-lines-per-function': ['error', 40],
|
||||
'no-console': ['warn', {allow: ['warn', 'error']}],
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
|
||||
'no-unused-vars': 'off',
|
||||
'vue/html-closing-bracket-spacing': ['error', {
|
||||
startTag: 'never',
|
||||
endTag: 'never',
|
||||
selfClosingTag: 'always',
|
||||
}],
|
||||
'vue/html-indent': ['error', 4, {
|
||||
attribute: 1,
|
||||
closeBracket: 0,
|
||||
alignAttributesVertically: true,
|
||||
ignores: [],
|
||||
}],
|
||||
},
|
||||
|
||||
overrides: [{
|
||||
files: ['*/**/shims-*.d.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-empty-interface': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
},
|
||||
}, {
|
||||
files: ['*.js', '*.tale.vue'],
|
||||
rules: {
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
}
|
||||
}],
|
||||
}
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,6 +2,7 @@ node_modules
|
||||
coverage
|
||||
.cache
|
||||
.DS_Store
|
||||
.idea
|
||||
.vscode
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
|
5
.npmignore
Normal file
5
.npmignore
Normal file
@ -0,0 +1,5 @@
|
||||
.github
|
||||
.gitignore
|
||||
/storybook
|
||||
/node_modules
|
||||
/build
|
8
Makefile
Normal file
8
Makefile
Normal file
@ -0,0 +1,8 @@
|
||||
.PHONY: console
|
||||
console: ## Run container and switch to terminal
|
||||
docker-compose run --rm --service-ports node bash
|
||||
|
||||
.PHONY: help
|
||||
help: ## Help information
|
||||
@cat $(MAKEFILE_LIST) | grep -e "^[a-zA-Z_\-]*: *.*## *" | awk '\
|
||||
BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
@ -1,13 +1,13 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
test: {
|
||||
presets: [
|
||||
[ '@babel/preset-env', {
|
||||
targets: {
|
||||
node: '9'
|
||||
}
|
||||
} ]
|
||||
]
|
||||
env: {
|
||||
test: {
|
||||
presets: [
|
||||
['@babel/preset-env', {
|
||||
targets: {
|
||||
node: '9'
|
||||
}
|
||||
}]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,34 +1,32 @@
|
||||
import commonjs from '@rollup/plugin-commonjs' // Convert CommonJS modules to ES6
|
||||
import buble from '@rollup/plugin-buble' // Transpile/polyfill with reasonable browser support
|
||||
import alias from '@rollup/plugin-alias'
|
||||
import autoExternal from 'rollup-plugin-auto-external'
|
||||
import vue from 'rollup-plugin-vue' // Handle .vue SFC files
|
||||
import commonjs from '@rollup/plugin-commonjs'
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
import typescript from 'rollup-plugin-typescript2'
|
||||
import vue from 'rollup-plugin-vue'
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export default {
|
||||
input: 'src/Formulario.js', // Path relative to package.json
|
||||
output: [
|
||||
{
|
||||
name: 'Formulario',
|
||||
exports: 'default',
|
||||
globals: {
|
||||
'is-plain-object': 'isPlainObject',
|
||||
'nanoid/non-secure': 'nanoid',
|
||||
'is-url': 'isUrl',
|
||||
},
|
||||
sourcemap: false
|
||||
}
|
||||
],
|
||||
external: ['nanoid/non-secure'],
|
||||
plugins: [
|
||||
commonjs(),
|
||||
autoExternal(),
|
||||
vue({
|
||||
css: true, // Dynamically inject css as a <style> tag
|
||||
compileTemplate: true // Explicitly convert template to render function
|
||||
}),
|
||||
buble({
|
||||
objectAssign: 'Object.assign'
|
||||
}), // Transpile to ES5,
|
||||
terser()
|
||||
]
|
||||
input: 'src/index.ts',
|
||||
output: [{
|
||||
name: 'Formulario',
|
||||
exports: 'default',
|
||||
globals: {
|
||||
'is-plain-object': 'isPlainObject',
|
||||
'is-url': 'isUrl',
|
||||
'nanoid/non-secure': 'nanoid',
|
||||
vue: 'Vue',
|
||||
'vue-property-decorator': 'vuePropertyDecorator',
|
||||
},
|
||||
sourcemap: false,
|
||||
}],
|
||||
external: ['nanoid/non-secure', 'vue', 'vue-property-decorator'],
|
||||
plugins: [
|
||||
typescript({ check: false, sourceMap: false }),
|
||||
vue({ css: true, compileTemplate: true }),
|
||||
alias({ entries: [{ find: /^@\/(.+)/, replacement: './$1' }] }),
|
||||
commonjs(),
|
||||
autoExternal(),
|
||||
terser(),
|
||||
]
|
||||
}
|
||||
|
@ -1,36 +1,37 @@
|
||||
import resolve from '@rollup/plugin-node-resolve'
|
||||
import commonjs from '@rollup/plugin-commonjs' // Convert CommonJS modules to ES6
|
||||
import buble from '@rollup/plugin-buble' // Transpile/polyfill with reasonable browser support
|
||||
import vue from 'rollup-plugin-vue' // Handle .vue SFC files
|
||||
import alias from '@rollup/plugin-alias'
|
||||
import commonjs from '@rollup/plugin-commonjs'
|
||||
import internal from 'rollup-plugin-internal'
|
||||
import resolve from '@rollup/plugin-node-resolve'
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
import typescript from 'rollup-plugin-typescript2'
|
||||
import vue from 'rollup-plugin-vue'
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export default {
|
||||
input: 'src/Formulario.js', // Path relative to package.json
|
||||
output: {
|
||||
name: 'VueFormulario',
|
||||
exports: 'default',
|
||||
format: 'iife',
|
||||
globals: {
|
||||
'is-plain-object': 'isPlainObject',
|
||||
'nanoid/non-secure': 'nanoid',
|
||||
'is-url': 'isUrl',
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
resolve({
|
||||
browser: true,
|
||||
preferBuiltins: false
|
||||
}),
|
||||
commonjs(),
|
||||
internal(['is-plain-object', 'nanoid/non-secure', 'is-url']),
|
||||
vue({
|
||||
css: true, // Dynamically inject css as a <style> tag
|
||||
compileTemplate: true // Explicitly convert template to render function
|
||||
}),
|
||||
buble({
|
||||
objectAssign: 'Object.assign'
|
||||
}), // Transpile to ES5,
|
||||
terser()
|
||||
]
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
name: 'VueFormulario',
|
||||
exports: 'default',
|
||||
format: 'iife',
|
||||
globals: {
|
||||
'is-plain-object': 'isPlainObject',
|
||||
'is-url': 'isUrl',
|
||||
'nanoid/non-secure': 'nanoid',
|
||||
vue: 'Vue',
|
||||
'vue-property-decorator': 'vuePropertyDecorator',
|
||||
},
|
||||
},
|
||||
external: ['vue'],
|
||||
plugins: [
|
||||
resolve({
|
||||
browser: true,
|
||||
preferBuiltins: false,
|
||||
}),
|
||||
typescript({ check: false, sourceMap: false }),
|
||||
vue({ css: true, compileTemplate: true }),
|
||||
alias({ entries: [{ find: /^@\/(.+)/, replacement: './$1' }] }),
|
||||
commonjs(),
|
||||
internal(['is-plain-object', 'nanoid/non-secure', 'is-url', 'vue-property-decorator']),
|
||||
terser(),
|
||||
]
|
||||
}
|
||||
|
9
docker-compose.yml
Normal file
9
docker-compose.yml
Normal file
@ -0,0 +1,9 @@
|
||||
version: '3.6'
|
||||
services:
|
||||
node:
|
||||
image: library/node:12
|
||||
user: node
|
||||
volumes:
|
||||
- ./:/var/www/vue-formulario
|
||||
- "$SSH_AUTH_SOCK:/ssh-auth.sock"
|
||||
working_dir: /var/www/vue-formulario
|
@ -1,106 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
id="app"
|
||||
class="specimen-list"
|
||||
>
|
||||
<SpecimenButton />
|
||||
<SpecimenBox />
|
||||
<SpecimenFile />
|
||||
<SpecimenGroup />
|
||||
<SpecimenSelect />
|
||||
<SpecimenSlider />
|
||||
<SpecimenText />
|
||||
<SpecimenTextarea />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SpecimenText from './specimens/SpecimenText'
|
||||
import SpecimenTextarea from './specimens/SpecimenTextarea'
|
||||
import SpecimenGroup from './specimens/SpecimenGroup'
|
||||
import SpecimenFile from './specimens/SpecimenFile'
|
||||
import SpecimenButton from './specimens/SpecimenButton'
|
||||
import SpecimenBox from './specimens/SpecimenBox'
|
||||
import SpecimenSlider from './specimens/SpecimenSlider'
|
||||
import SpecimenSelect from './specimens/SpecimenSelect'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
SpecimenButton,
|
||||
SpecimenBox,
|
||||
SpecimenText,
|
||||
SpecimenTextarea,
|
||||
SpecimenGroup,
|
||||
SpecimenFile,
|
||||
SpecimenSlider,
|
||||
SpecimenSelect
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: $formulate-font-stack;
|
||||
}
|
||||
h2 {
|
||||
display: block;
|
||||
width: 100%;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: white;
|
||||
padding: .5em 0;
|
||||
color: $formulate-green;
|
||||
border-bottom: 1px solid $formulate-gray;
|
||||
margin: 2em 0 0 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.specimen-list {
|
||||
padding: 1em;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
@media (min-width: 756px) {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
}
|
||||
.specimens {
|
||||
@media (min-width: 500px) {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.specimen {
|
||||
max-width: 400px;
|
||||
padding: 1em;
|
||||
box-sizing: border-box;
|
||||
|
||||
@media (min-width: 500px) {
|
||||
width: 50%;
|
||||
border-bottom: 1px solid $formulate-gray;
|
||||
&:nth-of-type(odd) {
|
||||
border-right: 1px solid $formulate-gray;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 900px) {
|
||||
width: 33.332%;
|
||||
border-right: 1px solid $formulate-gray;
|
||||
|
||||
&:nth-of-type(3n) {
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,11 +0,0 @@
|
||||
import Vue from 'vue'
|
||||
import VueFormulate from '../src/Formulate'
|
||||
import FormulateSpecimens from './FormulateSpecimens.vue'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
Vue.use(VueFormulate)
|
||||
|
||||
new Vue({
|
||||
render: h => h(FormulateSpecimens)
|
||||
}).$mount('#app')
|
@ -1,30 +0,0 @@
|
||||
<template>
|
||||
<div class="specimens specimens--text">
|
||||
<h2>Box classification</h2>
|
||||
<div class="specimen">
|
||||
<h3>Single Checkbox</h3>
|
||||
<FormulateInput
|
||||
type="checkbox"
|
||||
label="Are you attractive?"
|
||||
help="In your own opinion at least."
|
||||
/>
|
||||
</div>
|
||||
<div class="specimen">
|
||||
<h3>Multi checkbox</h3>
|
||||
<FormulateInput
|
||||
type="checkbox"
|
||||
label="Which recent U.S. president is your favorite?"
|
||||
:options="{trump: 'Trump', obama: 'Obama', bush: 'Bush', clinton: 'Clinton'}"
|
||||
help="Your selection will be transmitted directly to the NSA."
|
||||
/>
|
||||
</div>
|
||||
<div class="specimen">
|
||||
<h3>Radio</h3>
|
||||
<FormulateInput
|
||||
type="radio"
|
||||
label="What heavily body would you like to visit?"
|
||||
:options="{moon: 'Moon', mars: 'Mars', venus: 'Venus', mercury: 'Mercury', sun: 'Sun'}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -1,19 +0,0 @@
|
||||
<template>
|
||||
<div class="specimens">
|
||||
<h2>Button classification</h2>
|
||||
<div class="specimen">
|
||||
<h3>Button</h3>
|
||||
<FormulateInput
|
||||
label="Click me"
|
||||
type="button"
|
||||
/>
|
||||
</div>
|
||||
<div class="specimen">
|
||||
<h3>Submit</h3>
|
||||
<FormulateInput
|
||||
label="Submit me"
|
||||
type="submit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -1,21 +0,0 @@
|
||||
<template>
|
||||
<div class="specimens specimens--text">
|
||||
<h2>File classification</h2>
|
||||
<div class="specimen specimen">
|
||||
<h3>Text</h3>
|
||||
<FormulateInput
|
||||
label="Upload a file"
|
||||
type="file"
|
||||
help="Select any file to upload"
|
||||
/>
|
||||
</div>
|
||||
<div class="specimen">
|
||||
<h3>Image</h3>
|
||||
<FormulateInput
|
||||
label="What do you look like?"
|
||||
type="image"
|
||||
help="Select a picture to upload."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -1,55 +0,0 @@
|
||||
<template>
|
||||
<div class="specimens specimens--group">
|
||||
<h2>Group classification</h2>
|
||||
<div class="specimen">
|
||||
<h3>Non-repeatable group</h3>
|
||||
<FormulateInput
|
||||
type="group"
|
||||
>
|
||||
<FormulateInput
|
||||
label="City"
|
||||
type="text"
|
||||
name="city"
|
||||
/>
|
||||
<FormulateInput
|
||||
label="State"
|
||||
type="select"
|
||||
:options="{NE: 'Nebraska', MO: 'Missouri', VA: 'Virginia'}"
|
||||
placeholder="Select a state"
|
||||
/>
|
||||
</FormulateInput>
|
||||
</div>
|
||||
<div class="specimen">
|
||||
<h3>Repeatable group</h3>
|
||||
<FormulateInput
|
||||
name="users"
|
||||
label="Invite some new users"
|
||||
type="group"
|
||||
placeholder="users"
|
||||
help="Fields can be grouped"
|
||||
:repeatable="true"
|
||||
>
|
||||
<FormulateInput
|
||||
label="First and last name"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="User’s name"
|
||||
validation="required"
|
||||
/>
|
||||
<FormulateInput
|
||||
name="email"
|
||||
label="Email address"
|
||||
type="email"
|
||||
placeholder="User’s email"
|
||||
validation="required|email"
|
||||
/>
|
||||
</FormulateInput>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
|
||||
}
|
||||
</script>
|
@ -1,14 +0,0 @@
|
||||
<template>
|
||||
<div class="specimens specimens--text">
|
||||
<h2>Select classification</h2>
|
||||
<div class="specimen specimen--single">
|
||||
<h3>Select</h3>
|
||||
<FormulateInput
|
||||
label="What planet is the hottest?"
|
||||
type="select"
|
||||
:options="{mercury: 'Mercury', venus: 'Venus', earth: 'Earth', mars: 'Mars'}"
|
||||
help="Average temperature on the surface of the planet."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<div class="specimens specimens--text">
|
||||
<h2>Slider classification</h2>
|
||||
<div class="specimen specimen--single">
|
||||
<h3>Range</h3>
|
||||
<FormulateInput
|
||||
label="How far should we crank it up?"
|
||||
type="range"
|
||||
min="0"
|
||||
max="11"
|
||||
:show-value="true"
|
||||
help="Whenever we’re allowed to have a party again..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -1,117 +0,0 @@
|
||||
<template>
|
||||
<div class="specimens specimens--text">
|
||||
<h2>Text classification</h2>
|
||||
<div class="specimen">
|
||||
<h3>Color</h3>
|
||||
<FormulateInput
|
||||
label="Pick a color?"
|
||||
type="color"
|
||||
help="Choose your favorite."
|
||||
/>
|
||||
</div>
|
||||
<div class="specimen">
|
||||
<h3>Date</h3>
|
||||
<FormulateInput
|
||||
label="Select a day."
|
||||
type="date"
|
||||
help="Choose your birthday."
|
||||
/>
|
||||
</div>
|
||||
<div class="specimen">
|
||||
<h3>Datetime-local</h3>
|
||||
<FormulateInput
|
||||
label="Select a day and time."
|
||||
type="datetime-local"
|
||||
help="When is the meeting?"
|
||||
/>
|
||||
</div>
|
||||
<div class="specimen">
|
||||
<h3>Email</h3>
|
||||
<FormulateInput
|
||||
label="What is your email?"
|
||||
type="email"
|
||||
placeholder="placeholder@example.com"
|
||||
help="What is your email address?"
|
||||
/>
|
||||
</div>
|
||||
<div class="specimen">
|
||||
<h3>Month</h3>
|
||||
<FormulateInput
|
||||
label="Favorite month"
|
||||
type="month"
|
||||
help="When is Christmas?"
|
||||
/>
|
||||
</div>
|
||||
<div class="specimen">
|
||||
<h3>Number</h3>
|
||||
<FormulateInput
|
||||
label="How old are you?"
|
||||
type="number"
|
||||
placeholder="25"
|
||||
help="Select your age"
|
||||
/>
|
||||
</div>
|
||||
<div class="specimen">
|
||||
<h3>Password</h3>
|
||||
<FormulateInput
|
||||
label="Enter a password."
|
||||
type="password"
|
||||
help="Choose something long and tricky."
|
||||
/>
|
||||
</div>
|
||||
<div class="specimen">
|
||||
<h3>Search</h3>
|
||||
<FormulateInput
|
||||
label="But I still haven't found."
|
||||
type="search"
|
||||
placeholder="What im looking for..."
|
||||
help="I have climbed the highest mountains."
|
||||
/>
|
||||
</div>
|
||||
<div class="specimen">
|
||||
<h3>Tel</h3>
|
||||
<FormulateInput
|
||||
label="Oh you like Mike & Ikes?"
|
||||
type="tel"
|
||||
placeholder="Can I have you number..."
|
||||
help="Can I have it?"
|
||||
/>
|
||||
</div>
|
||||
<div class="specimen">
|
||||
<h3>Time</h3>
|
||||
<FormulateInput
|
||||
label="What time is dinner?"
|
||||
type="time"
|
||||
placeholder="Pick a time"
|
||||
help="When will you eat your food?"
|
||||
/>
|
||||
</div>
|
||||
<div class="specimen">
|
||||
<h3>Text</h3>
|
||||
<FormulateInput
|
||||
label="Username"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
help="Select a username"
|
||||
/>
|
||||
</div>
|
||||
<div class="specimen">
|
||||
<h3>Url</h3>
|
||||
<FormulateInput
|
||||
label="Personal website"
|
||||
type="url"
|
||||
placeholder="https://"
|
||||
help="What is the url for your website?"
|
||||
/>
|
||||
</div>
|
||||
<div class="specimen">
|
||||
<h3>Week</h3>
|
||||
<FormulateInput
|
||||
label="I don’t know who would use this field."
|
||||
type="week"
|
||||
placeholder="Pick a week"
|
||||
help="What week is it?"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<div class="specimens specimens--text">
|
||||
<h2>Textarea classification</h2>
|
||||
<div class="specimen specimen--single">
|
||||
<h3>Textarea</h3>
|
||||
<FormulateInput
|
||||
label="What is your life story?"
|
||||
type="textarea"
|
||||
help="I want to hear all about it!"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
99
package.json
99
package.json
@ -1,61 +1,64 @@
|
||||
{
|
||||
"name": "@retailcrm/vue-formulario",
|
||||
"version": "0.1.0",
|
||||
"main": "dist/formulario.umd.js",
|
||||
"module": "dist/formulario.esm.js",
|
||||
"unpkg": "dist/formulario.min.js",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"browser": {
|
||||
"./sfc": "src/Formulario.js"
|
||||
},
|
||||
"author": "RetailDriverLLC <integration@retailcrm.ru>",
|
||||
"scripts": {
|
||||
"build": "npm run build:esm & npm run build:umd & npm run build:iife & wait && echo \"Build complete:\nesm: $(gzip -c dist/formulario.esm.js | wc -c)b gzip\numd: $(gzip -c dist/formulario.umd.js | wc -c)b gzip\nmin: $(gzip -c dist/formulario.min.js | wc -c)b gzip\"",
|
||||
"build:esm": "rollup --config build/rollup.config.js --format es --file dist/formulario.esm.js",
|
||||
"build:umd": "rollup --config build/rollup.config.js --format umd --file dist/formulario.umd.js",
|
||||
"build:iife": "rollup --config build/rollup.iife.config.js --format iife --file dist/formulario.min.js",
|
||||
"test": "NODE_ENV=test jest --config test/jest.conf.js",
|
||||
"test:watch": "NODE_ENV=test jest --config test/jest.conf.js --watch",
|
||||
"test:coverage": "NODE_ENV=test jest --config test/jest.conf.js --coverage",
|
||||
"build:size": "gzip -c dist/formulario.esm.js | wc -c",
|
||||
"dev": "vue-cli-service serve --port=7872 examples/main.js"
|
||||
"build:umd": "rollup --config build/rollup.config.js --format umd --file dist/formulario.umd.js",
|
||||
"lint": "vue-cli-service lint",
|
||||
"storybook:build": "vue-cli-service storybook:build -c storybook/config",
|
||||
"storybook:serve": "vue-cli-service storybook:serve -p 6006 -c storybook/config",
|
||||
"test": "NODE_ENV=test jest --config test/jest.conf.js",
|
||||
"test:coverage": "NODE_ENV=test jest --config test/jest.conf.js --coverage",
|
||||
"test:watch": "NODE_ENV=test jest --config test/jest.conf.js --watch"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+ssh://git@github.com/retailcrm/vue-formulario.git"
|
||||
"main": "dist/formulario.umd.js",
|
||||
"module": "dist/formulario.esm.js",
|
||||
"browser": {
|
||||
"./sfc": "src/index.ts"
|
||||
},
|
||||
"keywords": [
|
||||
"vue",
|
||||
"form",
|
||||
"forms",
|
||||
"validation",
|
||||
"validate"
|
||||
],
|
||||
"author": "RetailDriverLLC <integration@retailcrm.ru>",
|
||||
"contributors": [
|
||||
"Justin Schroeder <justin@wearebraid.com>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/retailcrm/vue-formulario/issues"
|
||||
"unpkg": "dist/formulario.min.js",
|
||||
"dependencies": {
|
||||
"is-plain-object": "^3.0.0",
|
||||
"is-url": "^1.2.4",
|
||||
"nanoid": "^2.1.11",
|
||||
"vue-class-component": "^7.2.3",
|
||||
"vue-property-decorator": "^8.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.9.6",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.9.6",
|
||||
"@babel/preset-env": "^7.9.6",
|
||||
"@rollup/plugin-alias": "^3.1.1",
|
||||
"@rollup/plugin-buble": "^0.21.3",
|
||||
"@rollup/plugin-commonjs": "^11.1.0",
|
||||
"@rollup/plugin-node-resolve": "^7.1.3",
|
||||
"@storybook/addon-actions": "^6.0.26",
|
||||
"@storybook/addon-knobs": "^6.0.26",
|
||||
"@storybook/addon-links": "^6.0.26",
|
||||
"@storybook/addon-notes": "^5.3.21",
|
||||
"@storybook/vue": "^6.0.26",
|
||||
"@types/is-url": "^1.2.28",
|
||||
"@types/jest": "^26.0.14",
|
||||
"@types/nanoid": "^2.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^2.26.0",
|
||||
"@typescript-eslint/parser": "^2.26.0",
|
||||
"@vue/cli-plugin-babel": "^4.3.1",
|
||||
"@vue/cli-plugin-eslint": "^4.3.1",
|
||||
"@vue/cli-plugin-typescript": "^4.5.7",
|
||||
"@vue/cli-service": "^4.5.4",
|
||||
"@vue/component-compiler-utils": "^3.1.2",
|
||||
"@vue/eslint-config-standard": "^5.1.2",
|
||||
"@vue/eslint-config-typescript": "^5.0.2",
|
||||
"@vue/test-utils": "^1.0.2",
|
||||
"autoprefixer": "^9.7.6",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^25.5.1",
|
||||
"bootstrap-scss": "^4.5.2",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-standard": "^12.0.0",
|
||||
"eslint-plugin-import": "^2.20.1",
|
||||
@ -64,26 +67,46 @@
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"eslint-plugin-vue": "^5.2.3",
|
||||
"flush-promises": "^1.0.2",
|
||||
"jest": "^25.5.4",
|
||||
"jest": "^26.5.2",
|
||||
"jest-vue-preprocessor": "^1.7.1",
|
||||
"node-sass": "^4.14.1",
|
||||
"rollup": "^1.32.1",
|
||||
"rollup-plugin-auto-external": "^2.0.0",
|
||||
"rollup-plugin-internal": "^1.0.4",
|
||||
"rollup-plugin-multi-input": "^1.1.1",
|
||||
"rollup-plugin-terser": "^5.3.0",
|
||||
"rollup-plugin-typescript2": "^0.27.3",
|
||||
"rollup-plugin-vue": "^5.1.7",
|
||||
"typescript": "^3.9.2",
|
||||
"sass-loader": "^10.0.3",
|
||||
"ts-jest": "^26.4.1",
|
||||
"typescript": "~3.9.3",
|
||||
"vue": "^2.6.11",
|
||||
"vue-cli-plugin-storybook": "^1.3.0",
|
||||
"vue-jest": "^3.0.5",
|
||||
"vue-runtime-helpers": "^1.1.2",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vue-template-es2015-compiler": "^1.9.1",
|
||||
"watch": "^1.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"is-plain-object": "^3.0.0",
|
||||
"is-url": "^1.2.4",
|
||||
"nanoid": "^2.1.11",
|
||||
"vue-i18n": "^8.17.7"
|
||||
"bugs": {
|
||||
"url": "https://github.com/retailcrm/vue-formulario/issues"
|
||||
},
|
||||
"contributors": [
|
||||
"Justin Schroeder <justin@wearebraid.com>"
|
||||
],
|
||||
"keywords": [
|
||||
"vue",
|
||||
"form",
|
||||
"forms",
|
||||
"validation",
|
||||
"validate"
|
||||
],
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+ssh://git@github.com/retailcrm/vue-formulario.git"
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,45 @@
|
||||
import nanoid from 'nanoid/non-secure'
|
||||
import { AxiosResponse, AxiosError } from '@/axios.types'
|
||||
|
||||
interface FileItem {
|
||||
uuid: string;
|
||||
name: string;
|
||||
path: string | false;
|
||||
progress: number | false;
|
||||
error: any | false;
|
||||
complete: boolean;
|
||||
file: File;
|
||||
justFinished: boolean;
|
||||
removeFile(): void;
|
||||
previewData: string | false;
|
||||
}
|
||||
|
||||
interface ProgressSetter {
|
||||
(progress: number): void;
|
||||
}
|
||||
|
||||
interface ErrorHandler {
|
||||
(error: AxiosError): any;
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
/**
|
||||
* The file upload class holds and represents a file’s upload state durring
|
||||
* the upload flow.
|
||||
*/
|
||||
class FileUpload {
|
||||
/**
|
||||
* Create a file upload object.
|
||||
* @param {FileList} fileList
|
||||
* @param {object} context
|
||||
*/
|
||||
constructor (input, context, options = {}) {
|
||||
public input: DataTransfer
|
||||
public fileList: FileList
|
||||
public files: FileItem[]
|
||||
public options: Record<string, any>
|
||||
public context: Record<string, any>
|
||||
public results: any[] | boolean
|
||||
|
||||
constructor (input: DataTransfer, context: Record<string, any> = {}, options: Record<string, any> = {}) {
|
||||
this.input = input
|
||||
this.fileList = input.files
|
||||
this.files = []
|
||||
this.options = { ...{ mimes: {} }, ...options }
|
||||
this.options = { mimes: {}, ...options }
|
||||
this.results = false
|
||||
this.context = context
|
||||
if (Array.isArray(this.fileList)) {
|
||||
@ -27,9 +52,8 @@ class FileUpload {
|
||||
/**
|
||||
* Given a pre-existing array of files, create a faux FileList.
|
||||
* @param {array} items expects an array of objects [{ url: '/uploads/file.pdf' }]
|
||||
* @param {string} pathKey the object-key to access the url (defaults to "url")
|
||||
*/
|
||||
rehydrateFileList (items) {
|
||||
rehydrateFileList (items: any[]): void {
|
||||
const fauxFileList = items.reduce((fileList, item) => {
|
||||
const key = this.options ? this.options.fileUrlKey : 'url'
|
||||
const url = item[key]
|
||||
@ -48,15 +72,12 @@ class FileUpload {
|
||||
|
||||
/**
|
||||
* Produce an array of files and alert the callback.
|
||||
* @param {FileList}
|
||||
* @param {FileList} fileList
|
||||
*/
|
||||
addFileList (fileList) {
|
||||
addFileList (fileList: FileList): void {
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
const file = fileList[i]
|
||||
const file: File = fileList[i]
|
||||
const uuid = nanoid()
|
||||
const removeFile = function () {
|
||||
this.removeFile(uuid)
|
||||
}
|
||||
this.files.push({
|
||||
progress: false,
|
||||
error: false,
|
||||
@ -66,7 +87,8 @@ class FileUpload {
|
||||
file,
|
||||
uuid,
|
||||
path: false,
|
||||
removeFile: removeFile.bind(this),
|
||||
removeFile: () => this.removeFile(uuid),
|
||||
// @ts-ignore
|
||||
previewData: file.previewData || false
|
||||
})
|
||||
}
|
||||
@ -75,7 +97,7 @@ class FileUpload {
|
||||
/**
|
||||
* Check if the file has an.
|
||||
*/
|
||||
hasUploader () {
|
||||
hasUploader (): boolean {
|
||||
return !!this.context.uploader
|
||||
}
|
||||
|
||||
@ -85,40 +107,35 @@ class FileUpload {
|
||||
*
|
||||
* https://github.com/axios/axios/issues/737
|
||||
*/
|
||||
uploaderIsAxios () {
|
||||
if (
|
||||
this.hasUploader &&
|
||||
uploaderIsAxios (): boolean {
|
||||
return this.hasUploader &&
|
||||
typeof this.context.uploader.request === 'function' &&
|
||||
typeof this.context.uploader.get === 'function' &&
|
||||
typeof this.context.uploader.delete === 'function' &&
|
||||
typeof this.context.uploader.post === 'function'
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new uploader function.
|
||||
*/
|
||||
getUploader (...args) {
|
||||
getUploader (...args: [File, ProgressSetter, ErrorHandler, Record<string, any>]) {
|
||||
if (this.uploaderIsAxios()) {
|
||||
const formData = new FormData()
|
||||
formData.append(this.context.name || 'file', args[0])
|
||||
const data = new FormData()
|
||||
data.append(this.context.name || 'file', args[0])
|
||||
if (this.context.uploadUrl === false) {
|
||||
throw new Error('No uploadURL specified: https://vueformulate.com/guide/inputs/file/#props')
|
||||
}
|
||||
return this.context.uploader.post(this.context.uploadUrl, formData, {
|
||||
return this.context.uploader.post(this.context.uploadUrl, data, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
onUploadProgress: progressEvent => {
|
||||
onUploadProgress: (event: ProgressEvent) => {
|
||||
// args[1] here is the upload progress handler function
|
||||
args[1](Math.round((progressEvent.loaded * 100) / progressEvent.total))
|
||||
args[1](Math.round((event.loaded * 100) / event.total))
|
||||
}
|
||||
})
|
||||
.then(res => res.data)
|
||||
.catch(err => args[2](err))
|
||||
.then((response: AxiosResponse) => response.data)
|
||||
.catch(args[2])
|
||||
}
|
||||
return this.context.uploader(...args)
|
||||
}
|
||||
@ -137,7 +154,7 @@ class FileUpload {
|
||||
Promise.all(this.files.map(file => {
|
||||
return file.path ? Promise.resolve(file.path) : this.getUploader(
|
||||
file.file,
|
||||
(progress) => {
|
||||
progress => {
|
||||
file.progress = progress
|
||||
if (progress >= 100) {
|
||||
if (!file.complete) {
|
||||
@ -147,7 +164,7 @@ class FileUpload {
|
||||
file.complete = true
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
error => {
|
||||
file.progress = 0
|
||||
file.error = error
|
||||
file.complete = true
|
||||
@ -165,16 +182,15 @@ class FileUpload {
|
||||
|
||||
/**
|
||||
* Remove a file from the uploader (and the file list)
|
||||
* @param {string} uuid
|
||||
*/
|
||||
removeFile (uuid) {
|
||||
removeFile (uuid: string): void {
|
||||
this.files = this.files.filter(file => file.uuid !== uuid)
|
||||
this.context.performValidation()
|
||||
if (window && this.fileList instanceof FileList) {
|
||||
const transfer = new DataTransfer()
|
||||
this.files.map(file => transfer.items.add(file.file))
|
||||
this.files.map(({ file }) => transfer.items.add(file))
|
||||
this.fileList = transfer.files
|
||||
this.input.files = this.fileList
|
||||
this.input = transfer
|
||||
}
|
||||
}
|
||||
|
||||
@ -185,6 +201,7 @@ class FileUpload {
|
||||
this.files.map(file => {
|
||||
if (!file.previewData && window && window.FileReader && /^image\//.test(file.file.type)) {
|
||||
const reader = new FileReader()
|
||||
// @ts-ignore
|
||||
reader.onload = e => Object.assign(file, { previewData: e.target.result })
|
||||
reader.readAsDataURL(file.file)
|
||||
}
|
||||
@ -205,7 +222,7 @@ class FileUpload {
|
||||
return this.files
|
||||
}
|
||||
|
||||
toString () {
|
||||
toString (): string {
|
||||
const descriptor = this.files.length ? this.files.length + ' files' : 'empty'
|
||||
return this.results ? JSON.stringify(this.results, null, ' ') : `FileUpload(${descriptor})`
|
||||
}
|
@ -1,36 +1,42 @@
|
||||
import { cloneDeep } from './libs/utils'
|
||||
import FileUpload from './FileUpload'
|
||||
import FormularioForm from '@/FormularioForm.vue'
|
||||
|
||||
export default class FormSubmission {
|
||||
public form: FormularioForm
|
||||
|
||||
/**
|
||||
* Initialize a formulario form.
|
||||
* @param {vm} form an instance of FormularioForm
|
||||
*/
|
||||
constructor (form) {
|
||||
constructor (form: FormularioForm) {
|
||||
this.form = form
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the form has any validation errors.
|
||||
*
|
||||
* @return {Promise} resolves a boolean
|
||||
*/
|
||||
hasValidationErrors () {
|
||||
return this.form.hasValidationErrors()
|
||||
hasValidationErrors (): Promise<boolean> {
|
||||
return (this.form as any).hasValidationErrors()
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously generate the values payload of this form.
|
||||
* @return {Promise} resolves to json
|
||||
*/
|
||||
values () {
|
||||
values (): Promise<Record<string, any>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const form = this.form as any
|
||||
const pending = []
|
||||
const values = cloneDeep(this.form.proxy)
|
||||
const values = cloneDeep(form.proxy)
|
||||
|
||||
for (const key in values) {
|
||||
if (typeof this.form.proxy[key] === 'object' && this.form.proxy[key] instanceof FileUpload) {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(values, key) &&
|
||||
typeof form.proxy[key] === 'object' &&
|
||||
form.proxy[key] instanceof FileUpload
|
||||
) {
|
||||
pending.push(
|
||||
this.form.proxy[key].upload().then(data => Object.assign(values, { [key]: data }))
|
||||
form.proxy[key].upload().then((data: Record<string, any>) => Object.assign(values, { [key]: data }))
|
||||
)
|
||||
}
|
||||
}
|
@ -1,267 +0,0 @@
|
||||
import library from './libs/library'
|
||||
import rules from './libs/rules'
|
||||
import mimes from './libs/mimes'
|
||||
import FileUpload from './FileUpload'
|
||||
import RuleValidationMessages from './RuleValidationMessages'
|
||||
import { arrayify, parseLocale, has } from './libs/utils'
|
||||
import isPlainObject from 'is-plain-object'
|
||||
import fauxUploader from './libs/faux-uploader'
|
||||
import FormularioForm from './FormularioForm.vue'
|
||||
import FormularioInput from './FormularioInput.vue'
|
||||
import FormularioGrouping from './FormularioGrouping.vue'
|
||||
|
||||
/**
|
||||
* The base formulario library.
|
||||
*/
|
||||
class Formulario {
|
||||
/**
|
||||
* Instantiate our base options.
|
||||
*/
|
||||
constructor () {
|
||||
this.options = {}
|
||||
this.defaults = {
|
||||
components: {
|
||||
FormularioForm,
|
||||
FormularioInput,
|
||||
FormularioGrouping,
|
||||
},
|
||||
library,
|
||||
rules,
|
||||
mimes,
|
||||
locale: false,
|
||||
uploader: fauxUploader,
|
||||
uploadUrl: false,
|
||||
fileUrlKey: 'url',
|
||||
uploadJustCompleteDuration: 1000,
|
||||
errorHandler: (err) => err,
|
||||
plugins: [ RuleValidationMessages ],
|
||||
validationMessages: {},
|
||||
idPrefix: 'formulario-'
|
||||
}
|
||||
this.registry = new Map()
|
||||
this.idRegistry = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install vue formulario, and register it’s components.
|
||||
*/
|
||||
install (Vue, options) {
|
||||
Vue.prototype.$formulario = this
|
||||
this.options = this.defaults
|
||||
var plugins = this.defaults.plugins
|
||||
if (options && Array.isArray(options.plugins) && options.plugins.length) {
|
||||
plugins = plugins.concat(options.plugins)
|
||||
}
|
||||
plugins.forEach(plugin => (typeof plugin === 'function') ? plugin(this) : null)
|
||||
this.extend(options || {})
|
||||
for (var componentName in this.options.components) {
|
||||
Vue.component(componentName, this.options.components[componentName])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce a deterministically generated id based on the sequence by which it
|
||||
* was requested. This should be *theoretically* the same SSR as client side.
|
||||
* However, SSR and deterministic ids can be very challenging, so this
|
||||
* implementation is open to community review.
|
||||
*/
|
||||
nextId (vm) {
|
||||
const path = vm.$route && vm.$route.path ? vm.$route.path : false
|
||||
const pathPrefix = path ? vm.$route.path.replace(/[/\\.\s]/g, '-') : 'global'
|
||||
if (!Object.prototype.hasOwnProperty.call(this.idRegistry, pathPrefix)) {
|
||||
this.idRegistry[pathPrefix] = 0
|
||||
}
|
||||
return `${this.options.idPrefix}${pathPrefix}-${++this.idRegistry[pathPrefix]}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a set of options, apply them to the pre-existing options.
|
||||
* @param {Object} extendWith
|
||||
*/
|
||||
extend (extendWith) {
|
||||
if (typeof extendWith === 'object') {
|
||||
this.options = this.merge(this.options, extendWith)
|
||||
return this
|
||||
}
|
||||
throw new Error(`VueFormulario extend() should be passed an object (was ${typeof extendWith})`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new object by copying properties of base and mergeWith.
|
||||
* Note: arrays don't overwrite - they push
|
||||
*
|
||||
* @param {Object} base
|
||||
* @param {Object} mergeWith
|
||||
* @param {boolean} concatArrays
|
||||
*/
|
||||
merge (base, mergeWith, concatArrays = true) {
|
||||
var merged = {}
|
||||
for (var key in base) {
|
||||
if (mergeWith.hasOwnProperty(key)) {
|
||||
if (isPlainObject(mergeWith[key]) && isPlainObject(base[key])) {
|
||||
merged[key] = this.merge(base[key], mergeWith[key], concatArrays)
|
||||
} else if (concatArrays && Array.isArray(base[key]) && Array.isArray(mergeWith[key])) {
|
||||
merged[key] = base[key].concat(mergeWith[key])
|
||||
} else {
|
||||
merged[key] = mergeWith[key]
|
||||
}
|
||||
} else {
|
||||
merged[key] = base[key]
|
||||
}
|
||||
}
|
||||
for (var prop in mergeWith) {
|
||||
if (!merged.hasOwnProperty(prop)) {
|
||||
merged[prop] = mergeWith[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
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validation rules by merging any passed in with global rules.
|
||||
* @return {object} object of validation functions
|
||||
*/
|
||||
rules (rules = {}) {
|
||||
return { ...this.options.rules, ...rules }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation message for a particular error.
|
||||
*/
|
||||
validationMessage (rule, validationContext, vm) {
|
||||
if (this.options.validationMessages.hasOwnProperty(rule)) {
|
||||
return this.options.validationMessages[rule](vm, validationContext)
|
||||
} else {
|
||||
return this.options.validationMessages['default'](vm, validationContext)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an instance of a FormularioForm register it.
|
||||
* @param {vm} form
|
||||
*/
|
||||
register (form) {
|
||||
if (form.$options.name === 'FormularioForm' && form.name) {
|
||||
this.registry.set(form.name, form)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an instance of a form, remove it from the registry.
|
||||
* @param {vm} form
|
||||
*/
|
||||
deregister (form) {
|
||||
if (
|
||||
form.$options.name === 'FormularioForm' &&
|
||||
form.name &&
|
||||
this.registry.has(form.name)
|
||||
) {
|
||||
this.registry.delete(form.name)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array, this function will attempt to make sense of the given error
|
||||
* and hydrate a form with the resulting errors.
|
||||
*
|
||||
* @param {error} err
|
||||
* @param {string} formName
|
||||
* @param {error}
|
||||
*/
|
||||
handle (err, formName, skip = false) {
|
||||
const e = skip ? err : this.options.errorHandler(err, formName)
|
||||
if (formName && this.registry.has(formName)) {
|
||||
this.registry.get(formName).applyErrors({
|
||||
formErrors: arrayify(e.formErrors),
|
||||
inputErrors: e.inputErrors || {}
|
||||
})
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset a form.
|
||||
* @param {string} formName
|
||||
* @param {object} initialValue
|
||||
*/
|
||||
reset (formName, initialValue = {}) {
|
||||
this.resetValidation(formName)
|
||||
this.setValues(formName, initialValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the form's validation messages.
|
||||
* @param {string} formName
|
||||
*/
|
||||
resetValidation (formName) {
|
||||
const form = this.registry.get(formName)
|
||||
form.hideErrors(formName)
|
||||
form.namedErrors = []
|
||||
form.namedFieldErrors = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the form values.
|
||||
* @param {string} formName
|
||||
* @param {object} values
|
||||
*/
|
||||
setValues (formName, values) {
|
||||
if (values && !Array.isArray(values) && typeof values === 'object') {
|
||||
const form = this.registry.get(formName)
|
||||
form.setValues({ ...values })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file uploader.
|
||||
*/
|
||||
getUploader () {
|
||||
return this.options.uploader || false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the global upload url.
|
||||
*/
|
||||
getUploadUrl () {
|
||||
return this.options.uploadUrl || false
|
||||
}
|
||||
|
||||
/**
|
||||
* When re-hydrating a file uploader with an array, get the sub-object key to
|
||||
* access the url of the file. Usually this is just "url".
|
||||
*/
|
||||
getFileUrlKey () {
|
||||
return this.options.fileUrlKey || 'url'
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of an upload.
|
||||
*/
|
||||
createUpload (fileList, context) {
|
||||
return new FileUpload(fileList, context, this.options)
|
||||
}
|
||||
}
|
||||
|
||||
export default new Formulario()
|
244
src/Formulario.ts
Normal file
244
src/Formulario.ts
Normal file
@ -0,0 +1,244 @@
|
||||
import { VueConstructor } from 'vue'
|
||||
|
||||
import library from '@/libs/library'
|
||||
import rules from '@/validation/rules'
|
||||
import mimes from '@/libs/mimes'
|
||||
import FileUpload from '@/FileUpload'
|
||||
import RuleValidationMessages from '@/RuleValidationMessages'
|
||||
import { arrayify, has } from '@/libs/utils'
|
||||
import fauxUploader from '@/libs/faux-uploader'
|
||||
import merge from '@/utils/merge'
|
||||
|
||||
import FormularioForm from '@/FormularioForm.vue'
|
||||
import FormularioInput from '@/FormularioInput.vue'
|
||||
import FormularioGrouping from '@/FormularioGrouping.vue'
|
||||
import { ValidationContext } from '@/validation/types'
|
||||
|
||||
interface ErrorHandler {
|
||||
(error: any, formName?: string): any;
|
||||
}
|
||||
|
||||
interface FormularioOptions {
|
||||
components?: { [name: string]: VueConstructor };
|
||||
plugins?: any[];
|
||||
library?: any;
|
||||
rules?: any;
|
||||
mimes?: any;
|
||||
locale?: any;
|
||||
uploader?: any;
|
||||
uploadUrl?: any;
|
||||
fileUrlKey?: any;
|
||||
errorHandler?: ErrorHandler;
|
||||
uploadJustCompleteDuration?: any;
|
||||
validationMessages?: any;
|
||||
idPrefix?: string;
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
/**
|
||||
* The base formulario library.
|
||||
*/
|
||||
export default class Formulario {
|
||||
public options: FormularioOptions
|
||||
public defaults: FormularioOptions
|
||||
public registry: Map<string, FormularioForm>
|
||||
public idRegistry: { [name: string]: number }
|
||||
|
||||
/**
|
||||
* Instantiate our base options.
|
||||
*/
|
||||
constructor () {
|
||||
this.options = {}
|
||||
this.defaults = {
|
||||
components: {
|
||||
FormularioForm,
|
||||
FormularioInput,
|
||||
FormularioGrouping,
|
||||
},
|
||||
library,
|
||||
rules,
|
||||
mimes,
|
||||
locale: false,
|
||||
uploader: fauxUploader,
|
||||
uploadUrl: false,
|
||||
fileUrlKey: 'url',
|
||||
uploadJustCompleteDuration: 1000,
|
||||
errorHandler: (error: any) => error,
|
||||
plugins: [RuleValidationMessages],
|
||||
validationMessages: {},
|
||||
idPrefix: 'formulario-'
|
||||
}
|
||||
this.registry = new Map()
|
||||
this.idRegistry = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install vue formulario, and register it’s components.
|
||||
*/
|
||||
install (Vue: VueConstructor, options?: FormularioOptions) {
|
||||
Vue.prototype.$formulario = this
|
||||
this.options = this.defaults
|
||||
let plugins = this.defaults.plugins as any[]
|
||||
if (options && Array.isArray(options.plugins) && options.plugins.length) {
|
||||
plugins = plugins.concat(options.plugins)
|
||||
}
|
||||
plugins.forEach(plugin => (typeof plugin === 'function') ? plugin(this) : null)
|
||||
this.extend(options || {})
|
||||
for (const componentName in this.options.components) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.options.components, componentName)) {
|
||||
Vue.component(componentName, this.options.components[componentName])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce a deterministically generated id based on the sequence by which it
|
||||
* was requested. This should be *theoretically* the same SSR as client side.
|
||||
* However, SSR and deterministic ids can be very challenging, so this
|
||||
* implementation is open to community review.
|
||||
*/
|
||||
nextId (vm: Vue) {
|
||||
const options = this.options as FormularioOptions
|
||||
const path = vm.$route && vm.$route.path ? vm.$route.path : false
|
||||
const pathPrefix = path ? vm.$route.path.replace(/[/\\.\s]/g, '-') : 'global'
|
||||
if (!has(this.idRegistry, pathPrefix)) {
|
||||
this.idRegistry[pathPrefix] = 0
|
||||
}
|
||||
return `${options.idPrefix}${pathPrefix}-${++this.idRegistry[pathPrefix]}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a set of options, apply them to the pre-existing options.
|
||||
*/
|
||||
extend (extendWith: FormularioOptions) {
|
||||
if (typeof extendWith === 'object') {
|
||||
this.options = merge(this.options as FormularioOptions, extendWith)
|
||||
return this
|
||||
}
|
||||
throw new Error(`VueFormulario extend() should be passed an object (was ${typeof extendWith})`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validation rules by merging any passed in with global rules.
|
||||
*/
|
||||
rules (rules: Record<string, any> = {}) {
|
||||
return { ...this.options.rules, ...rules }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation message for a particular error.
|
||||
*/
|
||||
validationMessage (rule: string, context: ValidationContext, vm: Vue) {
|
||||
if (has(this.options.validationMessages, rule)) {
|
||||
return this.options.validationMessages[rule](vm, context)
|
||||
} else {
|
||||
return this.options.validationMessages.default(vm, context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an instance of a FormularioForm register it.
|
||||
*/
|
||||
register (form: FormularioForm) {
|
||||
// @ts-ignore
|
||||
if (form.$options.name === 'FormularioForm' && form.name) {
|
||||
// @ts-ignore
|
||||
this.registry.set(form.name, form)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an instance of a form, remove it from the registry.
|
||||
*/
|
||||
deregister (form: FormularioForm) {
|
||||
if (
|
||||
form.$options.name === 'FormularioForm' &&
|
||||
// @ts-ignore
|
||||
form.name &&
|
||||
// @ts-ignore
|
||||
this.registry.has(form.name as string)
|
||||
) {
|
||||
// @ts-ignore
|
||||
this.registry.delete(form.name as string)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array, this function will attempt to make sense of the given error
|
||||
* and hydrate a form with the resulting errors.
|
||||
*/
|
||||
handle (error: any, formName: string, skip: boolean = false) {
|
||||
// @ts-ignore
|
||||
const e = skip ? error : this.options.errorHandler(error, formName)
|
||||
if (formName && this.registry.has(formName)) {
|
||||
const form = this.registry.get(formName) as FormularioForm
|
||||
// @ts-ignore
|
||||
form.applyErrors({
|
||||
formErrors: arrayify(e.formErrors),
|
||||
inputErrors: e.inputErrors || {}
|
||||
})
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset a form.
|
||||
*/
|
||||
reset (formName: string, initialValue: Object = {}) {
|
||||
this.resetValidation(formName)
|
||||
this.setValues(formName, initialValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the form's validation messages.
|
||||
*/
|
||||
resetValidation (formName: string) {
|
||||
const form = this.registry.get(formName) as FormularioForm
|
||||
// @ts-ignore
|
||||
form.hideErrors(formName)
|
||||
// @ts-ignore
|
||||
form.namedErrors = []
|
||||
// @ts-ignore
|
||||
form.namedFieldErrors = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the form values.
|
||||
*/
|
||||
setValues (formName: string, values?: Record<string, any>) {
|
||||
if (values) {
|
||||
const form = this.registry.get(formName) as FormularioForm
|
||||
// @ts-ignore
|
||||
form.setValues({ ...values })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file uploader.
|
||||
*/
|
||||
getUploader () {
|
||||
return this.options.uploader || false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the global upload url.
|
||||
*/
|
||||
getUploadUrl (): string | boolean {
|
||||
return this.options.uploadUrl || false
|
||||
}
|
||||
|
||||
/**
|
||||
* When re-hydrating a file uploader with an array, get the sub-object key to
|
||||
* access the url of the file. Usually this is just "url".
|
||||
*/
|
||||
getFileUrlKey () {
|
||||
return this.options.fileUrlKey || 'url'
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of an upload.
|
||||
*/
|
||||
createUpload (data: DataTransfer, context: Record<string, any>) {
|
||||
return new FileUpload(data, context, this.options)
|
||||
}
|
||||
}
|
@ -16,13 +16,16 @@
|
||||
>
|
||||
<img
|
||||
:src="file.previewData"
|
||||
alt=""
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="formulario-file-name"
|
||||
:title="file.name"
|
||||
v-text="file.name"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="file.progress !== false"
|
||||
:data-just-finished="file.justFinished"
|
||||
@ -34,12 +37,14 @@
|
||||
:style="{width: file.progress + '%'}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="(file.complete && !file.justFinished) || file.progress === false"
|
||||
class="formulario-file-remove"
|
||||
@click="file.removeFile"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="file.error"
|
||||
class="formulario-file-upload-error"
|
||||
@ -54,21 +59,25 @@ import FileUpload from './FileUpload'
|
||||
|
||||
export default {
|
||||
name: 'FormularioFiles',
|
||||
|
||||
props: {
|
||||
files: {
|
||||
type: FileUpload,
|
||||
required: true
|
||||
},
|
||||
|
||||
imagePreview: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
fileUploads () {
|
||||
return this.files.files || []
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
files () {
|
||||
if (this.imagePreview) {
|
||||
@ -76,6 +85,7 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
if (this.imagePreview) {
|
||||
this.files.loadPreviews()
|
||||
|
@ -1,167 +1,274 @@
|
||||
<template>
|
||||
<form
|
||||
:class="classes"
|
||||
@submit.prevent="formSubmitted"
|
||||
>
|
||||
<form @submit.prevent="formSubmitted">
|
||||
<slot :errors="mergedFormErrors" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { arrayify, has } from './libs/utils'
|
||||
import useRegistry, { useRegistryComputed, useRegistryMethods, useRegistryProviders } from './libs/registry'
|
||||
import FormSubmission from './FormSubmission'
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import {
|
||||
Component,
|
||||
Model,
|
||||
Prop,
|
||||
Provide,
|
||||
Watch,
|
||||
} from 'vue-property-decorator'
|
||||
import { arrayify, getNested, has, setNested, shallowEqualObjects } from '@/libs/utils'
|
||||
import Registry from '@/libs/registry'
|
||||
import FormSubmission from '@/FormSubmission'
|
||||
import FormularioInput from '@/FormularioInput.vue'
|
||||
|
||||
export default {
|
||||
provide () {
|
||||
return {
|
||||
...useRegistryProviders(this),
|
||||
observeErrors: this.addErrorObserver,
|
||||
removeErrorObserver: this.removeErrorObserver,
|
||||
formularioFieldValidation: this.formularioFieldValidation,
|
||||
path: ''
|
||||
}
|
||||
},
|
||||
name: 'FormularioForm',
|
||||
model: {
|
||||
prop: 'formularioValue',
|
||||
event: 'input'
|
||||
},
|
||||
props: {
|
||||
name: {
|
||||
type: [String, Boolean],
|
||||
default: false
|
||||
},
|
||||
formularioValue: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
values: {
|
||||
type: [Object, Boolean],
|
||||
default: false
|
||||
},
|
||||
errors: {
|
||||
type: [Object, Boolean],
|
||||
default: false
|
||||
},
|
||||
formErrors: {
|
||||
type: Array,
|
||||
default: () => ([])
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
...useRegistry(this),
|
||||
formShouldShowErrors: false,
|
||||
errorObservers: [],
|
||||
namedErrors: [],
|
||||
namedFieldErrors: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...useRegistryComputed(),
|
||||
classes () {
|
||||
const classes = { 'formulario-form': true }
|
||||
if (this.name) {
|
||||
classes[`formulario-form--${this.name}`] = true
|
||||
@Component
|
||||
export default class FormularioForm extends Vue {
|
||||
@Provide() formularioFieldValidation (errorObject): void {
|
||||
this.$emit('validation', errorObject)
|
||||
}
|
||||
|
||||
@Provide() formularioRegister = this.register
|
||||
@Provide() formularioDeregister = this.deregister
|
||||
@Provide() formularioSetter = this.setFieldValue
|
||||
@Provide() getFormValues = (): Record<string, any> => this.proxy
|
||||
@Provide() path = ''
|
||||
|
||||
@Provide() removeErrorObserver (observer): void {
|
||||
this.errorObservers = this.errorObservers.filter(obs => obs.callback !== observer)
|
||||
}
|
||||
|
||||
@Model('input', {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}) readonly formularioValue!: Record<string, any>
|
||||
|
||||
@Prop({
|
||||
type: [String, Boolean],
|
||||
default: false
|
||||
}) public readonly name!: string | boolean
|
||||
|
||||
@Prop({
|
||||
type: [Object, Boolean],
|
||||
default: false
|
||||
}) readonly values!: Record<string, any> | boolean
|
||||
|
||||
@Prop({ default: false }) readonly errors!: Record<string, any> | boolean
|
||||
@Prop({ default: () => ([]) }) readonly formErrors!: []
|
||||
|
||||
public proxy: Record<string, any> = {}
|
||||
|
||||
registry: Registry = new Registry(this)
|
||||
|
||||
childrenShouldShowErrors = false
|
||||
|
||||
formShouldShowErrors = false
|
||||
|
||||
errorObservers: [] = []
|
||||
|
||||
namedErrors: [] = []
|
||||
|
||||
namedFieldErrors: Record<string, any> = {}
|
||||
|
||||
get mergedFormErrors (): Record<string, any> {
|
||||
return this.formErrors.concat(this.namedErrors)
|
||||
}
|
||||
|
||||
get mergedFieldErrors (): Record<string, any> {
|
||||
const errors = {}
|
||||
|
||||
if (this.errors) {
|
||||
for (const fieldName in this.errors) {
|
||||
errors[fieldName] = arrayify(this.errors[fieldName])
|
||||
}
|
||||
return classes
|
||||
},
|
||||
mergedFormErrors () {
|
||||
return this.formErrors.concat(this.namedErrors)
|
||||
},
|
||||
mergedFieldErrors () {
|
||||
const errors = {}
|
||||
if (this.errors) {
|
||||
for (const fieldName in this.errors) {
|
||||
errors[fieldName] = arrayify(this.errors[fieldName])
|
||||
}
|
||||
}
|
||||
for (const fieldName in this.namedFieldErrors) {
|
||||
errors[fieldName] = arrayify(this.namedFieldErrors[fieldName])
|
||||
}
|
||||
return errors
|
||||
},
|
||||
hasFormErrorObservers () {
|
||||
return !!this.errorObservers.filter(o => o.type === 'form').length
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
formularioValue: {
|
||||
handler (values) {
|
||||
if (this.isVmodeled &&
|
||||
values &&
|
||||
typeof values === 'object'
|
||||
) {
|
||||
this.setValues(values)
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
mergedFormErrors (errors) {
|
||||
this.errorObservers
|
||||
.filter(o => o.type === 'form')
|
||||
.forEach(o => o.callback(errors))
|
||||
},
|
||||
mergedFieldErrors: {
|
||||
handler (errors) {
|
||||
this.errorObservers
|
||||
.filter(o => o.type === 'input')
|
||||
.forEach(o => o.callback(errors[o.field] || []))
|
||||
},
|
||||
immediate: true
|
||||
|
||||
for (const fieldName in this.namedFieldErrors) {
|
||||
errors[fieldName] = arrayify(this.namedFieldErrors[fieldName])
|
||||
}
|
||||
},
|
||||
created () {
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
get hasFormErrorObservers (): boolean {
|
||||
return this.errorObservers.some(o => o.type === 'form')
|
||||
}
|
||||
|
||||
get hasInitialValue (): boolean {
|
||||
return (
|
||||
(this.formularioValue && typeof this.formularioValue === 'object') ||
|
||||
(this.values && typeof this.values === 'object') ||
|
||||
(this.isGrouping && typeof this.context.model[this.index] === 'object')
|
||||
)
|
||||
}
|
||||
|
||||
get isVmodeled (): boolean {
|
||||
return !!(has(this.$options.propsData, 'formularioValue') &&
|
||||
this._events &&
|
||||
Array.isArray(this._events.input) &&
|
||||
this._events.input.length)
|
||||
}
|
||||
|
||||
get initialValues (): Record<string, any> {
|
||||
if (
|
||||
has(this.$options.propsData, 'formularioValue') &&
|
||||
typeof this.formularioValue === 'object'
|
||||
) {
|
||||
// If there is a v-model on the form/group, use those values as first priority
|
||||
return Object.assign({}, this.formularioValue) // @todo - use a deep clone to detach reference types
|
||||
} else if (
|
||||
has(this.$options.propsData, 'values') &&
|
||||
typeof this.values === 'object'
|
||||
) {
|
||||
// If there are values, use them as secondary priority
|
||||
return Object.assign({}, this.values)
|
||||
} else if (
|
||||
this.isGrouping && typeof this.context.model[this.index] === 'object'
|
||||
) {
|
||||
return this.context.model[this.index]
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
@Watch('formularioValue', { deep: true })
|
||||
onFormularioValueChanged (values): void {
|
||||
if (this.isVmodeled && values && typeof values === 'object') {
|
||||
this.setValues(values)
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('mergedFormErrors')
|
||||
onMergedFormErrorsChanged (errors): void {
|
||||
this.errorObservers
|
||||
.filter(o => o.type === 'form')
|
||||
.forEach(o => o.callback(errors))
|
||||
}
|
||||
|
||||
@Watch('mergedFieldErrors', { immediate: true })
|
||||
onMergedFieldErrorsChanged (errors): void {
|
||||
this.errorObservers
|
||||
.filter(o => o.type === 'input')
|
||||
.forEach(o => o.callback(errors[o.field] || []))
|
||||
}
|
||||
|
||||
created (): void {
|
||||
this.$formulario.register(this)
|
||||
this.applyInitialValues()
|
||||
},
|
||||
destroyed () {
|
||||
}
|
||||
|
||||
destroyed (): void {
|
||||
this.$formulario.deregister(this)
|
||||
},
|
||||
methods: {
|
||||
...useRegistryMethods(),
|
||||
applyErrors ({ formErrors, inputErrors }) {
|
||||
// given an object of errors, apply them to this form
|
||||
this.namedErrors = formErrors
|
||||
this.namedFieldErrors = inputErrors
|
||||
},
|
||||
addErrorObserver (observer) {
|
||||
if (!this.errorObservers.find(obs => observer.callback === obs.callback)) {
|
||||
this.errorObservers.push(observer)
|
||||
if (observer.type === 'form') {
|
||||
observer.callback(this.mergedFormErrors)
|
||||
} else if (has(this.mergedFieldErrors, observer.field)) {
|
||||
observer.callback(this.mergedFieldErrors[observer.field])
|
||||
}
|
||||
}
|
||||
|
||||
public register (field: string, component: FormularioInput): void {
|
||||
this.registry.register(field, component)
|
||||
}
|
||||
|
||||
public deregister (field: string): void {
|
||||
this.registry.remove(field)
|
||||
}
|
||||
|
||||
applyErrors ({ formErrors, inputErrors }): void {
|
||||
// given an object of errors, apply them to this form
|
||||
this.namedErrors = formErrors
|
||||
this.namedFieldErrors = inputErrors
|
||||
}
|
||||
|
||||
@Provide()
|
||||
addErrorObserver (observer: ErrorObserver): void {
|
||||
if (!this.errorObservers.find(obs => observer.callback === obs.callback)) {
|
||||
this.errorObservers.push(observer)
|
||||
if (observer.type === 'form') {
|
||||
observer.callback(this.mergedFormErrors)
|
||||
} else if (has(this.mergedFieldErrors, observer.field)) {
|
||||
observer.callback(this.mergedFieldErrors[observer.field])
|
||||
}
|
||||
},
|
||||
removeErrorObserver (observer) {
|
||||
this.errorObservers = this.errorObservers.filter(obs => obs.callback !== observer)
|
||||
},
|
||||
registerErrorComponent (component) {
|
||||
if (!this.errorComponents.includes(component)) {
|
||||
this.errorComponents.push(component)
|
||||
}
|
||||
},
|
||||
formSubmitted () {
|
||||
// perform validation here
|
||||
this.showErrors()
|
||||
const submission = new FormSubmission(this)
|
||||
this.$emit('submit-raw', submission)
|
||||
return submission.hasValidationErrors()
|
||||
.then(hasErrors => hasErrors ? undefined : submission.values())
|
||||
.then(data => {
|
||||
if (typeof data !== 'undefined') {
|
||||
this.$emit('submit', data)
|
||||
return data
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
},
|
||||
formularioFieldValidation (errorObject) {
|
||||
this.$emit('validation', errorObject)
|
||||
}
|
||||
}
|
||||
|
||||
registerErrorComponent (component): void {
|
||||
if (!this.errorComponents.includes(component)) {
|
||||
this.errorComponents.push(component)
|
||||
}
|
||||
}
|
||||
|
||||
formSubmitted (): Promise<void> {
|
||||
// perform validation here
|
||||
this.showErrors()
|
||||
const submission = new FormSubmission(this)
|
||||
this.$emit('submit-raw', submission)
|
||||
return submission.hasValidationErrors()
|
||||
.then(hasErrors => hasErrors ? undefined : submission.values())
|
||||
.then(data => {
|
||||
if (typeof data !== 'undefined') {
|
||||
this.$emit('submit', data)
|
||||
return data
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
applyInitialValues (): void {
|
||||
if (this.hasInitialValue) {
|
||||
this.proxy = this.initialValues
|
||||
}
|
||||
}
|
||||
|
||||
setFieldValue (field, value): void {
|
||||
if (value === undefined) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { [field]: value, ...proxy } = this.proxy
|
||||
this.proxy = proxy
|
||||
} else {
|
||||
setNested(this.proxy, field, value)
|
||||
}
|
||||
this.$emit('input', Object.assign({}, this.proxy))
|
||||
}
|
||||
|
||||
hasValidationErrors (): Promise<boolean> {
|
||||
return Promise.all(this.registry.reduce((resolvers, cmp) => {
|
||||
resolvers.push(cmp.performValidation() && cmp.getValidationErrors())
|
||||
return resolvers
|
||||
}, [])).then(errorObjects => errorObjects.some(item => item.hasErrors))
|
||||
}
|
||||
|
||||
showErrors (): void {
|
||||
this.childrenShouldShowErrors = true
|
||||
this.registry.forEach((input: FormularioInput) => {
|
||||
input.formShouldShowErrors = true
|
||||
})
|
||||
}
|
||||
|
||||
hideErrors (): void {
|
||||
this.childrenShouldShowErrors = false
|
||||
this.registry.forEach((input: FormularioInput) => {
|
||||
input.formShouldShowErrors = false
|
||||
input.behavioralErrorVisibility = false
|
||||
})
|
||||
}
|
||||
|
||||
setValues (values: Record<string, any>): void {
|
||||
// Collect all keys, existing and incoming
|
||||
const keys = Array.from(new Set(Object.keys(values).concat(Object.keys(this.proxy))))
|
||||
keys.forEach(field => {
|
||||
if (this.registry.hasNested(field)) {
|
||||
this.registry.getNested(field).forEach((registryField, registryKey) => {
|
||||
if (
|
||||
!shallowEqualObjects(
|
||||
getNested(values, registryKey),
|
||||
getNested(this.proxy, registryKey)
|
||||
)
|
||||
) {
|
||||
this.setFieldValue(registryKey, getNested(values, registryKey))
|
||||
}
|
||||
|
||||
if (
|
||||
!shallowEqualObjects(
|
||||
getNested(values, registryKey),
|
||||
this.registry.get(registryKey).proxy
|
||||
)
|
||||
) {
|
||||
this.registry.get(registryKey).context.model = getNested(values, registryKey)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
this.applyInitialValues()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -7,37 +7,35 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FormularioGrouping',
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
isArrayItem: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
provide () {
|
||||
return {
|
||||
path: this.groupPath
|
||||
}
|
||||
},
|
||||
inject: ['path'],
|
||||
computed: {
|
||||
groupPath () {
|
||||
if (this.isArrayItem) {
|
||||
return this.path + '[' + this.name + ']';
|
||||
} else {
|
||||
if (this.path === '') {
|
||||
return this.name;
|
||||
}
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import {
|
||||
Component,
|
||||
Inject,
|
||||
Prop,
|
||||
Provide,
|
||||
} from 'vue-property-decorator'
|
||||
|
||||
return this.path + '.' + this.name;
|
||||
}
|
||||
@Component
|
||||
export default class FormularioGrouping extends Vue {
|
||||
@Inject({ default: '' }) path!: string
|
||||
|
||||
@Prop({ required: true })
|
||||
readonly name!: string
|
||||
|
||||
@Prop({ default: false })
|
||||
readonly isArrayItem!: boolean
|
||||
|
||||
@Provide('path') get groupPath (): string {
|
||||
if (this.isArrayItem) {
|
||||
return `${this.path}[${this.name}]`
|
||||
}
|
||||
|
||||
if (this.path === '') {
|
||||
return this.name
|
||||
}
|
||||
|
||||
return `${this.path}.${this.name}`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -5,336 +5,514 @@
|
||||
:data-is-showing-errors="hasVisibleErrors"
|
||||
:data-type="type"
|
||||
>
|
||||
<slot :id="id" :context="context" :errors="errors" :validationErrors="validationErrors" />
|
||||
<slot
|
||||
:id="id"
|
||||
:context="context"
|
||||
:errors="errors"
|
||||
:validationErrors="validationErrors"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import context from './libs/context'
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import {
|
||||
Component,
|
||||
Inject,
|
||||
Model,
|
||||
Prop,
|
||||
Provide,
|
||||
Watch,
|
||||
} from 'vue-property-decorator'
|
||||
import { shallowEqualObjects, parseRules, snakeToCamel, has, arrayify, groupBails } from './libs/utils'
|
||||
import { ValidationError } from '@/validation/types'
|
||||
|
||||
export default {
|
||||
name: 'FormularioInput',
|
||||
inheritAttrs: false,
|
||||
provide () {
|
||||
return {
|
||||
// Allows sub-components of this input to register arbitrary rules.
|
||||
formularioRegisterRule: this.registerRule,
|
||||
formularioRemoveRule: this.removeRule
|
||||
const ERROR_BEHAVIOR = {
|
||||
BLUR: 'blur',
|
||||
LIVE: 'live',
|
||||
SUBMIT: 'submit',
|
||||
}
|
||||
|
||||
@Component({ inheritAttrs: false })
|
||||
export default class FormularioInput extends Vue {
|
||||
@Inject({ default: undefined }) formularioSetter!: Function|undefined
|
||||
@Inject({ default: () => (): void => {} }) formularioFieldValidation!: Function
|
||||
@Inject({ default: undefined }) formularioRegister!: Function|undefined
|
||||
@Inject({ default: undefined }) formularioDeregister!: Function|undefined
|
||||
@Inject({ default: () => (): Record<string, any> => ({}) }) getFormValues!: Function
|
||||
@Inject({ default: undefined }) addErrorObserver!: Function|undefined
|
||||
@Inject({ default: undefined }) removeErrorObserver!: Function|undefined
|
||||
@Inject({ default: '' }) path!: string
|
||||
|
||||
@Provide() formularioRegisterRule = this.registerRule
|
||||
@Provide() formularioRemoveRule = this.removeRule
|
||||
|
||||
@Model('input', {
|
||||
default: '',
|
||||
}) formularioValue: any
|
||||
|
||||
@Prop({
|
||||
type: [String, Number, Boolean],
|
||||
default: false,
|
||||
}) id!: string|number|boolean
|
||||
|
||||
@Prop({ default: 'text' }) type!: string
|
||||
@Prop({ required: true }) name!: string|boolean
|
||||
@Prop({ default: false }) value!: any
|
||||
|
||||
@Prop({
|
||||
type: [String, Boolean, Array],
|
||||
default: false,
|
||||
}) validation
|
||||
|
||||
@Prop({
|
||||
type: [String, Boolean],
|
||||
default: false,
|
||||
}) validationName!: string|boolean
|
||||
|
||||
@Prop({
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
}) validationRules!: Record<string, any>
|
||||
|
||||
@Prop({
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
}) validationMessages!: Record<string, any>
|
||||
|
||||
@Prop({
|
||||
type: [Array, String, Boolean],
|
||||
default: false,
|
||||
}) errors!: []|string|boolean
|
||||
|
||||
@Prop({
|
||||
type: String,
|
||||
default: ERROR_BEHAVIOR.BLUR,
|
||||
validator: value => [ERROR_BEHAVIOR.BLUR, ERROR_BEHAVIOR.LIVE, ERROR_BEHAVIOR.SUBMIT].includes(value)
|
||||
}) errorBehavior!: string
|
||||
|
||||
@Prop({ default: false }) showErrors!: boolean
|
||||
@Prop({ default: false }) disableErrors!: boolean
|
||||
@Prop({ default: true }) preventWindowDrops!: boolean
|
||||
@Prop({ default: 'preview' }) imageBehavior!: string
|
||||
@Prop({ default: false }) uploader!: Function|Record<string, any>|boolean
|
||||
@Prop({ default: false }) uploadUrl!: string|boolean
|
||||
@Prop({ default: 'live' }) uploadBehavior!: string
|
||||
|
||||
defaultId: string = this.$formulario.nextId(this)
|
||||
localAttributes: Record<string, any> = {}
|
||||
localErrors: ValidationError[] = []
|
||||
proxy: Record<string, any> = this.getInitialValue()
|
||||
behavioralErrorVisibility: boolean = this.errorBehavior === 'live'
|
||||
formShouldShowErrors = false
|
||||
validationErrors: [] = []
|
||||
pendingValidation: Promise<any> = Promise.resolve()
|
||||
// These registries are used for injected messages registrants only (mostly internal).
|
||||
ruleRegistry: [] = []
|
||||
messageRegistry: Record<string, any> = {}
|
||||
|
||||
get context (): Record<string, any> {
|
||||
return this.defineModel({
|
||||
id: this.id || this.defaultId,
|
||||
name: this.nameOrFallback,
|
||||
attributes: this.elementAttributes,
|
||||
blurHandler: this.blurHandler.bind(this),
|
||||
disableErrors: this.disableErrors,
|
||||
errors: this.explicitErrors,
|
||||
allErrors: this.allErrors,
|
||||
formShouldShowErrors: this.formShouldShowErrors,
|
||||
getValidationErrors: this.getValidationErrors.bind(this),
|
||||
hasGivenName: this.hasGivenName,
|
||||
hasValidationErrors: this.hasValidationErrors.bind(this),
|
||||
imageBehavior: this.imageBehavior,
|
||||
performValidation: this.performValidation.bind(this),
|
||||
preventWindowDrops: this.preventWindowDrops,
|
||||
setErrors: this.setErrors.bind(this),
|
||||
showValidationErrors: this.showValidationErrors,
|
||||
uploadBehavior: this.uploadBehavior,
|
||||
uploadUrl: this.mergedUploadUrl,
|
||||
uploader: this.uploader || this.$formulario.getUploader(),
|
||||
validationErrors: this.validationErrors,
|
||||
value: this.value,
|
||||
visibleValidationErrors: this.visibleValidationErrors,
|
||||
})
|
||||
}
|
||||
|
||||
get parsedValidationRules () {
|
||||
const parsedValidationRules = {}
|
||||
Object.keys(this.validationRules).forEach(key => {
|
||||
parsedValidationRules[snakeToCamel(key)] = this.validationRules[key]
|
||||
})
|
||||
return parsedValidationRules
|
||||
}
|
||||
|
||||
get messages (): Record<string, any> {
|
||||
const messages = {}
|
||||
Object.keys(this.validationMessages).forEach((key) => {
|
||||
messages[snakeToCamel(key)] = this.validationMessages[key]
|
||||
})
|
||||
Object.keys(this.messageRegistry).forEach((key) => {
|
||||
messages[snakeToCamel(key)] = this.messageRegistry[key]
|
||||
})
|
||||
return messages
|
||||
}
|
||||
|
||||
/**
|
||||
* Reducer for attributes that will be applied to each core input element.
|
||||
*/
|
||||
get elementAttributes (): Record<string, any> {
|
||||
const attrs = Object.assign({}, this.localAttributes)
|
||||
// pass the ID prop through to the root element
|
||||
if (this.id) {
|
||||
attrs.id = this.id
|
||||
} else {
|
||||
attrs.id = this.defaultId
|
||||
}
|
||||
},
|
||||
inject: {
|
||||
formularioSetter: { default: undefined },
|
||||
formularioFieldValidation: { default: () => () => ({}) },
|
||||
formularioRegister: { default: undefined },
|
||||
formularioDeregister: { default: undefined },
|
||||
getFormValues: { default: () => () => ({}) },
|
||||
observeErrors: { default: undefined },
|
||||
removeErrorObserver: { default: undefined },
|
||||
path: { default: '' }
|
||||
},
|
||||
model: {
|
||||
prop: 'formularioValue',
|
||||
event: 'input'
|
||||
},
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
/* eslint-disable */
|
||||
formularioValue: {
|
||||
default: ''
|
||||
},
|
||||
value: {
|
||||
default: false
|
||||
},
|
||||
/* eslint-enable */
|
||||
id: {
|
||||
type: [String, Boolean, Number],
|
||||
default: false
|
||||
},
|
||||
errors: {
|
||||
type: [String, Array, Boolean],
|
||||
default: false
|
||||
},
|
||||
validation: {
|
||||
type: [String, Boolean, Array],
|
||||
default: false
|
||||
},
|
||||
validationName: {
|
||||
type: [String, Boolean],
|
||||
default: false
|
||||
},
|
||||
errorBehavior: {
|
||||
type: String,
|
||||
default: 'blur',
|
||||
validator: function (value) {
|
||||
return ['blur', 'live', 'submit'].includes(value)
|
||||
}
|
||||
},
|
||||
showErrors: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
imageBehavior: {
|
||||
type: String,
|
||||
default: 'preview'
|
||||
},
|
||||
uploadUrl: {
|
||||
type: [String, Boolean],
|
||||
default: false
|
||||
},
|
||||
uploader: {
|
||||
type: [Function, Object, Boolean],
|
||||
default: false
|
||||
},
|
||||
uploadBehavior: {
|
||||
type: String,
|
||||
default: 'live'
|
||||
},
|
||||
preventWindowDrops: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
validationMessages: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
validationRules: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
disableErrors: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
// pass an explicitly given name prop through to the root element
|
||||
if (this.hasGivenName) {
|
||||
attrs.name = this.name
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
defaultId: this.$formulario.nextId(this),
|
||||
localAttributes: {},
|
||||
localErrors: [],
|
||||
proxy: this.getInitialValue(),
|
||||
behavioralErrorVisibility: (this.errorBehavior === 'live'),
|
||||
formShouldShowErrors: false,
|
||||
validationErrors: [],
|
||||
pendingValidation: Promise.resolve(),
|
||||
// These registries are used for injected messages registrants only (mostly internal).
|
||||
ruleRegistry: [],
|
||||
messageRegistry: {}
|
||||
|
||||
// If there is help text, have this element be described by it.
|
||||
if (this.help) {
|
||||
attrs['aria-describedby'] = `${attrs.id}-help`
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...context,
|
||||
parsedValidationRules () {
|
||||
const parsedValidationRules = {}
|
||||
Object.keys(this.validationRules).forEach(key => {
|
||||
parsedValidationRules[snakeToCamel(key)] = this.validationRules[key]
|
||||
})
|
||||
return parsedValidationRules
|
||||
},
|
||||
messages () {
|
||||
const messages = {}
|
||||
Object.keys(this.validationMessages).forEach((key) => {
|
||||
messages[snakeToCamel(key)] = this.validationMessages[key]
|
||||
})
|
||||
Object.keys(this.messageRegistry).forEach((key) => {
|
||||
messages[snakeToCamel(key)] = this.messageRegistry[key]
|
||||
})
|
||||
return messages
|
||||
|
||||
return attrs
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the element’s name, or select a fallback.
|
||||
*/
|
||||
get nameOrFallback (): string {
|
||||
return this.path !== '' ? `${this.path}.${this.name}` : this.name
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an input has a user-defined name.
|
||||
*/
|
||||
get hasGivenName (): boolean {
|
||||
return typeof this.name !== 'boolean'
|
||||
}
|
||||
|
||||
/**
|
||||
* The validation label to use.
|
||||
*/
|
||||
get mergedValidationName (): string {
|
||||
return this.validationName || this.name
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the uploadURL on the input if it exists, otherwise use the uploadURL
|
||||
* that is defined as a plugin option.
|
||||
*/
|
||||
get mergedUploadUrl (): string | boolean {
|
||||
return this.uploadUrl || this.$formulario.getUploadUrl()
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this computed property have errors
|
||||
*/
|
||||
get hasErrors (): boolean {
|
||||
return this.allErrors.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if form has actively visible errors (of any kind)
|
||||
*/
|
||||
get hasVisibleErrors (): boolean {
|
||||
return ((this.validationErrors && this.showValidationErrors) || !!this.explicitErrors.length)
|
||||
}
|
||||
|
||||
/**
|
||||
* The merged errors computed property.
|
||||
* Each error is an object with fields message (translated message), rule (rule name) and context
|
||||
*/
|
||||
get allErrors (): ValidationError[] {
|
||||
return [
|
||||
...this.explicitErrors,
|
||||
...arrayify(this.validationErrors)
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* All of the currently visible validation errors (does not include error handling)
|
||||
*/
|
||||
get visibleValidationErrors (): ValidationError[] {
|
||||
return (this.showValidationErrors && this.validationErrors.length) ? this.validationErrors : []
|
||||
}
|
||||
|
||||
/**
|
||||
* These are errors we that have been explicity passed to us.
|
||||
*/
|
||||
get explicitErrors (): ValidationError[] {
|
||||
return [
|
||||
...arrayify(this.errors),
|
||||
...this.localErrors,
|
||||
].map(message => ({ rule: null, context: null, message }))
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this formulario element is v-modeled or not.
|
||||
*/
|
||||
get isVmodeled (): boolean {
|
||||
return !!(Object.prototype.hasOwnProperty.call(this.$options.propsData, 'formularioValue') &&
|
||||
this._events &&
|
||||
Array.isArray(this._events.input) &&
|
||||
this._events.input.length)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the field should show it's error (if it has one)
|
||||
*/
|
||||
get showValidationErrors (): boolean {
|
||||
return this.showErrors || this.formShouldShowErrors || this.behavioralErrorVisibility
|
||||
}
|
||||
|
||||
@Watch('$attrs', { deep: true })
|
||||
onAttrsChanged (value): void {
|
||||
this.updateLocalAttributes(value)
|
||||
}
|
||||
|
||||
@Watch('proxy')
|
||||
onProxyChanged (newValue, oldValue): void {
|
||||
this.performValidation()
|
||||
if (!this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
|
||||
this.context.model = newValue
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$attrs': {
|
||||
handler (value) {
|
||||
this.updateLocalAttributes(value)
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
proxy (newValue, oldValue) {
|
||||
this.performValidation()
|
||||
if (!this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
|
||||
this.context.model = newValue
|
||||
}
|
||||
},
|
||||
formularioValue (newValue, oldValue) {
|
||||
if (this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
|
||||
this.context.model = newValue
|
||||
}
|
||||
},
|
||||
showValidationErrors: {
|
||||
handler (val) {
|
||||
this.$emit('error-visibility', val)
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
|
||||
@Watch('formularioValue')
|
||||
onFormularioValueChanged (newValue, oldValue): void {
|
||||
if (this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
|
||||
this.context.model = newValue
|
||||
}
|
||||
},
|
||||
created () {
|
||||
}
|
||||
|
||||
@Watch('showValidationErrors', { immediate: true })
|
||||
onShowValidationErrorsChanged (val): void {
|
||||
this.$emit('error-visibility', val)
|
||||
}
|
||||
|
||||
created (): void {
|
||||
this.applyInitialValue()
|
||||
if (this.formularioRegister && typeof this.formularioRegister === 'function') {
|
||||
this.formularioRegister(this.nameOrFallback, this)
|
||||
}
|
||||
if (!this.disableErrors && typeof this.observeErrors === 'function') {
|
||||
this.observeErrors({ callback: this.setErrors, type: 'input', field: this.nameOrFallback })
|
||||
if (!this.disableErrors && typeof this.addErrorObserver === 'function') {
|
||||
this.addErrorObserver({ callback: this.setErrors, type: 'input', field: this.nameOrFallback })
|
||||
}
|
||||
this.updateLocalAttributes(this.$attrs)
|
||||
if (this.errorBehavior === 'live') {
|
||||
if (this.errorBehavior === ERROR_BEHAVIOR.LIVE) {
|
||||
this.performValidation()
|
||||
}
|
||||
},
|
||||
beforeDestroy () {
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
beforeDestroy (): void {
|
||||
if (!this.disableErrors && typeof this.removeErrorObserver === 'function') {
|
||||
this.removeErrorObserver(this.setErrors)
|
||||
}
|
||||
if (typeof this.formularioDeregister === 'function') {
|
||||
this.formularioDeregister(this.nameOrFallback)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getInitialValue () {
|
||||
if (has(this.$options.propsData, 'value')) {
|
||||
return this.value
|
||||
} else if (has(this.$options.propsData, 'formularioValue')) {
|
||||
return this.formularioValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the model used throughout the existing context.
|
||||
*/
|
||||
defineModel (context): Record<string, any> {
|
||||
return Object.defineProperty(context, 'model', {
|
||||
get: this.modelGetter.bind(this),
|
||||
set: this.modelSetter.bind(this),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value from a model.
|
||||
*/
|
||||
modelGetter (): any {
|
||||
const model = this.isVmodeled ? 'formularioValue' : 'proxy'
|
||||
if (this[model] === undefined) {
|
||||
return ''
|
||||
},
|
||||
applyInitialValue () {
|
||||
// This should only be run immediately on created and ensures that the
|
||||
// proxy and the model are both the same before any additional registration.
|
||||
if (!shallowEqualObjects(this.context.model, this.proxy)) {
|
||||
this.context.model = this.proxy
|
||||
}
|
||||
},
|
||||
updateLocalAttributes (value) {
|
||||
if (!shallowEqualObjects(value, this.localAttributes)) {
|
||||
this.localAttributes = value
|
||||
}
|
||||
},
|
||||
performValidation () {
|
||||
let rules = parseRules(this.validation, this.$formulario.rules(this.parsedValidationRules))
|
||||
// Add in ruleRegistry rules. These are added directly via injection from
|
||||
// children and not part of the standard validation rule set.
|
||||
rules = this.ruleRegistry.length ? this.ruleRegistry.concat(rules) : rules
|
||||
this.pendingValidation = this.runRules(rules)
|
||||
.then(messages => this.didValidate(messages))
|
||||
return this.pendingValidation
|
||||
},
|
||||
runRules (rules) {
|
||||
const run = ([rule, args, ruleName, modifier]) => {
|
||||
var res = rule({
|
||||
value: this.context.model,
|
||||
getFormValues: this.getFormValues.bind(this),
|
||||
name: this.context.name
|
||||
}, ...args)
|
||||
res = (res instanceof Promise) ? res : Promise.resolve(res)
|
||||
return res.then(result => result ? false : this.getMessageObject(ruleName, args))
|
||||
}
|
||||
}
|
||||
return this[model]
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
const resolveGroups = (groups, allMessages = []) => {
|
||||
const ruleGroup = groups.shift()
|
||||
if (Array.isArray(ruleGroup) && ruleGroup.length) {
|
||||
Promise.all(ruleGroup.map(run))
|
||||
.then(messages => messages.filter(m => !!m))
|
||||
.then(messages => {
|
||||
messages = Array.isArray(messages) ? messages : []
|
||||
// The rule passed or its a non-bailing group, and there are additional groups to check, continue
|
||||
if ((!messages.length || !ruleGroup.bail) && groups.length) {
|
||||
return resolveGroups(groups, allMessages.concat(messages))
|
||||
}
|
||||
return resolve(allMessages.concat(messages))
|
||||
})
|
||||
} else {
|
||||
resolve([])
|
||||
}
|
||||
}
|
||||
resolveGroups(groupBails(rules))
|
||||
})
|
||||
},
|
||||
didValidate (messages) {
|
||||
const validationChanged = !shallowEqualObjects(messages, this.validationErrors)
|
||||
this.validationErrors = messages
|
||||
if (validationChanged) {
|
||||
const errorObject = this.getErrorObject()
|
||||
this.$emit('validation', errorObject)
|
||||
if (this.formularioFieldValidation && typeof this.formularioFieldValidation === 'function') {
|
||||
this.formularioFieldValidation(errorObject)
|
||||
}
|
||||
}
|
||||
},
|
||||
getMessageObject (ruleName, args) {
|
||||
let context = {
|
||||
args,
|
||||
name: this.mergedValidationName,
|
||||
/**
|
||||
* Set the value from a model.
|
||||
*/
|
||||
modelSetter (value): void {
|
||||
if (!shallowEqualObjects(value, this.proxy)) {
|
||||
this.proxy = value
|
||||
}
|
||||
this.$emit('input', value)
|
||||
if (this.context.name && typeof this.formularioSetter === 'function') {
|
||||
this.formularioSetter(this.context.name, value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bound into the context object.
|
||||
*/
|
||||
blurHandler (): void {
|
||||
this.$emit('blur')
|
||||
if (this.errorBehavior === 'blur') {
|
||||
this.behavioralErrorVisibility = true
|
||||
}
|
||||
}
|
||||
|
||||
getInitialValue (): any {
|
||||
if (has(this.$options.propsData as Record<string, any>, 'value')) {
|
||||
return this.value
|
||||
} else if (has(this.$options.propsData as Record<string, any>, 'formularioValue')) {
|
||||
return this.formularioValue
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
applyInitialValue (): void {
|
||||
// This should only be run immediately on created and ensures that the
|
||||
// proxy and the model are both the same before any additional registration.
|
||||
if (!shallowEqualObjects(this.context.model, this.proxy)) {
|
||||
this.context.model = this.proxy
|
||||
}
|
||||
}
|
||||
|
||||
updateLocalAttributes (value): void {
|
||||
if (!shallowEqualObjects(value, this.localAttributes)) {
|
||||
this.localAttributes = value
|
||||
}
|
||||
}
|
||||
|
||||
performValidation () {
|
||||
let rules = parseRules(this.validation, this.$formulario.rules(this.parsedValidationRules))
|
||||
// Add in ruleRegistry rules. These are added directly via injection from
|
||||
// children and not part of the standard validation rule set.
|
||||
rules = this.ruleRegistry.length ? this.ruleRegistry.concat(rules) : rules
|
||||
this.pendingValidation = this.runRules(rules)
|
||||
.then(messages => this.didValidate(messages))
|
||||
return this.pendingValidation
|
||||
}
|
||||
|
||||
runRules (rules) {
|
||||
const run = ([rule, args, ruleName]) => {
|
||||
let res = rule({
|
||||
value: this.context.model,
|
||||
vm: this,
|
||||
formValues: this.getFormValues()
|
||||
};
|
||||
let message = this.getMessageFunc(ruleName)(context);
|
||||
getFormValues: this.getFormValues.bind(this),
|
||||
name: this.context.name
|
||||
}, ...args)
|
||||
res = (res instanceof Promise) ? res : Promise.resolve(res)
|
||||
return res.then(result => result ? false : this.getMessageObject(ruleName, args))
|
||||
}
|
||||
|
||||
return {
|
||||
message: message,
|
||||
rule: ruleName,
|
||||
context: context
|
||||
}
|
||||
},
|
||||
getMessageFunc (ruleName) {
|
||||
ruleName = snakeToCamel(ruleName)
|
||||
if (this.messages && typeof this.messages[ruleName] !== 'undefined') {
|
||||
switch (typeof this.messages[ruleName]) {
|
||||
case 'function':
|
||||
return this.messages[ruleName]
|
||||
case 'string':
|
||||
case 'boolean':
|
||||
return () => this.messages[ruleName]
|
||||
return new Promise(resolve => {
|
||||
const resolveGroups = (groups, allMessages = []) => {
|
||||
const ruleGroup = groups.shift()
|
||||
if (Array.isArray(ruleGroup) && ruleGroup.length) {
|
||||
Promise.all(ruleGroup.map(run))
|
||||
.then(messages => messages.filter(m => !!m))
|
||||
.then(messages => {
|
||||
messages = Array.isArray(messages) ? messages : []
|
||||
// The rule passed or its a non-bailing group, and there are additional groups to check, continue
|
||||
if ((!messages.length || !ruleGroup.bail) && groups.length) {
|
||||
return resolveGroups(groups, allMessages.concat(messages))
|
||||
}
|
||||
return resolve(allMessages.concat(messages))
|
||||
})
|
||||
} else {
|
||||
resolve([])
|
||||
}
|
||||
}
|
||||
return (context) => this.$formulario.validationMessage(ruleName, context, this)
|
||||
},
|
||||
hasValidationErrors () {
|
||||
return new Promise(resolve => {
|
||||
this.$nextTick(() => {
|
||||
this.pendingValidation.then(() => resolve(!!this.validationErrors.length))
|
||||
})
|
||||
resolveGroups(groupBails(rules))
|
||||
})
|
||||
}
|
||||
|
||||
didValidate (messages): void {
|
||||
const validationChanged = !shallowEqualObjects(messages, this.validationErrors)
|
||||
this.validationErrors = messages
|
||||
if (validationChanged) {
|
||||
const errorObject = this.getErrorObject()
|
||||
this.$emit('validation', errorObject)
|
||||
if (this.formularioFieldValidation && typeof this.formularioFieldValidation === 'function') {
|
||||
this.formularioFieldValidation(errorObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getMessageObject (ruleName, args) {
|
||||
const context = {
|
||||
args,
|
||||
name: this.mergedValidationName,
|
||||
value: this.context.model,
|
||||
vm: this,
|
||||
formValues: this.getFormValues()
|
||||
}
|
||||
const message = this.getMessageFunc(ruleName)(context)
|
||||
|
||||
return {
|
||||
rule: ruleName,
|
||||
context,
|
||||
message,
|
||||
}
|
||||
}
|
||||
|
||||
getMessageFunc (ruleName: string): Function {
|
||||
ruleName = snakeToCamel(ruleName)
|
||||
if (this.messages && typeof this.messages[ruleName] !== 'undefined') {
|
||||
switch (typeof this.messages[ruleName]) {
|
||||
case 'function':
|
||||
return this.messages[ruleName]
|
||||
case 'string':
|
||||
case 'boolean':
|
||||
return (): string => this.messages[ruleName]
|
||||
}
|
||||
}
|
||||
return (context): string => this.$formulario.validationMessage(ruleName, context, this)
|
||||
}
|
||||
|
||||
hasValidationErrors (): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
this.$nextTick(() => {
|
||||
this.pendingValidation.then(() => resolve(!!this.validationErrors.length))
|
||||
})
|
||||
},
|
||||
getValidationErrors () {
|
||||
return new Promise(resolve => {
|
||||
this.$nextTick(() => this.pendingValidation.then(() => resolve(this.getErrorObject())))
|
||||
})
|
||||
},
|
||||
getErrorObject () {
|
||||
return {
|
||||
name: this.context.nameOrFallback || this.context.name,
|
||||
errors: this.validationErrors.filter(s => typeof s === 'object'),
|
||||
hasErrors: !!this.validationErrors.length
|
||||
}
|
||||
},
|
||||
setErrors (errors) {
|
||||
this.localErrors = arrayify(errors)
|
||||
},
|
||||
registerRule (rule, args, ruleName, message = null) {
|
||||
if (!this.ruleRegistry.some(r => r[2] === ruleName)) {
|
||||
// These are the raw rule format since they will be used directly.
|
||||
this.ruleRegistry.push([rule, args, ruleName])
|
||||
if (message !== null) {
|
||||
this.messageRegistry[ruleName] = message
|
||||
}
|
||||
}
|
||||
},
|
||||
removeRule (key) {
|
||||
const ruleIndex = this.ruleRegistry.findIndex(r => r[2] === key)
|
||||
if (ruleIndex >= 0) {
|
||||
this.ruleRegistry.splice(ruleIndex, 1)
|
||||
delete this.messageRegistry[key]
|
||||
})
|
||||
}
|
||||
|
||||
getValidationErrors () {
|
||||
return new Promise(resolve => {
|
||||
this.$nextTick(() => this.pendingValidation.then(() => resolve(this.getErrorObject())))
|
||||
})
|
||||
}
|
||||
|
||||
getErrorObject () {
|
||||
return {
|
||||
name: this.context.nameOrFallback || this.context.name,
|
||||
errors: this.validationErrors.filter(s => typeof s === 'object'),
|
||||
hasErrors: !!this.validationErrors.length
|
||||
}
|
||||
}
|
||||
|
||||
setErrors (errors): void {
|
||||
this.localErrors = arrayify(errors)
|
||||
}
|
||||
|
||||
registerRule (rule, args, ruleName, message = null): void {
|
||||
if (!this.ruleRegistry.some(r => r[2] === ruleName)) {
|
||||
// These are the raw rule format since they will be used directly.
|
||||
this.ruleRegistry.push([rule, args, ruleName])
|
||||
if (message !== null) {
|
||||
this.messageRegistry[ruleName] = message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeRule (key): void {
|
||||
const ruleIndex = this.ruleRegistry.findIndex(r => r[2] === key)
|
||||
if (ruleIndex >= 0) {
|
||||
this.ruleRegistry.splice(ruleIndex, 1)
|
||||
delete this.messageRegistry[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -1,223 +0,0 @@
|
||||
/**
|
||||
* This is an object of functions that each produce valid responses. There's no
|
||||
* need for these to be 1-1 with english, feel free to change the wording or
|
||||
* use/not use any of the variables available in the object or the
|
||||
* arguments for the message to make the most sense in your language and culture.
|
||||
*
|
||||
* The validation context object includes the following properties:
|
||||
* {
|
||||
* args // Array of rule arguments: between:5,10 (args are ['5', '10'])
|
||||
* name: // The validation name to be used
|
||||
* value: // The value of the field (do not mutate!),
|
||||
* vm: the // FormulateInput instance this belongs to,
|
||||
* formValues: // If wrapped in a FormulateForm, the value of other form fields.
|
||||
* }
|
||||
*/
|
||||
const validationMessages = {
|
||||
/**
|
||||
* The default render method for error messages.
|
||||
*/
|
||||
default: function (vm, context) {
|
||||
return vm.$t(`validation.default`, context)
|
||||
},
|
||||
|
||||
/**
|
||||
* Valid accepted value.
|
||||
*/
|
||||
accepted: function (vm, context) {
|
||||
return vm.$t(`validation.accepted`, context)
|
||||
},
|
||||
|
||||
/**
|
||||
* The date is not after.
|
||||
*/
|
||||
after: function (vm, context) {
|
||||
if (Array.isArray(context.args) && context.args.length) {
|
||||
context['compare'] = context.args[0]
|
||||
return vm.$t(`validation.after.compare`, context)
|
||||
}
|
||||
|
||||
return vm.$t(`validation.after.default`, context)
|
||||
},
|
||||
|
||||
/**
|
||||
* The value is not a letter.
|
||||
*/
|
||||
alpha: function (vm, context) {
|
||||
return vm.$t(`validation.alpha`, context)
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: checks if the value is alpha numeric
|
||||
*/
|
||||
alphanumeric: function (vm, context) {
|
||||
return vm.$t(`validation.alphanumeric`, context)
|
||||
},
|
||||
|
||||
/**
|
||||
* The date is not before.
|
||||
*/
|
||||
before: function (vm, context) {
|
||||
if (Array.isArray(context.args) && context.args.length) {
|
||||
context['compare'] = context.args[0]
|
||||
return vm.$t(`validation.before.compare`, context)
|
||||
}
|
||||
|
||||
return vm.$t(`validation.before.default`, context)
|
||||
},
|
||||
|
||||
/**
|
||||
* The value is not between two numbers or lengths
|
||||
*/
|
||||
between: function (vm, context) {
|
||||
context['from'] = context.args[0]
|
||||
context['to'] = context.args[1]
|
||||
|
||||
const force = Array.isArray(context.args) && context.args[2] ? context.args[2] : false
|
||||
if ((!isNaN(value) && force !== 'length') || force === 'value') {
|
||||
return vm.$t(`validation.between.force`, context)
|
||||
}
|
||||
|
||||
return vm.$t(`validation.between.default`, context)
|
||||
},
|
||||
|
||||
/**
|
||||
* The confirmation field does not match
|
||||
*/
|
||||
confirm: function (vm, context) {
|
||||
return vm.$t(`validation.confirm`, context)
|
||||
},
|
||||
|
||||
/**
|
||||
* Is not a valid date.
|
||||
*/
|
||||
date: function (vm, context) {
|
||||
if (Array.isArray(context.args) && context.args.length) {
|
||||
context['format'] = context.args[0]
|
||||
return vm.$t(`validation.date.format`, context)
|
||||
}
|
||||
|
||||
return vm.$t(`validation.date.default`, context)
|
||||
},
|
||||
|
||||
/**
|
||||
* Is not a valid email address.
|
||||
*/
|
||||
email: function (vm, context) {
|
||||
return vm.$t(`validation.email.default`, context)
|
||||
},
|
||||
|
||||
/**
|
||||
* Ends with specified value
|
||||
*/
|
||||
endsWith: function (vm, context) {
|
||||
return vm.$t(`validation.endsWith.default`, context)
|
||||
},
|
||||
|
||||
/**
|
||||
* Value is an allowed value.
|
||||
*/
|
||||
in: function (vm, context) {
|
||||
if (typeof context.value === 'string' && context.value) {
|
||||
return vm.$t(`validation.in.string`, context)
|
||||
}
|
||||
|
||||
return vm.$t(`validation.in.default`, context)
|
||||
},
|
||||
|
||||
/**
|
||||
* Value is not a match.
|
||||
*/
|
||||
matches: function (vm, context) {
|
||||
return vm.$t(`validation.matches.default`, context)
|
||||
},
|
||||
|
||||
/**
|
||||
* The maximum value allowed.
|
||||
*/
|
||||
max: function (vm, context) {
|
||||
context['maximum'] = context.args[0]
|
||||
|
||||
if (Array.isArray(context.value)) {
|
||||
return vm.$tc(`validation.max.array`, context['maximum'], context)
|
||||
}
|
||||
const force = Array.isArray(context.args) && context.args[1] ? context.args[1] : false
|
||||
if ((!isNaN(context.value) && force !== 'length') || force === 'value') {
|
||||
return vm.$tc(`validation.max.force`, context['maximum'], context)
|
||||
}
|
||||
return vm.$tc(`validation.max.default`, context['maximum'], context)
|
||||
},
|
||||
|
||||
/**
|
||||
* The (field-level) error message for mime errors.
|
||||
*/
|
||||
mime: function (vm, context) {
|
||||
context['types'] = context.args[0]
|
||||
if (context['types']) {
|
||||
return vm.$t(`validation.mime.default`, context)
|
||||
} else {
|
||||
return vm.$t(`validation.mime.no_formats_allowed`, context)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* The maximum value allowed.
|
||||
*/
|
||||
min: function (vm, context) {
|
||||
context['minimum'] = context.args[0]
|
||||
|
||||
if (Array.isArray(context.value)) {
|
||||
return vm.$tc(`validation.min.array`, context['minimum'], context)
|
||||
}
|
||||
const force = Array.isArray(context.args) && context.args[1] ? context.args[1] : false
|
||||
if ((!isNaN(context.value) && force !== 'length') || force === 'value') {
|
||||
return vm.$tc(`validation.min.force`, context['minimum'], context)
|
||||
}
|
||||
return vm.$tc(`validation.min.default`, context['minimum'], context)
|
||||
},
|
||||
|
||||
/**
|
||||
* The field is not an allowed value
|
||||
*/
|
||||
not: function (vm, context) {
|
||||
return vm.$t(`validation.not.default`, context)
|
||||
},
|
||||
|
||||
/**
|
||||
* The field is not a number
|
||||
*/
|
||||
number: function (vm, context) {
|
||||
return vm.$t(`validation.number.default`, context)
|
||||
},
|
||||
|
||||
/**
|
||||
* Required field.
|
||||
*/
|
||||
required: function (vm, context) {
|
||||
return vm.$t(`validation.required.default`, context)
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts with specified value
|
||||
*/
|
||||
startsWith: function (vm, context) {
|
||||
return vm.$t(`validation.startsWith.default`, context)
|
||||
},
|
||||
|
||||
/**
|
||||
* Value is not a url.
|
||||
*/
|
||||
url: function (vm, context) {
|
||||
return vm.$t(`validation.url.default`, context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This creates a vue-formulario plugin that can be imported and used on each
|
||||
* project.
|
||||
*/
|
||||
export default function (instance) {
|
||||
instance.extend({
|
||||
validationMessages: validationMessages
|
||||
})
|
||||
}
|
221
src/RuleValidationMessages.ts
Normal file
221
src/RuleValidationMessages.ts
Normal file
@ -0,0 +1,221 @@
|
||||
import Formulario from '@/Formulario'
|
||||
import FormularioInput from '@/FormularioInput.vue'
|
||||
import { ValidationContext } from '@/validation/types'
|
||||
|
||||
/**
|
||||
* This is an object of functions that each produce valid responses. There's no
|
||||
* need for these to be 1-1 with english, feel free to change the wording or
|
||||
* use/not use any of the variables available in the object or the
|
||||
* arguments for the message to make the most sense in your language and culture.
|
||||
*
|
||||
* The validation context object includes the following properties:
|
||||
* {
|
||||
* args // Array of rule arguments: between:5,10 (args are ['5', '10'])
|
||||
* name: // The validation name to be used
|
||||
* value: // The value of the field (do not mutate!),
|
||||
* vm: the // FormulateInput instance this belongs to,
|
||||
* formValues: // If wrapped in a FormulateForm, the value of other form fields.
|
||||
* }
|
||||
*/
|
||||
const validationMessages = {
|
||||
/**
|
||||
* The default render method for error messages.
|
||||
*/
|
||||
default (vm: FormularioInput, context: ValidationContext): string {
|
||||
return vm.$t('validation.default', context)
|
||||
},
|
||||
|
||||
/**
|
||||
* Valid accepted value.
|
||||
*/
|
||||
accepted (vm: FormularioInput, context: ValidationContext): string {
|
||||
return vm.$t('validation.accepted', context)
|
||||
},
|
||||
|
||||
/**
|
||||
* The date is not after.
|
||||
*/
|
||||
after (vm: FormularioInput, context: ValidationContext): string {
|
||||
if (Array.isArray(context.args) && context.args.length) {
|
||||
return vm.$t('validation.after.compare', context)
|
||||
}
|
||||
|
||||
return vm.$t('validation.after.default', context)
|
||||
},
|
||||
|
||||
/**
|
||||
* The value is not a letter.
|
||||
*/
|
||||
alpha (vm: FormularioInput, context: Record<string, any>): string {
|
||||
return vm.$t('validation.alpha', context)
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: checks if the value is alpha numeric
|
||||
*/
|
||||
alphanumeric (vm: FormularioInput, context: Record<string, any>): string {
|
||||
return vm.$t('validation.alphanumeric', context)
|
||||
},
|
||||
|
||||
/**
|
||||
* The date is not before.
|
||||
*/
|
||||
before (vm: FormularioInput, context: ValidationContext): string {
|
||||
if (Array.isArray(context.args) && context.args.length) {
|
||||
return vm.$t('validation.before.compare', context)
|
||||
}
|
||||
|
||||
return vm.$t('validation.before.default', context)
|
||||
},
|
||||
|
||||
/**
|
||||
* The value is not between two numbers or lengths
|
||||
*/
|
||||
between (vm: FormularioInput, context: ValidationContext): string {
|
||||
const force = Array.isArray(context.args) && context.args[2] ? context.args[2] : false
|
||||
|
||||
if ((!isNaN(context.value) && force !== 'length') || force === 'value') {
|
||||
return vm.$t('validation.between.force', context)
|
||||
}
|
||||
|
||||
return vm.$t('validation.between.default', context)
|
||||
},
|
||||
|
||||
/**
|
||||
* The confirmation field does not match
|
||||
*/
|
||||
confirm (vm: FormularioInput, context: ValidationContext): string {
|
||||
return vm.$t('validation.confirm', context)
|
||||
},
|
||||
|
||||
/**
|
||||
* Is not a valid date.
|
||||
*/
|
||||
date (vm: FormularioInput, context: ValidationContext): string {
|
||||
if (Array.isArray(context.args) && context.args.length) {
|
||||
return vm.$t('validation.date.format', context)
|
||||
}
|
||||
|
||||
return vm.$t('validation.date.default', context)
|
||||
},
|
||||
|
||||
/**
|
||||
* Is not a valid email address.
|
||||
*/
|
||||
email (vm: FormularioInput, context: ValidationContext): string {
|
||||
return vm.$t('validation.email.default', context)
|
||||
},
|
||||
|
||||
/**
|
||||
* Ends with specified value
|
||||
*/
|
||||
endsWith (vm: FormularioInput, context: ValidationContext): string {
|
||||
return vm.$t('validation.endsWith.default', context)
|
||||
},
|
||||
|
||||
/**
|
||||
* Value is an allowed value.
|
||||
*/
|
||||
in: function (vm: FormularioInput, context: ValidationContext): string {
|
||||
if (typeof context.value === 'string' && context.value) {
|
||||
return vm.$t('validation.in.string', context)
|
||||
}
|
||||
|
||||
return vm.$t('validation.in.default', context)
|
||||
},
|
||||
|
||||
/**
|
||||
* Value is not a match.
|
||||
*/
|
||||
matches (vm: FormularioInput, context: ValidationContext): string {
|
||||
return vm.$t('validation.matches.default', context)
|
||||
},
|
||||
|
||||
/**
|
||||
* The maximum value allowed.
|
||||
*/
|
||||
max (vm: FormularioInput, context: ValidationContext): string {
|
||||
const maximum = context.args[0] as number
|
||||
|
||||
if (Array.isArray(context.value)) {
|
||||
return vm.$tc('validation.max.array', maximum, context)
|
||||
}
|
||||
const force = Array.isArray(context.args) && context.args[1] ? context.args[1] : false
|
||||
if ((!isNaN(context.value) && force !== 'length') || force === 'value') {
|
||||
return vm.$tc('validation.max.force', maximum, context)
|
||||
}
|
||||
return vm.$tc('validation.max.default', maximum, context)
|
||||
},
|
||||
|
||||
/**
|
||||
* The (field-level) error message for mime errors.
|
||||
*/
|
||||
mime (vm: FormularioInput, context: ValidationContext): string {
|
||||
const types = context.args[0]
|
||||
|
||||
if (types) {
|
||||
return vm.$t('validation.mime.default', context)
|
||||
} else {
|
||||
return vm.$t('validation.mime.no_formats_allowed', context)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* The maximum value allowed.
|
||||
*/
|
||||
min (vm: FormularioInput, context: ValidationContext): string {
|
||||
const minimum = context.args[0] as number
|
||||
|
||||
if (Array.isArray(context.value)) {
|
||||
return vm.$tc('validation.min.array', minimum, context)
|
||||
}
|
||||
const force = Array.isArray(context.args) && context.args[1] ? context.args[1] : false
|
||||
if ((!isNaN(context.value) && force !== 'length') || force === 'value') {
|
||||
return vm.$tc('validation.min.force', minimum, context)
|
||||
}
|
||||
return vm.$tc('validation.min.default', minimum, context)
|
||||
},
|
||||
|
||||
/**
|
||||
* The field is not an allowed value
|
||||
*/
|
||||
not (vm: FormularioInput, context: Record<string, any>): string {
|
||||
return vm.$t('validation.not.default', context)
|
||||
},
|
||||
|
||||
/**
|
||||
* The field is not a number
|
||||
*/
|
||||
number (vm: FormularioInput, context: Record<string, any>): string {
|
||||
return vm.$t('validation.number.default', context)
|
||||
},
|
||||
|
||||
/**
|
||||
* Required field.
|
||||
*/
|
||||
required (vm: FormularioInput, context: Record<string, any>): string {
|
||||
return vm.$t('validation.required.default', context)
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts with specified value
|
||||
*/
|
||||
startsWith (vm: FormularioInput, context: Record<string, any>): string {
|
||||
return vm.$t('validation.startsWith.default', context)
|
||||
},
|
||||
|
||||
/**
|
||||
* Value is not a url.
|
||||
*/
|
||||
url (vm: FormularioInput, context: Record<string, any>): string {
|
||||
return vm.$t('validation.url.default', context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This creates a vue-formulario plugin that can be imported and used on each
|
||||
* project.
|
||||
*/
|
||||
export default function (instance: Formulario): void {
|
||||
instance.extend({ validationMessages })
|
||||
}
|
5
src/axios.types.ts
Normal file
5
src/axios.types.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface AxiosResponse {
|
||||
data: any
|
||||
}
|
||||
|
||||
export interface AxiosError {}
|
3
src/index.ts
Normal file
3
src/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import Formulario from '@/Formulario.ts'
|
||||
|
||||
export default new Formulario()
|
@ -1,243 +0,0 @@
|
||||
import { map, arrayify, shallowEqualObjects } from './utils'
|
||||
|
||||
/**
|
||||
* For a single instance of an input, export all of the context needed to fully
|
||||
* render that element.
|
||||
* @return {object}
|
||||
*/
|
||||
export default {
|
||||
context () {
|
||||
return defineModel.call(this, {
|
||||
attributes: this.elementAttributes,
|
||||
blurHandler: blurHandler.bind(this),
|
||||
disableErrors: this.disableErrors,
|
||||
errors: this.explicitErrors,
|
||||
allErrors: this.allErrors,
|
||||
formShouldShowErrors: this.formShouldShowErrors,
|
||||
getValidationErrors: this.getValidationErrors.bind(this),
|
||||
hasGivenName: this.hasGivenName,
|
||||
hasValidationErrors: this.hasValidationErrors.bind(this),
|
||||
help: this.help,
|
||||
id: this.id || this.defaultId,
|
||||
imageBehavior: this.imageBehavior,
|
||||
limit: this.limit,
|
||||
name: this.nameOrFallback,
|
||||
performValidation: this.performValidation.bind(this),
|
||||
preventWindowDrops: this.preventWindowDrops,
|
||||
repeatable: this.repeatable,
|
||||
setErrors: this.setErrors.bind(this),
|
||||
showValidationErrors: this.showValidationErrors,
|
||||
uploadBehavior: this.uploadBehavior,
|
||||
uploadUrl: this.mergedUploadUrl,
|
||||
uploader: this.uploader || this.$formulario.getUploader(),
|
||||
validationErrors: this.validationErrors,
|
||||
value: this.value,
|
||||
visibleValidationErrors: this.visibleValidationErrors,
|
||||
})
|
||||
},
|
||||
// Used in sub-context
|
||||
nameOrFallback,
|
||||
hasGivenName,
|
||||
elementAttributes,
|
||||
mergedUploadUrl,
|
||||
|
||||
// These items are not passed as context
|
||||
isVmodeled,
|
||||
mergedValidationName,
|
||||
explicitErrors,
|
||||
allErrors,
|
||||
hasErrors,
|
||||
hasVisibleErrors,
|
||||
showValidationErrors,
|
||||
visibleValidationErrors
|
||||
}
|
||||
|
||||
/**
|
||||
* Reducer for attributes that will be applied to each core input element.
|
||||
* @return {object}
|
||||
*/
|
||||
function elementAttributes () {
|
||||
const attrs = Object.assign({}, this.localAttributes)
|
||||
// pass the ID prop through to the root element
|
||||
if (this.id) {
|
||||
attrs.id = this.id
|
||||
} else {
|
||||
attrs.id = this.defaultId
|
||||
}
|
||||
// pass an explicitly given name prop through to the root element
|
||||
if (this.hasGivenName) {
|
||||
attrs.name = this.name
|
||||
}
|
||||
|
||||
// If there is help text, have this element be described by it.
|
||||
if (this.help) {
|
||||
attrs['aria-describedby'] = `${attrs.id}-help`
|
||||
}
|
||||
|
||||
return attrs
|
||||
}
|
||||
|
||||
/**
|
||||
* The validation label to use.
|
||||
*/
|
||||
function mergedValidationName () {
|
||||
if (this.validationName) {
|
||||
return this.validationName
|
||||
}
|
||||
|
||||
return this.name
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the uploadURL on the input if it exists, otherwise use the uploadURL
|
||||
* that is defined as a plugin option.
|
||||
*/
|
||||
function mergedUploadUrl () {
|
||||
return this.uploadUrl || this.$formulario.getUploadUrl()
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the field should show it's error (if it has one)
|
||||
* @return {boolean}
|
||||
*/
|
||||
function showValidationErrors () {
|
||||
if (this.showErrors || this.formShouldShowErrors) {
|
||||
return true
|
||||
}
|
||||
|
||||
return this.behavioralErrorVisibility
|
||||
}
|
||||
|
||||
/**
|
||||
* All of the currently visible validation errors (does not include error handling)
|
||||
* @return {array}
|
||||
*/
|
||||
function visibleValidationErrors () {
|
||||
return (this.showValidationErrors && this.validationErrors.length) ? this.validationErrors : []
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the element’s name, or select a fallback.
|
||||
*/
|
||||
function nameOrFallback () {
|
||||
if (this.path !== '') {
|
||||
return this.path + '.' + this.name
|
||||
}
|
||||
|
||||
return this.name
|
||||
}
|
||||
|
||||
/**
|
||||
* determine if an input has a user-defined name
|
||||
*/
|
||||
function hasGivenName () {
|
||||
return typeof this.name !== 'boolean'
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this formulario element is v-modeled or not.
|
||||
*/
|
||||
function isVmodeled () {
|
||||
return !!(this.$options.propsData.hasOwnProperty('formularioValue') &&
|
||||
this._events &&
|
||||
Array.isArray(this._events.input) &&
|
||||
this._events.input.length)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) && options && typeof options === 'object') {
|
||||
const optionList = []
|
||||
const that = this
|
||||
for (const value in options) {
|
||||
optionList.push({ value, label: options[value], id: `${that.elementAttributes.id}_${value}` })
|
||||
}
|
||||
return optionList
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
/**
|
||||
* These are errors we that have been explicity passed to us.
|
||||
*/
|
||||
function explicitErrors () {
|
||||
let result = arrayify(this.errors)
|
||||
.concat(this.localErrors)
|
||||
.concat(arrayify(this.error))
|
||||
result = result.map(message => ({'message': message, 'rule': null, 'context': null}))
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* The merged errors computed property.
|
||||
* Each error is an object with fields message (translated message), rule (rule name) and context
|
||||
*/
|
||||
function allErrors () {
|
||||
return this.explicitErrors
|
||||
.concat(arrayify(this.validationErrors))
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this computed property have errors
|
||||
*/
|
||||
function hasErrors () {
|
||||
return !!this.allErrors.length
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if form has actively visible errors (of any kind)
|
||||
*/
|
||||
function hasVisibleErrors () {
|
||||
return ((this.validationErrors && this.showValidationErrors) || !!this.explicitErrors.length)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bound into the context object.
|
||||
*/
|
||||
function blurHandler () {
|
||||
this.$emit('blur')
|
||||
if (this.errorBehavior === 'blur') {
|
||||
this.behavioralErrorVisibility = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the model used throughout the existing context.
|
||||
* @param {object} context
|
||||
*/
|
||||
function defineModel (context) {
|
||||
return Object.defineProperty(context, 'model', {
|
||||
get: modelGetter.bind(this),
|
||||
set: modelSetter.bind(this)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value from a model.
|
||||
**/
|
||||
function modelGetter () {
|
||||
const model = this.isVmodeled ? 'formularioValue' : 'proxy'
|
||||
if (this[model] === undefined) {
|
||||
return ''
|
||||
}
|
||||
return this[model]
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value from a model.
|
||||
**/
|
||||
function modelSetter (value) {
|
||||
if (!shallowEqualObjects(value, this.proxy)) {
|
||||
this.proxy = value
|
||||
}
|
||||
this.$emit('input', value)
|
||||
if (this.context.name && typeof this.formularioSetter === 'function') {
|
||||
this.formularioSetter(this.context.name, value)
|
||||
}
|
||||
}
|
@ -1,3 +1,8 @@
|
||||
interface UploadedFile {
|
||||
url: string
|
||||
name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* A fake uploader used by default.
|
||||
*
|
||||
@ -6,8 +11,8 @@
|
||||
* @param {function} error
|
||||
* @param {object} options
|
||||
*/
|
||||
export default function (file, progress, error, options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
export default function (file: any, progress: any, error: any, options: any): Promise<UploadedFile> {
|
||||
return new Promise(resolve => {
|
||||
const totalTime = (options.fauxUploaderDuration || 2000) * (0.5 + Math.random())
|
||||
const start = performance.now()
|
||||
|
@ -1,11 +0,0 @@
|
||||
/**
|
||||
* The default backend error handler assumes a failed axios instance. You can
|
||||
* easily override this function with fetch. The expected output is defined
|
||||
* on the documentation site vueformulate.com.
|
||||
*/
|
||||
export default function (err) {
|
||||
if (typeof err === 'object' && err.response) {
|
||||
|
||||
}
|
||||
return {}
|
||||
}
|
@ -5,10 +5,11 @@
|
||||
* overly terse for that reason alone, we wouldn't necessarily recommend this.
|
||||
*/
|
||||
const fi = 'FormularioInput'
|
||||
const add = (n, c) => ({
|
||||
classification: n,
|
||||
component: fi + (c || (n[0].toUpperCase() + n.substr(1)))
|
||||
const add = (classification: string, c?: string) => ({
|
||||
classification,
|
||||
component: fi + (c || (classification[0].toUpperCase() + classification.substr(1)))
|
||||
})
|
||||
|
||||
export default {
|
||||
// === SINGLE LINE TEXT STYLE INPUTS
|
||||
...[
|
@ -1,10 +0,0 @@
|
||||
const i = 'image/'
|
||||
export default {
|
||||
'csv': 'text/csv',
|
||||
'gif': i + 'gif',
|
||||
'jpg': i + 'jpeg',
|
||||
'jpeg': i + 'jpeg',
|
||||
'png': i + 'png',
|
||||
'pdf': 'application/pdf',
|
||||
'svg': i + 'svg+xml'
|
||||
}
|
9
src/libs/mimes.ts
Normal file
9
src/libs/mimes.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export default {
|
||||
csv: 'text/csv',
|
||||
gif: 'image/gif',
|
||||
jpeg: 'image/jpeg',
|
||||
jpg: 'image/jpeg',
|
||||
png: 'image/png',
|
||||
pdf: 'application/pdf',
|
||||
svg: 'image/svg+xml'
|
||||
}
|
@ -1,250 +0,0 @@
|
||||
import { shallowEqualObjects, has, getNested, setNested } from './utils'
|
||||
|
||||
/**
|
||||
* Component registry with inherent depth to handle complex nesting. This is
|
||||
* important for features such as grouped fields.
|
||||
*/
|
||||
class Registry {
|
||||
/**
|
||||
* Create a new registry of components.
|
||||
* @param {vm} ctx The host vm context of the registry.
|
||||
*/
|
||||
constructor (ctx) {
|
||||
this.registry = new Map()
|
||||
this.ctx = ctx
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an item to the registry.
|
||||
* @param {string|array} key
|
||||
* @param {vue} component
|
||||
*/
|
||||
add (name, component) {
|
||||
this.registry.set(name, component)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an item from the registry.
|
||||
* @param {string} name
|
||||
*/
|
||||
remove (name) {
|
||||
this.registry.delete(name)
|
||||
const { [name]: value, ...newProxy } = this.ctx.proxy
|
||||
this.ctx.proxy = newProxy
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the registry has the given key.
|
||||
* @param {string|array} key
|
||||
*/
|
||||
has (key) {
|
||||
return this.registry.has(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a particular registry value.
|
||||
* @param {string} key
|
||||
*/
|
||||
get (key) {
|
||||
return this.registry.get(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Map over the registry (recursively).
|
||||
* @param {function} callback
|
||||
*/
|
||||
map (callback) {
|
||||
const value = {}
|
||||
this.registry.forEach((component, field) => Object.assign(value, { [field]: callback(component, field) }))
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the keys of the registry.
|
||||
*/
|
||||
keys () {
|
||||
return Array.from(this.registry.keys())
|
||||
}
|
||||
|
||||
/**
|
||||
* Fully register a component.
|
||||
* @param {string} field name of the field.
|
||||
* @param {vm} component the actual component instance.
|
||||
*/
|
||||
register (field, component) {
|
||||
if (this.registry.has(field)) {
|
||||
return false
|
||||
}
|
||||
this.registry.set(field, component)
|
||||
const hasVModelValue = has(component.$options.propsData, 'formularioValue')
|
||||
const hasValue = has(component.$options.propsData, 'value')
|
||||
if (
|
||||
!hasVModelValue &&
|
||||
this.ctx.hasInitialValue &&
|
||||
getNested(this.ctx.initialValues, field) !== undefined
|
||||
) {
|
||||
// In the case that the form is carrying an initial value and the
|
||||
// element is not, set it directly.
|
||||
component.context.model = getNested(this.ctx.initialValues, field)
|
||||
} else if (
|
||||
(hasVModelValue || hasValue) &&
|
||||
!shallowEqualObjects(component.proxy, getNested(this.ctx.initialValues, field))
|
||||
) {
|
||||
// In this case, the field is v-modeled or has an initial value and the
|
||||
// form has no value or a different value, so use the field value
|
||||
this.ctx.setFieldValue(field, component.proxy)
|
||||
}
|
||||
if (this.childrenShouldShowErrors) {
|
||||
component.formShouldShowErrors = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce the registry.
|
||||
* @param {function} callback
|
||||
*/
|
||||
reduce (callback, accumulator) {
|
||||
this.registry.forEach((component, field) => {
|
||||
accumulator = callback(accumulator, component, field)
|
||||
})
|
||||
return accumulator
|
||||
}
|
||||
|
||||
/**
|
||||
* Data props to expose.
|
||||
*/
|
||||
dataProps () {
|
||||
return {
|
||||
proxy: {},
|
||||
registry: this,
|
||||
register: this.register.bind(this),
|
||||
deregister: field => this.remove(field),
|
||||
childrenShouldShowErrors: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The context component.
|
||||
* @param {component} contextComponent
|
||||
*/
|
||||
export default function useRegistry (contextComponent) {
|
||||
const registry = new Registry(contextComponent)
|
||||
return registry.dataProps()
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed properties related to the registry.
|
||||
*/
|
||||
export function useRegistryComputed () {
|
||||
return {
|
||||
hasInitialValue () {
|
||||
return (
|
||||
(this.formularioValue && typeof this.formularioValue === 'object') ||
|
||||
(this.values && typeof this.values === 'object') ||
|
||||
(this.isGrouping && typeof this.context.model[this.index] === 'object')
|
||||
)
|
||||
},
|
||||
isVmodeled () {
|
||||
return !!(this.$options.propsData.hasOwnProperty('formularioValue') &&
|
||||
this._events &&
|
||||
Array.isArray(this._events.input) &&
|
||||
this._events.input.length)
|
||||
},
|
||||
initialValues () {
|
||||
if (
|
||||
has(this.$options.propsData, 'formularioValue') &&
|
||||
typeof this.formularioValue === 'object'
|
||||
) {
|
||||
// If there is a v-model on the form/group, use those values as first priority
|
||||
return Object.assign({}, this.formularioValue) // @todo - use a deep clone to detach reference types
|
||||
} else if (
|
||||
has(this.$options.propsData, 'values') &&
|
||||
typeof this.values === 'object'
|
||||
) {
|
||||
// If there are values, use them as secondary priority
|
||||
return Object.assign({}, this.values)
|
||||
} else if (
|
||||
this.isGrouping && typeof this.context.model[this.index] === 'object'
|
||||
) {
|
||||
return this.context.model[this.index]
|
||||
}
|
||||
return {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods used in the registry.
|
||||
*/
|
||||
export function useRegistryMethods (without = []) {
|
||||
const methods = {
|
||||
applyInitialValues () {
|
||||
if (this.hasInitialValue) {
|
||||
this.proxy = this.initialValues
|
||||
}
|
||||
},
|
||||
setFieldValue (field, value) {
|
||||
if (value === undefined) {
|
||||
const { [field]: value, ...proxy } = this.proxy
|
||||
this.proxy = proxy
|
||||
} else {
|
||||
setNested(this.proxy, field, value);
|
||||
}
|
||||
this.$emit('input', Object.assign({}, this.proxy))
|
||||
},
|
||||
getFormValues () {
|
||||
return this.proxy
|
||||
},
|
||||
hasValidationErrors () {
|
||||
return Promise.all(this.registry.reduce((resolvers, cmp, name) => {
|
||||
resolvers.push(cmp.performValidation() && cmp.getValidationErrors())
|
||||
return resolvers
|
||||
}, [])).then(errorObjects => errorObjects.some(item => item.hasErrors))
|
||||
},
|
||||
showErrors () {
|
||||
this.childrenShouldShowErrors = true
|
||||
this.registry.map(input => {
|
||||
input.formShouldShowErrors = true
|
||||
})
|
||||
},
|
||||
hideErrors () {
|
||||
this.childrenShouldShowErrors = false
|
||||
this.registry.map(input => {
|
||||
input.formShouldShowErrors = false
|
||||
input.behavioralErrorVisibility = false
|
||||
})
|
||||
},
|
||||
setValues (values) {
|
||||
// Collect all keys, existing and incoming
|
||||
const keys = Array.from(new Set(Object.keys(values).concat(Object.keys(this.proxy))))
|
||||
keys.forEach(field => {
|
||||
if (this.registry.has(field) &&
|
||||
!shallowEqualObjects(getNested(values, field), getNested(this.proxy, field)) &&
|
||||
!shallowEqualObjects(getNested(values, field), this.registry.get(field).proxy)
|
||||
) {
|
||||
this.setFieldValue(field, getNested(values, field))
|
||||
this.registry.get(field).context.model = getNested(values, field)
|
||||
}
|
||||
})
|
||||
this.applyInitialValues()
|
||||
}
|
||||
}
|
||||
return Object.keys(methods).reduce((withMethods, key) => {
|
||||
return without.includes(key) ? withMethods : { ...withMethods, [key]: methods[key] }
|
||||
}, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Providers related to the registry.
|
||||
*/
|
||||
export function useRegistryProviders (ctx) {
|
||||
return {
|
||||
formularioSetter: ctx.setFieldValue,
|
||||
formularioRegister: ctx.register,
|
||||
formularioDeregister: ctx.deregister,
|
||||
getFormValues: ctx.getFormValues
|
||||
}
|
||||
}
|
159
src/libs/registry.ts
Normal file
159
src/libs/registry.ts
Normal file
@ -0,0 +1,159 @@
|
||||
import { shallowEqualObjects, has, getNested } from './utils'
|
||||
import FormularioForm from '@/FormularioForm.vue'
|
||||
import FormularioInput from '@/FormularioInput.vue'
|
||||
|
||||
/**
|
||||
* Component registry with inherent depth to handle complex nesting. This is
|
||||
* important for features such as grouped fields.
|
||||
*/
|
||||
export default class Registry {
|
||||
private ctx: FormularioForm
|
||||
private registry: Map<string, FormularioInput>
|
||||
|
||||
/**
|
||||
* Create a new registry of components.
|
||||
* @param {FormularioForm} ctx The host vm context of the registry.
|
||||
*/
|
||||
constructor (ctx: FormularioForm) {
|
||||
this.registry = new Map()
|
||||
this.ctx = ctx
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an item to the registry.
|
||||
*/
|
||||
add (name: string, component: FormularioInput): void {
|
||||
this.registry.set(name, component)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an item from the registry.
|
||||
*/
|
||||
remove (name: string): void {
|
||||
this.registry.delete(name)
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { [name]: value, ...newProxy } = this.ctx.proxy
|
||||
// @ts-ignore
|
||||
this.ctx.proxy = newProxy
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the registry has the given key.
|
||||
*/
|
||||
has (key: string): boolean {
|
||||
return this.registry.has(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the registry has elements, that equals or nested given key
|
||||
*/
|
||||
hasNested (key: string): boolean {
|
||||
for (const i of this.registry.keys()) {
|
||||
if (i === key || i.includes(key + '.')) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a particular registry value.
|
||||
*/
|
||||
get (key: string): FormularioInput | undefined {
|
||||
return this.registry.get(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get registry value for key or nested to given key
|
||||
*/
|
||||
getNested (key: string): Map<string, FormularioInput> {
|
||||
const result = new Map()
|
||||
|
||||
for (const i of this.registry.keys()) {
|
||||
if (i === key || i.includes(key + '.')) {
|
||||
result.set(i, this.registry.get(i))
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Map over the registry (recursively).
|
||||
*/
|
||||
map (mapper: Function): Record<string, any> {
|
||||
const value = {}
|
||||
this.registry.forEach((component, field) => Object.assign(value, { [field]: mapper(component, field) }))
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Map over the registry (recursively).
|
||||
*/
|
||||
forEach (callback: Function): void {
|
||||
this.registry.forEach((component, field) => {
|
||||
callback(component, field)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the keys of the registry.
|
||||
*/
|
||||
keys (): string[] {
|
||||
return Array.from(this.registry.keys())
|
||||
}
|
||||
|
||||
/**
|
||||
* Fully register a component.
|
||||
* @param {string} field name of the field.
|
||||
* @param {FormularioForm} component the actual component instance.
|
||||
*/
|
||||
register (field: string, component: FormularioInput): void {
|
||||
if (this.registry.has(field)) {
|
||||
return
|
||||
}
|
||||
this.registry.set(field, component)
|
||||
const hasVModelValue = has(component.$options.propsData as Record<string, any>, 'formularioValue')
|
||||
const hasValue = has(component.$options.propsData as Record<string, any>, 'value')
|
||||
if (
|
||||
!hasVModelValue &&
|
||||
// @ts-ignore
|
||||
this.ctx.hasInitialValue &&
|
||||
// @ts-ignore
|
||||
getNested(this.ctx.initialValues, field) !== undefined
|
||||
) {
|
||||
// In the case that the form is carrying an initial value and the
|
||||
// element is not, set it directly.
|
||||
// @ts-ignore
|
||||
component.context.model = getNested(this.ctx.initialValues, field)
|
||||
} else if (
|
||||
(hasVModelValue || hasValue) &&
|
||||
// @ts-ignore
|
||||
!shallowEqualObjects(component.proxy, getNested(this.ctx.initialValues, field))
|
||||
) {
|
||||
// In this case, the field is v-modeled or has an initial value and the
|
||||
// form has no value or a different value, so use the field value
|
||||
// @ts-ignore
|
||||
this.ctx.setFieldValue(field, component.proxy)
|
||||
}
|
||||
// @ts-ignore
|
||||
if (this.ctx.childrenShouldShowErrors) {
|
||||
// @ts-ignore
|
||||
component.formShouldShowErrors = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce the registry.
|
||||
* @param {function} callback
|
||||
* @param accumulator
|
||||
*/
|
||||
reduce<U> (callback: Function, accumulator: U): U {
|
||||
this.registry.forEach((component, field) => {
|
||||
accumulator = callback(accumulator, component, field)
|
||||
})
|
||||
return accumulator
|
||||
}
|
||||
}
|
@ -1,48 +1,46 @@
|
||||
import FileUpload from '../FileUpload'
|
||||
import FileUpload from '@/FileUpload'
|
||||
|
||||
/**
|
||||
* Function to map over an object.
|
||||
* @param {Object} obj An object to map over
|
||||
* @param {Object} original 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])
|
||||
export function map (original: Record<string, any>, callback: Function): Record<string, any> {
|
||||
const obj: Record<string, any> = {}
|
||||
for (const key in original) {
|
||||
if (Object.prototype.hasOwnProperty.call(original, key)) {
|
||||
obj[key] = callback(key, original[key])
|
||||
}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
/**
|
||||
* Shallow equal.
|
||||
* @param {} objA
|
||||
* @param {*} objB
|
||||
*/
|
||||
export function shallowEqualObjects (objA, objB) {
|
||||
export function shallowEqualObjects (objA: Record<string, any>, objB: Record<string, any>): boolean {
|
||||
if (objA === objB) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!objA || !objB) {
|
||||
return false
|
||||
}
|
||||
var aKeys = Object.keys(objA)
|
||||
var bKeys = Object.keys(objB)
|
||||
var len = aKeys.length
|
||||
|
||||
if (bKeys.length !== len) {
|
||||
const aKeys = Object.keys(objA)
|
||||
const bKeys = Object.keys(objB)
|
||||
|
||||
if (bKeys.length !== aKeys.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (objA instanceof Date && objB instanceof Date) {
|
||||
return objA.getTime() === objB.getTime();
|
||||
return objA.getTime() === objB.getTime()
|
||||
}
|
||||
|
||||
if (len === 0) {
|
||||
return objA === objB;
|
||||
if (aKeys.length === 0) {
|
||||
return objA === objB
|
||||
}
|
||||
|
||||
for (var i = 0; i < len; i++) {
|
||||
var key = aKeys[i]
|
||||
for (let i = 0; i < aKeys.length; i++) {
|
||||
const key = aKeys[i]
|
||||
|
||||
if (objA[key] !== objB[key]) {
|
||||
return false
|
||||
@ -55,7 +53,7 @@ export function shallowEqualObjects (objA, objB) {
|
||||
* Given a string, convert snake_case to camelCase
|
||||
* @param {String} string
|
||||
*/
|
||||
export function snakeToCamel (string) {
|
||||
export function snakeToCamel (string: string | any): string | any {
|
||||
if (typeof string === 'string') {
|
||||
return string.replace(/([_][a-z0-9])/ig, ($1) => {
|
||||
if (string.indexOf($1) !== 0 && string[string.indexOf($1) - 1] !== '_') {
|
||||
@ -68,10 +66,21 @@ export function snakeToCamel (string) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string, object, falsey, or array - return an array.
|
||||
* @param {mixed} item
|
||||
* Return the rule name with the applicable modifier as an array.
|
||||
*/
|
||||
export function arrayify (item) {
|
||||
function parseModifier (ruleName: any): [string|any, string|null] {
|
||||
if (typeof ruleName === 'string' && /^[\^]/.test(ruleName.charAt(0))) {
|
||||
return [snakeToCamel(ruleName.substr(1)), ruleName.charAt(0)]
|
||||
}
|
||||
return [snakeToCamel(ruleName), null]
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to array.
|
||||
* If given parameter is not string, object ot array, result will be an empty array.
|
||||
* @param {*} item
|
||||
*/
|
||||
export function arrayify (item: any): any[] {
|
||||
if (!item) {
|
||||
return []
|
||||
}
|
||||
@ -87,13 +96,47 @@ export function arrayify (item) {
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string or function, parse it and return an array in the format
|
||||
* [fn, [...arguments]]
|
||||
*/
|
||||
function parseRule (rule: any, rules: Record<string, any>) {
|
||||
if (typeof rule === 'function') {
|
||||
return [rule, []]
|
||||
}
|
||||
|
||||
if (Array.isArray(rule) && rule.length) {
|
||||
rule = rule.slice() // light clone
|
||||
const [ruleName, modifier] = parseModifier(rule.shift())
|
||||
if (typeof ruleName === 'string' && Object.prototype.hasOwnProperty.call(rules, ruleName)) {
|
||||
return [rules[ruleName], rule, ruleName, modifier]
|
||||
}
|
||||
if (typeof ruleName === 'function') {
|
||||
return [ruleName, rule, ruleName, modifier]
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof rule === 'string') {
|
||||
const segments = rule.split(':')
|
||||
const [ruleName, modifier] = parseModifier(segments.shift())
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(rules, ruleName)) {
|
||||
return [rules[ruleName], segments.length ? segments.join(':').split(',') : [], ruleName, modifier]
|
||||
} else {
|
||||
throw new Error(`Unknown validation rule ${rule}`)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array or string return an array of callables.
|
||||
* @param {array|string} validation
|
||||
* @param {array} rules and array of functions
|
||||
* @return {array} an array of functions
|
||||
*/
|
||||
export function parseRules (validation, rules) {
|
||||
export function parseRules (validation: any[]|string, rules: any): any[] {
|
||||
if (typeof validation === 'string') {
|
||||
return parseRules(validation.split('|'), rules)
|
||||
}
|
||||
@ -103,49 +146,6 @@ export function parseRules (validation, rules) {
|
||||
return validation.map(rule => parseRule(rule, rules)).filter(f => !!f)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string or function, parse it and return an array in the format
|
||||
* [fn, [...arguments]]
|
||||
* @param {string|function} rule
|
||||
*/
|
||||
function parseRule (rule, rules) {
|
||||
if (typeof rule === 'function') {
|
||||
return [rule, []]
|
||||
}
|
||||
if (Array.isArray(rule) && rule.length) {
|
||||
rule = rule.map(r => r) // light clone
|
||||
const [ruleName, modifier] = parseModifier(rule.shift())
|
||||
if (typeof ruleName === 'string' && rules.hasOwnProperty(ruleName)) {
|
||||
return [rules[ruleName], rule, ruleName, modifier]
|
||||
}
|
||||
if (typeof ruleName === 'function') {
|
||||
return [ruleName, rule, ruleName, modifier]
|
||||
}
|
||||
}
|
||||
if (typeof rule === 'string') {
|
||||
const segments = rule.split(':')
|
||||
const [ruleName, modifier] = parseModifier(segments.shift())
|
||||
if (rules.hasOwnProperty(ruleName)) {
|
||||
return [rules[ruleName], segments.length ? segments.join(':').split(',') : [], ruleName, modifier]
|
||||
} else {
|
||||
throw new Error(`Unknown validation rule ${rule}`)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the rule name with the applicable modifier as an array.
|
||||
* @param {string} ruleName
|
||||
* @return {array} [ruleName, modifier]
|
||||
*/
|
||||
function parseModifier (ruleName) {
|
||||
if (/^[\^]/.test(ruleName.charAt(0))) {
|
||||
return [snakeToCamel(ruleName.substr(1)), ruleName.charAt(0)]
|
||||
}
|
||||
return [snakeToCamel(ruleName), null]
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array of rules, group them by bail signals. For example for this:
|
||||
* bail|required|min:10|max:20
|
||||
@ -159,7 +159,7 @@ function parseModifier (ruleName) {
|
||||
* [[required, min, max]]
|
||||
* @param {array} rules
|
||||
*/
|
||||
export function groupBails (rules) {
|
||||
export function groupBails (rules: any[]) {
|
||||
const groups = []
|
||||
const bailIndex = rules.findIndex(([,, rule]) => rule.toLowerCase() === 'bail')
|
||||
if (bailIndex >= 0) {
|
||||
@ -174,11 +174,13 @@ export function groupBails (rules) {
|
||||
}
|
||||
|
||||
return groups.reduce((groups, group) => {
|
||||
// @ts-ignore
|
||||
const splitByMod = (group, bailGroup = false) => {
|
||||
if (group.length < 2) {
|
||||
return Object.defineProperty([group], 'bail', { value: bailGroup })
|
||||
}
|
||||
const splits = []
|
||||
// @ts-ignore
|
||||
const modIndex = group.findIndex(([,,, modifier]) => modifier === '^')
|
||||
if (modIndex >= 0) {
|
||||
const preMod = group.splice(0, modIndex)
|
||||
@ -200,7 +202,7 @@ export function groupBails (rules) {
|
||||
* Escape a string for use in regular expressions.
|
||||
* @param {string} string
|
||||
*/
|
||||
export function escapeRegExp (string) {
|
||||
export function escapeRegExp (string: string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
|
||||
}
|
||||
|
||||
@ -208,8 +210,8 @@ export function escapeRegExp (string) {
|
||||
* Given a string format (date) return a regex to match against.
|
||||
* @param {string} format
|
||||
*/
|
||||
export function regexForFormat (format) {
|
||||
let escaped = `^${escapeRegExp(format)}$`
|
||||
export function regexForFormat (format: string) {
|
||||
const escaped = `^${escapeRegExp(format)}$`
|
||||
const formats = {
|
||||
MM: '(0[1-9]|1[012])',
|
||||
M: '([1-9]|1[012])',
|
||||
@ -219,15 +221,16 @@ export function regexForFormat (format) {
|
||||
YY: '\\d{2}'
|
||||
}
|
||||
return new RegExp(Object.keys(formats).reduce((regex, format) => {
|
||||
// @ts-ignore
|
||||
return regex.replace(format, formats[format])
|
||||
}, escaped))
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if
|
||||
* @param {mixed} data
|
||||
* @param {*} data
|
||||
*/
|
||||
export function isValueType (data) {
|
||||
export function isScalar (data: any): boolean {
|
||||
switch (typeof data) {
|
||||
case 'symbol':
|
||||
case 'number':
|
||||
@ -236,10 +239,7 @@ export function isValueType (data) {
|
||||
case 'undefined':
|
||||
return true
|
||||
default:
|
||||
if (data === null) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return data === null
|
||||
}
|
||||
}
|
||||
|
||||
@ -247,29 +247,33 @@ export function isValueType (data) {
|
||||
* A simple (somewhat non-comprehensive) cloneDeep function, valid for our use
|
||||
* case of needing to unbind reactive watchers.
|
||||
*/
|
||||
export function cloneDeep (obj) {
|
||||
if (typeof obj !== 'object') {
|
||||
return obj
|
||||
export function cloneDeep (value: any): any {
|
||||
if (typeof value !== 'object') {
|
||||
return value
|
||||
}
|
||||
const isArr = Array.isArray(obj)
|
||||
const newObj = isArr ? [] : {}
|
||||
for (const key in obj) {
|
||||
if (obj[key] instanceof FileUpload || isValueType(obj[key])) {
|
||||
newObj[key] = obj[key]
|
||||
} else {
|
||||
newObj[key] = cloneDeep(obj[key])
|
||||
|
||||
const copy: any | Record<string, any> = Array.isArray(value) ? [] : {}
|
||||
|
||||
for (const key in value) {
|
||||
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
||||
if (isScalar(value[key]) || value[key] instanceof FileUpload) {
|
||||
copy[key] = value[key]
|
||||
} else {
|
||||
copy[key] = cloneDeep(value[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
return newObj
|
||||
|
||||
return copy
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a locale string, parse the options.
|
||||
* @param {string} locale
|
||||
*/
|
||||
export function parseLocale (locale) {
|
||||
export function parseLocale (locale: string): string[] {
|
||||
const segments = locale.split('-')
|
||||
return segments.reduce((options, segment) => {
|
||||
return segments.reduce((options: string[], segment: string) => {
|
||||
if (options.length) {
|
||||
options.unshift(`${options[0]}-${segment}`)
|
||||
}
|
||||
@ -280,75 +284,65 @@ export function parseLocale (locale) {
|
||||
/**
|
||||
* Shorthand for Object.prototype.hasOwnProperty.call (space saving)
|
||||
*/
|
||||
export function has (ctx, prop) {
|
||||
export function has (ctx: Record<string, any>, prop: string): boolean {
|
||||
return Object.prototype.hasOwnProperty.call(ctx, prop)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a unique Symbol identifier on an object.
|
||||
* @param {object} o
|
||||
* @param {Symbol} id
|
||||
*/
|
||||
export function setId (o, id) {
|
||||
return Object.defineProperty(o, '__id', Object.assign(Object.create(null), { value: id || Symbol('uuid') }))
|
||||
}
|
||||
export function getNested (obj: Record<string, any>, field: string): any {
|
||||
const fieldParts = field.split('.')
|
||||
|
||||
export function getNested(obj, field) {
|
||||
let fieldParts = field.split('.');
|
||||
let result: Record<string, any> = obj
|
||||
|
||||
let result = obj;
|
||||
for (const key in fieldParts) {
|
||||
let matches = fieldParts[key].match(/(.+)\[(\d+)\]$/);
|
||||
const matches = fieldParts[key].match(/(.+)\[(\d+)\]$/)
|
||||
if (result === undefined) {
|
||||
return null
|
||||
}
|
||||
if (matches) {
|
||||
result = result[matches[1]];
|
||||
result = result[matches[1]]
|
||||
|
||||
if (result === undefined) {
|
||||
return null
|
||||
}
|
||||
result = result[matches[2]];
|
||||
result = result[matches[2]]
|
||||
} else {
|
||||
result = result[fieldParts[key]];
|
||||
result = result[fieldParts[key]]
|
||||
}
|
||||
}
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
|
||||
export function setNested(obj, field, value) {
|
||||
let fieldParts = field.split('.');
|
||||
export function setNested (obj: Record<string, any>, field: string, value: any): void {
|
||||
const fieldParts = field.split('.')
|
||||
|
||||
let subProxy = obj;
|
||||
let subProxy: Record<string, any> = obj
|
||||
for (let i = 0; i < fieldParts.length; i++) {
|
||||
let fieldPart = fieldParts[i];
|
||||
|
||||
let matches = fieldPart.match(/(.+)\[(\d+)\]$/);
|
||||
const fieldPart = fieldParts[i]
|
||||
const matches = fieldPart.match(/(.+)\[(\d+)\]$/)
|
||||
|
||||
if (matches) {
|
||||
if (subProxy[matches[1]] === undefined) {
|
||||
subProxy[matches[1]] = [];
|
||||
subProxy[matches[1]] = []
|
||||
}
|
||||
subProxy = subProxy[matches[1]];
|
||||
subProxy = subProxy[matches[1]]
|
||||
|
||||
if (i == fieldParts.length - 1) {
|
||||
if (i === fieldParts.length - 1) {
|
||||
subProxy[matches[2]] = value
|
||||
break;
|
||||
break
|
||||
} else {
|
||||
subProxy = subProxy[matches[2]];
|
||||
subProxy = subProxy[matches[2]]
|
||||
}
|
||||
} else {
|
||||
if (i == fieldParts.length - 1) {
|
||||
if (i === fieldParts.length - 1) {
|
||||
subProxy[fieldPart] = value
|
||||
break;
|
||||
break
|
||||
} else {
|
||||
// eslint-disable-next-line max-depth
|
||||
if (subProxy[fieldPart] === undefined) {
|
||||
subProxy[fieldPart] = {};
|
||||
subProxy[fieldPart] = {}
|
||||
}
|
||||
subProxy = subProxy[fieldPart];
|
||||
subProxy = subProxy[fieldPart]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
20
src/shims-ext.d.ts
vendored
Normal file
20
src/shims-ext.d.ts
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
import Formulario from '@/Formulario'
|
||||
|
||||
declare module 'vue/types/vue' {
|
||||
interface Vue {
|
||||
$formulario: Formulario;
|
||||
$route: VueRoute;
|
||||
$t: Function;
|
||||
$tc: Function;
|
||||
}
|
||||
|
||||
interface VueRoute {
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface FormularioForm extends Vue {
|
||||
name: string | boolean;
|
||||
proxy: Record<string, any>;
|
||||
hasValidationErrors(): Promise<boolean>;
|
||||
}
|
||||
}
|
11
src/shims-tsx.d.ts
vendored
Normal file
11
src/shims-tsx.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
import Vue, { VNode } from 'vue'
|
||||
|
||||
declare global {
|
||||
namespace JSX {
|
||||
interface Element extends VNode {}
|
||||
interface ElementClass extends Vue {}
|
||||
interface IntrinsicElements {
|
||||
[elem: string]: any;
|
||||
}
|
||||
}
|
||||
}
|
4
src/shims-vue.d.ts
vendored
Normal file
4
src/shims-vue.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
declare module '*.vue' {
|
||||
import Vue from 'vue'
|
||||
export default Vue
|
||||
}
|
40
src/utils/merge.ts
Normal file
40
src/utils/merge.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import isPlainObject from 'is-plain-object'
|
||||
import { has } from '@/libs/utils.ts'
|
||||
|
||||
/**
|
||||
* Create a new object by copying properties of base and mergeWith.
|
||||
* Note: arrays don't overwrite - they push
|
||||
*
|
||||
* @param {Object} a
|
||||
* @param {Object} b
|
||||
* @param {boolean} concatArrays
|
||||
*/
|
||||
export default function merge (
|
||||
a: Record<string, any>,
|
||||
b: Record<string, any>,
|
||||
concatArrays = true
|
||||
): Record<string, any> {
|
||||
const merged: Record<string, any> = {}
|
||||
|
||||
for (const key in a) {
|
||||
if (has(b, key)) {
|
||||
if (isPlainObject(b[key]) && isPlainObject(a[key])) {
|
||||
merged[key] = merge(a[key], b[key], concatArrays)
|
||||
} else if (concatArrays && Array.isArray(a[key]) && Array.isArray(b[key])) {
|
||||
merged[key] = a[key].concat(b[key])
|
||||
} else {
|
||||
merged[key] = b[key]
|
||||
}
|
||||
} else {
|
||||
merged[key] = a[key]
|
||||
}
|
||||
}
|
||||
|
||||
for (const prop in b) {
|
||||
if (!has(merged, prop)) {
|
||||
merged[prop] = b[prop]
|
||||
}
|
||||
}
|
||||
|
||||
return merged
|
||||
}
|
@ -1,6 +1,13 @@
|
||||
// @ts-ignore
|
||||
import isUrl from 'is-url'
|
||||
import FileUpload from '../FileUpload'
|
||||
import { shallowEqualObjects, regexForFormat } from './utils'
|
||||
import { shallowEqualObjects, regexForFormat, has } from '@/libs/utils'
|
||||
import { ValidatableData } from '@/validation/types'
|
||||
|
||||
interface ConfirmValidatableData extends ValidatableData {
|
||||
getFormValues: () => Record<string, any>;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Library of rules
|
||||
@ -9,68 +16,70 @@ export default {
|
||||
/**
|
||||
* Rule: the value must be "yes", "on", "1", or true
|
||||
*/
|
||||
accepted: function ({ value }) {
|
||||
accepted ({ value }: ValidatableData): Promise<boolean> {
|
||||
return Promise.resolve(['yes', 'on', '1', 1, true, 'true'].includes(value))
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: checks if a value is after a given date. Defaults to current time
|
||||
*/
|
||||
after: function ({ value }, compare = false) {
|
||||
const timestamp = Date.parse(compare || new Date())
|
||||
const fieldValue = Date.parse(value)
|
||||
after ({ value }: { value: Date|string }, compare: string | false = false): Promise<boolean> {
|
||||
const timestamp = compare !== false ? Date.parse(compare) : Date.now()
|
||||
const fieldValue = value instanceof Date ? value.getTime() : Date.parse(value)
|
||||
return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue > timestamp))
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: checks if the value is only alpha
|
||||
*/
|
||||
alpha: function ({ value }, set = 'default') {
|
||||
alpha ({ value }: { value: string }, set = 'default'): Promise<boolean> {
|
||||
const sets = {
|
||||
default: /^[a-zA-ZÀ-ÖØ-öø-ÿ]+$/,
|
||||
latin: /^[a-zA-Z]+$/
|
||||
}
|
||||
const selectedSet = sets.hasOwnProperty(set) ? set : 'default'
|
||||
const selectedSet = has(sets, set) ? set : 'default'
|
||||
// @ts-ignore
|
||||
return Promise.resolve(sets[selectedSet].test(value))
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: checks if the value is alpha numeric
|
||||
*/
|
||||
alphanumeric: function ({ value }, set = 'default') {
|
||||
alphanumeric ({ value }: { value: string }, set = 'default'): Promise<boolean> {
|
||||
const sets = {
|
||||
default: /^[a-zA-Z0-9À-ÖØ-öø-ÿ]+$/,
|
||||
latin: /^[a-zA-Z0-9]+$/
|
||||
}
|
||||
const selectedSet = sets.hasOwnProperty(set) ? set : 'default'
|
||||
const selectedSet = has(sets, set) ? set : 'default'
|
||||
// @ts-ignore
|
||||
return Promise.resolve(sets[selectedSet].test(value))
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: checks if a value is after a given date. Defaults to current time
|
||||
*/
|
||||
before: function ({ value }, compare = false) {
|
||||
const timestamp = Date.parse(compare || new Date())
|
||||
const fieldValue = Date.parse(value)
|
||||
before ({ value }: { value: Date|string }, compare: string|false = false): Promise<boolean> {
|
||||
const timestamp = compare !== false ? Date.parse(compare) : Date.now()
|
||||
const fieldValue = value instanceof Date ? value.getTime() : Date.parse(value)
|
||||
return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue < timestamp))
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: checks if the value is between two other values
|
||||
*/
|
||||
between: function ({ value }, from = 0, to = 10, force) {
|
||||
return Promise.resolve((() => {
|
||||
between ({ value }: { value: string|number }, from: number|any = 0, to: number|any = 10, force?: string): Promise<boolean> {
|
||||
return Promise.resolve(((): boolean => {
|
||||
if (from === null || to === null || isNaN(from) || isNaN(to)) {
|
||||
return false
|
||||
}
|
||||
if ((!isNaN(value) && force !== 'length') || force === 'value') {
|
||||
if ((!isNaN(Number(value)) && force !== 'length') || force === 'value') {
|
||||
value = Number(value)
|
||||
from = Number(from)
|
||||
to = Number(to)
|
||||
return (value > from && value < to)
|
||||
}
|
||||
if (typeof value === 'string' || force === 'length') {
|
||||
value = !isNaN(value) ? value.toString() : value
|
||||
value = (!isNaN(Number(value)) ? value.toString() : value) as string
|
||||
return value.length > from && value.length < to
|
||||
}
|
||||
return false
|
||||
@ -81,10 +90,10 @@ export default {
|
||||
* Confirm that the value of one field is the same as another, mostly used
|
||||
* for password confirmations.
|
||||
*/
|
||||
confirm: function ({ value, getFormValues, name }, field) {
|
||||
return Promise.resolve((() => {
|
||||
confirm ({ value, getFormValues, name }: ConfirmValidatableData, field?: string): Promise<boolean> {
|
||||
return Promise.resolve(((): boolean => {
|
||||
const formValues = getFormValues()
|
||||
var confirmationFieldName = field
|
||||
let confirmationFieldName = field
|
||||
if (!confirmationFieldName) {
|
||||
confirmationFieldName = /_confirm$/.test(name) ? name.substr(0, name.length - 8) : `${name}_confirm`
|
||||
}
|
||||
@ -96,64 +105,51 @@ export default {
|
||||
* Rule: ensures the value is a date according to Date.parse(), or a format
|
||||
* regex.
|
||||
*/
|
||||
date: function ({ value }, format = false) {
|
||||
return Promise.resolve((() => {
|
||||
if (format && typeof format === 'string') {
|
||||
return regexForFormat(format).test(value)
|
||||
}
|
||||
return !isNaN(Date.parse(value))
|
||||
})())
|
||||
date ({ value }: { value: string }, format: string | false = false): Promise<boolean> {
|
||||
return Promise.resolve(format ? regexForFormat(format).test(value) : !isNaN(Date.parse(value)))
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: tests
|
||||
*/
|
||||
email: function ({ value }) {
|
||||
email ({ value }: { value: string }): Promise<boolean> {
|
||||
if (!value) {
|
||||
return Promise.resolve(() => { return true })
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
const isEmail = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i
|
||||
const isEmail = /^(([^<>()\[\].,;:\s@"]+(\.[^<>()\[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i
|
||||
return Promise.resolve(isEmail.test(value))
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: Value ends with one of the given Strings
|
||||
*/
|
||||
endsWith: function ({ value }, ...stack) {
|
||||
endsWith ({ value }: { value: any }, ...stack: any[]): Promise<boolean> {
|
||||
if (!value) {
|
||||
return Promise.resolve(() => { return true })
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
|
||||
return Promise.resolve((() => {
|
||||
if (typeof value === 'string' && stack.length) {
|
||||
return stack.find(item => {
|
||||
return value.endsWith(item)
|
||||
}) !== undefined
|
||||
} else if (typeof value === 'string' && stack.length === 0) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})())
|
||||
if (typeof value === 'string') {
|
||||
return Promise.resolve(stack.length === 0 || stack.some(str => value.endsWith(str)))
|
||||
}
|
||||
|
||||
return Promise.resolve(false)
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: Value is in an array (stack).
|
||||
*/
|
||||
in: function ({ value }, ...stack) {
|
||||
return Promise.resolve(stack.find(item => {
|
||||
if (typeof item === 'object') {
|
||||
return shallowEqualObjects(item, value)
|
||||
}
|
||||
return item === value
|
||||
}) !== undefined)
|
||||
in ({ value }: { value: any }, ...stack: any[]): Promise<boolean> {
|
||||
return Promise.resolve(stack.some(item => {
|
||||
return typeof item === 'object' ? shallowEqualObjects(item, value) : item === value
|
||||
}))
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: Match the value against a (stack) of patterns or strings
|
||||
*/
|
||||
matches: function ({ value }, ...stack) {
|
||||
matches ({ value }: { value: any }, ...stack: any[]): Promise<boolean> {
|
||||
return Promise.resolve(!!stack.find(pattern => {
|
||||
if (typeof pattern === 'string' && pattern.substr(0, 1) === '/' && pattern.substr(-1) === '/') {
|
||||
pattern = new RegExp(pattern.substr(1, pattern.length - 2))
|
||||
@ -168,26 +164,23 @@ export default {
|
||||
/**
|
||||
* Check the file type is correct.
|
||||
*/
|
||||
mime: function ({ value }, ...types) {
|
||||
return Promise.resolve((() => {
|
||||
if (value instanceof FileUpload) {
|
||||
const fileList = value.getFiles()
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
const file = fileList[i].file
|
||||
if (!types.includes(file.type)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})())
|
||||
mime ({ value }: { value: any }, ...types: string[]): Promise<boolean> {
|
||||
if (value instanceof FileUpload) {
|
||||
const files = value.getFiles()
|
||||
const isMimeCorrect = (file: File): boolean => types.includes(file.type)
|
||||
const allValid: boolean = files.reduce((valid: boolean, { file }) => valid && isMimeCorrect(file), true)
|
||||
|
||||
return Promise.resolve(allValid)
|
||||
}
|
||||
|
||||
return Promise.resolve(true)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check the minimum value of a particular.
|
||||
*/
|
||||
min: function ({ value }, minimum = 1, force) {
|
||||
return Promise.resolve((() => {
|
||||
min ({ value }: { value: any }, minimum: number | any = 1, force?: string): Promise<boolean> {
|
||||
return Promise.resolve(((): boolean => {
|
||||
if (Array.isArray(value)) {
|
||||
minimum = !isNaN(minimum) ? Number(minimum) : minimum
|
||||
return value.length >= minimum
|
||||
@ -207,10 +200,10 @@ export default {
|
||||
/**
|
||||
* Check the maximum value of a particular.
|
||||
*/
|
||||
max: function ({ value }, maximum = 10, force) {
|
||||
return Promise.resolve((() => {
|
||||
max ({ value }: { value: any }, maximum: string | number = 10, force?: string): Promise<boolean> {
|
||||
return Promise.resolve(((): boolean => {
|
||||
if (Array.isArray(value)) {
|
||||
maximum = !isNaN(maximum) ? Number(maximum) : maximum
|
||||
maximum = !isNaN(Number(maximum)) ? Number(maximum) : maximum
|
||||
return value.length <= maximum
|
||||
}
|
||||
if ((!isNaN(value) && force !== 'length') || force === 'value') {
|
||||
@ -228,28 +221,25 @@ export default {
|
||||
/**
|
||||
* Rule: Value is not in stack.
|
||||
*/
|
||||
not: function ({ value }, ...stack) {
|
||||
return Promise.resolve(stack.find(item => {
|
||||
if (typeof item === 'object') {
|
||||
return shallowEqualObjects(item, value)
|
||||
}
|
||||
return item === value
|
||||
}) === undefined)
|
||||
not ({ value }: { value: any }, ...stack: any[]): Promise<boolean> {
|
||||
return Promise.resolve(!stack.some(item => {
|
||||
return typeof item === 'object' ? shallowEqualObjects(item, value) : item === value
|
||||
}))
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: checks if the value is only alpha numeric
|
||||
*/
|
||||
number: function ({ value }) {
|
||||
return Promise.resolve(!isNaN(value))
|
||||
number ({ value }: { value: any }): Promise<boolean> {
|
||||
return Promise.resolve(!isNaN(Number(value)))
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: must be a value
|
||||
*/
|
||||
required: function ({ value }, isRequired = true) {
|
||||
return Promise.resolve((() => {
|
||||
if (!isRequired || ['no', 'false'].includes(isRequired)) {
|
||||
required ({ value }: { value: any }, isRequired: string|boolean = true): Promise<boolean> {
|
||||
return Promise.resolve(((): boolean => {
|
||||
if (!isRequired || ['no', 'false'].includes(isRequired as string)) {
|
||||
return true
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
@ -271,34 +261,29 @@ export default {
|
||||
/**
|
||||
* Rule: Value starts with one of the given Strings
|
||||
*/
|
||||
startsWith: function ({ value }, ...stack) {
|
||||
startsWith ({ value }: { value: any }, ...stack: string[]): Promise<boolean> {
|
||||
if (!value) {
|
||||
return Promise.resolve(() => { return true })
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
|
||||
return Promise.resolve((() => {
|
||||
if (typeof value === 'string' && stack.length) {
|
||||
return stack.find(item => {
|
||||
return value.startsWith(item)
|
||||
}) !== undefined
|
||||
} else if (typeof value === 'string' && stack.length === 0) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})())
|
||||
if (typeof value === 'string') {
|
||||
return Promise.resolve(stack.length === 0 || stack.some(str => value.startsWith(str)))
|
||||
}
|
||||
|
||||
return Promise.resolve(false)
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: checks if a string is a valid url
|
||||
*/
|
||||
url: function ({ value }) {
|
||||
url ({ value }: { value: string }): Promise<boolean> {
|
||||
return Promise.resolve(isUrl(value))
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: not a true rule — more like a compiler flag.
|
||||
*/
|
||||
bail: function () {
|
||||
bail (): Promise<boolean> {
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
}
|
25
src/validation/types.ts
Normal file
25
src/validation/types.ts
Normal file
@ -0,0 +1,25 @@
|
||||
interface ValidatableData {
|
||||
value: any;
|
||||
}
|
||||
|
||||
interface ValidationContext {
|
||||
args: any[];
|
||||
name: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
interface ValidationError {
|
||||
rule?: string;
|
||||
context?: any;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export { ValidatableData }
|
||||
export { ValidationContext }
|
||||
export { ValidationError }
|
||||
|
||||
export interface ErrorObserver {
|
||||
type: string;
|
||||
field: string;
|
||||
callback: Function;
|
||||
}
|
4
storybook/config/addons.js
Normal file
4
storybook/config/addons.js
Normal file
@ -0,0 +1,4 @@
|
||||
import '@storybook/addon-actions/register'
|
||||
import '@storybook/addon-knobs/register'
|
||||
import '@storybook/addon-links/register'
|
||||
import '@storybook/addon-notes/register'
|
7
storybook/config/config.js
Normal file
7
storybook/config/config.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { configure } from '@storybook/vue'
|
||||
|
||||
const req = require.context('../stories/', true, /.stories.js$/)
|
||||
|
||||
configure(() => {
|
||||
req.keys().forEach(filename => req(filename))
|
||||
}, module)
|
8
storybook/config/webpack.config.js
Normal file
8
storybook/config/webpack.config.js
Normal file
@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
devServer: {
|
||||
watchOptions: {
|
||||
aggregateTimeout: 300,
|
||||
poll: true,
|
||||
},
|
||||
},
|
||||
}
|
76
storybook/stories/FormularioGrouping.tale.vue
Normal file
76
storybook/stories/FormularioGrouping.tale.vue
Normal file
@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<FormularioForm v-model="values">
|
||||
<FormularioGrouping name="group">
|
||||
<FormularioInput
|
||||
v-slot="{ context }"
|
||||
class="mb-3"
|
||||
name="text"
|
||||
validation="number|required"
|
||||
>
|
||||
<label for="text-field">Text field (number|required)</label>
|
||||
<input
|
||||
id="text-field"
|
||||
v-model="context.model"
|
||||
type="text"
|
||||
class="form-control"
|
||||
style="max-width: 250px;"
|
||||
>
|
||||
|
||||
<div
|
||||
v-for="(error, index) in context.allErrors"
|
||||
:key="index"
|
||||
class="text-danger"
|
||||
>
|
||||
{{ error.message }}
|
||||
</div>
|
||||
</FormularioInput>
|
||||
|
||||
<FormularioInput
|
||||
v-slot="{ context }"
|
||||
:validation-messages="{ in: 'The value was different than expected' }"
|
||||
class="mb-3"
|
||||
name="abcdef-field"
|
||||
validation="in:abcdef"
|
||||
>
|
||||
<label for="abcdef-field">Text field (in:abcdef)</label>
|
||||
<input
|
||||
id="abcdef-field"
|
||||
v-model="context.model"
|
||||
type="text"
|
||||
class="form-control"
|
||||
style="max-width: 250px;"
|
||||
>
|
||||
|
||||
<div
|
||||
v-for="(error, index) in context.allErrors"
|
||||
:key="index"
|
||||
class="text-danger"
|
||||
>
|
||||
{{ error.message }}
|
||||
</div>
|
||||
</FormularioInput>
|
||||
</FormularioGrouping>
|
||||
|
||||
<div>{{ values }}</div>
|
||||
</FormularioForm>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormularioForm from '@/FormularioForm'
|
||||
import FormularioGrouping from '@/FormularioGrouping'
|
||||
import FormularioInput from '@/FormularioInput'
|
||||
|
||||
export default {
|
||||
name: 'FormularioGroupingTale',
|
||||
|
||||
components: {
|
||||
FormularioForm,
|
||||
FormularioGrouping,
|
||||
FormularioInput,
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
values: {},
|
||||
}),
|
||||
}
|
||||
</script>
|
76
storybook/stories/FormularioInput.tale.vue
Normal file
76
storybook/stories/FormularioInput.tale.vue
Normal file
@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<FormularioForm v-model="values">
|
||||
<FormularioInput
|
||||
v-slot="{ context }"
|
||||
:validation-messages="{
|
||||
number: 'The value is not a number',
|
||||
required: 'Value required',
|
||||
}"
|
||||
class="mb-3"
|
||||
name="number"
|
||||
validation="number|required"
|
||||
>
|
||||
<label for="text-field">Text field (number|required)</label>
|
||||
<input
|
||||
id="text-field"
|
||||
v-model="context.model"
|
||||
type="text"
|
||||
class="form-control"
|
||||
style="max-width: 250px;"
|
||||
>
|
||||
|
||||
<div
|
||||
v-for="(error, index) in context.allErrors"
|
||||
:key="index"
|
||||
class="text-danger"
|
||||
>
|
||||
{{ error.message }}
|
||||
</div>
|
||||
</FormularioInput>
|
||||
|
||||
<FormularioInput
|
||||
v-slot="{ context }"
|
||||
:validation-messages="{ in: 'The value is not in range (abc,def)' }"
|
||||
class="mb-3"
|
||||
name="abcdef-field"
|
||||
validation="in:abc,def"
|
||||
>
|
||||
<label for="abcdef-field">Text field (in:abc,def)</label>
|
||||
<input
|
||||
id="abcdef-field"
|
||||
v-model="context.model"
|
||||
type="text"
|
||||
class="form-control"
|
||||
style="max-width: 250px;"
|
||||
>
|
||||
|
||||
<div
|
||||
v-for="(error, index) in context.allErrors"
|
||||
:key="index"
|
||||
class="text-danger"
|
||||
>
|
||||
{{ error.message }}
|
||||
</div>
|
||||
</FormularioInput>
|
||||
|
||||
<div>{{ values }}</div>
|
||||
</FormularioForm>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormularioForm from '@/FormularioForm'
|
||||
import FormularioInput from '@/FormularioInput'
|
||||
|
||||
export default {
|
||||
name: 'FormularioInputTale',
|
||||
|
||||
components: {
|
||||
FormularioForm,
|
||||
FormularioInput,
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
values: {},
|
||||
}),
|
||||
}
|
||||
</script>
|
1
storybook/stories/bootstrap.scss
vendored
Normal file
1
storybook/stories/bootstrap.scss
vendored
Normal file
@ -0,0 +1 @@
|
||||
@import "~bootstrap-scss";
|
22
storybook/stories/index.stories.js
Normal file
22
storybook/stories/index.stories.js
Normal file
@ -0,0 +1,22 @@
|
||||
import './bootstrap.scss'
|
||||
|
||||
import { storiesOf } from '@storybook/vue'
|
||||
|
||||
import Vue from 'vue'
|
||||
import Formulario from '@/Formulario.ts'
|
||||
|
||||
import FormularioGroupingTale from './FormularioGrouping.tale'
|
||||
import FormularioInputTale from './FormularioInput.tale'
|
||||
|
||||
Vue.mixin({
|
||||
methods: {
|
||||
$t (text) {
|
||||
return text
|
||||
}
|
||||
}
|
||||
})
|
||||
Vue.use(new Formulario())
|
||||
|
||||
storiesOf('FormularioInput', module)
|
||||
.add('Default', () => FormularioInputTale)
|
||||
.add('Grouping', () => FormularioGroupingTale)
|
@ -1,24 +1,25 @@
|
||||
const path = require('path')
|
||||
|
||||
module.exports = {
|
||||
rootDir: path.resolve(__dirname, '../'),
|
||||
moduleFileExtensions: [
|
||||
'js',
|
||||
'json',
|
||||
'vue'
|
||||
],
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/src/$1'
|
||||
},
|
||||
modulePaths: [
|
||||
"<rootDir>"
|
||||
],
|
||||
transform: {
|
||||
'.*\\.js$': '<rootDir>/node_modules/babel-jest',
|
||||
'.*\\.(vue)$': '<rootDir>/node_modules/jest-vue-preprocessor'
|
||||
},
|
||||
collectCoverageFrom: [
|
||||
"src/*.{js,vue}",
|
||||
],
|
||||
// verbose: true
|
||||
rootDir: path.resolve(__dirname, '../'),
|
||||
moduleFileExtensions: [
|
||||
'js',
|
||||
'json',
|
||||
'ts',
|
||||
'vue',
|
||||
],
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/src/$1'
|
||||
},
|
||||
modulePaths: [
|
||||
"<rootDir>"
|
||||
],
|
||||
transform: {
|
||||
'.*\\.js$': '<rootDir>/node_modules/babel-jest',
|
||||
'.*\\.ts$': '<rootDir>/node_modules/ts-jest',
|
||||
'.*\\.(vue)$': '<rootDir>/node_modules/jest-vue-preprocessor'
|
||||
},
|
||||
collectCoverageFrom: [
|
||||
"src/*.{js,vue}",
|
||||
],
|
||||
}
|
||||
|
@ -1,65 +1,6 @@
|
||||
import Formulario from '@/Formulario.js'
|
||||
import Formulario from '@/index.ts'
|
||||
|
||||
describe('Formulario', () => {
|
||||
it('can merge simple object', () => {
|
||||
let a = {
|
||||
optionA: true,
|
||||
optionB: '1234'
|
||||
}
|
||||
let b = {
|
||||
optionA: false
|
||||
}
|
||||
expect(Formulario.merge(a, b)).toEqual({
|
||||
optionA: false,
|
||||
optionB: '1234'
|
||||
})
|
||||
})
|
||||
|
||||
it('can add to simple array', () => {
|
||||
let a = {
|
||||
optionA: true,
|
||||
optionB: ['first', 'second']
|
||||
}
|
||||
let b = {
|
||||
optionB: ['third']
|
||||
}
|
||||
expect(Formulario.merge(a, b, true)).toEqual({
|
||||
optionA: true,
|
||||
optionB: ['first', 'second', 'third']
|
||||
})
|
||||
})
|
||||
|
||||
it('can merge recursively', () => {
|
||||
let a = {
|
||||
optionA: true,
|
||||
optionC: {
|
||||
first: '123',
|
||||
third: {
|
||||
a: 'b'
|
||||
}
|
||||
},
|
||||
optionB: '1234'
|
||||
}
|
||||
let b = {
|
||||
optionB: '567',
|
||||
optionC: {
|
||||
first: '1234',
|
||||
second: '789',
|
||||
}
|
||||
}
|
||||
expect(Formulario.merge(a, b)).toEqual({
|
||||
optionA: true,
|
||||
optionC: {
|
||||
first: '1234',
|
||||
third: {
|
||||
a: 'b'
|
||||
},
|
||||
second: '789'
|
||||
},
|
||||
optionB: '567'
|
||||
})
|
||||
})
|
||||
|
||||
it('installs on vue instance', () => {
|
||||
const components = [
|
||||
'FormularioForm',
|
||||
@ -71,7 +12,7 @@ describe('Formulario', () => {
|
||||
Vue.component = function (name, instance) {
|
||||
registry.push(name)
|
||||
}
|
||||
Formulario.install(Vue, { extended: true })
|
||||
Formulario.install(Vue)
|
||||
expect(Vue.prototype.$formulario).toBe(Formulario)
|
||||
expect(registry).toEqual(components)
|
||||
})
|
||||
|
@ -1,10 +1,9 @@
|
||||
import Vue from 'vue'
|
||||
import { mount, shallowMount } from '@vue/test-utils'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import flushPromises from 'flush-promises'
|
||||
import Formulario from '../../src/Formulario.js'
|
||||
import FormSubmission from '../../src/FormSubmission.js'
|
||||
import FormSubmission from '@/FormSubmission.ts'
|
||||
import Formulario from '@/index.ts'
|
||||
import FormularioForm from '@/FormularioForm.vue'
|
||||
import FormularioInput from '@/FormularioInput.vue'
|
||||
|
||||
function validationMessages (instance) {
|
||||
instance.extend({
|
||||
@ -161,8 +160,6 @@ describe('FormularioForm', () => {
|
||||
expect(wrapper.find('span').text()).toBe('234')
|
||||
})
|
||||
|
||||
|
||||
|
||||
// ===========================================================================
|
||||
/**
|
||||
* @todo in vue-test-utils version 1.0.0-beta.29 has some bugs related to
|
||||
@ -266,9 +263,8 @@ describe('FormularioForm', () => {
|
||||
const wrapper = mount(FormularioForm, {
|
||||
propsData: { values: { name: 'Dave Barnett', candy: true } },
|
||||
slots: { default: `
|
||||
|
||||
<FormularioInput v-slot="inputProps" name="name" validation="required" >
|
||||
<input v-model="inputProps.context.model" type="text">
|
||||
<FormularioInput v-slot="{ context }" name="name" validation="required">
|
||||
<input v-model="context.model" type="text">
|
||||
</FormularioInput>
|
||||
` }
|
||||
})
|
||||
@ -289,12 +285,8 @@ describe('FormularioForm', () => {
|
||||
const wrapper = mount({
|
||||
template: `
|
||||
<div>
|
||||
<FormularioForm
|
||||
name="login"
|
||||
/>
|
||||
<FormularioForm
|
||||
name="register"
|
||||
/>
|
||||
<FormularioForm name="login" />
|
||||
<FormularioForm name="register" />
|
||||
</div>
|
||||
`
|
||||
})
|
||||
@ -309,12 +301,14 @@ describe('FormularioForm', () => {
|
||||
template: `
|
||||
<div>
|
||||
<FormularioForm
|
||||
class="formulario-form formulario-form--login"
|
||||
name="login"
|
||||
v-slot="vSlot"
|
||||
>
|
||||
<span v-for="error in vSlot.errors">{{ error }}</span>
|
||||
</FormularioForm>
|
||||
<FormularioForm
|
||||
class="formulario-form formulario-form--register"
|
||||
name="register"
|
||||
v-slot="vSlot"
|
||||
>
|
||||
@ -336,12 +330,14 @@ describe('FormularioForm', () => {
|
||||
template: `
|
||||
<div>
|
||||
<FormularioForm
|
||||
class="formulario-form formulario-form--login"
|
||||
name="login"
|
||||
v-slot="vSlot"
|
||||
>
|
||||
<span v-for="error in vSlot.errors">{{ error }}</span>
|
||||
</FormularioForm>
|
||||
<FormularioForm
|
||||
class="formulario-form formulario-form--register"
|
||||
name="register"
|
||||
v-slot="vSlot"
|
||||
>
|
||||
@ -387,10 +383,10 @@ describe('FormularioForm', () => {
|
||||
|
||||
it('displays field errors on inputs with errors prop', async () => {
|
||||
const wrapper = mount(FormularioForm, {
|
||||
propsData: { errors: { sipple: ['This field has an error'] }},
|
||||
propsData: { errors: { fieldWithErrors: ['This field has an error'] }},
|
||||
slots: {
|
||||
default: `
|
||||
<FormularioInput v-slot="vSlot" name="sipple">
|
||||
<FormularioInput v-slot="vSlot" name="fieldWithErrors">
|
||||
<span v-for="error in vSlot.context.allErrors">{{ error.message }}</span>
|
||||
</FormularioInput>
|
||||
`
|
||||
|
@ -1,22 +1,21 @@
|
||||
import Vue from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import flushPromises from 'flush-promises'
|
||||
import Formulario from '@/Formulario.js'
|
||||
import FormularioInput from '@/FormularioInput.vue'
|
||||
import Formulario from '@/index.ts'
|
||||
import FormularioForm from '@/FormularioForm.vue'
|
||||
import FormularioGrouping from '@/FormularioGrouping.vue'
|
||||
|
||||
Vue.use(Formulario)
|
||||
|
||||
describe('FormularioGrouping', () => {
|
||||
it('grouped fields to be setted', async () => {
|
||||
it('Grouped fields to be set', async () => {
|
||||
const wrapper = mount(FormularioForm, {
|
||||
propsData: { name: 'form', },
|
||||
propsData: { name: 'form' },
|
||||
slots: {
|
||||
default: `
|
||||
<FormularioGrouping name="sub">
|
||||
<FormularioInput name="text" v-slot="vSlot">
|
||||
<input type="text" v-model="vSlot.context.model">
|
||||
<FormularioGrouping name="group">
|
||||
<FormularioInput name="text" v-slot="{ context }">
|
||||
<input type="text" v-model="context.model">
|
||||
</FormularioInput>
|
||||
</FormularioGrouping>
|
||||
`
|
||||
@ -26,38 +25,40 @@ describe('FormularioGrouping', () => {
|
||||
wrapper.find('input[type="text"]').setValue('test')
|
||||
|
||||
const submission = await wrapper.vm.formSubmitted()
|
||||
expect(submission).toEqual({sub: {text: 'test'}})
|
||||
expect(submission).toEqual({ group: { text: 'test' } })
|
||||
})
|
||||
|
||||
it('grouped fields to be getted', async () => {
|
||||
it('Grouped fields to be got', async () => {
|
||||
const wrapper = mount(FormularioForm, {
|
||||
propsData: { name: 'form', formularioValue: { sub: {text: 'initial value'}, text: 'simple text' } },
|
||||
propsData: {
|
||||
name: 'form',
|
||||
formularioValue: {
|
||||
group: { text: 'Group text' },
|
||||
text: 'Text',
|
||||
}
|
||||
},
|
||||
slots: {
|
||||
default: `
|
||||
<FormularioGrouping name="sub">
|
||||
<FormularioInput name="text" v-slot="vSlot">
|
||||
<input type="text" v-model="vSlot.context.model">
|
||||
<FormularioGrouping name="group">
|
||||
<FormularioInput name="text" v-slot="{ context }">
|
||||
<input type="text" v-model="context.model">
|
||||
</FormularioInput>
|
||||
</FormularioGrouping>
|
||||
`
|
||||
}
|
||||
})
|
||||
expect(wrapper.find('input[type="text"]').element.value).toBe('initial value')
|
||||
expect(wrapper.find('input[type="text"]').element.value).toBe('Group text')
|
||||
})
|
||||
|
||||
it('data reactive with grouped fields', async () => {
|
||||
it('Data reactive with grouped fields', async () => {
|
||||
const wrapper = mount({
|
||||
data () {
|
||||
return {
|
||||
formValues: {}
|
||||
}
|
||||
},
|
||||
data: () => ({ values: {} }),
|
||||
template: `
|
||||
<FormularioForm name="form" v-model="formValues">
|
||||
<FormularioGrouping name="sub">
|
||||
<FormularioInput name="text" v-slot="vSlot">
|
||||
<input type="text" v-model="vSlot.context.model">
|
||||
<span>{{ formValues.sub.text }}</span>
|
||||
<FormularioForm name="form" v-model="values">
|
||||
<FormularioGrouping name="group">
|
||||
<FormularioInput name="text" v-slot="{ context }">
|
||||
<input type="text" v-model="context.model">
|
||||
<span>{{ values.group.text }}</span>
|
||||
</FormularioInput>
|
||||
</FormularioGrouping>
|
||||
</FormularioForm>
|
||||
@ -69,18 +70,20 @@ describe('FormularioGrouping', () => {
|
||||
expect(wrapper.find('span').text()).toBe('test')
|
||||
})
|
||||
|
||||
it('errors are setted for grouped fields', async () => {
|
||||
it('Errors are set for grouped fields', async () => {
|
||||
const wrapper = mount({
|
||||
data () {
|
||||
return {
|
||||
formValues: {}
|
||||
}
|
||||
},
|
||||
data: () => ({ values: {} }),
|
||||
template: `
|
||||
<FormularioForm name="form" v-model="formValues" :errors="{'sub.text': 'Test error'}">
|
||||
<FormularioGrouping name="sub">
|
||||
<FormularioInput name="text" v-slot="vSlot">
|
||||
<span v-for="error in vSlot.context.allErrors">{{ error }}</span>
|
||||
<FormularioForm
|
||||
v-model="values"
|
||||
:errors="{'group.text': 'Test error'}"
|
||||
name="form"
|
||||
>
|
||||
<FormularioGrouping name="group">
|
||||
<FormularioInput name="text" v-slot="{ context }">
|
||||
<span v-for="error in context.allErrors">
|
||||
{{ error }}
|
||||
</span>
|
||||
</FormularioInput>
|
||||
</FormularioGrouping>
|
||||
</FormularioForm>
|
||||
|
@ -1,12 +1,12 @@
|
||||
import Vue from 'vue'
|
||||
import VueI18n from 'vue-i18n'
|
||||
import flushPromises from 'flush-promises'
|
||||
import { mount, createLocalVue } from '@vue/test-utils'
|
||||
import Formulario from '@/Formulario.js'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
import Formulario from '@/index.ts'
|
||||
import FormularioForm from '@/FormularioForm.vue'
|
||||
import FormularioInput from '@/FormularioInput.vue'
|
||||
|
||||
const globalRule = jest.fn((context) => { return false })
|
||||
const globalRule = jest.fn(() => { return false })
|
||||
|
||||
function validationMessages (instance) {
|
||||
instance.extend({
|
||||
@ -21,9 +21,7 @@ function validationMessages (instance) {
|
||||
|
||||
Vue.use(Formulario, {
|
||||
plugins: [validationMessages],
|
||||
rules: {
|
||||
globalRule
|
||||
}
|
||||
rules: { globalRule }
|
||||
})
|
||||
|
||||
describe('FormularioInput', () => {
|
||||
@ -38,7 +36,7 @@ describe('FormularioInput', () => {
|
||||
},
|
||||
scopedSlots: {
|
||||
default: `<div><span v-for="error in props.context.allErrors">{{ error.message }}</span></div>`
|
||||
}
|
||||
},
|
||||
})
|
||||
await flushPromises()
|
||||
expect(wrapper.find('span').text()).toBe('the value was different than expected')
|
||||
@ -77,6 +75,22 @@ describe('FormularioInput', () => {
|
||||
expect(wrapper.find('span').text()).toBe('The string other value is not correct.')
|
||||
})
|
||||
|
||||
it('no validation on created when errorBehavior is not live', async () => {
|
||||
const wrapper = mount(FormularioInput, {
|
||||
propsData: {
|
||||
name: 'test',
|
||||
validation: 'required|in:abcdef',
|
||||
validationMessages: {in: 'the value was different than expected'},
|
||||
value: 'other value'
|
||||
},
|
||||
scopedSlots: {
|
||||
default: `<div><span v-for="error in props.context.allErrors">{{ error.message }}</span></div>`
|
||||
}
|
||||
})
|
||||
await flushPromises()
|
||||
expect(wrapper.find('span').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('uses custom async validation rules on defined on the field', async () => {
|
||||
const wrapper = mount(FormularioInput, {
|
||||
propsData: {
|
||||
@ -135,13 +149,14 @@ describe('FormularioInput', () => {
|
||||
})
|
||||
|
||||
it('emits correct validation event', async () => {
|
||||
const wrapper = mount(FormularioInput, { propsData: {
|
||||
name: 'test',
|
||||
validation: 'required',
|
||||
errorBehavior: 'live',
|
||||
value: '',
|
||||
name: 'testinput',
|
||||
} })
|
||||
const wrapper = mount(FormularioInput, {
|
||||
propsData: {
|
||||
validation: 'required',
|
||||
errorBehavior: 'live',
|
||||
value: '',
|
||||
name: 'testinput',
|
||||
}
|
||||
})
|
||||
await flushPromises()
|
||||
const errorObject = wrapper.emitted('validation')[0][0]
|
||||
expect(errorObject).toEqual({
|
||||
@ -160,7 +175,6 @@ describe('FormularioInput', () => {
|
||||
it('emits a error-visibility event on blur', async () => {
|
||||
const wrapper = mount(FormularioInput, {
|
||||
propsData: {
|
||||
name: 'test',
|
||||
validation: 'required',
|
||||
errorBehavior: 'blur',
|
||||
value: '',
|
||||
@ -178,13 +192,14 @@ describe('FormularioInput', () => {
|
||||
})
|
||||
|
||||
it('emits error-visibility event immediately when live', async () => {
|
||||
const wrapper = mount(FormularioInput, { propsData: {
|
||||
name: 'test',
|
||||
validation: 'required',
|
||||
errorBehavior: 'live',
|
||||
value: '',
|
||||
name: 'testinput',
|
||||
} })
|
||||
const wrapper = mount(FormularioInput, {
|
||||
propsData: {
|
||||
validation: 'required',
|
||||
errorBehavior: 'live',
|
||||
value: '',
|
||||
name: 'testinput',
|
||||
}
|
||||
})
|
||||
await flushPromises()
|
||||
expect(wrapper.emitted('error-visibility').length).toBe(1)
|
||||
})
|
||||
@ -192,7 +207,6 @@ describe('FormularioInput', () => {
|
||||
it('Does not emit an error-visibility event if visibility did not change', async () => {
|
||||
const wrapper = mount(FormularioInput, {
|
||||
propsData: {
|
||||
name: 'test',
|
||||
validation: 'in:xyz',
|
||||
errorBehavior: 'live',
|
||||
value: 'bar',
|
||||
@ -275,7 +289,7 @@ describe('FormularioInput', () => {
|
||||
|
||||
it('displays errors when error-behavior is submit and form is submitted', async () => {
|
||||
const wrapper = mount(FormularioForm, {
|
||||
propsData: {name: 'test'},
|
||||
propsData: { name: 'test' },
|
||||
slots: {
|
||||
default: `
|
||||
<FormularioInput v-slot="inputProps" validation="required" name="testinput" error-behavior="submit">
|
||||
@ -284,12 +298,13 @@ describe('FormularioInput', () => {
|
||||
`
|
||||
}
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
expect(wrapper.find('span').exists()).toBe(false)
|
||||
|
||||
wrapper.trigger('submit')
|
||||
await flushPromises()
|
||||
|
||||
await flushPromises()
|
||||
expect(wrapper.find('span').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
56
test/unit/merge.test.js
Normal file
56
test/unit/merge.test.js
Normal file
@ -0,0 +1,56 @@
|
||||
import merge from '@/utils/merge.ts'
|
||||
|
||||
describe('merge', () => {
|
||||
it('Can merge simple object', () => {
|
||||
expect(merge({
|
||||
optionA: true,
|
||||
optionB: '1234',
|
||||
}, {
|
||||
optionA: false,
|
||||
})).toEqual({
|
||||
optionA: false,
|
||||
optionB: '1234',
|
||||
})
|
||||
})
|
||||
|
||||
it('Can add to simple array', () => {
|
||||
expect(merge({
|
||||
optionA: true,
|
||||
optionB: ['first', 'second']
|
||||
}, {
|
||||
optionB: ['third']
|
||||
}, true)).toEqual({
|
||||
optionA: true,
|
||||
optionB: ['first', 'second', 'third']
|
||||
})
|
||||
})
|
||||
|
||||
it('Can merge recursively', () => {
|
||||
expect(merge({
|
||||
optionA: true,
|
||||
optionC: {
|
||||
first: '123',
|
||||
third: {
|
||||
a: 'b',
|
||||
},
|
||||
},
|
||||
optionB: '1234',
|
||||
}, {
|
||||
optionB: '567',
|
||||
optionC: {
|
||||
first: '1234',
|
||||
second: '789',
|
||||
}
|
||||
})).toEqual({
|
||||
optionA: true,
|
||||
optionC: {
|
||||
first: '1234',
|
||||
third: {
|
||||
a: 'b',
|
||||
},
|
||||
second: '789',
|
||||
},
|
||||
optionB: '567',
|
||||
})
|
||||
})
|
||||
})
|
@ -1,4 +1,4 @@
|
||||
import rules from '@/libs/rules'
|
||||
import rules from '@/validation/rules.ts'
|
||||
import FileUpload from '../../src/FileUpload'
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { parseRules, parseLocale, regexForFormat, cloneDeep, isValueType, snakeToCamel, groupBails } from '@/libs/utils'
|
||||
import rules from '@/libs/rules'
|
||||
import FileUpload from '@/FileUpload';
|
||||
import { parseRules, parseLocale, regexForFormat, cloneDeep, isScalar, snakeToCamel, groupBails } from '@/libs/utils'
|
||||
import rules from '@/validation/rules.ts'
|
||||
import FileUpload from '@/FileUpload'
|
||||
|
||||
describe('parseRules', () => {
|
||||
it('parses single string rules, returning empty arguments array', () => {
|
||||
@ -99,22 +99,22 @@ describe('regexForFormat', () => {
|
||||
it('fails date like YYYY-MM-DD with out of bounds day', () => expect(regexForFormat('YYYY-MM-DD').test('1987-01-32')).toBe(false))
|
||||
})
|
||||
|
||||
describe('isValueType', () => {
|
||||
it('passes on strings', () => expect(isValueType('hello')).toBe(true))
|
||||
describe('isScalar', () => {
|
||||
it('passes on strings', () => expect(isScalar('hello')).toBe(true))
|
||||
|
||||
it('passes on numbers', () => expect(isValueType(123)).toBe(true))
|
||||
it('passes on numbers', () => expect(isScalar(123)).toBe(true))
|
||||
|
||||
it('passes on booleans', () => expect(isValueType(false)).toBe(true))
|
||||
it('passes on booleans', () => expect(isScalar(false)).toBe(true))
|
||||
|
||||
it('passes on symbols', () => expect(isValueType(Symbol(123))).toBe(true))
|
||||
it('passes on symbols', () => expect(isScalar(Symbol(123))).toBe(true))
|
||||
|
||||
it('passes on null', () => expect(isValueType(null)).toBe(true))
|
||||
it('passes on null', () => expect(isScalar(null)).toBe(true))
|
||||
|
||||
it('passes on undefined', () => expect(isValueType(undefined)).toBe(true))
|
||||
it('passes on undefined', () => expect(isScalar(undefined)).toBe(true))
|
||||
|
||||
it('fails on pojo', () => expect(isValueType({})).toBe(false))
|
||||
it('fails on pojo', () => expect(isScalar({})).toBe(false))
|
||||
|
||||
it('fails on custom type', () => expect(isValueType(FileUpload)).toBe(false))
|
||||
it('fails on custom type', () => expect(isScalar(FileUpload)).toBe(false))
|
||||
})
|
||||
|
||||
describe('cloneDeep', () => {
|
||||
|
41
tsconfig.json
Normal file
41
tsconfig.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2015",
|
||||
"module": "es2015",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"types": [
|
||||
"webpack-env"
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.js",
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"tests/**/*.ts",
|
||||
"tests/**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user