Angular: How to dynamically add components to parent Dashboard component? - html

I have two components. One is a dashboard of cards (created with ng generate #angular/material:material-dashboard --name=my-dashboard) where the cards are created like this:
<div class="grid-container">
<h1 class="mat-h1">Dashboard</h1>
<mat-grid-list cols="2" rowHeight="350px">
<mat-grid-tile *ngFor="let card of cards" [colspan]="card.cols" [rowspan]="card.rows">
<mat-card class="dashboard-card">
<mat-card-header>
<mat-card-title> {{card.title}}
<button mat-icon-button class="more-button" [matMenuTriggerFor]="menu" aria-label="Toggle menu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu" xPosition="before">
<button mat-menu-item>Expand</button>
<button mat-menu-item>Remove</button>
</mat-menu>
</mat-card-title>
</mat-card-header>
<mat-card-content class="dashboard-card-content">
<div>{{ card.content }}</div>
</mat-card-content>
</mat-card>
</mat-grid-tile>
</mat-grid-list>
</div>
And is filled like that:
export class DashComponent {
cards = [
{ title: 'Character', cols: 2, rows: 1, content: '<app-character-sheet></app-character-sheet>' },
{ title: 'Card 2', cols: 1, rows: 1 },
{ title: 'Card 3', cols: 1, rows: 2 },
{ title: 'Card 4', cols: 1, rows: 1 }
];
}
I want to fill the cards with different components. But when I went and tried it like you can see above (with the content variable), the HTML didn't parse the selector. The card just hat the literal content: .
How can I dynamically fill my cards with components?
Cheers

If you really need to dynamically insert components, read this article.
Alternative solution is to use structural directives like ngIf or ngSwitchCase for the case where you have limited set of content options:
<mat-card-content class="dashboard-card-content">
<div *ngIf="card.content"><app-character-sheet></app-character-sheet></div>
</mat-card-content>
You can unsafely insert HTML without post processing (but it is not recommended):
<div [innerHTML]="card.content"></div>

Related

Variabilizing the # from <mat-menu>

I'm currently trying to generate a dynamic menu from a JSON blob in my typescript files, Angular project.
I'm using the / component from Angular Material.
My json menu has this structure :
{
id: 0,
titreTranslate: 'menu-accueil',
isDescriptionRequired: true,
routerLink: '/accueil',
icon: faHome,
isAllowed: true,
hasSubOptions: false,
trigger: 'accueil'
}
My code look something like this :
<mat-toolbar-row class="navigation-row">
<span *ngFor="let option of menuOptions">
<button mat-button
[matMenuTriggerFor]="admin"
routerLink="{{option.routerLink}}"
(keyup.enter)="router.navigate([option.routerLink], { queryParams: option.queryParams })"
[routerLinkActive]="['active-menu']"
[queryParams]="option.queryParams"
class="link bouton-menu-gauche flex-row"
*ngIf="option.isAllowed"
>
<fa-icon
*ngIf="option.icon"
class="primary-color"
[icon]="option.icon"></fa-icon>
{{ option.titreTranslate | translate }}
</button>
<mat-menu #{{option.trigger}}="matMenu">
<span *ngFor="let subOption of option.menuOptions">
<button mat-menu-item
*ngIf="option.menuOptions"
routerLink="{{subOption.routerLink}}"
(keyup.enter)="router.navigate([subOption.routerLink], { queryParams: subOption.queryParams })"
[routerLinkActive]="['active-menu']"
[queryParams]="subOption.queryParams"
class="link bouton-menu-gauche flex-row">
<fa-icon
*ngIf="subOption.icon"
class="primary-color"
[icon]="subOption.icon"></fa-icon>
{{ subOption.titreTranslate | translate }}
</button>
</span>
</mat-menu>
</span>
</mat-toolbar-row>
The line with " <mat-menu #{{option.trigger}}="matMenu"> " is what concerns me here ; I've tried many ways to variabilize this #, such as putting it directly in the Json menu or trying different syntax ; It always fail, and won't give me the code structure i want.
If i had to guess, i'd say the generated code should look like : <mat-menu #"accueil"="matMenu">, with "" that fail to compile ; but i don't get any compilation errors, so i'm lost here.
Does anybody had to work with that kind of structure before ?
(apologies for my english if it's bad, i'm french)
Check out this answer:
https://stackoverflow.com/a/44441164/2025458
It's easier to search for the answer if you call things by the name they are given in the documentation, the '#' is a template reference or template variable
And since it's inside a structural directive (*ngfor or any other directive strating with *)
It binds to a template created by the structural directive, so every loop of the ngfor generates its own nested template with its own instance of the variable, so you can just use one name

Why mat-select clear the value in this reactive form?

I´m working on an app like google forms, we have a form with questions, we get these questions from an api and dynamically render the form. the problem is that when i want to add validation to the form with angular reactive forms, the mat-select stops working and every time i select a value, the select is clear.
I already try to adapt this https://medium.com/aubergine-solutions/add-push-and-remove-form-fields-dynamically-to-formarray-with-reactive-forms-in-angular-acf61b4a2afe with no luck
template
<mat-horizontal-stepper [linear]="true" #stepper style="background: transparent">
<mat-step *ngFor="let materia of materias;let m = index;" [stepControl]="createForm(materia.id)">
<ng-template matStepLabel>{{materia.descripcion}}</ng-template>
<form [formGroup]="getForm(materia.id)">
<div fxLayout="column" fxLayoutAlign="space-around stretch" fxLayoutGap="10px">
<div>
<mat-card *ngFor="let competencia of materia.competencias">
<mat-card-header>
<mat-card-title>{{competencia.nombre}}</mat-card-title>
</mat-card-header>
<mat-card-content>
<div fxLayout="column" fxLayoutGap="30px" formArrayName="{{createFormArray(materia.id,competencia.id)}}">
<div fxLayout="row">
<i>{{competencia.descripcion}}</i>
</div>
<div fxLayout="column" fxLayoutGap="2px">
<span *ngFor="let competenciaSimple of competencia.competenciasSimples; let i = index;" fxLayout="row">
<mat-form-field appearance="outline"
*ngIf="competenciaSimple !== null && competenciaSimple !== undefined" fxFlex>
<mat-select (selectionChange)="onSelected($event,competenciaSimple)"
[value]="getSelectedValue(competenciaSimple)" [formControl]="createControl(materia.id,competencia.id)" [id]="i">
<span *ngFor="let fase of competenciaSimple.fases">
<mat-option *ngFor="let conductaObservable of fase.conductasObservables"
[value]="conductaObservable">
{{conductaObservable.descripcion}}
</mat-option>
</span>
</mat-select>
</mat-form-field>
<!-- <input type="hidden" value="competenciaSimple" [(ngModel)]="calificacion.conductasObservables[i].competenciaSimple"/> -->
</span>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
<div fxLayout="row">
<button mat-flat-button type="button" matStepperPrevious *ngIf="!isFirstPage" color="primary"
fxFlex="20">ANTERIOR</button>
<span fxFlex="80"></span>
<button mat-flat-button type="button" *ngIf="!isLastPage" color="primary" fxFlex="20">SIGUIENTE</button>
<button mat-flat-button type="button" (click)="onSubmit()" *ngIf="isLastPage" color="accent"
fxFlex="20">TERMINAR</button>
</div>
</div>
</form>
</mat-step>
</mat-horizontal-stepper>
component.ts
forms = new Array<FormGroup>();
getForm(index: number): FormGroup {
return this.forms[index];
}
createForm(index: number): FormGroup {
const form = this.formBuilder.group({});
this.forms[index] = form;
return form;
}
createFormArray(index: number, name: string): string {
const array = this.formBuilder.array([]);
const arrayName = `competencia_${name}`;
this.forms[index].addControl(arrayName, array);
return arrayName;
}
createControl(index: number, arrayName: string): FormControl {
const control = this.formBuilder.control(false);
const an = `competencia_${arrayName}`;
const array = (this.forms[index].get(an));
(array as FormArray).push(control);
return control;
}
getControlId(): string {
this.controlNumber += 1;
return this.controlNumber.toString()
}
The idea is to validate the mat-select as required so the stepper cant advance when no value is selected, that works if a add the Validator, but the mat-select doesn´t persist the value so if i remove the [formControl]="createControl(materia.id,competencia.id)" the mat-select work but no validation result.
There are several popular open source Angular Form Libraries (you can take a look at the source code and see how they dynamically add validation's to a control):
GitHub: ng-dynamic-forms
GitHub: ngx-formly
GitHub: angular-schema-form (AngularJS) - Generate forms from a JSON schema
GitHub: ngx-schema-form - HTML form generation based on JSON Schema
GitHub: angular2-json-schema-form - Angular 2 JSON Schema Form builder

Angular 7 iterate json by button click

As I mentioned in title, I want to iterate by button click over my json response. See below my json file:
{
"id": 2,
"name": "Słownictwo",
"flashcardLists": [
{
"id": 17,
"frontside": "dasfasdv",
"backside": "csdascd"
},
{
"id": 18,
"frontside": "dsadsaad",
"backside": "sdasdadad"
},
{
"id": 19,
"frontside": "dasdsadd",
"backside": "sdaddsa"
}
]
}
FlashcardHTML:
<div class="flip-container" (click)="flip()" [class.flipped]="flipped" >
<mat-card-header></mat-card-header>
<mat-card-content *ngFor="let flashcard of flashcardLists" class="flipper">
<mat-card class="front">
{{flashcard.frontside}}
</mat-card>
<mat-card class="back">
{{flashcard.backside}}
</mat-card>
</mat-card-content>
</div>
I have component where in one time I want only one "frontside" and "backside".
Next, it will be replaced by button clicked which will increase counter but I don't know how to do that. I tried something like this flashcard[0].frontside, but it was KO. Maybe someone has encountered the same problem and can help me.
For all answers thanks in advance
If I understand properly, you wanna be flipping a single card.
Something like this should do:
<mat-card-content *ngFor="let flashcard of flashcardLists" class="flipper">
<mat-card [class.front]="!flashcards[i].isFlipped"
[class.back]="flashcards[i].isFlipped"
(click)="flashcards[i].isFlipped = !flashcards[i].isFlipped "
>
{{ flashcards[i].isFlipped ? flashcard.backside : flashcard.frontside }}
</mat-card>
</mat-card-content>
Just add this map of card states to y0our component:
flashcards: { [key: string]: boolean } = {};
You don't need an *ngFor in your mat-card-content, instead bind it to a flipCard variable, which will change on button iterate click. As for the front or back, use another variable isFront which you'll update on flip button click. something like this:
component.html:
<mat-card>
<mat-card-header></mat-card-header>
<mat-card-content class="flipper">
<div class="front" *ngIf="isFront">
{{flashcard.frontside}}
</div>
<div class="back" *ngIf="!isFront">
{{flashcard.backside}}
</div>
</mat-card-content>
<mat-card-actions>
<button (click)="iterate()" class="btn-flashcard" mat-button mat-raised-button id="previous">
Następna
</button>
<button mat-button (click)="flip()" [class.flipped]="flipped">LIKE</button>
<button mat-button>SHARE</button>
</mat-card-actions>
</mat-card>
component.ts:
flashcardList: any[] // use your flashCard class instead of any
flashcard: any; // use your flashCard class instead of any
isFront = true;
ngOnInit() {
// init first card
// flashcard = first card
}
iterate() {
// logic for getting the next card
// flashcard = next card
// e.g.
let index = this.flashcardList.indexOf(this.flashcard);
this.flashcard = this.flashcardList[index + 1];
}
flip() {
this.isFront = !this.isFront;
}

Angular 6: How to use an array in template syntax with ngFor?

I have two arrays: books and booksDisplay. In my template I use *ngFor to iterate through my booksDisplay array:
<div *ngFor="let bookDisplay of booksDisplay">
<div class="book">
<div class="title" *ngIf="???">{{ ???.title }}
</div>
</div>
But inside my *ngFor-Element I need to look up in my books-Array, where id is equal to the current bookId in my booksDisplay-Array. So how can I use something like a where clause inside my *ngFor-Element?
Something like this should work. However, I would better prepare data in a model instead of making such calculations in the template..
<div *ngFor="let bookDisplay of booksDisplay>
<div class="book">
<ng-container *ngIf="checkIfExist(bookDisplay.id)">
<div class="title">{{ books[bookDisplay.id] }}
</ng-container>
</div>
</div>
Template:
checkIfExist(id: number) {
return this.books.find( book => book['id'] === id )
}
Assuming your component.ts looks something like this:
export class AppComponent {
booksDisplay = [
{
id: 1,
title: 'a'
},
{
id: 2,
title: 'b'
},
{
id: 3,
title: 'c'
},
]
}
You can a counter to ngFor and use that value to filter on with ngIf conditional like so:
<div *ngFor="let book of booksDisplay; let i= index">
<div class="book">
<div class="title" *ngIf="i === book.id">{{ book.title }}
</div>
<p>{{i}}</p>
</div>
Like this; https://angular.io/guide/displaying-data
<ng-container *ngFor="let book of booksDisplay; let i = index">
<ng-container *ngIf="book.id === '1'">
<p> {{book.description}}</p>
</ng-container>

How to toggle mat-expansion-panel with button click?

Is there any way in which I can expand a particular mat-expansion-panel by clicking an external button?
I have tried linking to the ID of the panel, but with no success...
<mat-expansion-panel id="panel1"> ... </>
...
<button (click)="document.getElementById('panel1').toggle()>Click me</button>
Here is my stackblitz code for example
My eventual plan is to use this method to open different panels within a list generated from an array: <mat-expansion-panel *ngFor="let d of data"> ...
In your html file:
<mat-expansion-panel [expanded]="panelOpenState">
<mat-expansion-panel-header>
<mat-panel-title>
TITLE
</mat-panel-title>
</mat-expansion-panel-header>
<p>BODY</p>
</mat-expansion-panel>
<button mat-raised-button (click)="togglePanel">TOGGLE</button>
In your TS file:
panelOpenState: boolean = false;
togglePanel() {
this.panelOpenState = !this.panelOpenState
}
If you use *ngFor to generate the expansion panels:
<mat-expansion-panel [expanded]="isOpen" *ngFor="let d of data">
<mat-expansion-panel-header>
{{ d.header }}
</mat-expansion-panel-header>
<p>{{ d.content }}</p>
</mat-expansion-panel>
<button mat-raised-button (click)="togglePanel">TOGGLE</button>
If you press the button all of the expanded panels opens
simultaneously.
To open only one panel with one button, add a "expanded" property to the data array for each element like this:
data = [
{id:1, header:'HEADER 1', content:'CONTENT 1', expanded: false},
{id:2, header:'HEADER 2', content:'CONTENT 2', expanded: false},
{id:3, header:'HEADER 3', content:'CONTENT 3', expanded: false},
{id:4, header:'HEADER 4', content:'CONTENT 4', expanded: false},
]
Then in your template:
<mat-expansion-panel [(ngModel)]="d.expanded"
[expanded]="d.expanded" *ngFor="let d of data" ngDefaultControl>
<mat-expansion-panel-header>
<button (click)="toggle(d.expanded)">TOGGLE</button>
{{ d.header }}
</mat-expansion-panel-header>
<p>{{ d.content }}</p>
</mat-expansion-panel>
And the method raised by the button click:
toggle(expanded) {
expanded = !expanded;
}
<mat-expansion-panel [disabled]="true"
#mep="matExpansionPanel"
*ngFor="let foo of list">
<mat-expansion-panel-header>
<button (click)="mep.expanded = !mep.expanded">Toggle</button>
</mat-expansion-panel-header>
<p>Text</p>
</mat-expansion-panel>
Use two-way binding on the expanded attribute of mat-expansion-panel. Here is an example live in StackBlitz:
https://stackblitz.com/edit/angular-gtsqg8
<button (click)='xpandStatus=xpandStatus?false:true'>Toggle it</button>
<p>
<mat-expansion-panel [(expanded)]="xpandStatus">
<mat-expansion-panel-header>
<mat-panel-title>This an expansion panel</mat-panel-title>
<mat-panel-description>xpandStatus is {{xpandStatus}}</mat-panel-description>
</mat-expansion-panel-header>
Two-way binding on the expanded attribute gives us a way to store and manipulate the expansion status.
</mat-expansion-panel>
</p>
You can use the method toggle().
First give the element an id.
<mat-expansion-panel #matExpansionPanel>
Next, access the element from javascript. Import necessary libraries (MatExpansionPanel, ViewChild)
#ViewChild(MatExpansionPanel, {static: true}) matExpansionPanelElement: MatExpansionPanel;
Lastly, call the toggle function
this.matExpansionPanelElement.toggle(); //open(), close()
<mat-nav-list>
<mat-expansion-panel *ngFor="let row of rows" #mep="matExpansionPanel">
<mat-expansion-panel-header>
{{row}}
</mat-expansion-panel-header>
<h2>Test</h2>
<button (click)="mep.toggle()">Toggle</button>
</mat-expansion-panel>
</mat-nav-list>
Working Example:
https://stackblitz.com/edit/mat-expansion-panel-vymjsq?file=app%2Fexpansion-overview-example.html
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<mat-accordion displayMode="flat" multi class="mat-table">
<section matSort class="mat-elevation-z8 mat-header-row">
<span class="mat-header-cell" mat-sort-header="vesselName"></span>
<span class="mat-header-cell" mat-sort-header="vesselName">d</span>
</section>
<mat-expansion-panel [disabled]="true" #mep="matExpansionPanel"
*ngFor="let d of data">
<mat-expansion-panel-header>
<span class="mat-cell" (click)="mep.expanded = !mep.expanded">
<mat-icon class="icon" *ngIf="!mep.expanded">expand_more</mat-icon>
<mat-icon class="icon" *ngIf="mep.expanded">expand_less</mat-icon>
</span>
<span (click)="dClicked(d)" class="mat-cell">{{d.dataSet}}</span>
</mat-expansion-panel-header>
<div><pre>{{d | json}}</pre></div>
</mat-expansion-panel>
<div class="well" *ngIf="!d || d.length == 0">
<p>There are no d for this.</p>
</div>
</mat-accordion>
html:
<mat-accordion >
<mat-expansion-panel #first="matExpansionPanel">
<mat-expansion-panel-header>
<mat-panel-title>...</mat-panel-title>
</mat-expansion-panel-header>
...
</mat-expansion-panel>
<mat-expansion-panel #second="matExpansionPanel" expanded="true">
<mat-expansion-panel-header>
<mat-panel-title>...</mat-panel-title>
</mat-expansion-panel-header>
...
</mat-expansion-panel>
</mat-accordion>
<button (click)="doSomething(first, second)">Click</button>
ts:
import { Component } from '#angular/core';
import { MatExpansionPanel } from '#angular/material';
#Component({
selector: 'app-home',
templateUrl: './home.component.html'
})
export class HomeComponent {
doSomething(first: MatExpansionPanel, second: MatExpansionPanel) {
if (first.expanded ) { // check if first panel is expanded
first.close(); // close first panel
second.open(); // open second panel
// ...
}
}
}
Read more