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:

  1. generate a table component, using the material table schematic
  2. create a service with Angular CLI connecting to a SpringBoot backend to retrieve objects, using the typical CRUD methods (getAll, save, delete, …)
  3. 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

  1. call the material table schematics
  2. 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 UserServicewith 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 instance innerHTMLis transformed into inner-html

So if name has the value actionHero, a file called action-hero.service.tsis 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 filessubfolder
  • Next are the rules:
    • an object with functions (classify, dasherize, camelize) and variables (name, backendUrl) to be used in the template are provided to applyTemplates
    • when generation is completed, move the resulting file to some path

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 the app 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
  • 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 👍