How to properly split a Vuejs component - javascript

So, I've got a huge Vue file which do more than 1500 lines CSS excluded.
This file is a wrapper for a lot of other components but mostly can't be breaked into smaller components.
The things is I still got some logics like groups of methods which could be extracted. However it has impact on some data or watched properties.
Is it possible to export differents parts of my file and use them as a more "global import".
Something which would be like :
// components/component.logicX.vue/js?
export default {
data() {
return {
....
}
},
methods: {
aFewMethods(){ this.data = "something" },
...
},
watch: {
...
}
}
// component/component.vue
import logicX from '.component.logicX.vue'
export default {
components: {
logicX
},
data() {
data: '' //this data is modified from a logicX component method.
}
}

Related

share a js-class between .vue files

Back in my day we had this crazy thing called include or import or whatever. It meant that you wrote some code in one file and then reused it in different files. So you would create a class in file x, include this file in another file y and instantiate an object from this. Basically this means, that you were able to call a function form file y even though it is written in file x.
How can something like this be done in vue.js? I understand that the new hip way to do it is called "components api". Online I only found really convoluted examples that had some coupling with the dom, something I don't need.
Or should I use mixins? Mixins seem to be working at least, but they have a scope problem.
Here is the file I want to share (x.vue):
<script>
export default
{
data ()
{
return {
a: 'test'
}
}
,mounted ()
{
}
,methods:
{
test ()
{
console.log( "test : " + this.a );
}
}
}
</script>
The y.vue:
<script>
import x from './x.vue'
export default {
name:"whatever"
,components:
{
x
}
,mounted()
{
x.a = "it is working";
x.test();
}
}
</script>
The web-browser tells me that a is not a function. does anyone know where the mistake is? Or should i just create a js class and try to import that?
Thanks for any help.
I believe you're looking for mixins:
https://v2.vuejs.org/v2/guide/mixins.html
They Allow you to share functions, data, hooks and all other from another file.
This would be an approach in your case.
mixins.js:
export default {
methods: {
sayHello () {
console.log( "test : " + this.a);
}
}
};
component.vue:
<script>
import mixins from "#/path/to/mixins/mixins";
export default {
name: "whatever",
mixins: [mixins],
data ()
{
return {
a: 'HELLO'
}
},
created() {
this.sayHello();
}
}
</script>
For further information: Mixins
EDIT: Looks like there is something that matches your needs!
mixins.js:
export default {
name: 'Mixin',
methods: {
sayHello() {
console.log("Hello")
}
}
}
component.js:
export default {
name: 'whatever',
mixins: [Mixin],
created() {
this.$super(Mixin).sayHello();
}
}
EDIT2: As you requested, an simple example to pass arguments.
mixins.js:
export default {
methods: {
sayHello(foo, bar) {
console.log(foo + bar)
}
}
}
component.js:
export default {
name: 'whatever',
mixins: [Mixin],
data () {
return {
baz: 'Say'
}
},
created() {
this.sayHello(this.baz, " Hello");
}
}
Mixins are not useful in this case as there cannot be more than one instance of a mixin. It is like copy and pastes the code from the mixin into the other file.
Sharing code between Vue components can be done by many savers with standard js classes. All that is needed is that the class is declared with export default class.

Two way binding computed property with mappers

I use vuex in my Vue 2 project.
I have this HTML element and I try to implement two way binding:
<input v-model="message">
computed: {
message: {
get () {
return this.$store.state.obj.message
},
set (value) {
this.$store.commit('updateMessage', value)
}
}
}
inside get and set I want to use mappers so the code will look cleaner:
computed: {
message: {
get () {
return ...mapState("obj", ["message"])
},
set (value) {
...mapMutations("obj/updateMessage", value)
}
}
}
But I get errors on two rows:
return ...mapState("obj", ["message"]) - Expression expected.
...mapMutations("obj/updateMessage", value) - Declaration or statement expected.
How can I use mappers inside get and set?
UPDATE:
mapMutations and mapState are imported to the component.
You will need to import them first, as you did
import { mapState, mapActions } from 'vuex'
Import actions tho, and not mutations. Indeed, only actions are async and the flow should always be dispatch a action > commit a mutation > state is updated.
Then, plug them where they belong
computed: {
...mapState('obj', ['message']),
// other computed properties ...
}
methods: {
...mapActions('obj', ['updateMessage']),
// other methods ...
}
Then comes the interesting part
computed: {
message: {
get () {
const cloneDeepMessage = cloneDeep(this.message)
// you could make some destructuring here like >> const { id, title, description } = cloneDeepMessage
return cloneDeepMessage // or if destructured some fields >> return { id, title, description }
},
set (value) {
this.updateMessage(value)
}
}
}
As you can see, I would recommend to also import cloneDeep from 'lodash/cloneDeep' to avoid mutating the state directly thanks to cloneDeep when accessing your state.
This is the kind of warning that the Vuex strict mode will give you, that's why I recommend to enable it in development only.
The official docs are not super explicit on this part (you need to read various parts of them and mix them all together) but it's basically a good way of doing things IMHO.

How to loop over components object in the template?

Usually we define in a Nuxt.js component something like this:
<script>
export default {
components: {
// components
}
data() {
return {
// key/value data
}
},
methods: {
// method definitions
}
}
</script>
Is there a way to read the components object as we read data() and methods ?
This is because I have several components and I want to loop on them to refactor parts of my code.
You can get Component data by using $options.
Try this.
created() {
console.log(this.$options.components)
}
it returns an object, keys are the component names, values are the contructors.
codepen - https://codesandbox.io/s/yk9km5m0wv

VueJS: Best practice for working with global object between components?

there is User.js class and user object(user = new User();).
The user object is being used in all nested components. in User class there are so many important methods.
How can I simply use/access this.user or this.$user and its methods in any component?
1-solution (temporary working solution): Setting user in vuex's store and define in all components' data:
data(){
return {
user:this.$store.state.user
}
}
Cons: in every component, this should be added. Note: there are so many components.
2-solution: adding user to Vue's prototype like plugin:
Vue.prototype.$user = user
Cons: when user's data changes, it doesn't effect in DOM element (UI).
3-solution: putting to components's props.
Cons: in every component, this should be added. Note: Again there are so many components.
All of the solutions I found have issues, especially as the project gets larger and larger.
Any suggestion and solution will be appreciated!
Note: Applies for Vue 2x
Proposal 1: Using getters from vuex
You could use getters along with mapGetters from Vuex to include users within computed properties for each component.
Vuex
getters: {
// ...
getUser: (state, getters) => {
return getters.user
}
}
component
import { mapGetters } from 'vuex'
computed: {
...mapGetters([getUser])
}
Proposal 2: add a watcher via plugin
Vue
// When using CommonJS via Browserify or Webpack
const Vue = require('vue')
const UserPlug = require('./user-watcher-plugin')
// Don't forget to call this
Vue.use(UserPlug)
user-watcher-plugin.js
const UserPlug = {
install(Vue, options) {
// We call Vue.mixin() here to inject functionality into all components.
Vue.watch: 'user'
}
};
export default UserPlug;
Proposal 3: add a computed property user as plugin via mixin
Vue
// When using CommonJS via Browserify or Webpack
const Vue = require('vue')
const UserPlug = require('./user-watcher-plugin')
// Don't forget to call this
Vue.use(UserPlug)
user-watcher-plugin.js
const UserPlug = {
install(Vue, options) {
// We call Vue.mixin() here to inject functionality into all components.
Vue.mixin({
computed: {
user: function() {
return this.$store.state.user
}
}
})
}
};
export default UserPlug;
Based on #Denis answer, specifically Proposal 3, Here is the UserPlugin.js:
import store from '#/store/store';
import User from './User';
const UserPlugin = {
install(Vue) {
const $user = new User();
window.$user = $user;
store.commit('setUser', $user);
Vue.mixin({
computed: {
$user() {
return store.state.user;
}
}
});
}
};
export default UserPlugin;
and main.js:
import UserPlugin from './common/UserPlugin';
Vue.use(UserPlugin);
new Vue({
render: h => h(App)
}).$mount('#app');
For further usage, I published small library for solving these kinda issues:
https://www.npmjs.com/package/vue-global-var
Assuming you don't actually use all methods/attributes of user in every component, but a subset of them everytime, I don't see any reason why solution 1 & 2 do not work for you, since passing the whole user object to every component is not necessary.
Let's say your object User have some attributes (a1, a2, a3, etc.) and methods (m1, m2, m3...). If a component only needs some of them (e.g. a1, a2, m1, m2, m3) then with Vuex, you can use mapping functions (mapState, mapGetters, mapMutations and mapActions) to get the exact info from user
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
export default {
computed: {
...mapState('user', [ 'a1' ]),
...mapGetters('user', [ 'a2' ])
},
methods: {
...mapMutations('user', [ 'm1' ]),
...mapActions('user', [ 'm2', 'm3' ])
}
}
For solution 2 (using prototype), to make component update when user data changes, you can map the necessary data to component via methods.
export default {
methods: {
userA1() {
return this.$user.attributes.a1;
},
userM1() {
this.$user.methods.m1();
}
// and so on
}
}
Even better, you can create mixins to explicitly map data from user, and reuse your mixins to avoid duplicated code in components. It can be applied for both Vuex solution and prototype solution.
// mixin1:
const mixin1 = {
computed: {
...mapState('user', [ 'a1' ]),
},
methods: {
...mapMutations('user', [ 'm1' ])
}
}
// mixin2:
const mixin2 = {
computed: {
...mapGetters('user', [ 'a2' ]),
},
methods: {
...mapActions('user', [ 'm2', 'm3' ])
}
}
// component1
export default {
mixins: [ mixin1 ]
}
// component 2
export default {
mixins: [ mixin1, mixin2 ]
}
But if you really need to pass the whole object user to every component, then nothing could do. Rather, you should review your implementation and see if there is any better way to break the object into smaller meaningful ones.
You can use mixins to add User.js to your root component like
import userLib from './User';
//User.js path should correct
Then
var app = new Vue({
router,
mixins: [
userLib
],
//.....
});
After that you can use any of these User method in your any component like
this.$parent.userClassMehtod();
or if any data access
this.$parent.userClassData;
Finally dont forget to add export default{//..} in User.js
Note: This is only work if you export all method of User.js into export default
I just created the minimal codesandbox to clear the idea of how dependency Injection works in vue.
You can have a second Vue instance and declare a reactive property.
See: Reactivity in depth

Pass prop as module name when mapping to namespaced module

I'm trying to pass the store module namespace via props to a component. When I try and map to getters with the prop, it throws this error,
Uncaught TypeError: Cannot convert undefined or null to object
If I pass the name as a string it works.
This Works
<script>
export default {
props: ['store'],
computed: {
...mapGetters('someString', [
'filters'
])
}
}
</script>
This does not work
this.store is defined
this.store typeof is a String
<script>
export default {
props: ['store'],
computed: {
...mapGetters(this.store, [
'filters'
])
}
}
</script>
I used this style utilising beforeCreate to access the variables you want, I used the props passed into the component instance:
import { createNamespacedHelpers } from "vuex";
import module from '#/store/modules/mymod';
export default {
name: "someComponent",
props: ['namespace'],
beforeCreate() {
let namespace = this.$options.propsData.namespace;
const { mapActions, mapState } = createNamespacedHelpers(namespace);
// register your module first
this.$store.registerModule(namespace, module);
// now that createNamespacedHelpers can use props we can now use neater mapping
this.$options.computed = {
...mapState({
name: state => state.name,
description: state => state.description
}),
// because we use spread operator above we can still add component specifics
aFunctionComputed(){ return this.name + "functions";},
anArrowComputed: () => `${this.name}arrows`,
};
// set up your method bindings via the $options variable
this.$options.methods = {
...mapActions(["initialiseModuleData"])
};
},
created() {
// call your actions passing your payloads in the first param if you need
this.initialiseModuleData({ id: 123, name: "Tom" });
}
}
I personally use a helper function in the module I'm importing to get a namespace, so if I hadmy module storing projects and passed a projectId of 123 to my component/page using router and/or props it would look like this:
import { createNamespacedHelpers } from "vuex";
import projectModule from '#/store/project.module';
export default{
props['projectId'], // eg. 123
...
beforeCreate() {
// dynamic namespace built using whatever module you want:
let namespace = projectModule.buildNamespace(this.$options.propsData.projectId); // 'project:123'
// ... everything else as above with no need to drop namespaces everywhere
this.$options.computed = {
...mapState({
name: state => state.name,
description: state => state.description
})
}
}
}
Hope you find this useful.
I tackled this problem for hours, too. Then I finally came up with one idea.
Add attachStore function in a child vue component. A function nama is not important. Any name is ok except vue reserved word.
export default {
:
attachStore (namespace) {
Object.assign(this.computed, mapGetters(namespace, ['filters']))
}
}
When this vue component is imported, call attachStore with namespace parameter. Then use it at parent components attributes.
import Child from './path/to/child'
Child.attachStore('someStoresName')
export default {
name: 'parent',
components: { Child }
:
}
The error you're encountering is being thrown during Vue/Vuex's initialization process, this.store cannot be converted because it doesn't exist yet. I haven't had to work with namespacing yet, and this is untested so I don't know if it will work, but you may be able to solve this problem by having an intermediary like this:
<script>
export default {
props: ['store'],
data {
namespace: (this.store !== undefined) ? this.store : 'null',
},
computed: {
...mapGetters(this.namespace, [
'filters'
])
}
}
</script>
That ternary expression will return a string if this.store is undefined, if it isn't undefined then it will return the value in this.store.
Note that there is also a discussion about this on Vue's Github page here: https://github.com/vuejs/vuex/issues/863
Until Vue formally supports it, I replaced something like
...mapState({
foo: state => state.foo
})
with
foo () {
return this.$store.state[this.namespace + '/foo'] || 0
}
Where namespace is passed to my child component using a prop:
props: {
namespace: { type: String, required: true }
}

Categories

Resources