Vue3 package component (with callback event)


foreword

This article follows the previous article ( Vue3 Encapsulates Global Functional Components ) and continues to share the vue3 component encapsulation. The last article uses Toastprompts as an example to encapsulate components. In addition, there are Dialogpop-up boxes that we often encapsulate and call , there is not much difference between them, the main reason is that more buttons can trigger callbacks, so this article introduces the writing method of callback events in functional components, hoping to inspire everyone.


1. Ideas

First of all, the component call needs to meet the chain call, and I want to achieve the following writing method:

okToast.show()
  .then(res => {
    
    
    console.log('点击了确认');
  })
  .catch(err => {
    
    
    console.log('点击了取消');
  })

The logic of clicking confirmation is processed after then, and the logic of clicking cancel is processed after catch, and the second pop-up window can continue to be invoked after then to satisfy the chain call.
Then it is natural to think of using PromisePromise to pass back the status of the asynchronous operation in time so that the callback function can be called in time.

2. Code example

1. vue document

The code is as follows (example):

<template>
  <transition name="toast" @after-leave="onAfterLeave">
    <div class="toast" v-if="isShow" :style="{ width: toastWidth }">
      <div
        v-if="time < 0 && type !== 'confirm' && type !== 'confirmAndcancel'"
        class="cancel"
        @click="hidden"
      ></div>
      <img
        v-if="type === 'success' || type === 'icon'"
        class="img"
        src="../../assets/images/[email protected]"
        alt="success"
      />
      <img
        v-if="type === 'warn'"
        class="img"
        src="../../assets/images/7vip_web_toast_warn.png"
        alt="warn"
      />
      <div v-if="content && type !== 'icon'" class="content" :style="{ textAlign }">
        {
   
   { content }}
      </div>
      <!-- 主要在这里增加了两种样式 -->
      <!-- 这是只有一个确定按钮的 -->
      <div class="operation" v-if="type === 'confirm'">
        <div class="confirm" @click="successHandle">{
   
   { successText }}</div>
      </div>
      <!-- 这是同时有确定与取消按钮的 -->
      <div class="operation" v-if="type === 'confirmAndcancel'">
        <div class="close" @click="cancelHandle">{
   
   { cancelText }}</div>
        <div class="confirm" @click="successHandle">{
   
   { successText }}</div>
      </div>
    </div>
  </transition>
</template>
<script setup>
import {
      
       ref, computed, nextTick } from 'vue';

const props = defineProps({
      
      
  content: {
      
      
    type: String,
    default: 'success'
  },
  time: {
      
      
    type: Number,
    default: 2000
  },
  width: {
      
      
    default: 310
  },
  textAlign: {
      
      
    type: String,
    default: 'center'
  },
  type: {
      
      
    type: String,
    default: 'success'
  },
  hide: {
      
      
    type: Function,
    default: () => {
      
      }
  },
  successText: {
      
      
    type: String,
    default: '确认'
  },
  cancelText: {
      
      
    type: String,
    default: '取消'
  },
  successBtn: {
      
      
    type: Function,
    default: () => {
      
      }
  },
  cancelBtn: {
      
      
    type: Function,
    default: () => {
      
      }
  }
});
const isShow = ref(false);
const toastWidth = computed(
  () => (parseInt(props.width.toString()) / 750) * document.documentElement.clientWidth + 'px'
);

const show = () => {
      
      
  isShow.value = true;
  if (props.time >= 0) {
      
      
    setTimeout(() => {
      
      
      // isShow.value = false;
      successHandle();
    }, props.time);
  }
};
defineExpose({
      
      
  show
});

const hidden = () => {
      
      
  isShow.value = false;
};
const onAfterLeave = () => {
      
      
  props.hide();
};

//新增处理确认的方法
const successHandle = () => {
      
      
  props.successBtn();
  nextTick(() => {
      
      
    hidden();
  });
};
//新增的处理取消的方法
const cancelHandle = () => {
      
      
  props.cancelBtn();
  nextTick(() => {
      
      
    hidden();
  });
};
</script>

<style lang="scss" scoped>
.toast-enter-active,
.toast-leave-active {
      
      
  transition: opacity 0.3s ease-out;
}
.toast-enter-from,
.toast-leave-to {
      
      
  opacity: 0;
}
.toast {
      
      
  position: fixed;
  top: 45%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 99;
  background: #333333;
  border-radius: 20px;
  padding: 20px;
  text-align: center;
  .cancel {
      
      
    background: url('../../assets/images/[email protected]') no-repeat center / contain;
    position: absolute;
    top: 10px;
    right: 10px;
    width: 20px;
    height: 20px;
    &::before {
      
      
      content: '';
      position: absolute;
      top: -10px;
      right: -10px;
      bottom: -10px;
      left: -10px;
    }
  }
  .img {
      
      
    width: 40px;
    height: 40px;
  }
  .content {
      
      
    margin-top: 10px;
    font-size: 16px;
    color: #ffcc99;
    text-align: initial;
  }
  .operation {
      
      
    display: flex;
    justify-content: space-around;
    align-items: center;
    margin-top: 20px;
    .confirm {
      
      
      color: white;
      opacity: 0.9;
    }
    .close {
      
      
      color: #ffcc99;
      opacity: 0.9;
    }
  }
}
</style>

Explanation ①:

const successHandle = () => {
    
    
  props.successBtn();
  nextTick(() => {
    
    
    hidden();
  });
};

Here is a detail. In the successHandle method in the vue file, the pop-up window needs to be closed after clicking, so the hidden method is called, but the successBtn method must be executed first, that is, resolve() in the Promise, and then the logic after the callback is processed. Close the logic of unloading, otherwise the pop-up window may be closed due to hidden reasons before the callback is executed, so this execution order is very important. The method added to vue here ensures that hidden is triggered at the end of all asynchronous nextTicktasks . The cancelHandle method is the same.

Explanation ②:

const show = () => {
    
    
  isShow.value = true;
  if (props.time >= 0) {
    
    
    setTimeout(() => {
    
    
      // isShow.value = false;
      successHandle();
    }, props.time);
  }
};

The show method does not directly close the pop-up window, but calls the passed method, so that the common writing method, proxy.$okToast(), can also use chain calls.

2. js file

The code is as follows (example):

import {
    
     createApp } from 'vue';
import OkToast from './okToast.vue';

let rootNode = null;
let app = null;

const okToast = options => {
    
    
  const dom = document.body.querySelector('.my-dialog');
  if (!dom) {
    
    
    // 创建元素节点
    rootNode = document.createElement('div');
    rootNode.className = `my-dialog`;
    // 在body标签内部插入此元素
    document.body.appendChild(rootNode);
  } else {
    
    
    app.unmount();
  }
  // 创建应用实例(第一个参数是根组件。第二个参数可选,它是要传递给根组件的 props)
  app = createApp(OkToast, {
    
    
    ...options,
    hide() {
    
    
      // 卸载已挂载的应用实例
      if (app) {
    
    
        app.unmount();
        app = null;
      }
      // 删除rootNode节点
      if (rootNode) {
    
    
        document.body.removeChild(rootNode);
        rootNode = null;
      }
    }
  });
  // 将应用实例挂载到创建的 DOM 元素上
  return app.mount(rootNode);
};

// 需要给options设默认值,否则直接调用okToast()会出错
const okFun = (options = {
     
     }) => {
    
    
  return new Promise((resolve, reject) => {
    
    
    options.successBtn = () => {
    
    
      resolve();
    };
    options.cancelBtn = () => {
    
    
      reject();
    };
    okToast(options).show();
  });
};

okToast.install = app => {
    
    
  // 注册全局组件
  // app.component("Toast", OkToast);
  // 注册全局属性,类似于 Vue2 的 Vue.prototype
  // app.config.globalProperties.$okToast = options => okToast(options).show();
  app.config.globalProperties.$okToast = options => okFun(options);
};
// 定义show方法用于直接调用
// okToast.show = options => okToast(options).show();
okToast.show = options => okFun(options);

export default okToast;

Explanation ①:

The original writing method is okToast(options).show(), but now it calls the okFun method, which returns a Promise object in the okFun method, and the second parameter of the createApp method can be passed to the root component, then you can Define two methods on the parameters. These two methods are bound to the OK and Cancel buttons respectively. When the button is clicked and the event here is triggered, then the state of the Promise can be changed and the callback will be triggered. Very interesting!


3. Application method and effect display

If the input type is "confirmAndcancel", there will be two buttons, and if the input type is "confirm", there will be only one button. Time needs to be passed -1 so that the pop-up window stays in the window. For detailed parameters, see the props in the above vue file. At the same time, since Promise supports the finally method, it is also added as an example. The code is as follows (example):

proxy
  .$okToast({
    
    
    time: -1,
    width: 500,
    type: 'confirmAndcancel',
    content: '如果解决方法是丑陋的,那就肯定还有更好的解决方法,只是还没有发现而已。',
  })
  .then(res => {
    
    
    console.log('点击了确认1');
    proxy
      .$okToast({
    
    
        time: -1,
        width: 500,
        type: 'confirm',
        content: '再点一下弹窗就消失了',
        successText: 'OK'
      })
      .then(res => {
    
    
        console.log('点击了确认2');
      });
  })
  .catch(err => {
    
    
    console.log('点击了取消');
    proxy.$okToast({
    
    
      time: -1,
      width: 500,
      type: 'confirm',
      content: '再点一下弹窗就消失了',
      successText: 'OK',
    });
  })
  .finally(() => {
    
    
    console.log('finally');
  });

Show results:
Please add a picture description

It can be seen that the callback of the chain call is in effect normally.

4. Add a little detail to improve the effect

The above effect has basically achieved Dialogthe pop-up window effect. Just when I thought I was done, I realized that something was missing compared to the Dialog on the UI component library. . . That's right, it is the mask layer. In addition to the style, the mask layer also has the logic of prohibiting scrolling in the background. Well, quickly fill it up.

  1. First define the style of the mask layer, the code is as follows (example):
.my-overlay {
    
    
  position: fixed;
  top: 0;
  left: 0;
  z-index: 99;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.6);
}
  1. Then add the mask layer element when creating the popup window DOM. At the beginning, the idea is very simple and the effect is realized, which is to directly monitor “touchmove”the event of the element to prevent scrolling, such as this:
let overlayNode = null;
let rootNode = null;
let app = null;
// 阻止默认事件
let noScroll = e => {
    
    
  e.preventDefault();
};

const okToast = options => {
    
    
  const dom = document.body.querySelector(".my-dialog");
  if (!dom) {
    
    
    if (options.type === "confirmAndcancel" || options.type === "confirm") {
    
    
      // 创建遮罩层
      overlayNode = document.createElement("div");
      overlayNode.className = `my-overlay`;
      document.body.appendChild(overlayNode);
      // 监听滚动事件
      document.body.querySelector(".my-overlay").addEventListener("touchmove", noScroll);
    }
    rootNode = document.createElement("div");
    rootNode.className = `my-dialog`;
    document.body.appendChild(rootNode);
    // 监听滚动事件
    document.body.querySelector(".my-dialog").addEventListener("touchmove", noScroll);
  } else {
    
    
    app.unmount();
  }
  app = createApp(OkToast, {
    
    
    ...options,
    hide() {
    
    
      if (options.type === "confirmAndcancel" || options.type === "confirm") {
    
    
        // 解除监听
        document.body.querySelector(".my-overlay").removeEventListener("touchmove", noScroll);
        document.body.querySelector(".my-dialog").removeEventListener("touchmove", noScroll);
        if (overlayNode) {
    
    
          document.body.removeChild(overlayNode);
          overlayNode = null;
        }
      }
      if (app) {
    
    
        app.unmount();
        app = null;
      }
      if (rootNode) {
    
    
        document.body.removeChild(rootNode);
        rootNode = null;
      }
    },
  });
  return app.mount(rootNode);
};

Existing problems:
But in this case, not only the scrolling of the background is prohibited, but also the content in the pop-up window is prohibited from scrolling. If there are too many texts in the pop-up window, the height of the pop-up window needs to be limited, and the content of the pop-up window is scrolled, and When considering the situation that the background cannot be scrolled but the pop-up window can be scrolled, we encountered the problem of scrolling background penetration on the mobile terminal, and we had to consider the compatibility issue, which was really a twists and turns.

Solution:
After trying various writing methods, fortunately, I finally found a relatively simple writing method to achieve the effect, which is to add the app element created by Vue at the same level. When the position = "fixed"pop-up window appears, the fixed positioning makes the background layer page unable to scroll. , bottomthe value is positioned at the scrolling distance of the current page so as not to change the page position. When the pop-up window disappears, cancel the fixed positioning. At this time, the page will automatically be at the top, but it doesn’t matter. Just restore the value of the page back. Although this scrollTopmethod I can't guarantee 100% success, but it has also been tested on my Android and Apple machines. If you encounter any problems, please point them out in the comment area.
The code is as follows (example):

let overlayNode = null;
let rootNode = null;
let app = null;
let scrollTop = 0;
const createOverlay = () => {
    
    
  const app= document.querySelector('#app');
  if (!scrollTop) {
    
    
    scrollTop = app.scrollTop;
  }
  // 禁止app元素滚动
  app.style.position = 'fixed';
  app.style.bottom = scrollTop + 'px';
  // 兼容ios手机fixed定位不生效,得加overflow
  app.style.overflow = 'visible';

  // 创建遮罩层
  overlayNode = document.createElement('div');
  overlayNode.className = `my-overlay`;
  // // 在body标签内部插入此元素
  document.body.appendChild(overlayNode);
};
const deleteOverlay = () => {
    
    
  // 删除overlayNode节点
  if (overlayNode) {
    
    
    document.body.removeChild(overlayNode);
    overlayNode = null;
  }
  // 解除app元素滚动,移除样式
  const app =document.querySelector('#app')
  app.style.removeProperty('position')
  app.style.removeProperty('bottom')
  app.style.removeProperty('overflow')
  // 恢复页面滚动距离
  app.scrollTop = scrollTop;
  // 恢复默认值
  scrollTop = 0;
};

const okToast = options => {
    
    
  const dom = document.body.querySelector('.my-dialog');
  if (!dom) {
    
    
  	// 将type与遮罩层关系解耦,根据传入的time参数确定遮罩层显示或隐藏,time小于0则是Dialog,大于0或者不传time则是Toast,当然更好的方式是区分开这两个组件,然后单独做配置项,这里只是为了兼容之前的Toast逻辑
    // if (options.type === "confirmAndcancel" || options.type === "confirm") {
    
    
    if (options.time && options.time < 0) {
    
    
      createOverlay();
    }
    // 创建元素节点
    rootNode = document.createElement('div');
    rootNode.className = `my-dialog`;
    document.body.appendChild(rootNode);
  } else {
    
    
    app.unmount();
    // 根据传入配置去掉遮罩层
    if (!options.time || options.time > 0) {
    
    
      deleteOverlay();
    }
  }
  app = createApp(OkToast, {
    
    
    ...options,
    hide() {
    
    
      if (options.time && options.time < 0) {
    
    
        deleteOverlay();
      }
      if (app) {
    
    
        app.unmount();
        app = null;
      }
      if (rootNode) {
    
    
        document.body.removeChild(rootNode);
        rootNode = null;
      }
    }
  });
  // 将应用实例挂载到创建的 DOM 元素上
  return app.mount(rootNode);
};

In addition, change the style of the vue file to meet the scrolling effect of the content area

.content {
    
    
  margin-top: 10px;
  font-size: 32px;
  color: #ffcc99;
  text-align: initial;
  max-height: 50vh;
  overflow-y: scroll;
}
  1. Show results:
    Please add a picture description

Summarize

The above is all the content. This article Dialogexplores the encapsulation method of Vue3 functional components by encapsulating the pop-up box component, and at the same time solves the related problems of background scrolling of the pop-up box on the mobile terminal. In the process of exploring and learning this time, I deeply realized that when encountering problems, I can change my thinking, try more, and at the same time look for information, learn from other people's experience and skills, because some common problems must have The predecessors have encountered it, so standing on the shoulders of giants and adding your own thinking, the problem will be solved.

If this article is helpful to you, you are welcome to [Like] and [Favorite]! You are also welcome to [comments] to leave valuable comments, discuss and learn together~

Guess you like

Origin blog.csdn.net/m0_55119483/article/details/130289309