Vue mixins are the recommended way of sharing common functionality between components. They are perfectly fine until you use more than one for them. That's because they are implicit by design and pollute your component's context. Let's try to fix this by giving them as much explicitness as we can.
Goal
We would like to have a global mixin that gives any component a prop called types
and outputs an array of CSS classes called mods
that derive from baseClass
.
Given this markup:
<SampleComponent :types="['active', 'block']"></SampleComponent>
We would expect to have this (assuming our baseClass
is sample-component
):
<div class="sample-component sample-component--active sample-component--block"></div>
Naive Approach
From reading only Vue documentation your first thought might be to just use built-in property merge and provide mods
as a computed property in a desired component.
// bemMods.js
export default (baseClass) => ({
props: {
types: {
type: Array,
default: () => []
}
},
computed: {
mods() {
return this.types.map(type => `${baseClass}--${type}`);
}
}
})
// SampleComponent.vue
<template>
<div class="sample-component" :class="mods"><slot /></div>
</template>
import bemMods from 'bemMods.js';
export default {
name: 'SampleComponent',
mixins: [
bemMods('sample-component')
]
}
This approach suffers from many problems:
- Boilerplate code in every component. (at least in a Vue approach)
- Dependency on a
baseClass
argument. - No clear indication where the
mods
property came from. - Name conflicts are easily possible.
We'll try to fix all of these problems in a next step.
Mixin with an explicit export
Vue has a Dependency Injection mechanism, called Inject\Provide. It can potentially solve our problem with polluting context.
At first, let's switch from a simple mixin to a plugin, that accepts options, which we'll use later to avoid name conflicts.
Secondly, we can also reuse our component's name as a baseClass
and not include that as a custom option in every single component.
And lastly we 'll leave an option to pass baseClass
as a function argument in case our component's baseClass
doesn't match its name.
// bemMods.js
// Converts ComponentName to component-name
const transformName = string => string.replace(/\s+/g, '-').toLowerCase();
const install = (Vue, { propName = 'types', modsName = 'mods' } = {}) => {
Vue.mixin({
props: {
// Prop name is now dynamic and allows to avoid conflits
[propName]: {
type: Array,
default: () => [],
}
},
// Dependency injection forces us to explicitly require that function
provide: {
[modsName](baseClass) {
baseClass = baseClass || transformName(this.$options.name);
return (this[propName] || []).map(type => `${baseClass}--${type}`);
}
}
});
};
export default { install };
We're now ready to register our plugin globally.
import Vue from 'vue';
import bemMods from 'bemMods.js';
Vue.use(bemMods);
We can also customize how our props are called, by providing an options object.
import Vue from 'vue';
import bemMods from 'bemMods.js';
Vue.use(bemMods, {
propName: 'modifiers',
modsName: 'classes'
});
And here's how our component looks like after mixin refactoring:
<template>
<div class="sample-component" :class="mods"><slot /></div>
</template>
export default {
name: 'SampleComponent',
// Explicit property
inject: ['mods']
}
Let's imagine our component doesn't have name
or it has a different baseClass
from it's name:
<template>
<div class="special-component" :class="mods('snowflake')"><slot /></div>
</template>
export default {
name: 'SpecialComponent',
inject: ['mods']
}
Or if we want to be ready for a refactoring or plugin removal:
export default {
name: 'SomeComponent',
inject: {
// If mixin export property changes name it's now possible to replace it in every single component instance withouth any additional rework
'mods': {
// In this case 'mods' becomes 'classes'
from: 'classes',
}
}
}
You can also use Symbol
as a mods name to completely eliminate name conflicts, but that would require you to include that symbol in every single component where you would like to use bemMods
.
We didn't implicitly specifiy our prop name, but that's a core mixin limitation, which we tried to overcome with a custom prop name in a plugin config.
Hope this was helpful for you and you've found a better way of writing mixins for Vue.