1
0
mirror of synced 2025-02-08 01:49:21 +03:00

Merge pull request #7 from wearebraid/v2.1.0

Version 2.1.0
This commit is contained in:
Justin Schroeder 2020-03-02 01:44:41 -05:00 committed by GitHub
commit b5edaa9b3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 466 additions and 112 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

3
dist/snow.css vendored
View File

@ -397,7 +397,8 @@
z-index: 2; } z-index: 2; }
.formulate-input[data-classification="file"] .formulate-files .formualte-file-name { .formulate-input[data-classification="file"] .formulate-files .formualte-file-name {
padding-left: 1.5em; padding-left: 1.5em;
padding-right: 2em; } padding-right: 2em;
max-width: 100%; }
.formulate-input[data-classification="file"] .formulate-files .formualte-file-name::before { .formulate-input[data-classification="file"] .formulate-files .formualte-file-name::before {
position: absolute; position: absolute;
left: .7em; left: .7em;

2
dist/snow.min.css vendored

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,5 @@
import nanoid from 'nanoid/non-secure' import nanoid from 'nanoid/non-secure'
import mimes from './libs/mimes'
/** /**
* The file upload class holds and represents a files upload state durring * The file upload class holds and represents a files upload state durring
@ -15,9 +16,35 @@ class FileUpload {
this.fileList = input.files this.fileList = input.files
this.files = [] this.files = []
this.options = options this.options = options
this.addFileList(this.fileList)
this.context = context
this.results = false this.results = false
this.context = context
if (Array.isArray(this.fileList)) {
this.rehydrateFileList(this.fileList)
} else {
this.addFileList(this.fileList)
}
}
/**
* Given a pre-existing array of files, create a faux FileList.
* @param {array} items expects an array of objects [{ url: '/uploads/file.pdf' }]
* @param {string} pathKey the object-key to access the url (defaults to "url")
*/
rehydrateFileList (items) {
const fauxFileList = items.reduce((fileList, item) => {
const key = this.options ? this.options.fileUrlKey : 'url'
const url = item[key]
const ext = (url && url.lastIndexOf('.') !== -1) ? url.substr(url.lastIndexOf('.') + 1) : false
const mime = mimes[ext] || false
fileList.push(Object.assign({}, item, url ? {
name: url.substr((url.lastIndexOf('/') + 1) || 0),
type: item.type ? item.type : mime,
previewData: url
} : {}))
return fileList
}, [])
this.results = items
this.addFileList(fauxFileList)
} }
/** /**
@ -41,7 +68,7 @@ class FileUpload {
uuid, uuid,
path: false, path: false,
removeFile: removeFile.bind(this), removeFile: removeFile.bind(this),
previewData: false previewData: file.previewData || false
}) })
} }
} }
@ -54,15 +81,18 @@ class FileUpload {
} }
/** /**
* Check if the given uploader is axios instance. * Check if the given uploader is axios instance. This isn't a great way of
* testing if it is or not, but AFIK there isn't a better way right now:
*
* https://github.com/axios/axios/issues/737
*/ */
uploaderIsAxios () { uploaderIsAxios () {
if ( if (
this.hasUploader && this.hasUploader &&
typeof this.hasUploader.request === 'function' && typeof this.context.uploader.request === 'function' &&
typeof this.hasUploader.get === 'function' && typeof this.context.uploader.get === 'function' &&
typeof this.hasUploader.delete === 'function' && typeof this.context.uploader.delete === 'function' &&
typeof this.hasUploader.post === 'function' typeof this.context.uploader.post === 'function'
) { ) {
return true return true
} }
@ -76,14 +106,19 @@ class FileUpload {
if (this.uploaderIsAxios()) { if (this.uploaderIsAxios()) {
const formData = new FormData() const formData = new FormData()
formData.append(this.context.name || 'file', args[0]) formData.append(this.context.name || 'file', args[0])
return this.uploader.post(this.context.uploadUrl, formData, { if (this.context.uploadUrl === false) {
throw new Error('No uploadURL specified: https://vueformulate.com/guide/inputs/file/#props')
}
return this.context.uploader.post(this.context.uploadUrl, formData, {
headers: { headers: {
'Content-Type': 'multipart/form-data' 'Content-Type': 'multipart/form-data'
}, },
onUploadProgress: progressEvent => { onUploadProgress: progressEvent => {
// args[1] here is the upload progress handler function
args[1](Math.round((progressEvent.loaded * 100) / progressEvent.total)) args[1](Math.round((progressEvent.loaded * 100) / progressEvent.total))
} }
}) })
.then(res => res.data)
.catch(err => args[2](err)) .catch(err => args[2](err))
} }
return this.context.uploader(...args) return this.context.uploader(...args)
@ -135,7 +170,8 @@ class FileUpload {
*/ */
removeFile (uuid) { removeFile (uuid) {
this.files = this.files.filter(file => file.uuid !== uuid) this.files = this.files.filter(file => file.uuid !== uuid)
if (window) { this.context.performValidation()
if (window && this.fileList instanceof FileList) {
const transfer = new DataTransfer() const transfer = new DataTransfer()
this.files.map(file => transfer.items.add(file.file)) this.files.map(file => transfer.items.add(file.file))
this.fileList = transfer.files this.fileList = transfer.files

View File

@ -43,6 +43,8 @@ class Formulate {
rules, rules,
locale: 'en', locale: 'en',
uploader: fauxUploader, uploader: fauxUploader,
uploadUrl: false,
fileUrlKey: 'url',
uploadJustCompleteDuration: 1000, uploadJustCompleteDuration: 1000,
plugins: [], plugins: [],
locales: { locales: {
@ -162,6 +164,21 @@ class Formulate {
return this.options.uploader || false return this.options.uploader || false
} }
/**
* Get the global upload url.
*/
getUploadUrl () {
return this.options.uploadUrl || false
}
/**
* When re-hydrating a file uploader with an array, get the sub-object key to
* access the url of the file. Usually this is just "url".
*/
getFileUrlKey () {
return this.options.fileUrlKey || 'url'
}
/** /**
* Create a new instance of an upload. * Create a new instance of an upload.
*/ */

View File

@ -92,21 +92,33 @@ export default {
} }
} }
}, },
deep: true, deep: true
immediate: false
} }
}, },
created () { created () {
if (this.hasInitialValue) { this.applyInitialValues()
this.internalFormModelProxy = this.initialValues
}
}, },
methods: { methods: {
applyInitialValues () {
if (this.hasInitialValue) {
this.internalFormModelProxy = this.initialValues
}
},
setFieldValue (field, value) { setFieldValue (field, value) {
Object.assign(this.internalFormModelProxy, { [field]: value }) Object.assign(this.internalFormModelProxy, { [field]: value })
this.$emit('input', Object.assign({}, this.internalFormModelProxy)) 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) { 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 this.registry[field] = component
const hasVModelValue = Object.prototype.hasOwnProperty.call(component.$options.propsData, 'formulateValue') const hasVModelValue = Object.prototype.hasOwnProperty.call(component.$options.propsData, 'formulateValue')
const hasValue = Object.prototype.hasOwnProperty.call(component.$options.propsData, 'value') const hasValue = Object.prototype.hasOwnProperty.call(component.$options.propsData, 'value')

View File

@ -173,16 +173,17 @@ export default {
validationRules: { validationRules: {
type: Object, type: Object,
default: () => ({}) default: () => ({})
},
checked: {
type: [String, Boolean],
default: false
} }
}, },
data () { data () {
return { return {
/**
* @todo consider swapping out nanoid for this._uid
*/
defaultId: nanoid(9), defaultId: nanoid(9),
localAttributes: {}, localAttributes: {},
internalModelProxy: this.formulateValue || this.value, internalModelProxy: this.getInitialValue(),
behavioralErrorVisibility: (this.errorBehavior === 'live'), behavioralErrorVisibility: (this.errorBehavior === 'live'),
formShouldShowErrors: false, formShouldShowErrors: false,
validationErrors: [], validationErrors: [],
@ -219,6 +220,7 @@ export default {
} }
}, },
created () { created () {
this.applyInitialValue()
if (this.formulateFormRegister && typeof this.formulateFormRegister === 'function') { if (this.formulateFormRegister && typeof this.formulateFormRegister === 'function') {
this.formulateFormRegister(this.nameOrFallback, this) this.formulateFormRegister(this.nameOrFallback, this)
} }
@ -226,6 +228,30 @@ export default {
this.performValidation() this.performValidation()
}, },
methods: { methods: {
getInitialValue () {
// Manually request classification, pre-computed props
var classification = this.$formulate.classify(this.type)
classification = (classification === 'box' && this.options) ? 'group' : classification
if (classification === 'box' && this.checked) {
return this.value || true
} else if (Object.prototype.hasOwnProperty.call(this.$options.propsData, 'value') && classification !== 'box') {
return this.value
} else if (Object.prototype.hasOwnProperty.call(this.$options.propsData, 'formulateValue')) {
return this.formulateValue
}
return ''
},
applyInitialValue () {
// This should only be run immediately on created and ensures that the
// proxy and the model are both the same before any additional registration.
if (
!shallowEqualObjects(this.context.model, this.internalModelProxy) &&
// 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')
) {
this.context.model = this.internalModelProxy
}
},
updateLocalAttributes (value) { updateLocalAttributes (value) {
if (!shallowEqualObjects(value, this.localAttributes)) { if (!shallowEqualObjects(value, this.localAttributes)) {
this.localAttributes = value this.localAttributes = value

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="formulate-input-group"> <div class="formulate-input-group">
<component <component
:is="optionContext.component" :is="subComponent"
v-for="optionContext in optionsWithContext" v-for="optionContext in optionsWithContext"
:key="optionContext.id" :key="optionContext.id"
v-model="context.model" v-model="context.model"
@ -24,16 +24,36 @@ export default {
options () { options () {
return this.context.options || [] return this.context.options || []
}, },
subComponent () {
// @todo - rough-in for future flexible input-groups
return 'FormulateInput'
},
optionsWithContext () { optionsWithContext () {
const { options, labelPosition, attributes, classification, ...context } = this.context const {
return this.options.map(option => this.groupItemContext(context, option)) // The following are a list of items to pull out of the context object
options,
labelPosition,
attributes: { id, ...groupApplicableAttributes },
classification,
blurHandler,
performValidation,
hasValidationErrors,
component,
hasLabel,
...context
} = this.context
return this.options.map(option => this.groupItemContext(
context,
option,
groupApplicableAttributes
))
} }
}, },
methods: { methods: {
groupItemContext (...args) { groupItemContext (context, option, groupAttributes) {
return Object.assign({}, ...args, { const optionAttributes = {}
component: 'FormulateInput' const ctx = Object.assign({}, context, option, groupAttributes, optionAttributes)
}) return ctx
} }
} }
} }

View File

@ -52,6 +52,15 @@ export default {
return !!(this.context.model instanceof FileUpload && this.context.model.files.length) return !!(this.context.model instanceof FileUpload && this.context.model.files.length)
} }
}, },
created () {
if (Array.isArray(this.context.model)) {
if (typeof this.context.model[0][this.$formulate.getFileUrlKey()] === 'string') {
this.context.model = this.$formulate.createUpload({
files: this.context.model
}, this.context)
}
}
},
mounted () { mounted () {
// Add a listener to the window to prevent drag/drops that miss the dropzone // Add a listener to the window to prevent drag/drops that miss the dropzone
// from opening the file and navigating the user away from the page. // from opening the file and navigating the user away from the page.

View File

@ -1,5 +1,5 @@
import nanoid from 'nanoid/non-secure' import nanoid from 'nanoid/non-secure'
import { map, arrayify } from './utils' import { map, arrayify, shallowEqualObjects } from './utils'
/** /**
* For a single instance of an input, export all of the context needed to fully * For a single instance of an input, export all of the context needed to fully
@ -19,9 +19,10 @@ export default {
label: this.label, label: this.label,
labelPosition: this.logicalLabelPosition, labelPosition: this.logicalLabelPosition,
attributes: this.elementAttributes, attributes: this.elementAttributes,
performValidation: this.performValidation.bind(this),
blurHandler: blurHandler.bind(this), blurHandler: blurHandler.bind(this),
imageBehavior: this.imageBehavior, imageBehavior: this.imageBehavior,
uploadUrl: this.uploadUrl, uploadUrl: this.mergedUploadUrl,
uploader: this.uploader || this.$formulate.getUploader(), uploader: this.uploader || this.$formulate.getUploader(),
uploadBehavior: this.uploadBehavior, uploadBehavior: this.uploadBehavior,
preventWindowDrops: this.preventWindowDrops, preventWindowDrops: this.preventWindowDrops,
@ -37,7 +38,8 @@ export default {
mergedErrors, mergedErrors,
hasErrors, hasErrors,
showFieldErrors, showFieldErrors,
mergedValidationName mergedValidationName,
mergedUploadUrl
} }
/** /**
@ -111,6 +113,14 @@ function mergedValidationName () {
return this.type return this.type
} }
/**
* Use the uploadURL on the input if it exists, otherwise use the uploadURL
* that is defined as a plugin option.
*/
function mergedUploadUrl () {
return this.uploadUrl || this.$formulate.getUploadUrl()
}
/** /**
* Determines if the field should show it's error (if it has one) * Determines if the field should show it's error (if it has one)
* @return {boolean} * @return {boolean}
@ -220,7 +230,9 @@ function modelGetter () {
* Set the value from a model. * Set the value from a model.
**/ **/
function modelSetter (value) { function modelSetter (value) {
this.internalModelProxy = value if (!shallowEqualObjects(value, this.internalModelProxy)) {
this.internalModelProxy = value
}
this.$emit('input', value) this.$emit('input', value)
if (this.context.name && typeof this.formulateFormSetter === 'function') { if (this.context.name && typeof this.formulateFormSetter === 'function') {
this.formulateFormSetter(this.context.name, value) this.formulateFormSetter(this.context.name, value)

74
src/libs/mimes.js Normal file
View File

@ -0,0 +1,74 @@
export default {
'aac': 'audio/aac',
'abw': 'application/x-abiword',
'arc': 'application/x-freearc',
'avi': 'video/x-msvideo',
'azw': 'application/vnd.amazon.ebook',
'bin': 'application/octet-stream',
'bmp': 'image/bmp',
'bz': 'application/x-bzip',
'bz2': 'application/x-bzip2',
'csh': 'application/x-csh',
'css': 'text/css',
'csv': 'text/csv',
'doc': 'application/msword',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'eot': 'application/vnd.ms-fontobject',
'epub': 'application/epub+zip',
'gz': 'application/gzip',
'gif': 'image/gif',
'htm': 'text/html',
'html': 'text/html',
'ico': 'image/vnd.microsoft.icon',
'ics': 'text/calendar',
'jar': 'application/java-archive',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'js': 'text/javascript',
'json': 'application/json',
'jsonld': 'application/ld+json',
'midi': 'audio/x-midi',
'mid': 'audio/midi',
'mjs': 'text/javascript',
'mp3': 'audio/mpeg',
'mpeg': 'video/mpeg',
'mpkg': 'application/vnd.apple.installer+xml',
'odp': 'application/vnd.oasis.opendocument.presentation',
'ods': 'application/vnd.oasis.opendocument.spreadsheet',
'odt': 'application/vnd.oasis.opendocument.text',
'oga': 'audio/ogg',
'ogv': 'video/ogg',
'ogx': 'application/ogg',
'opus': 'audio/opus',
'otf': 'font/otf',
'png': 'image/png',
'pdf': 'application/pdf',
'php': 'application/php',
'ppt': 'application/vnd.ms-powerpoint',
'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'rar': 'application/vnd.rar',
'rtf': 'application/rtf',
'sh': 'application/x-sh',
'svg': 'image/svg+xml',
'swf': 'application/x-shockwave-flash',
'tar': 'application/x-tar',
'tif': 'image/tiff',
'tiff': 'image/tiff',
'ts': 'video/mp2t',
'ttf': 'font/ttf',
'txt': 'text/plain',
'vsd': 'application/vnd.visio',
'wav': 'audio/wav',
'weba': 'audio/webm',
'webm': 'video/webm',
'webp': 'image/webp',
'woff': 'font/woff',
'woff2': 'font/woff2',
'xhtml': 'application/xhtml+xml',
'xls': 'application/vnd.ms-excel',
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'xml': 'text/xml',
'xul': 'application/vnd.mozilla.xul+xml',
'zip': 'application/zip',
'7z': 'application/x-7z-compressed'
}

View File

@ -164,9 +164,9 @@ export default {
mime: function ({ value }, ...types) { mime: function ({ value }, ...types) {
return Promise.resolve((() => { return Promise.resolve((() => {
if (value instanceof FileUpload) { if (value instanceof FileUpload) {
const fileList = value.getFileList() const fileList = value.getFiles()
for (let i = 0; i < fileList.length; i++) { for (let i = 0; i < fileList.length; i++) {
const file = fileList[i] const file = fileList[i].file
if (!types.includes(file.type)) { if (!types.includes(file.type)) {
return false return false
} }
@ -227,6 +227,9 @@ export default {
if (Array.isArray(value)) { if (Array.isArray(value)) {
return !!value.length return !!value.length
} }
if (value instanceof FileUpload) {
return value.getFiles().length > 0
}
if (typeof value === 'string') { if (typeof value === 'string') {
return !!value return !!value
} }

View File

@ -46,11 +46,12 @@ describe('FormulateForm', () => {
expect(Object.keys(wrapper.vm.registry)).toEqual(['subinput1', 'subinput2']) expect(Object.keys(wrapper.vm.registry)).toEqual(['subinput1', 'subinput2'])
}) })
it('can set a fields initial value', () => { it('can set a fields initial value', async () => {
const wrapper = mount(FormulateForm, { const wrapper = mount(FormulateForm, {
propsData: { formulateValue: { testinput: 'has initial value' } }, propsData: { formulateValue: { testinput: 'has initial value' } },
slots: { default: '<FormulateInput type="text" name="testinput" />' } slots: { default: '<FormulateInput type="text" name="testinput" />' }
}) })
await flushPromises()
expect(wrapper.find('input').element.value).toBe('has initial value') expect(wrapper.find('input').element.value).toBe('has initial value')
}) })
@ -76,6 +77,31 @@ describe('FormulateForm', () => {
expect(wrapper.vm.formValues).toEqual({ name: '123' }) expect(wrapper.vm.formValues).toEqual({ name: '123' })
}) })
it('can set initial checked attribute on single checkboxes', () => {
const wrapper = mount(FormulateForm, {
propsData: { formulateValue: { box1: true } },
slots: { default: '<FormulateInput type="checkbox" name="box1" />' }
})
expect(wrapper.find('input[type="checkbox"]').is(':checked')).toBe(true)
});
it('can set initial unchecked attribute on single checkboxes', () => {
const wrapper = mount(FormulateForm, {
propsData: { formulateValue: { box1: false } },
slots: { default: '<FormulateInput type="checkbox" name="box1" />' }
})
expect(wrapper.find('input[type="checkbox"]').is(':checked')).toBe(false)
});
it('can set checkbox initial value with options', async () => {
const wrapper = mount(FormulateForm, {
propsData: { formulateValue: { box2: ['second', 'third'] } },
slots: { default: '<FormulateInput type="checkbox" name="box2" :options="{first: \'First\', second: \'Second\', third: \'Third\'}" />' }
})
await flushPromises()
expect(wrapper.findAll('input').length).toBe(3)
});
it('receives updates to form model when individual fields are edited', () => { it('receives updates to form model when individual fields are edited', () => {
const wrapper = mount({ const wrapper = mount({
data () { data () {
@ -184,4 +210,15 @@ describe('FormulateForm', () => {
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)
}) })
it('shows error messages when it includes a checkbox with options', async () => {
const wrapper = mount(FormulateForm, {
propsData: { formulateValue: { box3: [] } },
slots: { default: '<FormulateInput type="checkbox" name="box3" validation="required" :options="{first: \'First\', second: \'Second\', third: \'Third\'}" />' }
})
wrapper.trigger('submit')
await wrapper.vm.$nextTick()
await flushPromises()
expect(wrapper.find('.formulate-input-error').exists()).toBe(true)
})
}) })

View File

@ -8,75 +8,177 @@ import FormulateInputGroup from '@/FormulateInputGroup.vue'
Vue.use(Formulate) Vue.use(Formulate)
test('type "checkbox" renders a box element', () => { describe('FormulateInputBox', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox' } }) it('renders a box element when type "checkbox" ', () => {
expect(wrapper.contains(FormulateInputBox)).toBe(true) const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox' } })
}) expect(wrapper.contains(FormulateInputBox)).toBe(true)
})
test('type "radio" renders a box element', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'radio' } }) it('renders a box element when type "radio"', () => {
expect(wrapper.contains(FormulateInputBox)).toBe(true) const wrapper = mount(FormulateInput, { propsData: { type: 'radio' } })
}) expect(wrapper.contains(FormulateInputBox)).toBe(true)
})
test('box inputs properly process options object in context library', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'} } }) it('box inputs properly process options object in context library', () => {
expect(Array.isArray(wrapper.vm.context.options)).toBe(true) const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'} } })
}) expect(Array.isArray(wrapper.vm.context.options)).toBe(true)
})
test('type "checkbox" with options renders a group', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'} } }) it('renders a group when type "checkbox" with options', () => {
expect(wrapper.contains(FormulateInputGroup)).toBe(true) const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'} } })
}) expect(wrapper.contains(FormulateInputGroup)).toBe(true)
})
test('type "radio" with options renders a group', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'} } }) it('renders a group when type "radio" with options', () => {
expect(wrapper.contains(FormulateInputGroup)).toBe(true) const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'} } })
}) expect(wrapper.contains(FormulateInputGroup)).toBe(true)
})
test('labelPosition of type "checkbox" defaults to after', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox' } }) it('defaults labelPosition to "after" when type "checkbox"', () => {
expect(wrapper.vm.context.labelPosition).toBe('after') const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox' } })
}) expect(wrapper.vm.context.labelPosition).toBe('after')
})
test('labelPosition of type "checkbox" with options defaults to before', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'}}}) it('labelPosition of defaults to before when type "checkbox" with options', () => {
expect(wrapper.vm.context.labelPosition).toBe('before') const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'}}})
}) expect(wrapper.vm.context.labelPosition).toBe('before')
})
test('type radio renders multiple inputs with options', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'} } }) it('renders multiple inputs with options when type "radio"', () => {
expect(wrapper.findAll('input[type="radio"]').length).toBe(2) const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'} } })
}) expect(wrapper.findAll('input[type="radio"]').length).toBe(2)
})
test('type "radio" auto generate ids if not provided', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'} } }) it('generates ids if not provided when type "radio"', () => {
expect(wrapper.findAll('input[type="radio"]').is('[id]')).toBe(true) const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'} } })
}) expect(wrapper.findAll('input[type="radio"]').is('[id]')).toBe(true)
})
/**
* Test data binding it('additional context does not bleed through to attributes with type "radio" and options', () => {
*/ const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'} } })
expect(Object.keys(wrapper.find('input[type="radio"]').attributes())).toEqual(["type", "id", "value"])
test('type "checkbox" sets array of values via v-model', async () => { })
const wrapper = mount({
data () { it('additional context does not bleed through to attributes with type "checkbox" and options', () => {
return { const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'} } })
checkboxValues: [], expect(Object.keys(wrapper.find('input[type="checkbox"]').attributes())).toEqual(["type", "id", "value"])
options: {foo: 'Foo', bar: 'Bar', fooey: 'Fooey'} })
}
}, it('allows external attributes to make it down to the inner box elements', () => {
template: ` const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'}, readonly: 'true' } })
<div> expect(Object.keys(wrapper.find('input[type="radio"]').attributes()).includes('readonly')).toBe(true)
<FormulateInput type="checkbox" v-model="checkboxValues" :options="options" /> })
</div>
` it('does not use the value attribute to be checked', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', value: '123' } })
expect(wrapper.find('input').is(':checked')).toBe(false)
})
it('uses the checked attribute to be checked', async () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', checked: 'true' } })
await flushPromises()
await wrapper.vm.$nextTick()
expect(wrapper.find('input').is(':checked')).toBe(true)
})
it('uses the value attribute to select "type" radio when using options', async () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'}, value: 'b' } })
await flushPromises()
expect(wrapper.findAll('input:checked').length).toBe(1)
})
it('uses the value attribute to select "type" checkbox when using options', async () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2', c: '123'}, value: ['b', 'c'] } })
await flushPromises()
expect(wrapper.findAll('input:checked').length).toBe(2)
})
/**
* it data binding
*/
it('sets array of values via v-model when type "checkbox"', async () => {
const wrapper = mount({
data () {
return {
checkboxValues: [],
options: {foo: 'Foo', bar: 'Bar', fooey: 'Fooey'}
}
},
template: `
<div>
<FormulateInput type="checkbox" v-model="checkboxValues" :options="options" />
</div>
`
})
const fooInputs = wrapper.findAll('input[value^="foo"]')
expect(fooInputs.length).toBe(2)
fooInputs.at(0).setChecked()
await flushPromises()
fooInputs.at(1).setChecked()
await flushPromises()
expect(wrapper.vm.checkboxValues).toEqual(['foo', 'fooey'])
})
it('does not pre-set internal value when type "radio" with options', async () => {
const wrapper = mount({
data () {
return {
radioValue: '',
options: {foo: 'Foo', bar: 'Bar', fooey: 'Fooey'}
}
},
template: `
<div>
<FormulateInput type="radio" v-model="radioValue" :options="options" />
</div>
`
})
await wrapper.vm.$nextTick()
await flushPromises()
expect(wrapper.vm.radioValue).toBe('')
})
it('does not pre-set internal value of FormulateForm when type "radio" with options', async () => {
const wrapper = mount({
data () {
return {
radioValue: '',
formValues: {},
options: {foo: 'Foo', bar: 'Bar', fooey: 'Fooey'}
}
},
template: `
<FormulateForm
v-model="formValues"
>
<FormulateInput type="radio" v-model="radioValue" name="foobar" :options="options" />
</FormulateForm>
`
})
await wrapper.vm.$nextTick()
await flushPromises()
expect(wrapper.vm.formValues.foobar).toBe('')
})
it('does precheck the correct input when radio with options', async () => {
const wrapper = mount({
data () {
return {
radioValue: 'fooey',
options: {foo: 'Foo', bar: 'Bar', fooey: 'Fooey', gooey: 'Gooey'}
}
},
template: `
<div>
<FormulateInput type="radio" v-model="radioValue" :options="options" />
</div>
`
})
await flushPromises()
const checkboxes = wrapper.findAll('input[type="radio"]:checked')
expect(checkboxes.length).toBe(1)
expect(checkboxes.at(0).element.value).toBe('fooey')
}) })
const fooInputs = wrapper.findAll('input[value^="foo"]')
expect(fooInputs.length).toBe(2)
fooInputs.at(0).setChecked()
await flushPromises()
fooInputs.at(1).setChecked()
await flushPromises()
expect(wrapper.vm.checkboxValues).toEqual(['foo', 'fooey'])
}) })

View File

@ -151,9 +151,9 @@ describe('FormulateInputText', () => {
expect(wrapper.find('input').element.value).toBe('initial val') expect(wrapper.find('input').element.value).toBe('initial val')
}) })
it('uses the v-model value as the initial value when value prop is provided', () => { it('uses the value prop as the initial value when v-model provided', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'text', formulateValue: 'initial val', value: 'initial other val' } }) const wrapper = mount(FormulateInput, { propsData: { type: 'text', formulateValue: 'initial val', value: 'initial other val' } })
expect(wrapper.find('input').element.value).toBe('initial val') expect(wrapper.find('input').element.value).toBe('initial other val')
}) })
it('uses a proxy model internally if it doesnt have a v-model', () => { it('uses a proxy model internally if it doesnt have a v-model', () => {

View File

@ -24,6 +24,10 @@ describe('required', () => {
it('passes with empty value if second argument is false', async () => expect(await rules.required({ value: '' }, false)).toBe(true)) it('passes with empty value if second argument is false', async () => expect(await rules.required({ value: '' }, false)).toBe(true))
it('passes with empty value if second argument is false string', async () => expect(await rules.required({ value: '' }, 'false')).toBe(true)) it('passes with empty value if second argument is false string', async () => expect(await rules.required({ value: '' }, 'false')).toBe(true))
it('passes with FileUpload', async () => expect(await rules.required({ value: new FileUpload({ files: [{ name: 'j.png' }] }) })).toBe(true))
it('fails with empty FileUpload', async () => expect(await rules.required({ value: new FileUpload({ files: [] }) })).toBe(false))
}) })

View File

@ -501,6 +501,7 @@
.formualte-file-name { .formualte-file-name {
padding-left: 1.5em; padding-left: 1.5em;
padding-right: 2em; padding-right: 2em;
max-width: 100%;
&::before { &::before {
position: absolute; position: absolute;