1
0
mirror of synced 2025-01-31 14:51:41 +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; }
.formulate-input[data-classification="file"] .formulate-files .formualte-file-name {
padding-left: 1.5em;
padding-right: 2em; }
padding-right: 2em;
max-width: 100%; }
.formulate-input[data-classification="file"] .formulate-files .formualte-file-name::before {
position: absolute;
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 mimes from './libs/mimes'
/**
* The file upload class holds and represents a files upload state durring
@ -15,9 +16,35 @@ class FileUpload {
this.fileList = input.files
this.files = []
this.options = options
this.addFileList(this.fileList)
this.context = context
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,
path: false,
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 () {
if (
this.hasUploader &&
typeof this.hasUploader.request === 'function' &&
typeof this.hasUploader.get === 'function' &&
typeof this.hasUploader.delete === 'function' &&
typeof this.hasUploader.post === 'function'
typeof this.context.uploader.request === 'function' &&
typeof this.context.uploader.get === 'function' &&
typeof this.context.uploader.delete === 'function' &&
typeof this.context.uploader.post === 'function'
) {
return true
}
@ -76,14 +106,19 @@ class FileUpload {
if (this.uploaderIsAxios()) {
const formData = new FormData()
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: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: progressEvent => {
// args[1] here is the upload progress handler function
args[1](Math.round((progressEvent.loaded * 100) / progressEvent.total))
}
})
.then(res => res.data)
.catch(err => args[2](err))
}
return this.context.uploader(...args)
@ -135,7 +170,8 @@ class FileUpload {
*/
removeFile (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()
this.files.map(file => transfer.items.add(file.file))
this.fileList = transfer.files

View File

@ -43,6 +43,8 @@ class Formulate {
rules,
locale: 'en',
uploader: fauxUploader,
uploadUrl: false,
fileUrlKey: 'url',
uploadJustCompleteDuration: 1000,
plugins: [],
locales: {
@ -162,6 +164,21 @@ class Formulate {
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.
*/

View File

@ -92,21 +92,33 @@ export default {
}
}
},
deep: true,
immediate: false
deep: true
}
},
created () {
if (this.hasInitialValue) {
this.internalFormModelProxy = this.initialValues
}
this.applyInitialValues()
},
methods: {
applyInitialValues () {
if (this.hasInitialValue) {
this.internalFormModelProxy = this.initialValues
}
},
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')

View File

@ -173,16 +173,17 @@ export default {
validationRules: {
type: Object,
default: () => ({})
},
checked: {
type: [String, Boolean],
default: false
}
},
data () {
return {
/**
* @todo consider swapping out nanoid for this._uid
*/
defaultId: nanoid(9),
localAttributes: {},
internalModelProxy: this.formulateValue || this.value,
internalModelProxy: this.getInitialValue(),
behavioralErrorVisibility: (this.errorBehavior === 'live'),
formShouldShowErrors: false,
validationErrors: [],
@ -219,6 +220,7 @@ export default {
}
},
created () {
this.applyInitialValue()
if (this.formulateFormRegister && typeof this.formulateFormRegister === 'function') {
this.formulateFormRegister(this.nameOrFallback, this)
}
@ -226,6 +228,30 @@ export default {
this.performValidation()
},
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) {
if (!shallowEqualObjects(value, this.localAttributes)) {
this.localAttributes = value

View File

@ -1,7 +1,7 @@
<template>
<div class="formulate-input-group">
<component
:is="optionContext.component"
:is="subComponent"
v-for="optionContext in optionsWithContext"
:key="optionContext.id"
v-model="context.model"
@ -24,16 +24,36 @@ export default {
options () {
return this.context.options || []
},
subComponent () {
// @todo - rough-in for future flexible input-groups
return 'FormulateInput'
},
optionsWithContext () {
const { options, labelPosition, attributes, classification, ...context } = this.context
return this.options.map(option => this.groupItemContext(context, option))
const {
// 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: {
groupItemContext (...args) {
return Object.assign({}, ...args, {
component: 'FormulateInput'
})
groupItemContext (context, option, groupAttributes) {
const optionAttributes = {}
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)
}
},
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 () {
// 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.

View File

@ -1,5 +1,5 @@
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
@ -19,9 +19,10 @@ export default {
label: this.label,
labelPosition: this.logicalLabelPosition,
attributes: this.elementAttributes,
performValidation: this.performValidation.bind(this),
blurHandler: blurHandler.bind(this),
imageBehavior: this.imageBehavior,
uploadUrl: this.uploadUrl,
uploadUrl: this.mergedUploadUrl,
uploader: this.uploader || this.$formulate.getUploader(),
uploadBehavior: this.uploadBehavior,
preventWindowDrops: this.preventWindowDrops,
@ -37,7 +38,8 @@ export default {
mergedErrors,
hasErrors,
showFieldErrors,
mergedValidationName
mergedValidationName,
mergedUploadUrl
}
/**
@ -111,6 +113,14 @@ function mergedValidationName () {
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)
* @return {boolean}
@ -220,7 +230,9 @@ function modelGetter () {
* Set the value from a model.
**/
function modelSetter (value) {
this.internalModelProxy = value
if (!shallowEqualObjects(value, this.internalModelProxy)) {
this.internalModelProxy = value
}
this.$emit('input', value)
if (this.context.name && typeof this.formulateFormSetter === 'function') {
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) {
return Promise.resolve((() => {
if (value instanceof FileUpload) {
const fileList = value.getFileList()
const fileList = value.getFiles()
for (let i = 0; i < fileList.length; i++) {
const file = fileList[i]
const file = fileList[i].file
if (!types.includes(file.type)) {
return false
}
@ -227,6 +227,9 @@ export default {
if (Array.isArray(value)) {
return !!value.length
}
if (value instanceof FileUpload) {
return value.getFiles().length > 0
}
if (typeof value === 'string') {
return !!value
}

View File

@ -46,11 +46,12 @@ describe('FormulateForm', () => {
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, {
propsData: { formulateValue: { testinput: 'has initial value' } },
slots: { default: '<FormulateInput type="text" name="testinput" />' }
})
await flushPromises()
expect(wrapper.find('input').element.value).toBe('has initial value')
})
@ -76,6 +77,31 @@ describe('FormulateForm', () => {
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', () => {
const wrapper = mount({
data () {
@ -184,4 +210,15 @@ describe('FormulateForm', () => {
expect(wrapper.find('input[type="text"]').element.value).toBe('Dave Barnett')
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)
test('type "checkbox" renders a box element', () => {
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' } })
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'} } })
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'} } })
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'} } })
expect(wrapper.contains(FormulateInputGroup)).toBe(true)
})
test('labelPosition of type "checkbox" defaults to 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'}}})
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'} } })
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'} } })
expect(wrapper.findAll('input[type="radio"]').is('[id]')).toBe(true)
})
/**
* Test data binding
*/
test('type "checkbox" sets array of values via v-model', 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>
`
describe('FormulateInputBox', () => {
it('renders a box element when type "checkbox" ', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox' } })
expect(wrapper.contains(FormulateInputBox)).toBe(true)
})
it('renders a box element when type "radio"', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'radio' } })
expect(wrapper.contains(FormulateInputBox)).toBe(true)
})
it('box inputs properly process options object in context library', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'} } })
expect(Array.isArray(wrapper.vm.context.options)).toBe(true)
})
it('renders a group when type "checkbox" with options', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'} } })
expect(wrapper.contains(FormulateInputGroup)).toBe(true)
})
it('renders a group when type "radio" with options', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'} } })
expect(wrapper.contains(FormulateInputGroup)).toBe(true)
})
it('defaults labelPosition to "after" when type "checkbox"', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox' } })
expect(wrapper.vm.context.labelPosition).toBe('after')
})
it('labelPosition of defaults to before when type "checkbox" with options', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'}}})
expect(wrapper.vm.context.labelPosition).toBe('before')
})
it('renders multiple inputs with options when type "radio"', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'} } })
expect(wrapper.findAll('input[type="radio"]').length).toBe(2)
})
it('generates ids if not provided when type "radio"', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'} } })
expect(wrapper.findAll('input[type="radio"]').is('[id]')).toBe(true)
})
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"])
})
it('additional context does not bleed through to attributes with type "checkbox" and options', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'checkbox', options: {a: '1', b: '2'} } })
expect(Object.keys(wrapper.find('input[type="checkbox"]').attributes())).toEqual(["type", "id", "value"])
})
it('allows external attributes to make it down to the inner box elements', () => {
const wrapper = mount(FormulateInput, { propsData: { type: 'radio', options: {a: '1', b: '2'}, readonly: 'true' } })
expect(Object.keys(wrapper.find('input[type="radio"]').attributes()).includes('readonly')).toBe(true)
})
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')
})
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' } })
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', () => {

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 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 {
padding-left: 1.5em;
padding-right: 2em;
max-width: 100%;
&::before {
position: absolute;