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 knowModal
There is such a wonderful way to achieve, shun and simply think of theUI
The implementation principle of component library is finished.
This article will describe the realization principle of Modal popover class components:
1. Modal
The 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 change
UI
Components 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:
- Without encapsulation, component code needs to be pasted everywhere.
- Even if encapsulated, they are created under each routing page
<div/>
, easy to cause style pollution. - Class shopping cart popover, and how to deal with data and rendering?
- Further, what if the component library is used as a performance review and every environment looks different?
1.1 Jquery
Era 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 no
React
/Vue
The concept of the root node is generally:
- 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
. - through
overflow: hidden
ordisplay: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:
- The original image is fixed in the area.
2. Overflow occurred after a small pop-up display.
As the
React / Vue
The development of advanced library, also had a variety of program choice in succession…
1.2 React / Vue
Early 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:
- Execution of the life cycle can be messy.
- Need to pass through
redux
orprops
Manage data, but that’s not good for aUI
Too 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 portalPortal
plan
React / Vue
The second set of solutions are all based on operational virtualitydom
:
Define a set of components that will be within the componentvnode/ReactDOM
Move to another component and render separately.
2. React
thePortal
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
- The first parameter (
child
) is anything renderableReact
Child elements, such as an element, string, or fragment. - The second parameter (
container
) is aDOM
Elements.
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 Design
The 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:
import Dialog from 'rc-dialog';
import Portal from 'rc-util/lib/PortalWrapper';
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
render
In 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 Hooks
Version of the popup window:useModal
Step 1: Create oneModal
component
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 3
thePortal
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
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:
- Building a Simple and Reusable Modal with React Hooks and Portals
- Portal technology in Vue
“] (www.dazhuanlan.com/2019/10/05/.)
- Portal — A New Feature in Vue 3
- 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:
- Like, so that more people can see this content (collection does not like, is a rogue -_-)
- Pay attention to “front-end persuaders” and share original knowledge from time to time.
- 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. ~