Playing with Schematics — Angular

If you want to skill up your team, use clean code practices and bump up productivity. Angular-cli can be your answer

Motivations:

A ·Simple concept, it’s easy to add delete or change the behavior of your templates and logic.

· Saving Time, Create angular features by one click (components, directives, services, modules, and just interface or some class you need in any creation)

· Safety ,if you try to create the same file again you got an error “creating file already exists”

How to install schematics:

npm install -g @angular-devkit/schematics-cli

#Create new blank project of schematics:

schematics blank — name=my-component

Compile schematics environments and get all useable packages

#Navigate to schematics the main folder and run the command:

1. `npm install`

2. `npm run build` or `npm run build -w`(-w: watch on all changes)

#For Debugging:

node — inspect-brk $(which schematics) .:myComponent — name=test

We got a set of rules under some directory:

figure: 1.1

collection.json:

figure: 1.2

schematics are sets of schematics-project that installed by the user (we set just some-schematics)

$schema: link to configure of what we can set on our schematics:

· factory: A folder or file path to the schematic factory

· Description: A description of the schematic

· extends An schematic override. It can be a local schematic or from another collection (in the format ‘collection:schematic’)

· schema: Location of the schema.json file of the schematic

· hidden: Whether or not this schematic should be listed by the tooling. This does not prevent the tooling to run this schematic, just removes its name from listSchematicNames().

· private: Whether or not this schematic can be called from an external schematic, or a tool. This implies hidden: true.

· Required: [“factory”, ”description”] — required field as you see in our file

index.ts file:

figure: 1.3

# Tree: contains sets of files that already exist in our project, schematics can get any Tree/s

Tree:

1. Rules: the function that gets a tree and returns “calculated” Tree.

2. “someFeature” function — is RuleFactory, as we can see someFunction use _options parameters, we got parameters from cmd arguments that user passed.

3. We can use an interface instead of any type and setting validation on these options (we will see example)

Another useful method on the tree:

tree.create('some-file', 'content in file'); - create file 
tree.rename('some-file', 'my-component'); - rename file name
tree.read('some-file') – read file
tree.delete('my-component'); - delete file

(somefeature/node_modules/@angulardevkit/schematics/src/tree/interface.d.ts)

So, after we add some logic now We will need to compile schematics environments and get all useable packages:

We now will need to navigate to the main folder of schematics and to run the command:

1. `npm install` — if you didn’t run it before.

2. `npm run build` or `npm run build -w`(-w : watch on all changes)

Finally ready to use our new schematics project:

Run test script and see that a file is created in the root:

schematics .:some-feature — name=my-code-example

That’s it! we added a new schematics project to our environments!!!!

It’s nice… but still not enough for us to change our working method.

We need to create our new environments, by adding some validation to our requires parameters that we will retrieve it from the user commands. Then we will need to link between the collections to the new schema.ts in Collection.json file:

{
  "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
  "schematics": {
    "some-feature": {
      "description": "A blank schematic.",
      "factory": "./some-feature/index#someFeature",
      "schema": "./feature/schema.json"
    }
  }
}

We add one new line:

“schema”: “./feature/schema.json”.

schema.json:

{
  "$schema": "http://json-schema.org/schema",
  "id":"feature",
  "title": "Feature Schema",
  "type": "object",
  "properties":{
    "name":{
      "type":"string",
      "description": "name of all template",
      "default": "app",
    },
    "appRoot":{
      "type":"string"
    },
    "path":{
      "type":"string",
      "default": "src/app/pages/",
      "description": "Path to create all templates"
    }
  }
}

In the Schema.json we can add arguments for commands, Then use it in our code to generate all our templates,

schema.ts:

export interface schemaOptions {
  name: string;
  appRoot: string;
  path: string;
  sourceDir: string;
}
}

That’s it, we are ready to use it in our FactoryRule methods by running index.ts

index.ts — all imports that we need in our project:

import {
  chain, Rule, apply, url, template,
  branchAndMerge, mergeWith,
} from '@angular-devkit/schematics';
import { schemaOptions } from './schema';
import { normalize } from '@angular-devkit/core';
import { classify, dasherize, camelize, underscore } from '@angular-devkit/core/src/utils/strings';
const stringUtils = {classify, dasherize, camelize, underscore };

We will use functions of @angular-devkit/core/src/utils/strings

· classify — change the text to the class convention (my-comp calculate to MyComp)

· dasherize — change the text to “-“ convention (MyComp calculate to my-comp)

· camelize — change the text to ‘camelCase’ convention (my-comp calculate to myComp)

· underscore — change the text to ‘_’convention (my-comp calculate to my_comp)

we create an object which contains all these function “stringUtils”.

Index.ts,

The logic section in the file: FactoryRule methods from (logic section)

export function someFeature(options: schemaOptions): Rule {
options.path = options.path ? normalize(options.path) : options.path;
  const templateSource = apply(url('./files'), [
template({
...stringUtils,
...options,
}),
]);
return chain([
branchAndMerge(chain([
mergeWith(templateSource),
])),
]);
}

After user run command with arguments of SchemaOption interface, one of the mandatory arguments is “path” we will use it to know where we need to generate the new code.

We are using method apply of schematics:

apply (source: Source, rules: Rule[]): Source;

· Source: save the place where all template exists

· rules: Rules[] : in which rules we use (according to explain behind rule contain parameters + methods and can be a custom rule too)

Merge between many rules to one script of the rule. And merge it together by chain and mergeWith.

Now , we are ready with the logic of schematics, we need to create our templates, run the magic of this logic and we will get our ready to use files.

Let’s speak on files structures:

figure: 1.4

We use arguments/parameters from schemaOptions.ts, together with schematic methods on files according to this rules:

· __name__ : name from generating a script (we got it from the user)

· @dasherize : method from the schematic package, we added all functions in index.ts

Now run:

`ng generate feature:feature — name= heroDetails — path=src/app/pages` ,

figure: 1.5

We use “dasherize” method on “name” argument: dasherize(“heroDetailes”) was changed to hero-details

This is our templates of __name@dasherize__.component :

import { Component, OnDestroy, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { <%=classify(name)%>Service } from '../<%=dasherize(name)%>.service';
@Component({
  selector: 'dn-<%=dasherize(name)%>',
  templateUrl: './<%=dasherize(name)%>.component.html',
  styleUrls: ['./<%=dasherize(name)%>.component.css'],
})
export class <%=classify(name)%>Component {
  <%=camelize(name)%>Fetch$: Observable<any>;
  constructor(private <%=camelize(name)%>Service: <%=classify(name)%>Service) {
  }
  private get<%=classify(name)%>() {
    this.<%=camelize(name)%>Fetch$ = this.<%=camelize(name)%>Service.get<%=classify(name)%>();
  }
}

We use functions of @angular-devkit/core/src/utils/strings methods

classify, “dasherize”, “underscore” and “camelize” to get our generated files, we run :

ng generate feature:feature — name= heroDetails — path=src/app/pages

We got this generated file:

import { Component, OnDestroy, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { HeroDetailsService } from '../hw-inventory.service';
@Component({
selector: 'hero-details,
templateUrl: './hero-details.component.html',
styleUrls: ['./hero-details.component.css'],
})
export class HeroDetailsComponent {
heroDetailsFetch$: Observable<any>;
constructor(private heroDetailsService: HeroDetailsService) {}
private getHeroDetails() {
this. heroDetailsFetch$ = this. heroDetailsService.getHeroDetails();
}
}

Conclusion:

Use schematics for better code, clearly and clean