I love automation, and I hate repeating mindlessly. For data-centric applications, I do not want to handcraft master-detail for every entity repeatedly. So I spent some time designing a module with reusable components that define the look and handle most of the CRUD behaviours. The aim is that for each entity CRUD, I only need to do some configurations specific to the entity.
As a result, I have a few components composed together in four files for each entity. Nrwl/Nx has bundled a set of schematics to generate code, such as generating lib and components. Nx spoils me, and I do not want to copy and paste and change names etc. I want to have it generated automatically by a simple command like this:
nx workspace-schematic crud entity-name lib-name
There aren't many official documents for making a custom schematic. There isn't one for making React schematic. Guess what? The source code for the in-house schematics is the best document we could hope for.
I share my crud
schematic code here for the convenience of like-minded developers who wants to make their own schematics.
The Nx project has a tools/generators folder. This is where our schematics live.
Nx comes with a schematic to generate custom schematics:
nx g workspace-schematic your-schematic
. It will give us a skeleton to start with. We will have two files:index.ts
andschema.json
1.index.ts
is where we write our schematic code. Below is mine.
It is a straightforward one. It takes an entity name and a lib name and creates a folder under the specified lib with four files, and update the lib barrel index.ts
import { | |
applyChangesToString, | |
formatFiles, | |
generateFiles, | |
joinPathFragments, | |
names, | |
readProjectConfiguration, | |
Tree, | |
} from '@nrwl/devkit'; | |
import * as path from 'path'; | |
import { addImport } from '@nrwl/react/src/utils/ast-utils'; | |
import ts = require('typescript'); | |
interface Schema { | |
name: string; | |
project: string; | |
} | |
function getSourceRoot(tree: Tree, options: Schema) { | |
const sourceRoot = readProjectConfiguration(tree, options.project).sourceRoot; | |
if (!sourceRoot) { | |
throw new Error('Project source root not found'); | |
} | |
return sourceRoot; | |
} | |
export default async function (tree: Tree, options: Schema) { | |
const sourceRoot = getSourceRoot(tree, options); | |
const name = names(options.name); | |
generateFiles( | |
tree, | |
path.join(__dirname, 'files'), | |
path.join(`${sourceRoot}/lib`, name.propertyName), | |
name | |
); | |
addExportsToBarrel(tree, options); | |
await formatFiles(tree); | |
} | |
function addExportsToBarrel(tree: Tree, options: Schema) { | |
const sourceRoot = getSourceRoot(tree, options); | |
const name = names(options.name); | |
const indexFilePath = joinPathFragments(`${sourceRoot}/lib`, 'index.ts'); | |
const buffer = tree.read(indexFilePath); | |
if (!!buffer) { | |
const indexSource = buffer.toString('utf-8'); | |
const indexSourceFile = ts.createSourceFile( | |
indexFilePath, | |
indexSource, | |
ts.ScriptTarget.Latest, | |
true | |
); | |
const changes = applyChangesToString( | |
indexSource, | |
addImport(indexSourceFile, `export * from './${name.propertyName}';`) | |
); | |
tree.write(indexFilePath, changes); | |
} | |
} |
How it works:
Nx provides a
names
function. It takes a stringname
which will be passed as part of the required options in our command(in my case its the entity name) and returns an object with useful names such asclassName
andpropertyName
. For example, if my entity name is "purchase-order",className
would be "PurchaseOrder" andpropertyName
would be "purchaseOrder".generateFiles
take files in the sub-folder files and generate code from them. Notice the last parameter. It is the substitutions we can provide for Nx to replace markups in our file names and contents. I providedname
. Its shape looks like this:
{className: string, propertyName: string, constantName: string, fileName: string}
I wanted my CRUD file names to start with entity name, so I name my template files like this:
__className__Form.tsx
. When Nx sees__lassName__
, it looks forclassName
property in my providedname
object.-
Same goes with the contents. The markup looks like this:
<%= className%>
. One of my templates looks like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersimport { EpicDetailContainer, MasterDetailContent, MasterDetailTitleBar, useMasterDetailStates, } from '@epic/core/master-detail'; import { <%= className %>List } from './<%= className %>List'; import React, { PropsWithChildren } from 'react'; import { <%= className %>Detail } from './<%= className %>Detail'; export function <%= className %>Master({ dialogMode = false, title }: PropsWithChildren<{ dialogMode?: boolean; title: string;}>) { const { listProps, detailProps, addNewProps, detailContainerProps, searchBoxProps, } = useMasterDetailStates(dialogMode); return ( <> <MasterDetailTitleBar title={title} addNewProps={addNewProps} searchBoxProps={searchBoxProps} /> <MasterDetailContent> <<%= className %>List {...listProps}/> <EpicDetailContainer {...detailContainerProps}> <<%= className %>Detail {...detailProps}/> </EpicDetailContainer> </MasterDetailContent> </> ); }
One last thing is that if you have ts
files to generate in your files
folder, you will need to add "exclude": ["**/files/**/*.ts]
in your tsconfig.tools.json
file. It prevents your template TypeScript files gets compiled.
With the above, I can scaffold CRUD very quickly.
Creating schematics is easier than you think.
Top comments (0)