Best practice for abstracting Angular HTML code? - html

In my Angular project I have a basic component
TS
#Component({
selector: 'app-cardboxes',
templateUrl: './cardboxes.component.html',
styleUrls: ['./cardboxes.component.scss']
})
export class CardboxesComponent implements OnInit {
constructor(public dialog: MatDialog) { }
ngOnInit() {}
}
In the HTML file, I use this component about 16 times. I have it completely hardcoded for 16 different "cardboxes", like so:
HTML
<mat-card id="CARDBOX">
<img class="logoy" src="assets/image" height=35px>
Box1
<input type="image" id="info" title="Click for description" src="assets/image2" height=20px/>
</mat-card>
<mat-card id="CARDBOX">
<img class="logoy" src="assets/image3" height=35px/>
Box2
<input type="image" id="info" title="Click for description" src="assets/image2.png" height=20px/>
</mat-card>
Essentially this renders a little box with a couple images, and a button that takes the user to an external link. There are 16 of these boxes typed into the HTML file, and everytime I want to add a new one, I have to rewrite essentially the same code with only the inputs different.
As you can see this format is heavily reused, but it is arduously hardcoded into the HTML file. Is there a way that I could abstract the reused code so that it is not tediously hardcoded in? How can I make a template for this that can be used multiple times? Going further, how can I add to, remove from, and alter this list of items?

In your component.ts file, you can add a property called something like: cards.
#Component({
selector: 'app-cardboxes',
templateUrl: './cardboxes.component.html',
styleUrls: ['./cardboxes.component.scss']
})
export class CardboxesComponent implements OnInit {
cards = [
{
'imageUrl': '/assets/image.png',
'link': 'link1.com',
... More Stuff YOu like
},
{
'imageUrl': '/assets/image1.png',
'link': 'link1.com',
... More Stuff YOu like
},
.. More items ...
];
constructor(public dialog: MatDialog) { }
ngOnInit() {}
}
Then in your html, you can loop through that cards array:
<mat-card id="CARDBOX" *ngFor="let card of cards">
<img class="logoy" src="{{ card.imageUrl }}" height=35px/>
Box2
<input type="image" id="info" title="Click for description" src="assets/image2.png" height=20px/>
</mat-card>
Note: I would also avoid adding the CARDBOX id to each element, as that is bad practice in HTML5.
Then your problem should be solved ... Let me know if it helps you.

Did not test it, but a ngFor can work wonders in this cases.
Make an array of a new interface cardDetails
export interface CardDetail {
id: number;
image: string;
link: string;
button: string;
input: string;
}
<mat-card *ngFor="let item of cardDetailArray">
<img class="logoy" src="{{item.image}}" height=35px>
{{item.button}}
<input type="image" id="info" title="Click for description" src="{{image.input}}" height=20px/>
</mat-card>
If it's literally only a diff on numbers. just make a number array[1,2,3,...x] and use the ngfor let item in numberArray , concatenate all things with the number variable.

Related

Why ngfor directive is not working, though I have created proper object in Typescript class

I have created proper Ingredient object in Typesript, also proper "model" for the object. Though ngfor directive is not working. I am getting this error "NG0303: Can't bind to 'ngforOf' since it isn't a known property of 'a'" on inspecting in browser.
My model code
export class Ingredient {
constructor(public name: string, public amount: number){}
}
My TypeScript code
import { Component, OnInit } from '#angular/core';
import { Ingredient } from '../shared/ingredient.model';
#Component({
selector: 'app-shopping-list',
templateUrl: './shopping-list.component.html',
styleUrls: ['./shopping-list.component.css']
})
export class ShoppingListComponent implements OnInit {
ingredients: Ingredient[] = [
new Ingredient ("Apple", 5),
new Ingredient ("Tomato", 5)
];
constructor() { }
ngOnInit(): void {
}
}
My HTML code
<div class="row">
<div class="col-xs-10">
<app-shopping-edit></app-shopping-edit>
<hr>
<ul class = "list-group">
<a class = "list-group-item"
style = "cursor : pointer"
*ngfor = "let ingredient of ingredients">
{{ ingredient.name }}({{ ingredient.amount}})
</a>
</ul>
</div>
</div>
Page is loading properly, but ingredients are not loading. On inspecting in browser this error "NG0303: Can't bind to 'ngforOf' since it isn't a known property of 'a'" is coming.
Can someone please help.
Try moving the *ngFor inside the ul tag.
Edit: You have a typo.. It's *ngFor="", not *ngfor="".
If you are inside AppModule check if you have BroswerModule in the imports array there.
If you are in some different module then check if CommonModule is a part of that module's array. CommonModule provides us with core Angular features like *ngIf or *ngFor in your specific case.
Also watch out, you typed *ngfor instead *ngFor.

Adding to component constructor in Angular makes the entire page return blank?

I am trying to add a basic MatDialog to my project. In the project I have 2 components, a header for the page and another called "CardBox", which basically just holds cardboxes of links to different websites.
When you click on the "i" icon, I would like to open a dialog box with more information.
See image below.
Initially, my understanding was that I just add a MatDialog field in the constructor of Cardbox component. Like so:
cardboxes.component.html
<mat-card id="CARDBOX">
<img class="info" src="path/image.jpg" alt="image" height=25px (click)="openDialog()"/>
</mat-card>
cardboxes.component.ts
#Component({
selector: 'app-cardbox',
templateUrl: './cardbox.component.html',
styleUrls: ['./cardbox.component.scss']
})
export class CardboxComponent implements OnInit {
constructor(private dialog: MatDialog) { }
ngOnInit(): void {}
openDialog() {
this.dialog.open(CardBoxComponent);
}
}
(I'm aware that this is calling its own component, and would just open the same thing again. I am just trying to get it to work first.)
app.component.html
<div id="bg">
<app-header></app-header>
<br>
<app-cardbox></app-cardbox>
</div>
However, in doing so, it removes EVERYTHING from the page except the background, including the header component. This is what it looks like when the program is run when there is SOMETHING in the constructor of Cardbox.
As you can see, having something in the constructor gets rid of everything on the page, which does not make sense to me as it removes the header, which is a completely separate component from the cardbox. I have tried everything to make it work but still it is not working.
Why is touching the constructor makes the entire project blank? Is there something I forgot to add to another file? And how can I add a MatDialog popup feature to the project in a way that works?
TLDR: When I put anything in the constructor of one of my components, the entire page disappears. How do I resolve this?
Still seeking answer to this :(
You are using it wrong.
I am surprised your app compiles when doing this.dialog.open(CardBoxComponent)
What you need to do is, first create your dialog component.
To make things simple you can create it in the same file as you CardBox component, but make sure you put it outside CardBox class:
cardboxes.component.ts
#Component({
selector: 'dialog-overview-example-dialog',
templateUrl: 'dialog-overview-example-dialog.html',
})
export class DialogOverviewExampleDialog {
constructor(
public dialogRef: MatDialogRef<DialogOverviewExampleDialog>,
// data is gonna be the data you pass to dialog when you open it from CardBox
#Inject(MAT_DIALOG_DATA) public data: DialogData) {}
onNoClick(): void {
this.dialogRef.close();
}
}
then you create a template for the dialog component:
dialog-overview-example-dialog.html
<h1 mat-dialog-title>more info</h1>
<div mat-dialog-content>
<p>{{data.info}}</p>
</div>
finally you add openDialog(myInfo) function to your ts file, inside CardBox component:
cardboxes.component.ts
openDialog(myInfo): void {
const dialogRef = this.dialog.open(DialogOverviewExampleDialog, {
width: '250px',
// data you pass to your dialog
data: {info: myInfo}
});
dialogRef.afterClosed().subscribe(result => {
console.log('The dialog was closed');
this.animal = result;
});
}
and add it to your template too:
cardboxes.component.ts
<mat-card id="CARDBOX">
<img class="info" src="path/image.jpg" alt="image" height=25px (click)="openDialog('info about first site')"/>
</mat-card>
in this example I pass the info as a text, but it can be an object too.
Here is a demo to make things easier for you: link

Angular interpolate inside a component like mat-checkbox

So I want to have a mat-checkbox component with a HTML string inside the label.
I tried the following:
<mat-checkbox class="check">
{{ someHtml }}
</mat-checkbox>
But it prints the HTML string as a string and doesn't render it.
Using the following doesn't work either:
<mat-checkbox class="check" [innerHtml]="someHtml">
</mat-checkbox>
This just replaces the whole content, including the checkbox that gets generated at runtime. Is there any way to inject the html into the label?
You could use Angular Directives
The idea here is to fetch the element from the HTML, then append some raw HTML dynamically.
Supose this scenario
app.component.html
<mat-checkbox class="check" [appendHtml]="innerHtml"></mat-checkbox>
app.component.ts
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
innerHtml = `<div style="border: 1px solid red;"> Text inside </div>`;
constructor() {}
}
As you can see, I added a appendHtml attribute to the mat-checkbox element. This is a custom directive that expects a string as "raw" HTML.
append-html.directive.ts
#Directive({
selector: '[appendHtml]'
})
export class AppendHtmlDirective implements AfterViewInit {
#Input('appendHtml') html: string
constructor(private element: ElementRef) {
}
ngAfterViewInit() {
const d = this.element.nativeElement.querySelector('label');
d.insertAdjacentHTML('beforeend', this.html);
}
}
The AppendHtmlDirective expects an html property of type string and implements AfterViewInit interface (from Angular) to fetch the element once it is rendered. By injection, Angular provides us the element which is being applied; so, the ElementRef from the constructor is our MatCheckbox element, in that case.
We can use the insertAdjacentHTML function to append childs to the element. I just fetched the label element from the MatCheckbox to fit inside of it. In every case, you should see where to append the HTML.
I mean, label here works, bc MatCheckbox has a tag whitin matching that. If you want to reuse this Directive for other elements, you should be passing the literal to find inside.
i.e.:
append-hmtl.directive.ts
// ...
#Input() innerSelector: string
// ...
ngAfterViewInit() {
const d = this.element.nativeElement.querySelector(this.innerSelector);
d.insertAdjacentHTML('beforeend', this.html);
}
app.component.hmtl
<mat-checkbox class="check" [appendHtml]="innerHtml" innerSelector="label"></mat-checkbox>
Moreover, you can pass as many inputs as you need to customize the styling or behavior of your directive.
Cheers
I think you should just wrap everything in a div and put it on the outside.
<div>
<mat-checkbox class="check"> </mat-checkbox>
{{ someHtml }}
</div>

Multiple Select component for Angular with list style

I need a select component like
The problem is they don't have it in Material Angular, so I tried using default HTML select inside the component. It works fine until I tried to destroy the view of the HTML select(for example when you redirect to other page), it will freeze the whole page for a couple of seconds(the larger the list the longer it will freeze).
First, anyone know the reason why Angular takes a while to destroy non material angular component? Then does anyone have a solution whether to make the freeze gone or appoint me to select component library that could be use in Angular perfectly? I really need the support of being able to select multiple items with click + shift
Here's my component code:
HTML:
<div class="chart">
<div class="toolbar">
<div class="row">
<i *ngIf="multiple" (click)="resetFilter()" class="option material-icons left">refresh</i>
<h4>Sample Id</h4>
<span class="option right"></span>
</div>
</div>
<div class="content">
<select *ngIf="!showSampleCSV" [multiple]="multiple" [size]="100" class="samples-list" [(ngModel)]="selectedSamples" (ngModelChange)="onSelect($event)">
<option *ngFor="let sampleID of sampleIDs" [value]="sampleID">{{sampleID}}</option>
</select>
<app-samples-text *ngIf="showSampleCSV" [samples]="selectedSamples" [multiple]="multiple" (filterSamples)="filterCSV($event)"></app-samples-text>
</div>
</div>
TS:
import { Component, OnInit, Input, Output, EventEmitter, ChangeDetectionStrategy, OnDestroy } from '#angular/core';
#Component({
selector: 'app-samples-list',
templateUrl: './samples-list.component.html',
styleUrls: ['./samples-list.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SamplesListComponent implements OnInit, OnDestroy {
#Input() sampleIDs : string[] = [];
#Input() showSampleCSV : boolean;
#Input() selectedSamples : string[];
#Output() onSelectSamples = new EventEmitter<string[]>();
#Output() onUpdateSamples = new EventEmitter<string[]>();
#Input() multiple: boolean = true;
size = this.sampleIDs.length;
constructor() { }
ngOnInit() {
}
resetFilter() {
this.onSelectSamples.emit(this.sampleIDs);
}
onSelect(samples){
this.onSelectSamples.emit(samples);
}
filterCSV(samples){
this.onUpdateSamples.emit(samples.map(sample => sample.trim()));
}
ngOnDestroy() {
}
}
Problem illustration on stackblitz https://stackblitz.com/edit/angular-qojyqc?embed=1&file=src/app/app.component.html
Material does provide an option for multi select values
<mat-form-field>
<mat-label>Toppings</mat-label>
<mat-select [formControl]="toppings" multiple>
<mat-option *ngFor="let topping of toppingList" [value]="topping">{{topping}}</mat-
option>
</mat-select>
</mat-form-field>
For more information go Here

How to create material cards on button click?

I'm trying to create mat-cards when clicking on a button.
This is the information, which should be in the mat-card (this information comes from a service).
blockHash: "iejg5gpylg6l9gjxor3bnvigs0ipaonr"
blockNumber: 1
previousBlock: "00000000000000000000000000000000"
transactions: Array (1)
0 {sender: "10", recipient: null, amount: null, fee: null}
At the beginning the section, where the mat cards are at should be completely empty. When clicking on a button, that section should be filled with one mat-card; when clicking again, a second mat-card should appear and so on.
This is the block with the information (in another component), which gets send to the component, which should add up the material cards.
This is how it should look like (This is just hardcoded at the moment).
What's an elegant way to do that?
You should create a parent-component that displays multiple card-components.
https://stackblitz.com/edit/angular-zvjblo
parent-component
The parent-component holds your list of blocks and displays multiple card-components by supplying each card-component with the block data for that card. There is also a button to add a new block to the list.
template
<button (click)="addCard()">Add Card</button>
<app-block-card *ngFor="let block of blocks" [blockData]="block"></app-block-card>
code
import { Component, OnInit } from '#angular/core';
import { BlockData } from './block-data';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
blocks: BlockData[];
ngOnInit() {
this.blocks = [];
}
addCard() {
this.blocks.push({
blockHash: '9348534985720587',
blockNumber: 3,
previousBlock: "0000",
transactions: [
{
sender: 'sender',
recipient: 'recipient',
amount: 1,
fee: 200
}
]
});
}
}
card-component
The card-component receives the data of one block from the parent component and displays it.
template
<mat-card class="card">
<p>{{blockData.blockHash}}</p>
<p>{{blockData.blockNumber}}</p>
<p>{{blockData.previousBlock}}</p>
<p>{{blockData.transactions | json}}</p>
</mat-card>
code
import { Component, OnInit, Input } from '#angular/core';
import { BlockData } from '../block-data';
#Component({
selector: 'app-block-card',
templateUrl: './block-card.component.html',
styleUrls: ['./block-card.component.css']
})
export class BlockCardComponent implements OnInit {
#Input() blockData: BlockData;
constructor() { }
ngOnInit() {
}
}
You could be using a list, but I am not sure how to make it horizontal so that might require some tweaking.
But, I would say a drag and drop list would also be very fitting.
The documentation easily describes how to apply these with a horizontal view. But if the drag and drop feature is not for you, then the go with the list.
Using either you can run a *ngFor loop that can be performed upon arrays of data.
Example of such:
<div cdkDropList cdkDropListOrientation="horizontal" class="example-list"
(cdkDropListDropped)="drop($event)">
<div class="example-box" *ngFor="let card of cardArray" cdkDrag>
<mat-card class="example-card">
<mat-card-header>
<div mat-card-avatar class="example-header-image"></div>
<mat-card-title>Shiba Inu</mat-card-title>
<mat-card-subtitle>Dog Breed</mat-card-subtitle>
</mat-card-header>
<img mat-card-image src="https://material.angular.io/assets/img/examples/shiba2.jpg" alt="Photo of a Shiba Inu">
<mat-card-content>
<p>
text
</p>
</mat-card-content>
<mat-card-actions>
<button mat-button>LIKE</button>
<button mat-button>SHARE</button>
</mat-card-actions>
</mat-card>
</div>
</div>
In your case you will have to apply the card template inside the *ngFor div, and bind the data accordingly to your naming.
To add a new cards through a button you will simply have to add a new element to the array that you loop over, and it will appear when it has been added.