preface

On a dark, windy night… No drama brush I accidentally think of the previous processing of some popover pit.

And then accidentally brush”Portal“, just knowModalThere is such a wonderful way to achieve, shun and simply think of theUIThe implementation principle of component library is finished.

This article will describe the realization principle of Modal popover class components:

1. ModalThe basic principle of popovers

I define a popover class as a component that is free from a fixed hierarchy and no longer subject to a cascading context.

Common Modal Modal boxes, Dialog Dialog boxes and Notification Notification boxes are the most commonly used interaction modes.

In our page sometimes need some specific popovers, through the changeUIComponents are too cumbersome.

At this point, the cuttot level will think: easy, create a

for absolute positioning.

If only for the current routing page, it is ok. But when it comes to component reuse and abstraction into declarative form, there are big pitfalls:

  1. Without encapsulation, component code needs to be pasted everywhere.
  2. Even if encapsulated, they are created under each routing page<div/>, easy to cause style pollution.
  3. Class shopping cart popover, and how to deal with data and rendering?
  4. Further, what if the component library is used as a performance review and every environment looks different?

1.1 JqueryEra of popover implementation

When I first started out, I went to various resource sites to find the UI components of Jquery. I think the front end of three or four years of experience has enjoyed this.

Popovers from this era (three or four years ago), because there’s noReact/VueThe concept of the root node is generally:

  1. Direct manipulation of the real DOM, using familiar DOM manipulation methods to append the element where the instruction is located to another DOM node.Such as: document.body.appendChild.
  2. throughoverflow: hiddenordisplay:none(or adjust thez-index) to hide.

The cost of this kind of manipulation of the real DOM, triggering redraw/reflow on a large project, is terrible, and the internal data/styles are not easily changed. Situations like the following are easy to occur:

  1. The original image is fixed in the area.

2. Overflow occurred after a small pop-up display.As theReact / VueThe development of advanced library, also had a variety of program choice in succession…

1.2 React / VueEarly implementation.

Early implementations of React/Vue were similar to those of Jquery: they relied on parent data and mounted pop-ups within the current component.

Vue is a little better, with custom instructions this way.

From: Portal Technology in Vue

In the vue-dom-portal example, the code is as simple as moving the current DOM to the specified location:

function (node = document.body) { if (node === true) return document.body; return node instanceof window.Node ? node : document.querySelector(node); } const homes = new Map(); const directive = { inserted(el, { value }, vnode) { const { parentNode } = el; const home = document.createComment(""); let hasMovedOut = false; if (value ! == false) { parentNode.replaceChild(home, el); // moving out, el is no longer in the document getTarget(value).appendChild(el); // moving into new place hasMovedOut = true; } if (! homes.has(el)) homes.set(el, { parentNode, home, hasMovedOut }); // Remember where home is or should be}, componentUpdated(el, {value}) {const {parentNode, home, hasMovedOut } = homes.get(el); // recall where home is if (! hasMovedOut && value) { parentNode.replaceChild(home, el); getTarget(value).appendChild(el); homes.set(el, Object.assign({}, homes.get(el), { hasMovedOut: true })); } else if (hasMovedOut && value === false) { parentNode.replaceChild(el, home); homes.set(el, Object.assign({}, homes.get(el), { hasMovedOut: false })); } else if (value) { getTarget(value).appendChild(el); } }, unbind(el, binding) { homes.delete(el); }}; function plugin(Vue, { name = "dom-portal" } = {}) { Vue.directive(name, directive); } the plugin, version = "0.1.6"; export default plugin; if (typeof window ! == "undefined" && window.Vue) { window.Vue.use(plugin); }Copy the code

As you can see, get the EL (real DOM) of the instance while inserted, then replace it, and then manipulate the DOM again with the value of the directive when componentUpdated.

While inserted, the parent of the current node and the DOM node to which the node was replaced (a comment node), as well as the status of the node being moved, are recorded in an external map that can be used in other periodic function declarations. Double counting can be avoided.

However, React/Vue implementations all suffer from similar problems:

  1. Execution of the life cycle can be messy.
  2. Need to pass throughreduxorpropsManage data, but that’s not good for aUIToo bloated for components.

React officials also realized that building components that are separate from their parent components can be tricky, so they introduced a feature called “Portal” in v16. Vue3 also borrows from good plug-ins and uses Portal as a built-in component.

1.3 portalPortalplan

React / VueThe second set of solutions are all based on operational virtualitydom:

Define a set of components that will be within the componentvnode/ReactDOMMove to another component and render separately.

2. ReactthePortal

React Portal is called Portal because it does exactly the same thing as “Portal” : Render into a component that actually changes the DOM structure of another part of the page.

ReactDOM.createPortal(child, container)
Copy the code
  1. The first parameter (child) is anything renderableReactChild elements, such as an element, string, or fragment.
  2. The second parameter (container) is aDOMElements.

In V16, creating a Dialog component with Portal is much easier. There is no need to invoke componentDidMount, componentDidUpdate, or API to clean up Portal.

import React from 'react'; import {createPortal} from 'react-dom'; class Dialog extends React.Component { constructor() { super(... arguments); const doc = window.document; this.node = doc.createElement('div'); doc.body.appendChild(this.node); } render() { return createPortal( <div class="dialog"> {this.props.children} </div>, // JSX this.node // DOM node); } componentWillUnmount() { window.document.body.removeChild(this.node); }Copy the code

Of course, as a React Hooks player, we don’t do anything about it.

2.1 Popular Component librariesAnt DesignThe implementation in

I was hoping to get a peek into the Ant Design library, but it turns out things aren’t that simple.

Three libraries/places were addressed before the key to the implementation was discovered:

  1. import Dialog from 'rc-dialog';
  2. import Portal from 'rc-util/lib/PortalWrapper';
  3. import Portal from './Portal';

The implementation is as I expected:

import React from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; export default class Portal extends React.Component { static propTypes = { getContainer: PropTypes.func.isRequired, children: PropTypes.node.isRequired, didUpdate: PropTypes.func, } componentDidMount() { this.createContainer(); } componentDidUpdate(prevProps) { const { didUpdate } = this.props; if (didUpdate) { didUpdate(prevProps); } } componentWillUnmount() { this.removeContainer(); } createContainer() { this._container = this.props.getContainer(); this.forceUpdate(); } removeContainer() { if (this._container) { this._container.parentNode.removeChild(this._container); } } render() { if (this._container) { return ReactDOM.createPortal(this.props.children, this._container); } return null; }}Copy the code

renderIn theReactDOM.createPortal

** This is also why most Modal components do not provide an API for tampering with the overall style, only by resetting the style globally. `

2.1 React HooksVersion of the popup window:useModal

Step 1: Create oneModalcomponent

import React from 'react' import ReactDOM from 'react-dom' type Props = { children: React.ReactChild closeModal: () => void } const Modal = React.memo(({ children, closeModal }: Props) => { const domEl = document.getElementById('modal-root') if (! domEl) return null return ReactDOM.createPortal( <div> <button onClick={closeModal}>Close</button> {children} </div>, domEl ) }) export default ModalCopy the code

Step 2: CustomizeuseModal

Import React, {useState} from 'React' import Modal from './Modal' // Modal components of the most basic two events, show/hide export const useModal = () => { const [isVisible, setIsVisible] = useState(false) const show = () => setIsVisible(true) const hide = () => setIsVisible(false) const RenderModal = ({ children }: { children: React.ReactChild }) => ( <React.Fragment> {isVisible && <Modal closeModal={hide}>{children}</Modal>} </React.Fragment> )  return { show, hide, RenderModal, } }Copy the code

It is easy to understand, do not understand the advice to switch to writing Vue.

Step 3: Use it

import React from 'react' import { useModal } from './useModal' const App = React.memo(() => { const { show, hide, RenderModal } = useModal() return ( <div> <div> <p>some content... </p> <button onClick={show}> Open </button> <button onClick={hide}> close </button> <RenderModal> <p> </p> </RenderModal> </div> <div ID ='modal-root' /> </div>)}) export default AppCopy the code

3. Vue 3thePortal

Vue is a reference, but it’s much easier to use.

<OtherComponent>
  <Portal target="#popup-target">
    <Modal />
  </Portal>
</OtherComponent>
....
<div id="popup-target"></div>
Copy the code

In the example above, the
component will be rendered in a container with ID =portal-target, even though it is inside the OtherComponent component.

This, this… It smells so good. Further usage is as follows:

<! -- UserCard.vue --> <template> <div class="user-card"> <b> {{ user.name }} </b> <button @click="isPopUpOpen = true">Remove user</button> <Portal target="#popup-target"> <div v-show="isPopUpOpen"> <p>Are you sure?</p> <button @click="removeUser">Yes</button> <button @click="isPopUpOpen = false">No</button> </div> </Portal> </div> </template>Copy the code

Then I went to the Vue 3 source under implementation: in the packages/runtime – core/SRC/components/Portal. Ts directory:

import { ComponentInternalInstance } from '.. /component' import { SuspenseBoundary } from './Suspense' import { RendererInternals, MoveType } from '.. /renderer' import { VNode, VNodeArrayChildren, VNodeProps } from '.. /vnode' import { isString, ShapeFlags, PatchFlags } from '@vue/shared' import { warn } from '.. /warning' export const isPortal = (type: any): boolean => type.__isPortal export interface PortalProps { target: string | object } export const PortalImpl = { __isPortal: true, process( n1: VNode | null, n2: VNode, container: object, anchor: object | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, optimized: boolean, { mc: mountChildren, pc: patchChildren, pbc: patchBlockChildren, m: move, o: { insert, querySelector, setElementText, createComment } }: RendererInternals ) { const targetSelector = n2.props && n2.props.target const { patchFlag, shapeFlag, children } = n2 if (n1 == null) { // insert an empty node as the placeholder for the portal insert((n2.el = createComment(`portal`)), container, anchor) if (__DEV__ && isString(targetSelector) && ! querySelector) { warn( `Current renderer does not support string target for Portals. ` + `(missing querySelector renderer option)` ) } } else { //.... The core is to createComment nodes and insert them into different nodes via createComment. // Finally setElementText, resets the contents of the inserted node. } } } // Force-casted public typing for h and TSX props inference export const Portal = (PortalImpl as any) as { __isPortal: true new (): { $props: VNodeProps & PortalProps } }Copy the code

Important explanation, all in afore-mentioned notes, see temporarily, say wrong thank pointing out.

Where: createComment is the further encapsulation of DOM.createcomment by Vue.

Conclusion & reference

This article is my midnight boring toss out, the original plan is to write three components, but the implementation of the popover class is more interesting.

I will write this series while looking for a job, and the next one will talk about the realization of Steps and Transfer shuttle box (of course, it is too difficult to just talk about it, heh heh).

By the way, send me any internal tweets that fit.

Reference article:

  1. Building a Simple and Reusable Modal with React Hooks and Portals
  2. Portal technology in Vue

“] (www.dazhuanlan.com/2019/10/05/.)

  1. Portal — A New Feature in Vue 3
  2. React Portal’s past life

“] (juejin. Cn/post / 684490…).

❤️ Read three things

If you find this article inspiring, I’d like to invite you to do me three small favors:

  1. Like, so that more people can see this content (collection does not like, is a rogue -_-)
  2. Pay attention to “front-end persuaders” and share original knowledge from time to time.
  3. Look at other articles as well

Personal wechat: Huab119

You can also get all the posts from my GitHub blog:

Front-end persuasion guide: github.com/roger-hiro/… Let’s play. ~