Pull to refresh

Vue mixins, the explicit way (by an example of BEM modifiers plugin)

Reading time3 min
Views7K


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:


  1. Boilerplate code in every component. (at least in a Vue approach)
  2. Dependency on a baseClass argument.
  3. No clear indication where the mods property came from.
  4. 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.

Tags:
Hubs:
+26
Comments0

Articles

Change theme settings