Hello, my name is Maxim. For several years I have been doing front-end development. I often have to deal with the layout of various html templates. In my daily work, I usually use the webpack builder with a customized pug template engine, and I also use the BEM methodology. In order to make my life easier I use a wonderful package .
Recently I needed to make a small project on Angular, and since I was used to working with my favorite tools, I did not want to return to the bare html. In this connection, the problem arose of how to make bempug friends with an angular, and not just make friends, but also generate components from cli with the structure I needed.
Who cares how I did it all, welcome to cat.
To begin, create a test project on which we will test our template.
We execute at the command line:
ng g test-project
.
In the settings, I chose the scss preprocessor, since it is more convenient for me to work with it.
The project was created, but the default component templates in our html, now fix it. First of all, you need to make angular cli friends with the pug template engine, for this I used the ng-cli-pug-loader package
Install the package, for this, go to the project folder and execute:
ng add ng-cli-pug-loader
.
Now you can use pug template files. Next, we rewrite the root decorator of the AppComponent component to:
@Component({ selector: 'app-root', templateUrl: './app.component.pug', styleUrls: ['./app.component.scss'] })
Accordingly, we change the file extension app.component.html to app.component.pug, and the content is written in the template syntax. In this file, I deleted everything except the router.
Finally, let's start creating our component generator!
To generate templates, we need to create our own scheme. I am using the schematics-cli package from @ angular-devkit. Install the package globally with the command:
npm install -g @angular-devkit/schematics-cli
.
I created the scheme in a separate directory outside the project with the command:
schematics blank --name=bempug-component
.
We go into the created scheme, we are now interested in the src / collection.json file. It looks like this:
"$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json", "schematics": { "bempug-component": { "description": "A blank schematic.", "factory": "./bempug-component/index#bempugComponent" } } }
This is a description file of our scheme, where the parameter is "factory": "./bempug-component/index#bempugComponent": this is the description of the main function of the "factory" of our generator.
Initially, it looks something like this:
import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
You can make the function export by default, then the parameter "factory" can be rewritten as "./bempug-component/index".
Next, in the directory of our scheme, create the file schema.json, it will describe all the parameters of our scheme.
{ "$schema": "http://json-schema.org/schema", "id": "SchemanticsForMenu", "title": "Bempug Schema", "type": "object", "properties": { "name": { "type": "string", "$default": { "$source": "argv", "index": 0 } }, "path": { "type": "string", "format": "path", "description": "The path to create the component.", "visible": false }, "project": { "type": "string", "description": "The name of the project.", "$default": { "$source": "projectName" } } } }
Parameters are in properties, namely:
- name name of the entity (in our case it will be a component);
- Path is the path by which the generator creates the component files;
- Project is the project itself, in which the component will be generated;
Add a few more parameters to the file that will be needed in the future.
"module": { "type": "string", "description": "The declaring module.", "alias": "m" }, "componentModule": { "type": "boolean", "default": true, "description": "Patern module per Component", "alias": "mc" }, "export": { "type": "boolean", "default": false, "description": "Export component from module?" }
- module here will be stored a link to the module in which the component will be included, or rather the component module;
- componentModule there is a flag whether to create for the component its own module (then I came to the conclusion that it will always be created and set it to true);
- export: whether it is a flag to export from the module into which we are importing our component module;
Next, we create an interface with the parameters of our component, the schema.d.ts file.
export interface BemPugOptions { name: string; project?: string; path?: string; module?: string; componentModule?: boolean; module?: string; export?: boolean; bemPugMixinPath?: string; }
In it, properties duplicate properties from schema.json. Next, prepare our factory, go to the index.ts file. In it, we create two filterTemplates functions, which will be responsible for creating a module for a component depending on the value of componentModule, and setupOptions, which sets up the parameters necessary for the factory.
function filterTemplates(options: BemPugOptions): Rule { if (!options.componentModule) { return filter(path => !path.match(/\.module\.ts$/) && !path.match(/-item\.ts$/) && !path.match(/\.bak$/)); } return filter(path => !path.match(/\.bak$/)); } function setupOptions(options: BemPugOptions, host: Tree): void { const workspace = getWorkspace(host); if (!options.project) { options.project = Object.keys(workspace.projects)[0]; } const project = workspace.projects[options.project]; if (options.path === undefined) { const projectDirName = project.projectType === 'application' ? 'app' : 'lib'; options.path = `/${project.root}/src/${projectDirName}`; } const parsedPath = parseName(options.path, options.name); options.name = parsedPath.name; options.path = parsedPath.path; }
Next, we write in the main function:
export function bempugComponent(options: BemPugOptions): Rule { return (host: Tree, context: SchematicContext) => { setupOptions(options, host); const templateSource = apply(url('./files'), [ filterTemplates(options), template({ ...strings, ...options }), move(options.path || '') ]); const rule = chain([ branchAndMerge(chain([ mergeWith(templateSource), ])) ]); return rule(host, context); } }
The factory is ready and it can already generate component files by processing templates from the files folder, which is not yet available. It doesn’t matter, we create a bempug-component files folder in our scheme folder in my case. In the files folder, create the folder __name@dasherize__
, during generation, the factory will replace __name@dasherize__
with the name of the component.
Next, inside the __name@dasherize__
create files
__name@dasherize__
.component.pug pug component template__name@dasherize__
.component.spec.ts unit test file for the component__name@dasherize__
.component.ts file of the component itself__name@dasherize__
-component.module.ts component module__name@dasherize__
-component.scss component stylesheet
Now we will add support for updating modules to our factory, for this we will create the add-to-module-context.ts file to store the parameters that the factory will need to work with the module.
import * as ts from 'typescript'; export class AddToModuleContext {
Add module support to the factory.
const stringUtils = { dasherize, classify };
Now, when adding the -m <module reference> parameter to the cli command, our component module will add import to the specified module and add the export from it when adding the –export flag. Next, add BEM support. To do this, I took the sources for the npm bempug package and made the code in one bempugMixin.pug file, which I placed in the common folder and inside in another common folder so that the mixin is copied to the common folder in the project on the angular.
Our task is that this mixin is connected in each of our template files, and not duplicated when generating new components, for this we add this functionality to our factory.
import { Rule, SchematicContext, Tree, filter, apply, template, move, chain, branchAndMerge, mergeWith, url, SchematicsException } from '@angular-devkit/schematics'; import {BemPugOptions} from "./schema"; import {getWorkspace} from "@schematics/angular/utility/config"; import {parseName} from "@schematics/angular/utility/parse-name"; import {normalize, strings} from "@angular-devkit/core"; import { AddToModuleContext } from './add-to-module-context'; import * as ts from 'typescript'; import {classify, dasherize} from "@angular-devkit/core/src/utils/strings"; import {buildRelativePath, findModuleFromOptions, ModuleOptions} from "@schematics/angular/utility/find-module"; import {addExportToModule, addImportToModule} from "@schematics/angular/utility/ast-utils"; import {InsertChange} from "@schematics/angular/utility/change"; const stringUtils = { dasherize, classify };
It's time to fill in our template files.
__name@dasherize__.component.pug
:
include <%= bemPugMixinPath %> +b('<%= name %>') +e('item', {m:'test'}) | <%= name %> works
What is specified in <% =%> during generation will be replaced by the name of the component.
__name@dasherize__.component.spec.ts:
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import {NO_ERRORS_SCHEMA} from '@angular/core'; import { <%= classify(name) %>ComponentModule } from './<%= name %>-component.module'; import { <%= classify(name) %>Component } from './<%= name %>.component'; describe('<%= classify(name) %>Component', () => { let component: <%= classify(name) %>Component; let fixture: ComponentFixture<<%= classify(name) %>Component>; beforeEach(async(() => { TestBed.configureTestingModule({ imports: [<%= classify(name) %>ComponentModule], declarations: [], schemas: [ NO_ERRORS_SCHEMA ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(<%= classify(name) %>Component); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); });
In this case, <% = classify (name)%> is used to cast the name to CamelCase.
__name@dasherize__.component.ts:
import { Component, OnInit, ViewEncapsulation} from '@angular/core'; @Component({ selector: 'app-<%=dasherize(name)%>-component', templateUrl: '<%=dasherize(name)%>.component.pug', styleUrls: ['./<%=dasherize(name)%>-component.scss'], encapsulation: ViewEncapsulation.None }) export class <%= classify(name) %>Component implements OnInit { constructor() {} ngOnInit(): void { } }
__name@dasherize__-component.module.ts:
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import {<%= classify(name) %>Component} from './<%= name %>.component'; @NgModule({ declarations: [ <%= classify(name) %>Component, ], imports: [ CommonModule ], exports: [ <%= classify(name) %>Component, ] }) export class <%= classify(name) %>ComponentModule { }
__name@dasherize__-component.scss:
.<%= name %>{ }
We make the build of our scheme with the command `` npm run build``.
Everything is ready to generate components in the project!
To check, go back to our Angular project and create a module.
ng gm test-schema
Next, we do `` npm link <absolute path to the project folder with our scheme> '', in order to add our scheme to the node_modules of the project.
And we try the circuit with the ng g bempug-component:bempug-component test -m /src/app/test-schema/test-schema.module.ts –export
.
Our scheme will create a component and add it to the specified module with export.
The scheme is ready, you can start making the application on familiar technologies.
You can see the final version here , and also the package is available in npm .
When creating the scheme, I used articles on this topic, I express my gratitude to the authors.
Thank you for your attention, everyone who read to the end, you are the best!
And another exciting project awaits me. See you soon!