What is this post about?
I’m a huge fan of the SpringBoot backend - Angular frontend combination. And since my UX/UI skills are very limited, I really enjoy working with angular material, a really nice library of common widgets implemented as Angular components using the material design.
I found myself doing the same thing over and over again:
- generate a table component, using the material table schematic
- create a service with Angular CLI connecting to a SpringBoot backend to retrieve objects, using the typical CRUD methods (getAll, save, delete, …)
- create an interface matching the json obtained from the backend
Then I suddenly realized:
If the material table schematic exists and I can generate components using the Angular CLI, shouldn’t it be possible to create my own schematic that executes the above steps?
Short answer: Yes, it’s possible! A quick search led me to the offical docs and a post on the offical Angular blog. Below I describe my journey how I tackled this challenge!
Install & Generate boilerplate
This one is easy, just follow along with the steps mentioned in the Angular blogpost.
Calling Material Table schematics and Chaining tasks
As mentioned in the introduction, I need multiple steps
- call the material table schematics
- generate the service and a dummy interface using a template -
and that happened to be the next section ‘Calling Another Schematic’. I had some trouble finding out what the names were of the collection and schematics, but I figured it out based on the Angular Material Schematics Guide and the source code available on github. For future reference:
- schematic collection:
@angular/material
- schematic name:
table
When calling externalSchematic
, there is also an options
parameter - this corresponds to the parameters you can provide using the CLI. Check the schema.json on github. At the bottom of the file, there is the required
entry, an array with the mandatory parameters. You should at least add these to the option parameter you provide to externalSchematic
.
issue - cannot find module errors
When trying to run my schematic, I ended up having this kind of error:
Cannot find module '@schematics/angular/utility/change'
Require stack:
- /Users/pieterjd/Documents/table-service/node_modules/@angular/cdk/schematics/utils/ast.js
- /Users/pieterjd/Documents/table-service/node_modules/@angular/cdk/schematics/utils/index.js
- /Users/pieterjd/Documents/table-service/node_modules/@angular/cdk/schematics/index.js
- /Users/pieterjd/Documents/table-service/node_modules/@angular/material/schematics/ng-generate/table/index.js
- /Users/pieterjd/Documents/test-project/node_modules/@angular-devkit/schematics/tools/export-ref.js
- /Users/pieterjd/Documents/test-project/node_modules/@angular-devkit/schematics/tools/index.js
- /Users/pieterjd/Documents/test-project/node_modules/@angular/cli/utilities/json-schema.js
- /Users/pieterjd/Documents/test-project/node_modules/@angular/cli/models/command-runner.js
- /Users/pieterjd/Documents/test-project/node_modules/@angular/cli/lib/cli/index.js
It took some time to figure out, but my project was missing dependencies on @angular/cdk
, @angular/material
, @angular-devkit
, and so on. After fixing this with the necessary npm install --save
, my package.json file now contained all these dependencies:
{
//skip other stuff
"dependencies":{
"@angular/animations": "^11.0.5",
"@angular/cdk": "^11.0.3",
"@angular/cli": "^11.0.5",
"@angular/common": "^11.0.5",
"@angular/core": "^11.0.5",
"@angular/forms": "^11.0.5",
"@angular/material": "^11.0.3",
"@angular/platform-browser": "^11.0.5",
"typescript": "~4.0.2",
"zone.js": "^0.11.3"
}}
Defining input using schema.json
So far all component and service names were hardcoded. Off course you would like to define them on the CLI. That’s where the schema.json
file comes in - cf official docs.
In this case, only 2 prompts are required:
- Entity name, for instance
user
- Backend url, for instance
/api/v1/users
These two inputs then result in a userlist
component (generated with the material table schematics), and a UserService
with a HttpCLient
instance connecting to /api/v1/users
to get, post, … user instances. Both of them are mandatory.
Generating the service with templates
Generating the boilerplate code is done with templates - not sure, but I think the syntax is based on EJS.
A bit complicated, but the name of the template file needs to be named in a specific way. In my case, the template filename is __name@dasherize__.service.ts.template
. Whatever is between __
is interpolated:
name
is a variable@dasherize
is a function applied to the variable before the @. For instanceinnerHTML
is transformed intoinner-html
So if name
has the value actionHero
, a file called action-hero.service.ts
is generated.
It’s rather easy to define this - check following snippet:
const templateSource = apply(url('./files'), [
applyTemplates({
classify: strings.classify,
dasherize: strings.dasherize,
camelize: strings.camelize,
name: name,
backendUrl: _options.backendUrl
}),
move(normalize(join(_options.path,_options.servicePath) as string))
]);
- All templates are located in the
files
subfolder - Next are the rules:
- an object with functions (
classify
,dasherize
,camelize
) and variables (name
,backendUrl
) to be used in the template are provided toapplyTemplates
- when generation is completed, move the resulting file to some path
- an object with functions (
Issue: Cannot read property ‘match’ of undefined
make sure the normalize
function is imported from path
, not @angular-devkit/core
!
Service Template
Writing the service template is now straightforward - note the mix of the code of an actual service and the templating variables and functions as mentioned in a previous section.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
//dummy <%= classify(name) %> interface so everything compiles
export interface <%= classify(name) %>{}
@Injectable({
providedIn: 'root'
})
export class <%= classify(name) %>Service {
private URL: string = "<%= backendUrl %>";
constructor(private http: HttpClient) { }
public getAll(): Observable<<%= classify(name) %>[]> {
return this.http.get<<%= classify(name) %>[]>(this.URL);
}
public save<%= classify(name) %>(<%= camelize(name) %>: <%= classify(name) %>): void {
this.http.post(this.URL, <%= camelize(name) %>);
}
}
Conclusion
That’s it - I now have a working schematic that will help me generate boring, tedious boilerplate code!
- Points for improvement:
- I still need to add manually the new service to the
providers
section of theapp
module - this should be possible using schematics as well - automatically add a route pointing to the generated component containing a material table
- add an optional Snackbar, displaying a success or error message
- I still need to add manually the new service to the
- Good to know: if you don’t use a parameter of the function, you get a nasty ‘not read’ warning. Prefix the variable with _ and it disappears 👍