Recommended articles in the past (hope you have a good harvest)

  • Lodash’s “redundant” and eye-popping apis
  • Don’t JavaScript to use annotations to the fire @ Decorator (comments | Decorator | decoration)

The following code is quite large, so just pay attention to the part marked with *, which will not affect the reading experience

First, this paper harvest

  1. Why do we want to outlaw the normal bullet frame;
  2. Deep parameter combination;
  3. pickExtract and save data;
  4. How to unify Vue project popbox and simplify call

Second, why to unify the package frame; How do you package it

Through the example of conventional bullet frame writing method. Normaldialog. vue wraps dialogBody.vue (the body of the popup box) around the normaldialog. vue (the body of the popup box). Parent. Vue is required to set flag to control the display of the popbox hidden, normaldialog. vue is closed when the parent. Disadvantages: multifarious process, cumbersome configuration, inflexibility, inconsistent style and troublesome parameter transfer. If a project has more pop-ups, drawbacks will be more obvious, a large number of isXxxDialogShow, a large number of Vue files. Therefore, the project team urgently needed an API that could be easily configured to pop up the box.

1. Conventional bullet frame writing method

  1. dialoBody.vue(Frame body), adopted hereComposition APIIn the writing. Just a simple page, with checksums, and the usual logic to extract and store data.
<template> <div class="dialog-body"> <div class="item"> <div> name </div> <el-input V-model ="name"></el-input> </div> <div Class ="item"> <el-radio-group v-model="attention"> <el-radio label=" attention"> </el-radio> <el-radio label=" attention"> </el-radio> </el-radio-group> </div> <div class="item"> <el-radio-group V-model ="like"> <el-radio label=" liked "></el-radio> <el-radio </div> </div> </template> <script> import {reactive, toRefs } from '@vue/composition-api' import pick from 'lodash/pick' import { Message } from 'element-ui' export default { props: { defaultName: String,}, setup(props, CTX) {const ATTENTIONED = 'support' const LIKED = 'support' const state = reactive({name: function ()) // props. DefaultName, // props: props 'have thumb up, / / thumb up}) / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * page binding events suggest writing: * 1. Use methods.onxxx = ()=>{// Related logic} to define * Benefits 1: The location defined by onXxx is associated with the relevant business logic code * Benefit 2: Can be unified through... Advantages 3: when the page logic is complex, the data that needs to be operated has a strong correlation, and components cannot be disassembled; * Code of related business can be defined in a separate module; * Independent module exposed API handleXxx(Methods, State), assembly line processing methods; * and Vue2 source, pipeline processing. * / const the methods = {} / name/check the methods onNameBlur = () = > {} / / * * * * * * * * * * * * * * * * * * * * * * * * to expose apis * * * * * * * * * * * * * * * * * * * * * * * * const apiMethods = {/ / save before checking isCanSave () {if (state. Attention! == ATTENTIONED || state.like ! == new) {message. error(' new ', 'new', 'new ') return false} return true}, GetSaveData () {// ******* lodash pick return pick(state, ['name', 'attention', 'like'])}, } return { ... toRefs(state), ... methods, apiMethods, } }, } </script> <style lang="less"> .dialog-body { width: 100%; height: 100px; } </style>Copy the code
  1. normalDialog.vueWrap the body of the framedialoBody.vue
<template> <el-dialog title=" isShow" width="30%" :before-close="onClose" > <dialog-body <span slot="footer" class="dialog-footer"> <el-button @click="onClose"> </el-button type="primary" @click="onOK"> </el-button> </span> </el-dialog> </template> <script> import dialogBody from './dialogBody.vue' export default { components: { dialogBody, }, data() { return { isShow: true, } }, methods: {onClose () {/ / * * * * * * * * * * * modify the parent in vue * * * * * * * * this. $parent. IsNormalDialogShow = false}, / / * * * * * * * control preservation process * * * * * * * * onOK () {const inner = this. $refs. The inner / / check whether can be saved if (inner) apiMethods) isCanSave ()) {/ / For saving data const postData = inner. ApiMethods. GetSaveData () the console. The log (' > > > > > postData > > > > > ', // This. OnClose ()}},},} </script>Copy the code
  1. parent.vue
/ / HTML
<normal-dialog v-if="isNormalDialogShow" />

/ / Js parts
data(){
	isNormalDialogShow:false
}
methods:{
    onDialogShow(){ // ****** The control dialog box displays *****
        this.isNormalDialogShow = true}}Copy the code

2. How should it be packaged

2.1 API Appeal:

  • Simple to call;
  • Don’t setisXxxDialogShow;
  • Simple transfer of ginseng;
  • Easy to controlel-dialogStyle property of;
  • You can control the closing process of the frame.

2.2 Ideal API:

import dialogBody from './dialogBody.vue'
const dialog = new JSDialog({
  comonent: dialogBody, 
  dialogOpts: { // Scalable configuration
    title: 'JSDialog set the title of the popbox '.width: '400px'
  },
  props: {
    defaultName: 'Arguments passed by JSDialog',
  },
  onOK() {
    const inner = dialog.getInner() // Get a dialogBody reference
    // Control process
    if (inner.apiMethods.isCanSave()) {
      // Get the saved data
      const postData = inner.apiMethods.getSaveData()
      console.log('>>>>> postData >>>>>', postData)
      // Close the dialog box
      dialog.close()
    }
  },
  onCancel() {
    dialog.close() // The popbox closes
  },
})
dialog.show() // The box is displayed
Copy the code

Three, how to package

Dynamic control of display content, three scenarios come to mind: card slots, dynamic components, and overriding Render. The following is a simple comparison of the three schemes in the dynamic pop-up scenario.

  • Slot (card), andel-dialogThe principle is similar, but there is another layer of encapsulation, less definitionnormalDialog.vueFile.Disadvantages: Complex call, inflexible; Not easy to control the closure process; Only in thetemplateDefined in the.
  • Component (Dynamic)To createcommonDialog.vue, unified hanging inApp.vueBy using<component :is="componentId"></component>Dynamically switch the frame body,commonDialog.vueListening to thecomponentIdChange to switch the frame body.Cons: Register all popbox body components with commonDialog.vue components in advance; Relying on VUEX, it is highly invasive. Vuex pop-ups from pure JS files are relatively complex and inflexible.
  • rewriterender.renderisVueA back door for developers who build wheels. Dynamic frame can be used as an independent function module, internal through newVue, the rewriterenderControl rendering content.independentVueInstance, can be pre-created, can control the frame in any position, flexible, clear.Disadvantages: None at present

1. Overall code

Here’s a preview of the code as a whole, then break it down.

import Vue from 'vue'
import merge from 'lodash/merge'
import orderBy from 'lodash/orderBy'

// Button configuration item constructor
function btnBuilder(options) {
  const defaultBtn = {
    text: 'button'.// Display text
    clickFn: null.// Click callback
    type: 'default'./ / style
    isHide: false.// Whether to hide
    order: 2 / / order
  }
  return{... defaultBtn, ... options } }export default class JSDialog {
  constructor(originOptions) {
    this.options = {}
    this.vm = null
    this._mergeOptions(originOptions)
    this._initVm()
  }
  // Merge parameters
  _mergeOptions(originOptions) {
    const defaultOptions = {
      component: ' '.// Popbox main vue page
      / / extensible el - all the configuration dialog official API, small hump aaaBbbCcc
      dialogOpts: {
        width: '40%'.title: 'Default title'
      },
      // Pass the argument to the popbox body vue component
      props: {},
      // Click OK to callback
      onOK: (a)= > {
        console.log('JSDialog default OK'), this.close()
      },
      // Click to cancel the callback
      onCancel: (a)= > {
        console.log('JSDialog default cancel'), this.close()
      },
      footer: {
        ok: btnBuilder({
          text: 'sure'.type: 'primary'.order: 0
        }),
        cancel: btnBuilder({
          text: 'cancel'.order: 1}}})// The parameters are merged into this.options
    merge(this.options, defaultOptions, originOptions)
    const footer = this.options.footer
    Object.entries(footer).forEach(([key, btnOptions]) = > {
      // Determine and cancel the default buttons
      if (['ok'.'cancel'].includes(key)) {
        const clickFn = key === 'ok' ? this.options.onOK : this.options.onCancel
        // Default button callback priority: footer configured clickFn > options configured onOK and onCancel
        btnOptions.clickFn = btnOptions.clickFn || clickFn
      } else {
        // Add a button
        // Complete the configuration
        footer[key] = btnBuilder(btnOptions)
      }
    })
  }
  _initVm() {
    const options = this.options
    const beforeClose = this.options.footer.cancel.clickFn // The close button in the upper right corner of the popbox is called back
    this.vm = new Vue({
      data() {
        return {
          // Reactive data is required
          footer: options.footer, // Bottom button
          visible: false // The popbox is displayed and closed}},methods: {
        show() {
          // The box is displayed
          this.visible = true
        },
        close() {
          // The popbox closes
          this.visible = false
        },
        clearVm() {
          // Clear the VM instance
          this.$destroy()
        }
      },
      mounted() {
        // Mount to body
        document.body.appendChild(this.$el)
      },
      destroyed() {
        // Remove from the body
        document.body.removeChild(this.$el)
      },
      render(createElement) {
        // Frame body
        const inner = createElement(options.component, {
          props: options.props, // Pass parameters
          ref: 'inner' / / reference
        })
        // Control button display hidden
        const showBtns = Object.values(this.footer).filter(btn= >! btn.isHide)// Control button sequence
        const sortBtns = orderBy(showBtns, ['order'], ['desc'])
        // Bottom button JSX notation
        const footer = (
          <div slot="footer">
            {sortBtns.map(btn => (
              <el-button type={btn.type} onClick={btn.clickFn}>
                {btn.text}
              </el-button>
            ))}
          </div>Const elDialog = createElement('el-dialog', {// El-dialog configuration item: {... DialogOpts, visible: this.visible, beforeClose}, // **** { closed: this.clearVm }, ref: 'elDialog'}, // Popbox contents: Frame body and button [inner, Footer]) return elDialog}}).$mount()} close() {this.vm. Close ()} show() {this.vm. {return this.vm.$refs.inner}}Copy the code

2. Merge parameters

To achieve the API appeal: simple call, simple parameter transfer and extensible control frame style. Parameter combination is the least cost implementation scheme, and the effect is better with TS. Define default parameters and merge deep attributes using Lodash’s merge. Parameter merging also allows you to customize the footer button, control text, style, order, and execute callbacks.

// Merge parameters
_mergeOptions(originOptions) {
  const defaultOptions = {
    component: ' '.// Popbox main vue page
    / / extensible el - all the configuration dialog official API, small hump aaaBbbCcc
    dialogOpts: {
      width: '40%'.title: 'Default title'
    },
    // Pass the argument to the popbox body vue component
    props: {},
    // Click OK to callback
    onOK: (a)= > {
      console.log('JSDialog default OK'), this.close()
    },
    // Click to cancel the callback
    onCancel: (a)= > {
      console.log('JSDialog default cancel'), this.close()
    },
    footer: {
      ok: btnBuilder({
        text: 'sure'.type: 'primary'.order: 0
      }),
      cancel: btnBuilder({
        text: 'cancel'.order: 1}}})// The parameters are merged into this.options
  merge(this.options, defaultOptions, originOptions)
  const footer = this.options.footer
  Object.entries(footer).forEach(([key, btnOptions]) = > {
    // Determine and cancel the default buttons
    if (['ok'.'cancel'].includes(key)) {
      const clickFn = key === 'ok' ? this.options.onOK : this.options.onCancel
      // Default button callback priority: footer configured clickFn > options configured onOK and onCancel
      btnOptions.clickFn = btnOptions.clickFn || clickFn
    } else { // Add a button
      // Complete the configuration
      footer[key] = btnBuilder(btnOptions)
    }
  })
}
Copy the code

3. The render function

Vue recommends using templates to create your HTML in most cases. However, in some scenarios, you really need the full programming power of JavaScript. In this case you can use render functions, which are closer to the compiler than templates. The official documentation of rendering function writing method, parameters, corresponding JSX writing method has been very detailed, here will not repeat. The following code is run on the latest VUE-CLI create project, trying to write both JS parameter create elements and JSX create elements.

render(createElement) {
  // Frame body
  const inner = createElement(options.component, {
    props: options.props, // Pass parameters
    ref: 'inner' / / reference
  })
  // Control button display hidden
  const showBtns = Object.values(this.footer).filter(btn= >! btn.isHide)// Control button sequence
  const sortBtns = orderBy(showBtns, ['order'], ['desc'])
  // Bottom button JSX notation
  const footer = (
    <div slot="footer">
      {sortBtns.map(btn => (
        <el-button type={btn.type} onClick={btn.clickFn}>
          {btn.text}
        </el-button>
      ))}
    </div>Const elDialog = createElement('el-dialog', {// El-dialog configuration item: {... options.dialogOpts, visible: this.visible }, on: { closed: this.clearVm }, ref: [inner, footer]) return elDialog}Copy the code

4. Packaging API

For the time being, there are only three APIS, which can be extended according to different scenarios, such as undestroyed hidden frame, reload frame, etc.

  1. show(), the dialog box is displayed

The main display is to change the el-Dialog visible to True to control the display of the popbox mounted to the body.

show() {
  this.vm.show()
}
Copy the code
  1. close(), the dialog box closes

Change el-Dialog visible to false; Trigger the Closed event for the El-Dialog; Perform clearVm; Execute $destroy() for the VM; The destroyed() callback removes $el from the body.

close() {
  this.vm.close()
}
Copy the code
  1. getInner(), get the frame body instance, can be used to access the method on the instance, control button process
getInner() {
  return this.vm.$refs.inner
}
Copy the code

Four, how to use

1. In the simplest scenario, configure only the page

The button event callback uses the default callback, and the OK and cancel buttons close the popbox

import dialogBody from './renderJsx/dialogBody'
const dialog = new JSDialog({
  component: dialogBody,
})
dialog.show() // The box is displayed
Copy the code

The effect is as follows:

2. Control the frame style and determining process

The configuration items supported by el-Dialog can be customized, as shown in the Dialog box. For example, title and customClass. CustomClass allows you to control the style of the bullet frame in the project. Control ok cancel button code callback.

import dialogBody from './renderJsx/dialogBody'
const dialog = new JSDialog({
  component: dialogBody,
  dialogOpts: {
    title: 'Pretty boy, pretty girl. Oh, hey.'.customClass:'js-dialog'
  },
  props: {
    defaultName: 'Arguments passed by JSDialog'
  },
  onOK() {
    const inner = dialog.getInner() // Get a dialogBody reference
    // Control process
    if (inner.apiMethods.isCanSave()) {
      // Get the saved data
      const postData = inner.apiMethods.getSaveData()
      console.log('>>>>> postData >>>>>', postData)
      // Close the dialog box
      dialog.close()
    }
  },
  onCancel() {
    dialog.close() // The popbox closes}})Copy the code

The effect is as follows:

3. Customize footer

Custom buttons control the execution of callbacks, style, order, show and hide

import dialogBody from './renderJsx/dialogBody'
const dialog = new JSDialog({
  component: dialogBody,
  footer: {
    ok: { // Change the default button
      text: 'new'
    },
    cancel: { // Hide the default button
      isHide: true
    },
    add: { // Add a button
      text: 'Save as',
      clickFn() {
        dialog.close()
      },
      order: - 1 // Control the button sequence, order small display on the right
    },
    add2: {
      text: 'Add button 2',
      clickFn() {
        dialog.close()
      },
      order: 3
    }
  }
})
dialog.show() // The box is displayed
Copy the code

The effect is as follows: