How does angular2 distinguish between different <ng-template>? - javascript

I am writing a table component, usually the contents of the table are fixed, but some columns may require component users to create a column template, I am prepared to pass the to pass the custom column template, but I encountered a problem, I did not know which columns I needed to customize when I passed the column data.
Now I have simplified the code as follows:
#Component({
selector: 'example',
template: `
<ng-container *ngFor="let item of data">
<div *ngIf="!item.value">
<ng-template [ngTemplateOutlet]="item.ref" [ngOutletContext]="{}"></ng-template>
</div>
<div *ngIf="item.value">{{item.value}}</div>
</ng-container>
`
})
export class DatatableComponent{
#Input() data: object[];
#ContentChildren(TemplateRef) get refs(val: TemplateRef<any>) {
// How to map refs here to map above
}
}
// use it:
#Component({
selector: 'app',
template: `
<example [data]="data">
<ng-template name="test1">test1</ng-template>
<ng-template name="test2">test2</ng-template>
</example>
`
})
export class DatatableComponent{
// I know that you can get references here
// But this hard code is too cumbersome,
// I was wondering if there was any other better way
public data: object[] = [
{name: 'test1'},
{name: 'test2'},
{name: 'test3', value: 'test3'}
]
}

Related

How to manipulate limited objects from an array of object using typescript/javascript

I have an array of object, I want to manipulate only 2 out of 3 object, When I am using slice, its gives some error(cannot read the property slice of undefined..) in my project, Here working fine. So I want to filter from typescript only and populate in ngfor. Here is the code below
app.component.html
<hello name="{{ name }}"></hello>
<p>
Start editing to see some magic happen :)
</p>
<div *ngFor="let data of json.slice(0,02); let i=index">{{data.text}}</div>
app.component.ts
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
name = 'Angular';
json: any;
ngOnInit(): void {
this.json = [
{
id: 1,
text: 'one'
},
{
id: 2,
text: 'two'
},
{
id: 3,
text: 'three'
}
];
}
}
In Angular, Always use Angular Pipes.
Codesandbox Example Here...
<div *ngFor="let data of json | slice:0:2; let i=index">{{data.text}}</div>

does angular content projection inside *ngFor share same data? How to access project content of child component from parent?

currently I trying to project a third component in a child component which is projected inside ngFor loop (inside child), but in parent whenever I change or set some property in the projected content using index of query list (ViewChildren('#thirdComponent')) in parent all the child's projected content shows same change. Is there any proper way of doing this.
Is it due to duplicating of select property binding at the place of content projection in child component.Child's projection is done inside a accordion with one or many panels opened at a time.
#Component({
selector: "my-app",
template: `
<child-comp #child>
<ng-container selected>
<some-other-comp #someOtherComp></some-other-comp>
</ng-container>
</child-comp>
`,
styleUrls: ["./app.component.css"]
})
export class AppComponent implements AfterViewInit {
h = 0;
i = 1;
j = 2;
k = 3;
#ViewChildren("someOtherComp") otherCompList: QueryList<SomeOtherComponent>;
ngAfterViewInit(): void {
this.otherCompList.toArray()[this.h].prop = this.h;
// below will result in undefined due to QueryList of size 1
// this.otherCompList.toArray()[this.i].prop = this.i;
// this.otherCompList.toArray()[this.j].prop = this.j;
// this.otherCompList.toArray()[this.k].prop = this.k;
}
}
#Component({
selector: "child-comp",
template: `
<div *ngFor="let value of [1, 2, 3]; let i = index">
<!-- if ngIf is removed than only the last projection is diplayed -->
<div *ngIf="i === 0">
<ng-content select="[selected]"> </ng-content>
</div>
</div>
`,
styleUrls: ["./app.component.css"]
})
export class ChildComponent {}
#Component({
selector: "some-other-comp",
template: `
<p>{{ prop }}</p>
`,
styleUrls: ["./app.component.css"]
})
export class SomeOtherComponent {
prop: any;
}
Stackblitz
Utilizing *ngTemplateOutlet and let-variables
We can pass along a template into our child-component, and utilize the #Input() decorator in conjunction with *ngTemplateOutlet to directly access the property from the HTML template in the parent.
Example
First, I've defined an array in my parent component which I want to use as the basis for my loop in my outer-child component.
Parent Component
#Component({
selector: 'parent',
templateUrl: 'parent.component.html',
styleUrls: ['parent.component.scss']
})
export class ParentComponent implements OnInit {
dataItems: { title: string, description: string }[] = [{
title: 'First Element',
description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eveniet, nihil!'
}...] // remaining items truncated for brevity.
constructor() {
}
ngOnInit(): void {
}
}
This parent component then has a child component, which takes an input of the entire list of items
<child [items]="dataItems"></child>
Child-Component (fist level)
#Component({
selector: 'child',
templateUrl: 'child.component.html',
styleUrls: ['child.component.scss']
})
export class ChildComponent implements OnInit {
#Input() items!: any[];
constructor() {
}
ngOnInit(): void {
}
}
<ng-container *ngFor="let childItem of items">
<projected [item]="childItem">
<ng-template let-item>
<h4>{{item.title}}</h4>
<p>{{item.description}}</p>
</ng-template>
</projected>
</ng-container>
Projected component (sub-child)
#Component({
selector: 'projected',
templateUrl: 'projected.component.html',
styleUrls: ['projected.component.scss']
})
export class ProjectedComponent implements OnInit {
#Input() item: any;
#ContentChild(TemplateRef) templateOutlet!: TemplateRef<any>
constructor() {
}
ngOnInit(): void {
}
}
<ng-container *ngTemplateOutlet="templateOutlet; context: {$implicit: item}"></ng-container>
<ng-content></ng-content>
How does it work
The Parent Component isn't strictly necessary in this relationship, as we aren't projecting content directly from the parent into the ProjectedComponent, I simply chose to define a list of items here to keep a hierarchy similar to your question.
The Child Component
The child component does two things:
Defines a *ngFor loop to loop thru some collection of elements.
Defines a template for how these elements should be utilized in the ProjectedComponent's template.
In the ProjectedComponent we utilize the #ContentChild decorator to select the TemplateRef which we expect to be given via <ng-content>
This template is then put into a container using the *ngTemplateOutlet which also allows us to create a data-binding context to a local variable.
the context: {$implicit: item} tells Angular that any let-* variable defined on the template without any explicit binding should bind to the item property in our component.
Thus, we are able to reference this property in the template at the parent-component level.
Edit
Technically, the context binding is not necessary if you want to define the template directly inside of the child component, as you have a direct reference to the *ngFor template, however it becomes necessary if you want to lift the template out to the ParentComponent level to make the solution more reusable.
You are correct the reason for the bug (changing just the last element) is because when rendered you have multiple elements with the same select value.
A possible solution is to use template reference to pass the desired child component from the top level to the place where you want it to be projected.
Here is a working StackBlitz
import {
AfterViewInit,
Component,
Input,
QueryList,
ViewChildren
} from "#angular/core";
#Component({
selector: "my-app",
template: `
<child-comp #child [templateRef]="templateRef"> </child-comp>
<ng-template #templateRef>
<some-other-comp #someOtherComp></some-other-comp>
</ng-template>
`,
styleUrls: ["./app.component.css"]
})
export class AppComponent implements AfterViewInit {
h = 0;
i = 1;
j = 2;
k = 3;
#ViewChildren("someOtherComp") otherCompList: QueryList<SomeOtherComponent>;
ngAfterViewInit(): void {
this.otherCompList.toArray()[this.h].prop = this.h;
this.otherCompList.toArray()[this.i].prop = this.i;
this.otherCompList.toArray()[this.j].prop = this.j;
this.otherCompList.toArray()[this.k].prop = this.k;
}
}
#Component({
selector: "child-comp",
template: `
<div *ngFor="let value of [1, 2, 3, 4]; let i = index">
<!-- if ngIf is removed than only the last projection is diplayed -->
<ng-container *ngTemplateOutlet="templateRef"></ng-container>
</div>
`,
styleUrls: ["./app.component.css"]
})
export class ChildComponent {
#Input() templateRef;
}
#Component({
selector: "some-other-comp",
template: `
<p>{{ prop }}</p>
`,
styleUrls: ["./app.component.css"]
})
export class SomeOtherComponent {
prop: any;
}

Using dynamic component within Angular structural directive produces extra HTML tag. How to remove or replace it?

I have quite complex infrastructure in my project which contains of
host component
structural directive used in host component's template (MyDir)
another component used in structural directive (MyComp)
Simplified version looks like the following.
host component
#Component({
selector: 'my-app',
template: `
<table>
<tr *myDir="let item of data">
<td>{{item.text}}</td>
</tr>
</table>`
})
export class AppComponent {
data = [ { text: 'item 1' }, { text: 'item 2' } ];
}
structural directive
import { MyComp } from './myComp';
#Directive({ selector: '[myDir][myDirOf]' })
export class MyDir implements OnInit {
private data: any;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
private resolver: ComponentFactoryResolver
) {
}
#Input() set myDirOf(data: any) {
this.data = data;
}
ngOnInit() {
const templateView = this.templateRef.createEmbeddedView({});
const compFactory = this.resolver.resolveComponentFactory(MyComp);
const componentRef = this.viewContainer.createComponent(
compFactory, undefined, this.viewContainer.injector, [templateView.rootNodes]
);
componentRef.instance.data = this.data;
componentRef.instance.template = this.templateRef;
}
}
structural directive's component
#Component({
selector: '[my-comp]',
template: `
<tr><td>custom td</td></tr>
<ng-template *ngFor="let item of data"
[ngTemplateOutlet]="template"
[ngTemplateOutletContext]="{ $implicit: item }"
></ng-template>`
})
export class MyComp {
public template: TemplateRef<any>;
public data: any;
}
The output is
custom td
item 1
item 2
which is fine except the markup which is
<table>
<div my-comp>
<tr><td>custom td</td></tr>
<tr><td>item 1</td></tr>
<tr><td>item 2</td></tr>
</div>
</table>
Problem
I want to remove intermediate <div my-comp> from the result view or at least replace it with <tbody>. To see the whole picture I prepared Stackblitz DEMO, hope it will help... Also, it might be obvious the example is artificial, but this is what I came with trying to reproduce the issue with minimal code. So the problem should have a solution in the given infrastructure.
Update
#AlexG found simple way to replace intermediate div with tbody and stackblitz demo showed a good result at first. But when I tried to apply it to my project locally I faced new issue: browser arranges its own tbody before the dynamic contents of the table are ready to render, which results in two nested tbody in the end view, which seems inconsistent per html specs
<table>
<tbody>
<tbody my-comp>
<tr><td>custom td</td></tr>
<tr><td>item 1</td></tr>
<tr><td>item 2</td></tr>
</tbody>
</tbody>
</table>
Stackblitz demo has no such problem, only tbody my-comp is present. But exactly the same project in my local dev environment does. So I'm still trying to find a way how to remove intermediate my-comp container.
Update 2
The demo had been updated in accordance with the solution suggested by #markdBC.
My answer is inspired by Slim's answer to a similar question found here: https://stackoverflow.com/a/56887630/12962012.
You can remove the intermediate <div my-comp> by
Creating a TemplateRef object representing the template of the MyComp component inside the MyComp component.
Accessing this TemplateRef object from the structural directive.
Creating an embedded view inside the view container with the TemplateRef object from the structural directive.
The resulting code looks something like:
MyComp component
#Component({
selector: "[my-comp]",
template: `
<ng-template #mytemplate>
<tr>
<td>custom td</td>
</tr>
<ng-template
*ngFor="let item of data"
[ngTemplateOutlet]="template"
[ngTemplateOutletContext]="{ $implicit: item }"
></ng-template>
</ng-template>
`
})
export class MyComp {
public template: TemplateRef<any>;
public data: any;
#ViewChild("mytemplate", { static: true }) mytemplate: TemplateRef<any>;
}
MyDir directive
export class MyDir implements OnInit {
private version: string;
private data: any;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
private resolver: ComponentFactoryResolver
) {}
#Input() set myDirOf(data: any) {
this.data = data;
}
ngOnInit() {
const compFactory = this.resolver.resolveComponentFactory(MyComp);
const componentRef = compFactory.create(this.viewContainer.injector);
componentRef.instance.data = this.data;
componentRef.instance.template = this.templateRef;
this.viewContainer.createEmbeddedView(componentRef.instance.mytemplate);
}
}
The resulting HTML looks something like:
<table>
<tr><td>custom td</td></tr>
<tr><td>item 1</td></tr>
<tr><td>item 2</td></tr>
</table>
I've prepared a StackBlitz demo at https://stackblitz.com/edit/table-no-div-wrapper.
Change the selector of your component from [my-comp] to tbody [my-comp] and your will have a <tbody my-comp> instead of a <div my-comp> which would be sufficient if I understood you correctly.

Dynamically nested angular material menu

Please tell me how I can solve the following problem:
I need to implement a dynamically created menu with different nesting levels depending on the data model object. At the moment, using recursion, we managed to create the menu as such, however, there is a problem of assigning the attribute [matMenuTriggerFor] for, directly, the submenu. The problem is that all subsequent submenus in fact refer to the very first, so when you hover over any of the submenus, it causes a "flip" to the original one (example on image: menu, which includes elements: Device, Extension, Queue, Queue member (with submenu elements)). Thus, for a fraction of seconds, I see the other submenu frame (example on image: submenu Grouped list), after which the very first becomes active. Of course, maybe I didn’t do everything right, so I’m turning here. Help me please. Thank you all.
imenu-item.ts
export interface IMenuItem {
name: string | string[];
link: string;
subItems: IMenuItem[];
}
dynamic-menu.service.ts
import {Inject, Injectable} from '#angular/core';
import {APP_CONFIG_ROUTES} from '../../../config/routes/app.config.routes';
import {IAppConfigRoutes} from '../../../config/routes/iapp.config.routes';
import {IMenuItem} from './imenu-item';
import {_} from '#biesbjerg/ngx-translate-extract/dist/utils/utils';
#Injectable({
providedIn: 'root'
})
export class DynamicMenuService {
private readonly appConfig: any;
constructor(#Inject(APP_CONFIG_ROUTES) appConfig: IAppConfigRoutes) {
this.appConfig = appConfig;
}
getMenuItems(): IMenuItem[] {
return [
{
name: _('labels.device'),
link: '/' + this.appConfig.routes.device,
subItems: null
},
{
name: _('labels.extension'),
link: '/' + this.appConfig.routes.extension,
subItems: null
},
{
name: _('labels.queue'),
link: '/' + this.appConfig.routes.queue,
subItems: null
},
{
name: _('labels.queueMember'),
link: null,
subItems: [{
name: _('labels.fullList'),
link: '/' + this.appConfig.routes.queueMember.all,
subItems: null
}, {
name: _('labels.groupedList'),
link: '/' + this.appConfig.routes.queueMember.grouped,
subItems: [{
name: 'subName',
link: 'subLink',
subItems: [{
name: 'subSubName1',
link: 'subSubLink1',
subItems: null
}, {
name: 'subSubName2',
link: 'subSubLink2',
subItems: null
}]
}]
}]
}
];
}
}
dynamic-menu.component.ts
import {Component, Input, OnInit} from '#angular/core';
import {IMenuItem} from './imenu-item';
#Component({
selector: 'app-dynamic-menu',
templateUrl: './dynamic-menu.component.html',
styleUrls: ['./dynamic-menu.component.scss']
})
export class DynamicMenuComponent implements OnInit {
dynamicMenuItemsData: IMenuItem[];
constructor(private dynamicMenuService: DynamicMenuService) {
}
ngOnInit() {
this.dynamicMenuItemsData = this.dynamicMenuService.getMenuItems();
}
}
dynamic-menu.component.html
<div>
<ng-container [ngTemplateOutlet]="recursiveListMenuItems"
[ngTemplateOutletContext]="{$implicit: dynamicMenuItemsData}">
</ng-container>
</div>
<ng-template #recursiveListMenuItems let-listMenuItems>
<div *ngFor="let menuItem of listMenuItems">
<ng-container [ngTemplateOutlet]="menuItem.subItems != null ? subMenuItem : simpleMenuItem"
[ngTemplateOutletContext]="{$implicit: menuItem}">
</ng-container>
</div>
</ng-template>
<ng-template #simpleMenuItem let-menuItemArg>
<a class="mat-button"
mat-menu-item
routerLink="{{menuItemArg.link}}">
<span>{{menuItemArg.name | translate}}</span>
</a>
</ng-template>
<ng-template #subMenuItem let-menuItemArg>
<a class="mat-button"
mat-menu-item
routerLink="{{menuItemArg.link}}"
[matMenuTriggerFor]="subItemsMenu">
<span>{{menuItemArg.name | translate}}</span>
<mat-menu #subItemsMenu="matMenu"
[overlapTrigger]="false">
<ng-container [ngTemplateOutlet]="recursiveListMenuItems"
[ngTemplateOutletContext]="{$implicit: menuItemArg.subItems}">
</ng-container>
</mat-menu>
</a>
</ng-template>
As a result, it turned out, relying on several similar problems with others. The examples from HERE (dynamic nested menu example) and from HERE (the problem with mat-menu hides immediately on opening) helped to figure it out (in the last example it was enough just to update zone.js by npm)
Sorry for the late answer, but maybe you can still find it helpful.
I wrote a little library called ng-action-outlet that is doing that quite neatly in my opinion.
It looks like this:
group: ActionGroup;
constructor(private actionOutlet: ActionOutletFactory) {
this.group = this.actionOutlet.createGroup();
this.group.createButton().setIcon('home').fire$.subscribe(this.callback);
this.group.createButton().setIcon('settings').fire$.subscribe(this.callback);
}
<ng-container *actionOutlet="group"></ng-container>
DEMO: https://stackblitz.com/edit/ng-action-outlet-demo?file=src/app/app.component.ts

Pass transcluded content to grandchild component of a nested list

I know there are a few questions similar to this one but they aren't quite the same. I'm building a nested list and I want to display a custom html content in each grandchild along side common html. When I add the to ListComponent outside of the loop works, but if I pass it inside the loop to the inner child, it doesn't work like the example bellow. The html I pass in the in the code bellow isn't shown. I'm probably trying to solve this the wrong way but I couldn't get it to work any way I tried. Any of you guys know how to make this work?
Thanks!
export class Model {
title: string;
children?: Model[] = [];
}
#Component({
selector: 'list',
template: `
<ul>
<li *ngFor="let item of items">
<list-item [item]="item">
<div main-content>
<ng-content select="[main-content]"></ng-content>
</div>
</list-item>
<list [items]="item.children"></list>
</li>
</ul>
`
})
export class List {
#Input() items: Model[];
}
#Component({
selector: 'list-item',
template: `
<h1>{‌{ item.title }}</h1>
<div class="content">
<ng-content select="[main-content]"></ng-content>
</div>
`
})
export class ListItem {
#Input() item: Model;
}
#Component({
selector: 'app-main',
template: `
<list [items]="items">
<div main-content>
<h1>Test</h1>
</div>
</list>
`
})
export class AppMainComponent {
}
After much testing and going further through the duplicate question that was mentioned and its inner-links, it doesn't quite solve my issue, because the template I'm not trying to duplicate the content as it is in plain. I'm trying to inject into another component with other common html inside it, so if I just iterate through I'll just replicate the the template that I'm passing but I'll lose all other html that is inside.
I know it's a little too late but I've recently encountered a similar problem and an answer here would have helped me.
From what I understood you wish to define the template for your list items in the component in which you are using the list.
To do that you need to use ng-template in both the child component(the list) and also in the grandchild(the item).
The grandchild should be something like this:
#Component({
selector: 'app-item',
template:`
<ng-container *ngTemplateOutlet="itemTemplate; context: {$implicit: data}">
</ng-container>
`
})
export class ItemComponent {
#Input() public data;
#ContentChild('itemTemplate') public itemTemplate;
}
And the child:
#Component({
selector: 'app-list',
template:`
<app-item [data]="dataItem" *ngFor="let dataItem of data">
<ng-template #itemTemplate let-item>
<ng-container *ngTemplateOutlet="itemTemplate1; context: {$implicit: item}">
</ng-container>
</ng-template>
</app-item>
`
})
export class ListComponent {
#Input() public data;
#ContentChild('itemTemplate') public itemTemplate1;
}
And the component that uses the list:
<app-list [data]="data">
<ng-template #itemTemplate let-item>
<h1>--{{ item?.value}}--</h1>
</ng-template>
</app-list>
In the item you use an ng-template received as content and pass it the input as context. Inside this template you can define how the item will look inside of your list and though the context you have access to the data of that item. Because you can't just skip a level, you must also expect an ng-template in the list. Inside the list you define the ng-template for the item and inside that ng-template you are just using the ng-template received from above, through an ng-container. And finally at the highest level you can simply define the template for the item.

Categories