I am working with a back-end API that I query for flight data, which I then loop over and display the data. My template code looks like this...
<form novalidate #form="ngForm">
<div class="flight-table">
<header fxLayout="row" fxLayoutAlign="space-between">
<div fxFlex="25">Flights</div>
<div fxFlex="17">Class</div>
<div fxFlex="18">Price (AED)</div>
<div fxFlex="15">PAX</div>
<div fxFlex="25">Total</div>
</header>
<main>
<div class="flights-body--row" fxLayout="row" fxLayoutAlign="space-between" *ngFor="let flight of data.flights; let i = index">
<p fxFlex="25">{{flight['flightName']}}</p>
<p fxFlex="17">{{flight['flightClass']}}</p>
<p fxFlex="18">{{flight['flightPrice'] | number}}</p>
<p fxFlex="15" fxLayout="column">
<mat-select placeholder="Ticket(s)">
<mat-option *ngFor="let pax of counter" [value]="pax">{{ pax }} </mat-option>
</mat-select>
</p>
<p fxFlex="25">AED 0</p>
</div>
<div class="flights-body--row" fxLayout="row" fxLayoutAlign="space-between">
<p class="capitalize" fxFlex="75"><strong>Total transport</strong></p>
<p class="center-text" fxFlex="25"><strong>AED 0</strong></p>
</div>
</main>
</div>
</form>
When a user selects a number of tickets from the PAX dropdown, I'd like to update both the row total and the total transport. I'd need to be able to grab the value of the PAX select box and multiply it by flight['flightPrice'] but I can't quite figure out how to, because I can't know how many items are in the flights array. What you see is just one row but there could be more.
Also, I'd like to capture the value for each row. I've thought about adding a hidden form field for each row or sth like that. I'm not quite sure how to deal with this.
Is there some better way?
Update
I'm struggling with how to capture the selected rows. I've tried adding a hidden input field in each row like so..
<input
matInput
[name]="flight['flightName']-flight['flightClass']-pax-flight['flightPrice']"
[value]="total"
ngModel>
That doesn't seem to work. The goal is to have each selected row stored in an object like so...
The dashes separate the flightName, flightClass, pax and flightPrice with the value being the total (pax * flightPrice) and pax the number of passengers.
flights: {
lufthansa-business-2-60: 120,
flyEmirates-first-3-50: 150
}
How should I go about it?
I would recommend that you create a dedicated component for your basket items.
You then have an isolated scope you can work with and i.e. use [(ngModel)] to set the selected number of pax on your component (for which you have to import the FormsModule in your app/feature module).
You can then reuse that value to compute the total price for a flight.
To get the total for the entire transport (sum over all basket items), you'd also need to pass data between components. A BasketService that keeps track of your basket items can help with that. I created a little stackblitz to demonstrate how to do this: https://stackblitz.com/edit/angular-tpenou
The relevant parts are:
export class BasketItemComponent implements OnDestroy {
#Input() flight: Flight;
paxOptions = [0, 1, 2, 3, 4];
pax = 0;
constructor(private basketService: BasketService) {
this.basketService.registerBasketItemComponent(this);
}
ngOnDestroy(): void {
this.basketService.removeBasketItemComponent(this);
}
get total() {
return this.flight.flightPrice * this.pax;
}
}
And the template:
<div class="flights-body--row" fxLayout="row" fxLayoutAlign="space-between" >
<p fxFlex="25">{{flight['flightName']}}</p>
<p fxFlex="17">{{flight['flightClass']}}</p>
<p fxFlex="18">{{flight['flightPrice'] | number}}</p>
<p fxFlex="15" fxLayout="column">
<mat-select placeholder="Ticket(s)" [(ngModel)]="pax">
<mat-option *ngFor="let pax of paxOptions" [value]="pax">{{ pax }} </mat-option>
</mat-select>
</p>
<p fxFlex="25">AED {{ total }}</p>
</div>
And the BasketService:
export class BasketService {
basketItems: BasketItemComponent[] = [];
constructor() { }
get total() {
return this.basketItems.reduce( (a, b) => a + b.total, 0);
}
registerBasketItemComponent(c: BasketItemComponent) {
this.basketItems.push(c);
}
removeBasketItemComponent(c: BasketItemComponent) {
this.basketItems.splice(this.basketItems.indexOf(c), 1);
}
}
You would also have to alter your for loop to make use of your newly created custom component:
<form novalidate #form="ngForm">
<div class="flight-table">
<header fxLayout="row" fxLayoutAlign="space-between">
<div fxFlex="25">Flights</div>
<div fxFlex="17">Class</div>
<div fxFlex="18">Price (AED)</div>
<div fxFlex="15">PAX</div>
<div fxFlex="25">Total</div>
</header>
<main>
<app-basket-item [flight]="flight" *ngFor="let flight of data.flights; let i = index">
</app-basket-item>
<div class="flights-body--row" fxLayout="row" fxLayoutAlign="space-between">
<p class="capitalize" fxFlex="75"><strong>Total transport</strong></p>
<p class="center-text" fxFlex="25"><strong>AED {{transportTotal}}</strong></p>
</div>
</main>
</div>
</form>
Related
I have a collection of array which is having datas like
[0]: username {kavya} secret {password}
[1]: lorem ipsem text data value
[2]: lorem {{lrm}} datas {{pp}}
I am using foreach to show this data in frontend with
<div *ngFor="let data of output;let i=index">
<div *ngIf="data.includes('{') || data.includes('{{');else isNotEdited;" >
<div class="variable-textarea" contenteditable="false" >
<span>
{{data | slice:' ' }}
</span>
</div>
</div>
<ng-template #isNotEdited>
<ngx-md>{{data}}</ngx-md>
</ng-template>
</div>
Here I achieved like 0,2 row of div will be editable and in case of 1st array is non-editable.
But I want to do like specific matches which word starts with { or {{ and that particular word needs to be highlight and editable.
Is there any option to do in this way
Thanks in advance.
You could split the data into words:
<div *ngFor="let data of arr">
<span *ngFor="let word of data.split(' ')">
<span *ngIf="word.indexOf('{') > -1;else isNotEdited;">
<span class="variable-textarea-2" contenteditable="true">
{{word | slice:' ' }}
</span>
</span>
<ng-template #isNotEdited>
<span class="variable-text-2" contenteditable="false">
{{word}}
</span>
</ng-template>
</span>
</div>
Check this Stackblitz example I made based on your code: https://stackblitz.com/edit/angular-pkg6i9
this is a performance nightmare, you don't want to be running this many functions in template, and your format isn't helping you. map your data ahead of time into a friendlier view model:
this.mappedOutput = this.output.map(data => {
const editable = data.includes('{'); // checking for doubles is redundant
return {
editable,
data: !editable
? data
: data.split(' ')
.map(word => ({
word,
editable: word.trim().startsWith('{') && word.trim().endsWith('}')
}))
};
})
run this whenever your output changes, then use it in template:
<div *ngFor="let data of mappedOutput;let i=index">
<div *ngIf="data.editable;else isNotEdited;" >
<div class="variable-text">
<ng-container *ngFor="let word of data.data">
<div *ngIf="word.editable; else wordTmp" class="variable-textarea inline" contenteditable="true" >
<span>{{word.word}}</span>
</div>
<ng-template #wordTmp>
{{word.word}}
</ng-template>
</ng-container>
</div>
</div>
<ng-template #isNotEdited>
<ngx-md>{{data.data}}</ngx-md>
</ng-template>
</div>
and adjust the styles by adding this to your css:
.variable-textarea.inline {
display: inline-block;
width: fit-content;
margin: 0;
}
here's an edited blitz: https://stackblitz.com/edit/angular-arayve?file=src/app/app.component.html
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
I have a reactive input form with a dynamic amount of inputs that I need to add form controls to.
The parent control Mammals will contain a variable amount of MammalId's, and each MammalId will contain a FormArray of variable length.
I have created typescript code that will generate the form as I described, however, I haven't been able to set my form to my HTML.
And an example of my generated form:
I've tried mapping the form to my html in the following way:
<form [formGroup]="inputForm">
<div *ngFor="let mammal of Mammals; let i = index">
<div *ngFor="let id of mammal.ids; let z = index">
<!-- parent form control -->
<div [formGroupName]="mammals">
<!-- mammals[i].id resolves to 'mammalIdx' -->
<div [formGroupName]="mammals[i].id">
<!-- this should map as the controls Form Array -->
<input [formControlName]="z">
</div>
</div>
</div>
</div>
</form>
But when I try and run this, I get:
Error: Cannot find control with unspecified name attribute
How can I set my html to my form values?
Thank you
The parent control Mammals will contain a variable amount of MammalId's, and each MammalId will contain a FormArray of variable length.
As per my understand the arry you expected may be like this
[
{
"Mammals":[
{
"mammalId":[
{
}
]
}
]
}
]
for that above array the form will be like this
this.inputForm = this.fb.group({
Mammals: this.fb.array([}
mammalId: this.fb.array([{
z: ''
}
])}
])
})
for that above array html should be like this
HTML
<form [formGroup]="inputForm">
<div formArrayName="Mammals">
<div *ngFor="let mammal of inputForm.get('Mammals'); let i = index" [formGroupName]="i">
<div formArrayName="mammalId">
<div *ngFor="let id of mammal.get(mammalId); let z = index" [formGroupName]="z">
<!-- parent form control -->
<!-- mammals[i].id resolves to 'mammalIdx' -->
<!-- this should map as the controls Form Array -->
<input [formControlName]="z">
</div>
</div>
</div>
</div>
</div>
</div>
</form>
It may be helps you
On my website if I have more than one element in my array. My template looks like this.
I want to have a button to go to the next element of this array and only display one set of data and use the button to control which element of the array the user sees.
My current code looks like this:
<div class='panel-body' *ngIf ='case'>
<h3> Details </h3>
<div id="left-side" *ngFor="let tag of case?.incidents ">
<p>Date: <span class="name">{{tag.date}}</span> </p>
<p>DCU: <span class="name">{{tag.dcu}}</span></p>
<p>Location:<span class="name"> {{tag.location}}</span> </p>
</div>
I was thinking of using some sort of index or an ng-container or some work around using ngIf or ngFor. I am unsure of how to implement this.
All help would be greatly appreciated!
You're not going to need an ngFor or ngIf in this situation. What you'll want is a variable to keep track of the user's index, and then a function that changes that index.
<h3> Details </h3>
<div id="left-side" >
<p>Date: <span class="name">{{case?.incidents[userIndex].date}}</span> </p>
<p>DCU: <span class="name">{{case?.incidents[userIndex].dcu}}</span></p>
<p>Location:<span class="name"> {{case?.incidents[userIndex].location}}</span> </p>
</div>
<button (click)="changeIndex(-1);">Previous</button>
<button (click)="changeIndex(1);">Next</button>
and in your component.ts you'll have:
userIndex = 0;
changeIndex(number) {
if (this.userIndex > 0 && number < 0 || //index must be greater than 0 at all times
this.userIndex < this.case?.incidents.length && number > 0 ) { //index must be less than length of array
this.userIndex += number;
}
This will be a standard for in-view paging systems for other projects as well.
To achieve this you can use angular's default SlicePipe like this example,
#Component({
selector: 'slice-list-pipe',
template: `<ul>
<li *ngFor="let i of collection | slice:1:3">{{i}}</li>
</ul>`
})
export class SlicePipeListComponent {
collection: string[] = ['a', 'b', 'c', 'd'];
}
You can find more details here
I'm writing out images to the web page. Every three images I would like to start a new row. Does angular 2 support this?
You can achieve it by doing following:
<div *ngFor="let t of temp(math.ceil(arr.length/3)).fill(); let i = index" class="row">
<div *ngFor="let item of arr.slice(3*i,3*i + 3);" class="item">
{{item}}
</div>
</div>
And in your component:
export class App {
temp = Array;
math = Math;
arr= [1,2,3,4,5,6,7,8,9,10,11];
}
Here's working Plunker
You can access the index of the iteration from the *ngFor like so:
*ngFor="let x of container; let i = index;"
you can then reference that index in an *ngIf inside of the *ngFor to display your new row:
<div *ngIf="i%3 === 0">
Borrowing from both answers, but leaning more on Alex's, here's how I accomplished it in ng2 (v2.0.1).
<template ... is deprecated. Where you see <template ... you should use <ng-template ... after v5.
<template ngFor let-peer [ngForOf]="peers" let-p="index" let-last="last">
<div class="row" *ngIf="p % 2 === 0">
<div class="col-xs-8 col-sm-6">
<label><input type="checkbox" ... {{peers[p].name}}</label>
</div>
<div class="col-xs-8 col-sm-6" *ngIf="!last">
<label><input type="checkbox" ... {{peers[p+1].name}}</label>
</div>
</div>
</template>
Note that while I get each item from the collection, let-peer [ngForOf]="peers", I don't specifically use it. Instead I use the collection and the index, let-p="index", adding to the index as needed, e.g., peers[p] and peers[p+n].
Adjust your modulus as needed. The last row should check to be sure that the first column to the last column is not the last item in the iterable.