0.1.2-56 • Published 2 years ago

grafu-lib v0.1.2-56

Weekly downloads
-
License
MIT
Repository
-
Last release
2 years ago

lib

Data visualization library

Documentation can be found in docs.

Table of Contents

How I Built This

I created a blank repository and built up from there to more easily configure our library to our needs. This process is adapted from an existing tutorial. It provides great additional explanation for each step.

Create Project

# it is required to house both lib and demo in a common folder (i.e. grafu/ as on gitlab)
cd grafu

# make directory
mkdir lib

# initialize project (create package.json)
yarn init

# answer questions
question name (lib): [enter]
question version (1.0.0): 0.1.0
question description: data visualization library
question entry point (index.js): index.ts
question repository url: [enter]
question author: [enter]
question license (MIT): [enter]
question private: true 

# go into directory
cd lib

Create remaining files and directories

We will continue to update each file as we set up.

lib 
├── src/
│   ├── components/
│   │   ├── Component1/
│   │   ├── Component2/
│   ├── styles/
│   ├── index.ts
│   ├── react-app-env.d.ts
│   ├── setupTests.ts
│   └── index.ts
│   
├── .gitignore
├── package.json
├── README.md
├── rollup.config.js
└── tsconfig.json

Update package.json and add standard app dependencies

# react
yarn add react react-dom @types/react react-scripts

# typescript
yarn add typescript

# material ui
yarn add @mui/material @emotion/react @emotion/styled @mui/lab @mui/icons-material

Once installed, we'll configure of these dependencies to be peer dependencies instead. Remove remove the following imports from dependencies, and updated the peerDependencies:

"peerDependencies": {
    "@emotion/react": ">=11.9.3",
    "@emotion/styled": ">=11.9.3",
    "@mui/icons-material": ">=5.8.4",
    "@mui/material": ">=5.8.5",
    "react": ">=18.2.0",
    "react-dom": ">=18.2.0"
}

Add additional libraries as needed.

Update typescript files

First, we need to update tsconfig.json:

{
  "compilerOptions": {
    "declaration": true, // generate types of our components
    "declarationDir": ".", // place types of our components in root of dist/
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": [
    "src"
  ],
  "exclude": [
    "node_modules",
    "dist",
  ]
}

Next, we need to update react-app-env.d.ts. This is needed for the library to understand types and imports.

/// <reference types="react-scripts" />

Set up eslint

yarn eslint --init

# answer style questions
Need to install the following packages:
  @eslint/create-config
Ok to proceed? (y) y
✔ How would you like to use ESLint? · To check syntax, find problems, and enforce code style
✔ What type of modules does your project use? · JavaScript modules (import/export)
✔ Which framework does your project use? · React
✔ Does your project use TypeScript? · Yes
✔ Where does your code run? · Browser
✔ How would you like to define a style for your project? · Answer questions about your style
✔ What format do you want your config file to be in? · JSON
✔ What style of indentation do you use? · Spaces
✔ What quotes do you use for strings? · Single
✔ What line endings do you use? · Unix
? Do you require semicolons? › No
Local ESLint installation not found.
The config that you\'ve selected requires the following dependencies:

eslint-plugin-react@latest @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest eslint@latest
? Would you like to install them now with npm? › Yes

.eslintrc.json will be generated in the root directory. Update with the following information:

...
"settings": {
  "react": {
    "version": "16.12.0"
  }
},
...
"rules": {
  "indent": [
    "error",
    4
  ],
  "linebreak-style": [
    "error",
    "unix"
  ],
  "quotes": [
    "warn",
    "single"
  ],
  "no-unused-vars": "off",
  "react/prop-types": "off",
  "react/display-name": "off"
}
...

Add lint script to package.json

"scripts": {
    "lint": "eslint src/**/*.{ts,tsx} --fix",
}

Configure Rollup

Rollup is "a module bundler for JavaScript which compiles small pieces of code into something larger and more complex, such as a library or application". It will be used to bundle our library into a package that can be used by another application locally or published to npm as a public library.

Install plugins

yarn add -D rollup @rollup/plugin-typescript @rollup/plugin-commonjs @rollup/plugin-node-resolve rollup-plugin-peer-deps-external rollup-plugin-terser rollup-plugin-delete

Update rollup.config.js

import commonjs from "@rollup/plugin-commonjs"; // enables transpilation into CommonJS (CJS) format
import del from 'rollup-plugin-delete'; // delete files and folders using Rollup
import peerDepsExternal from "rollup-plugin-peer-deps-external"; // prevents Rollup from bundling the peer dependencies
import resolve from "@rollup/plugin-node-resolve"; // bundles third party dependencies
import { terser } from "rollup-plugin-terser"; // minify Rollup bundle
import typescript from "@rollup/plugin-typescript"; // transpiles TypeScript into JavaScript
import pkg from './package.json'

export default {
    input: pkg.source,
    output: [
        {
            file: pkg.main,
            format: "cjs",
            sourcemap: true
        },
        {
            file: pkg.module,
            format: "esm",
            sourcemap: true
        }
    ],
    plugins: [
        peerDepsExternal(),
        resolve(),
        commonjs(),
        del({ 
            targets: ['dist/*'] // clear dist/ folder contents on new bundle creation
        }),
        typescript({ 
            tsconfig: './tsconfig.json', // use options specified in tsconfig
            exclude: ["**/*.test.tsx"], // exclude test files from bundle
        }),
        terser(),
        copy({
            targets: [{ src: 'package.json', dest: 'dist' }]
        })
    ]
};

Add build script

In package.json add the following script:

"scripts": {
    ...
    "build" "rollup -c"
}

Add custom scripts for quick actions as needed.

Set up testing

A note on Jest testing in the library that are different from in the demo app: Because there is no store, you do not need to wrap the component being testing with <Provider></Provider>. So, as you port over existing test files, they will need to be updated and rerun to ensure they still pass.

Add required imports

yarn add @testing-library/jest-dom @testing-library/react @testing-library/user-event @types/jest

Create setupTests.ts file

In the src/ directory, create a setupTests.ts file:

// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

Alternatively, you could choose to import this line at the top of all test files, but this is an import that will be run prior to tests.

Getting started

Now, you are able to build the library so it can be used in other applications.

Run the command yarn build to create a Rollup bundle in dist/. This bundle is what will be published when we choose to publish the library. For our current method of development, we will instead link this directory locally to import the library.

To add this library as a dependency to an application locally, add the following to the application's package.json:

"dependencies": {
    "grafu": "file:<relative-path-to-grafu-lib>/dist",
    ...
}

Important Note: In order to see the changes to the lib locally from demo or another application, you will need to update the package version in package.json everytime prior to a new build. The value we change this to doesn't matter. You can incremement or decrement the last digit by 1+. All that matters is the number after your changes is different to the version number prior to your changes.

Component structure

Here, I will outline how we should structure and implement components for consistency.

Directory Contents

A sample component directory looks like this:

ComponentA 
├── ComponentB/
│   ├── ChildComponentB.test.tsx
│   └── ChildComponentB.tsx
│
├── ComponentC/
│   ├── ChildComponentC.test.tsx
│   └── ChildComponentC.tsx
│
├── ComponentA.types.ts
├── ComponentA.styles.ts
├── ComponentA.test.tsx
└── ComponentA.tsx

Let's break this down:

  • ComponentA is an example data visualization component, a parent to ComponentB and ComponentC.
  • The root directory for ComponentA contains a types file (ComponentA.types.ts), styles file (ComponentA.styles.ts), a test file (ComponentA.test.tsx), and the component itself (ComponentA.tsx).

Important Notes:

  • Only create ComponentA.types.ts when there are common types and interfaces shared between the parent and child components, else types can be stored directly in a component file. IMPORTANT NOTE ABOUT TYPE FILES With the current configuration of the rollup bundler, if a file ends in d.ts it will not be bundled properly and will cause errors. Name all types files with the .types.ts suffix.
  • Only create ComponentA.styles.ts to help reduce the amount of code within the component itself or if these are common styles shared between parent and child components.

Sample Component File

Here is a sample of what ComponentA.tsx may look like:

import * as React from 'react';
import ComponentB from './ComponentB/ComponentB';
import ComponentC from './ComponentC/ComponentC';

/** title type */
type ComponentATitle = string;

export interface ComponentAProps {
  /** component title */
  title: ComponentATitle;
}

/**
 * 
 * @param props ComponentAProps
 * @returns ComponentA component
 */
export const ComponentA: React.FC<ComponentAProps> = props => {

  // destructure props
  const { title } = props;

  // return component
  return (
    <div>
      <h1>{title}</h1>
      <ComponentB />
      <ComponentC />
    </div>
  );
};

export default ComponentA;

Documentation

Note the documentation on the type and interface. We will do inline documenation for the type right above its declaration, and for the interface right above each prop.

Additionally, we should do inline documentation for all functions and important variables so that developers will better understand the code.

Exporting

For components and types that you wish to export in the library package, add the export keyword to the beginning of each declaration. Also add the export default to the bottom of the component. This way you are able to export both the component and its type together in index.ts.

Else, to just export a component to be used by another component within the library, the standard format for exporting a component applies.

Styling

Because we want to have this library accept different color modes easily from the parent application, we needed to modify how we handled styling. Sass and Material UI handle styling based on mode individually, making for a complicated process handling style changes. Additionally, Sass styling within the component library had no way of knowing the parent app's mode simply. So, for this library we will be using a combination of Styled Components and in-line styles.

When to use styled components? When you need to access theme variables.

When to use in-line styles? For simple css styling, without accessing theme.

Another key practice to do when styling components is to rely on the provided Theme's styling. That means, using their established color palette, typography, etc when styling a library component. This will allow the user developer to more easily match the style to whichever application they are using this library.

Styled Components

A styled component creates a custom component of the type you specify with its own styling. Documentation from material ui can be found here. Let's run through some example uses:

import { Button, Paper, styled } from '@mui/material';

// html styled component
const StyledDiv = styled('div')(({ theme }) => ({
    width: 400,
    height: '100%,
    border: `solid 1px ${theme.palette.mode=='light' ? 'red' : 'blue'}`
}));

// material ui styled component
const StyledPaper = styled(Paper)(({ theme }) => ({
    borderRadius: 3,
    padding: '10px 2px 5px',
    margin: 10,
    [theme.breakpoints.down('sm')]: {
        width: 'calc(100vw - 150px)',
    }
}));

// material ui styled component
const StyledButton = styled(Button)(({ theme }) => ({
    '&:hover': {
        cursor: 'pointer',
    }
    '&.MuiButton-outlined': {
        borderColor: theme.palette.primary.main,
    }
    '.custom-button-class': {
        color: 'red',
    }
}));

In-line Styles

When you need to apply styles that don't rely on theme, it is simplest to just use in-line styles. For example:

<div styles={{ backgroundColor: 'green' }} />

// or
import { CSSProperties } from 'react';

const divStyles: CSSProperties = {
    backgroundColor: 'green'
};

<div styles={divStyles} />