The last article explained how to build a React library shelf from 0 to 1, but building a library for just one or two components is overdone.

This time, Popup, a common component on mobile terminals, is used as an example to release a complete NPM package in the most convenient and quick form.

  • 🚀 Online Preview
  • ✨ Warehouse address

If it helps you, please like Star and PR.

If there is any mistake, please correct it in the comments section.

This article contains the following:

  1. Popup component development;

  2. Use of some tools

    • TSDX: Project initialization, development, and packaging master;
    • Np: One-click release of NPM package;
    • Gh-pages: Deployment example demo;
    • readme-md-generator: Generates a canonical copyREADME.mdFile.

This article does not dwell on the details of packaging as the component library article did, because there are fundamental differences in packaging between individual components and component libraries. Component libraries need to provide the ability to import on demand, so components are simply compiled syntactically (and styled in a more convoluted way), so GULP is chosen to manage the packaging process. A single component is different. Webpack and rollup are more suitable for this scenario, since there is no need to provide the ability to import on demand and only need to package a JS bundle and CSS bundle.

Project initialization

TSDX is a scaffold with three built-in project templates:

  1. Basic => Tool package template
  2. React => React component template that uses parcel as example for debugging
  3. React-with-storybook => again, use storybook for documentation and example debugging

Templates also come with NPM scripts such as Start, build, test, and Lint built in, which are literally out of the box with zero configuration.

Select the React template for easy explanation.

Run NPX TSDX create react-easy-popup, select React to create the project and go to the project directory.

Configuration TSDX

Awkwardness: TSDX does not provide support for style file packaging (foreign developers really like CSS in JS).

Our intention is to develop a component that will not impose a styled- Components dependency on the user, so we will need to configure style file processing support (less).

Refer to the section on Customization-TSDX for configuration.

Installation-related dependencies:

yarn add rollup-plugin-postcss autoprefixer cssnano less --dev
Copy the code

Create tsdx.config.js and write the following:

tsdx.config.js

const postcss = require('rollup-plugin-postcss');
const autoprefixer = require('autoprefixer');
const cssnano = require('cssnano');

module.exports = {
  rollup(config, options) {
    config.plugins.push(
      postcss({
        plugins: [
          autoprefixer(),
          cssnano({
            preset: 'default',})],inject: false.extract: 'react-easy-popup.min.css',}));returnconfig; }};Copy the code

Configure the Browserslist field in package.json.

package.json

// ...
+ "browserslist": [
+ "last 2 versions",
+ "Android > = 4.4",
+ "iOS >= 9"
+],
// ...
Copy the code

Clear the SRC directory and create index.tsx and index.less.

src/index.tsx

import * as React from 'react';
import './index.less';

const Popup = (a)= > (
  <div className="react-easy-popup">hello,react-easy-popup</div>
);

export default Popup;
Copy the code

src/index.less

.react-easy-popup {
  display: flex;
  color: skyblue;
}
Copy the code

example/index.tsx

import 'react-app-polyfill/ie11';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import Popup from '.. /. '; // There is a parcel alias below
import '.. /dist/react-easy-popup.min.css'; // There is no parcel alias write a relative path

const App = (a)= > {
  return (
    <div>
      <Popup />
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById('root'));
Copy the code

Go to the project root directory and run the following command:

yarn start
Copy the code

Changes to content in the SRC directory are now monitored in real time, and the generated dist folder in the root directory contains the packaged content.

The debug folder at development time is Example, starting with a separate terminal. Run the following command:

cd example
yarn # install dependencies
yarn start Example # start
Copy the code

In localhost:1234, you can see that the project is started with the style in effect and the browser prefix.

If an error occurs after example is started, delete the. Cache and dist directories in example and start again

Note that example’s entry file index. TSX imports our packaged file, dist/index.js.

But the import path is ‘.. /.’, this is because TSDX uses parcel aliasing.

Also, look at the dist folder in the root directory:

dist

├ ─ ─ the index, which sComponent declaration file├ ─ ─ index. JsComponent entry├ ─ ─ the react - easy - popup. CJS. Development. Js# Commonjs specification for component code introduced during development├ ─ ─ the react - easy - popup. CJS. Development. Js. The map# soucemap├ ─ ─ the react - easy - popup. CJS. Production. Min. Js# Compressed component code├ ─ ─ the react - easy - popup. CJS. Production. Min. Js. The map# sourcemap├ ─ ─ the react - easy - popup. Esm. JsComponent code for the ES Module specification├ ─ ─ the react - easy - popup. Esm. Js. The map# sourcemap└ ─ ─ the react - easy - popup. Min. CSS# style file
Copy the code

The main, module, and Typings configurations can also be easily found in package.json.

It is not difficult to manually build a component template based on rollup, but the community has provided a convenient wheel, so don’t duplicate the wheel. You need both the ability to make wheels and the will not to make wheels. Seems like we’re building wheels?

Implement the Portal

Popup is very common in mobile scenarios. It is internally implemented based on Portal and can itself be used as a lower component of components such as Toast and Modal.

To implement Popup, you first implement a Portal based on reactdom.createPortal.

Here is a brief summary based on official documents.

  1. What is a portal? Portal is an excellent solution for rendering child nodes to DOM nodes that exist outside the parent component.

  2. Why do you need a portal? The parent component has overflow: Hidden or Z-Index style, and we need the child component to visually “jump” out of its container. Examples are dialog boxes, hover cards, and prompt boxes.

It is also important to note that portal behaves like normal React child nodes and still exists in the React tree, so the Context is still accessible. There are some shell components that provide xxx.show() API for popup, which is convenient. Although the underlying layer is also Portal based, reactdom. render is executed internally, and the React tree is removed from the main application, so it cannot obtain the Context.

Recommended reading: Portal: React Portal- Cheng Mo Morgan

Clear the SRC directory and create the following files:

├ ─ ─ index. Less# style file├ ─ ─ index. Ts# import file├ ─ ─ popup. TSX# popup components├ ─ ─ portal. TSX# portal components└ ─ ─ the tsType definition file
Copy the code

Before writing the code, you need to determine the API of the Portal component.

The parameters are basically the same as those accepted by the reactdom.createPortal method: the specified mount node and the content. The only difference is that Portal creates a node for use when it does not pass in the specified mount node.

attribute instructions type The default value
node Optional, custom container node HTMLElement
children What needs to be delivered ReactNode

In type.ts, write the Props type definition for the Portal.

src/type.ts

export typePortalProps = React.PropsWithChildren<{ node? : HTMLElement; } >.Copy the code

Now write the code:

import * as React from 'react';
import * as ReactDOM from 'react-dom';

import { PortalProps } from './type';

const Portal = ({ node, children }: PortalProps) = > {
  return ReactDOM.createPortal(children, node);
};

export default Portal;
Copy the code

Note: react-typescript-cheatsheet:Section 2: Getting Started => Function Components => What about React.FC/React.FunctionComponent?

The code is relatively simple to implement, just call reactdom.createPortal, does not take into account that the user has not passed in the node: the node needs to be created internally and destroyed when the component is destroyed.

import * as React from "react";
import * as ReactDOM from "react-dom";

import { PortalProps } from "./type";

// Check whether it is a browser environment
constcanUseDOM = !! (typeof window! = ="undefined" &&
  window.document &&
  window.document.createElement
);

const Portal = ({ node, children }: PortalProps) = > {
  // Use ref to record the initial value of the internally created node as null
  const defaultNodeRef = React.useRef<HTMLElement | null> (null);

  // Remove the node when the component is uninstalled
  React.useEffect(
    (a)= >() = > {if (defaultNodeRef.current) {
        document.body.removeChild(defaultNodeRef.current); }} []);// If the non-browser environment directly returns null server rendering required
  if(! canUseDOM)return null;

  // If the user does not pass in a node and Portal does not create a node, the node is created and added to the body
  if(! node && ! defaultNodeRef.current) {const defaultNode = document.createElement("div");
    defaultNode.className = "react-easy-popup__portal";
    defaultNodeRef.current = defaultNode;
    document.body.appendChild(defaultNode);
  }

  returnReactDOM.createPortal(children, (node || defaultNodeRef.current)!) ;// Assertion is required here
};

export default Portal;
Copy the code

Also, in order for non-TS users to enjoy good runtime error prompts, prop-types need to be installed.

yarn add prop-types
Copy the code

src/portal.tsx

// ...

+ Portal.propTypes = {
+ node: canUseDOM ? PropTypes.instanceOf(HTMLElement) : PropTypes.any,
+ children: PropTypes.node,
+};

export default Portal;
Copy the code

This completes the writing of the Portal component, which is exported in the entry file.

src/index.ts

export { default as Portal } from './portal';
Copy the code

Example /index.ts introduces Portal to test.

example/index.tsx

import "react-app-polyfill/ie11";
import * as React from "react";
import * as ReactDOM from "react-dom";
- import Popup from ".. /. "; // There is a parcel alias below
- import ".. /dist/react-easy-popup.min.css"; // It does not exist here
+ import { Portal } from '.. /. ';// Create a custom node node+ const node = document.createElement('div');
+ node.className = 'react-easy-popup__test-node';
+ document.body.appendChild(node);

const App = () => {
  return (
    <div>
- 
      
+ 
      
       123
      
+ 
      
       456
      
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("root"));
Copy the code

You see the expected DOM structure in the web page.

Realize the Popup

The API combed

The old rule: plan the API, write the type definition, then start writing the code.

I wrote this component with reference to popup-Cube-UI.

The final API is as follows:

attribute instructions type The default value
visible Optional, control popUp implicit boolean false
position Optional, content location ‘center’ / ‘top’ / ‘bottom’ / ‘left’ / ‘right’ ‘center’
mask Optional, control the hidden layer boolean true
maskClosable Optional, click to see if the mask can be closed boolean false
onClose Optionally, close the function. If maskClosable is true, click the mask to call the function function () = > {}
node Optional, the element mounts the node HTMLElement
destroyOnClose Optional, turns off whether to unload internal elements boolean false
wrapClassName Optional, customize the Popup outer container class name string ‘ ‘

src/type.ts

export type Position = 'top' | 'right' | 'bottom' | 'left' | 'center';

typePopupPropsWithoutChildren = { node? : HTMLElement; } &typeof defaultProps;

export type PopupProps = React.PropsWithChildren<PopupPropsWithoutChildren>;

// It's hard to write the default property here because typescript doesn't declare the react component's default property properly
export const defaultProps = {
  visible: false,
  position: 'center' as Position,
  mask: true,
  maskClosable: false,
  onClose: (a)= > {},
  destroyOnClose: false};Copy the code

Write the basic structure of Popup.

src/popup.tsx

import * as React from 'react';
import PropTypes from 'prop-types';

import { PopupProps, defaultProps } from './type';
import './index.less';

const Popup = (props: PopupProps) = > {
  console.log(props);
  return <div className="react-easy-popup">hello,react-easy-popup</div>;
};

Popup.propTypes = {
  visible: PropTypes.bool,
  position: PropTypes.oneOf(['top'.'right'.'bottom'.'left'.'center']),
  mask: PropTypes.bool,
  maskClosable: PropTypes.bool,
  onClose: PropTypes.func,
  stopScrollUnderMask: PropTypes.bool,
  destroyOnClose: PropTypes.bool,
};

Popup.defaultProps = defaultProps;

export default Popup;
Copy the code

Export in the entry file.

src/index.ts

+ export { default as Popup } from './popup';
Copy the code

Pre-css knowledge

Before we start developing the logic, let’s be clear:

There are animation effects for the entrance and entrance of the Mask and Content. Specific performance is as follows: For example, if the content is in the middle (position === ‘center’), the animation effect will be Fade; if it is on the left (position === ‘left’), the animation effect will be SlideRight. The same applies to other positions.

Review zhang Xinxu’s article: Small tip: Transition and Visibility

To highlight:

  1. opacityThe value of the01Transition between (transitionYou can implement the Fade animation. However, an element can be overwritten even if its transparency goes to 0 and it is invisible to the naked eye, but it is still clicked on the page. We want the element to fade out, and the element can be hidden automatically after the animation is over.
  2. Element hiding is easy to imaginedisplay:none. whiledisplay:noneUnable to applytransitionEffects, even destructive effects;
  3. visibility:hiddenCan be seen asvisibility:0;visibility:visibleCan be seen asvisibility:1. In fact, as long asvisibilityThe value is greater than0It’s just displayed.

To sum up: we want to use opacity to implement Fade, but we want the element to be hidden after it fades out, instead of being overwritten on other elements with transparency of 0. Therefore, the visibility attribute needs to be configured. When the fade animation ends, the visibility value also changes from Visible to Hidden, and the element is successfully hidden.

If the opacity of the mask fades to 0 after the animation ends, but it is not hidden, then the mask disappears visually but is actually covered on the page, and events on the page cannot be triggered.

Preset animation style

Use the react-transition-group to animate the effects, and you’ll need some built-in animation styles.

Create a new animation. Less and write the following animation style.

Expand to view code
@animationDuration: 300ms;

.react-easy-popup {
  /* Fade */
  &-fade-enter.&-fade-appear.&-fade-exit-done {
    visibility: hidden;
    opacity: 0;
  }

  &-fade-appear-active.&-fade-enter-active {
    visibility: visible;
    opacity: 1;
    transition: opacity @animationDuration, visibility @animationDuration;
  }
  &-fade-exit.&-fade-enter-done {
    visibility: visible;
    opacity: 1;
  }
  &-fade-exit-active {
    visibility: hidden;
    opacity: 0;
    transition: opacity @animationDuration, visibility @animationDuration;
  }

  /* SlideUp */
  &-slide-up-enter.&-slide-up-appear.&-slide-up-exit-done {
    transform: translate(0.100%);
  }
  &-slide-up-enter-active.&-slide-up-appear-active {
    transform: translate(0.0);
    transition: transform @animationDuration;
  }
  &-slide-up-exit.&-slide-up-enter-done {
    transform: translate(0.0);
  }
  &-slide-up-exit-active {
    transform: translate(0.100%);
    transition: transform @animationDuration;
  }

  /* SlideDown */
  &-slide-down-enter.&-slide-down-appear.&-slide-down-exit-done {
    transform: translate(0, -100%);
  }
  &-slide-down-enter-active.&-slide-down-appear-active {
    transform: translate(0.0);
    transition: transform @animationDuration;
  }
  &-slide-down-exit.&-slide-down-enter-done {
    transform: translate(0.0);
  }
  &-slide-down-exit-active {
    transform: translate(0, -100%);
    transition: transform @animationDuration;
  }

  /* SlideLeft */
  &-slide-left-enter.&-slide-left-appear.&-slide-left-exit-done {
    transform: translate(100%.0);
  }

  &-slide-left-enter-active.&-slide-left-appear-active {
    transform: translate(0.0);
    transition: transform @animationDuration;
  }

  &-slide-left-exit.&-slide-left-enter-done {
    transform: translate(0.0);
  }

  &-slide-left-exit-active {
    transform: translate(100%.0);
    transition: transform @animationDuration;
  }

  /* SlideRight */
  &-slide-right-enter.&-slide-right-appear.&-slide-right-exit-done {
    transform: translate(-100%.0);
  }

  &-slide-right-enter-active.&-slide-right-appear-active {
    transform: translate(0.0);
    transition: transform @animationDuration;
  }

  &-slide-right-exit.&-slide-right-enter-done {
    transform: translate(0.0);
  }

  &-slide-right-exit-active {
    transform: translate(-100%.0);
    transition: transform @animationDuration; }}Copy the code

Complete basic logic

Install dependencies.

yarn add react-transition-group classnames

yarn add @types/classnames @types/react-transition-group --dev
Copy the code
  • Node: passPortalCan;
  • Visible: Assigns this property to the mask and to the content outer layerCSSTransitionThe component’sinAttribute, control layer and content transition explicit;
  • DestroyOnClose: Assigns this property to the content outer layerCSSTransitionThe component’sunmountOnExitProperty that determines whether to unload the content node while hiding;
  • WrapClassName: concatenated to the outer container nodeclassName
  • Position: 1) Used to obtain the corresponding animation name of the content node; 2) Determine the container node and the content node class name, and determine the content node position with the style;
  • Mask: determines the mask nodeclassNameSo as to control the existence of the layer;
  • MaskClose: Determines whether clicking the mask triggers the onClose function.

Those of you who have used ANTD know that the content node will not be mounted before the first visible === True of ANTD modal, only the first visible === True, the content node will be mounted, and then it will be hidden in style, instead of unmounting the content node. Unless you manually set the destroyOnClose property, we’ll implement this feature as well.

The code logic is relatively simple, in the concatenation of class names with the style file to read together, important points are marked with comments.

Expand to view the logical code
// Class name prefix
const prefixCls = "react-easy-popup";
// Animation length
const duration = 300;
// The mapping between position and animation
const animations: { [key in Position]: string } = {
  bottom: `${prefixCls}-slide-up`.right: `${prefixCls}-slide-left`.left: `${prefixCls}-slide-right`.top: `${prefixCls}-slide-down`.center: `${prefixCls}-fade`};const Popup = (props: PopupProps) = > {
  const firstRenderRef = React.useRef(false);

  const { visible } = props;
  // Return null until first visible === true
  if(! firstRenderRef.current && ! visible)return null;
  if(! firstRenderRef.current) { firstRenderRef.current =true;
  }

  const {
    node,
    mask,
    maskClosable,
    onClose,
    wrapClassName,
    position,
    destroyOnClose,
    children,
  } = props;

  // Mask the click event
  const onMaskClick = (a)= > {
    if(maskClosable) { onClose(); }};// Splice the container node class name
  const rootCls = classnames(
    prefixCls,
    wrapClassName,
    `${prefixCls}__${position}`
  );

  // Splice mask node class name
  const maskCls = classnames(`${prefixCls}-mask`, {[`${prefixCls}-mask__visible`]: mask,
  });

  // Concatenate the content node class name
  const contentCls = classnames(
    `${prefixCls}-content`.`${prefixCls}-content__${position}`
  );

  // Content transition animation
  const contentAnimation = animations[position];

  return (
    <Portal node={node}>
      <div className={rootCls}>
        <CSSTransition
          in={visible}
          timeout={duration}
          classNames={` ${prefixCls}-fade`}
          appear
        >
          <div className={maskCls} onClick={onMaskClick}></div>
        </CSSTransition>
        <CSSTransition
          in={visible}
          timeout={duration}
          classNames={contentAnimation}
          unmountOnExit={destroyOnClose}
          appear
        >
          <div className={contentCls}>{children}</div>
        </CSSTransition>
      </div>
    </Portal>
  );
};
Copy the code
Expand to view the style code
@import './animation.less';

@popupPrefix: react-easy-popup;

.@{popupPrefix} {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 1999;
  pointer-events: none; // Note that none produces a point-through effect, which means that the container node does not exist at all

  .@{popupPrefix}-mask {
    position: absolute;
    top: 0;
    left: 0;
    display: none; // mask is hidden by default
    width: 100%;
    height: 100%;
    overflow: hidden;
    background-color: rgba(0.0.0.0.72);
    pointer-events: auto;

    &__visible {
      display: block; / / show the mask
    }

    // fix some android webview opacity render bug
    &::before {
      display: block;
      width: 1px;
      height: 1px;
      margin-left: -10px;
      background-color: rgba(0.0.0.0.1);
      content: '. '; }}/* Use Flex to center position with center */
  &__center {
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .@{popupPrefix}-content {
    position: relative;
    width: 100%;
    color: rgba(113.113.113.1);
    pointer-events: auto;
    -webkit-overflow-scrolling: touch; /* ios5+ */
    ::-webkit-scrollbar {
      display: none;
    }

    &__top {
      position: absolute;
      left: 0;
      top: 0;
    }

    &__bottom {
      position: absolute;
      left: 0;
      bottom: 0;
    }

    &__left {
      position: absolute;
      width: auto;
      max-width: 100%;
      height: 100%;
    }

    &__right {
      position: absolute;
      right: 0;
      width: auto;
      max-width: 100%;
      height: 100%;
    }

    &__center {
      width: auto;
      max-width: 100%; }}}Copy the code

Once the component is written, you can write the related sample test functionality in example/index.ts.

example/index.ts

Deployment of making pages

Most people using an NPM package will look at the examples first and then the documentation.

Next, package the sample project in Example and deploy it on Github Pages.

Install the gh – pages.

yarn add gh-pages --dev
Copy the code

Package. json added script.

package.json

{
  "scripts": {
    //...
    "predeploy": "npm run build && cd example && npm run build",
    "deploy": "gh-pages -d ./example/dist"
  }
}
Copy the code

Due to gh – the default deployment under the https://username.github.io/repo pages, rather than the root. To be able to reference static resources correctly, you also need to modify the packaged public-URL.

Modify package.json for example:

{
  "scripts":{
- "build": "parcel build index.html"
+ "build": "parcel build index.html --public-url https://username.github.io/repo"}}Copy the code

https://username.github.io/repo remember with your own oh.

Execute YARN deploy in the root directory and wait until the script is executed.

Write the README. Md

A formal README would be professional, so use the readME-MD-Generator to generate the basic framework and fill it in.

Readme-md-generator :📄 CLI that generates beautiful readme.md files

npx readme-md-generator -y
Copy the code

README.md

Use NP to send packets

In the last article, a script was written specifically to address the following six points:

  1. Version update
  2. Generate the CHANGELOG
  3. Push to git repository
  4. Component packaging
  5. Release to NPM
  6. Tag and push to Git

This time will not generate a CHANGELOG file, the other five points with NP, the operation is very simple.

np:A better npm publish

yarn add np --dev
Copy the code

package.json

{
  "scripts": {
    // ...
    "release": "np --no-yarn --no-tests --no-cleanup"
  }
}
Copy the code
npm login
npm run release
Copy the code
  • --no-yarn: do not useyarn. There are some problems between NPM and YARN when sending packets.
  • --no-tests: Test cases are not written yet, so skip first;
  • --no-cleanup: Do not reinstall node_modules when sending packages;
  • The first time a new package is released, an error may be reported because NP has NPM two-factor authentication, but it can still be released successfully, waiting for subsequent updates.

See the official documentation for more configurations.

conclusion

This article is very fast (and tiring), especially for the component logic, which relies heavily on animations, and my CSS is not very good.

If it helps you, feel free to like Star and PR and, of course, use this component.

If there is any mistake, please correct it in the comments section.

  • Warehouse address: Poke me ✨