Angular4, update component on route change - javascript

How to update component when route changes. I have this component :
import { Component, OnInit } from '#angular/core';
import { ActivatedRoute } from '#angular/router';
import { ListService } from '../list/list.service';
#Component({
selector: 'view',
template: `
<div *ngIf="!entity">
<p>Select <b (click)="showRow()">row {{entity}}</b>!</p>
</div>
<div *ngIf="entity">
<p >{{entity.id}}</p>
<p >{{entity.name}}</p>
<p >{{entity.weight}}</p>
<p >{{entity.symbol}}</p>
</div>
`,
styles: []
})
export class ViewComponent implements OnInit {
constructor(
private route: ActivatedRoute,
private service: ListService
) {
this.route.params.subscribe(params => {
const id = parseInt(params['id']);
if (id) {
const entity = this.service.getRow(id);
this.entity = entity
}
});
}
entity;
showRow() {
console.log(this.entity);
}
ngOnInit() {
}
}
in this.entity inside constructor i have desired object but when i execute showRow this.entity is undefined, what i'm doing wrong ? I have tried to change property to different name, and it didn't work as expected, if any one knows how to resolve this or point me to right direction.
EDIT:
getRow from service
getRow(id) {
console.log(id, 'test');
return this.datasource.find(row => row.id === id);//returns good row
}

Move your code to ngOnInit() method and check will you get value or not.
ngOnInit() {
this.route.params.subscribe(params => {
const id = parseInt(params['id']);
if (id) {
const entity = this.service.getRow(id);
this.entity = entity
}
});
}

I believe you need to define/initialize the entity that is positioned above showRow, such as:
const entity = Entity;
or something along those lines. apologies I am quite new to angular as well.

I found answer to my problem, i just needed to put router-outlet in template just like that :
....
....
template: `
<router-outlet>
<div *ngIf="row?.id; else elseBlock">
<div>
<p>{{row.id}}</p>
<p>{{row.name}}</p>
<p>{{row.weight}}</p>
<p>{{row.symbol}}</p>
</div>
</div>
</router-outlet>
`,

Related

At click route to details of given object

I have two components: recipe and recipe-detail.
Recipe is displaying list of recipes from my database and recipe-detail should display details of given recipe. What I want to achieve is to whenever someone click on recipe name I want to route him to recipe-detail component.
This is my recipe html:
<section class="columns">
<div class="column" *ngFor="let recipe of RecipeList">
<h2><a routerLink="recipes/{{recipe.Id}}" routerLinkActive="true">{{recipe.name}}</a></h2>
<p>{{recipe.recipeBody}}</p>
</div>
</section>
My routing file:
{path:'recipes', component: RecipeComponent},
{path: 'recipes/:id', component: RecipeDetailsComponent}
And my recipe-detail ts:
export class RecipeDetailsComponent implements OnInit {
#Input() recipe! : any;
constructor(private route: ActivatedRoute,
private sharedService: SharedService) { }
ngOnInit(): void {
this.getRecipe();
}
getRecipe() : void{
const id = Number(this.route.snapshot.paramMap.get('id'));
this.sharedService.getRecipeById(id).subscribe(recipe => this.recipe == recipe);
}
}
Simple html for test:
<h2>Name : {{recipe.name}}</h2>
<h2>RecipeBody: {{recipe.recipeBody}}</h2>
<h2>IntendedUse: {{recipe.IntendedUse}}</h2>
<h2>CoffeeId: {{recipe.coffeeId}}</h2>
Results:
When I click on recipe name (recipe.component) it redirects me to: http://localhost:4200/recipes/recipes when it sould be for example http://localhost:4200/recipes/1
And in the console i get: TypeError: Cannot read properties of undefined (reading 'name')
Edit:
How i fetch it:
getRecipes() : Observable<any[]> {
return this.http.get<any>(this.ApiUrl + '/Recipes')
}
getRecipeById(val:any){
return this.http.get<any>(this.ApiUrl + '/Recipes/', val);
}
I would recommend doing it as below by subscribing
HTML
<h2><a [routerLink]="['/recipes', recipe.Id]" routerLinkActive="true">{{recipe.name}}</a></h2>
import { ActivatedRoute } from '#angular/router';
constructor(private route: ActivatedRoute,private sharedService: SharedService) {
ngOnInit() {
this.route.params
.subscribe(
(params: Params) => {
this.sharedService.getRecipeById(params['id']).subscribe(recipe => {
this.recipe = recipe;
})
}
);
}
}
or even make it better with switchMap
You need to use = to assign the values but not ==.
From:
this.sharedService.getRecipeById(id).subscribe(recipe => this.recipe == recipe);
To:
this.sharedService.getRecipeById(id).subscribe((recipe:any) => this.recipe = recipe);

How do I send data from a Component to another Component in Angular?

I'm very new to Angular, and I'm really struggling to find a concise answer to this problem. I have a Form Component Here:
(I'm excluding the directives and imports as they're not really relevant)
export class JournalFormComponent implements OnInit {
public entries: EntriesService;
constructor(entries: EntriesService) {
this.entries = entries;
}
ngOnInit(): void {
}
}
The EntriesService service just stores an array of entries:
export class Entry {
constructor (
public num: number,
public name: string,
public description: string,
public text: string
) { }
}
The Form Component template renders a <h2> and a <app-input> Component for each entry in the EntriesService, which works. That looks like this:
<div *ngFor="let entry of entries.entries">
<h2> {{ entry.num }}. {{ entry.name }} </h2>
<app-input id="{{entry.num}}"></app-input>
</div>
Here's the <app-input> Input Component:
#Component({
selector: 'app-input',
template: `
<textarea #box
(keyup.enter)="update(box.value)"
(blur)="update(box.value)">
</textarea>
`
})
export class InputComponent {
private value = '';
update(value: string) {
this.value = value;
}
getValue () {
return this.value;
}
}
The InputComponent stores the user's text perfectly, but I don't know how to pass that data to the Form Component's EntriesService to update the Entry in order to Export it or Save it later. How is this done?
I think I'm phrasing this question well, but I'm not sure. If you need clarification I'll provide it.
Not sure if it matters, but I'm using Angular 9.1.11
There are many ways to update the data from one component to another.
component to component using service or subjects
parent~child component data exchange using Input() and Output() decorators. Or by using #ViweChild() interactions.
and many more
But please do check the angular docs https://angular.io/guide/component-interaction .
Use the below simple code, u might need to include modules like FormsModule. and import Input(), Output etc
#Component({
selector: 'app-journal-form',
template: `
<div *ngFor="let entry of entries.entries; let i=index">
<h2> {{ entry.num }}. {{ entry.name }} </h2>
<app-input id="{{entry.num}}" [entry]="entry" [arrayIndex]="i" (updateEntry)="updateEntry($event)" ></app-input>
</div>`
})
export class JournalFormComponent implements OnInit {
constructor(private entries: EntriesService) {
this.entries = entries;
}
ngOnInit(): void {
}
updateEntry(event){
console.log(event);
this.entries[event.arrayIndex] = event.entry;
}
}
#Component({
selector: 'app-input',
template: `
<textarea [(ngModel)]="name"
(keyup.enter)="update()"
(blur)="update()">
</textarea>
`
})
export class InputComponent {
#Input() entry: any;
#Input() arrayIndex: number;
#Output() updateEntry: EventEmitter<any> = new EventEmitter();
name:string;
constructor() {
console.log(entry);
this.name = entry.name;
}
update(){
this.entry.name = this.name;
this.updateEntry.emit({entry: this.entry, arrayIndex});
}
}
Output event will help in this situation.
<div *ngFor="let entry of entries.entries">
<h2> {{ entry.num }}. {{ entry.name }} </h2>
<app-input id="{{entry.num}}" (entryChange) = "entry.text = $event"></app-input>
</div>
app-input component
export class InputComponent {
private value = '';
#Output() entryChange = new EventEmitter<string>();
update(value: string) {
this.value = value;
this.entryChange.emit(value);
}
}
Instead of entry.text = $event you can also pass it to any save function, like saveEntry($event);

Asynchronous call in angular using event emitter and services for cross-component communication

cannot store the value received from subscribe method in a template variable.
photo-detail component
import { Component, OnInit, Input } from "#angular/core";
import { PhotoSevice } from "../photo.service";
import { Photo } from "src/app/model/photo.model";
#Component({
selector: "app-photo-detail",
templateUrl: "./photo-detail.component.html",
styleUrls: ["./photo-detail.component.css"]
})
export class PhotoDetailComponent implements OnInit {
url: string;
constructor(private photoService: PhotoSevice) {
this.photoService.photoSelected.subscribe(data => {
this.url = data;
console.log(this.url);
});
console.log(this.url);
}
ngOnInit() {
}
}
the outside console.log gives undefined, and nothing is rendered in the view, but inside the subscibe method i can see the value.So, how can i display it in my view?
photos component
import { Component, OnInit } from "#angular/core";
import { ActivatedRoute, Params, Router } from "#angular/router";
import { FnParam } from "#angular/compiler/src/output/output_ast";
import { AlbumService } from "../service/album.service";
import { Photo } from "../model/photo.model";
import { PhotoSevice } from "./photo.service";
#Component({
selector: "app-photos",
templateUrl: "./photos.component.html",
styleUrls: ["./photos.component.css"]
})
export class PhotosComponent implements OnInit {
selectedAlbumId: string;
photoList: Photo[] = [];
photoSelected: Photo;
isLoading: Boolean;
constructor(
private rout: ActivatedRoute,
private albumService: AlbumService,
private router: Router,
private photoService: PhotoSevice
) { }
ngOnInit() {
this.isLoading = true;
this.rout.params.subscribe((params: Params) => {
this.selectedAlbumId = params["id"];
this.getPhotos(this.selectedAlbumId);
});
}
getPhotos(id: string) {
this.albumService.fetchPhotos(this.selectedAlbumId).subscribe(photo => {
this.photoList = photo;
this.isLoading = false;
});
}
displayPhoto(url: string, title: string) {
console.log(url);
this.photoService.photoSelected.emit(url);
this.router.navigate(["/photo-detail"]);
}
}
please explain me how this works and how to work around it so that i can store and display the value received from subscribing and asynchronous call in a template view.
here are the views of the two components---
photo.component.html
<div *ngIf="isLoading">
<h3>Loading...</h3>
</div>
<div class="container" *ngIf="!isLoading">
<div class="card-columns">
<div *ngFor="let photo of photoList" class="card">
<img
class="card-img-top"
src="{{ photo.thumbnailUrl }}"
alt="https://source.unsplash.com/random/300x200"
/>
<div class="card-body">
<a
class="btn btn-primary btn-block"
(click)="displayPhoto(photo.url, photo.title)"
>Enlarge Image</a
>
</div>
</div>
</div>
</div>
photo-detail.component.ts
<div class="container">
<div class="card-columns">
<div class="card">
<img class="card-img-top" src="{{ url }}" />
</div>
</div>
</div>
photo.service.ts
import { Injectable } from "#angular/core";
import { EventEmitter } from "#angular/core";
#Injectable({ providedIn: "root" })
export class PhotoSevice {
photoSelected = new EventEmitter();
// urlService: string;
}
here is a link to my github repo, i have kept the code in comments and used a different approach there.
If you check the albums component there also i have subscribed to http request and assigned the value in the template variable of albums component.
there also the value comes as undefined oustide the subscibe method, but i am able to access it in template.
https://github.com/Arpan619Banerjee/angular-accelerate
here are the details of albums component and service
pls compare this with the event emitter case and explain me whats the difference--
albums.component.ts
import { Component, OnInit } from "#angular/core";
import { AlbumService } from "../service/album.service";
import { Album } from "../model/album.model";
#Component({
selector: "app-albums",
templateUrl: "./albums.component.html",
styleUrls: ["./albums.component.css"]
})
export class AlbumsComponent implements OnInit {
constructor(private albumService: AlbumService) {}
listAlbums: Album[] = [];
isLoading: Boolean;
ngOnInit() {
this.isLoading = true;
this.getAlbums();
}
getAlbums() {
this.albumService.fetchAlbums().subscribe(data => {
this.listAlbums = data;
console.log("inside subscibe method-->" + this.listAlbums); // we have data here
this.isLoading = false;
});
console.log("outside subscribe method----->" + this.listAlbums); //empty list==== but somehow we have the value in the view , this doesn t work
//for my photo and photo-detail component.
}
}
albums.component.html
<div *ngIf="isLoading">
<h3>Loading...</h3>
</div>
<div class="container" *ngIf="!isLoading">
<h3>Albums</h3>
<app-album-details
[albumDetail]="album"
*ngFor="let album of listAlbums"
></app-album-details>
</div>
album.service.ts
import { Injectable } from "#angular/core";
import { HttpClient, HttpParams } from "#angular/common/http";
import { map, tap } from "rxjs/operators";
import { Album } from "../model/album.model";
import { Observable } from "rxjs";
import { UserName } from "../model/user.model";
#Injectable({ providedIn: "root" })
export class AlbumService {
constructor(private http: HttpClient) {}
albumUrl = "http://jsonplaceholder.typicode.com/albums";
userUrl = "http://jsonplaceholder.typicode.com/users?id=";
photoUrl = "http://jsonplaceholder.typicode.com/photos";
//get the album title along with the user name
fetchAlbums(): Observable<any> {
return this.http.get<Album[]>(this.albumUrl).pipe(
tap(albums => {
albums.map((album: { userId: String; userName: String }) => {
this.fetchUsers(album.userId).subscribe((user: any) => {
album.userName = user[0].username;
});
});
// console.log(albums);
})
);
}
//get the user name of the particular album with the help of userId property in albums
fetchUsers(id: String): Observable<any> {
//let userId = new HttpParams().set("userId", id);
return this.http.get(this.userUrl + id);
}
//get the photos of a particular album using the albumId
fetchPhotos(id: string): Observable<any> {
let selectedId = new HttpParams().set("albumId", id);
return this.http.get(this.photoUrl, {
params: selectedId
});
}
}
I have added console logs in the even emitters as told in the comments and this is the behavior i got which is expected.
Question's a two-parter.
Part 1 - photos and photo-detail component
EventEmitter is used to emit variables decorated with a #Output decorator from a child-component (not a service) to parent-component. It can then be bound to by the parent component in it's template. A simple and good example can be found here. Notice the (notify)="receiveNotification($event)" in app component template.
For your case, using a Subject or a BehaviorSubject is a better idea. Difference between them can be found in my other answer here. Try the following code
photo.service.ts
import { Injectable } from "#angular/core";
import { BehaviorSubject } from 'rxjs';
#Injectable({ providedIn: "root" })
export class PhotoSevice {
private photoSelectedSource = new BehaviorSubject<string>(undefined);
public setPhotoSelected(url: string) {
this.photoSelectedSource.next(url);
}
public getPhotoSelected() {
return this.photoSelectedSource.asObservable();
}
}
photos.component.ts
export class PhotosComponent implements OnInit {
.
.
.
displayPhoto(url: string, title: string) {
this.photoService.setPhotoSelected(url);
this.router.navigate(["/photo-detail"]);
}
}
photo-detail.component.ts
constructor(private photoService: PhotoSevice) {
this.photoService.getPhotoSelected().subscribe(data => {
this.url = data;
console.log(this.url);
});
console.log(this.url);
}
photo-detail.component.html
<ng-container *ngIf="url">
<div class="container">
<div class="card-columns">
<div class="card">
<img class="card-img-top" [src]="url"/>
</div>
</div>
</div>
</ng-container>
Part 2 - albums component and service
The call this.albumService.fetchAlbums() returns a HTTP GET Response observable. You are subscribing to it and updating the member variable value and using it in the template.
From your comment on the other answer:
i understand the behaviour and why the outside console.log is
underfined, its beacuse the execution context is diff for async calls
and it first executes the sync code and then comes the async code
I am afraid the difference between synchronous and asynchronous call is not as simple as that. Please see here for a good explanation of difference between them.
albums.components.ts
getAlbums() {
this.albumService.fetchAlbums().subscribe(data => {
this.listAlbums = data;
console.log("inside subscibe method-->" + this.listAlbums); // we have data here
this.isLoading = false;
});
console.log("outside subscribe method----->" + this.listAlbums); //empty list==== but somehow we have the value in the view , this doesn t work
//for my photo and photo-detail component.
}
albums.component.html
<div *ngIf="isLoading">
<h3>Loading...</h3>
</div>
<div class="container" *ngIf="!isLoading">
<h3>Albums</h3>
<app-album-details
[albumDetail]="album"
*ngFor="let album of listAlbums"
></app-album-details>
</div>
The question was to explain why the template displays the albums despite console.log("outside subscribe method----->" + this.listAlbums); printing undefined. In simple words, when you do outside console log, this.listAlbums is actually undefined in that it hasn't been initialized yet. But in the template, there is a loading check *ngIf="!isLoading". And from the controller code, isLoading is only set to false when listAlbums is assigned a value. So when you set isLoading to false it is assured that listAlbums contains the data to be shown.
I think you are trying to display a selected image in a photo detail component which gets the photo to display from a service.
The question doesn't mention how you are creating the photo detail component.
Is the component created after a user selects a photo to dislay?
Is the component created even before user selects a photo to display?
I think the first is what you are trying to do.
If so there are two things...
When you are subscribing inside the constructor, the code inside the subscribe runs after some time when the observable emits. in the mean time the code after the subscription i.e console.log(url) (the outside one) will run and so it will be undefined.
If the subscription happens after the event is emitted i.e you have emitted the event with url but by then the component didn't subscribe to the service event. so the event is lost and you don't get anything. For this you can do few things
a. Add the photo whose details are to be shown to the url and get it in the photo details component.
b. Convert the subject / event emitter in the service to behavioural subject. This will make sure that even if you subscribe at a later point of time you still get the event last emitted.
c. If the photo details component is inside the template of the photo component send the url as an input param (#Input() binding).
Hope this helps

How to call a function when element is loaded at Angular?

I want to call a function with an argument when an element is loaded.
Just like nginit in angualrjs. Can we do it in Angular 4 and above?
<div *ngFor="let item of questionnaireList"
(onload)="doSomething(item.id)" >
</div>
My Typescript function:
doSomething(id) {
console.log(id);
}
You need to write a directive
import {Directive, Input, Output, EventEmitter} from '#angular/core';
#Directive({
selector: '[ngInit]'
})
export class NgInitDirective {
#Input() isLast: boolean;
#Output('ngInit') initEvent: EventEmitter<any> = new EventEmitter();
ngOnInit() {
if (this.isLast) {
setTimeout(() => this.initEvent.emit(), 10);
}
}
}
Using in html
<div *ngFor="let quetionnaireData of questionnairelist ; let $last = last" [isLast]='$last'
(ngInit)="doSomething('Hello')"></div>
Also you declare your directive in app.module
#NgModule({
declarations: [
..
NgInitDirective
],
......
})
Use ngOnInit() and the #Input directive.
For example, in your child component:
import { Component, OnInit, Input } from '#angular/core';
#Component({
selector: 'my-component',
template: `
<h3>My id is: {{itemId}}</h3>
`
})
export class MyComponent implements OnInit
{
#Input() itemId: string;
//other code emitted for clarity
public ngOnInit(): void
{
// Now you can access to the itemId field e do what you need
console.log(this.itemId);
}
}
In your parent component
<div *ngFor="let item of questionnairelist">
<my-component itemId='{{item.Id}}'></my-component>
</div>
Your Function:
ExecuteMyFunction(value:any):void{
console.log(value);
}
If you wants to pass parameter which declared in component itself and set from component then try as below:
notificationMessage:string='';
#Input() message:string;
ngAfterViewInit(){
this.ExecuteMyFunction(this.notificationMessage);
}
If you set variable as Input parameter and set from other component then try as below: ngOnChanges will fire every time when your Input variable value is changed.
import { Component, OnChanges, Input } from '#angular/core';
ngOnChanges(changes: any) {
if (changes.message != null && changes.message.currentValue != null) {
this.ExecuteMyFunction(this.message);
}
}
HTML:
<ng-container *ngFor="let item of items">
<div *ngIf="doSomething(item.id)"></div>
</ng-container>
TS:
doSomething(value){
//logic
return true;
}
import { Router,NavigationEnd } from '#angular/router';
constructor( private router: Router ) {
this.router.events.subscribe((e) => {
if (e instanceof NavigationEnd) {
// Function you want to call here
}
});
}

Sharing and filtering a value between components in angular2

I have a component that pulls in a value posts like so:
import { Component, OnInit} from "#angular/core";
import template from "./event.component.html";
import style from "./event.component.scss";
#Component({
selector: "EventComponent",
template,
styles: [ style ]
})
export class EventComponent implements OnInit {
posts = [];
constructor() {}
ngOnInit() {
this.posts = {'test': 0,'test': 1};
}
}
This is then looped over in a html template like so AND injected into another component in this case called "mapCompenent" it is also filter in the html using a pipe:
loop 'EventComponent' content
<input id="search_events" type="text" name="search_events" [(ngModel)]="search" ngDefaultControl/>
<mapCompenent [(posts)]="posts"></mapCompenent>
<div class="col s6 m6 l4 cards-container" *ngFor="let post of posts | searchPipe:'name':search "></div>
filter
import { Pipe, PipeTransform, Input, ChangeDetectorRef } from '#angular/core';
import { FormGroup, FormControl, FormBuilder, Validators } from '#angular/forms';
#Pipe({
name : 'searchPipe',
pure: false,
})
export class SearchPipe implements PipeTransform {
public transform(value, key: string, term: string) {
if(term === '' || typeof term === undefined ){
return value;
}
return value.filter((item) => {
if (item.hasOwnProperty(key)) {
if (term) {
let regExp = new RegExp('\\b' + term, 'gi');
//this.ref.markForCheck();
return regExp.test(item[key]);
} else {
return true;
}
} else {
return false;
}
});
}
}
mapComponent
import { Component, OnInit, Input, OnChanges, SimpleChanges, SimpleChange } from "#angular/core";
import template from "./map.component.html";
import style from "./map.component.scss";
#Component({
selector: 'mapCompenent',
styles: [ style ],
template
})
export class MapComponent implements OnInit, OnChanges{
#Input() posts: object = {};
ngOnInit() {
}
ngOnChanges(changes: SimpleChanges) {
const posts: SimpleChange = changes.posts;
console.log('prev value: ', posts.previousValue);
console.log('got posts: ', posts.currentValue);
}
}
As soon as the page is loaded the mapcomponent grabs the ngOnChanges BUT not when the filter is used to filter the posts, the loop updates the posts fine and the filter works there the problem is the mapcomponent. What is the best way to notify the mapcomponent of a change to the posts Object?
The pipe will not overwrite the original posts property in EventComponent, so you are only using the filtered version in the *ngFor:
<input id="search_events" type="text" name="search_events" [(ngModel)]="search" ngDefaultControl/>
<mapCompenent [(posts)]="posts"></mapCompenent>
<div class="col s6 m6 l4 cards-container" *ngFor="let post of posts | searchPipe:'name':search "></div>
One solution is to add the pipe to the <mapComponent>'s posts attribute as well, but note it can't be two-way binded ([()]) then, you should change it to one-way ([]).
<input id="search_events" type="text" name="search_events" [(ngModel)]="search" ngDefaultControl/>
<mapCompenent [posts]="posts | searchPipe:'name':search"></mapCompenent>
<div class="col s6 m6 l4 cards-container" *ngFor="let post of posts | searchPipe:'name':search"></div>
A better solution would be to inject that pipe into the EventComponent constructor, listen for changes on the search input or watching search and update another attribute, let's say filteredPosts accordingly using the pipe, and use that one both in the *ngFor and the <mapCompenent>:
#Component({ ... })
export class EventComponent implements OnInit {
posts = [];
filteredPosts = [];
constructor(private searchPipe: SearchPipe) {}
ngOnInit() {
this.posts = ...;
this.form.search.valueChanges.subscribe((value) => {
this.filteredPosts = this.searchPipe.transform(this.posts, 'name', value);
});
}
}

Categories