Escrito por Adrian Lemes Caetano,

20 minutos de leitura

Boost productivity generating Angular Forms with Schematics

Compartilhe este post:
Angular Schematics

It has been a while since I wanted to start blogging about tech and I finally got the time and courage to write this post. I’m happy that my first tech post will be about a topic I’ve been spending a good amount of time and effort studying: Angular Schematics. My intention with this post is not to teach how to use Angular Schematic, yes I will give some tips and pass some links of good material to start, but my real intention is to share my experience working with this powerful tool and how you can use it to boost your product development. You can find the schematic I’ve developed here: https://github.com/ilegra/form-schematic.

As a frontend architect at ilegra (the company I work for), I’ve learned that I have two clients with different needs to work with it. The first one, the product user, where I have to worry about how to improve the experience, application performance, good scalability to always deliver new features and improve time to market. On the other hand, I have engineers, who I must be able to scale as well. No engineer likes to code in a bad architecture or in the mud — in a project with a lot of technical debts.

A good frontend architecture to provide scalability is composable for good design code, good decisions of tools, a well-defined structure, and proper isolation (something hard to do on browser but possible with micro frontends), and these topics would be a good fit to another post :). But today I will talk about something that almost engineers I know hate: manual and repetitive tasks.

Almost all frontend engineers I know, like to be challenged constantly, to think in good solutions and properly design, to work on things that matter, and deliver value. Although and unfortunately, they’re constantly in situations working with repetitive tasks.

One of my jobs is always looking for toil and try to eliminate it. Toil is defined by Google as “…the kind of work tied to running a production service that tends to be manual, repetitive, automatable, tactical, devoid of enduring value, and that scales linearly as a service grows”. We tend to think in toil a lot on DevOps and cloud, but in my opinion, we have a lack of this mindset on frontend development. I don’t know exactly the reason, but I think that maybe it is because we tend to think in frontend as an art with a lot of variation, but the truth is, we do a lot of repetitive and automatable tasks on frontend too.

Angular Schematic

I discovered Angular Schematic a few months ago when I was in a task to search for tools to automate repetitive tasks on the frontend. My intuit was not replace the engineer with a magic tool to generate code and deliver the product, but give him a boost and instead of he started from scratch, he had a good start point, something between 50% and 70% of his job.

Angular Schematic is a powerful template-based code generator used to maintain any application and created by the Angular team. Despite the Angular name, it can be used in any software development (React, Vue, NodeJS, even Java) but it really shines on Angular projects. A really good thing about Angular Schematic is the way it manages the filesystem. When you make changes, it’s not directly on the filesystem, Angular Schematic has a virtual tree that represents your filesystem and applies those changes only when everything is working. This way it avoids errors and unnecessary rollbacks on incomplete changes, it’s similar to Virtual DOM concept on frameworks like React or Vue.

Angular Schematic can be used to a lot of tasks, I mapped a few so far:

  • CRUD views (List, delete, forms to create and edit something)
  • Forms generators
  • Find and close observables (See here: https://medium.com/angular-in-depth/the-best-way-to-unsubscribe-rxjs-observable-in-the-angular-applications-d8f9aa42f6a0)
  • Update dependencies versions and breaking changes
  • Create a project from scratch (with helpers methods, Jest, E2E, Bootstrap/Material Design, pipeline, and other configurations)
  • Add your library/module in a project in the case where is necessary to update multiple files to make it work
  • API services with HTTP CRUD methods (getAll, getById, delete, update…)

We can do a lot with Angular Schematic, the only limitation is the time and effort we have to invest and the imagination to think about what we will create. The only trade-off I saw so far is the lack of good documentation, I needed to investigate other open-source projects and articles to learn how the things are done, for me it was not a problem, but it is a trade-off.

Form Schematic

Ok I found Angular Schematic, but what code I want to generate? ilegra as other companies has a lot of projects where it is necessary to create long forms with a lot of fields and rules. Most of the time is a repetitive task and not demand so much engineer thinking.

Angular has a really powerful resource to create forms called Reactive Forms, which works with immutability and allows us to react to anything in the form — like when the form is valid, or field change, or the entire form change for example. We can also create special validators only with functions without creating complicated directives. The problem of these cool features is the cost, we have to create a boilerplate with a bunch of code, most of the time is an annoying task.

And with that in mind and with ilegra support, I’ve developed a POC (proof of concept) to generate a forms view using bootstrap and reading a JSON with the definition of what we want to generate. The idea was to enable a business or UX team to pass this JSON to the engineer’s team to help them. But even if the engineer needs to create the JSON definition by himself, it is a win too. The result of what I’ve made can be check above:

And JSON form definition:

{
	"type": "object",
	"title": "Personal Informations",
	"properties": {
		"name": {
			"title": "Name",
			"type": "string",
			"maxLength": 80
		},
		"password": {
			"title": "Password",
			"type": "string",
			"inputType": "password",
			"minLength": 6
		},
		"email": {
			"title": "Email",
			"type": "string",
			"pattern": "^\\S+@\\S+$"
		},
		"age": {
			"title": "Age",
			"type": "number",
			"min": 0,
			"max": 120
		},
		"nationality": {
			"title": "Nationality",
			"type": "string"
		},
		"cpf": {
			"title": "CPF",
			"type": "string",
			"mask": "000.000.000-00"
		},
		"phoneNumber": {
			"title": "Phone Number",
			"type": "string",
			"mask": "(00) 00000-0000",
			"inputType": "tel"
		}
	},
	"required": ["name", "cpf", "password", "email", "age"]
}
view rawpersonal-information.json hosted with ❤ by GitHub

 

PS: I found this JSON definition form on a website (I will let the link below) and I used it because it was just a POC. But, to be honest, the required part was something that bothered me, and probably you too, and is something I want to change in the future. Maybe creating a validation property inside each field key, for example:

 

{ 
    "age": { 
        "title": "Age", 
        "type": "number", 
        "min": 0, 
        "max": 120,
        "validations": {
            "required": "Age must be filled",
            "min":"Age should not be less than 0",
            "max":"Age should not be more than 120"
        }
    }
},

validations-example.json hosted with ❤ by GitHub

 

The POC

In the first version of this project, we thought about creating a project with minimum features to prove an idea. The features we decide stands the following:

  • Generate forms only considering inputs
  • Input types -text, password, email…
  • Add masks to special fields like phone numbers using Ngx-mask (https://www.npmjs.com/package/ngx-mask)
  • Forms styled using bootstrap
  • Showing errors on inputs required
  • Simple inputs validations: max, min, maxLength, minLength and pattern with regex
  • Showing label to each input (properties -> title)

We still have a lot of work to do and you can check in the forms repository on Github. But with this scope, we had a good result and a schematic with a lot of possibilities and the great part: it’s easy to use and it updates automatically the module (with declarations and adding ReactiveFormsModule and FormsModule) where we add the form component, just like ng-cli.

Inside the project I add the library to generate form schema and I created an Angular example project to add and test our schematic, you can find instructions on how to run in the Readme project.

The file above shows how I read templates and generate the form code:

export function forms(_options: OptionsFormSchema): Rule {
	return (tree: Tree, _context: SchematicContext) => {
		// Log
		// context.logger.info('Info message');
		// context.logger.warn('Warn message');
		// context.logger.error('Error message');
		const workspaceConfig = tree.read('/angular.json');

		if (!workspaceConfig) {
			throw new NotValidAngularWorkspace();
		}

		const workspaceContent = workspaceConfig.toString();

		const workspace: workspace.WorkspaceSchema = JSON.parse(
			workspaceContent
		);

		if (!_options.project) {
			_options.project = workspace.defaultProject || '';
		}

		const projectName = _options.project;
		const project = workspace.projects[projectName];

		const jsonFormConfig = tree.read(`${_options.config}`);

		if (!jsonFormConfig) {
			throw new FormJsonNotFoundError();
		}

		const jsonFormContent = jsonFormConfig.toString();

		const formJsonObj = new FormJson(JSON.parse(jsonFormContent));

		const projectType =
			project.projectType === 'application'
				? ProjetTypeEnum.APP
				: ProjetTypeEnum.LIB;

		if (!_options.path) {
			_options.path = `${project.sourceRoot}/${projectType}`;
		}
		const parsedOptions = parseName(_options.path, _options.name);
		_options = { ..._options, ...parsedOptions };

		const templateSource = apply(url('./templates/forms'), [
			renameTemplateFiles(),
			template({
				...strings,
				..._options,
				formJsonObj
			}),
			move(normalize((_options.path + '/' + _options.name) as string))
		]);

		return chain([
			branchAndMerge(chain([mergeWith(templateSource)])),
			addTreeModulesToModule(_options),
			addDeclarationsToModule(_options)
		])(tree, _context);
	};
}
forms-index.ts hosted with ❤ by GitHub

Line 7–17: This block is responsible for reading the project workspace where we want to generate our forms. If it’s not a valid Angular workspace will throw an error.

Line 17–34: This block we will get the config JSON file and parse into a model with all our rules to generate forms. This model is embedded in templates to be used.

Line 34–55: We pass our FormModel created from JSON file into _options object to be used in our templates files

Line 55–61: We add our files into the tree and call helpers methods to add declaration array in the module where we want to put our new form-component.

And in the folder templates/forms we have 5 files.

__name@dasherize__-form.component.__style__.template — this one creates the style file, where the __style__ will be replaced by the extension the user pass. By default, it takes SCSS extension(a really good improvement to the future is to detect what style the workspace use automatically)

__name@dasherize__-form.component.html.template — This is the HTML file, here we use the formJsonObj.properties to iterate and generate all <inputs> fields

__name@dasherize__-form.component.spec.ts.template — This file we create our Spec file, we add the necessary modules and set up the TestBed to pass a minimum test — a smoke test. We want to add more tests in the future.

__name@dasherize__-form.component.ts.template — This is the component file with our formBuilder and forms creations, here we will add validators and any useful function to be used in the template.

__name@dasherize__.model.ts.template — And this is our form model when we call this.form.value. We generate this model based on the properties of formJsonObj.

I will let you some links to learn what means the special alias on the file name and even the syntax inside templates.

And what about the result generated? Ok, I will run the following command inside example/angular-template folder:

ng g f personalInformation ./jsons/personal-information.json

Where “f” is an alias to forms, just like ng-cli do. And the second param is our config JSON file with our form definitions I’ve shown earlier. My terminal output was:

And the code result was:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { NgxMaskModule } from 'ngx-mask';
import { AppComponent } from './app.component';
import { SharedModule } from './shared/shared.module';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { PersonalInformationFormComponent } from './personal-information/personal-information-form.component';

@NgModule({
	declarations: [AppComponent, PersonalInformationFormComponent],
	imports: [
		BrowserModule,
		SharedModule,
		NgxMaskModule.forRoot({
			validation: true
		}),
		ReactiveFormsModule,
		FormsModule,
	],
	providers: [],
	bootstrap: [AppComponent]
})
export class AppModule {}
after-app.module.ts hosted with ❤ by GitHub

 

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { NgxMaskModule } from 'ngx-mask';
import { AppComponent } from './app.component';
import { SharedModule } from './shared/shared.module';

@NgModule({
	declarations: [AppComponent],
	imports: [
		BrowserModule,
		SharedModule,
		NgxMaskModule.forRoot({
			validation: true
		}),
	],
	providers: [],
	bootstrap: [AppComponent]
})
export class AppModule {}
before-app.module.ts hosted with ❤ by GitHub

 

<app-section title="Personal Informations">
  <form
    class="form"
    [formGroup]="this.personalInformationForm"
    name="personal-information-form"
  >
    <div class="row">
      <div class="col-6">
        <app-custom-input label="Name">
          <input
            formControlName="name"
            name="name"
            type="text"
            class="form-control"
            id="input-name" 
            maxLength="80"
            appInputRef
          />
        </app-custom-input>
      </div>
      
      <div class="col-6">
        <app-custom-input label="Password">
          <input
            formControlName="password"
            name="password"
            type="password"
            class="form-control"
            id="input-password" 
            minLength="6"
            appInputRef
          />
        </app-custom-input>
      </div>
      
      <div class="col-6">
        <app-custom-input label="Email">
          <input
            formControlName="email"
            name="email"
            type="text"
            class="form-control"
            id="input-email" 
            pattern="^\S+@\S+$"
            appInputRef
          />
        </app-custom-input>
      </div>
      
      <div class="col-6">
        <app-custom-input label="Age">
          <input
            formControlName="age"
            name="age"
            type="number"
            class="form-control"
            id="input-age" 
            min="0" 
            max="120"
            appInputRef
          />
        </app-custom-input>
      </div>
      
      <div class="col-6">
        <app-custom-input label="Nationality">
          <input
            formControlName="nationality"
            name="nationality"
            type="text"
            class="form-control"
            id="input-nationality"
            appInputRef
          />
        </app-custom-input>
      </div>
      
      <div class="col-6">
        <app-custom-input label="CPF">
          <input
            formControlName="cpf"
            name="cpf"
            type="text"
            class="form-control"
            id="input-cpf" 
            mask="000.000.000-00"
            appInputRef
          />
        </app-custom-input>
      </div>
      
      <div class="col-6">
        <app-custom-input label="Phone Number">
          <input
            formControlName="phoneNumber"
            name="phone-number"
            type="tel"
            class="form-control"
            id="input-phone-number" 
            mask="(00) 00000-0000"
            appInputRef
          />
        </app-custom-input>
      </div>
      
    </div>
    <div class="row">
      <div class="col-6 d-flex justify-content-start w-25 p-3">
        <button
          type="reset"
          [disabled]="!this.personalInformationForm.touched"
          (click)="onReset()"
          class="btn btn-danger"
          name="reset"
        >
          Reset Form
        </button>
      </div>
      <div class="col-6 d-flex justify-content-end w-25 p-3">
        <button
          type="submit"
          [disabled]="!this.personalInformationForm.valid"
          (click)="onSubmit()"
          name="submit"
          class="btn btn-primary"
        >
          Submit
        </button>
      </div>
    </div>
  </form>
</app-section>

personal-information-form.component.html hosted with ❤ by GitHub

 

// blank file
personal-information-form.component.scss hosted with ❤ by GitHub

 

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { PersonalInformationFormComponent } from './personal-information-form.component';

describe('PersonalInformationFormComponent', () => {
  let component: PersonalInformationFormComponent;
  let fixture: ComponentFixture<PersonalInformationFormComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ PersonalInformationFormComponent ],
      schemas: [CUSTOM_ELEMENTS_SCHEMA],
      imports: [
        NoopAnimationsModule,
        ReactiveFormsModule,
        FormsModule
      ]
    }).compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(PersonalInformationFormComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should compile', () => {
    expect(component).toBeTruthy();
  });
});
personal-information-form.component.spec.ts hosted with ❤ by GitHub

 

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators  } from '@angular/forms';
import { PersonalInformationInterface } from './personal-information.model';

@Component({
  selector: 'app-personal-information-form',
  templateUrl: './personal-information-form.component.html',
  styleUrls: ['./personal-information-form.component.scss']
})
export class PersonalInformationFormComponent implements OnInit {
  public personalInformationForm: FormGroup;

  constructor(private fb: FormBuilder) { }

  ngOnInit() {
     this.personalInformationForm = this.initializeForm();
  }

  private initializeForm() {
     return this.fb.group({
       name: [null, [Validators.required] ], 
       password: [null, [Validators.required] ], 
       email: [null, [Validators.required] ], 
       age: [null, [Validators.required] ], 
       nationality: [null], 
       cpf: [null, [Validators.required] ], 
       phoneNumber: [null], 
     })
  }

  onSubmit() {
    console.log(this.getFormValue());
    alert('form submitted');
    alert(JSON.stringify(this.getFormValue()));
  }

  onReset() {
    this.personalInformationForm.reset();
  }

  getFormValue(): PersonalInformationInterface {
    return this.personalInformationForm.value;
  }

  getFormControlValue(formControlName: string) {
    return this.personalInformationForm.get(formControlName).value;
  }
}
personal-information-form.component.ts hosted with ❤ by GitHub

 

export interface PersonalInformationInterface {
name: string;
password: string;
email: string;
age: number;
nationality: string;
cpf: string;
phoneNumber: string;
}
personal-information.model.ts hosted with ❤ by GitHub

 

How to contribute to Forms Schematic

That’s it on this post, I hope you enjoyed. I also want to invite you to help us to improve the project and create a really useful schematic to boost forms creation, feel free to contact me on adrian.caetano@ilegra.com if you have any question or want to submit a PR to our project solving some issue.

At least, I want to say thank you to my friend Diego Pacheco who gave me the idea and motivation to start this forms generation to help our team and also created a POC using Yeoman and AngularJS generating a input field to convince me that was possible to achieve this hehe. And I want to mention Matheus Streb Vieira who started with me the creation of schematics to generate CRUD HTTP services to Angular applications and help me to think about the POC scope.

Source: medium.com/@adrianlemess

Compartilhe este post: