Apply the BEM specification in vue3, use the sass function to splice the BEM class name, and export sass variables to access in js

BEM specification in css

Bem is a class naming specification in CSS, and elementui names components according to the bem specification. Bem meaning:

  • Block represents a block, such as an input component, which is a component block, and the hierarchy is divided by -: el-input
  • Element represents one of the elements in a block, such as the shell part of the block, separated from the block by __: el-input__wrapper
  • Modifier is used to modify blocks or elements, representing their types, separated by -- from the modified block or element: el-buttom--primary
/* Block naming hierarchy is represented by - */
.tag-block {<!-- -->}
/* The elements in the block indicate the block they belong to with __ */
.tag-block__header {<!-- -->}
/* Element style modification with -- */
.tag-block__header--primary {<!-- -->}
.tag-block__bottom--blue {<!-- -->}

If you use sass to write classes, you usually use nesting to represent hierarchical relationships, but compiling css classes that conform to the bem specification does not require nesting.

For example, .tag-block .tag-block__header{ color:red; }, the class name already has hierarchical information, no need to nest, it should be directly written as .tag-block__header{ color :red; }.

Sass provides @at-root to modify classes, which can represent the hierarchical relationship of classes in sass, but when compiling classes into css, they will be raised to the top level and unnested.

/*scss*/
.el-input {
  color: red;
  @at-root.el-input__wraper { color: green; }
}
/*compile to css*/
.el-input { color: red; }
.el-input__wraper { color: green; }

BEM specification is used in vue3 project

Bem is a css specification that can be used in any project.

Since the bem specification uses fixed segmentation symbols like - __ --, in order to improve reusability and standardization, this article will show how to customize the sass function in vue3 for us to use The specified delimiter is used to concatenate class names.

At the same time, in order to synchronize the class names of sass in js, the export of sass variables will be configured, and these variables will be imported in js. In order to improve reuse, js also encapsulates the function of splicing class names that has the same function as the sass function, and mounts it on the vue instance.

Use vite as build tool

Define the sass function and automatically splice it into a class name that conforms to the specification

  1. Install sass dependencies npm i sass -D;

  2. Use the function functions provided by sass to define splicing sass functions that conform to the bem naming convention

    // ./src/bem.scss
    
    $block-split: "-" !default; // block separator, !default means no need to access other variables, it is an independent value
    $element-split: "__" !default; // element separator
    $modifier-split: "--" !default; // modifier splitter
    $namespace: 'el' !default; // namespace, usually the project name
    
    
    //Mix in function, automatically splicing class name: el-xxx
    // xxx is the actual parameter corresponding to $block
    @mixin b($block) {
        $B: $namespace + $block-split + $block; //local variables
        //Interpolation syntax#{}
        .#{$B} {
            @content; // content replacement
        }
    }
    /*
    * Example of use
    *
    * @include b(input) {
    * color: green;
    * }
    * compiles to .el-input {color: green;}
    */
    
    // Mix in functions, automatically splicing class names, and not nesting at the same level as the parent: __xxx
    @mixin e($element) {
        // @at-root indicates that the generated classes will not be nested
        @at-root {
            // splicing & amp;, splicing the class name with the previous class: & amp;__xxx
            #{ & amp; + $element-split + $element} {
                @content;
            }
        }
    }
    /*
    Example usage, (ps: must be nested inside a block):
    @include b(input) {
        e(wrapper) {
            color: red;
        }
    }
    
    compiles to:
    .el-input__wraper { color: red;}
    */
    
    // Mix in functions, automatically splicing class names, and not nesting at the same level as the parent: --xxx
    @mixin m($modifier) {
    
        @at-root {
            #{ & amp; + $modifier-split + $modifier} {
                @content;
            }
        }
    }
    
  3. In order not to manually introduce the functions in the above bem.css in each component, you can directly configure vite.config.css to introduce bem.sass for each component

    // ./vite.config.ts
    import {<!-- --> fileURLToPath, URL } from 'node:url'
    
    import {<!-- --> defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    
    // https://vitejs.dev/config/
    export default defineConfig({<!-- -->
      plugins: [
        vue(),
      ],
      css: {<!-- -->
        preprocessorOptions: {<!-- -->
          scss: {<!-- -->
            // Import ./src/bem.scss for the scss type style tag of each component
            additionalData: '@import "./src/bem.scss";'
          }
        }
      },
      resolve: {<!-- -->
        alias: {<!-- -->
          '@': fileURLToPath(new URL('./src', import.meta.url))
        }
      }
    })
    
  4. Write the class name of the bem specification in the component

    <!-- ./src/component/DemoSass.vue -->
    <script setup lang="ts">
    import My from './components/My.vue'
    </script>
    
    
    <template>
      <My></My>
      <div class="el-title el-title--primary">
        <div :class="el-title__wraper">Xiao Ming</div>
      </div>
    </template>
    
    <style lang="scss">
    @include b(title) {<!-- -->
      color: gray;
      @include e(wraper) {<!-- -->
        border: 1px solid black
      }
      @include m(primary) {<!-- -->
        background-color: #fff;
      }
    }
    /*
    compile result
    .el-title {
      color: gray;
    }
    .el-title__wraper {
      border: 1px solid black;
    }
    .el-title --primary {
      background-color: #fff;
    }
    */
    </style>
    

Export the variables in sass, get them in js, and splice them into class names

In the sass code, you can use the sass function to splice the class name flexibly, but the class name must be written in the label, so writing the sass function does not make much sense.

Variables in sass can be exported so that js can also access them. Spliced in js to the same class name as in sass.

  1. Use :export to export sass variables

    // ./src/config.module.scss
    @import './bem.scss'; // Import the above bem.scss, so that you can access the variables defined in bem.scss
    :export {
        b: $block-split;// $block-split is a variable defined in bem.scss
        e: $element-split;
        m: $modifier-split;
        namespace: $namespace;
    }
    
  2. Import variables exported by sass in js

    When sass exports string variables, double quotes will be included, so when using it in js, remove the double quotes

    // ./src/handleScssVar.ts name can be customized
    import config from './config.module.scss'; // Import variables exported in sass
    const scssVarObj: any = Object. create(null);
    
    // Note that when sass is exported, the double quotes of the variable will also be included: such as {b : ""-""},
    // Sass will export the double quotes as part of the variable, remove the double quotes here
    Object.keys(config).map((item: string) => scssVarObj[item] = config[item].slice(1, -1))
    
    export default scssVarObj; // The object exported in this way is the normal value in js, such as {b: '-'}
    
    // Or directly export several functions and directly return the stitched class name
    
    /**
     *
     * @param blockName the name of the block
     * @returns Returns the class name of the namespace splicing block name, such as: el-xxx
     */
    export function blockClass(blockName: string): string {<!-- -->
        return scssVarObj.namespace + scssVarObj.b + blockName
    }
    /**
     *
     * @param blockName the name of the block
     * @param elementName the name of the element inside the block
     * @returns sequentially splicing namespace, block, and class name of elements in the block, such as: el-xxx__wraper
     */
    export function elementClass(blockName: string, elementName: string): string {<!-- -->
        return blockClass(blockName) + scssVarObj.e + elementName
    }
    /**
     *
     * @param blockName the name of the block
     * @param modifierName Modifier block type
     * @returns sequentially splicing namespace, block, and block type modified class name, such as: el-xxx--primary
     */
    export function modifierClass(blockName: string, modifierName: string): string {<!-- -->
        return blockClass(blockName) + scssVarObj.m + modifierName
    }
    
    
  3. In order to access these functions without importing the above js files in each vue file, you can hang them on the vue instance

    // ./main.ts main entry file
    import {<!-- --> createApp } from 'vue'
    import App from './App.vue'
    import {<!-- --> blockClass, elementClass, modifierClass } from './handleScssVar';
    
    const app = createApp(App);
    // Hang on to the vue instance
    app.config.globalProperties.$blockClass = blockClass;
    app.config.globalProperties.$elementClass = elementClass;
    app.config.globalProperties.$modifierClass = modifierClass;
    app.mount('#app')
    
  4. At this point, these functions can be accessed through this, but an error will be reported in ts, because the ts declaration file of the vue module does not have these interfaces, we need to declare them

    // ./vueExtend.d.ts
    // Prevent overwriting the global.
    export {<!-- -->}
    
    declare module 'vue' {<!-- -->
      // Extend the interface on the vue module
      interface ComponentCustomProperties {<!-- -->
        $blockClass: (key: string) => string;
        $modifierClass(blockName: string, modifierName: string): string;
        $elementClass(blockName: string, elementName: string): string
      }
    }
    
  5. When finally used, the above component can be changed to

    <!-- ./src/component/DemoSass.vue -->
    <script setup lang="ts">
    import My from './My.vue'
    </script>
    
    <template>
      <My></My>
      <div :class="[$blockClass('title'), $modifierClass('title', 'primary')]">
        <div :class="$elementClass('title', 'wraper')">Xiao Ming</div>
      </div>
    </template>
    
    <style lang="scss">
    @include b(title) {<!-- -->
      color: gray;
      @include e(wraper) {<!-- -->
        border: 1px solid black
      }
      @include m(primary) {<!-- -->
        background-color: #fff;
      }
    }
    /*
    compile result
    .el-title {
      color: gray;
    }
    .el-title__wraper {
      border: 1px solid black;
    }
    .el-title --primary {
      background-color: #fff;
    }
    */
    </style>