wp-sequoia-admin-interface
v1.0.2
Published
Sequoia Admin Interface
Downloads
1
Readme
Sequoia Custom JavaScript and Blocks - Gutenberg Editor
General Information
| Resource | Version |
| --------- | ---------- |
| Node.js | v12.16.0
|
| Npm | ^v6.0.0
|
| React | v16.13.1
|
| Gutenberg | v6.0.0
|
Workflow
Create an Admin Page
For the creation of an admin page, please follow the next steps:
Add the route of the menu that you will be working at, to the
admin.php
.- Use the same callback function in other menu entries.
Add the configuration to the
menus.ts
Add the routes that you are going to need on your admin into the
routes.ts
{ path: '/the-route-path', title: 'submenutitle', component: ReactComponent useStore: BooleanIndicatingIfThisUsesStoreInsteadOfState }
Create the Components under
/components/
directory.
Create a Gutenberg Block
Create the following folder structure at
/components/Blocks/{type-of-block}
- The
{type-of-block}
can be according to what is going to do the Component or which post type will use it - Folder Structure:
📁 NameOfComponent ├─ 📄 attributes.ts ├─ 📄 edit.tsx └─ 📄 index.ts
NameOfComponent/
: The folder name should always be CamelCase formatted (first letter capitalized).edit.tsx
: File contains the Component that we want to show on the Editorindex.ts
: Contains all the configuration of the Blockattributes.ts
:
- The
Once the Component is created, add it to the
BlockManager.js
Development
Building and Watching
npm start
: Creates a development environment to compile and watch TypeScript and Sass files.- Related Documentation:
wp-scripts start
command
- Related Documentation:
npm run build
: Generates the final output in JavaScript and CSS format.- Related Documentation:
wp-scripts build
command
- Related Documentation:
npm run build:devel
: Generates the extended version of all files with the development configuration.
Formatters
These scripts use Prettier to beautify the code. Beware that Prettier only formats code, if you want to linter the code please read the Linters section.
If you want to exclude files from formatting, add the respective patterns to the .prettierignore
file (it uses gitignore syntax). Read more about this here.
npm run ts:format
: Prettifies the TypeScript code.npm run js:format
: Prettifies the JavaScript code (mainly workflow configuration files).npm run json:format
: Prettifies all JSON files.
Linters
npm run ts:lint
&npm run js:lint
: Print the linter errors for TypeScript and JavaScript files respectively.npm run ts:lint:fix
&npm run js:lint:fix
: Fix the linter errors (only those that are automatically fixable) and print the rest.npm run sass:lint
: Print the linter errors for Scss files.npm run sass:lint:fix
: Fix the linter errors (only those that are automatically fixable) and print the rest.
Testing and Other
npm test
: Runs thetest:e2e
andtest:unit
scripts.npm run test:unit
: Executes the unit tests.- Related Documentation:
wp-scripts test-unit-js
command
- Related Documentation:
npm run check-engines
: Check the server environment and check that the Node.js version is updated.
Naming Conventions
// TODO:
WordPress:
- Naming conventions
- ESLint Rules
- Best Practices
Justia naming conventions:
- Naming conventions
- ESLint Rules
- Best Practices
New Components
New components can be organized in two ways:
Folder (Recommended)
Use the following folder structure when the component has multiple sub-modules and styles (also in sub-modules):
📁 NewComponent
├─ 📄 index.tsx
├─ 📄 index.test.tsx
├─ 📄 helper-function.ts
├─ 📄 index.module.scss
└─ 📁 styles
└─ 📄 extra-styles.scss
NewComponent/
: The folder name should always be CamelCase formatted (first letter capitalized).index.tsx
:- Main entry file. Webpack will resolve to this file when searching for the component when imported.
- File name must be
index
when nested in a folder. - The extension must be
.tsx
if the component uses React. Otherwise, use.ts
.
index.test.tsx
: Read more about tests here.helper-functions.ts
:- This represents one of many possible modular files. You can add as many as you need.
- File name should always be kebab-case formatted.
- The extension must be
.tsx
if the any of the helper functions uses React. Otherwise, use.ts
.
index.module.scss
:- Main styles used directly in the React component.
- This file should always match the name of the main entry file followed by the extension
.module.scss
. The extension is required for Webpack to identify and properly set the CSS classes.
styles/
:- Create this folder if you need to modularize the styles within
index.module.scss
. - As shown above, the files contained in this folder should be named in kebab-case format and followed only by the extension
.scss
. - You can create as many sub-modular styles as you want and import them in the main file using the
@import
directive.
- Create this folder if you need to modularize the styles within
Files
This method is recommended for short components with short styles.
📄 NewComponent.tsx
📄 NewComponent.module.scss
NewComponent.tsx
:- The file name should always be CamelCase formatted (first letter capitalized).
- The extension must be
.tsx
if the component uses React. Otherwise, use.ts
.
NewComponent.module.scss
:- Main styles used directly in the React component.
- This file should always match the name of the main entry file followed by the extension
.module.scss
.
Styles
There is no need to manually add CSS vendor prefixes (-webkit-
, -moz-
, -ms-
, -o-
) since Webpack internally uses PostCSS and Autoprefixer to automatically add them based on the specified browser support in the package.json
.
TypeScript
These are some of the practices that this project uses. To learn more about do's and don'ts please read this.
Don't Use any
Don't use any
as a type unless you are in the process of migrating a JavaScript project to TypeScript. The compiler effectively treats any
as "please turn off type checking for this thing". It is similar to putting an @ts-ignore
comment around every usage of the variable. This can be very helpful when you are first migrating a JavaScript project to TypeScript as you can set the type for stuff you haven't migrated yet as any
, but in a full TypeScript project like this one you are disabling type checking for any parts of your program that use it.
In cases where you don't know what type you want to accept, or when you want to accept anything because you will be blindly passing it through without interacting with it, you can use unknown
.
type
Over interface
This is mainly for consistency.
All types should be in CamelCase format (first letter capitalized).
Object Literals / Functions
type Point = {
x: number;
y: number;
};
type SetPoint = (x: number, y: number) => void;
Other Types
Unlike an interface, the type alias can also be used for other types such as primitives, unions, and tuples.
// Primitive
type Name = string;
// Tuple
type Data = [number, string];
// Object Literal
type PartialPointX = { x: number; };
type PartialPointY = { y: number; };
// Union
type PartialPoint = PartialPointX | PartialPointY;
// Intersection
type Points = PartialPointX & PartialPointY;
Implements
A class can implement an interface or type alias, both in the same exact way. Note however that a class and interface are considered static blueprints. Therefore, they can not implement / extend a type alias that names a union type.
/* OK */
type Point = {
x: number;
y: number;
};
class SomePoint implements Point {
x = 1;
y = 2;
}
/* WRONG */
type PartialPoint = { x: number; } | { y: number; };
class SomePartialPoint implements PartialPoint { // Can not implement a union type.
x = 1;
y = 2;
}
Declaration Merging
In this case you will need interfaces since merging is not supported in type aliases.
You can use the declaration merging feature of the interface
for adding new properties and methods to an already declared interface
. This is useful for the ambient type declarations of third party libraries. When some declarations are missing for a third party library, you can declare the interface again with the same name and add new properties and methods.
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface Window {
foo: string;
bar(): void;
}
Since the linter disallows the usage of interfaces. You may need to add the respective disabling comment as shown above.
Read-only Function Parameters
Mutating function arguments can lead to confusing, hard to debug behavior. Whilst it's easy to implicitly remember to not modify function arguments, explicitly typing arguments as readonly
provides clear contract to consumers. This contract makes it easier for a consumer to reason about if a function has side-effects.
// `Readonly` utility
type FunctionProps = Readonly{
label: string;
type: number;
x: number;
y: number;
};
// `readonly` keyword
type FunctionProps = {
readonly label: string;
}
// Arrays
type Data = readonly [string, number];
type FunctionProps2 = Readonly{
labels: readonly string[];
types: ReadonlyArray<number>;
x: number;
y: number;
};
For more examples for this practice, please read this.
React
Function Components
React Function Components (also known as React Functional Components) are the status quo of writing modern React applications. In the past, there have been various React Component Types, but with the introduction of React Hooks it's possible to write your entire application with just functions as React components.
Since React Hooks have been introduced in React, Function Components are not anymore behind Class Components feature-wise. You can have state, side-effects and lifecycle methods in React Function Components now.
Function Components are more lightweight than Class Components and offer a sophisticated API for reusable yet encapsulated logic with React Hooks.
For the sake of comparison, check out the implementation of the following Class Component vs Functional Component:
/* Class Component */
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: localStorage.getItem('myValueInLocalStorage') || ''
};
}
componentDidUpdate() {
localStorage.setItem('myValueInLocalStorage', this.state.value);
}
onChange = (event) => {
this.setState({ value: event.target.value });
};
render() {
return (
<div>
<h1>Hello React ES6 Class Component!</h1>
<input value={this.state.value} type="text" onChange={this.onChange} />
<p>{this.state.value}</p>
</div>
);
}
}
/* Function Component */
const App = () => {
const [value, setValue] = React.useState(localStorage.getItem('myValueInLocalStorage') || '');
React.useEffect(() => {
localStorage.setItem('myValueInLocalStorage', value);
}, [value]);
const onChange = (event) => setValue(event.target.value);
return (
<div>
<h1>Hello React Function Component!</h1>
<input value={value} type="text" onChange={onChange} />
<p>{value}</p>
</div>
);
};
To learn more about React Function Components, please continue reading this article.
WordPress Configuration
URL: https://github.com/WordPress/gutenberg/tree/master/packages/scripts
Webpack Configuration
The final JavaScript files will be generated inside resources/assets/js
.
The final CSS files will be generated inside resources/assets/css
Tests
Sequoia uses Jest as its test runner.
Filename Conventions
Jest will look for test files with any of the following popular naming conventions:
- Files with
.test.ts
or.test.tsx
suffixes (recommened). - Files with
.spec.ts
or.spec.tsx
suffixes. - Files with
.ts
or.tsx
suffixes in__tests__
folders.
The .test.ts
/.spec.ts
files (or the __tests__
folders) can be located at any depth under the app
top level folder.
We recommend to put the test files (or __tests__
folders) next to the code they are testing so that relative imports appear shorter. For example, if App.test.tsx
and App.tsx
are in the same folder, the test only needs to import App from './App'
instead of a long relative path. Collocation also helps find tests more quickly in larger projects.
Writing Tests
To create tests, add test()
(or it()
) blocks with the name of the test and its code. You may optionally wrap them in describe()
blocks for logical grouping but this is neither required nor recommended.
Jest provides a built-in expect()
global function for making assertions. A basic test could look like this:
import sum from './sum';
test('Sums numbers', () => {
expect(sum(1, 2)).toEqual(3);
expect(sum(2, 2)).toEqual(4);
});
All expect()
matchers supported by Jest are extensively documented here.
You can also use jest.fn()
and expect(fn).toBeCalled()
to create "spies" or mock functions.
Sequoia uses react-testing-library
which is a library for testing React components in a way that resembles the way the components are used by end users. It is well suited for unit, integration, and end-to-end testing of React components and applications. It works more directly with DOM nodes, and therefore it's recommended to use with jest-dom
for improved assertions.
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('Renders welcome message', () => {
render(<App />);
expect(screen.getByText('Hello World!')).toBeInTheDocument();
});
Learn more about the utilities provided by react-testing-library
to facilitate testing asynchronous interactions as well as selecting form elements from the react-testing-library
documentation and examples.
Sequoia also includes @testing-library/user-event
when you need to test user events in the browser. @testing-library/user-event
tries to simulate the real events that would happen in the browser as the user interacts with it.
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('click', () => {
render(
<div>
<label htmlFor="checkbox">Check</label>
<input id="checkbox" type="checkbox" />
</div>
);
userEvent.click(screen.getByText('Check'));
expect(screen.getByLabelText('Check')).toHaveAttribute('checked', true);
});