Bumps dependencies
This commit is contained in:
commit
75e59c6269
@ -15,7 +15,8 @@ export default {
|
|||||||
'nanoid/non-secure': 'nanoid',
|
'nanoid/non-secure': 'nanoid',
|
||||||
'is-url': 'isUrl',
|
'is-url': 'isUrl',
|
||||||
'@braid/vue-formulate-i18n': 'VueFormulateI18n'
|
'@braid/vue-formulate-i18n': 'VueFormulateI18n'
|
||||||
}
|
},
|
||||||
|
sourcemap: false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
external: ['nanoid/non-secure'],
|
external: ['nanoid/non-secure'],
|
||||||
|
2
dist/formulate.esm.js
vendored
2
dist/formulate.esm.js
vendored
File diff suppressed because one or more lines are too long
6
dist/formulate.min.js
vendored
6
dist/formulate.min.js
vendored
File diff suppressed because one or more lines are too long
2
dist/formulate.umd.js
vendored
2
dist/formulate.umd.js
vendored
File diff suppressed because one or more lines are too long
79
dist/snow.css
vendored
79
dist/snow.css
vendored
@ -7,6 +7,9 @@
|
|||||||
font-size: .9em;
|
font-size: .9em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: .1em; }
|
margin-bottom: .1em; }
|
||||||
|
.formulate-input .formulate-input-label--before + .formulate-input-help--before {
|
||||||
|
margin-top: -.25em;
|
||||||
|
margin-bottom: .75em; }
|
||||||
.formulate-input .formulate-input-element {
|
.formulate-input .formulate-input-element {
|
||||||
max-width: 20em;
|
max-width: 20em;
|
||||||
margin-bottom: .1em; }
|
margin-bottom: .1em; }
|
||||||
@ -27,8 +30,6 @@
|
|||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
margin-bottom: .25em; }
|
margin-bottom: .25em; }
|
||||||
.formulate-input .formulate-input-group-item {
|
|
||||||
margin-bottom: .5em; }
|
|
||||||
.formulate-input:last-child {
|
.formulate-input:last-child {
|
||||||
margin-bottom: 0; }
|
margin-bottom: 0; }
|
||||||
.formulate-input[data-classification='text'] input {
|
.formulate-input[data-classification='text'] input {
|
||||||
@ -120,6 +121,7 @@
|
|||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
border-radius: 1em;
|
border-radius: 1em;
|
||||||
|
border: 0;
|
||||||
background-color: #41b883;
|
background-color: #41b883;
|
||||||
margin-top: calc(-.5em + 2px); }
|
margin-top: calc(-.5em + 2px); }
|
||||||
.formulate-input[data-classification='slider'] input::-moz-range-thumb {
|
.formulate-input[data-classification='slider'] input::-moz-range-thumb {
|
||||||
@ -128,6 +130,7 @@
|
|||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
border-radius: 1em;
|
border-radius: 1em;
|
||||||
|
border: 0;
|
||||||
background-color: #41b883;
|
background-color: #41b883;
|
||||||
margin-top: calc(-.5em + 2px); }
|
margin-top: calc(-.5em + 2px); }
|
||||||
.formulate-input[data-classification='slider'] input::-ms-thumb {
|
.formulate-input[data-classification='slider'] input::-ms-thumb {
|
||||||
@ -136,6 +139,7 @@
|
|||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
border-radius: 1em;
|
border-radius: 1em;
|
||||||
|
border: 0;
|
||||||
background-color: #41b883;
|
background-color: #41b883;
|
||||||
margin-top: calc(-.5em + 2px); }
|
margin-top: calc(-.5em + 2px); }
|
||||||
.formulate-input[data-classification='slider'] input::-webkit-slider-runnable-track {
|
.formulate-input[data-classification='slider'] input::-webkit-slider-runnable-track {
|
||||||
@ -146,6 +150,14 @@
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0; }
|
padding: 0; }
|
||||||
|
.formulate-input[data-classification='slider'] input::-moz-range-track {
|
||||||
|
appearance: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
background-color: #efefef;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0; }
|
||||||
.formulate-input[data-classification='textarea'] textarea {
|
.formulate-input[data-classification='textarea'] textarea {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
border-radius: .3em;
|
border-radius: .3em;
|
||||||
@ -198,6 +210,19 @@
|
|||||||
.formulate-input[data-classification='button'] button[disabled] {
|
.formulate-input[data-classification='button'] button[disabled] {
|
||||||
background-color: #cecece;
|
background-color: #cecece;
|
||||||
border-color: #cecece; }
|
border-color: #cecece; }
|
||||||
|
.formulate-input[data-classification='button'] button[data-ghost] {
|
||||||
|
color: #41b883;
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: currentColor; }
|
||||||
|
.formulate-input[data-classification='button'] button[data-minor] {
|
||||||
|
font-size: .75em;
|
||||||
|
display: inline-block; }
|
||||||
|
.formulate-input[data-classification='button'] button[data-danger] {
|
||||||
|
background-color: #960505;
|
||||||
|
border-color: #960505; }
|
||||||
|
.formulate-input[data-classification='button'] button[data-danger][data-ghost] {
|
||||||
|
color: #960505;
|
||||||
|
background-color: transparent; }
|
||||||
.formulate-input[data-classification='button'] button:active {
|
.formulate-input[data-classification='button'] button:active {
|
||||||
background-color: #64c89b;
|
background-color: #64c89b;
|
||||||
border-color: #64c89b; }
|
border-color: #64c89b; }
|
||||||
@ -288,8 +313,56 @@
|
|||||||
margin-left: .5em; }
|
margin-left: .5em; }
|
||||||
.formulate-input[data-classification='box'] .formulate-input-label--before {
|
.formulate-input[data-classification='box'] .formulate-input-label--before {
|
||||||
margin-right: .5em; }
|
margin-right: .5em; }
|
||||||
.formulate-input[data-classification="group"] > .formulate-input-wrapper > .formulate-input-label {
|
.formulate-input[data-classification='group'] .formulate-input-group-item {
|
||||||
margin-bottom: .5em; }
|
margin-bottom: .5em; }
|
||||||
|
.formulate-input[data-classification='group'] > .formulate-input-wrapper > .formulate-input-label {
|
||||||
|
margin-bottom: .5em; }
|
||||||
|
.formulate-input[data-classification='group'] [data-is-repeatable] {
|
||||||
|
padding: 1em;
|
||||||
|
border: 1px solid #efefef;
|
||||||
|
border-radius: .3em; }
|
||||||
|
.formulate-input[data-classification='group'] [data-is-repeatable] .formulate-input-grouping {
|
||||||
|
margin: -1em -1em 0 -1em; }
|
||||||
|
.formulate-input[data-classification='group'] [data-is-repeatable] .formulate-input-group-repeatable {
|
||||||
|
padding: 1em 3em 1em 1em;
|
||||||
|
border-bottom: 1px solid #efefef;
|
||||||
|
position: relative; }
|
||||||
|
.formulate-input[data-classification='group'] [data-is-repeatable] .formulate-input-group-repeatable-remove {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
top: calc(50% - .65em + .5em);
|
||||||
|
width: 1.3em;
|
||||||
|
height: 1.3em;
|
||||||
|
background-color: #cecece;
|
||||||
|
right: .85em;
|
||||||
|
border-radius: 1.3em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color .2s; }
|
||||||
|
.formulate-input[data-classification='group'] [data-is-repeatable] .formulate-input-group-repeatable-remove::before, .formulate-input[data-classification='group'] [data-is-repeatable] .formulate-input-group-repeatable-remove::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: calc(50% - .1em);
|
||||||
|
left: .325em;
|
||||||
|
display: block;
|
||||||
|
width: .65em;
|
||||||
|
height: .2em;
|
||||||
|
background-color: white;
|
||||||
|
transform-origin: center center;
|
||||||
|
transition: transform .25s; }
|
||||||
|
@media (pointer: fine) {
|
||||||
|
.formulate-input[data-classification='group'] [data-is-repeatable] .formulate-input-group-repeatable-remove:hover {
|
||||||
|
background-color: #dc2c2c; }
|
||||||
|
.formulate-input[data-classification='group'] [data-is-repeatable] .formulate-input-group-repeatable-remove:hover::after, .formulate-input[data-classification='group'] [data-is-repeatable] .formulate-input-group-repeatable-remove:hover::before {
|
||||||
|
height: .2em;
|
||||||
|
width: .75em;
|
||||||
|
left: .25em;
|
||||||
|
top: calc(50% - .075em); }
|
||||||
|
.formulate-input[data-classification='group'] [data-is-repeatable] .formulate-input-group-repeatable-remove:hover::after {
|
||||||
|
transform: rotate(45deg); }
|
||||||
|
.formulate-input[data-classification='group'] [data-is-repeatable] .formulate-input-group-repeatable-remove:hover::before {
|
||||||
|
transform: rotate(-45deg); } }
|
||||||
|
.formulate-input[data-classification='group'] [data-is-repeatable] .formulate-input-group-repeatable:last-child {
|
||||||
|
margin-bottom: 1em; }
|
||||||
.formulate-input[data-classification="file"] .formulate-input-upload-area {
|
.formulate-input[data-classification="file"] .formulate-input-upload-area {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
2
dist/snow.min.css
vendored
2
dist/snow.min.css
vendored
File diff suppressed because one or more lines are too long
@ -1,16 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
|
<SpecimenGroup />
|
||||||
<SpecimenText />
|
<SpecimenText />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import SpecimenText from './specimens/SpecimenText'
|
import SpecimenText from './specimens/SpecimenText'
|
||||||
|
import SpecimenGroup from './specimens/SpecimenGroup'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'App',
|
||||||
components: {
|
components: {
|
||||||
SpecimenText
|
SpecimenText,
|
||||||
|
SpecimenGroup
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@ -24,4 +27,13 @@ export default {
|
|||||||
body {
|
body {
|
||||||
font-family: $formulate-font-stack;
|
font-family: $formulate-font-stack;
|
||||||
}
|
}
|
||||||
|
.specimens {
|
||||||
|
margin-bottom: 2em;
|
||||||
|
padding-bottom: 2em;
|
||||||
|
border-bottom: 1px solid gray;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
82
examples/specimens/SpecimenGroup.vue
Normal file
82
examples/specimens/SpecimenGroup.vue
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<div class="specimens specimens--group">
|
||||||
|
<h2>Non-repeatable group</h2>
|
||||||
|
<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>
|
||||||
|
<h2>Repeatable group</h2>
|
||||||
|
<FormulateForm
|
||||||
|
v-model="formData"
|
||||||
|
@submit="save"
|
||||||
|
>
|
||||||
|
<FormulateInput
|
||||||
|
v-model="users"
|
||||||
|
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
|
||||||
|
v-model="email"
|
||||||
|
name="email"
|
||||||
|
label="Email address"
|
||||||
|
type="email"
|
||||||
|
placeholder="User’s email"
|
||||||
|
validation="required|email"
|
||||||
|
/>
|
||||||
|
</FormulateInput>
|
||||||
|
<FormulateInput
|
||||||
|
type="submit"
|
||||||
|
/>
|
||||||
|
</FormulateForm>
|
||||||
|
<!-- <span>Form Values</span>
|
||||||
|
<pre>{{ formData }}</pre>
|
||||||
|
<span>Save Values</span> -->
|
||||||
|
<pre>{{ saveValues }}</pre>
|
||||||
|
<!-- <pre>{{ email }}</pre>
|
||||||
|
<pre>{{ users }}</pre> -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
formData: {
|
||||||
|
},
|
||||||
|
users: [
|
||||||
|
{ name: 'Justin' },
|
||||||
|
{}
|
||||||
|
],
|
||||||
|
email: 'justin@wearebraid.com',
|
||||||
|
saveValues: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
save (values) {
|
||||||
|
this.saveValues = values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -6,15 +6,11 @@
|
|||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
help="Select a username"
|
help="Select a username"
|
||||||
/>
|
/>
|
||||||
|
<FormulateInput
|
||||||
|
label="How old are you?"
|
||||||
|
type="number"
|
||||||
|
placeholder="25"
|
||||||
|
help="Select your age"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
892
package-lock.json
generated
892
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
51
package.json
51
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@braid/vue-formulate",
|
"name": "@braid/vue-formulate",
|
||||||
"version": "2.2.6",
|
"version": "2.3.0",
|
||||||
"description": "The easiest way to build forms in Vue.",
|
"description": "The easiest way to build forms in Vue.",
|
||||||
"main": "dist/formulate.umd.js",
|
"main": "dist/formulate.umd.js",
|
||||||
"module": "dist/formulate.esm.js",
|
"module": "dist/formulate.esm.js",
|
||||||
@ -8,9 +8,6 @@
|
|||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
"engines": {
|
|
||||||
"node": ">=11"
|
|
||||||
},
|
|
||||||
"browser": {
|
"browser": {
|
||||||
"./sfc": "src/Formulate.js"
|
"./sfc": "src/Formulate.js"
|
||||||
},
|
},
|
||||||
@ -45,23 +42,22 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://www.vueformulate.com",
|
"homepage": "https://www.vueformulate.com",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.8.4",
|
"@babel/core": "^7.9.6",
|
||||||
"@babel/plugin-transform-modules-commonjs": "^7.8.3",
|
"@babel/plugin-transform-modules-commonjs": "^7.9.6",
|
||||||
"@babel/preset-env": "^7.8.4",
|
"@babel/preset-env": "^7.9.6",
|
||||||
"@rollup/plugin-buble": "^0.21.1",
|
"@rollup/plugin-buble": "^0.21.3",
|
||||||
"@rollup/plugin-commonjs": "^11.0.2",
|
"@rollup/plugin-commonjs": "^11.1.0",
|
||||||
"@rollup/plugin-node-resolve": "^7.1.1",
|
"@rollup/plugin-node-resolve": "^7.1.3",
|
||||||
"@vue/cli-plugin-babel": "^4.2.3",
|
"@vue/cli-plugin-babel": "^4.3.1",
|
||||||
"@vue/cli-plugin-eslint": "^4.2.3",
|
"@vue/cli-plugin-eslint": "^4.3.1",
|
||||||
"@vue/cli-service": "^4.2.3",
|
"@vue/cli-service": "^4.3.1",
|
||||||
"@vue/component-compiler-utils": "^3.1.1",
|
"@vue/component-compiler-utils": "^3.1.2",
|
||||||
"@vue/test-utils": "^1.0.0-beta.31",
|
"@vue/test-utils": "^1.0.2",
|
||||||
"autoprefixer": "^9.7.4",
|
"autoprefixer": "^9.7.6",
|
||||||
"babel-core": "^7.0.0-bridge.0",
|
"babel-core": "^7.0.0-bridge.0",
|
||||||
"babel-eslint": "^10.0.1",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-jest": "^25.1.0",
|
"babel-jest": "^25.5.1",
|
||||||
"cssnano": "^4.1.10",
|
"cssnano": "^4.1.10",
|
||||||
"cypress": "^4.1.0",
|
|
||||||
"eslint": "^5.16.0",
|
"eslint": "^5.16.0",
|
||||||
"eslint-config-standard": "^12.0.0",
|
"eslint-config-standard": "^12.0.0",
|
||||||
"eslint-plugin-import": "^2.20.1",
|
"eslint-plugin-import": "^2.20.1",
|
||||||
@ -70,18 +66,19 @@
|
|||||||
"eslint-plugin-standard": "^4.0.0",
|
"eslint-plugin-standard": "^4.0.0",
|
||||||
"eslint-plugin-vue": "^5.2.3",
|
"eslint-plugin-vue": "^5.2.3",
|
||||||
"flush-promises": "^1.0.2",
|
"flush-promises": "^1.0.2",
|
||||||
"jest": "^25.1.0",
|
"jest": "^25.5.4",
|
||||||
"jest-vue-preprocessor": "^1.7.1",
|
"jest-vue-preprocessor": "^1.7.1",
|
||||||
"node-sass": "^4.13.1",
|
"node-sass": "^4.14.1",
|
||||||
"postcss": "^7.0.27",
|
"postcss": "^7.0.30",
|
||||||
"postcss-cli": "^7.1.0",
|
"postcss-cli": "^7.1.1",
|
||||||
"rollup": "^1.31.1",
|
"rollup": "^1.32.1",
|
||||||
"rollup-plugin-auto-external": "^2.0.0",
|
"rollup-plugin-auto-external": "^2.0.0",
|
||||||
"rollup-plugin-internal": "^1.0.4",
|
"rollup-plugin-internal": "^1.0.4",
|
||||||
"rollup-plugin-multi-input": "^1.1.1",
|
"rollup-plugin-multi-input": "^1.1.1",
|
||||||
"rollup-plugin-terser": "^5.2.0",
|
"rollup-plugin-terser": "^5.3.0",
|
||||||
"rollup-plugin-vue": "^5.1.6",
|
"rollup-plugin-vue": "^5.1.7",
|
||||||
"sass-loader": "^8.0.2",
|
"sass-loader": "^8.0.2",
|
||||||
|
"typescript": "^3.9.2",
|
||||||
"vue": "^2.6.11",
|
"vue": "^2.6.11",
|
||||||
"vue-jest": "^3.0.5",
|
"vue-jest": "^3.0.5",
|
||||||
"vue-runtime-helpers": "^1.1.2",
|
"vue-runtime-helpers": "^1.1.2",
|
||||||
@ -90,7 +87,7 @@
|
|||||||
"watch": "^1.0.2"
|
"watch": "^1.0.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braid/vue-formulate-i18n": "^1.4.0",
|
"@braid/vue-formulate-i18n": "^1.6.1",
|
||||||
"is-plain-object": "^3.0.0",
|
"is-plain-object": "^3.0.0",
|
||||||
"is-url": "^1.2.4",
|
"is-url": "^1.2.4",
|
||||||
"nanoid": "^2.1.11"
|
"nanoid": "^2.1.11"
|
||||||
|
@ -26,11 +26,11 @@ export default class FormSubmission {
|
|||||||
values () {
|
values () {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const pending = []
|
const pending = []
|
||||||
const values = cloneDeep(this.form.internalFormModelProxy)
|
const values = cloneDeep(this.form.proxy)
|
||||||
for (const key in values) {
|
for (const key in values) {
|
||||||
if (typeof this.form.internalFormModelProxy[key] === 'object' && this.form.internalFormModelProxy[key] instanceof FileUpload) {
|
if (typeof this.form.proxy[key] === 'object' && this.form.proxy[key] instanceof FileUpload) {
|
||||||
pending.push(
|
pending.push(
|
||||||
this.form.internalFormModelProxy[key].upload().then(data => Object.assign(values, { [key]: data }))
|
this.form.proxy[key].upload().then(data => Object.assign(values, { [key]: data }))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
100
src/Formulate.js
100
src/Formulate.js
@ -2,21 +2,28 @@ import library from './libs/library'
|
|||||||
import rules from './libs/rules'
|
import rules from './libs/rules'
|
||||||
import mimes from './libs/mimes'
|
import mimes from './libs/mimes'
|
||||||
import FileUpload from './FileUpload'
|
import FileUpload from './FileUpload'
|
||||||
import { arrayify, parseLocale } from './libs/utils'
|
import { arrayify, parseLocale, has } from './libs/utils'
|
||||||
import isPlainObject from 'is-plain-object'
|
import isPlainObject from 'is-plain-object'
|
||||||
import { en } from '@braid/vue-formulate-i18n'
|
import { en } from '@braid/vue-formulate-i18n'
|
||||||
import fauxUploader from './libs/faux-uploader'
|
import fauxUploader from './libs/faux-uploader'
|
||||||
import FormulateInput from './FormulateInput.vue'
|
import FormulateSlot from './FormulateSlot'
|
||||||
import FormulateForm from './FormulateForm.vue'
|
import FormulateForm from './FormulateForm.vue'
|
||||||
|
import FormulateInput from './FormulateInput.vue'
|
||||||
import FormulateErrors from './FormulateErrors.vue'
|
import FormulateErrors from './FormulateErrors.vue'
|
||||||
import FormulateInputGroup from './FormulateInputGroup.vue'
|
import FormulateHelp from './slots/FormulateHelp.vue'
|
||||||
|
import FormulateGrouping from './FormulateGrouping.vue'
|
||||||
|
import FormulateLabel from './slots/FormulateLabel.vue'
|
||||||
|
import FormulateAddMore from './slots/FormulateAddMore.vue'
|
||||||
import FormulateInputBox from './inputs/FormulateInputBox.vue'
|
import FormulateInputBox from './inputs/FormulateInputBox.vue'
|
||||||
import FormulateInputText from './inputs/FormulateInputText.vue'
|
import FormulateInputText from './inputs/FormulateInputText.vue'
|
||||||
import FormulateInputFile from './inputs/FormulateInputFile.vue'
|
import FormulateInputFile from './inputs/FormulateInputFile.vue'
|
||||||
|
import FormulateRepeatable from './slots/FormulateRepeatable.vue'
|
||||||
|
import FormulateInputGroup from './inputs/FormulateInputGroup.vue'
|
||||||
import FormulateInputButton from './inputs/FormulateInputButton.vue'
|
import FormulateInputButton from './inputs/FormulateInputButton.vue'
|
||||||
import FormulateInputSelect from './inputs/FormulateInputSelect.vue'
|
import FormulateInputSelect from './inputs/FormulateInputSelect.vue'
|
||||||
import FormulateInputSlider from './inputs/FormulateInputSlider.vue'
|
import FormulateInputSlider from './inputs/FormulateInputSlider.vue'
|
||||||
import FormulateInputTextArea from './inputs/FormulateInputTextArea.vue'
|
import FormulateInputTextArea from './inputs/FormulateInputTextArea.vue'
|
||||||
|
import FormulateRepeatableProvider from './FormulateRepeatableProvider.vue'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base formulate library.
|
* The base formulate library.
|
||||||
@ -29,17 +36,31 @@ class Formulate {
|
|||||||
this.options = {}
|
this.options = {}
|
||||||
this.defaults = {
|
this.defaults = {
|
||||||
components: {
|
components: {
|
||||||
|
FormulateSlot,
|
||||||
FormulateForm,
|
FormulateForm,
|
||||||
|
FormulateHelp,
|
||||||
|
FormulateLabel,
|
||||||
FormulateInput,
|
FormulateInput,
|
||||||
FormulateErrors,
|
FormulateErrors,
|
||||||
|
FormulateAddMore,
|
||||||
|
FormulateGrouping,
|
||||||
FormulateInputBox,
|
FormulateInputBox,
|
||||||
FormulateInputText,
|
FormulateInputText,
|
||||||
FormulateInputFile,
|
FormulateInputFile,
|
||||||
|
FormulateRepeatable,
|
||||||
FormulateInputGroup,
|
FormulateInputGroup,
|
||||||
FormulateInputButton,
|
FormulateInputButton,
|
||||||
FormulateInputSelect,
|
FormulateInputSelect,
|
||||||
FormulateInputSlider,
|
FormulateInputSlider,
|
||||||
FormulateInputTextArea
|
FormulateInputTextArea,
|
||||||
|
FormulateRepeatableProvider
|
||||||
|
},
|
||||||
|
slotComponents: {
|
||||||
|
label: 'FormulateLabel',
|
||||||
|
help: 'FormulateHelp',
|
||||||
|
errors: 'FormulateErrors',
|
||||||
|
repeatable: 'FormulateRepeatable',
|
||||||
|
addMore: 'FormulateAddMore'
|
||||||
},
|
},
|
||||||
library,
|
library,
|
||||||
rules,
|
rules,
|
||||||
@ -51,9 +72,11 @@ class Formulate {
|
|||||||
uploadJustCompleteDuration: 1000,
|
uploadJustCompleteDuration: 1000,
|
||||||
errorHandler: (err) => err,
|
errorHandler: (err) => err,
|
||||||
plugins: [ en ],
|
plugins: [ en ],
|
||||||
locales: {}
|
locales: {},
|
||||||
|
idPrefix: 'formulate-'
|
||||||
}
|
}
|
||||||
this.registry = new Map()
|
this.registry = new Map()
|
||||||
|
this.idRegistry = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -73,6 +96,21 @@ class Formulate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
* Given a set of options, apply them to the pre-existing options.
|
||||||
* @param {Object} extendWith
|
* @param {Object} extendWith
|
||||||
@ -139,7 +177,20 @@ class Formulate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get validation rules.
|
* What component should be rendered for the given slot location and type.
|
||||||
|
* @param {string} type the type of component
|
||||||
|
* @param {string} slot the name of the slot
|
||||||
|
*/
|
||||||
|
slotComponent (type, slot) {
|
||||||
|
const def = this.options.library[type]
|
||||||
|
if (def && def.slotComponents && def.slotComponents[slot]) {
|
||||||
|
return def.slotComponents[slot]
|
||||||
|
}
|
||||||
|
return this.options.slotComponents[slot]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get validation rules by merging any passed in with global rules.
|
||||||
* @return {object} object of validation functions
|
* @return {object} object of validation functions
|
||||||
*/
|
*/
|
||||||
rules (rules = {}) {
|
rules (rules = {}) {
|
||||||
@ -176,7 +227,7 @@ class Formulate {
|
|||||||
}
|
}
|
||||||
if (locale) {
|
if (locale) {
|
||||||
const option = parseLocale(locale)
|
const option = parseLocale(locale)
|
||||||
.find(locale => Object.prototype.hasOwnProperty.call(this.options.locales, locale))
|
.find(locale => has(this.options.locales, locale))
|
||||||
if (option) {
|
if (option) {
|
||||||
selection = option
|
selection = option
|
||||||
}
|
}
|
||||||
@ -236,7 +287,7 @@ class Formulate {
|
|||||||
* @param {error}
|
* @param {error}
|
||||||
*/
|
*/
|
||||||
handle (err, formName, skip = false) {
|
handle (err, formName, skip = false) {
|
||||||
const e = skip ? err : this.options.errorHandler(err)
|
const e = skip ? err : this.options.errorHandler(err, formName)
|
||||||
if (formName && this.registry.has(formName)) {
|
if (formName && this.registry.has(formName)) {
|
||||||
this.registry.get(formName).applyErrors({
|
this.registry.get(formName).applyErrors({
|
||||||
formErrors: arrayify(e.formErrors),
|
formErrors: arrayify(e.formErrors),
|
||||||
@ -246,6 +297,39 @@ class Formulate {
|
|||||||
return e
|
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.
|
* Get the file uploader.
|
||||||
*/
|
*/
|
||||||
|
@ -52,6 +52,7 @@ export default {
|
|||||||
},
|
},
|
||||||
visibleErrors () {
|
visibleErrors () {
|
||||||
return Array.from(new Set(this.mergedErrors.concat(this.visibleValidationErrors)))
|
return Array.from(new Set(this.mergedErrors.concat(this.visibleValidationErrors)))
|
||||||
|
.filter(message => typeof message === 'string')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
@ -12,15 +12,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { shallowEqualObjects, arrayify } from './libs/utils'
|
import { arrayify, has } from './libs/utils'
|
||||||
|
import useRegistry, { useRegistryComputed, useRegistryMethods, useRegistryProviders } from './libs/registry'
|
||||||
import FormSubmission from './FormSubmission'
|
import FormSubmission from './FormSubmission'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
provide () {
|
provide () {
|
||||||
return {
|
return {
|
||||||
formulateFormSetter: this.setFieldValue,
|
...useRegistryProviders(this),
|
||||||
formulateFormRegister: this.register,
|
|
||||||
getFormValues: this.getFormValues,
|
|
||||||
observeErrors: this.addErrorObserver,
|
observeErrors: this.addErrorObserver,
|
||||||
removeErrorObserver: this.removeErrorObserver,
|
removeErrorObserver: this.removeErrorObserver,
|
||||||
formulateFieldValidation: this.formulateFieldValidation
|
formulateFieldValidation: this.formulateFieldValidation
|
||||||
@ -55,8 +54,7 @@ export default {
|
|||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
registry: {},
|
...useRegistry(this),
|
||||||
internalFormModelProxy: {},
|
|
||||||
formShouldShowErrors: false,
|
formShouldShowErrors: false,
|
||||||
errorObservers: [],
|
errorObservers: [],
|
||||||
namedErrors: [],
|
namedErrors: [],
|
||||||
@ -64,43 +62,12 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
/**
|
...useRegistryComputed(),
|
||||||
* @todo in 2.3.0 this will expand and be extracted to a separate module to
|
|
||||||
* support better scoped slot interoperability.
|
|
||||||
*/
|
|
||||||
formContext () {
|
formContext () {
|
||||||
return {
|
return {
|
||||||
errors: this.mergedFormErrors
|
errors: this.mergedFormErrors
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hasInitialValue () {
|
|
||||||
return (
|
|
||||||
(this.formulateValue && typeof this.formulateValue === 'object') ||
|
|
||||||
(this.values && typeof this.values === 'object')
|
|
||||||
)
|
|
||||||
},
|
|
||||||
isVmodeled () {
|
|
||||||
return !!(this.$options.propsData.hasOwnProperty('formulateValue') &&
|
|
||||||
this._events &&
|
|
||||||
Array.isArray(this._events.input) &&
|
|
||||||
this._events.input.length)
|
|
||||||
},
|
|
||||||
initialValues () {
|
|
||||||
if (
|
|
||||||
Object.prototype.hasOwnProperty.call(this.$options.propsData, 'formulateValue') &&
|
|
||||||
typeof this.formulateValue === 'object'
|
|
||||||
) {
|
|
||||||
// If there is a v-model on the form, use those values as first priority
|
|
||||||
return Object.assign({}, this.formulateValue) // @todo - use a deep clone to detach reference types
|
|
||||||
} else if (
|
|
||||||
Object.prototype.hasOwnProperty.call(this.$options.propsData, 'values') &&
|
|
||||||
typeof this.values === 'object'
|
|
||||||
) {
|
|
||||||
// If there are values, use them as secondary priority
|
|
||||||
return Object.assign({}, this.values)
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
classes () {
|
classes () {
|
||||||
const classes = { 'formulate-form': true }
|
const classes = { 'formulate-form': true }
|
||||||
if (this.name) {
|
if (this.name) {
|
||||||
@ -129,20 +96,12 @@ export default {
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
formulateValue: {
|
formulateValue: {
|
||||||
handler (newValue, oldValue) {
|
handler (values) {
|
||||||
if (this.isVmodeled &&
|
if (this.isVmodeled &&
|
||||||
newValue &&
|
values &&
|
||||||
typeof newValue === 'object'
|
typeof values === 'object'
|
||||||
) {
|
) {
|
||||||
for (const field in newValue) {
|
this.setValues(values)
|
||||||
if (this.registry.hasOwnProperty(field) &&
|
|
||||||
!shallowEqualObjects(newValue[field], this.internalFormModelProxy[field]) &&
|
|
||||||
!shallowEqualObjects(newValue[field], this.registry[field].internalModelProxy[field])
|
|
||||||
) {
|
|
||||||
this.setFieldValue(field, newValue[field])
|
|
||||||
this.registry[field].context.model = newValue[field]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
@ -169,11 +128,7 @@ export default {
|
|||||||
this.$formulate.deregister(this)
|
this.$formulate.deregister(this)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
applyInitialValues () {
|
...useRegistryMethods(),
|
||||||
if (this.hasInitialValue) {
|
|
||||||
this.internalFormModelProxy = this.initialValues
|
|
||||||
}
|
|
||||||
},
|
|
||||||
applyErrors ({ formErrors, inputErrors }) {
|
applyErrors ({ formErrors, inputErrors }) {
|
||||||
// given an object of errors, apply them to this form
|
// given an object of errors, apply them to this form
|
||||||
this.namedErrors = formErrors
|
this.namedErrors = formErrors
|
||||||
@ -184,7 +139,7 @@ export default {
|
|||||||
this.errorObservers.push(observer)
|
this.errorObservers.push(observer)
|
||||||
if (observer.type === 'form') {
|
if (observer.type === 'form') {
|
||||||
observer.callback(this.mergedFormErrors)
|
observer.callback(this.mergedFormErrors)
|
||||||
} else if (Object.prototype.hasOwnProperty.call(this.mergedFieldErrors, observer.field)) {
|
} else if (has(this.mergedFieldErrors, observer.field)) {
|
||||||
observer.callback(this.mergedFieldErrors[observer.field])
|
observer.callback(this.mergedFieldErrors[observer.field])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -192,39 +147,6 @@ export default {
|
|||||||
removeErrorObserver (observer) {
|
removeErrorObserver (observer) {
|
||||||
this.errorObservers = this.errorObservers.filter(obs => obs.callback !== observer)
|
this.errorObservers = this.errorObservers.filter(obs => obs.callback !== observer)
|
||||||
},
|
},
|
||||||
setFieldValue (field, value) {
|
|
||||||
Object.assign(this.internalFormModelProxy, { [field]: value })
|
|
||||||
this.$emit('input', Object.assign({}, this.internalFormModelProxy))
|
|
||||||
},
|
|
||||||
getUniqueRegistryName (base, count = 0) {
|
|
||||||
if (Object.prototype.hasOwnProperty.call(this.registry, base + (count || ''))) {
|
|
||||||
return this.getUniqueRegistryName(base, count + 1)
|
|
||||||
}
|
|
||||||
return base + (count || '')
|
|
||||||
},
|
|
||||||
register (field, component) {
|
|
||||||
// Don't re-register fields... @todo come up with another way of handling this that doesn't break multi option
|
|
||||||
if (Object.prototype.hasOwnProperty.call(this.registry, field)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
this.registry[field] = component
|
|
||||||
const hasVModelValue = Object.prototype.hasOwnProperty.call(component.$options.propsData, 'formulateValue')
|
|
||||||
const hasValue = Object.prototype.hasOwnProperty.call(component.$options.propsData, 'value')
|
|
||||||
if (
|
|
||||||
!hasVModelValue &&
|
|
||||||
this.hasInitialValue &&
|
|
||||||
this.initialValues[field]
|
|
||||||
) {
|
|
||||||
// In the case that the form is carrying an initial value and the
|
|
||||||
// element is not, set it directly.
|
|
||||||
component.context.model = this.initialValues[field]
|
|
||||||
} else if (
|
|
||||||
(hasVModelValue || hasValue) &&
|
|
||||||
!shallowEqualObjects(component.internalModelProxy, this.initialValues[field])
|
|
||||||
) {
|
|
||||||
this.setFieldValue(field, component.internalModelProxy)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
registerErrorComponent (component) {
|
registerErrorComponent (component) {
|
||||||
if (!this.errorComponents.includes(component)) {
|
if (!this.errorComponents.includes(component)) {
|
||||||
this.errorComponents.push(component)
|
this.errorComponents.push(component)
|
||||||
@ -245,27 +167,8 @@ export default {
|
|||||||
return undefined
|
return undefined
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
showErrors () {
|
|
||||||
for (const fieldName in this.registry) {
|
|
||||||
this.registry[fieldName].formShouldShowErrors = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getFormValues () {
|
|
||||||
return this.internalFormModelProxy
|
|
||||||
},
|
|
||||||
formulateFieldValidation (errorObject) {
|
formulateFieldValidation (errorObject) {
|
||||||
this.$emit('validation', errorObject)
|
this.$emit('validation', errorObject)
|
||||||
},
|
|
||||||
hasValidationErrors () {
|
|
||||||
const resolvers = []
|
|
||||||
for (const fieldName in this.registry) {
|
|
||||||
if (typeof this.registry[fieldName].getValidationErrors === 'function') {
|
|
||||||
resolvers.push(this.registry[fieldName].getValidationErrors())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Promise.all(resolvers).then((errorObjects) => {
|
|
||||||
return errorObjects.some(item => item.hasErrors)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
102
src/FormulateGrouping.vue
Normal file
102
src/FormulateGrouping.vue
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<template>
|
||||||
|
<FormulateSlot
|
||||||
|
name="grouping"
|
||||||
|
class="formulate-input-grouping"
|
||||||
|
:context="context"
|
||||||
|
:force-wrap="context.repeatable"
|
||||||
|
>
|
||||||
|
<FormulateRepeatableProvider
|
||||||
|
v-for="(item, index) in items"
|
||||||
|
:ref="`provider-${index}`"
|
||||||
|
:key="index"
|
||||||
|
:index="index"
|
||||||
|
:set-field-value="(field, value) => setFieldValue(index, field, value)"
|
||||||
|
:context="context"
|
||||||
|
@remove="removeItem"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</FormulateRepeatableProvider>
|
||||||
|
</FormulateSlot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FormulateGrouping',
|
||||||
|
props: {
|
||||||
|
context: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
provide () {
|
||||||
|
return {
|
||||||
|
isSubField: () => true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
inject: ['formulateRegisterRule', 'formulateRemoveRule'],
|
||||||
|
computed: {
|
||||||
|
items () {
|
||||||
|
if (Array.isArray(this.context.model)) {
|
||||||
|
if (!this.context.repeatable && this.context.model.length === 0) {
|
||||||
|
return [{}]
|
||||||
|
}
|
||||||
|
return this.context.model
|
||||||
|
}
|
||||||
|
return [{}]
|
||||||
|
},
|
||||||
|
providers () {
|
||||||
|
return this.items.map((item, i) => Array.isArray(this.$refs[`provider-${i}`]) ? this.$refs[`provider-${i}`][0] : false)
|
||||||
|
},
|
||||||
|
formShouldShowErrors () {
|
||||||
|
return this.context.formShouldShowErrors
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
providers () {
|
||||||
|
if (this.formShouldShowErrors) {
|
||||||
|
this.showErrors()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formShouldShowErrors (val) {
|
||||||
|
if (val) {
|
||||||
|
this.showErrors()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
// We register with an error message of 'true' which causes the validation to fail but no message output.
|
||||||
|
this.formulateRegisterRule(this.validateGroup.bind(this), [], 'formulateGrouping', true)
|
||||||
|
},
|
||||||
|
destroyed () {
|
||||||
|
this.formulateRemoveRule('formulateGrouping')
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setFieldValue (index, field, value) {
|
||||||
|
const values = Array.isArray(this.context.model) ? this.context.model : []
|
||||||
|
values.splice(index, 1, Object.assign(
|
||||||
|
{},
|
||||||
|
typeof this.context.model[index] === 'object' ? this.context.model[index] : {},
|
||||||
|
{ [field]: value }
|
||||||
|
))
|
||||||
|
this.context.model = values
|
||||||
|
},
|
||||||
|
validateGroup () {
|
||||||
|
return Promise.all(this.providers.reduce((resolvers, provider) => {
|
||||||
|
if (provider && typeof provider.hasValidationErrors === 'function') {
|
||||||
|
resolvers.push(provider.hasValidationErrors())
|
||||||
|
}
|
||||||
|
return resolvers
|
||||||
|
}, [])).then(providersHasErrors => !providersHasErrors.some(hasErrors => !!hasErrors))
|
||||||
|
},
|
||||||
|
showErrors () {
|
||||||
|
this.providers.map(p => p && typeof p.showErrors === 'function' && p.showErrors())
|
||||||
|
},
|
||||||
|
removeItem (index) {
|
||||||
|
if (Array.isArray(this.context.model)) {
|
||||||
|
this.context.model.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -8,14 +8,25 @@
|
|||||||
>
|
>
|
||||||
<div class="formulate-input-wrapper">
|
<div class="formulate-input-wrapper">
|
||||||
<slot
|
<slot
|
||||||
v-if="context.hasLabel && context.labelPosition === 'before'"
|
v-if="context.labelPosition === 'before'"
|
||||||
name="label"
|
name="label"
|
||||||
v-bind="context"
|
v-bind="context"
|
||||||
>
|
>
|
||||||
<label
|
<component
|
||||||
class="formulate-input-label formulate-input-label--before"
|
:is="context.slotComponents.label"
|
||||||
:for="context.attributes.id"
|
v-if="context.hasLabel"
|
||||||
v-text="context.label"
|
:context="context"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
<slot
|
||||||
|
v-if="context.helpPosition === 'before'"
|
||||||
|
name="help"
|
||||||
|
v-bind="context"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="context.slotComponents.help"
|
||||||
|
v-if="context.help"
|
||||||
|
:context="context"
|
||||||
/>
|
/>
|
||||||
</slot>
|
</slot>
|
||||||
<slot
|
<slot
|
||||||
@ -25,50 +36,71 @@
|
|||||||
<component
|
<component
|
||||||
:is="context.component"
|
:is="context.component"
|
||||||
:context="context"
|
:context="context"
|
||||||
|
@click="$emit('click', $event)"
|
||||||
>
|
>
|
||||||
<slot v-bind="context" />
|
<slot v-bind="context" />
|
||||||
</component>
|
</component>
|
||||||
</slot>
|
</slot>
|
||||||
<slot
|
<slot
|
||||||
v-if="context.hasLabel && context.labelPosition === 'after'"
|
v-if="context.labelPosition === 'after'"
|
||||||
name="label"
|
name="label"
|
||||||
v-bind="context.label"
|
v-bind="context"
|
||||||
>
|
>
|
||||||
<label
|
<component
|
||||||
class="formulate-input-label formulate-input-label--after"
|
:is="context.slotComponents.label"
|
||||||
:for="context.attributes.id"
|
v-if="context.hasLabel"
|
||||||
v-text="context.label"
|
:context="context"
|
||||||
/>
|
/>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<slot
|
||||||
v-if="help"
|
v-if="context.helpPosition === 'after'"
|
||||||
class="formulate-input-help"
|
name="help"
|
||||||
v-text="help"
|
v-bind="context"
|
||||||
/>
|
>
|
||||||
<FormulateErrors
|
<component
|
||||||
v-if="!disableErrors"
|
:is="context.slotComponents.help"
|
||||||
:type="`input`"
|
v-if="context.help"
|
||||||
:context="context"
|
:context="context"
|
||||||
/>
|
/>
|
||||||
|
</slot>
|
||||||
|
<slot
|
||||||
|
name="errors"
|
||||||
|
v-bind="context"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="context.slotComponents.errors"
|
||||||
|
v-if="!context.disableErrors"
|
||||||
|
:type="context.slotComponents.errors === 'FormulateErrors' ? 'input' : false"
|
||||||
|
:context="context"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import context from './libs/context'
|
import context from './libs/context'
|
||||||
import { shallowEqualObjects, parseRules, snakeToCamel, arrayify } from './libs/utils'
|
import { shallowEqualObjects, parseRules, snakeToCamel, has, arrayify, groupBails } from './libs/utils'
|
||||||
import nanoid from 'nanoid/non-secure'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FormulateInput',
|
name: 'FormulateInput',
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
|
provide () {
|
||||||
|
return {
|
||||||
|
// Allows sub-components of this input to register arbitrary rules.
|
||||||
|
formulateRegisterRule: this.registerRule,
|
||||||
|
formulateRemoveRule: this.removeRule
|
||||||
|
}
|
||||||
|
},
|
||||||
inject: {
|
inject: {
|
||||||
formulateFormSetter: { default: undefined },
|
formulateSetter: { default: undefined },
|
||||||
formulateFieldValidation: { default: () => () => ({}) },
|
formulateFieldValidation: { default: () => () => ({}) },
|
||||||
formulateFormRegister: { default: undefined },
|
formulateRegister: { default: undefined },
|
||||||
|
formulateDeregister: { default: undefined },
|
||||||
getFormValues: { default: () => () => ({}) },
|
getFormValues: { default: () => () => ({}) },
|
||||||
observeErrors: { default: undefined },
|
observeErrors: { default: undefined },
|
||||||
removeErrorObserver: { default: undefined }
|
removeErrorObserver: { default: undefined },
|
||||||
|
isSubField: { default: () => () => false }
|
||||||
},
|
},
|
||||||
model: {
|
model: {
|
||||||
prop: 'formulateValue',
|
prop: 'formulateValue',
|
||||||
@ -111,18 +143,26 @@ export default {
|
|||||||
type: [String, Boolean],
|
type: [String, Boolean],
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
limit: {
|
||||||
|
type: Number,
|
||||||
|
default: Infinity
|
||||||
|
},
|
||||||
help: {
|
help: {
|
||||||
type: [String, Boolean],
|
type: [String, Boolean],
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
debug: {
|
helpPosition: {
|
||||||
type: Boolean,
|
type: [String, Boolean],
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
errors: {
|
errors: {
|
||||||
type: [String, Array, Boolean],
|
type: [String, Array, Boolean],
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
repeatable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
validation: {
|
validation: {
|
||||||
type: [String, Boolean, Array],
|
type: [String, Boolean, Array],
|
||||||
default: false
|
default: false
|
||||||
@ -139,7 +179,7 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'blur',
|
default: 'blur',
|
||||||
validator: function (value) {
|
validator: function (value) {
|
||||||
return ['blur', 'live'].includes(value)
|
return ['blur', 'live', 'submit'].includes(value)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showErrors: {
|
showErrors: {
|
||||||
@ -185,18 +225,25 @@ export default {
|
|||||||
disableErrors: {
|
disableErrors: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
addLabel: {
|
||||||
|
type: [Boolean, String],
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
defaultId: nanoid(9),
|
defaultId: this.$formulate.nextId(this),
|
||||||
localAttributes: {},
|
localAttributes: {},
|
||||||
localErrors: [],
|
localErrors: [],
|
||||||
internalModelProxy: this.getInitialValue(),
|
proxy: this.getInitialValue(),
|
||||||
behavioralErrorVisibility: (this.errorBehavior === 'live'),
|
behavioralErrorVisibility: (this.errorBehavior === 'live'),
|
||||||
formShouldShowErrors: false,
|
formShouldShowErrors: false,
|
||||||
validationErrors: [],
|
validationErrors: [],
|
||||||
pendingValidation: Promise.resolve()
|
pendingValidation: Promise.resolve(),
|
||||||
|
// These registries are used for injected messages registrants only (mostly internal).
|
||||||
|
ruleRegistry: [],
|
||||||
|
messageRegistry: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -210,7 +257,7 @@ export default {
|
|||||||
},
|
},
|
||||||
parsedValidationRules () {
|
parsedValidationRules () {
|
||||||
const parsedValidationRules = {}
|
const parsedValidationRules = {}
|
||||||
Object.keys(this.validationRules).forEach((key) => {
|
Object.keys(this.validationRules).forEach(key => {
|
||||||
parsedValidationRules[snakeToCamel(key)] = this.validationRules[key]
|
parsedValidationRules[snakeToCamel(key)] = this.validationRules[key]
|
||||||
})
|
})
|
||||||
return parsedValidationRules
|
return parsedValidationRules
|
||||||
@ -220,6 +267,9 @@ export default {
|
|||||||
Object.keys(this.validationMessages).forEach((key) => {
|
Object.keys(this.validationMessages).forEach((key) => {
|
||||||
messages[snakeToCamel(key)] = this.validationMessages[key]
|
messages[snakeToCamel(key)] = this.validationMessages[key]
|
||||||
})
|
})
|
||||||
|
Object.keys(this.messageRegistry).forEach((key) => {
|
||||||
|
messages[snakeToCamel(key)] = this.messageRegistry[key]
|
||||||
|
})
|
||||||
return messages
|
return messages
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -230,7 +280,7 @@ export default {
|
|||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
},
|
},
|
||||||
internalModelProxy (newValue, oldValue) {
|
proxy (newValue, oldValue) {
|
||||||
this.performValidation()
|
this.performValidation()
|
||||||
if (!this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
|
if (!this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
|
||||||
this.context.model = newValue
|
this.context.model = newValue
|
||||||
@ -250,8 +300,8 @@ export default {
|
|||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
this.applyInitialValue()
|
this.applyInitialValue()
|
||||||
if (this.formulateFormRegister && typeof this.formulateFormRegister === 'function') {
|
if (this.formulateRegister && typeof this.formulateRegister === 'function') {
|
||||||
this.formulateFormRegister(this.nameOrFallback, this)
|
this.formulateRegister(this.nameOrFallback, this)
|
||||||
}
|
}
|
||||||
if (!this.disableErrors && typeof this.observeErrors === 'function') {
|
if (!this.disableErrors && typeof this.observeErrors === 'function') {
|
||||||
this.observeErrors({ callback: this.setErrors, type: 'input', field: this.nameOrFallback })
|
this.observeErrors({ callback: this.setErrors, type: 'input', field: this.nameOrFallback })
|
||||||
@ -259,10 +309,13 @@ export default {
|
|||||||
this.updateLocalAttributes(this.$attrs)
|
this.updateLocalAttributes(this.$attrs)
|
||||||
this.performValidation()
|
this.performValidation()
|
||||||
},
|
},
|
||||||
destroyed () {
|
beforeDestroy () {
|
||||||
if (!this.disableErrors && typeof this.removeErrorObserver === 'function') {
|
if (!this.disableErrors && typeof this.removeErrorObserver === 'function') {
|
||||||
this.removeErrorObserver(this.setErrors)
|
this.removeErrorObserver(this.setErrors)
|
||||||
}
|
}
|
||||||
|
if (typeof this.formulateDeregister === 'function') {
|
||||||
|
this.formulateDeregister(this.nameOrFallback)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getInitialValue () {
|
getInitialValue () {
|
||||||
@ -271,9 +324,9 @@ export default {
|
|||||||
classification = (classification === 'box' && this.options) ? 'group' : classification
|
classification = (classification === 'box' && this.options) ? 'group' : classification
|
||||||
if (classification === 'box' && this.checked) {
|
if (classification === 'box' && this.checked) {
|
||||||
return this.value || true
|
return this.value || true
|
||||||
} else if (Object.prototype.hasOwnProperty.call(this.$options.propsData, 'value') && classification !== 'box') {
|
} else if (has(this.$options.propsData, 'value') && classification !== 'box') {
|
||||||
return this.value
|
return this.value
|
||||||
} else if (Object.prototype.hasOwnProperty.call(this.$options.propsData, 'formulateValue')) {
|
} else if (has(this.$options.propsData, 'formulateValue')) {
|
||||||
return this.formulateValue
|
return this.formulateValue
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
@ -282,11 +335,11 @@ export default {
|
|||||||
// This should only be run immediately on created and ensures that the
|
// This should only be run immediately on created and ensures that the
|
||||||
// proxy and the model are both the same before any additional registration.
|
// proxy and the model are both the same before any additional registration.
|
||||||
if (
|
if (
|
||||||
!shallowEqualObjects(this.context.model, this.internalModelProxy) &&
|
!shallowEqualObjects(this.context.model, this.proxy) &&
|
||||||
// we dont' want to set the model if we are a sub-box of a multi-box field
|
// we dont' want to set the model if we are a sub-box of a multi-box field
|
||||||
(Object.prototype.hasOwnProperty(this.$options.propsData, 'options') && this.classification === 'box')
|
(Object.prototype.hasOwnProperty(this.$options.propsData, 'options') && this.classification === 'box')
|
||||||
) {
|
) {
|
||||||
this.context.model = this.internalModelProxy
|
this.context.model = this.proxy
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateLocalAttributes (value) {
|
updateLocalAttributes (value) {
|
||||||
@ -295,21 +348,45 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
performValidation () {
|
performValidation () {
|
||||||
const rules = parseRules(this.validation, this.$formulate.rules(this.parsedValidationRules))
|
let rules = parseRules(this.validation, this.$formulate.rules(this.parsedValidationRules))
|
||||||
this.pendingValidation = Promise.all(
|
// Add in ruleRegistry rules. These are added directly via injection from
|
||||||
rules.map(([rule, args, ruleName]) => {
|
// 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({
|
var res = rule({
|
||||||
value: this.context.model,
|
value: this.context.model,
|
||||||
getFormValues: this.getFormValues.bind(this),
|
getFormValues: this.getFormValues.bind(this),
|
||||||
name: this.context.name
|
name: this.context.name
|
||||||
}, ...args)
|
}, ...args)
|
||||||
res = (res instanceof Promise) ? res : Promise.resolve(res)
|
res = (res instanceof Promise) ? res : Promise.resolve(res)
|
||||||
return res.then(res => res ? false : this.getMessage(ruleName, args))
|
return res.then(result => result ? false : this.getMessage(ruleName, args))
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
})
|
})
|
||||||
)
|
|
||||||
.then(result => result.filter(result => result))
|
|
||||||
.then(messages => this.didValidate(messages))
|
|
||||||
return this.pendingValidation
|
|
||||||
},
|
},
|
||||||
didValidate (messages) {
|
didValidate (messages) {
|
||||||
const validationChanged = !shallowEqualObjects(messages, this.validationErrors)
|
const validationChanged = !shallowEqualObjects(messages, this.validationErrors)
|
||||||
@ -338,6 +415,7 @@ export default {
|
|||||||
case 'function':
|
case 'function':
|
||||||
return this.messages[ruleName]
|
return this.messages[ruleName]
|
||||||
case 'string':
|
case 'string':
|
||||||
|
case 'boolean':
|
||||||
return () => this.messages[ruleName]
|
return () => this.messages[ruleName]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -352,20 +430,34 @@ export default {
|
|||||||
},
|
},
|
||||||
getValidationErrors () {
|
getValidationErrors () {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => this.pendingValidation.then(() => resolve(this.getErrorObject())))
|
||||||
this.pendingValidation.then(() => resolve(this.getErrorObject()))
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getErrorObject () {
|
getErrorObject () {
|
||||||
return {
|
return {
|
||||||
name: this.context.nameOrFallback || this.context.name,
|
name: this.context.nameOrFallback || this.context.name,
|
||||||
errors: this.validationErrors,
|
errors: this.validationErrors.filter(s => typeof s === 'string'),
|
||||||
hasErrors: !!this.validationErrors.length
|
hasErrors: !!this.validationErrors.length
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setErrors (errors) {
|
setErrors (errors) {
|
||||||
this.localErrors = arrayify(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]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,66 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="formulate-input-group">
|
|
||||||
<component
|
|
||||||
:is="subComponent"
|
|
||||||
v-for="optionContext in optionsWithContext"
|
|
||||||
:key="optionContext.id"
|
|
||||||
v-model="context.model"
|
|
||||||
v-bind="optionContext"
|
|
||||||
:disable-errors="true"
|
|
||||||
class="formulate-input-group-item"
|
|
||||||
@blur="context.blurHandler"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'FormulateInputGroup',
|
|
||||||
props: {
|
|
||||||
context: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
options () {
|
|
||||||
return this.context.options || []
|
|
||||||
},
|
|
||||||
subComponent () {
|
|
||||||
// @todo - rough-in for future flexible input-groups
|
|
||||||
return 'FormulateInput'
|
|
||||||
},
|
|
||||||
optionsWithContext () {
|
|
||||||
const {
|
|
||||||
// The following are a list of items to pull out of the context object
|
|
||||||
options,
|
|
||||||
labelPosition,
|
|
||||||
attributes: { id, ...groupApplicableAttributes },
|
|
||||||
classification,
|
|
||||||
blurHandler,
|
|
||||||
performValidation,
|
|
||||||
hasValidationErrors,
|
|
||||||
getValidationErrors,
|
|
||||||
validationErrors,
|
|
||||||
setErrors,
|
|
||||||
visibleValidationErrors,
|
|
||||||
component,
|
|
||||||
hasLabel,
|
|
||||||
...context
|
|
||||||
} = this.context
|
|
||||||
return this.options.map(option => this.groupItemContext(
|
|
||||||
context,
|
|
||||||
option,
|
|
||||||
groupApplicableAttributes
|
|
||||||
))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
groupItemContext (context, option, groupAttributes) {
|
|
||||||
const optionAttributes = {}
|
|
||||||
const ctx = Object.assign({}, context, option, groupAttributes, optionAttributes)
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
63
src/FormulateRepeatableProvider.vue
Normal file
63
src/FormulateRepeatableProvider.vue
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<FormulateSlot
|
||||||
|
name="repeatable"
|
||||||
|
:context="context"
|
||||||
|
:index="index"
|
||||||
|
:remove-item="removeItem"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="context.slotComponents.repeatable"
|
||||||
|
:context="context"
|
||||||
|
:index="index"
|
||||||
|
:remove-item="removeItem"
|
||||||
|
>
|
||||||
|
<FormulateSlot
|
||||||
|
:context="context"
|
||||||
|
:index="index"
|
||||||
|
name="default"
|
||||||
|
/>
|
||||||
|
</component>
|
||||||
|
</FormulateSlot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import useRegistry, { useRegistryComputed, useRegistryMethods, useRegistryProviders } from './libs/registry'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
provide () {
|
||||||
|
return {
|
||||||
|
...useRegistryProviders(this),
|
||||||
|
formulateSetter: (field, value) => this.setFieldValue(field, value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
index: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
setFieldValue: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
...useRegistry(this),
|
||||||
|
isGrouping: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...useRegistryComputed()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...useRegistryMethods(['setFieldValue']),
|
||||||
|
removeItem () {
|
||||||
|
this.$emit('remove', this.index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
36
src/FormulateSlot.js
Normal file
36
src/FormulateSlot.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
export default {
|
||||||
|
inheritAttrs: false,
|
||||||
|
functional: true,
|
||||||
|
render (h, { props, data, parent, children }) {
|
||||||
|
var p = parent
|
||||||
|
var { name, forceWrap, context, ...mergeWithContext } = props
|
||||||
|
|
||||||
|
// Look up the ancestor tree for the first FormulateInput
|
||||||
|
while (p && p.$options.name !== 'FormulateInput') {
|
||||||
|
p = p.$parent
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we never found the proper parent, just end it.
|
||||||
|
if (!p) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we found a formulate input, check for a matching scoped slot
|
||||||
|
if (p.$scopedSlots && p.$scopedSlots[props.name]) {
|
||||||
|
return p.$scopedSlots[props.name]({ ...context, ...mergeWithContext })
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we found no scoped slot, take the children and render those inside a wrapper if there are multiple
|
||||||
|
if (Array.isArray(children) && (children.length > 1 || (forceWrap && children.length > 0))) {
|
||||||
|
const { name, context, ...attrs } = data.attrs
|
||||||
|
return h('div', { ...data, ...{ attrs } }, children)
|
||||||
|
|
||||||
|
// If there is only one child, render it alone
|
||||||
|
} else if (Array.isArray(children) && children.length === 1) {
|
||||||
|
return children[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are no children, render nothing
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@
|
|||||||
<button
|
<button
|
||||||
:type="type"
|
:type="type"
|
||||||
v-bind="attributes"
|
v-bind="attributes"
|
||||||
|
@click="$emit('click', $event)"
|
||||||
>
|
>
|
||||||
<slot>
|
<slot>
|
||||||
<span
|
<span
|
||||||
|
110
src/inputs/FormulateInputGroup.vue
Normal file
110
src/inputs/FormulateInputGroup.vue
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="formulate-input-group"
|
||||||
|
:data-is-repeatable="context.repeatable"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-if="subType !== 'grouping'"
|
||||||
|
>
|
||||||
|
<FormulateInput
|
||||||
|
v-for="optionContext in optionsWithContext"
|
||||||
|
:key="optionContext.id"
|
||||||
|
v-model="context.model"
|
||||||
|
v-bind="optionContext"
|
||||||
|
:disable-errors="true"
|
||||||
|
class="formulate-input-group-item"
|
||||||
|
@blur="context.blurHandler"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template
|
||||||
|
v-else
|
||||||
|
>
|
||||||
|
<FormulateGrouping
|
||||||
|
:context="context"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</FormulateGrouping>
|
||||||
|
<FormulateSlot
|
||||||
|
v-if="canAddMore"
|
||||||
|
name="addmore"
|
||||||
|
:context="context"
|
||||||
|
:add-more="addItem"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="context.slotComponents.addMore"
|
||||||
|
:context="context"
|
||||||
|
:add-more="addItem"
|
||||||
|
@add="addItem"
|
||||||
|
/>
|
||||||
|
</FormulateSlot>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'FormulateInputGroup',
|
||||||
|
props: {
|
||||||
|
context: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
options () {
|
||||||
|
return this.context.options || []
|
||||||
|
},
|
||||||
|
subType () {
|
||||||
|
return (this.context.type === 'group') ? 'grouping' : 'inputs'
|
||||||
|
},
|
||||||
|
optionsWithContext () {
|
||||||
|
const {
|
||||||
|
// The following are a list of items to pull out of the context object
|
||||||
|
attributes: { id, ...groupApplicableAttributes },
|
||||||
|
blurHandler,
|
||||||
|
classification,
|
||||||
|
component,
|
||||||
|
getValidationErrors,
|
||||||
|
hasLabel,
|
||||||
|
hasValidationErrors,
|
||||||
|
isSubField,
|
||||||
|
labelPosition,
|
||||||
|
options,
|
||||||
|
performValidation,
|
||||||
|
setErrors,
|
||||||
|
slotComponents,
|
||||||
|
validationErrors,
|
||||||
|
visibleValidationErrors,
|
||||||
|
...context
|
||||||
|
} = this.context
|
||||||
|
return this.options.map(option => this.groupItemContext(
|
||||||
|
context,
|
||||||
|
option,
|
||||||
|
groupApplicableAttributes
|
||||||
|
))
|
||||||
|
},
|
||||||
|
canAddMore () {
|
||||||
|
return (this.context.repeatable && this.items.length < this.context.limit)
|
||||||
|
},
|
||||||
|
items () {
|
||||||
|
return Array.isArray(this.context.model) ? this.context.model : [{}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addItem () {
|
||||||
|
if (Array.isArray(this.context.model)) {
|
||||||
|
this.context.model.push({})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.context.model = this.items.concat([{}])
|
||||||
|
},
|
||||||
|
groupItemContext (context, option, groupAttributes) {
|
||||||
|
const optionAttributes = {}
|
||||||
|
const ctx = Object.assign({}, context, option, groupAttributes, optionAttributes, !context.hasGivenName ? {
|
||||||
|
name: true
|
||||||
|
} : {})
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -1,4 +1,3 @@
|
|||||||
import nanoid from 'nanoid/non-secure'
|
|
||||||
import { map, arrayify, shallowEqualObjects } from './utils'
|
import { map, arrayify, shallowEqualObjects } from './utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -9,38 +8,50 @@ import { map, arrayify, shallowEqualObjects } from './utils'
|
|||||||
export default {
|
export default {
|
||||||
context () {
|
context () {
|
||||||
return defineModel.call(this, {
|
return defineModel.call(this, {
|
||||||
type: this.type,
|
addLabel: this.logicalAddLabel,
|
||||||
value: this.value,
|
attributes: this.elementAttributes,
|
||||||
name: this.nameOrFallback,
|
blurHandler: blurHandler.bind(this),
|
||||||
classification: this.classification,
|
classification: this.classification,
|
||||||
component: this.component,
|
component: this.component,
|
||||||
id: this.id || this.defaultId,
|
disableErrors: this.disableErrors,
|
||||||
|
errors: this.explicitErrors,
|
||||||
|
formShouldShowErrors: this.formShouldShowErrors,
|
||||||
|
getValidationErrors: this.getValidationErrors.bind(this),
|
||||||
|
hasGivenName: this.hasGivenName,
|
||||||
hasLabel: (this.label && this.classification !== 'button'),
|
hasLabel: (this.label && this.classification !== 'button'),
|
||||||
|
hasValidationErrors: this.hasValidationErrors.bind(this),
|
||||||
|
help: this.help,
|
||||||
|
helpPosition: this.logicalHelpPosition,
|
||||||
|
id: this.id || this.defaultId,
|
||||||
|
imageBehavior: this.imageBehavior,
|
||||||
label: this.label,
|
label: this.label,
|
||||||
labelPosition: this.logicalLabelPosition,
|
labelPosition: this.logicalLabelPosition,
|
||||||
attributes: this.elementAttributes,
|
limit: this.limit,
|
||||||
|
name: this.nameOrFallback,
|
||||||
performValidation: this.performValidation.bind(this),
|
performValidation: this.performValidation.bind(this),
|
||||||
blurHandler: blurHandler.bind(this),
|
|
||||||
imageBehavior: this.imageBehavior,
|
|
||||||
uploadUrl: this.mergedUploadUrl,
|
|
||||||
uploader: this.uploader || this.$formulate.getUploader(),
|
|
||||||
uploadBehavior: this.uploadBehavior,
|
|
||||||
preventWindowDrops: this.preventWindowDrops,
|
preventWindowDrops: this.preventWindowDrops,
|
||||||
hasValidationErrors: this.hasValidationErrors,
|
repeatable: this.repeatable,
|
||||||
getValidationErrors: this.getValidationErrors.bind(this),
|
|
||||||
validationErrors: this.validationErrors,
|
|
||||||
errors: this.explicitErrors,
|
|
||||||
setErrors: this.setErrors.bind(this),
|
setErrors: this.setErrors.bind(this),
|
||||||
showValidationErrors: this.showValidationErrors,
|
showValidationErrors: this.showValidationErrors,
|
||||||
|
slotComponents: this.slotComponents,
|
||||||
|
type: this.type,
|
||||||
|
uploadBehavior: this.uploadBehavior,
|
||||||
|
uploadUrl: this.mergedUploadUrl,
|
||||||
|
uploader: this.uploader || this.$formulate.getUploader(),
|
||||||
|
validationErrors: this.validationErrors,
|
||||||
|
value: this.value,
|
||||||
visibleValidationErrors: this.visibleValidationErrors,
|
visibleValidationErrors: this.visibleValidationErrors,
|
||||||
|
isSubField: this.isSubField,
|
||||||
...this.typeContext
|
...this.typeContext
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
// Used in sub-context
|
// Used in sub-context
|
||||||
nameOrFallback,
|
nameOrFallback,
|
||||||
|
hasGivenName,
|
||||||
typeContext,
|
typeContext,
|
||||||
elementAttributes,
|
elementAttributes,
|
||||||
logicalLabelPosition,
|
logicalLabelPosition,
|
||||||
|
logicalHelpPosition,
|
||||||
mergedUploadUrl,
|
mergedUploadUrl,
|
||||||
|
|
||||||
// These items are not passed as context
|
// These items are not passed as context
|
||||||
@ -51,7 +62,19 @@ export default {
|
|||||||
hasErrors,
|
hasErrors,
|
||||||
hasVisibleErrors,
|
hasVisibleErrors,
|
||||||
showValidationErrors,
|
showValidationErrors,
|
||||||
visibleValidationErrors
|
visibleValidationErrors,
|
||||||
|
slotComponents,
|
||||||
|
logicalAddLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The label to display when adding a new group.
|
||||||
|
*/
|
||||||
|
function logicalAddLabel () {
|
||||||
|
if (typeof this.addLabel === 'boolean') {
|
||||||
|
return `+ ${this.label || this.name || 'Add'}`
|
||||||
|
}
|
||||||
|
return this.addLabel
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -85,16 +108,27 @@ function typeContext () {
|
|||||||
*/
|
*/
|
||||||
function elementAttributes () {
|
function elementAttributes () {
|
||||||
const attrs = Object.assign({}, this.localAttributes)
|
const attrs = Object.assign({}, this.localAttributes)
|
||||||
|
// pass the ID prop through to the root element
|
||||||
if (this.id) {
|
if (this.id) {
|
||||||
attrs.id = this.id
|
attrs.id = this.id
|
||||||
} else {
|
} else {
|
||||||
attrs.id = this.defaultId
|
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
|
return attrs
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine the a best-guess location for the label (before or after).
|
* Determine the best-guess location for the label (before or after).
|
||||||
* @return {string} before|after
|
* @return {string} before|after
|
||||||
*/
|
*/
|
||||||
function logicalLabelPosition () {
|
function logicalLabelPosition () {
|
||||||
@ -109,6 +143,21 @@ function logicalLabelPosition () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the best location for the label based on type (before or after).
|
||||||
|
*/
|
||||||
|
function logicalHelpPosition () {
|
||||||
|
if (this.helpPosition) {
|
||||||
|
return this.helpPosition
|
||||||
|
}
|
||||||
|
switch (this.classification) {
|
||||||
|
case 'group':
|
||||||
|
return 'before'
|
||||||
|
default:
|
||||||
|
return 'after'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The validation label to use.
|
* The validation label to use.
|
||||||
*/
|
*/
|
||||||
@ -168,6 +217,13 @@ function nameOrFallback () {
|
|||||||
return this.name
|
return this.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* determine if an input has a user-defined name
|
||||||
|
*/
|
||||||
|
function hasGivenName () {
|
||||||
|
return typeof this.name !== 'boolean'
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if this formulate element is v-modeled or not.
|
* Determines if this formulate element is v-modeled or not.
|
||||||
*/
|
*/
|
||||||
@ -192,8 +248,6 @@ function createOptionList (options) {
|
|||||||
optionList.push({ value, label: options[value], id: `${that.elementAttributes.id}_${value}` })
|
optionList.push({ value, label: options[value], id: `${that.elementAttributes.id}_${value}` })
|
||||||
}
|
}
|
||||||
return optionList
|
return optionList
|
||||||
} else if (Array.isArray(options) && !options.length) {
|
|
||||||
return [{ value: this.value, label: (this.label || this.name), id: this.context.id || nanoid(9) }]
|
|
||||||
}
|
}
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
@ -229,6 +283,19 @@ function hasVisibleErrors () {
|
|||||||
return ((this.validationErrors && this.showValidationErrors) || !!this.explicitErrors.length)
|
return ((this.validationErrors && this.showValidationErrors) || !!this.explicitErrors.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The component that should be rendered in the label slot as default.
|
||||||
|
*/
|
||||||
|
function slotComponents () {
|
||||||
|
return {
|
||||||
|
label: this.$formulate.slotComponent(this.type, 'label'),
|
||||||
|
help: this.$formulate.slotComponent(this.type, 'help'),
|
||||||
|
errors: this.$formulate.slotComponent(this.type, 'errors'),
|
||||||
|
repeatable: this.$formulate.slotComponent(this.type, 'repeatable'),
|
||||||
|
addMore: this.$formulate.slotComponent(this.type, 'addMore')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bound into the context object.
|
* Bound into the context object.
|
||||||
*/
|
*/
|
||||||
@ -254,7 +321,7 @@ function defineModel (context) {
|
|||||||
* Get the value from a model.
|
* Get the value from a model.
|
||||||
**/
|
**/
|
||||||
function modelGetter () {
|
function modelGetter () {
|
||||||
const model = this.isVmodeled ? 'formulateValue' : 'internalModelProxy'
|
const model = this.isVmodeled ? 'formulateValue' : 'proxy'
|
||||||
if (this.type === 'checkbox' && !Array.isArray(this[model]) && this.options) {
|
if (this.type === 'checkbox' && !Array.isArray(this[model]) && this.options) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@ -268,11 +335,11 @@ function modelGetter () {
|
|||||||
* Set the value from a model.
|
* Set the value from a model.
|
||||||
**/
|
**/
|
||||||
function modelSetter (value) {
|
function modelSetter (value) {
|
||||||
if (!shallowEqualObjects(value, this.internalModelProxy)) {
|
if (!shallowEqualObjects(value, this.proxy)) {
|
||||||
this.internalModelProxy = value
|
this.proxy = value
|
||||||
}
|
}
|
||||||
this.$emit('input', value)
|
this.$emit('input', value)
|
||||||
if (this.context.name && typeof this.formulateFormSetter === 'function') {
|
if (this.context.name && typeof this.formulateSetter === 'function') {
|
||||||
this.formulateFormSetter(this.context.name, value)
|
this.formulateSetter(this.context.name, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,5 +47,8 @@ export default {
|
|||||||
|
|
||||||
// === FILE TYPE
|
// === FILE TYPE
|
||||||
file: add('file'),
|
file: add('file'),
|
||||||
image: add('file')
|
image: add('file'),
|
||||||
|
|
||||||
|
// === GROUP TYPE
|
||||||
|
group: add('group')
|
||||||
}
|
}
|
||||||
|
249
src/libs/registry.js
Normal file
249
src/libs/registry.js
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
import { shallowEqualObjects, has } 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, 'formulateValue')
|
||||||
|
const hasValue = has(component.$options.propsData, 'value')
|
||||||
|
if (
|
||||||
|
!hasVModelValue &&
|
||||||
|
this.ctx.hasInitialValue &&
|
||||||
|
this.ctx.initialValues[field]
|
||||||
|
) {
|
||||||
|
// In the case that the form is carrying an initial value and the
|
||||||
|
// element is not, set it directly.
|
||||||
|
component.context.model = this.ctx.initialValues[field]
|
||||||
|
} else if (
|
||||||
|
(hasVModelValue || hasValue) &&
|
||||||
|
!shallowEqualObjects(component.proxy, 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.formulateValue && typeof this.formulateValue === 'object') ||
|
||||||
|
(this.values && typeof this.values === 'object') ||
|
||||||
|
(this.isGrouping && typeof this.context.model[this.index] === 'object')
|
||||||
|
)
|
||||||
|
},
|
||||||
|
isVmodeled () {
|
||||||
|
return !!(this.$options.propsData.hasOwnProperty('formulateValue') &&
|
||||||
|
this._events &&
|
||||||
|
Array.isArray(this._events.input) &&
|
||||||
|
this._events.input.length)
|
||||||
|
},
|
||||||
|
initialValues () {
|
||||||
|
if (
|
||||||
|
has(this.$options.propsData, 'formulateValue') &&
|
||||||
|
typeof this.formulateValue === 'object'
|
||||||
|
) {
|
||||||
|
// If there is a v-model on the form/group, use those values as first priority
|
||||||
|
return Object.assign({}, this.formulateValue) // @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 {
|
||||||
|
Object.assign(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(values[field], this.proxy[field]) &&
|
||||||
|
!shallowEqualObjects(values[field], this.registry.get(field).proxy)
|
||||||
|
) {
|
||||||
|
this.setFieldValue(field, values[field])
|
||||||
|
this.registry.get(field).context.model = values[field]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
formulateSetter: ctx.setFieldValue,
|
||||||
|
formulateRegister: ctx.register,
|
||||||
|
formulateDeregister: ctx.deregister,
|
||||||
|
getFormValues: ctx.getFormValues
|
||||||
|
}
|
||||||
|
}
|
@ -147,6 +147,9 @@ export default {
|
|||||||
*/
|
*/
|
||||||
matches: function ({ value }, ...stack) {
|
matches: function ({ value }, ...stack) {
|
||||||
return Promise.resolve(!!stack.find(pattern => {
|
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))
|
||||||
|
}
|
||||||
if (pattern instanceof RegExp) {
|
if (pattern instanceof RegExp) {
|
||||||
return pattern.test(value)
|
return pattern.test(value)
|
||||||
}
|
}
|
||||||
@ -278,5 +281,12 @@ export default {
|
|||||||
*/
|
*/
|
||||||
url: function ({ value }) {
|
url: function ({ value }) {
|
||||||
return Promise.resolve(isUrl(value))
|
return Promise.resolve(isUrl(value))
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rule: not a true rule — more like a compiler flag.
|
||||||
|
*/
|
||||||
|
bail: function () {
|
||||||
|
return Promise.resolve(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,19 +106,19 @@ function parseRule (rule, rules) {
|
|||||||
}
|
}
|
||||||
if (Array.isArray(rule) && rule.length) {
|
if (Array.isArray(rule) && rule.length) {
|
||||||
rule = rule.map(r => r) // light clone
|
rule = rule.map(r => r) // light clone
|
||||||
const ruleName = snakeToCamel(rule.shift())
|
const [ruleName, modifier] = parseModifier(rule.shift())
|
||||||
if (typeof ruleName === 'string' && rules.hasOwnProperty(ruleName)) {
|
if (typeof ruleName === 'string' && rules.hasOwnProperty(ruleName)) {
|
||||||
return [rules[ruleName], rule, ruleName]
|
return [rules[ruleName], rule, ruleName, modifier]
|
||||||
}
|
}
|
||||||
if (typeof ruleName === 'function') {
|
if (typeof ruleName === 'function') {
|
||||||
return [ruleName, rule, ruleName]
|
return [ruleName, rule, ruleName, modifier]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (typeof rule === 'string') {
|
if (typeof rule === 'string') {
|
||||||
const segments = rule.split(':')
|
const segments = rule.split(':')
|
||||||
const ruleName = snakeToCamel(segments.shift())
|
const [ruleName, modifier] = parseModifier(segments.shift())
|
||||||
if (rules.hasOwnProperty(ruleName)) {
|
if (rules.hasOwnProperty(ruleName)) {
|
||||||
return [rules[ruleName], segments.length ? segments.join(':').split(',') : [], ruleName]
|
return [rules[ruleName], segments.length ? segments.join(':').split(',') : [], ruleName, modifier]
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unknown validation rule ${rule}`)
|
throw new Error(`Unknown validation rule ${rule}`)
|
||||||
}
|
}
|
||||||
@ -126,6 +126,68 @@ function parseRule (rule, rules) {
|
|||||||
return false
|
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
|
||||||
|
* we would expect:
|
||||||
|
* [[required], [min], [max]]
|
||||||
|
* because any sub-array failure would cause a shutdown. While
|
||||||
|
* ^required|min:10|max:10
|
||||||
|
* would return:
|
||||||
|
* [[required], [min, max]]
|
||||||
|
* and no bailing would produce:
|
||||||
|
* [[required, min, max]]
|
||||||
|
* @param {array} rules
|
||||||
|
*/
|
||||||
|
export function groupBails (rules) {
|
||||||
|
const groups = []
|
||||||
|
const bailIndex = rules.findIndex(([,, rule]) => rule.toLowerCase() === 'bail')
|
||||||
|
if (bailIndex >= 0) {
|
||||||
|
// Get all the rules until the first bail rule (dont include the bail)
|
||||||
|
const preBail = rules.splice(0, bailIndex + 1).slice(0, -1)
|
||||||
|
// Rules before the `bail` rule are non-bailing
|
||||||
|
preBail.length && groups.push(preBail)
|
||||||
|
// All remaining rules are bailing rule groups
|
||||||
|
rules.map(rule => groups.push(Object.defineProperty([rule], 'bail', { value: true })))
|
||||||
|
} else {
|
||||||
|
groups.push(rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups.reduce((groups, group) => {
|
||||||
|
const splitByMod = (group, bailGroup = false) => {
|
||||||
|
if (group.length < 2) {
|
||||||
|
return Object.defineProperty([group], 'bail', { value: bailGroup })
|
||||||
|
}
|
||||||
|
const splits = []
|
||||||
|
const modIndex = group.findIndex(([,,, modifier]) => modifier === '^')
|
||||||
|
if (modIndex >= 0) {
|
||||||
|
const preMod = group.splice(0, modIndex)
|
||||||
|
// rules before the modifier are non-bailing rules.
|
||||||
|
preMod.length && splits.push(...splitByMod(preMod, bailGroup))
|
||||||
|
splits.push(Object.defineProperty([group.shift()], 'bail', { value: true }))
|
||||||
|
// rules after the modifier are non-bailing rules.
|
||||||
|
group.length && splits.push(...splitByMod(group, bailGroup))
|
||||||
|
} else {
|
||||||
|
splits.push(group)
|
||||||
|
}
|
||||||
|
return splits
|
||||||
|
}
|
||||||
|
return groups.concat(splitByMod(group))
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Escape a string for use in regular expressions.
|
* Escape a string for use in regular expressions.
|
||||||
* @param {string} string
|
* @param {string} string
|
||||||
@ -178,7 +240,11 @@ export function isValueType (data) {
|
|||||||
* case of needing to unbind reactive watchers.
|
* case of needing to unbind reactive watchers.
|
||||||
*/
|
*/
|
||||||
export function cloneDeep (obj) {
|
export function cloneDeep (obj) {
|
||||||
const newObj = {}
|
if (typeof obj !== 'object') {
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
const isArr = Array.isArray(obj)
|
||||||
|
const newObj = isArr ? [] : {}
|
||||||
for (const key in obj) {
|
for (const key in obj) {
|
||||||
if (obj[key] instanceof FileUpload || isValueType(obj[key])) {
|
if (obj[key] instanceof FileUpload || isValueType(obj[key])) {
|
||||||
newObj[key] = obj[key]
|
newObj[key] = obj[key]
|
||||||
@ -202,3 +268,18 @@ export function parseLocale (locale) {
|
|||||||
return options.length ? options : [segment]
|
return options.length ? options : [segment]
|
||||||
}, [])
|
}, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand for Object.prototype.hasOwnProperty.call (space saving)
|
||||||
|
*/
|
||||||
|
export function has (ctx, prop) {
|
||||||
|
return Object.prototype.hasOwnProperty.call(ctx, prop)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a registry object, map over it recursively entering groups.
|
||||||
|
* @param {Object} registry key => component
|
||||||
|
*/
|
||||||
|
export function mapRegistry (registry) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
26
src/slots/FormulateAddMore.vue
Normal file
26
src/slots/FormulateAddMore.vue
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<div class="formulate-input-group-add-more">
|
||||||
|
<FormulateInput
|
||||||
|
type="button"
|
||||||
|
:label="context.addLabel"
|
||||||
|
data-minor
|
||||||
|
data-ghost
|
||||||
|
@click="addMore"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
context: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
addMore: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
19
src/slots/FormulateHelp.vue
Normal file
19
src/slots/FormulateHelp.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="context.help"
|
||||||
|
:id="`${context.id}-help`"
|
||||||
|
:class="`formulate-input-help formulate-input-help--${context.helpPosition}`"
|
||||||
|
v-text="context.help"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
context: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
18
src/slots/FormulateLabel.vue
Normal file
18
src/slots/FormulateLabel.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<label
|
||||||
|
:class="`formulate-input-label formulate-input-label--${context.labelPosition}`"
|
||||||
|
:for="context.id"
|
||||||
|
v-text="context.label"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
context: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
33
src/slots/FormulateRepeatable.vue
Normal file
33
src/slots/FormulateRepeatable.vue
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="formulate-input-group-repeatable"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-if="context.repeatable"
|
||||||
|
class="formulate-input-group-repeatable-remove"
|
||||||
|
role="button"
|
||||||
|
@click.prevent="removeItem"
|
||||||
|
@keypress.enter="removeItem"
|
||||||
|
/>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
context: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
removeItem: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
index: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -62,17 +62,24 @@ describe('Formulate', () => {
|
|||||||
|
|
||||||
it('installs on vue instance', () => {
|
it('installs on vue instance', () => {
|
||||||
const components = [
|
const components = [
|
||||||
|
'FormulateSlot',
|
||||||
'FormulateForm',
|
'FormulateForm',
|
||||||
|
'FormulateHelp',
|
||||||
|
'FormulateLabel',
|
||||||
'FormulateInput',
|
'FormulateInput',
|
||||||
'FormulateErrors',
|
'FormulateErrors',
|
||||||
|
'FormulateAddMore',
|
||||||
|
'FormulateGrouping',
|
||||||
'FormulateInputBox',
|
'FormulateInputBox',
|
||||||
'FormulateInputText',
|
'FormulateInputText',
|
||||||
'FormulateInputFile',
|
'FormulateInputFile',
|
||||||
|
'FormulateRepeatable',
|
||||||
'FormulateInputGroup',
|
'FormulateInputGroup',
|
||||||
'FormulateInputButton',
|
'FormulateInputButton',
|
||||||
'FormulateInputSelect',
|
'FormulateInputSelect',
|
||||||
'FormulateInputSlider',
|
'FormulateInputSlider',
|
||||||
'FormulateInputTextArea'
|
'FormulateInputTextArea',
|
||||||
|
'FormulateRepeatableProvider'
|
||||||
]
|
]
|
||||||
const registry = []
|
const registry = []
|
||||||
function Vue () {}
|
function Vue () {}
|
||||||
|
@ -28,13 +28,11 @@ describe('FormulateForm', () => {
|
|||||||
const wrapper = mount(FormulateForm, {
|
const wrapper = mount(FormulateForm, {
|
||||||
slots: {
|
slots: {
|
||||||
default: "<button type='submit' />"
|
default: "<button type='submit' />"
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
formSubmitted
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const spy = jest.spyOn(wrapper.vm, 'formSubmitted')
|
||||||
wrapper.find('form').trigger('submit')
|
wrapper.find('form').trigger('submit')
|
||||||
expect(formSubmitted).toBeCalled()
|
expect(spy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('registers its subcomponents', () => {
|
it('registers its subcomponents', () => {
|
||||||
@ -42,7 +40,28 @@ describe('FormulateForm', () => {
|
|||||||
propsData: { formulateValue: { testinput: 'has initial value' } },
|
propsData: { formulateValue: { testinput: 'has initial value' } },
|
||||||
slots: { default: '<FormulateInput type="text" name="subinput1" /><FormulateInput type="checkbox" name="subinput2" />' }
|
slots: { default: '<FormulateInput type="text" name="subinput1" /><FormulateInput type="checkbox" name="subinput2" />' }
|
||||||
})
|
})
|
||||||
expect(Object.keys(wrapper.vm.registry)).toEqual(['subinput1', 'subinput2'])
|
expect(wrapper.vm.registry.keys()).toEqual(['subinput1', 'subinput2'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deregisters a subcomponents', async () => {
|
||||||
|
const wrapper = mount({
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
active: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<FormulateForm>
|
||||||
|
<FormulateInput v-if="active" type="text" name="subinput1" />
|
||||||
|
<FormulateInput type="checkbox" name="subinput2" />
|
||||||
|
</FormulateForm>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.findComponent(FormulateForm).vm.registry.keys()).toEqual(['subinput1', 'subinput2'])
|
||||||
|
wrapper.setData({ active: false })
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.findComponent(FormulateForm).vm.registry.keys()).toEqual(['subinput2'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can set a field’s initial value', async () => {
|
it('can set a field’s initial value', async () => {
|
||||||
@ -81,7 +100,7 @@ describe('FormulateForm', () => {
|
|||||||
propsData: { formulateValue: { box1: true } },
|
propsData: { formulateValue: { box1: true } },
|
||||||
slots: { default: '<FormulateInput type="checkbox" name="box1" />' }
|
slots: { default: '<FormulateInput type="checkbox" name="box1" />' }
|
||||||
})
|
})
|
||||||
expect(wrapper.find('input[type="checkbox"]').is(':checked')).toBe(true)
|
expect(wrapper.find('input[type="checkbox"]').element.checked).toBeTruthy()
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can set initial unchecked attribute on single checkboxes', () => {
|
it('can set initial unchecked attribute on single checkboxes', () => {
|
||||||
@ -89,7 +108,7 @@ describe('FormulateForm', () => {
|
|||||||
propsData: { formulateValue: { box1: false } },
|
propsData: { formulateValue: { box1: false } },
|
||||||
slots: { default: '<FormulateInput type="checkbox" name="box1" />' }
|
slots: { default: '<FormulateInput type="checkbox" name="box1" />' }
|
||||||
})
|
})
|
||||||
expect(wrapper.find('input[type="checkbox"]').is(':checked')).toBe(false)
|
expect(wrapper.find('input[type="checkbox"]').element.checked).toBeFalsy()
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can set checkbox initial value with options', async () => {
|
it('can set checkbox initial value with options', async () => {
|
||||||
@ -171,6 +190,26 @@ describe('FormulateForm', () => {
|
|||||||
expect(wrapper.emitted().input[wrapper.emitted().input.length - 1]).toEqual([{ testinput: 'override-data' }])
|
expect(wrapper.emitted().input[wrapper.emitted().input.length - 1]).toEqual([{ testinput: 'override-data' }])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('updates an inputs value when the form v-model is modified', async () => {
|
||||||
|
const wrapper = mount({
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
formValues: {
|
||||||
|
testinput: 'abcd',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<FormulateForm v-model="formValues">
|
||||||
|
<FormulateInput type="text" name="testinput" />
|
||||||
|
</FormulateForm>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
wrapper.vm.formValues = { testinput: '1234' }
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.find('input[type="text"]').element.value).toBe('1234')
|
||||||
|
})
|
||||||
|
|
||||||
it('emits an instance of FormSubmission', async () => {
|
it('emits an instance of FormSubmission', async () => {
|
||||||
const wrapper = mount(FormulateForm, {
|
const wrapper = mount(FormulateForm, {
|
||||||
@ -205,7 +244,6 @@ describe('FormulateForm', () => {
|
|||||||
slots: { default: `<FormulateInput type="text" name="name" validation="required" /><FormulateInput type="checkbox" name="candy" />` }
|
slots: { default: `<FormulateInput type="text" name="name" validation="required" /><FormulateInput type="checkbox" name="candy" />` }
|
||||||
})
|
})
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
// expect(wrapper.vm.internalFormModelProxy).toEqual({ name: 'Dave Barnett', candy: true })
|
|
||||||
expect(wrapper.find('input[type="text"]').element.value).toBe('Dave Barnett')
|
expect(wrapper.find('input[type="text"]').element.value).toBe('Dave Barnett')
|
||||||
expect(wrapper.find('input[type="checkbox"]').element.checked).toBe(true)
|
expect(wrapper.find('input[type="checkbox"]').element.checked).toBe(true)
|
||||||
})
|
})
|
||||||
@ -229,6 +267,47 @@ describe('FormulateForm', () => {
|
|||||||
expect(wrapper.vm.$formulate.registry.get('login')).toBe(wrapper.vm)
|
expect(wrapper.vm.$formulate.registry.get('login')).toBe(wrapper.vm)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('calls custom error handler with error and name', async () => {
|
||||||
|
const mockHandler = jest.fn((err, name) => err);
|
||||||
|
const wrapper = mount({
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
|
<FormulateForm
|
||||||
|
name="login"
|
||||||
|
/>
|
||||||
|
<FormulateForm
|
||||||
|
name="register"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
wrapper.vm.$formulate.extend({ errorHandler: mockHandler })
|
||||||
|
wrapper.vm.$formulate.handle({ formErrors: ['This is an error message'] }, 'login')
|
||||||
|
expect(mockHandler.mock.calls.length).toBe(1);
|
||||||
|
expect(mockHandler.mock.calls[0]).toEqual([{ formErrors: ['This is an error message'] }, 'login']);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('errors are displayed on correctly named components', async () => {
|
||||||
|
const wrapper = mount({
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
|
<FormulateForm
|
||||||
|
name="login"
|
||||||
|
/>
|
||||||
|
<FormulateForm
|
||||||
|
name="register"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
expect(wrapper.vm.$formulate.registry.has('login') && wrapper.vm.$formulate.registry.has('register')).toBe(true)
|
||||||
|
wrapper.vm.$formulate.handle({ formErrors: ['This is an error message'] }, 'login')
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.findAll('.formulate-form').length).toBe(2)
|
||||||
|
expect(wrapper.find('.formulate-form--login .formulate-form-errors').exists()).toBe(true)
|
||||||
|
expect(wrapper.find('.formulate-form--register .formulate-form-errors').exists()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
it('errors are displayed on correctly named components', async () => {
|
it('errors are displayed on correctly named components', async () => {
|
||||||
const wrapper = mount({
|
const wrapper = mount({
|
||||||
template: `
|
template: `
|
||||||
@ -266,7 +345,7 @@ describe('FormulateForm', () => {
|
|||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.findAll('.formulate-form-errors').length).toBe(1)
|
expect(wrapper.findAll('.formulate-form-errors').length).toBe(1)
|
||||||
// Ensure that we moved the position of the errors
|
// Ensure that we moved the position of the errors
|
||||||
expect(wrapper.find('h1 + *').is('.formulate-form-errors')).toBe(true)
|
expect(wrapper.find('h1 + *').element.classList.contains('formulate-form-errors')).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('allows rendering multiple locations', async () => {
|
it('allows rendering multiple locations', async () => {
|
||||||
@ -383,10 +462,10 @@ describe('FormulateForm', () => {
|
|||||||
`
|
`
|
||||||
})
|
})
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.find(FormulateForm).vm.errorObservers.length).toBe(1)
|
expect(wrapper.findComponent(FormulateForm).vm.errorObservers.length).toBe(1)
|
||||||
wrapper.setData({ hasField: false })
|
wrapper.setData({ hasField: false })
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.find(FormulateForm).vm.errorObservers.length).toBe(0)
|
expect(wrapper.findComponent(FormulateForm).vm.errorObservers.length).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('emits correct validation event on entry', async () => {
|
it('emits correct validation event on entry', async () => {
|
||||||
@ -434,4 +513,88 @@ describe('FormulateForm', () => {
|
|||||||
hasErrors: false
|
hasErrors: false
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('removes field data when that field is de-registered', async () => {
|
||||||
|
const wrapper = mount({
|
||||||
|
template: `
|
||||||
|
<FormulateForm
|
||||||
|
v-model="formData"
|
||||||
|
>
|
||||||
|
<FormulateInput type="text" name="foo" value="abc123" />
|
||||||
|
<FormulateInput type="checkbox" name="bar" v-if="formData.foo !== 'bar'" :value="1" />
|
||||||
|
</FormulateForm>
|
||||||
|
`,
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
formData: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
wrapper.find('input[type="text"]').setValue('bar')
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.findComponent(FormulateForm).vm.proxy).toEqual({ foo: 'bar' })
|
||||||
|
expect(wrapper.vm.formData).toEqual({ foo: 'bar' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('it allows the removal of properties in proxy.', async () => {
|
||||||
|
const wrapper = mount({
|
||||||
|
template: `
|
||||||
|
<FormulateForm
|
||||||
|
v-model="formData"
|
||||||
|
name="login"
|
||||||
|
ref="form"
|
||||||
|
>
|
||||||
|
<FormulateInput type="text" name="username" validation="required" v-model="username" />
|
||||||
|
<FormulateInput type="password" name="password" validation="required|min:4,length" />
|
||||||
|
</FormulateForm>
|
||||||
|
`,
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
formData: {},
|
||||||
|
username: undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
wrapper.find('input[type="text"]').setValue('foo')
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.vm.username).toEqual('foo')
|
||||||
|
expect(wrapper.vm.formData).toEqual({ username: 'foo' })
|
||||||
|
wrapper.vm.$refs.form.setValues({})
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.vm.formData).toEqual({ username: '' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('it allows resetting a form, hiding validation and clearing inputs.', async () => {
|
||||||
|
const wrapper = mount({
|
||||||
|
template: `
|
||||||
|
<FormulateForm
|
||||||
|
v-model="formData"
|
||||||
|
name="login"
|
||||||
|
>
|
||||||
|
<FormulateInput type="text" name="username" validation="required" />
|
||||||
|
<FormulateInput type="password" name="password" validation="required|min:4,length" />
|
||||||
|
</FormulateForm>
|
||||||
|
`,
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
formData: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const password = wrapper.find('input[type="password"]')
|
||||||
|
password.setValue('foo')
|
||||||
|
password.trigger('blur')
|
||||||
|
wrapper.find('form').trigger('submit')
|
||||||
|
wrapper.vm.$formulate.handle({
|
||||||
|
inputErrors: { username: ['Failed'] }
|
||||||
|
}, 'login')
|
||||||
|
await flushPromises()
|
||||||
|
// First make sure we showed the errors
|
||||||
|
expect(wrapper.findAll('.formulate-input-error').length).toBe(3)
|
||||||
|
wrapper.vm.$formulate.reset('login')
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.findAll('.formulate-input-error').length).toBe(0)
|
||||||
|
expect(wrapper.vm.formData).toEqual({})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import flushPromises from 'flush-promises'
|
import flushPromises from 'flush-promises'
|
||||||
import { mount } from '@vue/test-utils'
|
import { mount, createLocalVue } from '@vue/test-utils'
|
||||||
import Formulate from '@/Formulate.js'
|
import Formulate from '@/Formulate.js'
|
||||||
|
import FormulateForm from '@/FormulateForm.vue'
|
||||||
import FormulateInput from '@/FormulateInput.vue'
|
import FormulateInput from '@/FormulateInput.vue'
|
||||||
import FormulateInputBox from '@/inputs/FormulateInputBox.vue'
|
import FormulateInputBox from '@/inputs/FormulateInputBox.vue'
|
||||||
|
|
||||||
@ -113,7 +114,7 @@ describe('FormulateInput', () => {
|
|||||||
value: 'bar'
|
value: 'bar'
|
||||||
} })
|
} })
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.contains(FormulateInputBox)).toBe(true)
|
expect(wrapper.findComponent(FormulateInputBox).exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('emits correct validation event', async () => {
|
it('emits correct validation event', async () => {
|
||||||
@ -177,4 +178,142 @@ describe('FormulateInput', () => {
|
|||||||
expect(wrapper.emitted('error-visibility').length).toBe(1)
|
expect(wrapper.emitted('error-visibility').length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('allows overriding the label default slot component', async () => {
|
||||||
|
const localVue = createLocalVue()
|
||||||
|
localVue.component('CustomLabel', {
|
||||||
|
render: function (h) {
|
||||||
|
return h('div', { class: 'custom-label' }, [`custom: ${this.context.label}`])
|
||||||
|
},
|
||||||
|
props: ['context']
|
||||||
|
})
|
||||||
|
localVue.use(Formulate, { slotComponents: { label: 'CustomLabel' } })
|
||||||
|
const wrapper = mount(FormulateInput, { localVue, propsData: { label: 'My label here' } })
|
||||||
|
expect(wrapper.find('.custom-label').html()).toBe('<div class="custom-label">custom: My label here</div>')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows overriding the help default slot component', async () => {
|
||||||
|
const localVue = createLocalVue()
|
||||||
|
localVue.component('CustomHelp', {
|
||||||
|
render: function (h) {
|
||||||
|
return h('small', { class: 'custom-help' }, [`custom: ${this.context.help}`])
|
||||||
|
},
|
||||||
|
props: ['context']
|
||||||
|
})
|
||||||
|
localVue.use(Formulate, { slotComponents: { help: 'CustomHelp' } })
|
||||||
|
const wrapper = mount(FormulateInput, { localVue, propsData: { help: 'My help here' } })
|
||||||
|
expect(wrapper.find('.custom-help').html()).toBe('<small class="custom-help">custom: My help here</small>')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows overriding the errors component', async () => {
|
||||||
|
const localVue = createLocalVue()
|
||||||
|
localVue.component('CustomErrors', {
|
||||||
|
render: function (h) {
|
||||||
|
return h('ul', { class: 'my-errors' }, this.context.visibleValidationErrors.map(message => h('li', message)))
|
||||||
|
},
|
||||||
|
props: ['context']
|
||||||
|
})
|
||||||
|
localVue.use(Formulate, { slotComponents: { errors: 'CustomErrors' } })
|
||||||
|
const wrapper = mount(FormulateInput, { localVue, propsData: {
|
||||||
|
help: 'My help here',
|
||||||
|
errorBehavior: 'live',
|
||||||
|
validation: 'required'
|
||||||
|
} })
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.find('.my-errors').html())
|
||||||
|
.toBe(`<ul class="my-errors">\n <li>Text is required.</li>\n</ul>`)
|
||||||
|
// Clean up after this call — we should probably get rid of the singleton all together....
|
||||||
|
Formulate.extend({ slotComponents: { errors: 'FormulateErrors' }})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('links help text with `aria-describedby`', async () => {
|
||||||
|
const wrapper = mount(FormulateInput, {
|
||||||
|
propsData: {
|
||||||
|
type: 'text',
|
||||||
|
validation: 'required',
|
||||||
|
errorBehavior: 'live',
|
||||||
|
value: 'bar',
|
||||||
|
help: 'Some help text'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
const id = `${wrapper.vm.context.id}-help`
|
||||||
|
expect(wrapper.find('input').attributes('aria-describedby')).toBe(id)
|
||||||
|
expect(wrapper.find('.formulate-input-help').attributes().id).toBe(id)
|
||||||
|
});
|
||||||
|
|
||||||
|
it('it does not use aria-describedby if there is no help text', async () => {
|
||||||
|
const wrapper = mount(FormulateInput, {
|
||||||
|
propsData: {
|
||||||
|
type: 'text',
|
||||||
|
validation: 'required',
|
||||||
|
errorBehavior: 'live',
|
||||||
|
value: 'bar',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.find('input').attributes('aria-describedby')).toBeFalsy()
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can bail on validation when encountering the bail rule', async () => {
|
||||||
|
const wrapper = mount(FormulateInput, {
|
||||||
|
propsData: { type: 'text', validation: 'bail|required|in:xyz', errorBehavior: 'live' }
|
||||||
|
})
|
||||||
|
await flushPromises();
|
||||||
|
expect(wrapper.vm.context.visibleValidationErrors.length).toBe(1);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can show multiple validation errors if they occur before the bail rule', async () => {
|
||||||
|
const wrapper = mount(FormulateInput, {
|
||||||
|
propsData: { type: 'text', validation: 'required|in:xyz|bail', errorBehavior: 'live' }
|
||||||
|
})
|
||||||
|
await flushPromises();
|
||||||
|
expect(wrapper.vm.context.visibleValidationErrors.length).toBe(2);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can avoid bail behavior by using modifier', async () => {
|
||||||
|
const wrapper = mount(FormulateInput, {
|
||||||
|
propsData: { type: 'text', validation: '^required|in:xyz|min:10,length', errorBehavior: 'live', value: '123' }
|
||||||
|
})
|
||||||
|
await flushPromises();
|
||||||
|
expect(wrapper.vm.context.visibleValidationErrors.length).toBe(2);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('prevents later error messages when modified rule fails', async () => {
|
||||||
|
const wrapper = mount(FormulateInput, {
|
||||||
|
propsData: { type: 'text', validation: '^required|in:xyz|min:10,length', errorBehavior: 'live' }
|
||||||
|
})
|
||||||
|
await flushPromises();
|
||||||
|
expect(wrapper.vm.context.visibleValidationErrors.length).toBe(1);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can bail in the middle of the rule set with a modifier', async () => {
|
||||||
|
const wrapper = mount(FormulateInput, {
|
||||||
|
propsData: { type: 'text', validation: 'required|^in:xyz|min:10,length', errorBehavior: 'live' }
|
||||||
|
})
|
||||||
|
await flushPromises();
|
||||||
|
expect(wrapper.vm.context.visibleValidationErrors.length).toBe(2);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not show errors on blur when set error-behavior is submit', async () => {
|
||||||
|
const wrapper = mount(FormulateInput, { propsData: {
|
||||||
|
type: 'text',
|
||||||
|
validation: 'required',
|
||||||
|
errorBehavior: 'submit',
|
||||||
|
} })
|
||||||
|
wrapper.find('input').trigger('input')
|
||||||
|
wrapper.find('input').trigger('blur')
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.find('.formulate-input-errors').exists()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays errors when error-behavior is submit and form is submitted', async () => {
|
||||||
|
const wrapper = mount(FormulateForm, {
|
||||||
|
slots: {
|
||||||
|
default: `<FormulateInput error-behavior="submit" validation="required" />`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
wrapper.trigger('submit')
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.find('.formulate-input-errors').exists()).toBe(true)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -4,19 +4,29 @@ import { mount } from '@vue/test-utils'
|
|||||||
import Formulate from '../../src/Formulate.js'
|
import Formulate from '../../src/Formulate.js'
|
||||||
import FormulateInput from '@/FormulateInput.vue'
|
import FormulateInput from '@/FormulateInput.vue'
|
||||||
import FormulateInputBox from '@/inputs/FormulateInputBox.vue'
|
import FormulateInputBox from '@/inputs/FormulateInputBox.vue'
|
||||||
import FormulateInputGroup from '@/FormulateInputGroup.vue'
|
import FormulateInputGroup from '@/inputs/FormulateInputGroup.vue'
|
||||||
|
|
||||||
Vue.use(Formulate)
|
Vue.use(Formulate)
|
||||||
|
|
||||||
describe('FormulateInputBox', () => {
|
describe('FormulateInputBox', () => {
|
||||||
it('renders a box element when type "checkbox" ', () => {
|
it('renders a box element when type "checkbox" ', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox' } })
|
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox' } })
|
||||||
expect(wrapper.contains(FormulateInputBox)).toBe(true)
|
expect(wrapper.findComponent(FormulateInputBox).exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders a box element when type "radio"', () => {
|
it('renders a box element when type "radio"', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: { type: 'radio' } })
|
const wrapper = mount(FormulateInput, { propsData: { type: 'radio' } })
|
||||||
expect(wrapper.contains(FormulateInputBox)).toBe(true)
|
expect(wrapper.findComponent(FormulateInputBox).exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('passes an explicitly given name prop through to the root radio elements', () => {
|
||||||
|
const wrapper = mount(FormulateInput, { propsData: { type: 'radio', name: 'foo', options: {a: '1', b: '2'} } })
|
||||||
|
expect(wrapper.findAll('input[name="foo"]')).toHaveLength(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('passes an explicitly given name prop through to the root checkbox elements', () => {
|
||||||
|
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', name: 'foo', options: {a: '1', b: '2'} } })
|
||||||
|
expect(wrapper.findAll('input[name="foo"]')).toHaveLength(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('box inputs properly process options object in context library', () => {
|
it('box inputs properly process options object in context library', () => {
|
||||||
@ -26,12 +36,12 @@ describe('FormulateInputBox', () => {
|
|||||||
|
|
||||||
it('renders a group when type "checkbox" with options', () => {
|
it('renders a group when type "checkbox" with options', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'} } })
|
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'} } })
|
||||||
expect(wrapper.contains(FormulateInputGroup)).toBe(true)
|
expect(wrapper.findComponent(FormulateInputGroup).exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders a group when type "radio" with options', () => {
|
it('renders a group when type "radio" with options', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'} } })
|
const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'} } })
|
||||||
expect(wrapper.contains(FormulateInputGroup)).toBe(true)
|
expect(wrapper.findComponent(FormulateInputGroup).exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('defaults labelPosition to "after" when type "checkbox"', () => {
|
it('defaults labelPosition to "after" when type "checkbox"', () => {
|
||||||
@ -52,7 +62,7 @@ describe('FormulateInputBox', () => {
|
|||||||
|
|
||||||
it('generates ids if not provided when type "radio"', () => {
|
it('generates ids if not provided when type "radio"', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'} } })
|
const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'} } })
|
||||||
expect(wrapper.findAll('input[type="radio"]').is('[id]')).toBe(true)
|
expect(wrapper.find('input[type="radio"]').attributes().id).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('additional context does not bleed through to attributes with type "radio" and options', () => {
|
it('additional context does not bleed through to attributes with type "radio" and options', () => {
|
||||||
@ -72,14 +82,14 @@ describe('FormulateInputBox', () => {
|
|||||||
|
|
||||||
it('does not use the value attribute to be checked', () => {
|
it('does not use the value attribute to be checked', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', value: '123' } })
|
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', value: '123' } })
|
||||||
expect(wrapper.find('input').is(':checked')).toBe(false)
|
expect(wrapper.find('input').element.checked).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses the checked attribute to be checked', async () => {
|
it('uses the checked attribute to be checked', async () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', checked: 'true' } })
|
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', checked: 'true' } })
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
expect(wrapper.find('input').is(':checked')).toBe(true)
|
expect(wrapper.find('input').element.checked).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses the value attribute to select "type" radio when using options', async () => {
|
it('uses the value attribute to select "type" radio when using options', async () => {
|
||||||
@ -201,4 +211,18 @@ describe('FormulateInputBox', () => {
|
|||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.find('.formulate-input-error').exists()).toBe(true)
|
expect(wrapper.find('.formulate-input-error').exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('renders no boxes when options array is empty', async () => {
|
||||||
|
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: [] } })
|
||||||
|
expect(wrapper.findComponent(FormulateInputGroup).exists()).toBe(true)
|
||||||
|
expect(wrapper.find('input[type="checkbox"]').exists()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders multiple labels both with correct id', async () => {
|
||||||
|
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', label: 'VueFormulate FTW!'} })
|
||||||
|
const id = wrapper.find('input[type="checkbox"]').attributes('id')
|
||||||
|
const labelIds = wrapper.findAll('label').wrappers.map(label => label.attributes('for'));
|
||||||
|
expect(labelIds.length).toBe(2);
|
||||||
|
expect(labelIds.filter(labelId => labelId === id).length).toBe(2);
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,22 +1,25 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
import flushPromises from 'flush-promises'
|
||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import Formulate from '../../src/Formulate.js'
|
import Formulate from '@/Formulate.js'
|
||||||
import FormulateInput from '@/FormulateInput.vue'
|
import FormulateInput from '@/FormulateInput.vue'
|
||||||
import FormulateInputButton from '@/inputs/FormulateInputButton.vue'
|
import FormulateInputButton from '@/inputs/FormulateInputButton.vue'
|
||||||
|
|
||||||
Vue.use(Formulate)
|
Vue.use(Formulate)
|
||||||
|
|
||||||
test('type "button" renders a button element', () => {
|
describe('FormulateInputButton', () => {
|
||||||
|
|
||||||
|
it('renders a button element', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: { type: 'button' } })
|
const wrapper = mount(FormulateInput, { propsData: { type: 'button' } })
|
||||||
expect(wrapper.contains(FormulateInputButton)).toBe(true)
|
expect(wrapper.findComponent(FormulateInputButton).exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('type "submit" renders a button element', () => {
|
it('renders a button element when type submit', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: { type: 'submit' } })
|
const wrapper = mount(FormulateInput, { propsData: { type: 'submit' } })
|
||||||
expect(wrapper.contains(FormulateInputButton)).toBe(true)
|
expect(wrapper.findComponent(FormulateInputButton).exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('type "button" uses value as highest priority content', () => {
|
it('uses value as highest priority content', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: {
|
const wrapper = mount(FormulateInput, { propsData: {
|
||||||
type: 'submit',
|
type: 'submit',
|
||||||
value: 'Value content',
|
value: 'Value content',
|
||||||
@ -24,49 +27,49 @@ test('type "button" uses value as highest priority content', () => {
|
|||||||
name: 'Name content'
|
name: 'Name content'
|
||||||
}})
|
}})
|
||||||
expect(wrapper.find('button').text()).toBe('Value content')
|
expect(wrapper.find('button').text()).toBe('Value content')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('type "button" uses label as second highest priority content', () => {
|
it('uses label as second highest priority content', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: {
|
const wrapper = mount(FormulateInput, { propsData: {
|
||||||
type: 'submit',
|
type: 'submit',
|
||||||
label: 'Label content',
|
label: 'Label content',
|
||||||
name: 'Name content'
|
name: 'Name content'
|
||||||
}})
|
}})
|
||||||
expect(wrapper.find('button').text()).toBe('Label content')
|
expect(wrapper.find('button').text()).toBe('Label content')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('type "button" uses name as lowest priority content', () => {
|
it('uses name as lowest priority content', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: {
|
const wrapper = mount(FormulateInput, { propsData: {
|
||||||
type: 'submit',
|
type: 'submit',
|
||||||
name: 'Name content'
|
name: 'Name content'
|
||||||
}})
|
}})
|
||||||
expect(wrapper.find('button').text()).toBe('Name content')
|
expect(wrapper.find('button').text()).toBe('Name content')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('type "button" uses "Submit" as default content', () => {
|
it('uses "Submit" as default content', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: {
|
const wrapper = mount(FormulateInput, { propsData: {
|
||||||
type: 'submit',
|
type: 'submit',
|
||||||
}})
|
}})
|
||||||
expect(wrapper.find('button').text()).toBe('Submit')
|
expect(wrapper.find('button').text()).toBe('Submit')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('type "button" with label does not render label element', () => {
|
it('with label does not render label element', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: {
|
const wrapper = mount(FormulateInput, { propsData: {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
label: 'my label'
|
label: 'my label'
|
||||||
}})
|
}})
|
||||||
expect(wrapper.find('label').exists()).toBe(false)
|
expect(wrapper.find('label').exists()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('type "submit" with label does not render label element', () => {
|
it('does not render label element when type "submit"', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: {
|
const wrapper = mount(FormulateInput, { propsData: {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
label: 'my label'
|
label: 'my label'
|
||||||
}})
|
}})
|
||||||
expect(wrapper.find('label').exists()).toBe(false)
|
expect(wrapper.find('label').exists()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('type "button" renders slot inside button', () => {
|
it('renders slot inside button when type "button"', () => {
|
||||||
const wrapper = mount(FormulateInput, {
|
const wrapper = mount(FormulateInput, {
|
||||||
propsData: {
|
propsData: {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
@ -77,4 +80,36 @@ test('type "button" renders slot inside button', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
expect(wrapper.find('button > span').html()).toBe('<span>My custom slot</span>')
|
expect(wrapper.find('button > span').html()).toBe('<span>My custom slot</span>')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits a click event when the button itself is clicked', async () => {
|
||||||
|
const handle = jest.fn();
|
||||||
|
const wrapper = mount({
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
|
<FormulateInput
|
||||||
|
type="submit"
|
||||||
|
@click="handle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
methods: {
|
||||||
|
handle
|
||||||
|
}
|
||||||
|
})
|
||||||
|
wrapper.find('button[type="submit"]').trigger('click')
|
||||||
|
await flushPromises();
|
||||||
|
expect(handle.mock.calls.length).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('passes an explicitly given name prop through to the root element', () => {
|
||||||
|
const wrapper = mount(FormulateInput, { propsData: { type: 'button', name: 'foo' } })
|
||||||
|
expect(wrapper.find('button[name="foo"]').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('additional context does not bleed through to button input attributes', () => {
|
||||||
|
const wrapper = mount(FormulateInput, { propsData: { type: 'button' } } )
|
||||||
|
expect(Object.keys(wrapper.find('button').attributes())).toEqual(["type", "id"])
|
||||||
|
})
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import flushPromises from 'flush-promises'
|
import flushPromises from 'flush-promises'
|
||||||
import Formulate from '../../src/Formulate.js'
|
import Formulate from '@/Formulate.js'
|
||||||
import FileUpload from '../../src/FileUpload.js'
|
import FileUpload from '@/FileUpload.js'
|
||||||
import FormulateInput from '@/FormulateInput.vue'
|
import FormulateInput from '@/FormulateInput.vue'
|
||||||
import FormulateInputFile from '@/inputs/FormulateInputFile.vue'
|
import FormulateInputFile from '@/inputs/FormulateInputFile.vue'
|
||||||
|
|
||||||
@ -12,12 +12,12 @@ describe('FormulateInputFile', () => {
|
|||||||
|
|
||||||
it('type "file" renders a file element', () => {
|
it('type "file" renders a file element', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: { type: 'file' } })
|
const wrapper = mount(FormulateInput, { propsData: { type: 'file' } })
|
||||||
expect(wrapper.contains(FormulateInputFile)).toBe(true)
|
expect(wrapper.findComponent(FormulateInputFile).exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('type "image" renders a file element', () => {
|
it('type "image" renders a file element', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: { type: 'image' } })
|
const wrapper = mount(FormulateInput, { propsData: { type: 'image' } })
|
||||||
expect(wrapper.contains(FormulateInputFile)).toBe(true)
|
expect(wrapper.findComponent(FormulateInputFile).exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('forces an error-behavior live mode when upload-behavior is live and it has content', () => {
|
it('forces an error-behavior live mode when upload-behavior is live and it has content', () => {
|
||||||
@ -37,6 +37,16 @@ describe('FormulateInputFile', () => {
|
|||||||
expect(file.attributes('data-has-preview')).toBe('true')
|
expect(file.attributes('data-has-preview')).toBe('true')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('passes an explicitly given name prop through to the root element', () => {
|
||||||
|
const wrapper = mount(FormulateInput, { propsData: { type: 'image', name: 'foo' } })
|
||||||
|
expect(wrapper.find('input[name="foo"]').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('additional context does not bleed through to file input attributes', () => {
|
||||||
|
const wrapper = mount(FormulateInput, { propsData: { type: 'image' } } )
|
||||||
|
expect(Object.keys(wrapper.find('input[type="file"]').attributes())).toEqual(["type", "id"])
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ===========================================================================
|
* ===========================================================================
|
||||||
* Currently there appears to be no way to properly mock upload data in
|
* Currently there appears to be no way to properly mock upload data in
|
||||||
|
443
test/unit/FormulateInputGroup.test.js
Normal file
443
test/unit/FormulateInputGroup.test.js
Normal file
@ -0,0 +1,443 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import flushPromises from 'flush-promises'
|
||||||
|
import Formulate from '@/Formulate.js'
|
||||||
|
import FileUpload from '@/FileUpload.js'
|
||||||
|
import FormulateInput from '@/FormulateInput.vue'
|
||||||
|
import FormulateForm from '@/FormulateForm.vue'
|
||||||
|
import FormulateGrouping from '@/FormulateGrouping.vue'
|
||||||
|
import FormulateRepeatableProvider from '@/FormulateRepeatableProvider.vue'
|
||||||
|
|
||||||
|
Vue.use(Formulate)
|
||||||
|
|
||||||
|
describe('FormulateInputGroup', () => {
|
||||||
|
it('allows nested fields to be sub-rendered', async () => {
|
||||||
|
const wrapper = mount(FormulateInput, {
|
||||||
|
propsData: { type: 'group' },
|
||||||
|
slots: {
|
||||||
|
default: '<FormulateInput type="text" />'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(wrapper.findAll('.formulate-input-group-repeatable input[type="text"]').length).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('registers sub-fields with grouping', async () => {
|
||||||
|
const wrapper = mount(FormulateInput, {
|
||||||
|
propsData: { type: 'group' },
|
||||||
|
slots: {
|
||||||
|
default: '<FormulateInput type="text" name="persona" />'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(wrapper.findComponent(FormulateRepeatableProvider).vm.registry.has('persona')).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('is not repeatable by default', async () => {
|
||||||
|
const wrapper = mount(FormulateInput, {
|
||||||
|
propsData: { type: 'group' },
|
||||||
|
slots: {
|
||||||
|
default: '<FormulateInput type="text" />'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(wrapper.findAll('.formulate-input-group-add-more').length).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds an add more button when repeatable', async () => {
|
||||||
|
const wrapper = mount(FormulateInput, {
|
||||||
|
propsData: { type: 'group', repeatable: true },
|
||||||
|
slots: {
|
||||||
|
default: '<FormulateInput type="text" />'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(wrapper.findAll('.formulate-input-group-add-more').length).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('repeats the default slot when adding more', async () => {
|
||||||
|
const wrapper = mount(FormulateInput, {
|
||||||
|
propsData: { type: 'group', repeatable: true },
|
||||||
|
slots: {
|
||||||
|
default: '<div class="wrap"><FormulateInput type="text" /></div>'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
wrapper.find('.formulate-input-group-add-more button').trigger('click')
|
||||||
|
await flushPromises();
|
||||||
|
expect(wrapper.findAll('.wrap').length).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('re-hydrates a repeatable field', async () => {
|
||||||
|
const wrapper = mount(FormulateInput, {
|
||||||
|
propsData: { type: 'group', repeatable: true, value: [{email: 'jon@example.com'}, {email:'jane@example.com'}] },
|
||||||
|
slots: {
|
||||||
|
default: '<div class="wrap"><FormulateInput type="text" name="email" /></div>'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
const fields = wrapper.findAll('input[type="text"]')
|
||||||
|
expect(fields.length).toBe(2)
|
||||||
|
expect(fields.at(0).element.value).toBe('jon@example.com')
|
||||||
|
expect(fields.at(1).element.value).toBe('jane@example.com')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('v-modeling a subfield changes all values', async () => {
|
||||||
|
const wrapper = mount({
|
||||||
|
template: `
|
||||||
|
<FormulateInput
|
||||||
|
v-model="users"
|
||||||
|
type="group"
|
||||||
|
>
|
||||||
|
<FormulateInput type="text" v-model="email" name="email" />
|
||||||
|
<FormulateInput type="text" name="name" />
|
||||||
|
</FormulateInput>
|
||||||
|
`,
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
users: [{email: 'jon@example.com'}, {email:'jane@example.com'}],
|
||||||
|
email: 'jim@example.com'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
const fields = wrapper.findAll('input[type="text"]')
|
||||||
|
expect(fields.length).toBe(4)
|
||||||
|
expect(fields.at(0).element.value).toBe('jim@example.com')
|
||||||
|
expect(fields.at(2).element.value).toBe('jim@example.com')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('v-modeling a subfield updates group v-model value', async () => {
|
||||||
|
const wrapper = mount({
|
||||||
|
template: `
|
||||||
|
<FormulateInput
|
||||||
|
v-model="users"
|
||||||
|
type="group"
|
||||||
|
>
|
||||||
|
<FormulateInput type="text" v-model="email" name="email" />
|
||||||
|
<FormulateInput type="text" name="name" />
|
||||||
|
</FormulateInput>
|
||||||
|
`,
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
users: [{email: 'jon@example.com'}, {email:'jane@example.com'}],
|
||||||
|
email: 'jim@example.com'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.vm.users).toEqual([{email: 'jim@example.com'}, {email:'jim@example.com'}])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('prevents form submission when children have validation errors', async () => {
|
||||||
|
const submit = jest.fn()
|
||||||
|
const wrapper = mount({
|
||||||
|
template: `
|
||||||
|
<FormulateForm
|
||||||
|
@submit="submit"
|
||||||
|
>
|
||||||
|
<FormulateInput
|
||||||
|
type="text"
|
||||||
|
validation="required"
|
||||||
|
value="testing123"
|
||||||
|
name="name"
|
||||||
|
/>
|
||||||
|
<FormulateInput
|
||||||
|
v-model="users"
|
||||||
|
type="group"
|
||||||
|
>
|
||||||
|
<FormulateInput type="text" name="email" />
|
||||||
|
<FormulateInput type="text" name="name" validation="required" />
|
||||||
|
</FormulateInput>
|
||||||
|
<FormulateInput type="submit" />
|
||||||
|
</FormulateForm>
|
||||||
|
`,
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
users: [{email: 'jon@example.com'}, {email:'jane@example.com'}],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
submit
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const form = wrapper.findComponent(FormulateForm)
|
||||||
|
await form.vm.formSubmitted()
|
||||||
|
expect(submit.mock.calls.length).toBe(0);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows form submission with children when there are no validation errors', async () => {
|
||||||
|
const submit = jest.fn()
|
||||||
|
const wrapper = mount({
|
||||||
|
template: `
|
||||||
|
<FormulateForm
|
||||||
|
@submit="submit"
|
||||||
|
>
|
||||||
|
<FormulateInput
|
||||||
|
type="text"
|
||||||
|
validation="required"
|
||||||
|
value="testing123"
|
||||||
|
name="name"
|
||||||
|
/>
|
||||||
|
<FormulateInput
|
||||||
|
name="users"
|
||||||
|
type="group"
|
||||||
|
>
|
||||||
|
<FormulateInput type="text" name="email" validation="required|email" value="justin@wearebraid.com" />
|
||||||
|
<FormulateInput type="text" name="name" validation="required" value="party" />
|
||||||
|
</FormulateInput>
|
||||||
|
<FormulateInput type="submit" />
|
||||||
|
</FormulateForm>
|
||||||
|
`,
|
||||||
|
methods: {
|
||||||
|
submit
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const form = wrapper.findComponent(FormulateForm)
|
||||||
|
await form.vm.formSubmitted()
|
||||||
|
expect(submit.mock.calls.length).toBe(1);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays validation errors on group children when form is submitted', async () => {
|
||||||
|
const wrapper = mount({
|
||||||
|
template: `
|
||||||
|
<FormulateForm>
|
||||||
|
<FormulateInput
|
||||||
|
name="users"
|
||||||
|
type="group"
|
||||||
|
:repeatable="true"
|
||||||
|
>
|
||||||
|
<FormulateInput type="text" name="name" validation="required" />
|
||||||
|
</FormulateInput>
|
||||||
|
<FormulateInput type="submit" />
|
||||||
|
</FormulateForm>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
const form = wrapper.findComponent(FormulateForm)
|
||||||
|
await form.vm.formSubmitted()
|
||||||
|
expect(wrapper.find('[data-classification="text"] .formulate-input-error').exists()).toBe(true);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays error messages on newly registered fields when formShouldShowErrors is true', async () => {
|
||||||
|
const wrapper = mount({
|
||||||
|
template: `
|
||||||
|
<FormulateForm>
|
||||||
|
<FormulateInput
|
||||||
|
name="users"
|
||||||
|
type="group"
|
||||||
|
:repeatable="true"
|
||||||
|
>
|
||||||
|
<FormulateInput type="text" name="name" validation="required" />
|
||||||
|
</FormulateInput>
|
||||||
|
<FormulateInput type="submit" />
|
||||||
|
</FormulateForm>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
const form = wrapper.findComponent(FormulateForm)
|
||||||
|
await form.vm.formSubmitted()
|
||||||
|
// Click the add more button
|
||||||
|
wrapper.find('button[type="button"]').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.findAll('[data-classification="text"] .formulate-input-error').length).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays error messages on newly registered fields when formShouldShowErrors is true', async () => {
|
||||||
|
const wrapper = mount({
|
||||||
|
template: `
|
||||||
|
<FormulateForm>
|
||||||
|
<FormulateInput
|
||||||
|
name="users"
|
||||||
|
type="group"
|
||||||
|
:repeatable="true"
|
||||||
|
>
|
||||||
|
<FormulateInput type="text" name="name" validation="required" />
|
||||||
|
</FormulateInput>
|
||||||
|
<FormulateInput type="submit" />
|
||||||
|
</FormulateForm>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
const form = wrapper.findComponent(FormulateForm)
|
||||||
|
await form.vm.formSubmitted()
|
||||||
|
// Click the add more button
|
||||||
|
wrapper.find('button[type="button"]').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.findAll('[data-classification="text"] .formulate-input-error').length).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows the removal of groups', async () => {
|
||||||
|
const wrapper = mount({
|
||||||
|
template: `
|
||||||
|
<FormulateForm>
|
||||||
|
<FormulateInput
|
||||||
|
name="users"
|
||||||
|
type="group"
|
||||||
|
:repeatable="true"
|
||||||
|
v-model="users"
|
||||||
|
>
|
||||||
|
<FormulateInput type="text" name="name" validation="required" />
|
||||||
|
</FormulateInput>
|
||||||
|
<FormulateInput type="submit" />
|
||||||
|
</FormulateForm>
|
||||||
|
`,
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
users: [{name: 'justin'}, {name: 'bill'}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
wrapper.find('.formulate-input-group-repeatable-remove').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.vm.users).toEqual([{name: 'bill'}])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can override the add more text', async () => {
|
||||||
|
const wrapper = mount(FormulateInput, {
|
||||||
|
propsData: { addLabel: '+ Add a user', type: 'group', repeatable: true },
|
||||||
|
slots: {
|
||||||
|
default: '<div />'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(wrapper.find('button').text()).toEqual('+ Add a user')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not allow more than the limit', async () => {
|
||||||
|
const wrapper = mount(FormulateInput, {
|
||||||
|
propsData: { addLabel: '+ Add a user', type: 'group', repeatable: true, limit: 2, value: [{}, {}]},
|
||||||
|
slots: {
|
||||||
|
default: '<div class="repeated"/>'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(wrapper.findAll('.repeated').length).toBe(2)
|
||||||
|
expect(wrapper.find('button').exists()).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not truncate the number of items if value is more than limit', async () => {
|
||||||
|
const wrapper = mount(FormulateInput, {
|
||||||
|
propsData: { addLabel: '+ Add a user', type: 'group', repeatable: true, limit: 2, value: [{}, {}, {}, {}]},
|
||||||
|
slots: {
|
||||||
|
default: '<div class="repeated"/>'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(wrapper.findAll('.repeated').length).toBe(4)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows a slot override of the add button and has addItem prop', async () => {
|
||||||
|
const wrapper = mount(FormulateInput, {
|
||||||
|
propsData: { type: 'group', repeatable: true, addLabel: '+ Name' },
|
||||||
|
scopedSlots: {
|
||||||
|
default: '<div class="repeatable" />',
|
||||||
|
addmore: '<span class="add-name" @click="props.addMore">{{ props.addLabel }}</span>'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(wrapper.find('.formulate-input-group-add-more').exists()).toBeFalsy()
|
||||||
|
const addButton = wrapper.find('.add-name')
|
||||||
|
expect(addButton.text()).toBe('+ Name')
|
||||||
|
addButton.trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.findAll('.repeatable').length).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows a slot override of the repeatable area', async () => {
|
||||||
|
const wrapper = mount(FormulateInput, {
|
||||||
|
propsData: { type: 'group', repeatable: true, value: [{}, {}]},
|
||||||
|
scopedSlots: {
|
||||||
|
repeatable: '<div class="repeat">{{ props.index }}<div class="remove" @click="props.removeItem" /></div>',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const repeats = wrapper.findAll('.repeat')
|
||||||
|
expect(repeats.length).toBe(2)
|
||||||
|
expect(repeats.at(1).text()).toBe("1")
|
||||||
|
wrapper.find('.remove').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.findAll('.repeat').length).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not show an error message on group input when child has an error', async () => {
|
||||||
|
const wrapper = mount({
|
||||||
|
template: `
|
||||||
|
<FormulateForm>
|
||||||
|
<FormulateInput
|
||||||
|
type="text"
|
||||||
|
validation="required"
|
||||||
|
value="testing123"
|
||||||
|
name="name"
|
||||||
|
/>
|
||||||
|
<FormulateInput
|
||||||
|
v-model="users"
|
||||||
|
type="group"
|
||||||
|
>
|
||||||
|
<FormulateInput type="text" name="email" />
|
||||||
|
<FormulateInput type="text" name="name" validation="required" />
|
||||||
|
</FormulateInput>
|
||||||
|
<FormulateInput type="submit" />
|
||||||
|
</FormulateForm>
|
||||||
|
`,
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
users: [{email: 'jon@example.com'}, {}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const form = wrapper.findComponent(FormulateForm)
|
||||||
|
await form.vm.formSubmitted()
|
||||||
|
expect(wrapper.find('[data-classification="group"] > .formulate-input-errors').exists()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('exposes the index to the context object on default slot', async () => {
|
||||||
|
const wrapper = mount({
|
||||||
|
template: `
|
||||||
|
<FormulateInput
|
||||||
|
type="group"
|
||||||
|
name="test"
|
||||||
|
#default="{ name, index }"
|
||||||
|
:value="[{}, {}]"
|
||||||
|
>
|
||||||
|
<div class="repeatable">{{ name }}-{{ index }}</div>
|
||||||
|
</FormulateInput>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
const repeatables = wrapper.findAll('.repeatable')
|
||||||
|
expect(repeatables.length).toBe(2)
|
||||||
|
expect(repeatables.at(0).text()).toBe('test-0')
|
||||||
|
expect(repeatables.at(1).text()).toBe('test-1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('forces non-repeatable groups to not initialize with an empty array', async () => {
|
||||||
|
const wrapper = mount({
|
||||||
|
template: `
|
||||||
|
<FormulateInput
|
||||||
|
type="group"
|
||||||
|
name="test"
|
||||||
|
v-model="model"
|
||||||
|
>
|
||||||
|
<div class="repeatable" />
|
||||||
|
</FormulateInput>
|
||||||
|
`,
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
model: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await flushPromises();
|
||||||
|
expect(wrapper.findComponent(FormulateGrouping).vm.items).toEqual([{}])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows repeatable groups to initialize with an empty array', async () => {
|
||||||
|
const wrapper = mount({
|
||||||
|
template: `
|
||||||
|
<FormulateInput
|
||||||
|
type="group"
|
||||||
|
name="test"
|
||||||
|
:repeatable="true"
|
||||||
|
v-model="model"
|
||||||
|
>
|
||||||
|
<div class="repeatable" />
|
||||||
|
</FormulateInput>
|
||||||
|
`,
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
model: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await flushPromises();
|
||||||
|
expect(wrapper.findComponent(FormulateGrouping).vm.items).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
55
test/unit/FormulateInputSelect.test.js
Normal file
55
test/unit/FormulateInputSelect.test.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import flushPromises from 'flush-promises'
|
||||||
|
import Formulate from '@/Formulate.js'
|
||||||
|
import FormulateInput from '@/FormulateInput.vue'
|
||||||
|
import FormulateInputSelect from '@/inputs/FormulateInputSelect.vue'
|
||||||
|
|
||||||
|
Vue.use(Formulate)
|
||||||
|
|
||||||
|
describe('FormulateInputSelect', () => {
|
||||||
|
it('renders select input when type is "select"', () => {
|
||||||
|
const wrapper = mount(FormulateInput, { propsData: { type: 'select' } })
|
||||||
|
expect(wrapper.findComponent(FormulateInputSelect).exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders select options when options object is passed', () => {
|
||||||
|
const wrapper = mount(FormulateInput, { propsData: { type: 'select', options: { first: 'First', second: 'Second' } } })
|
||||||
|
const option = wrapper.find('option[value="second"]')
|
||||||
|
expect(option.exists()).toBe(true)
|
||||||
|
expect(option.text()).toBe('Second')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders select options when options array is passed', () => {
|
||||||
|
const wrapper = mount(FormulateInput, { propsData: { type: 'select', options: [
|
||||||
|
{ value: 13, label: 'Jane' },
|
||||||
|
{ value: 22, label: 'Jon' }
|
||||||
|
]} })
|
||||||
|
const option = wrapper.find('option[value="22"]')
|
||||||
|
expect(option.exists()).toBe(true)
|
||||||
|
expect(option.text()).toBe('Jon')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders select list with no options when empty array is passed.', () => {
|
||||||
|
const wrapper = mount(FormulateInput, { propsData: { type: 'select', options: []} })
|
||||||
|
const option = wrapper.find('option')
|
||||||
|
expect(option.exists()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders select list placeholder option.', () => {
|
||||||
|
const wrapper = mount(FormulateInput, { propsData: { type: 'select', placeholder: 'Select this', options: []} })
|
||||||
|
const options = wrapper.findAll('option')
|
||||||
|
expect(options.length).toBe(1)
|
||||||
|
expect(options.at(0).attributes('disabled')).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('passes an explicitly given name prop through to the root element', () => {
|
||||||
|
const wrapper = mount(FormulateInput, { propsData: { type: 'select', options: [], name: 'foo' } })
|
||||||
|
expect(wrapper.find('select[name="foo"]').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('additional context does not bleed through to text select attributes', () => {
|
||||||
|
const wrapper = mount(FormulateInput, { propsData: { type: 'select' } } )
|
||||||
|
expect(Object.keys(wrapper.find('select').attributes())).toEqual(["id"])
|
||||||
|
})
|
||||||
|
})
|
@ -13,7 +13,7 @@ Vue.use(Formulate)
|
|||||||
describe('FormulateInputSlider', () => {
|
describe('FormulateInputSlider', () => {
|
||||||
it('renders range input when type is "range"', () => {
|
it('renders range input when type is "range"', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: { type: 'range' } })
|
const wrapper = mount(FormulateInput, { propsData: { type: 'range' } })
|
||||||
expect(wrapper.contains(FormulateInputSlider)).toBe(true)
|
expect(wrapper.findComponent(FormulateInputSlider).exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not show value if the show-value prop is not set', () => {
|
it('does not show value if the show-value prop is not set', () => {
|
||||||
@ -25,4 +25,14 @@ describe('FormulateInputSlider', () => {
|
|||||||
const wrapper = mount(FormulateInput, { propsData: { type: 'range', showValue: 'true', value: '15', min: '0', max: '100' } })
|
const wrapper = mount(FormulateInput, { propsData: { type: 'range', showValue: 'true', value: '15', min: '0', max: '100' } })
|
||||||
expect(wrapper.find('.formulate-input-element-range-value').text()).toBe('15')
|
expect(wrapper.find('.formulate-input-element-range-value').text()).toBe('15')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('passes an explicitly given name prop through to the root element', () => {
|
||||||
|
const wrapper = mount(FormulateInput, { propsData: { type: 'range', name: 'foo' } })
|
||||||
|
expect(wrapper.find('input[name="foo"]').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('additional context does not bleed through to range input attributes', () => {
|
||||||
|
const wrapper = mount(FormulateInput, { propsData: { type: 'range' } } )
|
||||||
|
expect(Object.keys(wrapper.find('input[type="range"]').attributes())).toEqual(["type", "id"])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -15,62 +15,62 @@ Vue.use(Formulate)
|
|||||||
describe('FormulateInputText', () => {
|
describe('FormulateInputText', () => {
|
||||||
it('renders text input when type is "text"', () => {
|
it('renders text input when type is "text"', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: { type: 'text' } })
|
const wrapper = mount(FormulateInput, { propsData: { type: 'text' } })
|
||||||
expect(wrapper.contains(FormulateInputText)).toBe(true)
|
expect(wrapper.findComponent(FormulateInputText).exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders search input when type is "search"', () => {
|
it('renders search input when type is "search"', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: { type: 'search' } })
|
const wrapper = mount(FormulateInput, { propsData: { type: 'search' } })
|
||||||
expect(wrapper.contains(FormulateInputText)).toBe(true)
|
expect(wrapper.findComponent(FormulateInputText).exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders email input when type is "email"', () => {
|
it('renders email input when type is "email"', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: { type: 'email' } })
|
const wrapper = mount(FormulateInput, { propsData: { type: 'email' } })
|
||||||
expect(wrapper.contains(FormulateInputText)).toBe(true)
|
expect(wrapper.findComponent(FormulateInputText).exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders number input when type is "number"', () => {
|
it('renders number input when type is "number"', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: { type: 'number' } })
|
const wrapper = mount(FormulateInput, { propsData: { type: 'number' } })
|
||||||
expect(wrapper.contains(FormulateInputText)).toBe(true)
|
expect(wrapper.findComponent(FormulateInputText).exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders color input when type is "color"', () => {
|
it('renders color input when type is "color"', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: { type: 'color' } })
|
const wrapper = mount(FormulateInput, { propsData: { type: 'color' } })
|
||||||
expect(wrapper.contains(FormulateInputText)).toBe(true)
|
expect(wrapper.findComponent(FormulateInputText).exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders date input when type is "date"', () => {
|
it('renders date input when type is "date"', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: { type: 'date' } })
|
const wrapper = mount(FormulateInput, { propsData: { type: 'date' } })
|
||||||
expect(wrapper.contains(FormulateInputText)).toBe(true)
|
expect(wrapper.findComponent(FormulateInputText).exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders month input when type is "month"', () => {
|
it('renders month input when type is "month"', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: { type: 'month' } })
|
const wrapper = mount(FormulateInput, { propsData: { type: 'month' } })
|
||||||
expect(wrapper.contains(FormulateInputText)).toBe(true)
|
expect(wrapper.findComponent(FormulateInputText).exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders password input when type is "password"', () => {
|
it('renders password input when type is "password"', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: { type: 'password' } })
|
const wrapper = mount(FormulateInput, { propsData: { type: 'password' } })
|
||||||
expect(wrapper.contains(FormulateInputText)).toBe(true)
|
expect(wrapper.findComponent(FormulateInputText).exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders tel input when type is "tel"', () => {
|
it('renders tel input when type is "tel"', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: { type: 'tel' } })
|
const wrapper = mount(FormulateInput, { propsData: { type: 'tel' } })
|
||||||
expect(wrapper.contains(FormulateInputText)).toBe(true)
|
expect(wrapper.findComponent(FormulateInputText).exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders time input when type is "time"', () => {
|
it('renders time input when type is "time"', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: { type: 'time' } })
|
const wrapper = mount(FormulateInput, { propsData: { type: 'time' } })
|
||||||
expect(wrapper.contains(FormulateInputText)).toBe(true)
|
expect(wrapper.findComponent(FormulateInputText).exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders url input when type is "url"', () => {
|
it('renders url input when type is "url"', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: { type: 'url' } })
|
const wrapper = mount(FormulateInput, { propsData: { type: 'url' } })
|
||||||
expect(wrapper.contains(FormulateInputText)).toBe(true)
|
expect(wrapper.findComponent(FormulateInputText).exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders week input when type is "week"', () => {
|
it('renders week input when type is "week"', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: { type: 'week' } })
|
const wrapper = mount(FormulateInput, { propsData: { type: 'week' } })
|
||||||
expect(wrapper.contains(FormulateInputText)).toBe(true)
|
expect(wrapper.findComponent(FormulateInputText).exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -83,6 +83,26 @@ describe('FormulateInputText', () => {
|
|||||||
expect(wrapper.find(`input[id="${wrapper.vm.context.attributes.id}"]`).exists()).toBe(true)
|
expect(wrapper.find(`input[id="${wrapper.vm.context.attributes.id}"]`).exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('passes an explicitly given name prop through to the root text element', () => {
|
||||||
|
const wrapper = mount(FormulateInput, { propsData: { type: 'text', name: 'foo' } })
|
||||||
|
expect(wrapper.find('input[name="foo"]').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('passes an explicitly given name prop through to the root textarea element', () => {
|
||||||
|
const wrapper = mount(FormulateInput, { propsData: { type: 'textarea', name: 'foo' } })
|
||||||
|
expect(wrapper.find('textarea[name="foo"]').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('additional context does not bleed through to text input attributes', () => {
|
||||||
|
const wrapper = mount(FormulateInput, { propsData: { type: 'text' } } )
|
||||||
|
expect(Object.keys(wrapper.find('input[type="text"]').attributes())).toEqual(["type", "id"])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('additional context does not bleed through to textarea input attributes', () => {
|
||||||
|
const wrapper = mount(FormulateInput, { propsData: { type: 'textarea' } } )
|
||||||
|
expect(Object.keys(wrapper.find('textarea').attributes())).toEqual(["id"])
|
||||||
|
})
|
||||||
|
|
||||||
it('doesn’t automatically add a label', () => {
|
it('doesn’t automatically add a label', () => {
|
||||||
const wrapper = mount(FormulateInput, { propsData: { type: 'text' } })
|
const wrapper = mount(FormulateInput, { propsData: { type: 'text' } })
|
||||||
expect(wrapper.find('label').exists()).toBe(false)
|
expect(wrapper.find('label').exists()).toBe(false)
|
||||||
@ -130,15 +150,15 @@ describe('FormulateInputText', () => {
|
|||||||
`
|
`
|
||||||
})
|
})
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
const firstContext = wrapper.find({ref: "first"}).vm.context
|
const firstContext = wrapper.findComponent({ref: "first"}).vm.context
|
||||||
const secondContext = wrapper.find({ref: "second"}).vm.context
|
const secondContext = wrapper.findComponent({ref: "second"}).vm.context
|
||||||
wrapper.find('input').setValue('new value')
|
wrapper.find('input').setValue('new value')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(firstContext).toBeTruthy()
|
expect(firstContext).toBeTruthy()
|
||||||
expect(wrapper.vm.valueA === 'new value').toBe(true)
|
expect(wrapper.vm.valueA === 'new value').toBe(true)
|
||||||
expect(wrapper.vm.valueB === 'second value').toBe(true)
|
expect(wrapper.vm.valueB === 'second value').toBe(true)
|
||||||
expect(wrapper.find({ref: "first"}).vm.context === firstContext).toBe(false)
|
expect(wrapper.findComponent({ref: "first"}).vm.context === firstContext).toBe(false)
|
||||||
expect(wrapper.find({ref: "second"}).vm.context === secondContext).toBe(true)
|
expect(wrapper.findComponent({ref: "second"}).vm.context === secondContext).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses the v-model value as the initial value', () => {
|
it('uses the v-model value as the initial value', () => {
|
||||||
@ -160,7 +180,7 @@ describe('FormulateInputText', () => {
|
|||||||
const wrapper = mount(FormulateInput, { propsData: { type: 'textarea' } })
|
const wrapper = mount(FormulateInput, { propsData: { type: 'textarea' } })
|
||||||
const input = wrapper.find('textarea')
|
const input = wrapper.find('textarea')
|
||||||
input.setValue('changed value')
|
input.setValue('changed value')
|
||||||
expect(wrapper.vm.internalModelProxy).toBe('changed value')
|
expect(wrapper.vm.proxy).toBe('changed value')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -241,4 +261,53 @@ describe('FormulateInputText', () => {
|
|||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.find('[data-has-errors]').exists()).toBe(true)
|
expect(wrapper.find('[data-has-errors]').exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
it('allows label-before override with scoped slot', async () => {
|
||||||
|
const wrapper = mount(FormulateInput, {
|
||||||
|
propsData: { type: 'text', label: 'flavor' },
|
||||||
|
scopedSlots: {
|
||||||
|
label: '<label>{{ props.label }} town</label>'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(wrapper.find('label').text()).toBe('flavor town')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows label-after override with scoped slot', async () => {
|
||||||
|
const wrapper = mount(FormulateInput, {
|
||||||
|
propsData: { type: 'text', label: 'flavor', labelPosition: 'after' },
|
||||||
|
scopedSlots: {
|
||||||
|
label: '<label>{{ props.label }} town</label>'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(wrapper.find('label').text()).toBe('flavor town')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows help-before override', async () => {
|
||||||
|
const wrapper = mount(FormulateInput, {
|
||||||
|
propsData: { type: 'text', label: 'flavor', help: 'I love this next field...', helpPosition: 'before' },
|
||||||
|
})
|
||||||
|
expect(wrapper.find('label + *').classes('formulate-input-help')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Allow help text override with scoped slot', async () => {
|
||||||
|
const wrapper = mount(FormulateInput, {
|
||||||
|
propsData: { type: 'text', name: 'soda', help: 'Do you want some'},
|
||||||
|
scopedSlots: {
|
||||||
|
help: '<small>{{ props.help }} {{ props.name }}?</small>'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(wrapper.find('small').text()).toBe('Do you want some soda?')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Allow errors override with scoped slot', async () => {
|
||||||
|
const wrapper = mount(FormulateInput, {
|
||||||
|
propsData: { type: 'text', name: 'soda', validation: 'required|in:foo,bar', errorBehavior: 'live' },
|
||||||
|
scopedSlots: {
|
||||||
|
errors: '<ul class="my-errors"><li v-for="error in props.visibleValidationErrors">{{ error }}</li></ul>'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await flushPromises();
|
||||||
|
expect(wrapper.findAll('.my-errors li').length).toBe(2)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -306,6 +306,18 @@ describe('matches', () => {
|
|||||||
it('passes on matching mixed regex and string', async () => {
|
it('passes on matching mixed regex and string', async () => {
|
||||||
expect(await rules.matches({ value: 'first-fourth' }, 'second', /^third/, /fourth$/)).toBe(true)
|
expect(await rules.matches({ value: 'first-fourth' }, 'second', /^third/, /fourth$/)).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('fails on a regular expression encoded as a string', async () => {
|
||||||
|
expect(await rules.matches({ value: 'mypassword' }, '/[0-9]/')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('passes on a regular expression encoded as a string', async () => {
|
||||||
|
expect(await rules.matches({ value: 'mypa55word' }, '/[0-9]/')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('passes on a regular expression containing slashes', async () => {
|
||||||
|
expect(await rules.matches({ value: 'https://' }, '/https?:///')).toBe(true)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,37 +1,37 @@
|
|||||||
import { parseRules, parseLocale, regexForFormat, cloneDeep, isValueType, snakeToCamel } from '@/libs/utils'
|
import { parseRules, parseLocale, regexForFormat, cloneDeep, isValueType, snakeToCamel, groupBails } from '@/libs/utils'
|
||||||
import rules from '@/libs/rules'
|
import rules from '@/libs/rules'
|
||||||
import FileUpload from '@/FileUpload';
|
import FileUpload from '@/FileUpload';
|
||||||
|
|
||||||
describe('parseRules', () => {
|
describe('parseRules', () => {
|
||||||
it('parses single string rules, returning empty arguments array', () => {
|
it('parses single string rules, returning empty arguments array', () => {
|
||||||
expect(parseRules('required', rules)).toEqual([
|
expect(parseRules('required', rules)).toEqual([
|
||||||
[rules.required, [], 'required']
|
[rules.required, [], 'required', null]
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('throws errors for invalid validation rules', () => {
|
it('throws errors for invalid validation rules', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
parseRules('required|notarule', rules)
|
parseRules('required|notarule', rules, null)
|
||||||
}).toThrow()
|
}).toThrow()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('parses arguments for a rule', () => {
|
it('parses arguments for a rule', () => {
|
||||||
expect(parseRules('in:foo,bar', rules)).toEqual([
|
expect(parseRules('in:foo,bar', rules)).toEqual([
|
||||||
[rules.in, ['foo', 'bar'], 'in']
|
[rules.in, ['foo', 'bar'], 'in', null]
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('parses multiple string rules and arguments', () => {
|
it('parses multiple string rules and arguments', () => {
|
||||||
expect(parseRules('required|in:foo,bar', rules)).toEqual([
|
expect(parseRules('required|in:foo,bar', rules)).toEqual([
|
||||||
[rules.required, [], 'required'],
|
[rules.required, [], 'required', null],
|
||||||
[rules.in, ['foo', 'bar'], 'in']
|
[rules.in, ['foo', 'bar'], 'in', null]
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('parses multiple array rules and arguments', () => {
|
it('parses multiple array rules and arguments', () => {
|
||||||
expect(parseRules(['required', 'in:foo,bar'], rules)).toEqual([
|
expect(parseRules(['required', 'in:foo,bar'], rules)).toEqual([
|
||||||
[rules.required, [], 'required'],
|
[rules.required, [], 'required', null],
|
||||||
[rules.in, ['foo', 'bar'], 'in']
|
[rules.in, ['foo', 'bar'], 'in', null]
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -39,7 +39,21 @@ describe('parseRules', () => {
|
|||||||
expect(parseRules([
|
expect(parseRules([
|
||||||
['matches', /^abc/, '1234']
|
['matches', /^abc/, '1234']
|
||||||
], rules)).toEqual([
|
], rules)).toEqual([
|
||||||
[rules.matches, [/^abc/, '1234'], 'matches']
|
[rules.matches, [/^abc/, '1234'], 'matches', null]
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses string rules with caret modifier', () => {
|
||||||
|
expect(parseRules('^required|min:10', rules)).toEqual([
|
||||||
|
[rules.required, [], 'required', '^'],
|
||||||
|
[rules.min, ['10'], 'min', null],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses array rule with caret modifier', () => {
|
||||||
|
expect(parseRules([['required'], ['^max', '10']], rules)).toEqual([
|
||||||
|
[rules.required, [], 'required', null],
|
||||||
|
[rules.max, ['10'], 'max', '^'],
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -116,6 +130,19 @@ describe('cloneDeep', () => {
|
|||||||
const clone = cloneDeep({ a: 123, b: c })
|
const clone = cloneDeep({ a: 123, b: c })
|
||||||
expect(clone.b === c).toBe(false)
|
expect(clone.b === c).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('retains array structures inside of a pojo', () => {
|
||||||
|
const obj = { a: 'abcd', d: ['first', 'second'] }
|
||||||
|
const clone = cloneDeep(obj)
|
||||||
|
expect(Array.isArray(clone.d)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removes references inside array structures', () => {
|
||||||
|
const deepObj = {foo: 'bar'}
|
||||||
|
const obj = { a: 'abcd', d: ['first', deepObj] }
|
||||||
|
const clone = cloneDeep(obj)
|
||||||
|
expect(clone.d[1] === deepObj).toBe(false)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('snakeToCamel', () => {
|
describe('snakeToCamel', () => {
|
||||||
@ -159,3 +186,72 @@ describe('parseLocale', () => {
|
|||||||
expect(parseLocale('en')).toEqual(['en'])
|
expect(parseLocale('en')).toEqual(['en'])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('groupBails', () => {
|
||||||
|
it('wraps non bailed rules in an array', () => {
|
||||||
|
const bailGroups = groupBails([[,,'required'], [,,'min']])
|
||||||
|
expect(bailGroups).toEqual(
|
||||||
|
[ [[,,'required'], [,,'min']] ] // dont bail on either of these
|
||||||
|
)
|
||||||
|
expect(bailGroups.map(group => !!group.bail)).toEqual([false])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('splits bailed rules into two arrays array', () => {
|
||||||
|
const bailGroups = groupBails([[,,'required'], [,,'max'], [,, 'bail'], [,, 'matches'], [,,'min']])
|
||||||
|
expect(bailGroups).toEqual([
|
||||||
|
[ [,,'required'], [,,'max'] ], // dont bail on these
|
||||||
|
[ [,, 'matches'] ], // bail on this one
|
||||||
|
[ [,,'min'] ] // bail on this one
|
||||||
|
])
|
||||||
|
expect(bailGroups.map(group => !!group.bail)).toEqual([false, true, true])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('splits entire rule set when bail is at the beginning', () => {
|
||||||
|
const bailGroups = groupBails([[,, 'bail'], [,,'required'], [,,'max'], [,, 'matches'], [,,'min']])
|
||||||
|
expect(bailGroups).toEqual([
|
||||||
|
[ [,, 'required'] ], // bail on this one
|
||||||
|
[ [,, 'max'] ], // bail on this one
|
||||||
|
[ [,, 'matches'] ], // bail on this one
|
||||||
|
[ [,, 'min'] ] // bail on this one
|
||||||
|
])
|
||||||
|
expect(bailGroups.map(group => !!group.bail)).toEqual([true, true, true, true])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('splits no rules when bail is at the end', () => {
|
||||||
|
const bailGroups = groupBails([[,,'required'], [,,'max'], [,, 'matches'], [,,'min'], [,, 'bail']])
|
||||||
|
expect(bailGroups).toEqual([
|
||||||
|
[ [,, 'required'], [,, 'max'], [,, 'matches'], [,, 'min'] ] // dont bail on these
|
||||||
|
])
|
||||||
|
expect(bailGroups.map(group => !!group.bail)).toEqual([false])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('splits individual modified names into two groups when at the begining', () => {
|
||||||
|
const bailGroups = groupBails([[,,'required', '^'], [,,'max'], [,, 'matches'], [,,'min'] ])
|
||||||
|
expect(bailGroups).toEqual([
|
||||||
|
[ [,, 'required', '^'] ], // bail on this one
|
||||||
|
[ [,, 'max'], [,, 'matches'], [,, 'min'] ] // dont bail on these
|
||||||
|
])
|
||||||
|
expect(bailGroups.map(group => !!group.bail)).toEqual([true, false])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('splits individual modified names into three groups when in the middle', () => {
|
||||||
|
const bailGroups = groupBails([[,,'required'], [,,'max'], [,, 'matches', '^'], [,,'min'] ])
|
||||||
|
expect(bailGroups).toEqual([
|
||||||
|
[ [,, 'required'], [,, 'max'] ], // dont bail on these
|
||||||
|
[ [,, 'matches', '^'] ], // bail on this one
|
||||||
|
[ [,, 'min'] ] // dont bail on this
|
||||||
|
])
|
||||||
|
expect(bailGroups.map(group => !!group.bail)).toEqual([false, true, false])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('splits individual modified names into four groups when used twice', () => {
|
||||||
|
const bailGroups = groupBails([[,,'required', '^'], [,,'max'], [,, 'matches', '^'], [,,'min'] ])
|
||||||
|
expect(bailGroups).toEqual([
|
||||||
|
[ [,, 'required', '^'] ], // bail on this
|
||||||
|
[ [,, 'max'] ], // dont bail on this
|
||||||
|
[ [,, 'matches', '^'] ], // bail on this
|
||||||
|
[ [,, 'min'] ] // dont bail on this
|
||||||
|
])
|
||||||
|
expect(bailGroups.map(group => !!group.bail)).toEqual([true, false, true, false])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
@ -11,6 +11,11 @@
|
|||||||
font-size: .9em;
|
font-size: .9em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: .1em;
|
margin-bottom: .1em;
|
||||||
|
|
||||||
|
&--before + .formulate-input-help--before {
|
||||||
|
margin-top: -.25em;
|
||||||
|
margin-bottom: .75em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.formulate-input-element {
|
.formulate-input-element {
|
||||||
@ -41,9 +46,16 @@
|
|||||||
margin-bottom: .25em;
|
margin-bottom: .25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.formulate-input-group-item {
|
// .formulate-input-group-item {
|
||||||
margin-bottom: .5em;
|
// margin-bottom: 1.5em;
|
||||||
}
|
// padding: 1.5em;
|
||||||
|
// border: 1px solid $formulate-gray;
|
||||||
|
// border-radius: .25em;
|
||||||
|
|
||||||
|
// &:last-child {
|
||||||
|
// margin-bottom: 1.5em;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
@ -156,6 +168,7 @@
|
|||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
border-radius: 1em;
|
border-radius: 1em;
|
||||||
|
border: 0;
|
||||||
background-color: $formulate-green;
|
background-color: $formulate-green;
|
||||||
margin-top: calc(-.5em + 2px);
|
margin-top: calc(-.5em + 2px);
|
||||||
}
|
}
|
||||||
@ -185,6 +198,10 @@
|
|||||||
&::-webkit-slider-runnable-track {
|
&::-webkit-slider-runnable-track {
|
||||||
@include track;
|
@include track;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&::-moz-range-track {
|
||||||
|
@include track;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,6 +235,29 @@
|
|||||||
border-color: $formulate-gray-d;
|
border-color: $formulate-gray-d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&[data-ghost] {
|
||||||
|
color: $formulate-green;
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-minor] {
|
||||||
|
font-size: .75em;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-danger] {
|
||||||
|
background-color: $formulate-error;
|
||||||
|
border-color: $formulate-error;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-danger][data-ghost] {
|
||||||
|
color: $formulate-error;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background-color: $formulate-green-l;
|
background-color: $formulate-green-l;
|
||||||
border-color: $formulate-green-l;
|
border-color: $formulate-green-l;
|
||||||
@ -345,11 +385,83 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-classification="group"] {
|
// Input groups
|
||||||
& > .formulate-input-wrapper {
|
// -----------------------------------------------------------------------------
|
||||||
& > .formulate-input-label {
|
|
||||||
|
&[data-classification='group'] {
|
||||||
|
.formulate-input-group-item {
|
||||||
margin-bottom: .5em;
|
margin-bottom: .5em;
|
||||||
}
|
}
|
||||||
|
& > .formulate-input-wrapper > .formulate-input-label {
|
||||||
|
margin-bottom: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-is-repeatable] {
|
||||||
|
padding: 1em;
|
||||||
|
border: 1px solid $formulate-gray;
|
||||||
|
border-radius: .3em;
|
||||||
|
|
||||||
|
.formulate-input-grouping {
|
||||||
|
margin: -1em -1em 0 -1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formulate-input-group-repeatable {
|
||||||
|
padding: 1em 3em 1em 1em;
|
||||||
|
border-bottom: 1px solid $formulate-gray;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&-remove {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
top: calc(50% - .65em + .5em);
|
||||||
|
width: 1.3em;
|
||||||
|
height: 1.3em;
|
||||||
|
background-color: $formulate-gray-d;
|
||||||
|
right: .85em;
|
||||||
|
border-radius: 1.3em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color .2s;
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: calc(50% - .1em);
|
||||||
|
left: .325em;
|
||||||
|
display: block;
|
||||||
|
width: .65em;
|
||||||
|
height: .2em;
|
||||||
|
background-color: white;
|
||||||
|
transform-origin: center center;
|
||||||
|
transition: transform .25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (pointer: fine) {
|
||||||
|
&:hover {
|
||||||
|
background-color: $formulate-error-l;
|
||||||
|
|
||||||
|
&::after,
|
||||||
|
&::before {
|
||||||
|
height: .2em;
|
||||||
|
width: .75em;
|
||||||
|
left: .25em;
|
||||||
|
top: calc(50% - .075em);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
&::before {
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user