David Findlay
David Findlay

Reputation: 1356

Get inner text from angular component selector

I am building an Angular message component:

<app-info-card>
  my message here
</app-info-card>

Here's my component:

import {AfterViewInit, Component, ContentChild, ElementRef, Input, OnInit} from '@angular/core';

@Component({
  selector: 'app-info-card',
  templateUrl: './info-card.component.html',
  styleUrls: ['./info-card.component.css']
})
export class InfoCardComponent implements OnInit, AfterViewInit {

  @ContentChild('app-info-card') messageRef: ElementRef;
  message: string;

  constructor() {
  }

  ngOnInit() {
  }

  ngAfterViewInit() {

    this.message = this.messageRef.nativeElement.innerHTML;
    console.log(this.messageRef.nativeElement.innerHTML);

  }

}

This gives me an error that messageRef is undefined. I'm trying to get the inner text from the component selector "my message here" into the message field of the component. Is there a way I can do this without adding attributes?

Upvotes: 11

Views: 23237

Answers (5)

hogan
hogan

Reputation: 1561

Based on your answers what you need is to pass in that message into your Child Component. There are different methods, have a look at https://angular.io/guide/component-interaction

I would suggest you work with Input binding in this case. Either with a variable in your parent template

<app-info-card [message]="message"></app-info-card>

Where in the corresponding component you define

public message: string = 'my message here';

Or directly if it's not dynamic (likely not the case)

<app-info-card [message]="'my message here'"></app-info-card>

pay attention to the ' inside

With both ways in your Child Component put it like this:

export class InfoCardComponent implements OnInit {

    @Input('message') message: string;

    ngOnInit() {
        // do whatever other stuff you want
    }

}

You might want to implement OnChanges as well. Let me know if you have further questions.

One alternative is to use content projection.

<app-info-card>my message here</app-info-card>

And in the template of the card, i.e.

<div class="card">
    <ng-content></ng-content>
</div>

Upvotes: 9

Tengiz
Tengiz

Reputation: 8429

I know this question is old, but I found the solution to this question without introducing the additional (inner/wrapped) component directive.

Inside the template of the component that needs to access the content as string, you need to use the wrapped combination of ng-template and ng-content. Then, grab the pointer to the template using the ViewChild. The trick is that the template does not render by itself, while its content is still populated.

Template of the component that needs the content as string without rendering it:

<ng-template #template>
  <ng-content></ng-content>
</ng-template>

Inside that same component's ts code, you get the content as a TemplateRef that you can render anywhere you want:

@ViewChild('template') templateRef!: TemplateRef<unknown>;

Finally, you can pass the content exactly as you wanted to do:

<app-info-card>
  my message here
</app-info-card>

Upvotes: 2

Saeed
Saeed

Reputation: 11

import { Component, OnInit, ElementRef } from '@angular/core';

@Component({
  selector: 'app-info-card',
  template: '<ng-content></ng-content>',
  styleUrls: ['./info-card.component.css'],
})
export class InfoCardComponent implements OnInit {
  constructor(private el: ElementRef) { }
  ngOnInit(): void {
    console.log(this.el.nativeElement.innerText);
  }
}

Upvotes: 0

Almund
Almund

Reputation: 6226

As mentioned by @GHB I managed to get this working using a directive. It looks as though it's hard to use the component contents directly but by using a wrapping directive it's possible.

import { Component, OnInit, ContentChild, AfterContentInit, ElementRef, Directive } from '@angular/core';

@Directive({
  selector: 'app-example-code'
})
export class ExampleCodeDirective {}

@Component({
  selector: 'app-example-view',
  templateUrl: './example-view.component.html',
  styleUrls: ['./example-view.component.css']
})
export class ExampleViewComponent implements OnInit, AfterContentInit {  
  @ContentChild(ExampleCodeDirective, { read: ElementRef })
  content: ElementRef;

  constructor() { }

  ngAfterContentInit() {
    if (this.content) {
      console.log(this.content.nativeElement.innerText); 
    }
  }

  ngOnInit() {
  }

}

<app-example-view>
  <app-example-code>
    some text
  </app-example-code>
</app-example-view>

Upvotes: 4

GHB
GHB

Reputation: 947

you're basically using the ContentChild in a strange way. you are referencing 'app-info-card' in itself as it's own child. from the docs:

You can use ContentChild to get the first element or the directive matching the selector from the content DOM. If the content DOM changes, and a new child matches the selector, the property will be updated.

so, if you want to put some component/directive inside your component and want to access them later, you can use the ContentChild or ContentChildren, in a way like this: (from the same link again)

@Directive({selector: 'pane'})
export class Pane {
  @Input() id: string;
}

@Component({
  selector: 'tab',
  template: `
    <div>pane: {{pane?.id}}</div> 
  `
})
export class Tab {
  @ContentChild(Pane) pane: Pane;
}


@Component({
  selector: 'example-app',
  template: `
    <tab>
      <pane id="1" *ngIf="shouldShow"></pane>
      <pane id="2" *ngIf="!shouldShow"></pane>
    </tab>

    <button (click)="toggle()">Toggle</button>
  `,
})
export class ContentChildComp {
  shouldShow = true;

  toggle() { this.shouldShow = !this.shouldShow; }
}

that being said, you can use it with the class name instead of the selector string. something like this:

@ContentChild(InfoCardComponent) messageRef: InfoCardComponent;

which as i said, is a strange thing to do and would give you the InfoCardComponent component's class itself.

anyway, if you just want to have a component that wraps some block of your elements, you can do two things off the top of my head:

1- using @Input :

if you just want to wrap some text and show it in a special style, you can simply have a property like:

@Input() myMessage: string;

and pass it's value when using InfoCardComponent like this:

<app-info-card [myMessage]="'my message here'"></app-info-card>

and then use it via binding everywhere you want...

2- using <ng-content></ng-content> :

if you want to pass-in to the component, more than just a text and including elements (like <div>s and...), you can use the built-in <ng-content></ng-content> directive which contains everything that's been put inside the initial host element (which in this case, would be your <app-info-card></app-info-card>). and the way it works is that you simply put it inside your template like:

<div>
    <ng-content></ng-content>
</div> 

all of these ways have many details and fit certain cases depending on your need, and i just mentioned a brief case of using them. before using any of them i suggest you reading the docs again.

Upvotes: 6

Related Questions