angularnrwl-nx

What is the proper syntax for projecting content into a custom schematic?


When building my components I always copy and paste a comment at the top of my files to itemize all the specs and user stories as I think and build them out, something like as follows

/*


Some Component Name

PURPOSE :

VIEWS :

ELEMENTS :

INTERACTIVITY :

GETS DATA FROM :

SENDS DATA TO :

USER STORIES :


*/

I've been wanting to dive deeper into the capabilities of NX so I figured a good introduction would be to make my own schematics for components, services, directives etc. because I have slightly different things I like to list like for example, why would I want to itemize views and elements in a service file or list interactions with a back end in a component file?

I successfully generated a plugin library in my libs folder by running

nx g @nx/workspace:plugin libs/my-plugin

Then I created a new generator by running

nx g @nx/workspace:generator libs/my-plugin/src/generators/my-component-generator/my-component-generator

Once that completed I had to manually create a component folder and manually create the files I want to generate using some type of projection syntax. The file names are

// I'm using a code container here because the __ just makes the text bold
// in the post.


 __name@dasherize__.component.ts
 __name@dasherize__.component.spec.ts
 __name@dasherize__.component.html
 __name@dasherize__.component.css

So far I only tried defining the component.ts file which looks like this

import { Component } from '@angular/core';





@Component({
    selector    : 'app-__name@dasherize__',
    templateUrl : './__name@dasherize__.component.html',
    styleUrls   : [ './__name@dasherize__.component.css' ]
})

export class <%= classify(name) %>Component {}

The <%= classify(name) %> part is causing a ts 1139 error with the <%= %> part and saying name is deprecated on the name part. I tried looking deeper into how to do this and saw another method using __name@classify__ and got an error saying declaration expected. I came across other examples where instead of using __name@dasherize__ they used <%= dasherize(name) %>

In my index.ts.template file I have this

const variable = "<%= name %>";

so I tried doing

export class <%= name =>Component {}

and I'm still getting a red squiggly line. Can someone help me figure out how to get this working?


Solution

  • Ok after a lot of tinkering and experimenting I learned that the <%= %> thing comes from EJS..... which I had never heard of until figuring this out to in turn understand how to use it. The solution I eventually came to goes as follows

    1. Modify the name of my schematic files to use the following syntax

    __name__.component.css.template
    __name__.component.html.template
    __name__.component.ts.template
    __name__.component.spec.ts.template
    

    2. Define the options I want to provide in the CLI using the interface inside my schema.d.ts file

    /*
    
    this allows you to use
     
    --name=some-name --project=some-project --location=path/to/somewhere
    
    in the CLI or if you don't want to add them as flags you'll be prompted
    with questions to answer after you run your command.  You can define any
    type of property you want to suit whatever you want to be able to define
    as a flag or or question to answer.
    
    */
    
    export interface CustomComponentGeneratorSchema {
    
      name     : string;
      project  : string;
      location : string;
      
    }
    

    3. Configure the options defined in the CustomComponentGeneratorSchema in the "properties" object of the schema.json file

    {
        "$schema" : "...",
        "$id" : "...",
        "title" : "...",
        "type" : "...",
    
    
    
        "properties" : {
    
            "name" : {
                "type" : "string"
                "description" : "your description",
                "$default" : {
                    "$source" : "argv",
                    "index" : 0
                },
                "x-prompt" : "name your component"
            },
    
            "project" : {
                "type" : "string",
                "description : "your description",
                "x-prompt" : "what project would you like to add this to?",
            },
    
            "location" : {
                "type" : "string",
                "description" : "your description",
                "x-prompt" : "where do you want this located?"
            }
    
        },
    
    
    
        "required" : ["name", "project"]
    }
    

    3. Modify the main generator file to handle your schema options.

    import { CustomComponentGeneratorSchema as Schema } from "./schema";
     
    import { /*stuff*/ names, getProjects } from "@nx/devkit";
    
    import { strings } from '@angular-devkit/core';
    
    import * as path from 'path';
    
    
    
    
    export async function myCustomComponentGenerator(tree : Tree, options : Schema){
    
        //  the names method returns camel case, pascal case,
        //  kebab case, etc. versions of the name you enter in
        //  the --name flag
        const componentNames = names( options.name );
    
        //  getProjects() fetces all the projects in the workspace
        const projects = getProjects( tree );
    
        //  uses the --project property to search projects for
        //  the target project
        const targetProject = projects.get( options.project );
    
    
    
        if ( !targetProject ) {
    
            //  throw an error if the project isn't found
            throw new Error(`Project "${options.project}" not found.`); 
    
        }
      
      
        //  generates a file path for the component files
        const targetPath = path.join(
            //  the target project's root directory
            targetProject.root,
    
            //  the value passed into the --location prop
            options.location,
    
            //  fileName() returns the "kebab case" version of
            //  the --name passed in.
            componentNames.fileName
        );
    
    
    
        //  generates the files for the custom component
        generateFiles(
            tree,
            path.join(__dirname, 'files'),
            targetPath,
            { ...componentNames,  tmpl : '' }
          );
      
          await formatFiles(tree);
    
    }
    

    4. Pass in the appropriate value returned by the names() method into the scematic files using EJS syntax

    __name__component.ts.template

    /*
    
    <%= className %>
    
    PURPOSE :
    
    VIEWS :
    
    ELEMENTS :
    
    INTERACTIVITY :
    
    GETS DATA FROM :
    
    SENDS DATA TO :
    
    USER STORIES :
    
    */
    
    
    
    
    
    import { Component } from '@angular/core';
    
    
    
    
    
    @Component({
        selector    : 'lib-<%= fileName %>',
        templateUrl : './<%= fileName %>.component.html',
        styleUrls   : [ './<%= fileName %>.component.css' ]
    })
    
    export class <%= className %>Component {}
    

    name.component.spec.ts.template

    import { ComponentFixture, TestBed } from '@angular/core/testing';
    import { <%= className %>Component } from './<%= fileName %>.component';
    
    
    
    describe('<%= className %>Component', () => {
    
      let component : <%= className %>Component;
      let fixture   : ComponentFixture<<%= className %>Component>;
    
      beforeEach( async () => {
    
    
        await TestBed.configureTestingModule( {
    
          imports : [ <%= className %>Component ],
    
        } ).compileComponents();
    
        fixture   = TestBed.createComponent( <%= className %>Component );
        component = fixture.componentInstance;
    
        fixture.detectChanges();
    
    
      });
    
    
    
      it( 'should create' , () => {
    
        expect( component ).toBeTruthy();
    
      });
    
    
    
    });
    

    name.component.html.template

    <p><%= fileName %> works</p>
    

    Once that's complete we can just run the nx command in our CLI and everything will generate as expected.