preface

We know how to create the tag component and the built-in service component by creating the air-UI component (5) and the air-UI component (6). In this section, we will talk about how to create the directive component.

This time we will use the loading component of Element UI as a reference. This component is interesting and can be called in two ways:

  1. Global instruction mode
  2. How global methods are served

What this really tells us is that a component can be represented in any number of ways you want, whether it’s as a label, or as a directive, or as a service. Let’s look at the directory structure first:

components/
|    |--- loading/
|    |     |--- src/
|    |     |     |--- directive.js
|    |     |     |--- index.js
|    |     |     |--- loading.vue
|    |     |--- index.js
Copy the code

From the directory structure, it should make sense. .vue ends with DOM rendering, directive.js is the instruction encapsulation logic, and index.js is the service encapsulation logic. The index.js file in the root directory is exported

Loading service mode

We already saw how to create service-type components in the previous section, but it doesn’t hurt to review them again in this section. First let’s look at the code for load. vue, a vue component:

<template>
  <transition name="air-loading-fade" @after-leave="handleAfterLeave">
    <div
      v-show="visible"
      class="air-loading-mask"
      :style="{ backgroundColor: background || '' }"
      :class="[customClass, { 'is-fullscreen': fullscreen }]">
      <div class="air-loading-spinner">
        <svg v-if=! "" spinner" class="circular" viewBox="25 25 to 50 50">
          <circle class="path" cx="50" cy="50" r="20" fill="none"/>
        </svg>
        <i v-else :class="spinner"></i>
        <p v-if="text" class="air-loading-text">{{ text }}</p>
      </div>
    </div>
  </transition>
</template>

<script>
  export default {
    data() {
      return {
        text: null.spinner: null.background: null.fullscreen: true.visible: false.customClass: ' '
      };
    },

    methods: {
      handleAfterLeave() {
        this.$emit('after-leave');
      },
      setText(text) {
        this.text = text; }}};</script>
Copy the code

SRC /index.js SRC /index.js SRC /index.js SRC /index.js

import Vue from 'vue';
import loadingVue from './loading.vue';
import { addClass, removeClass, getStyle } from '.. /.. /.. /.. /src/utils/dom';
import { PopupManager } from '.. /.. /.. /.. /src/utils/popup';
import afterLeave from '.. /.. /.. /.. /src/utils/after-leave';
import merge from '.. /.. /.. /.. /src/utils/merge';

const LoadingConstructor = Vue.extend(loadingVue);

const defaults = {
  text: null.fullscreen: true.body: false.lock: false.customClass: ' '
};

let fullscreenLoading;

LoadingConstructor.prototype.originalPosition = ' ';
LoadingConstructor.prototype.originalOverflow = ' ';

LoadingConstructor.prototype.close = function() {
  if (this.fullscreen) {
    fullscreenLoading = undefined;
  }
  afterLeave(this, _ = > {const target = this.fullscreen || this.body
      ? document.body
      : this.target;
    removeClass(target, 'air-loading-parent--relative');
    removeClass(target, 'air-loading-parent--hidden');
    if (this.$el && this.$el.parentNode) {
      this.$el.parentNode.removeChild(this.$el);
    }
    this.$destroy();
  }, 300);
  this.visible = false;
};

const addStyle = (options, parent, instance) = > {
  let maskStyle = {};
  if (options.fullscreen) {
    instance.originalPosition = getStyle(document.body, 'position');
    instance.originalOverflow = getStyle(document.body, 'overflow');
    maskStyle.zIndex = PopupManager.nextZIndex();
  } else if (options.body) {
    instance.originalPosition = getStyle(document.body, 'position');
    ['top'.'left'].forEach(property= > {
      let scroll = property === 'top' ? 'scrollTop' : 'scrollLeft';
      maskStyle[property] = options.target.getBoundingClientRect()[property] +
        document.body[scroll] +
        document.documentElement[scroll] +
        'px';
    });
    ['height'.'width'].forEach(property= > {
      maskStyle[property] = options.target.getBoundingClientRect()[property] + 'px';
    });
  } else {
    instance.originalPosition = getStyle(parent, 'position');
  }
  Object.keys(maskStyle).forEach(property= > {
    instance.$el.style[property] = maskStyle[property];
  });
};

const Loading = (options = {}) = > {
  if (Vue.prototype.$isServer) return;
  options = merge({}, defaults, options);
  if (typeof options.target === 'string') {
    options.target = document.querySelector(options.target);
  }
  options.target = options.target || document.body;
  if(options.target ! = =document.body) {
    options.fullscreen = false;
  } else {
    options.body = true;
  }
  if (options.fullscreen && fullscreenLoading) {
    return fullscreenLoading;
  }

  let parent = options.body ? document.body : options.target;
  let instance = new LoadingConstructor({
    el: document.createElement('div'),
    data: options
  });

  addStyle(options, parent, instance);
  if(instance.originalPosition ! = ='absolute'&& instance.originalPosition ! = ='fixed') {
    addClass(parent, 'air-loading-parent--relative');
  }
  if (options.fullscreen && options.lock) {
    addClass(parent, 'air-loading-parent--hidden');
  }
  parent.appendChild(instance.$el);
  Vue.nextTick((a)= > {
    instance.visible = true;
  });
  if (options.fullscreen) {
    fullscreenLoading = instance;
  }
  return instance;
};

export default Loading;
Copy the code

Extend (loadingVue) generates a constructor, instantiates the constructor in the Loading function object, and then adds it to the body or target element. The instantiated object is returned, of course, with a lot of style processing, including whether or not the current screen is full.

The mounting of this service is:

import service from './components/loading/src/index';
Vue.prototype.$loading = service;
Copy the code

The same is true for calls:

const loading = this.$loading({
  lock: true.text: 'Loading'.spinner: 'air-icon-loading'.background: 'rgba (0, 0, 0, 0.7)'
});
setTimeout((a)= > {
  loading.close();
}, 2000);
Copy the code

Instruction way

SRC /directive.js:

import Vue from 'vue';
import Loading from './loading.vue';
import { addClass, removeClass, getStyle } from '.. /.. /.. /.. /src/utils/dom';
import { PopupManager } from '.. /.. /.. /.. /src/utils/popup';
import afterLeave from '.. /.. /.. /.. /src/utils/after-leave';
const Mask = Vue.extend(Loading);

const loadingDirective = {};
loadingDirective.install = Vue= > {
  if (Vue.prototype.$isServer) return;
  const toggleLoading = (el, binding) = > {
    if (binding.value) {
      Vue.nextTick((a)= > {
        if (binding.modifiers.fullscreen) {
          el.originalPosition = getStyle(document.body, 'position');
          el.originalOverflow = getStyle(document.body, 'overflow');
          el.maskStyle.zIndex = PopupManager.nextZIndex();

          addClass(el.mask, 'is-fullscreen');
          insertDom(document.body, el, binding);
        } else {
          removeClass(el.mask, 'is-fullscreen');

          if (binding.modifiers.body) {
            el.originalPosition = getStyle(document.body, 'position');

            ['top'.'left'].forEach(property= > {
              const scroll = property === 'top' ? 'scrollTop' : 'scrollLeft';
              el.maskStyle[property] = el.getBoundingClientRect()[property] +
                document.body[scroll] +
                document.documentElement[scroll] -
                parseInt(getStyle(document.body, `margin-${ property }`), 10) +
                'px';
            });
            ['height'.'width'].forEach(property= > {
              el.maskStyle[property] = el.getBoundingClientRect()[property] + 'px';
            });

            insertDom(document.body, el, binding);
          } else {
            el.originalPosition = getStyle(el, 'position'); insertDom(el, el, binding); }}}); }else {
      afterLeave(el.instance, _ => {
        if(! el.instance.hiding)return;
        el.domVisible = false;
        const target = binding.modifiers.fullscreen || binding.modifiers.body
          ? document.body
          : el;
        removeClass(target, 'air-loading-parent--relative');
        removeClass(target, 'air-loading-parent--hidden');
        el.instance.hiding = false;
      }, 300.true);
      el.instance.visible = false;
      el.instance.hiding = true; }};const insertDom = (parent, el, binding) = > {
    if(! el.domVisible && getStyle(el,'display')! = ='none' && getStyle(el, 'visibility')! = ='hidden') {
      Object.keys(el.maskStyle).forEach(property= > {
        el.mask.style[property] = el.maskStyle[property];
      });

      if(el.originalPosition ! = ='absolute'&& el.originalPosition ! = ='fixed') {
        addClass(parent, 'air-loading-parent--relative');
      }
      if (binding.modifiers.fullscreen && binding.modifiers.lock) {
        addClass(parent, 'air-loading-parent--hidden');
      }
      el.domVisible = true;

      parent.appendChild(el.mask);
      Vue.nextTick((a)= > {
        if (el.instance.hiding) {
          el.instance.$emit('after-leave');
        } else {
          el.instance.visible = true; }}); el.domInserted =true;
    } else if (el.domVisible && el.instance.hiding === true) {
      el.instance.visible = true;
      el.instance.hiding = false; }}; Vue.directive('loading', {
    bind: function(el, binding, vnode) {
      const textExr = el.getAttribute('element-loading-text');
      const spinnerExr = el.getAttribute('element-loading-spinner');
      const backgroundExr = el.getAttribute('element-loading-background');
      const customClassExr = el.getAttribute('element-loading-custom-class');
      const vm = vnode.context;
      const mask = new Mask({
        el: document.createElement('div'),
        data: {
          text: (vm && vm[textExr]) || textExr,
          spinner: (vm && vm[spinnerExr]) || spinnerExr,
          background: (vm && vm[backgroundExr]) || backgroundExr,
          customClass: (vm && vm[customClassExr]) || customClassExr,
          fullscreen:!!!!! binding.modifiers.fullscreen } }); el.instance = mask; el.mask = mask.$el; el.maskStyle = {}; binding.value && toggleLoading(el, binding); },update: function(el, binding) {
      el.instance.setText(el.getAttribute('element-loading-text'));
      if (binding.oldValue !== binding.value) {
        toggleLoading(el, binding);
      }
    },

    unbind: function(el, binding) {
      if (el.domInserted) {
        el.mask &&
        el.mask.parentNode &&
        el.mask.parentNode.removeChild(el.mask);
        toggleLoading(el, { value: false.modifiers: binding.modifiers }); } el.instance && el.instance.$destroy(); }}); };export default loadingDirective;
Copy the code

This time to talk about how vue create instructions, specific can see the documentation of custom instructions, the documentation is very clear, I am not going to go into too much detail, just mention, vue custom instructions have two ways:

  1. Register global components such as:
// Register a global custom directive 'V-focus'
Vue.directive('focus', {
  // When the bound element is inserted into the DOM...
  inserted: function (el) {
    // Focus elements
    el.focus()
  }
})
Copy the code
  1. Local components are registered in the component, and one is accepted in the componentdirectivesOptions:
directives: {
  focus: {
    // The definition of a directive
    inserted: function (el) {
      el.focus()
    }
  }
}
Copy the code

In fact, the loading described in this section is a globally registered directive, and element-UI also has local directives. The SRC /directives directory contains some local custom directives: Mousewheel and repeat-click, and some components, such as the table component, use these custom local directives.

And the following hook functions are available (all of which are optional):

  • Bind: Called only once, the first time a directive is bound to an element. This is where you can perform one-time initialization Settings.
  • Inserted: Called when the bound element is inserted into a parent (the parent is guaranteed to exist, but not necessarily inserted into the document).
  • Update: called when the component’s VNode is updated, but may occur before its child VNodes are updated. The value of the instruction may or may not have changed. However, you can ignore unnecessary template updates by comparing the values before and after the update (see below for detailed hook function parameters).
  • ComponentUpdated: Invoked when the VNode of the component where the directive resides and its child VNodes are all updated.
  • Unbind: Called only once, when an instruction is unbound from an element.

Let’s go back to the code above and analyze it:

  1. The first thing that comes backloadingDirectiveThis object, and this object only has oneinstallMethod, which indicates that the instruction must be initialized withVue.useIn the manner of reference.
  2. ininstallMethod, the definition of the register global directive appearsVue.directive('loading', {.. }, let’s briefly analyze these hook functions:
  3. bindThe first time you bind to an element,elThe argument represents the element bound by the directive and can be used to manipulate the DOM directly. Logic is gettingelTo render the loading DOM template, and then determine whether to insert into the loading DOM template according to whether to display the parameters in full screenelIn, or in the body.
  4. updateIn fact, we allow the copy to be updated during loading. The most common one is the percentage progress behind the loading progress bar, which means that the copy is always updated
  5. unbindThis is the action of removing the binding

The method of the call is something like this:

import directive from './components/loading/src/directive';
Vue.use(directive);
Copy the code

Entrance to the file

Since the loading way to have a global directives and built-in service (global), and initialize the way is different, so I use to use to call, the SRC/components/loading/index. Js

import directive from './src/directive';
import service from './src/index';

export default {
  install(Vue) {
    Vue.use(directive);
    Vue.prototype.$loading = service;
  },
  directive,
  service
};
Copy the code

The install method is included in the export, and the install method is initialized for both methods, so the main.js call is:

import Loading from './components/loading'

Vue.use(Loading)
Copy the code

That’s it. Both methods can be initialized. SRC /components/index.js: SRC /components/index.js:

. import Loadingfrom './loading'. const install =function (Vue) {...// Vue.use(Loading);
  // Both can be initialized directly with use as above, or separately as belowVue.use(Loading.directive); Vue.prototype.$loading = Loading.service; . }export default {
  install
}
Copy the code

test

Next we test home.vue by adding this to template:

<air-button
  type="primary"
  @click="openFullScreen1"
  v-loading.fullscreen.lock="fullscreenLoading">Instruction way</air-button>
<air-button
  type="primary"
  @click="openFullScreen2">Service mode</air-button>
Copy the code

Represents both calls, and then adds the corresponding arguments and methods to the script:

<script> export default { data () { return { ... fullscreenLoading: false } }, methods: { ... openFullScreen1() { this.fullscreenLoading = true; setTimeout(() => { this.fullscreenLoading = false; }, 2000); }, openFullScreen2() { const loading = this.$loading({ lock: true, text: 'Loading', spinner: 'air-icon-loading', background: 'rgba(0, 0, 0, 0.7)'}); setTimeout(() => { loading.close(); }, 2000); } } } </script>Copy the code

So you can see the effect:

Then click command mode to see the effect

Click the service mode, you can also see the effect, and the service mode is more customized

conclusion

This completes the creation of the directive type component. In the next video, how do you partially introduce components


Series of articles:

  • Air-ui (1) — Why do I need to build an Element UI component
  • Self-built VUE component AIR-UI (2) — Take a look at the Element UI project
  • Self-built VUE component AIR-UI (3) – CSS development specification
  • Air-ui (4) — Air-UI environment setup and directory structure
  • Air-ui (5) — Create the first vUE component, Button
  • Self-built VUE component AIR-UI (6) – Creates built-in service components
  • Build vUE component AIR-UI (7) – Create command component
  • Self-built VUE component AIR-UI (8) — Implementation part introduces components
  • Build your own VUE component air-UI (9) — document with Vuepress
  • Air-ui (10) — Vuepress Documentation (Advanced version)
  • Vue Component Air-UI (11) — Vuepress Documentation (Crawl version)
  • Self-built VUE component AIR-UI (12) — Internationalization mechanism
  • Self-built VUE Component AIR-UI (13) — Internationalization Mechanism (Advanced Version)
  • Self-built VUE component AIR-UI (14) — Packaged Build (Dev and Dist)
  • Self-built VUE component AIR-UI (15) — Theme customization
  • Self-built VUE component AIR-UI (16) – Packages to build pub tasks
  • Build your own VUE component AIR-UI (17) – Develop a pit crawl and summary