1

I have a component that is outputting comments on the page. In these comments, there is some HTML <footer>By John Doe on Jan 23, 2018</footer>.

I am trying to wrap the contents of the footer in an anchor link so that you can jump to the parent communication on the same page.

Component HTML:

<p [innerHTML]="linkComm(comm.Communication, comm)"></p>

comm.communication = Blah Blah Blah <footer>By John Doe on Jan 23, 2018</footer>

Component TS:

    /**
     * Link to parent communication
     * @param message 
     * @param comm 
     */
    linkComm(message, comm){
        let output = message;
        output = output.replace(/<footer>/gi, '<footer><a (click)="someMethod(comm.CommunicationID)">');
        output = output.replace(/<\/footer>/gi, '</a></footer>');
        return output;
    }

When I take this approach, no click event is being added to the link like I would expect.

I also tried a module suggested here (https://stackoverflow.com/a/46587554/2628921) that is meant for page scrolling but it doest appear that pageScroll is being added on there either.

output = output.replace(/<footer>/gi, '<footer><a pageScroll href="#commID_'+comm.CommunicationID+'">');

Is this a security issue where only certain attributes can be added to a link?

SBB
  • 8,560
  • 30
  • 108
  • 223
  • i might be wrong but i don't think that's possible... as far as i know and in simple terms templates get rendered by angular in such a way that's impossible to "pass a DOM element to it" via some strings... this is not jQuery :P do it angular style! – Laurent Schwitter Jan 23 '18 at 23:03
  • @LaurentSchwitter - How else would I do this? I tried the angular way by using a router link but its also the same situation you mentioned, passing something to an element. `output = output.replace(/ – SBB Jan 23 '18 at 23:05
  • 2
    Why not just create a comment component, which does it on its own, in a more Angular-ish way instead of replacing inner HTML? – Amit Beckenstein Jan 23 '18 at 23:10
  • 1
    @SBB omg please don't use a router to implement a link :P do what Amit suggested... or simply [(click)]="hasLink ? 'linkID' : ''" or something... i deeply suggest you read some angular documentation... it looks to me like you havn't and are trying to make it work like it's not angular... follow some quick start guide or tutorial for your own well being – Laurent Schwitter Jan 23 '18 at 23:21

1 Answers1

1

Angular is precompiled

Remember, Angular is precompiled. Angular template syntax is consumed and transformed when it is compiled, which means the DOM isn't actively checked for Angular syntax. In fact, one goal of Angular is to decouple your code from the DOM.

Think of it this way: adding a (click) to an element isn't valid vanilla HTML syntax. When you add that binding to a template in an Angular project, compile it, and inspect the output through your DevTools, that (click) attribute won't be on the element in the compiled HTML anymore. That's because the Angular compiler recognizes that (click) syntax, removes it from the outputted HTML, and instead registers an onclick handler behind the scenes that behaves as you'd expect in the scope it's assigned, and Angular manages this behavior in its own space. Once it's compiled as a component, simply altering the HTML won't add anything that Angular will be aware of -- the logic is there, but it's been consumed by the framework.

Raw HTML

Now, Angular shines with this idea of a template syntax, but when dealing with raw HTML things can be a bit trickier to do 'The Angular way.' The ideal solution would be to not parse raw HTML at all, but I'll assume for your case that it's a necessity.

Since Angular has the DOM wrapped up in its API, it's considered best practice to not directly manipulate it (note: binding potentially expensive function calls to inputs can also be bad...called on change detection cycles). Something you could try instead:

<app-link-comm [comm]="comm"></app-link-comm>

where app-link-comm is a component:

@Component({
  selector: 'app-link-comm',
  template: '<p [innerHTML]="comm.Communication"></p>`,
  styles: {some-style: stylish}
})
export class LinkCommComponent implements AfterViewInit {

  // Allow the parent to bind a comm to the component
  @Input('comm') comm;


  constructor(public render: Renderer2, public router: Router) {}

  // Wait until afterViewInit so the view is drawn and inputs are resolved
  ngAfterViewInit() {
    // Search within this node with a jQuery-like DOM querySelector
    let footer = this.renderer.selectRootElement('footer');
    // attach some Angular-aware behavior to it
    this.renderer.listen(footer, 'click', (event: Event) => {
      // do whatever you want on click here. We could emit an event that the parent component could listen to, but let's just assume we're changing routes and act on the router directly.
      this.router.navigate(`#commID_${this.comm.CommunicationId}`);
    }

    /** That leaves us with a click listener on the footer. 
    /* If we wanted to append a child element, we just use the Renderer with the new element:
    **/

    let a = document.createElement('a');
    // We could listen for clicks on the <a> like above, but let's just attach vanilla behavior:
    a.href = `#commID_${this.comm.CommunicationId}`;
    this.renderer.appendChild(footer, a);

  }

}

The Renderer2 is an injectable service that both wraps up differences in browser and mobile APIs and also allows us to attach behaviors to elements that Angular can manage through its NgZone.

Alternatively:

If you wanted to append an anchor element to the footer with an href The same thing could potentially be achieved with directives, pipes, or even dynamically rendered components, but I think this is a good solution when dealing with raw HTML. The component gets a single binding, the [innerHTML] allows Angular to sanitize the input, and DOM querying is done through the Angular Renderer instead of manual parsing.

Since this is now its own component, other localized behaviors can be kept here as well.

As far as adding directives to raw HTML, it's not going to be possible. Angular needs to be aware of the directive before compile time to attach its behaviors correctly. Your best bet would be to make a host component/element with the directive on it, then bind the correct (parsed) html elements to that element.

joh04667
  • 7,159
  • 27
  • 34