10

Let's say we have an existing angular component including menu.component.html :

<ul>
  <li><a href="/home">home</a></li>
  <li><a href="/about">about</a></li>
</ul>

The goal is to add a new link with angular schematics, just after "about"

 <li><a href="/contact">contact</a></li>

Any ideas?

Martin
  • 15,820
  • 4
  • 47
  • 56
user4649102
  • 463
  • 6
  • 13
  • 1
    How much have you looked at how to build your own schematic? Have you gone through articles such as this: https://medium.com/@tomastrajan/%EF%B8%8F-how-to-create-your-first-custom-angular-schematics-with-ease-%EF%B8%8F-bca859f3055d – DeborahK Jul 16 '18 at 20:03
  • I went through many articles. I found examples of modifying specific elements like a constructor or declaration in a module. here is an example [link](https://www.softwarearchitekt.at/post/2017/12/01/generating-angular-code-with-schematics-part-ii-modifying-ngmodules.aspx). – user4649102 Jul 18 '18 at 07:45

3 Answers3

17

You can do something like:

export function myComponent(options: any): Rule {
  return (tree: Tree, _context: SchematicContext) => {
    const content: Buffer | null = tree.read("./menu.component.html");
    let strContent: string = '';
    if(content) strContent = content.toString();

    const appendIndex = strContent.indexOf('</ul>');
    const content2Append = '    <li><a href="/contact">contact</a></li> \n';
    const updatedContent = strContent.slice(0, appendIndex) + content2Append + strContent.slice(appendIndex);

    tree.overwrite("./menu.component.html", updatedContent);
    return tree;
  };
}

Note that this is a very primitive solution. There are DOM parser libraries in nodejs which can make your life easier (jsdom). And you can take content2Append variable as an input and clean it up for your use case.

eko
  • 39,722
  • 10
  • 72
  • 98
  • Unfortunately jsdom doesn't support angular template syntax and I've been trying to search for a template parser that can help me add and manipulate a template any suggestions would be greatly appreciated! – saNiks Oct 08 '19 at 13:27
  • 1
    @saKins see my answer in how to use jsdom along with the tree recorder to update Angular templates. – Martin Apr 30 '20 at 10:01
6

Eko's answer is correct. I would like to expand on this answer and do two things:

  1. Use the tree recorder to update the template
  2. use jsdom to find the location in the template to update

With jsdom and similar tools, serializing an Angular template either will not work -- or break any camelCase, [boxed], (parenthetical), #tagged -- attributes. So we will only use jsdom to find the location of where we want to update.

function updateTemplate() {
  return (tree: Tree) => {
    const buffer = tree.read(TEMPLATE_PATH);
    const content = buffer?.toString();
    if (!content) {
      throw new SchematicsException(`Template ${TEMPLATE_PATH} not found`);
    }

    // the includeLocations flag is very important here
    const dom = new JSDOM(content, { includeNodeLocations: true });
    const element = dom.window.document.querySelector('ul');
    const locations = dom.nodeLocation(element);
    if (!locations) {
      throw new SchematicsException(`<ul> could not be found in ${TEMPLATE_PATH}`);
    }

    // now we update the template using the tree recorder
    // we don't use jsdom to directly update the template
    const recorder = tree.beginUpdate(TEMPLATE_PATH);
    const listItem = `  <li><a href="/contact">contact</a></li>\n`
    recorder.insertLeft(locations.endTag.startOffset, listItem);
    tree.commitUpdate(recorder);

    return tree;
  };
}

Again, eko's answer is correct. This answer illustrates how to use jsdom and the tree recorder together to update an Angular template -- which is useful for inserting directives, events, tags and methods into elements, wrapping sections and more complex transformations.

General Grievance
  • 4,555
  • 31
  • 31
  • 45
Martin
  • 15,820
  • 4
  • 47
  • 56
  • FYI, the simple htmlparser library seems to be able to read HTML without breaking the Angular-specific attributes. you can fairly easily modify the DOM. Rewriting it as a string is not as simple though. – Eric Leibenguth Feb 22 '22 at 18:25
0

Update : With the latest 1.0.0rc3 cheerio version, the xmlMode does self close tags, and this kills your template ! So instead, go with 0.22.0 cheerio version and lose the xmlMode option !


I'm testing shematics and I need to update angular component templates as well. Here is a useful article :

https://medium.com/humanitec-developers/update-and-insert-auto-generated-code-to-existing-typescript-html-and-json-files-with-angular-9b37bdadf041

Now, be careful, with the latest versions of cheerio, you should use the xmlMode option to load your component template, or you will have a whole HTML document instead :

https://github.com/cheeriojs/cheerio/issues/1031#issuecomment-368307598

You would end up adding some HTML to an Angular template this way :

function addComponent(): Rule {
  return (tree: Tree) => {
    const filePath = './src/app/app.component.html';
    const content: Buffer | null = tree.read(filePath);
    let strContent: string = '';
    if(content) strContent = content.toString('utf8');
    const $ = cheerio.load(strContent, {
      xmlMode: true
    });
    const contentToInsert = `<my-comp></my-comp>`;
    $.root().append(contentToInsert);
    tree.overwrite(filePath, $.html());
    return tree;
  };
}

I hope this can help :)

Alain Boudard
  • 768
  • 6
  • 16