触发式的自定义弹层

背景

web端日常开发经常会遇到各种弹层需求,弹层内容千奇百怪。根据我们日常的组件化开发经验,我们会把弹层分为2个组件

  • 弹层组件:包含蒙版, 展示隐藏逻辑
  • 内容组件:本次需求实际组件

触发式的自定义弹层_第1张图片

使用

import CitySelect from './components/CitySelect';
import modal from '@/components/modal';

...
openCitySelect() {
    modal.popup({
        content: CitySelect,
        props: {
            data,
            onSave: () => {...}
        }
    });
}
...

说明

这种用法的好处是显而易见的

  1. 调用方CitySelect都不需要维护一个visible变量
  2. 调用方CitySelect模块分离更加清晰,调用方只需要触发一次openCitySelect,之后就和弹层再无关系,CitySelect只关心自身逻辑不需要关心弹层的状态。

下面来看此api在vuereact上面的实现

Vue实现

vue下面实现此api的方法较多,可以使用dom操作, 动态组件, render函数来实现。我们以动态组件为例:



React实现

React实现需要配合react-dom,rn的话可以在app入口加一个常驻弹窗容器来实现,原理大致相同。
这里还是以web端为例,代码直接从ant抄过来的。

// 弹窗组件
class Popup extends Component {

  componentDidMount() {

  }

  render() {
    const {
      close,
      props = {},
      visible = false,
    } = this.props;

    if (!visible) {
      return null;
    }

    const DlgContent = content;

    return (
      
        
          
) } } // 方法 const popup = function(config) { const div = document.createElement('div'); document.body.appendChild(div); let currentConfig = { ...config, visible: true, close }; function close(...args) { currentConfig = { ...currentConfig, visible: false }; render(currentConfig); setTimeout(destroy, 100); } function destroy() { const unmountResult = ReactDOM.unmountComponentAtNode(div); if (unmountResult && div.parentNode) { div.parentNode.removeChild(div); } } function render(config) { ReactDOM.render(, div); } render(currentConfig); return { destroy: close }; }; const modal = { popup } ; export default modal;

注意

由于弹窗是我们手动new出来的,并没有包含在入口的jsx引用中,所以类似routerstorelocale这些Provider需要手动重新包装下,这样才能像页面一样使用相关功能

补充一个vue2的完整实现

import Vue from 'vue'
import { Vue as VueDecorator } from 'vue-property-decorator'

import Dialog from './modalDialog.vue'
import Popup from './popup.vue'

let store: Record
let router: Record
let i18n: Record
let apolloProvider: Record

interface CommonConfig {
  title?: string
  popType?: 'popup' | 'dialog'
  width?: string
}

interface ModalConfig extends CommonConfig {
  content?: Record & {
    kxDialogConfig?: CommonConfig
  }
  props?: Record
  context?: VueDecorator
}

/*
    popType: 弹窗类型 custom_popup: 完全自定义弹层 | dialog: el-dialog | popup: 没有footer的el-dialog
*/
const modal = function (cfg: ModalConfig) {
  const config = Object.assign({}, cfg)
  const { content, context } = config
  Reflect.deleteProperty(config, 'content')
  Reflect.deleteProperty(config, 'context')

  const popComp = [config.popType, content?.kxDialogConfig?.popType].includes('popup') ? Popup : Dialog
  let Component = Object.assign({}, popComp, {
    store,
    router,
    i18n,
    apolloProvider,
  }) as any

  // 加入provide支持
  if (context) {
    Object.assign(Component, {
      provide() {
        return (context as any)._provided
      },
    })
  }
  Object.assign(Component.components, {
    innerMain: content,
  })
  Component = Vue.extend(Component)

  const { props, ...prosData } = config
  const instance = new Component({
    propsData: {
      innerProps: props,
      compName: 'innerMain',
      ...prosData,
      ...(content?.kxDialogConfig || {}),
    },
  })

  document.body.appendChild(instance.$mount().$el)

  return instance
}

modal.setProvider = function (data: any) {
  if (data.store) {
    store = data.store
  }
  if (data.router) {
    router = data.router
  }
  if (data.i18n) {
    i18n = data.i18n
  }
  if (data.apolloProvider) {
    apolloProvider = data.apolloProvider
  }
}

export default modal
注意:store 和上下文无关,可以单独赋值。provide和上下文下关需要每次调用modal时传递一下context

你可能感兴趣的:(触发式的自定义弹层)