I am writing a simple counter. It has start,stop, toggle functionality in parent (app) and displaying changed value in child (counter) component using ChangeDetectionStrategy.OnPush.
Issue I am facing is not able to display initial counter value in child component on load.
Below are screenshot and code.
app.component.ts
import { Component } from '#angular/core';
import {BehaviorSubject} from 'rxjs';
#Component({
selector: 'app-root',
template: `<h1>Change Detection</h1>
<button (click)="start()">Start</button>
<button (click)="stop()">Stop</button>
<button (click)="toggleCD()">Toggle CD</button>
<hr>
<counter [data]="data$" [notifier]="notifier$"></counter>`,
})
export class AppComponent {
_counter = 0;
_interval;
_cdEnabled = false;
data$ = new BehaviorSubject({counter: 0});
notifier$ = new BehaviorSubject(false);
start() {
if (!this._interval) {
this._interval = setInterval((() => {
this.data$.next({counter: ++this._counter});
}), 10);
}
}
stop() {
clearInterval(this._interval);
this._interval = null;
}
toggleCD(){
this._cdEnabled = !this._cdEnabled;
this.notifier$.next(this._cdEnabled);
}
}
counter.component.ts
import {Component, Input, ChangeDetectionStrategy, OnInit, ChangeDetectorRef} from '#angular/core';
import {Observable} from 'rxjs/index';
#Component({
selector: 'counter',
template: `Items: {{_data.counter}}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent implements OnInit {
#Input() data: Observable<any>;
#Input() notifier: Observable<boolean>;
_data: any;
constructor(private cd: ChangeDetectorRef) {}
ngOnInit() {
this.data.subscribe((value) => {
/**
Below this._data.counter is showing 0 in console.log but
not in template
**/
this._data = value;
this.cd.markForCheck();
});
this.cd.detach();
this.notifier.subscribe((value) => {
if (value) {
this.cd.reattach();
} else {
this.cd.detach();
}
});
}
}
I'm using Angular 6.1.0
your AppComponent data$ is a BehaviorSubject, which you have given an initial value. your CounterComponent data expects an Observable, which you subscribe to. The defaulted BehaviorSubject does not fire until it changes. to get the value you have to query it upon load:
#Input() data: BehaviorSubject<any>;
ngOnInit() {
this._data = this.data.value; // get the initial value from the subject
this.data.subscribe((value) => {
this._data = value;
this.cd.markForCheck();
}
);
should do the trick.
Related
I defined a property here in my function
evs: string
...
openArticle(url){
this.evs = url
console.log(this.evs)
this.navCtrl.navigateForward('/url-page')
}
And I a trying to pass the value of 'this.evs' to another ts file and use its value but I do not know how to do this. I tried exporting it like this.
export const webpage = this.evs
but this.evs has no value until someone performs the openArticle function ad so I keep getting the error. "Cannot read property 'evs' of undefined"
What i need to do is tranfer the variable to the 'url-page' page and use the value of this.evs only after the openArticle function has bee called. How do I go about this?
As per my understanding you are trying to share data between two components.
So choose one of them as per your requirements.
Parent to Child: Sharing Data via Input().
Child to Parent: Sharing Data via Output() and EventEmitter.
Unrelated Components: Sharing Data with a Service.
This link will be helpful.
If the components have a parent/child relationship, You can share data between them via #Inpput() and #Output() decorators.
Sharing data from Parent to Child using #Input() :
<h3>Parent Component</h3>
<label>Parent Component</label>c
<input type="number" [(ngModel)]='parentValue'/>
<p>Value of child component is: </p>
<app-child [value]='parentValue'></app-child>
And in the child component, the 'parentValue' can be received as :
import { Component, OnInit, Input } from '#angular/core';
#Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.css']
})
export class ChildComponent implements OnInit {
#Input() value: number;
constructor() { }
ngOnInit() {
}
}
Now, in the case of sending data from Child to Parent, we can use an #Output() event emitter. So the parent would have a function to receive the emitted data from child as :
parent-app.component.html
<app-child [value]="parentValue" (childEvent)="childEvent($event)"></app-child>
parent-app.component.ts
childEvent(event) {
console.log(event);
}
And, the child.component.ts would look like :
import { Component, OnInit, Input, Output, EventEmitter } from '#angular/core';
#Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.css']
})
export class ChildComponent implements OnInit {
#Input() PData: number;
#Output() childEvent = new EventEmitter();
constructor() { }
onChange(value) {
this.childEvent.emit(value);
}
ngOnInit() {
}
}
If the components do not have a parent/child relationship, a shared service can be used, say, SharedService which has a BehavioralSubject, that emits value from either component, and the other component can then catch the changed value.
Eg:
import { Injectable } from '#angular/core';
import { BehaviorSubject } from "rxjs/BehaviorSubject";
#Injectable()
export class SharedService {
comp1Val: string;
_comp1ValueBS = new BehaviorSubject<string>('');
comp2Val: string;
_comp2ValueBS = new BehaviorSubject<string>('');
constructor() {
this.comp1Val;
this.comp2Val;
this._comp1ValueBS.next(this.comp1Val);
this._comp2ValueBS.next(this.comp2Val);
}
updateComp1Val(val) {
this.comp1Val = val;
this._comp1ValueBS.next(this.comp1Val);
}
updateComp2Val(val) {
this.comp2Val = val;
this._comp2ValueBS.next(this.comp2Val);
}
And component1 as follows :
import { Injectable } from '#angular/core';
import { BehaviorSubject } from "rxjs/BehaviorSubject";
#Injectable()
export class SharedService {
comp1Val: string;
_comp1ValueBS = new BehaviorSubject<string>('');
comp2Val: string;
_comp2ValueBS = new BehaviorSubject<string>('');
constructor() {
this.comp1Val;
this.comp2Val;
this._comp1ValueBS.next(this.comp1Val);
this._comp2ValueBS.next(this.comp2Val);
}
updateComp1Val(val) {
this.comp1Val = val;
this._comp1ValueBS.next(this.comp1Val);
}
updateComp2Val(val) {
this.comp2Val = val;
this._comp2ValueBS.next(this.comp2Val);
}
Component 2 :
import { Component, AfterContentChecked } from '#angular/core';
import { SharedService } from "../../common/shared.service";
#Component({
selector: 'app-component2',
templateUrl: './component2.component.html',
styleUrls: ['./component2.component.css']
})
export class Component2Component implements AfterContentChecked {
comp1Val: string;
comp2Val: string;
constructor(private sharedService: SharedService) {
this.sharedService.comp2Val = "Component 2 initial value";
}
ngAfterContentChecked() {
this.comp1Val = this.sharedService.comp1Val;
}
addValue(str) {
this.sharedService.updateComp2Val(str);
}
}
You can find more on different types of subjects here
ngDestroy lifecycle method is not triggering for dynamically created component.
I'm creating multiple component dynamically using ComponentFactoryResolver.
In my dynamically created component I'm fetching some data from API and I'm periodically fetching the data for every 5 mins using setInterval method. and I'm clearing the Interval instance inside ngDestroy method , while redirecting to different page, the component's ngDestroy is not triggering and the API is triggering even the component is not in the view.
This is how I'm creating components dynamically.
const factory = this.resolver.resolveComponentFactory(DynamicComponent); // Component Construction
const ref = factory.create(this.injector);
Here is my DynamicComponent which has the functionalities
import { Component, OnInit, OnDestroy } from "#angular/core";
#Component({
selector: "app-dynamic,
templateUrl: "./dynamic.component.html",
styleUrls: ["./dynamic.component.scss"]
})
export class DynamicComponent implements OnInit, OnDestroy {
loopCount: number;
autoRefreshInterval: any;
constructor() {}
ngOnInit() {
this.fetchData();
this.startAutoRefreshLoop();
}
ngOnDestroy(): void {
console.log("Destroying loop"); // ngOnDestroy is not triggering
this.clearAutoRefreshLoop();
}
clearAutoRefreshLoop() {
clearInterval(this.autoRefreshInterval);
}
/*
function for starting the Automatically recall the service for certain period of time
*/
startAutoRefreshLoop() {
console.log("starting loop");
this.loopCount = 10 * 1000;
this.autoRefreshInterval = setInterval(() => {
this.fetchData();
}, this.loopCount);
}
fetchData() {
// FETCHING DATA FROM API CODE ....
}
}
You need to destroy the dynamically loaded component by manually calling : this.componentRef.destroy(); to trigger ngOndestroy()
Example :
import {
Component,
ViewChild,
ViewContainerRef,
ComponentFactoryResolver,
ComponentRef,
ComponentFactory
} from '#angular/core';
import { DynamicComponent } from './dynamic.component';
#Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
title = 'app';
componentRef: any;
#ViewChild('container', { read: ViewContainerRef }) entry: ViewContainerRef;
constructor(private resolver: ComponentFactoryResolver) { }
createComponent(message) {
this.entry.clear();
const factory = this.resolver.resolveComponentFactory(DynamicComponent);
this.componentRef = this.entry.createComponent(factory);
}
destroyComponent() {
this.componentRef.destroy(); // you need to call this
}
}
For more information : how-to-dynamically-create-a-component-in-angular
I have three components. A parent component (dashboard.component.ts) which houses two siblings (poll-form.component.ts and poll.component.ts). One of the siblings, poll-component sends an Output() back to the parent called (editEvent)="editPoll($event)". When this Output() is sent I'd like that to trigger a function inside of the other sibling poll-form.component instead of triggering the function in the parent dashboard.component. What's the correct way of doing this?
My current implementation "sort of" works. It fires the function in the parent and then the parent imports the component and calls the function in the sibling component. The issue, however, is that sibling component skips the ngOnInit() and doesn't have access to the component variables needed within the function.
Ultimately what I'm trying to do is break out the form into its own component but the act of opening the form is triggered by a sibling component.
dashboard.component.html
<div class="page">
<div style="margin-bottom: 20px; margin-right: 20px; text-align: left;">
<button type="button" (click)="showCreateModal()" pButton icon="pi pi-check" label="Create Poll"></button>
</div>
<div *ngFor="let pollId of pollIds" style="display: inline-block;">
<app-poll [pollKey]="pollId" (editEvent)="editPoll($event)" (deleteEvent)="deletePoll($event)"></app-poll>
</div>
</div>
<div *ngIf="displayCreateModal">
<app-poll-form [type]="'create'" (closeEvent)="closeModal($event)"></app-poll-form>
</div>
<div *ngIf="displayEditModal">
<app-poll-form [type]="'edit'" (closeEvent)="closeModal($event)"></app-poll-form>
</div>
poll.component.ts
import { Component, OnInit, Input, Output, EventEmitter } from '#angular/core';
import * as Chart from 'chart.js';
import { Observable } from 'rxjs';
import { FirebaseService } from '../services/firebase.service';
import { first } from 'rxjs/operators';
import { CardModule } from 'primeng/card';
import { AngularFireAuth } from '#angular/fire/auth';
#Component({
selector: 'app-poll',
templateUrl: './poll.component.html',
styleUrls: ['./poll.component.scss']
})
export class PollComponent implements OnInit {
poll:any;
#Input()
pollKey: string;
#Output()
editEvent = new EventEmitter<string>();
constructor(private firebaseService: FirebaseService, private afAuth: AngularFireAuth) { }
ngOnInit() {
this.firebaseService.getPoll(this.pollKey).subscribe(pollDoc => {
if (!pollDoc.payload.exists) {
return;
}
const pollData:any = pollDoc.payload.data();
this.poll = {
id: pollDoc.payload.id,
helperText: pollData.helperText,
pollType: pollData.pollType,
scoringType: pollData.scoringType,
user: pollData.user
};
}
edit() {
this.editEvent.emit(this.poll);
}
}
poll-form.component.ts
import { Component, OnInit, Input, Output, EventEmitter } from '#angular/core';
import { DialogModule } from 'primeng/dialog';
import { DropdownModule } from 'primeng/dropdown';
import { AutoCompleteModule } from 'primeng/autocomplete';
import { SelectButtonModule } from 'primeng/selectbutton';
import { FirebaseService } from '../services/firebase.service';
import nflPlayers from '../../assets/data/players-nfl.json';
import nflPollTypes from '../../assets/types/poll-types-nfl.json';
import nflScoringTypes from '../../assets/types/scoring-types-nfl.json';
#Component({
selector: 'app-poll-form',
templateUrl: './poll-form.component.html',
styleUrls: ['./poll-form.component.scss']
})
export class PollFormComponent implements OnInit {
title:string;
btnLabel:string;
selectedScoringType:any;
choices:any;
selectedPollType:any;
selectedPollKey:any;
displayEditModal:boolean = false;
displayCreateModal:boolean = false;
filteredPlayersMultiple: any[];
displayModal:boolean = true;
nflPlayers:any = nflPlayers.Players;
nflPollTypes:any = nflPollTypes.types;
nflScoringTypes:any = nflScoringTypes.types;
activePlayers:any;
#Input()
type: string;
#Output()
closeEvent = new EventEmitter<string>();
constructor(
private firebaseService: FirebaseService
) { }
ngOnInit() {
this.initFormDefaults();
}
initFormDefaults() {
this.choices = [[],[]];
this.selectedScoringType = this.nflScoringTypes[0];
this.selectedPollType = this.nflPollTypes[0];
if (this.type == "create") {
this.btnLabel = "Create";
this.title = "Create Poll";
} else {
this.btnLabel = "Update";
this.title = "Edit Poll";
}
// Filter out active NFL players
this.activePlayers = this.nflPlayers.filter(player => player.active == "1");
}
editPoll(poll) {
this.type = "edit"
this.activePlayers = this.nflPlayers.filter(player => player.active == "1");
this.selectedPollKey = poll.id;
// Set scoring type
this.selectedScoringType = this.nflScoringTypes.find((type) => {
return type.code == poll.scoringType
});
// Set poll type
this.selectedPollType = this.nflPollTypes.find((type) => {
return type.code == poll.pollType
});
// Populate edit modal with properly formatted Player objects
for (let i=0; i < poll.choices.length; i++) {
this.choices[i] = poll.choices[i].players.map((choicePlayer:any) => {
return this.activePlayers.find(player => player.playerId == choicePlayer.id);
});
}
}
}
dashboard.component.ts
import { Component, OnInit } from '#angular/core';
import { AngularFireAuth } from '#angular/fire/auth';
import { first } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { PollFormComponent } from '../poll-form/poll-form.component';
#Component({
providers: [PollFormComponent],
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss']
})
export class DashboardComponent implements OnInit {
displayEditModal:boolean = false;
constructor(
private pollFormComponent: PollFormComponent
) { }
ngOnInit() {
}
editPoll(poll) {
this.displayEditModal = true;
this.pollFormComponent.editPoll(poll);
}
}
I have already done the project using simple java script. Now Its revamping as SPA using Angular.
Now I'm stucked to do the same using Angular.
Functionality:
Click the button to disable and append in particular div and if button clicks inside appended div then previously disabled button to be enabled.
That's it.
I have done other than to enable disabled button:
Problem is pBtn not available in ElementRef
Below is my code and stackblitz link:
Hope someone could help in this.
import { Component, OnInit, DoCheck, AfterViewInit, ViewChild, ElementRef,Renderer2 } from '#angular/core';
import { Interviewcreate } from '../../shared/interview-create';
import { Interview } from '../../shared/interview';
import { DataService } from '../../data-service';
import { Router } from '#angular/router';
import { HttpClient, HttpHeaders } from '#angular/common/http';
#Component({
selector: 'dashboard-component',
templateUrl: './dashboard-component.html',
styleUrls: [ './dashboard-component.css' ]
})
export class DashboardComponent implements OnInit, DoCheck, AfterViewInit, OnChanges {
users: Interviewcreate;
#ViewChild('answerbox') div:ElementRef;
#ViewChild('htmlToAdd') htmlToAdd:ElementRef;
#ViewChild('questionbox') questionbox:ElementRef;
question1 = ['<p>', '</p>', 'Polar bears live in the north pole']
constructor(private service: DataService,
private router: Router,
private http:HttpClient,
private renderer: Renderer2,
private el:ElementRef
){
}
ngOnInit(){
}
ngDoCheck(){
if(this.htmlToAdd.nativeElement.children.length>0){
Array.prototype.forEach.call(this.htmlToAdd.nativeElement.children, (element) => {
//console.log(element)
element.addEventListener('click', (e)=>{
this.resultview()
console.log(e)
e.target.remove()
})
});
}
}
ngAfterViewInit() {
let sss = this.el.nativeElement.querySelector('.dotted-box > button')
//.addEventListener('click', this.onClick.bind(this));
}
onClick(event) {
console.log(event);
}
getvalue(e){
const li = this.renderer.createElement('button');
const text = this.renderer.createText(e.target.textContent);
this.renderer.appendChild(li, text);
this.renderer.appendChild(this.htmlToAdd.nativeElement, li);
setTimeout(
()=>{
this.resultview()
}
,100)
e.target.disabled = true;
Array.prototype.forEach.call(this.htmlToAdd.nativeElement.children, (element) => {
this.renderer.addClass(element, 'btn');
this.renderer.addClass(element, 'btn-outline-primary');
});
}
resultview() {
this.div.nativeElement.innerHTML = this.htmlToAdd.nativeElement.textContent.trim();
}
}
Try out this, you had written some hard logic.
Push appending value will solve your problem.
export class DashboardComponent {
#ViewChild('answerbox') div:ElementRef;
#ViewChild('htmlToAdd') htmlToAdd:ElementRef;
#ViewChild('questionbox') questionbox:ElementRef;
question1 = ['<p>', '</p>', 'Polar bears live in the north pole' ]
questionboxvalue = [];
#Output() someEvent = new EventEmitter<string>();
constructor(private service: DataService,
private router: Router,
private http:HttpClient,
private renderer: Renderer2,
private el:ElementRef
){
}
onClick(event) {
console.log(event);
}
getvalue(e){
this.questionboxvalue.push({index: e.target.dataset.index, value: e.target.textContent.trim()})
e.target.disabled = true;
this.resultview();
}
getbvalue(event) {
this.someEvent.next(event);
Array.prototype.forEach.call(this.el.nativeElement.querySelectorAll('.shadowbutton'), (element, i)=>{
if(element.dataset.index === event.target.dataset.index) {
element.disabled = false;
this.questionboxvalue = this.questionboxvalue.filter((val)=>{
return val.index !== event.target.dataset.index;
})
this.resultview()
}
})
}
resultview() {
setTimeout(()=>{
this.div.nativeElement.innerHTML = this.htmlToAdd.nativeElement.textContent.trim();
}, 100)
}
}
So I have two not related components and I'm trying to communicate between them using a service and a BehaviorSubject. Everything is cool, data is exchanged, but when i call the service from one of the components, it doesn't trigger change detection on the other component.
So to show what I'm talking about in code:
The service:
import {Injectable, Optional, EventEmitter} from '#angular/core';
import {Http} from '#angular/http';
import 'rxjs/add/operator/map';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { forEach } from '#angular/router/src/utils/collection';
#Injectable()
export class SbkService {
items: any = [];
private _itemsSource = new BehaviorSubject<any>(0);
items$ = this._itemsSource.asObservable();
constructor (
private _localStorageService: LocalStorageService
) {}
storeSelection(item) {
this.items.push(item);
this.setLocalStorage();
}
removeSelection(selectionId) {
for (var i = this.items.length-1; i >= 0; i--) {
if (this.items[i].selectionId == selectionId)
this.items.splice(i, 1);
}
this.setLocalStorage();
return true;
}
getLocalStorage() {
this.items = this._localStorageService.get('items');
this._itemsSource.next(this.items);
return this.items;
}
setLocalStorage() {
this._localStorageService.set('items', this.items);
this._itemsSource.next(this.items);
return true;
}
}
Component 1:
import { Component, OnInit } from '#angular/core';
import { SbkService } from '../../services/sbk.service'
import {Subscription} from 'rxjs/Subscription';
#Component({
selector: 'app-right-sidebar',
template: `<ul>
<li *ngFor="let selection of selections">
{{selection.name}}
<span class="cutom-btn" (click)="remove(selection.selectionId)">
delete
</span>
</li>
</ul>`,
styles: []
})
export class RightSidebarComponent implements OnInit {
selections: any = [];
subscription:Subscription;
constructor (
private _sbkService: SbkService
) {
}
ngOnInit() {
this.subscription = this._sbkService.items$
.subscribe(selections => {
this.selections = selections })
this._sbkService.getLocalStorage();
}
ngOnDestroy() {
// prevent memory leak when component is destroyed
this.subscription.unsubscribe();
}
remove(selectionId) {
this._sbkService.removeSelection(selectionId);
}
}
Component 2:
import { Component, ViewChild, ElementRef } from '#angular/core';
import 'rxjs/add/operator/map';
import {forEach} from '#angular/router/src/utils/collection';
import {SbkService} from '../services/sbk.service'
#Component({
selector: 'app-match-table',
template: `
<div (click)="addItem('mumble', 1)">Add mumble</div>
<div (click)="addItem('ts', 2)">Add ts</div>
<div (click)="addItem('discord', 3)">Add discord</div>
`,
styles: []
})
export class MatchTableComponent {
constructor(
private _sbkService: SbkService
) {}
//Place a bet in the betslip
public addItem = (name, selectionId) => {
item: Object = {};
item.selectionId = selectionId;
item.name = name;
this._sbkService.storeSelection(item);
}
}
So, when I click on a div from component 2 (MatchTableComponent) it updates the selections array in component 1 (RightSideBarComponent) but doesn't trigger a change detection, so the sorted list doesn't get updated until i refresh the page. When i click on delete from RightSideBarComponent template, it updates the selections array and triggers the change detection.
How can I make this work? I tried subscribing to an event from SbkService in the AppComponent and from there triggering the setLocalStorage from SbkService, but no luck...
If I'm not wrong, you should set the next "sequence" on your Observable "items" through your BehaviourSubject.
Could you modify and try this?:
storeSelection(item){
const itemsAux = this._itemsSource.getValues();
itemsAux.push(item);
this._itemsSource.next(itemsAux);
}
setLocalStorage(){
this._localStorageService('items', this._itemsSource.getValues();
return true;
}