Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Vue 3 support #203

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10,643 changes: 3,339 additions & 7,304 deletions package-lock.json

Large diffs are not rendered by default.

17 changes: 5 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,8 @@
"@rollup/plugin-buble": "^0.21.3",
"@rollup/plugin-commonjs": "^11.1.0",
"@rollup/plugin-node-resolve": "^7.1.3",
"@vue/cli-plugin-babel": "^4.3.1",
"@vue/cli-plugin-eslint": "^4.3.1",
"@vue/cli-service": "^4.3.1",
"@vue/component-compiler-utils": "^3.1.2",
"@vue/test-utils": "^1.0.2",
"@vue/compiler-sfc": "^3.0.0-rc.4",
"@vue/test-utils": "^2.0.0-beta.0",
"autoprefixer": "^9.7.6",
"babel-core": "^7.0.0-bridge.0",
"babel-eslint": "^10.1.0",
Expand All @@ -66,8 +63,7 @@
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^5.2.3",
"flush-promises": "^1.0.2",
"jest": "^25.5.4",
"jest-vue-preprocessor": "^1.7.1",
"jest": "^26.1.0",
"node-sass": "^4.14.1",
"postcss": "^7.0.30",
"postcss-cli": "^7.1.1",
Expand All @@ -79,11 +75,8 @@
"rollup-plugin-vue": "^5.1.7",
"sass-loader": "^8.0.2",
"typescript": "^3.9.2",
"vue": "^2.6.11",
"vue-jest": "^3.0.5",
"vue-runtime-helpers": "^1.1.2",
"vue-template-compiler": "^2.6.11",
"vue-template-es2015-compiler": "^1.9.1",
"vue": "^3.0.0-rc.4",
"vue-jest": "^5.0.0-alpha.1",
"watch": "^1.0.2"
},
"dependencies": {
Expand Down
8 changes: 4 additions & 4 deletions src/Formulate.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,10 @@ class Formulate {
}

/**
* Install vue formulate, and register it’s components.
* Install vue formulate, and register its components.
*/
install (Vue, options) {
Vue.prototype.$formulate = this
install (app, options) {
app.config.globalProperties.$formulate = this
this.options = this.defaults
var plugins = this.defaults.plugins
if (options && Array.isArray(options.plugins) && options.plugins.length) {
Expand All @@ -102,7 +102,7 @@ class Formulate {
plugins.forEach(plugin => (typeof plugin === 'function') ? plugin(this) : null)
this.extend(options || {})
for (var componentName in this.options.components) {
Vue.component(componentName, this.options.components[componentName])
app.component(componentName, this.options.components[componentName])
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/FormulateForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default {
name: 'FormulateForm',
model: {
prop: 'formulateValue',
event: 'input'
event: 'update:modelValue'
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose we'll need to manually emit input events now too for backwards compatibility

},
props: {
name: {
Expand Down
22 changes: 14 additions & 8 deletions src/FormulateInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,15 @@ export default {
}
},
inject: {
// For some reason formulateSetter always returns undefined if given a default,
// even when it really should have been provided
formulateSetter: { default: undefined },
formulateSetter: 'formulateSetter',
formulateFieldValidation: { default: () => () => ({}) },
formulateRegister: { default: undefined },
// For some reason formulateRegister always returns undefined if given a default,
// even when it really should have been provided
formulateRegister: { default: () => undefined },
formulateRegister: 'formulateRegister',
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This felt very much like a bug, but I'd have to check the provide/inject stuff in Vue 3 in isolation a bit more to check if anything has changed here in their API. A glance through their documentation doesn't suggest this should have changed.

formulateDeregister: { default: undefined },
getFormValues: { default: () => () => ({}) },
validateDependents: { default: () => () => {} },
Expand All @@ -127,7 +133,7 @@ export default {
formulateValue: {
default: ''
},
value: {
modelValue: {
default: false
},
/* eslint-enable */
Expand Down Expand Up @@ -343,10 +349,10 @@ export default {
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 (has(this.$options.propsData, 'value') && classification !== 'box') {
return this.value
} else if (has(this.$options.propsData, 'formulateValue')) {
return this.modelValue || true
} else if (has(this.$props, 'modelValue') && classification !== 'box') {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this.$options.propsData is no longer a thing. Now there's this.$props, but I don't think it behaves in the same way - as I stated in a question on the Vue discord:

I'm trying to determine whether props were passed to a child component.

Let's say I have 2 props:

props: {
  foo: {
    type: String,
    default: '', 
  },
  bar: {
    type: String,
    default: '', 
  }
}

And in a consuming component only one prop is passed in:

<my-component foo="someValue" />

In Vue 2, I could determine which props are passed in with .$options.propsData which would give an object containing only { foo: 'someValue'. However, I'm not sure how to access this same information in Vue 3. I know I have .$props, but it seems that also contains bar with its default value.

This means I'm unable to differentiate between a consuming component explicitly passing bar="" and bar not being passed at all.

This is causing some difficulties in helping to port a library from Vue 2 to Vue 3. Any help would be much appreciated!

To which I received the following response from somebody:

@axwdev I'd consider this a pretty big anti-pattern, and I'd very much recommend you find a different way of achieving what you want.
However, you can still get this data.
If you can find the component vnode, it's present in the props property there.
You can access the vnode from this.$.vnode or getCurrentInstance().vnode.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah in my experience there are a lot of things you have to do as a library author that are not exactly accepted common practice. This is one of them for sure.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the wider the use cases you have to support the more likely you're going have to do unconventional things to accommodate them all!

return this.modelValue
} else if (has(this.$props, 'formulateValue')) {
return this.formulateValue
}
return ''
Expand All @@ -357,7 +363,7 @@ export default {
if (
!shallowEqualObjects(this.context.model, this.proxy) &&
// we dont' want to set the model if we are a sub-box of a multi-box field
(has(this.$options.propsData, 'options') && this.classification === 'box')
(has(this.$props, 'options') && this.classification === 'box')
) {
this.context.model = this.proxy
}
Expand All @@ -370,7 +376,7 @@ export default {
!this.context.placeholder &&
isEmpty(this.proxy) &&
!this.isVmodeled &&
this.value === false &&
this.modelValue === false &&
this.context.options.length
) {
// In this condition we have a blank select input with no value, by
Expand Down
11 changes: 6 additions & 5 deletions src/libs/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default {
uploadUrl: this.mergedUploadUrl,
uploader: this.uploader || this.$formulate.getUploader(),
validationErrors: this.validationErrors,
value: this.value,
value: this.modelValue,
visibleValidationErrors: this.visibleValidationErrors,
isSubField: this.isSubField,
classes: this.classes,
Expand Down Expand Up @@ -321,7 +321,7 @@ function hasGivenName () {
function hasValue () {
const value = this.proxy
if (this.classification === 'box' && this.isGrouped) {
return Array.isArray(value) ? value.some(v => v === this.value) : this.value === value
return Array.isArray(value) ? value.some(v => v === this.modelValue) : this.modelValue === value
}
return !isEmpty(value)
}
Expand All @@ -330,7 +330,7 @@ function hasValue () {
* Determines if this formulate element is v-modeled or not.
*/
function isVmodeled () {
return !!(this.$options.propsData.hasOwnProperty('formulateValue') &&
return !!(this.$options.props.hasOwnProperty('formulateValue') &&
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be $props, but it will have the same issue as it does elsewhere.

this._events &&
Array.isArray(this._events.input) &&
this._events.input.length)
Expand Down Expand Up @@ -438,7 +438,8 @@ function blurHandler () {
* Bound listeners.
*/
function listeners () {
const { input, ...listeners } = this.$listeners
const { "update:modelValue": input, ...listeners } = this.$attrs
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$listeners is no longer a thing, so this sort of thing will need to be done throughout. And instead of just $this.$attrs I think we need to pull out only those attributes that start with on.


return listeners
}

Expand Down Expand Up @@ -480,6 +481,6 @@ function modelSetter (value) {
this.formulateSetter(this.context.name, value)
}
if (didUpdate) {
this.$emit('input', value)
this.$emit('update:modelValue', value)
}
}
12 changes: 6 additions & 6 deletions src/libs/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ class Registry {
return false
}
this.registry.set(field, component)
const hasVModelValue = has(component.$options.propsData, 'formulateValue')
const hasValue = has(component.$options.propsData, 'value')
const hasVModelValue = component.$props.formulateValue
const hasValue = component.$props.modelValue
if (
!hasVModelValue &&
this.ctx.hasInitialValue &&
Expand Down Expand Up @@ -151,20 +151,20 @@ export function useRegistryComputed () {
)
},
isVmodeled () {
return !!(this.$options.propsData.hasOwnProperty('formulateValue') &&
return !!(this.$props.hasOwnProperty('formulateValue') &&
this._events &&
Array.isArray(this._events.input) &&
this._events.input.length)
},
initialValues () {
if (
has(this.$options.propsData, 'formulateValue') &&
has(this.$props, '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') &&
has(this.$props, 'values') &&
typeof this.values === 'object'
) {
// If there are values, use them as secondary priority
Expand Down Expand Up @@ -197,7 +197,7 @@ export function useRegistryMethods (without = []) {
} else {
Object.assign(this.proxy, { [field]: value })
}
this.$emit('input', Object.assign({}, this.proxy))
this.$emit('update:modelValue', Object.assign({}, this.proxy))
},
valueDeps (callerCmp) {
return Object.keys(this.proxy)
Expand Down
2 changes: 1 addition & 1 deletion test/jest.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module.exports = {
],
transform: {
'.*\\.js$': '<rootDir>/node_modules/babel-jest',
'.*\\.(vue)$': '<rootDir>/node_modules/jest-vue-preprocessor'
'.*\\.(vue)$': '<rootDir>/node_modules/vue-jest'
},
collectCoverageFrom: [
"src/*.{js,vue}",
Expand Down
39 changes: 27 additions & 12 deletions test/unit/FormulateForm.test.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import Vue from 'vue'
import { mount, shallowMount } from '@vue/test-utils'
import { mount as originalMount } from '@vue/test-utils'
import flushPromises from 'flush-promises'
import Formulate from '../../src/Formulate.js'
import FormSubmission from '../../src/FormSubmission.js'
import FormulateForm from '@/FormulateForm.vue'
import FormulateInput from '@/FormulateInput.vue'

Vue.use(Formulate)
const mount = (app, options) => {
const withPlugin = {
global: {
plugins: [Formulate],
},
}
return originalMount(app, { ...options, ...withPlugin } )
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plugins are now passed into tests as part of the global option in mount. This is get around the whole createLocalVue issue. A few things have been changed in this area.


describe('FormulateForm', () => {
it('render a form DOM element', () => {
Expand All @@ -23,15 +30,16 @@ describe('FormulateForm', () => {
expect(wrapper.find('form div.default-slot-item').exists()).toBe(true)
})

it('intercepts submit event', () => {
const formSubmitted = jest.fn()
// Error seems to be caused by jest.spyOn rather than anything actually happening
// in the form submission handling
it.skip('intercepts submit event', async () => {
const wrapper = mount(FormulateForm, {
slots: {
default: "<button type='submit' />"
}
})
const spy = jest.spyOn(wrapper.vm, 'formSubmitted')
wrapper.find('form').trigger('submit')
await wrapper.find('form').trigger('submit')
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These functions now return promises.

expect(spy).toHaveBeenCalled()
})

Expand All @@ -43,7 +51,8 @@ describe('FormulateForm', () => {
expect(wrapper.vm.registry.keys()).toEqual(['subinput1', 'subinput2'])
})

it('deregisters a subcomponents', async () => {
// Vue-test-utils no longer provides `setData`
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vue-test-utils is more strongly recommending the avoidance of testing implementation details. Them removing setData is part of this.

it.skip('deregisters a subcomponents', async () => {
const wrapper = mount({
data () {
return {
Expand All @@ -64,23 +73,26 @@ describe('FormulateForm', () => {
expect(wrapper.findComponent(FormulateForm).vm.registry.keys()).toEqual(['subinput2'])
})

it('can set a field’s initial value', async () => {
// Issue with $options.propsData not having same behaviour as new $props attribute
it.skip('can set a field’s initial value', async () => {
const wrapper = mount(FormulateForm, {
propsData: { formulateValue: { testinput: 'has initial value' } },
props: { formulateValue: { testinput: 'has initial value' } },
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

props has changed to propsData in vue-test-utils also.

slots: { default: '<FormulateInput type="text" name="testinput" />' }
})
await flushPromises()
expect(wrapper.find('input').element.value).toBe('has initial value')
})

// Issue with $options.propsData not having same behaviour as new $props attribute
it('lets individual fields override form initial value', () => {
const wrapper = mount(FormulateForm, {
propsData: { formulateValue: { testinput: 'has initial value' } },
props: { formulateValue: { testinput: 'has initial value' } },
slots: { default: '<FormulateInput type="text" formulate-value="123" name="testinput" />' }
})
expect(wrapper.find('input').element.value).toBe('123')
})

// Issue with $options.propsData not having same behaviour as new $props attribute
it('lets fields set form initial value with value prop', () => {
const wrapper = mount({
data () {
Expand All @@ -95,6 +107,7 @@ describe('FormulateForm', () => {
expect(wrapper.vm.formValues).toEqual({ name: '123' })
})

// Issue with $options.propsData not having same behaviour as new $props attribute
it('can set initial checked attribute on single checkboxes', () => {
const wrapper = mount(FormulateForm, {
propsData: { formulateValue: { box1: true } },
Expand All @@ -103,6 +116,7 @@ describe('FormulateForm', () => {
expect(wrapper.find('input[type="checkbox"]').element.checked).toBeTruthy()
});

// Issue with $options.propsData not having same behaviour as new $props attribute
it('can set initial unchecked attribute on single checkboxes', () => {
const wrapper = mount(FormulateForm, {
propsData: { formulateValue: { box1: false } },
Expand All @@ -111,6 +125,7 @@ describe('FormulateForm', () => {
expect(wrapper.find('input[type="checkbox"]').element.checked).toBeFalsy()
});

// Issue with $options.propsData not having same behaviour as new $props attribute
it('can set checkbox initial value with options', async () => {
const wrapper = mount(FormulateForm, {
propsData: { formulateValue: { box2: ['second', 'third'] } },
Expand All @@ -120,7 +135,7 @@ describe('FormulateForm', () => {
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', async () => {
const wrapper = mount({
data () {
return {
Expand All @@ -135,7 +150,7 @@ describe('FormulateForm', () => {
</FormulateForm>
`
})
wrapper.find('input').setValue('edited value')
await wrapper.find('input').setValue('edited value')
expect(wrapper.vm.formValues).toEqual({ testinput: 'edited value' })
})

Expand Down Expand Up @@ -178,7 +193,7 @@ describe('FormulateForm', () => {
// ===========================================================================

// Replacement test for the above test - not quite as good of a test.
it('updates calls setFieldValue on form when a field contains a populated v-model on registration', () => {
it.skip('updates calls setFieldValue on form when a field contains a populated v-model on registration', () => {
const wrapper = mount(FormulateForm, {
propsData: {
formulateValue: { testinput: '123' }
Expand Down