#ContentChildren not picking up items inside custom component - html

I'm trying to use #ContentChildren to pick up all items with the #buttonItem tag.
#ContentChildren('buttonItem', { descendants: true })
This works when we have the ref item directly in the parent component.
<!-- #ContentChildren returns child item -->
<parent-component>
<button #buttonItem></button>
<parent-component>
But, if the element with the #buttonItem ref is wrapped in a custom component, that does not get picked by the #ContentChildren even when I set the {descendants: true} option.
<!-- #ContentChildren returns empty -->
<parent-component>
<child-component-with-button-ref></child-component-with-button-ref>
<parent-component>
I have created a simple StackBlitz example demonstrating this.

Doesn't appear to be a timeline for a resolution of this item via github... I also found a comment stating you cannot query across an ng-content boundary.
https://github.com/angular/angular/issues/14320#issuecomment-278228336
Below is possible workaround to get the elements to bubble up from the OptionPickerComponent.
in OptionPickerComponent count #listItem there and emit the array AfterContentInit
#Output() grandchildElements = new EventEmitter();
#ViewChildren('listItem') _items
ngAfterContentInit() {
setTimeout(() => {
this.grandchildElements.emit(this._items)
})
}
Set template reference #picker, register to (grandchildElements) event and set the $event to picker.grandchildElements
<app-option-picker #picker [optionList]="[1, 2, 3]" (grandchildElements)="picker.grandchildElements = $event" popup-content>
Create Input on PopupComponent to accept values from picker.grandchildElements
#Input('grandchildElements') grandchildElements: any
In app.component.html accept picker.grandchildElements to the input
<app-popup [grandchildElements]="picker.grandchildElements">
popup.component set console.log for open and close
open() {
if (this.grandchildElements) {
console.log(this.grandchildElements);
}
else {
console.log(this.childItems);
}
close() {
if (this.grandchildElements) {
console.log(this.grandchildElements);
}
else {
console.log(this.childItems);
}
popup.component change your ContentChildren back to listItem
#ContentChildren('listItem', { descendants: true }) childItems: Element;
popup.component.html set header expression
<h3>Child Items: {{grandchildElements ? grandchildElements.length : childItems.length}}</h3>
Stackblitz
https://stackblitz.com/edit/angular-popup-child-selection-issue-bjhjds?embed=1&file=src/app/option-picker/option-picker.component.ts

I had the same issue. We are using Kendo Components for angular. It is required to define Columns as ContentChilds of the Grid component. When I wanted to wrap it into a custom component and tried to provide additional columns via ng-content it simply didn't work.
I managed to get it working by resetting the QueryList of the grid component AfterViewInit of the custom wrapping component.
#ViewChild(GridComponent, { static: true })
public grid: GridComponent;
#ContentChildren(ColumnBase)
columns: QueryList<ColumnBase>;
ngAfterViewInit(): void {
this.grid.columns.reset([...this.grid.columns.toArray(), ...this.columns.toArray()]);
this.grid.columnList = new ColumnList(this.grid.columns);
}

One option is re-binding to the content child.
In the template where you are adding the content child you want picked up:
<outer-component>
<content-child [someBinding]="true" (onEvent)="someMethod($event)">
e.g. inner text content
</content-child>
</outer-component>
And inside of the example fictional <outer-component>:
#Component()
class OuterComponent {
#ContentChildren(ContentChild) contentChildren: QueryList<ContentChild>;
}
and the template for <outer-component> adding the <content-child> component, re-binding to it:
<inner-template>
<content-child
*ngFor="let child of contentChildren?.toArray()"
[someBinding]="child.someBinding"
(onEvent)="child.onEvent.emit($event)"
>
<!--Here you'll have to get the inner text somehow-->
</content-child>
</inner-template>
Getting that inner text could be impossible depending on your case. If you have full control over the fictional <content-child> component you could expose access to the element ref:
#Component()
class ContentChildComponent {
constructor(public element: ElementRef<HTMLElement>)
}
And then when you're rebinding to it, you can add the [innerHTML] binding:
<content-child
*ngFor="let child of contentChildren?.toArray()"
[someBinding]="child.someBinding"
(onEvent)="child.onEvent.emit($event)"
[innerHTML]="child.element.nativeElement.innerHTML"
></content-child>
You may have to sanitize the input to [innerHTML] however.

Related

Angular innerHTML contents are jumbled. Only the last innerHTML is displayed correctly

In my Angular project (version 8) I am creating a list of static HTML from database and rendering it in parent HTML. Only the last div having innerHTML is rendered correctly, all the preceding divs having child html is not rendered correctly. The contents are jumbled. Basically the child html's style is not honored except for the last child html.
I am using sanitize html pipe for the div.
The angular component onInit queries DB in a loop. Each get call returns HTML text which is appended to an array of strings. The HTML text is basically PDF to HTML converted file. Each of the HTML file has its own style tag.
My guess is that only the last innerHTML's style is applied to all the preceding child innerHTML hence the jumbled contents (unless my guess is incorrect)
Any suggestion to solve the issue ?
HTML
<div *ngFor="let qBank of tsqm.selectedQuestions; let i = index">
<div class="page">
<div [innerHTML]="questionDataFromHtml[i] |
sanitizeHtml"></div>
</div>
</div>
Sanitize HTML:
#Pipe({ name: 'sanitizeHtml'})
export class SanitizeHtmlPipe implements PipeTransform {
constructor(private _sanitizer: DomSanitizer) { }
transform(value: string): SafeHtml {
return this._sanitizer.bypassSecurityTrustHtml(value);
}
}
Component:
ngOnInit(){
this.questionset = this.storage.get(quesId);
//pseudo code
forEach(item in this.questionset){
this.getHTMLfromDB(item)
}
}
getHTMLfromDB(question: QuestionBank) {
this.Service.getQuestionHtmlFile(question.questionFilePath).subscribe(res =>
{
this.questionDataFromHtml.push(res.text());
question.questionData.questionDataFromHtml = res.text();
});
Correct display. Question1 and Question2 are same
Correct display
Incorrect display. Question1 and Question2 are different
Incorrect display
Stackblitz:
stackblitz
The issue is all the css styling is overridden and the final values are applied.
Use id/class to apply the style to specific component.
I've made changes to your stackblitz example. Check here
In hello.component.ts
Applied red color to the text using text-red id.
export class HelloComponent {
#Input() name: string;
html1 =
"<html><head><style> #text-blue {color:blue;}</style></head><body><h2 id='text-blue'>Inner HTML1 in red</h2></body></html>";
html2 =
"<html><head><style> #text-red {color:red;}</style></head><body><h2 id='text-red'>Inner HTML2 in blue</h2></body></html>";
}
I solved this issue by using iFrame tag and srcdoc attribute. The backend service will return html text to angular. After DOM sanitizing the html documents are displayed in the iFrames.

Angular/Typescript Text with routerLink

Updated Question for more Clarity:
Need to display some texts and links as innerHTML(data from service/DB) in the Angular HTML and when user clicks, it should go to Typescript and programmatically navigates by router.navigate
Also, How to add DomSanitizer from #ViewChild/ElementRef
Added all example in below code
Here is the updated stackblitz code
As shown in screenshot from angular.io some texts and some links
Sorry, I didn't realize you answered my comment. Angular routing is not secondary, if you don't use Angular modules you'll end up with just an HTML/CSS/Typescript application. you need at least the RouterModule for Angular to be able to use routing and hence, do what it's supposed to with the DOM.
First:
You are not importing RouterModule
solution:
imports: [
BrowserModule,
FormsModule,
RouterModule.forRoot([]) // this one
]
Second:
You can't bind Angular events through innerHTML property
fix:
Make use of #ViewChild directive to change your innerHTML property and manually bind to the click event, so change in your app.component.html from
<div id="box" [innerHTML]="shouldbedivcontent" ></div>
to
<div #box id="box"></div>
Now, in your app.component.ts, add a property to hold a reference to that "box" element so you can later make some changes to the dom with it:
#ViewChild('box') container: ElementRef;
Implement AfterViewInit, that hook is where you will be able to actually handle your container, if you try using it for example in OnInit you'd get undefined because that component's html is not in the dom yet.
export class AppComponent implements AfterViewInit {
and
ngAfterViewInit() {
this.container.nativeElement.innerHTML = this.shouldbedivcontent;
this.container.nativeElement.addEventListener('click',
() => this.goto('bar')
);
}
change shouldbedivcontent property from:
'1) this is a click
<a (click)="goto("bar")">Click</a><br>
2)this is with routerlink
<a routerLink="" (click)="goto("bar")">Click</a><br>
3)This only works with href
bar and test'
to
'1) this is a click
<a id="link_1">Click</a><br>
2)this is with routerlink
<a [routerLink]="" (click)="goto(\'bar\')">Click</a><br>
3)This only works with href
bar and test'
And even so you'd still not get the default anchor style unless you apply some styling yourself.
Third
You are not HTML sanitizing, which could be dangerous. read more here
MY SUGGESTION:
Seems like a lot to do for you and a lot to read for someone else working alongside you for something you could easily do like in the example below!
Move your html to your app.component.html:
<div id="box">
1) this is a click
<a (click)="goto('bar')">Click</a><br>
2)this is with routerlink
<a routerLink="" (click)="goto('bar')">Click</a><br>
3)This only works with href
bar and test
</div>
<p>Below is actual content</p>
You'll notice that everything works now, except the anchor without routerLink or href, because that's not a link.
EDIT:
Looking at the new stackblitz, i suggest a change of approach, binding to innerHTML is ok when working with plain text or even some simple html but not a great choice to bind events or routing logic.
Angular's Renderer2 provides with a bunch of methods to dyncamically add elements to the DOM. With that on the table, you just need a little effort to take that simple html you get from your backend and turn it into something like (paste this property in your code to test it along the rest of the code provided below):
public jsonHTML = [
{
tagName: '',
text: 'some text with click ',
attributes: {
}
},
{
tagName: 'a',
text: 'bar',
attributes: {
value: 'bar' // goto parameter
}
},
{
tagName: '',
text: ' some more text with click ',
attributes: {
}
},
{
tagName: 'a',
text: 'foo',
attributes: {
value: 'foo' // goto parameter
}
}
]
Once you have it, it's way easier to create all of those elements dynamically:
this is for the code in your Q1:
Inject Renderer2 with private r2: Renderer2
And replace the Q1 related code in AfterViewInit hook to:
const parent = this.r2.createElement('div'); // container div to our stuff
this.jsonHTML.forEach((element) => {
const attributes = Object.keys(element.attributes);
const el = element.tagName && this.r2.createElement(element.tagName);
const text = this.r2.createText(element.text);
if (!el) { // when there's no tag to create we just create text directly into the div.
this.r2.appendChild(
parent,
text
);
} else { // otherwise we create it inside <a></a>
this.r2.appendChild(
el,
text
);
this.r2.appendChild(
parent,
el
);
}
if (attributes.length > 0) {
attributes.forEach((name) => {
if (el) {
this.r2.setAttribute(el, name, element.attributes[name]); // just the value attribute for now
if (name === 'value') {
this.r2.listen(el, 'click', () => {
this.goto(element.attributes[name]); // event binding with property "value" as parameter to navigate to
})
}
} else {
throw new Error('no html tag specified as element...');
}
})
}
})
this.r2.appendChild(this.container.nativeElement, parent); // div added to the DOM
No html sanitizer needed and no need to use routerLink either just inject Router and navigate to the route you want! Make improvements to the code t make it fit your needs, it should be at least a good starting point
Good Luck!
You have a css problem.
looks like a link
<a [routerLink]="something"></a> looks like a link, because if you inspect the HTML it actually gets an href property added because of routerLink
<a (click)="goTo()"></a> does NOT look like a link, because there is no href
Chrome and Safari default user agents css will not style <a> without an href (haven't confirmed Firefox but I'm sure its likely). Same thing for frameworks like bootstrap.
Updated stackblitz with CSS moved to global, not app.css
https://stackblitz.com/edit/angular-ivy-kkgmkc?embed=1&file=src/styles.css
This will style all links as the default blue, or -webkit-link if that browser supports it. It should be in your global.css file if you want it to work through the whole app.
a {
color: rgb(0, 0, 238);
color: -webkit-link;
cursor: pointer;
text-decoration: underline;
}
this works perfectly for me :D
#Directive({
selector: "[linkify]",
})
// * Apply Angular Routing behavior, PreventDefault behavior
export class CustomLinkDirective {
#Input()
appStyle: boolean = true;
constructor(
private router: Router,
private ref: ElementRef,
#Inject(PLATFORM_ID) private platformId: Object
) {}
#HostListener("click", ["$event"])
onClick(e: any) {
e.preventDefault();
const href = e.target.getAttribute("href");
href && this.router.navigate([href]);
}
ngAfterViewInit() {
if (isPlatformBrowser(this.platformId)) {
this.ref.nativeElement.querySelectorAll("a").forEach((a: HTMLElement) => {
const href = a.getAttribute("href");
href &&
this.appStyle &&
a.classList.add("text-indigo-600", "hover:text-indigo-500");
});
}
}
}
HOW I USE IT
<p linkify
class="mt-3 text-lg text-gray-500 include-link"
[innerHtml]="apiSectionText"
></p>
result

When a ViewContainerRef is injected into a directive what element is it bound to?

When a ViewContainerRef is injected into a directive what element is it bound to?
For example if we have a template:
template `<div><span vcdirective></span></div>`
And the constructor for the vcdirective looks like this:
constructor(vc: ViewContainerRef) {
}
Is the element that the vc:ViewContainerRef is bound to the span element?
Yes, it will bind to span element. If you console the vc in constructor, you will see ViewContainerRef object, check the element property of it, you will find span there. But when you will attach the view in the container, it will be appended next to span instead of being inserted inside it and span will be left hanging.
Yes, it's bound to span. ViewContainerRef represents a container where one or more views can be attached. We can use its methods such as createEmbeddedView() and createComponent(). ViewContainerRef is used to build dynamic components.
please check this to better understand. it is a placeholder that can attach to another elemetent. check https://netbasal.com/angular-2-understanding-viewcontainerref-acc183f3b682
something like:
#Component({
selector: 'vcr',
template: `
<template #tpl>
<h1>ViewContainerRef</h1>
</template>
`,
})
export class VcrComponent {
#ViewChild('tpl') tpl;
constructor(private _vcr: ViewContainerRef) {
}
ngAfterViewInit() {
this._vcr.createEmbeddedView(this.tpl);
}
}

Dynamically ADDING and REMOVING Components in Angular

The current official docs only shows how to dynamically change components within an <ng-template> tag. https://angular.io/guide/dynamic-component-loader
What I want to achieve is, let's say I have 3 components: header, section, and footer with the following selectors:
<app-header>
<app-section>
<app-footer>
And then there are 6 buttons that will add or remove each component: Add Header, Add Section, and Add Footer
and when I click Add Header, the page will add <app-header> to the page that renders it, so the page will contain:
<app-header>
And then if I click Add Section twice, the page will now contain:
<app-header>
<app-section>
<app-section>
And if I click Add Footer, the page will now contain all these components:
<app-header>
<app-section>
<app-section>
<app-footer>
Is it possible to achieve this in Angular? Note that ngFor is not the solution I'm looking for, as it only allows to add the same components, not different components to a page.
EDIT: ngIf and ngFor is not the solution I'm looking for as the templates are already predetermined. What I am looking for is something like a stack of components or an array of components where we can add, remove, and change any index of the array easily.
EDIT 2: To make it more clear, let's have another example of why ngFor does not work. Let's say we have the following components:
<app-header>
<app-introduction>
<app-camera>
<app-editor>
<app-footer>
Now here comes a new component, <app-description>, which the user wants to insert in between and <app-editor>. ngFor works only if there is one same component that I want to loop over and over. But for different components, ngFor fails here.
What you're trying to achieve can be done by creating components dynamically using the ComponentFactoryResolver and then injecting them into a ViewContainerRef. One way to do this dynamically is by passing the class of the component as an argument of your function that will create and inject the component.
See example below:
import {
Component,
ComponentFactoryResolver, Type,
ViewChild,
ViewContainerRef
} from '#angular/core';
// Example component (can be any component e.g. app-header app-section)
import { DraggableComponent } from './components/draggable/draggable.component';
#Component({
selector: 'app-root',
template: `
<!-- Pass the component class as an argument to add and remove based on the component class -->
<button (click)="addComponent(draggableComponentClass)">Add</button>
<button (click)="removeComponent(draggableComponentClass)">Remove</button>
<div>
<!-- Use ng-template to ensure that the generated components end up in the right place -->
<ng-template #container>
</ng-template>
</div>
`
})
export class AppComponent {
#ViewChild('container', {read: ViewContainerRef}) container: ViewContainerRef;
// Keep track of list of generated components for removal purposes
components = [];
// Expose class so that it can be used in the template
draggableComponentClass = DraggableComponent;
constructor(private componentFactoryResolver: ComponentFactoryResolver) {
}
addComponent(componentClass: Type<any>) {
// Create component dynamically inside the ng-template
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(componentClass);
const component = this.container.createComponent(componentFactory);
// Push the component so that we can keep track of which components are created
this.components.push(component);
}
removeComponent(componentClass: Type<any>) {
// Find the component
const component = this.components.find((component) => component.instance instanceof componentClass);
const componentIndex = this.components.indexOf(component);
if (componentIndex !== -1) {
// Remove component from both view and array
this.container.remove(this.container.indexOf(component));
this.components.splice(componentIndex, 1);
}
}
}
Notes:
If you want to make it easier to remove the components later on, you can keep track of them in a local variable, see this.components. Alternatively you can loop over all the elements inside the ViewContainerRef.
You have to register your component as an entry component. In your module definition register your component as an entryComponent (entryComponents: [DraggableComponent]).
Running example:
https://plnkr.co/edit/mrXtE1ICw5yeIUke7wl5
For more information:
https://angular.io/guide/dynamic-component-loader
Angular v13 or above - simple way to add dynamic components to DOM
parent.component.html
<ng-template #viewContainerRef></ng-template>
parent.component.ts
#ViewChild("viewContainerRef", { read: ViewContainerRef }) vcr!: ViewContainerRef;
ref!: ComponentRef<YourChildComponent>
addChild() {
this.ref = this.vcr.createComponent(YourChildComponent)
}
removeChild() {
const index = this.vcr.indexOf(this.ref.hostView)
if (index != -1) this.vcr.remove(index)
}
Angular v12 or below
I have created a demo to show the dynamic add and remove process.
The parent component creates the child components dynamically and removes them.
Click for demo
Parent Component
// .ts
export class ParentComponent {
#ViewChild("viewContainerRef", { read: ViewContainerRef })
VCR: ViewContainerRef;
child_unique_key: number = 0;
componentsReferences = Array<ComponentRef<ChildComponent>>()
constructor(private CFR: ComponentFactoryResolver) {}
createComponent() {
let componentFactory = this.CFR.resolveComponentFactory(ChildComponent);
let childComponentRef = this.VCR.createComponent(componentFactory);
let childComponent = childComponentRef.instance;
childComponent.unique_key = ++this.child_unique_key;
childComponent.parentRef = this;
// add reference for newly created component
this.componentsReferences.push(childComponentRef);
}
remove(key: number) {
if (this.VCR.length < 1) return;
let componentRef = this.componentsReferences.filter(
x => x.instance.unique_key == key
)[0];
let vcrIndex: number = this.VCR.indexOf(componentRef as any);
// removing component from container
this.VCR.remove(vcrIndex);
// removing component from the list
this.componentsReferences = this.componentsReferences.filter(
x => x.instance.unique_key !== key
);
}
}
// .html
<button type="button" (click)="createComponent()">
I am Parent, Create Child
</button>
<div>
<ng-template #viewContainerRef></ng-template>
</div>
Child Component
// .ts
export class ChildComponent {
public unique_key: number;
public parentRef: ParentComponent;
constructor() {
}
remove_me() {
console.log(this.unique_key)
this.parentRef.remove(this.unique_key)
}
}
// .html
<button (click)="remove_me()">I am a Child {{unique_key}}, click to Remove</button>

Apply CSS style based on JSON value

I am trying to apply some CSS to a particular section containing a certain JSON value.
Lets say I want to add a red background to a text containing the words "main article".
I have managed to hide the value by using a pipe like below
#Pipe({
name: 'exclusionfilter',
pure: false
})
#Injectable()
export class ExclusionFilterPipe implements PipeTransform {
transform(value: any) {
if((value!=null)&& (value.toLowerCase().indexOf("main article") != -1)){
return '';
}
else return value;
}
}
and the markup is like this
<h2 class="heading">{{ info.title | exclusionfilter }}</h2>
I want to apply some CSS to the h2 whenever the value is "main article".
I dont think that this approach is clean, but cant think of alternatives
Try to use dynamic css/class binding of angular :
<h2 class="heading" [ngClass]='{"redBackground" : info?.title == "main article"}'>{{ info.title | exclusionfilter }}</h2>
where redBackground is an css class having background Red.
You have to use ElementRef in that case. Find element from DOM and apply required style as below example: First find element who has text you required and then apply style
this.el.nativeElement.getElementsByClassName("k-grouping-header")[0].style.display = "block";
If you know at DOM level where to add style call it as below:
HTML:
<div [ngStyle]="setStyles()">
Typescript:
private setStyles(): any {
let styles = {
'font-size': '10px'
};
return styles;
}