I have a application with a table of cars:
This is my code:
Carcomponent.html
<tbody>
<tr *ngFor="let car of allCars; index as carId" \>
<td [routerLink]="['/cars', carId]">{{car.carId}}</td>
<td>{{car.brand}}</td>
<td>{{car.model}}</td>
<td>{{car.color}}</td>
<td>{{car.topSpeed }}</td>
</tr>
</tbody>
I have register the route like this:
{ path: 'cars/:carId', component: CardetailsComponent }
And this is my CarDetails.ts file:
import { Component, OnInit } from '#angular/core';
import { ActivatedRoute } from '#angular/router';
import { CarVM } from '../viewmodels/car-vm';
import { CarService } from '../services/car.service';
#Component({
selector: 'app-cardetails',
templateUrl: './cardetails.component.html',
styleUrls: ['./cardetails.component.css']
})
export class CardetailsComponent implements OnInit {
car: any;
carList: any;
constructor(private route: ActivatedRoute, private carservice: CarService) { }
ngOnInit() {
this.route.paramMap.subscribe(params => {
this.car = params.get('carId');
});
}
getCarList() {
this.carList = new CarVM();
this.carservice.getCarById(this.carList.carId).subscribe((res: any) => {
this.carList = res.data;
console.log(this.carList)
})
}
}
And on my Cardetails.html I want to show the selected car like this:
<h2>Car Details</h2>
<div *ngIf="car">
<h3>{{ car.brand }}</h3>
<h4>{{ car.model }}</h4>
<p>{{ car.color }}</p>
</div>
The routing is working fine and fetching the cars is working. Now I want to select one car and see the brand, model, color on the next page. I use a viewmodel for this:
export class CarVM {
CarId: number;
Brand: string;
Model: string;
Color: string;
TopSpeed: number;
}
How can I see the selected car on the next page?
I have followed this tutorial:
https://angular.io/start/routing
Ok, you seem to be bit confused. In cardetails component you want to process carId from route parameters and use it to get car details. You can either get them from server, or have the service return already loaded details of all cars.
Let's say we are trying to make it happen getting the first way, it might look like this:
import { map, switchMap } from 'rxjs/operators';
ngOnInit() {
this.getCar();
}
private getCar(): void {
this.route.paramMap.pipe(
map(params => params.get('carId')),
switchMap(carId => {
return this.carservice.getCarById(carId);
})
).subscribe(
res => {
this.car = res;
console.log('#My car:', this.car);
}
);
}
First, you'll get the carId from route.paramMap, map it using rxjs map, then use switchMap to call you carservice.getCarById(carId) and have it return Observable to which you can subscribe. This should do the trick. Don't forget to properly map it/create CarVM object from it.
The problem is, you don't have CarVM object properly on CardetailsComponent. You are only getting carId into CarVM here: this.car = CarVM[+params.get('carId')];
First you need to create CarVM properly with your class variables. And the you can call your index.
import { Component, OnInit } from '#angular/core';
import { ActivatedRoute } from '#angular/router';
import { CarVM } from '../viewmodels/car-vm';
#Component({
selector: 'app-cardetails',
templateUrl: './cardetails.component.html',
styleUrls: ['./cardetails.component.css']
})
export class CardetailsComponent implements OnInit {
car: any;
carList: any;
constructor(private route: ActivatedRoute) { }
ngOnInit() {
this.route.paramMap.subscribe(params => {
this.car = params.get('carId');
});
}
getCarList(){
this.carList = new CarVM();
//call your service here to fill your carList variable and once you get car list, you will be able to access variable using with your index (this.car).
}
}
Related
I have a Project that uses HTTP Calls to fetch Data from API Endpoint and than display on Screen.
It's a simple ToDoList. So you can add Items to the list, see all Items in your List, delete items and so on.
The Project structure is this:
Items-Component (Holds the entire App basically)
Item-list-component
Item-detail-component
Item-edit-component
item-add-component
Item.service
The Items.component.html looks like this:
<div class="row">
<div class="col-md-5">
<app-item-list></app-item-list>
</div>
<div class="col-md-7">
<router-outlet></router-outlet>
</div>
So we can see that the item-list-component and the other 3 components (binded via router-outlet) are sibling components, that's what I think.
So my Problem is now:
I want that whenever a new Item is created the items[] in the items.list component should refresh automatically. Now I must click a "Fetch Items" button to refresh the items[].
When I add a new Item, it fires a method from my item.service, it holds a fetchItems Method that just returns an Observable of the API Endpoint, like this:
Item-add component.ts:
#Component({
selector: 'app-item-add',
templateUrl: './item-add.component.html',
styleUrls: ['./item-add.component.css']
})
export class ItemAddComponent implements OnInit {
constructor(private itemService: ItemService, private route: ActivatedRoute, private router: Router) { }
ngOnInit(): void {
}
onCreatePost(item: Item) {
// Send Http request
this.itemService.createAndStorePost(item.description, item.isComplete);
//Here I want that the items[] in the items.list component refreshes when I add new Item
this.onCancel();
}
onCancel() {
this.router.navigate([''], {relativeTo: this.route});
}
}
And the item.service.ts:
#Injectable()
export class ItemService {
constructor(private http: HttpClient, private route: ActivatedRoute, private router: Router) {
}
fetchItems(): Observable<Item[]> {
return this.http.get<Item[]>('https://localhost:44321/api/TodoItems');
}
fetchItem(id: number): Observable<Item> {
return this.http.get<Item>('https://localhost:44321/api/TodoItems' + '/' + id);
}
createAndStorePost(description: string, isComplete: boolean) {
var item = { description: description, isComplete: isComplete };
this.http.post('https://localhost:44321/api/TodoItems', item)
.subscribe(Response => {
});
}
deleteItem(id: number): Observable<Item> {
return this.http.delete<Item>('https://localhost:44321/api/TodoItems' + '/' + id);
}
updateItem(id:number, item: Item) {
this.http.put<Item>('https://localhost:44321/api/TodoItems' + '/' + id, item).subscribe();
}
}
Then the items-list component catches that Observable and subscribes to it and sets the Response from that subscription to and items[] in the component itself:
#Component({
selector: 'app-item-list',
templateUrl: './item-list.component.html',
styleUrls: ['./item-list.component.css']
})
export class ItemListComponent implements OnInit {
items: Item[] = [];
constructor(private route: ActivatedRoute, private router: Router, private itemService: ItemService) { }
ngOnInit(): void {
this.onFetchItems();
}
onFetchItems() {
this.itemService.fetchItems().subscribe(Response => {
this.items = Response;
});
}
onNewItem() {
this.router.navigate(['new'], {relativeTo: this.route});
}
}
What can I do to trigger that the items.list should fetch Items again?
I can't use #ViewChild because it is no Parent-Child relation.
Can I implement and instance of item.list anywhere in the project and just call the onFetchItems Method?
Thanks!
you can use BehaviorSubject to share data between your different components.
Here is an example:
In your ItemService.
import { BehaviorSubject } from 'rxjs';
#Injectable()
export class ItemService {
private _itemsSource = new BehaviorSubject([]);
currentItems = this._itemsSource.asObservable();
constructor() { }
updateItems(items: []): void {
this._itemsSource.next(items)
}
}
In your ItemsComponent, you update the new value in the service after you get all the items,
#Component({
selector: 'app-item',
templateUrl: './item.component.html',
styleUrls: ['./item.component.css']
})
export class ItemComponent implements OnInit {
items: Item[] = [];
constructor(private itemService: ItemService) { }
ngOnInit(): void {
this.onFetchItems();
}
onFetchItems() {
this.itemService.fetchItems().subscribe(Response => {
this.items = Response;
this.updateItems(this.items)
});
}
updateItems(newItems: []): void {
this.itemService.updateItems(newItems)
}
}
And in your ItemListComponent
#Component({
selector: 'app-item-list',
templateUrl: './item-list.component.html',
styleUrls: ['./item-list.component.css']
})
export class ItemListComponent implements OnInit {
items: Item[] = [];
subscription: Subscription;
constructor(private route: ActivatedRoute,
private router: Router,
private itemService: ItemService) { }
ngOnInit(): void {
this.subscription = this.itemService.currentItems.subscribe(items => this.items = items)
}
onNewItem() {
this.router.navigate(['new'], {relativeTo: this.route});
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
I Have created a shopping cart with 2 components(Productlist and cart list). when I click on the 'add to cart' button in the product list it is successfully moving into the service file and from the service file to 'cart list' but Total was showing undefined. Please help me to solve this cart Total issue.
Product-list.ts file
import { Component, OnInit } from '#angular/core';
import { ProductService } from '../../../service/product.service';
import { CartService } from '../../../service/cart.service';
#Component({
selector: 'app-product-list',
templateUrl: './product-list.component.html',
styleUrls: ['./product-list.component.css']
})
export class ProductListComponent implements OnInit {
Productlist = [];
constructor(private productservice: ProductService,
private cartservice: CartService) {
this.Productlist = this.productservice.send()
}
ngOnInit(): void {
}
saveproduct(item) {
this.cartservice.get(item)
}
}
cart service file
import { Injectable } from '#angular/core';
#Injectable({
providedIn: 'root'
})
export class CartService {
cartproducts = [{Name: 'Mobile', Price: 13000, Qty: 1}]
constructor() { }
send() {
return this.cartproducts
}
get(items) {
this.cartproducts.push(items)
console.log(this.cartproducts)
}
}
cart list file
import { Component, OnInit, ɵConsole, ɵɵNgOnChangesFeature } from '#angular/core';
import { CartService } from '../../../service/cart.service';
#Component({
selector: 'app-cart',
templateUrl: './cart.component.html',
styleUrls: ['./cart.component.css']
})
export class CartComponent implements OnInit {
cartlist = [];
Total: number;
constructor( private cartservice: CartService) {
this.cartlist = this.cartservice.send()
console.log(this.cartlist);
console.log(this.Total);
}
ngOnInit() {
this.cartlist.forEach((_e: any) => {
this.Total += (_e.qty * _e.price);
});
}
}
When the Total property has been created, it is undefined by default until you assign it a value. You do assign a value to it in the ngOnInit but you calculate on an undefined value, so it returns undefined as well. Give Total a default value like so:
...
Total: number = 0;
...
Also the code you put in ngOnInit to calculate the Total, would be better placed in a get property. This way, Total will always be calculated when it's requested instead of pre-rendered and only calculated once. In that case your code would look like:
get Total(): number {
let value = 0;
this.cartlist?.forEach((_e: any) => {
value += (_e.qty * _e.price);
});
return value;
}
I have a loop on a component which represents a list of graph cards on my real app.
I have copied this component ( and loop it ) as the original
Hello Component
export class HelloComponent {
message:string;
printedMessage:string
#Input() elm:string;
constructor(private data: DataService, private router : Router) { }
ngOnInit() {
this.message = this.data.messageSource.value;
this.data.messageSource.subscribe(message => this.message = message)
}
updateService(){
this.data.changeMessage(this.message);
this.printedMessage=this.data.messageSource.value
}
navigateToSibling(){
this.router.navigate(['/sibling']);
}
}
app component
<div *ngFor="let elm of [1,2,3,4]">
<hello [elm]= "elm"></hello>
</div>
<h1>Copy </h1>
<div *ngFor="let elm of [1,2,3,4]">
<hello [elm]= "elm"></hello>
</div>
DataService component
export class DataService {
messageSource = new BehaviorSubject<string>("default message");
constructor() { }
changeMessage(message: string) {
this.messageSource.next(message)
}
}
Expected behaviour
What I would is when change the input value on the component 1 for example , only the value on the input of the copied component 1 changes.
Actual behaviour
Actually when I change a value inside an input all the other inputs are changings.
Here's a stackblitz example
Below is a solution that will solve you issue. This may not be a perfect solution but you need something similar.
hello.html
<h1>App component {{elm}}</h1>
<input type="text" [(ngModel)]="message">
<button (click)="updateService()" type="button">Save</button> {{printedMessage}}
Data Service
import {
Injectable
} from '#angular/core';
import {
BehaviorSubject
} from 'rxjs/BehaviorSubject';
#Injectable()
export class DataService {
messageSource = new BehaviorSubject < any > ("default message");
constructor() {}
changeMessage(message: string, elem: any) {
this.messageSource.next({
message: message,
elem: elem
});
}
}
HelloComponent
import {
Component,
Input
} from '#angular/core';
import {
DataService
} from "./dataService";
import {
Router
} from '#angular/router';
#Component({
selector: 'hello',
templateUrl: './hello.html',
styles: [`h1 { font-family: Lato; }`]
})
export class HelloComponent {
message: string;
printedMessage: string
#Input() elm: string;
constructor(private data: DataService, private router: Router) {}
ngOnInit() {
this.message = this.data.messageSource.value;
this.data.messageSource.subscribe(message => this.message = message.elem === this.elm ? message.message : this.message);
}
updateService() {
debugger
this.data.changeMessage(this.message, this.elm);
this.printedMessage = this.data.messageSource.value.message;
}
navigateToSibling() {
this.router.navigate(['/sibling']);
}
}
Have also updated the Stackblitz Demo. Hope this helps :)
There are two address shipping and billing Country. Both has different value.
Components are reused here.
A select-country component was made for this .
<select-country [addressType]="'shipping'"></select-country>
<select-country [addressType]="'billing'"></select-country>
The type can be shipping or billing.
Now in select-country
import { Component, OnInit,ViewChild, ElementRef,Input } from '#angular/core';
import { ConfigService } from '../services/config.service';
import { DataService } from '../services/data.service';
import { CheckOutService } from '../services/checkout/check-out.service';
import { HomeService } from './../services/banner/home.service';
import {MdlService} from './../services/material-design-lite/mdl.service';
import { Http } from "#angular/http";
import { apiUrl,used_currency,used_language } from './../services/global.constant';
import { Router,ActivatedRoute } from '#angular/router';
import {NgSelectModule, NgOption} from '#ng-select/ng-select';
import {HttpClient, HttpClientModule} from '#angular/common/http';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/switchMap';
declare var componentHandler: any;
#Component({
moduleId: module.id + '',
selector: 'select-country',
templateUrl: './get-country.component.html',
styleUrls: ['./get-country.component.css']
})
export class GetCountryComponent implements OnInit {
#Input('addressType') addressType;
searchQuery: string = '';
items;va;
countries = new Array;
constructor(public http: Http,public router: Router,
public configz: ConfigService,public shared: DataService,
private checkOutservice: CheckOutService ,
private service: HomeService, public material:MdlService) {
if(this.addressType=='shipping')
{va=shared.orderDetails.delivery_country}
else{va=shared.orderDetails.billing_country}
var data = { type: 'null' };
http.post(this.configz.url + 'getCountries', data).map(res => res.json()).subscribe(data => {
this.items = this.countries = data.data;
console.log(this.items);
setTimeout(() => { this.material.render(); }, 550);
});
}
ngAfterViewInit(){
}
ngOnInit() {
}
static mdlWrapper(element: ElementRef) {
componentHandler.upgradeElement(element.nativeElement);
}
}
<div tabindex="-1">
<select [(ngModel)]="shared.orderDetails.delivery_country" name="orderby" >
<option *ngFor="let item of items" value="" >{{ item.countries_name }}</option>
</select>
</div>
Shared is service which is shared among all component.
For shipping shared.orderDetails.delivery_country is used and for billing shared.orderDetails.billing_country
How to dynamically change ngModel and set shared.orderDetails .
I am making changes in shared only because there are multiple component and they need to share the same service to retain data.
EDIT : I tried setting a variable in Get Country Component. Edited it please check. It does not update the shared.orederDetails.
Why don't you build the <select-country> as a proper form control? That way you can use ngModel binding and forget about the details. Something like this:
<form>
<input [(ngModel)]="order.name" name="name">
<input [(ngModel)]="order.count" name="count">
<select-country [(ngModel)]="order.shippingCountry" name="shippingCountry"></select-country>
<select-country [(ngModel)]="order.billingCountry" name="billingCountry"></select-country>
</form>
The component you can build like this:
import { Component, Input, forwardRef, Output, EventEmitter, OnInit } from '#angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '#angular/forms';
import { HttpClient } from '#angular/common/http';
interface Country {
value: string;
countryName: string;
}
const SELECT_COUNTRY_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SelectCountryComponent),
multi: true,
};
#Component({
selector: 'select-country',
template: `
<div tabindex="-1">
<select [(ngModel)]="value" name="orderby" (ngModelChange)="updateValue($event)">
<option *ngFor="let country of countries" [selected]="country.value === value?.value" [value]="country.value">{{ country.countryName }}</option>
</select>
</div>`,
styles: [`:host { display: inline-block; }`],
providers: [SELECT_COUNTRY_VALUE_ACCESSOR]
})
export class SelectCountryComponent implements ControlValueAccessor, OnInit {
_value: Country;
countries = []
private onChangeCallback: Function;
private onTouchedCallback: Function;
constructor(private http: HttpClient) {}
ngOnInit() {
this.http.get('https://www.mocky.io/v2/5a90341e2f000061006caba7')
.subscribe((countries: Country[]) => this.countries = countries,
err => console.error('Error getting countries:', err));
}
/** that mock should return something like this:
* [{
"countryName": "Ireland",
"value": "IRELAND" // or whatever
}, ...]
*/
// update coming from external, e.g. form control
writeValue(value) {
// outside change of selected item
console.log('Update value:', value);
this._value = value;
}
// update coming from the view (our dropdown), we update it and then go on to inform Angular Forms.
// called by the dropdown selector.
updateValue(value) {
this._value = this.countries.filter(country => country.value === value)[0];
this.valueChange();
}
private valueChange() {
this.onTouchedCallback();
this.onChangeCallback(this._value);
}
// these two are set
registerOnChange(onChangeCallback: Function) {
this.onChangeCallback = onChangeCallback;
}
registerOnTouched(onTouchedCallback: any) {
this.onTouchedCallback = onTouchedCallback;
}
}
Of course, adjust to oyur needs, add validation, styling etc. You can see it in action here: https://stackblitz.com/edit/angular-country-picker?file=app/app.component.html
In addition to my other answer (with proposal to make a lil' standalone component you could reuse elsewhere), let's try a more direct approach, fixing your code:
First, your template options all lack values. Even when your user picks something, ngModel doesn't change (because it's always the same value, "").
<div tabindex="-1">
<select [(ngModel)]="shared.orderDetails.delivery_country" name="orderby">
<option *ngFor="let item of items" [value]="item.countries_name">{{ item.countries_name }}</option>
</select>
</div>
Second, your init logic is in the constructor. At that instance, you still don't have your inputs settled. Move init parts to your ngOnInit.
constructor(public http: Http,
public router: Router,
public configz: ConfigService,
public shared: DataService,
private checkOutservice: CheckOutService,
private service: HomeService,
public material:MdlService) {
}
ngOnInit() {
if(this.addressType === 'shipping') {
this.va = shared.orderDetails.delivery_country
} else {
this.va = shared.orderDetails.billing_country;
}
const data = { type: 'null' };
http.post(this.configz.url + 'getCountries', data).map(res => res.json()).subscribe(data => {
this.items = this.countries = data.data;
console.log(this.items);
setTimeout(() => { this.material.render(); }, 550);
});
}
Why is there a problem in binding a property on the same component? I already added Input() but still doesn't work. Do i need to put Input() even though it is on the same component when binding?
//output.component.ts
import { Component, OnInit} from '#angular/core';
import { DataService } from '../data.service';
#Component({
selector: 'app-output',
templateUrl: './output.component.html',
styleUrls: ['./output.component.css']
})
export class OutputComponent implements OnInit {
data: {name: string};
datas = [];
constructor(private dataService: DataService) { }
ngOnInit(){
this.datas = this.dataService.datas;
}
}
//output.component.html
<p *ngFor="let data of datas"></p>
<p>{{data.name}}</p>
//data.service.ts
export class DataService {
datas= [];
addData(name: string){
return this.datas.push({name: name});
}
}
For same component #input API is not required. It is used when you want to pass the data from Parentcomponent to a child component.
//output.component.html
<p *ngFor="let data of dataService.datas" > // removed [data]="data" and added dataService.datas
<p>{{data?.name}}</p>
</p> //changed the position of </p>
export class OutputComponent implements OnInit {
constructor(private dataService: DataService) {}
}
export class DataService {
datas= [];
addData(name: string){
return this.datas.push({name: name}); //return keyword was missing
}
}
Just for your reference
DEMO: https://plnkr.co/edit/XlJM2LHFwlAYpQe2ancM?p=preview