Angular material tabs using ngComponentOutlet - javascript

I am using Angular Material Tabs to navigate different sections of a document. I've defined each tab in a TabItem class which looks like this:
class TabItem {
constructor(
public component: Type<any>,
public data: TabData,
public active: boolean
) {}
}
In the view, I loop through each TabItem and use *ngComponentOutlet to render the component of each TabItem.
<mat-tab-group>
<ng-container *ngFor="let tab of tabs">
<mat-tab>
<ng-template mat-tab-label>
<div class="mat-label-text" (click)="setActiveTab(tab)">{{ tab.data.label }}</div>
</ng-template>
<ng-container *ngComponentOutlet="tab.component"></ng-container>
</mat-tab>
</ng-container>
</mat-tab-group>
Everything works well... except I need access to the current TabItem in each of the resolved components to access its id, label, etc. The reason I am having trouble is because the examples online only show how to use ngComponentOutlet as a dynamic component. My components arent dynamic though... they are fixed, but created on the fly.
I dont know how I can use an injector since I am in a for loop... unless I create an injector for each individual item. I also dont want to subscribe to a service in every component... thats just ridiculous.
Here is a stackblitz of what I am trying to accomplish.

You can create a directive that will port desired data to your components:
data-provider.directive.ts
import { Directive, Input } from "#angular/core";
#Directive({
selector: '[dataProvider]'
})
export class DataProviderDirective {
#Input('dataProvider') data: any;
}
tabs.html
<ng-container *ngFor="let tab of tabs">
<mat-tab [dataProvider]="tab">
Now your dynamically generated component can read data from that directive:
tab-one.component.ts
import { Component, OnInit } from '#angular/core';
import { DataProviderDirective } from './data-provider.directive';
#Component({
selector: 'app-tab-one',
template: `
<p>I am tab one!</p>
<p>How can I access my respective TabItem?</p>
<pre>{{ dataProvider.data | json }}</pre>
`,
})
export class TabOneComponent implements OnInit {
constructor(public dataProvider: DataProviderDirective) { }
ngOnInit() {
console.log(this.dataProvider.data)
}
}
Forked Stackblitz

Related

How can I receive output from another Angular component?

Watched angular tutorial, and just followed the steps but now I'm struggling with #Output. I can't receive the value from another component in my home component.
I tried to use Output with EventEmitter but somehow it's not working.
Home component
<mat-drawer-container [autosize]="true" class="min-h-full max-w-7xl mx-auto">
<mat-drawer mode="side" opened class="p-6">
<app-filters (showCategory)="onShowCategory($event)"></app-filters>
</mat-drawer>
<mat-drawer-content class="p-6"
><app-products-header (columnsCountChange)="onColumnsCountChange($event)">{{
category
}}</app-products-header></mat-drawer-content
>
</mat-drawer-container>
Home Component TS
import { Component } from '#angular/core';
#Component({
selector: 'app-home',
templateUrl: `home.component.html`,
})
export class HomeComponent {
cols = 3;
category: string | undefined;
onColumnsCountChange(colsNum: number): void {
this.cols = colsNum;
}
onShowCategory(newCategory: string): void {
this.category = newCategory;
}
}
<mat-expansion-panel *ngIf="categories">
<mat-expansion-panel-header>
<mat-panel-title>CATEGORIES</mat-panel-title>
</mat-expansion-panel-header>
<mat-selection-list [multiple]="false">
<mat-list-option *ngFor="let category of categories" [value]="category"
><button (click)="onShowCategory(category)">
{{ category }}
</button></mat-list-option
>
</mat-selection-list>
</mat-expansion-panel>
Category Filter TS
#Component({
selector: 'app-filters',
templateUrl: 'filters.component.html',
})
export class FiltersComponent {
#Output() showCategory = new EventEmitter<string>();
categories = ['shoes', 'sports'];
onShowCategory(category: string): void {
this.showCategory.emit(category);
}
}
Here you are sending data from parent component (Home Component) to a child component (Category Filter) so you should use #input decorator instead of #output. Check this article https://angular.io/guide/inputs-outputs.
#Component({
selector: 'app-filters',
templateUrl: 'filters.component.html',
})
export class FiltersComponent {
#Input() showCategory = new EventEmitter<string>();
categories = ['shoes', 'sports'];
onShowCategory(category: string): void {
this.showCategory.emit(category);
}
}
Maybe try
this.showCategory.next(category); //instead of emit.
I just realized I did this in my application with Angular 14. I know before it I used emit method so not sur if there is a difference but your code look good.
Other thing: you want to display in an other child (app-products-header) component so I think you need to use
<ng-template></ng-template>
in the html child
But you can pass the value through #Input to app-products-header too and display it with interpolation like any other variables

How to Render Child Components Dynamically using loops if possible? (Angular 8)

I am trying to render my angular components dynamically using loops but I'm lost.
What I want to achieve is drag and re-arrange my components (I was able to achieve this using Dragula - ng2-dragula) but I want to save the arrangement to my local storage or session storage.
The tricky part is I am using child components
<section [dragula]="'VAMPIRES'">
<div class="first_element_class" id="first_element">
<my-first-component
[outputData]="outputData"></my-first-component>
</div>
<div class="second_element_class" id="second_element">
<my-second-component
[outputData]="outputData"></my-second-component>
</div>
</section>
I tried using the DOM sanitizer pipe to render them via for loops
import { Pipe, PipeTransform } from '#angular/core';
import { DomSanitizer, SafeHtml } from '#angular/platform-browser';
#Pipe({
name: 'trustHtml'
})
export class TrustHtmlPipe implements PipeTransform {
constructor(readonly sr: DomSanitizer){}
transform(html: string) : SafeHtml {
return this.sr.bypassSecurityTrustHtml(html);
}
}
Updated HTML Code (Let's assume I added my html elements to an array of object)
objectHTML: any[] = [{
toBeRender: `<div class="first_element_class" id="first_element">
<my-first-component
[outputData]="outputData"></my-first-component>
</div>`
}];
<section [dragula]="'VAMPIRES'">
<div *ngFor="let element of objectHTML">
<div [innerHTML]="element.toBeRender | trustHtml" ></div>
</div>
</section>
My question is is it really possible to render the html child components/elements using ngFor? I tried using the DOMSanitizer but I'm only getting a blank page (No error on console)
I have a similar solution for you. Firstly, in your <my-first-component> ts file declare "outputData" as #Input and use ngFor in HTML. Here is my sample code below.
CUSTOM Control
HTML: event.component.html
<h2>Event {{ event.id }}</h2>
<button (click)="event.hidden = true">Close me</button>
TS:event.component.ts
import { Component, Input } from '#angular/core';
import { Event } from './event';
#Component({
selector: 'my-event',
templateUrl: './event.component.html'
})
export class EventComponent {
#Input() event: Event;
}
export interface Event
{
id: number;
hidden: boolean;
}
**HTML:** app.component.html (You can use that code where you need to.)
<button (click)="addEvent()">Add event</button>
<div *ngFor="let event of events">
<my-event *ngIf="!event.hidden" [event]="event"></my-event>
</div>
Note: Here [event] is the #Input for your custom control and *ngFor is the loop.
TS: app.component.ts
import { Component } from '#angular/core';
import { Event } from './event';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
events: Array<Event> = [1, 2, 3, 4, 5].map(i => ({id: i, hidden: true}));
addEvent() {
const event = this.events.find(e => e.hidden);
if (event) {
event.hidden = false;
}
}
}
Note: Please check the code and let me know. This code is also available in Stackblitz
LINK .
You can iterate with ngFor and render many times the child component by his selector.
components = [1, 2, 3, 4, 5];
<ng-container *ngFor="let component of components">
<child-component></child-component>
</ng-container>

How Can I Make Custom Tabs Using Angular Materials Tabs?

I want to use Angular Material library and build my own library with some custom designs. But facing some problems while splitting the material components. I think the problem is with shadow DOM. Here is the code that i want to achieve.
Code
custom-tabs-group.html -parent
<div class="custom-tabs">
<mat-tab-group disableRipple>
<ng-content></ng-content>
</mat-tab-group>
</div>
custom-tabs.html -child
<custom-tabs-group [tabContent]="tabContent">
<mat-tab *ngFor="let tab of tabContent" label="{{tab.title}}">{{tab.content}} </mat-tab>
</custom-tabs-group>
is it even possible? Please let me know
the code you shared got the ng-content usage backwards... the <custom-tabs-group> will be at the parent level and <ng-content> at the child level.
I tried 2 approaches:
strategy #1: pass the content to the custom child inside the <mat-tab>... this worked
strategy #2: pass the content to the custom child where <mat-tab> is inside the child... this didn't work
you can check the demo here
Actually i figured it out with some hack i don't know if its a good approch or not
custom-tabs.component.html
<div class="custom-tabs">
<mat-tab-group disableRipple>
<mat-tab *ngFor="let tab of tabsContentList" label="{{tab.label}}">
<div [innerHTML]="tab.htmlContent"></div>
</mat-tab>
</mat-tab-group>
</div>
custom-tabs-component.ts
import { DomSanitizer } from '#angular/platform-browser';
import { Component, OnInit, ViewEncapsulation, AfterContentInit, ContentChildren, Input, ViewChild, ElementRef, QueryList } from '#angular/core';
#Component({
selector: 'il-tabs-content',
template: `
<div #content>
<ng-content></ng-content>
</div>
`
,
})
export class TabsContentComponent implements OnInit {
#Input() label: String;
#ViewChild('content') set content(content: ElementRef) {
console.log("block three", content)
this.htmlContent = content;
if (this.htmlContent) {
this.htmlContent = this.htmlContent.nativeElement.innerHTML;
}
}
htmlContent: any;
constructor() { }
ngOnInit() {
}
}
#Component({
selector: 'il-tabs-group',
templateUrl: './tabs.component.html',
styleUrls: ['./tabs.component.css'],
encapsulation: ViewEncapsulation.None
})
export class TabsGroupComponent implements OnInit, AfterContentInit {
#ContentChildren(TabsContentComponent) tabsContentList: QueryList<TabsContentComponent>;
constructor(public sanitizer: DomSanitizer) { }
ngOnInit() {
}
ngAfterContentInit() {
this.tabsContentList.forEach((tabInstance) => {
var sanEle = this.sanitizer.bypassSecurityTrustHtml(tabInstance.htmlContent)
tabInstance.htmlContent = sanEle;
return tabInstance
})
}
}
usage
<il-tabs-group>
<il-tabs-content label="hello-1">
<h1>hello-1 content</h1>
</il-tabs-content>
<il-tabs-content label="hello-2">
<h1>hello-2 content</h1>
</il-tabs-content>
<il-tabs-content label="hello-3">
<h1>hello-3 content</h1>
<h2>extra content</h2>
</il-tabs-content>
</il-tabs-group>
i defined two components 'il-tabs-content' and 'li-tabs-group'. with this now i can use my own custom tabs build over angular material tabing with dynamic tabs. Anyone with better approch are welcome to share their ideas. thanks

Angular: pass single object within ngFor to router linked component

How do I pass the data binded information within the <a> tag (Within the volume-links.component.html ) to my page-view component when the link is clicked.
I want to pass that particular diary object to my page-view.
I've looked into parent and child component interaction but I don't think that is the proper way to do it. I've looked into communicating via a service but I do not know how that would work for a problem such as this.
volume-links.component.html
<ul class="navigation">
<li *ngFor="let d of diary">
<a id={{d.notebook_id}} routerLink="/page-view" routerLinkActive="active">Volume {{d.notebook_id}}, {{ d.date }}, {{ d.volume_id }}, Add MS {{ d.ms_id }}</a>
</li>
</ul>
volume-links.component.ts
import { Component, OnInit } from '#angular/core';
import { Http } from '#angular/http';
import { HttpClient, HttpClientModule } from '#angular/common/http';
import 'rxjs/add/operator/map'
#Component({
selector: 'app-volume-links',
templateUrl: './volume-links.component.html',
styleUrls: ['./volume-links.component.scss'],
//encapsulation: ViewEncapsulation.None
})
export class VolumeLinksComponent implements OnInit {
diary : String;
constructor(private http: HttpClient) { }
ngOnInit() {
this.http.get('/api/diaries').subscribe(data => {
this.diary = data["data"]["docs"];
console.log(this.diary);
})
}
}
You want to look at https://angular.io/guide/component-interaction
There are several methods / ways to achieve this and depending on your use case choose one.
I would define an Input property in VolumeLinksComponent and pass the diary object in there (that's the first part "Pass data from parent to child with input binding").
This would look something like:
<a *ngFor='let diary of diaries' (click)='chooseDiary(diary)'>
<my-diary-container [diary]='selectedDiary'></my-diary-container>
and that parent component of course needs a property 'selectedDiary' and a method:
chooseDiary(diary: Diary): void {
this.selectedDiary = diary;
}
But in your provided case it seems like you just need the specific id since you want to retrieve details from the api? In that case you could just define a route with the id and when the route is accessed ask an additional DiaryService to retrieve what you need.

*ngFor on elements of ng-content Angular

I was wondering if is it possible to create a iteration of certain elements of . I need a different css class to every element of the ng-content so i need to make a loop of every element of ng-content. Is it possible?
Right now i pass a parameter to the child element to enumerate him, but i would like to do it without the number. This is my code:
<sys-tab [tabs]="['Principal', 'Complementar']">
<sys-tab-content [num]="1">
<sys-panel header="Dados Gerais">
<sys-input-text header="Nome" tam="1"></sys-input-text>
<sys-input-mask header="CNPJ"></sys-input-mask>
<sys-input-mask header="CNES"></sys-input-mask>
<sys-input-mask header="Telefone"></sys-input-mask>
<sys-input-text header="Email"></sys-input-text>
</sys-panel>
</sys-tab-content>
<sys-tab-content [num]="2">
<sys-input-text header="Email"></sys-input-text>
</sys-tab-content>
</sys-tab>
As you can see, to the child i passs the number so i can recognize who is him, but i want to create a loop to the ng-coontent so i can add a different class to every "sys-tab-content"
Simple List Example with dynamic ngTemplate
app.component.ts
import { Component } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html'
})
export class AppComponent {
items = [ { name: 'abc' }, { name: 'cdf' }, { name: 'fgh' } ];
}
app.component.html
<nu-list [itemTpl]="itemTpl" [items]="items"></nu-list>
<ng-template let-item #itemTpl>
<h1>{{item.name}}</h1>
</ng-template>
list.component.ts
import { Component, Input } from '#angular/core';
#Component({
selector: 'nu-list',
templateUrl: './list.component.html'
})
export class ListComponent {
#Input() itemTpl;
#Input() items;
}
list.component.html
<ng-container *ngFor="let item of items">
<ng-container *ngTemplateOutlet="itemTpl; context: {$implicit: item}"></ng-container>
</ng-container>
Example Link: https://stackblitz.com/edit/angular-list-ngtemplateoutlet
There's two ways of adding a "ngFor" or iterate trough the childs.
One is by transclusion: and the other one is by checking the ViewChildren of the parent component.

Categories

Resources