I am trying invoke parent component from child component that nested within <router-outlet> tag. In my case, I tried to invoke timer.start() function that start the timer which lies within parent component.
I have succefully invoked the parent's function by importing to the child, but the timer is not working. I have tried to log the flag that indicated if the timer is running or not, and it's already in true condition.
Here is the code:
import { NavbarComponent } from './../navbar/navbar.component'; /* This is the parent component */
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-css-inoculation-scoring',
templateUrl: './css-inoculation-scoring.component.html',
styleUrls: ['./css-inoculation-scoring.component.scss'],
providers: [ NavbarComponent ]
})
export class CSSInoculationScoringComponent implements OnInit {
constructor(private _service: DataModelService, private _navbar: NavbarComponent) {}
ngOnInit() {
this.vessel.getVessel();
this._navbar.timer.start();
}
}
And this is the timer.start function:
start: () => {
this.timer.isTicking = true;
this.timer.startTime();
}
The timer.start function also called another function, here is the timer.startTimer() function:
startTime: () => {
if (this.timer.isTicking) {
let hour = parseInt(this.timer.hour, 10);
let minute = parseInt(this.timer.minute, 10);
let second = parseInt(this.timer.second, 10);
second += 1;
if (second > 60) {
minute += 1;
second = 0;
}
if (minute > 60) {
hour += 1;
minute = 0;
}
this.timer.second = second < 10 ? `0${second}` : `${second}`;
this.timer.minute = minute < 10 ? `0${minute}` : `${minute}`;
this.timer.hour = hour < 10 ? `0${hour}` : `${hour}`;
setTimeout(this.timer.startTime, 1000);
}
}
I have idea to change the value of isTicking through the service, and return the observable. I have another case similar, and it's work. But in timer.startTime() function also modified the properties of timer. Should i also use service for that? Or is there any other approach?
I assume that you want to call parent's method with parent's context.
I would recommend to avoid passing components as services, because if there's a functionality that needs to be shared - it should be a service. But if a child needs to trigger parent's method within parent's context, then you can pass it to a child and call if from there.
// child component
import { Component, EventEmitter, OnInit } from '#angular/core';
#Component({
selector: 'app-css-inoculation-scoring',
templateUrl: './css-inoculation-scoring.component.html',
styleUrls: ['./css-inoculation-scoring.component.scss'],
})
export class CSSInoculationScoringComponent implements OnInit {
#Output() startTimer: EventEmitter<any> = new EventEmitter();
constructor(private _service: DataModelService)
ngOnInit() {
this.vessel.getVessel();
this.startTimer.emit();
}
}
// PARENT COMPONENT TEMPLATE
<targeting
(startTimer)="timer.start()">
</targeting>
Theres no way a child component can invoke a function on its parent component directly. Using an EventEmitter as per the above example is the closest thing to it. But if your child component is declared as a child route in a routing module, you wont be able to do this (there now way to bind the event emitter).
I would suggest moving your timer logic into a shared service that can be injected into both components. This way you can have either component call the start functions as and when they need to.
If you provide this service as a singleton (by providing it only once via your module), you will be able to keep track of whether the timer is running via your isTicking flag.
Related
In console this.unselectedPlayerList.length is not shown, It will shown before using splice method. So I have doubt in splice method.
export default class MakeYourTeamChild extends LightningElement {
#api unselectedPlayerList=[];
SelectPlayer(event)
{
for(let index = 0 ; index < this.unselectedPlayerList.length; index++)
{
if(this.unselectedPlayerList[index].Name == event.target.title)
{
this.selectedPlayer = this.unselectedPlayerList[index].Name;
this.unselectedPlayerList.splice(index,1);
console.log('After Splice',this.unselectedPlayerList.length);
}
}
}
}
As per my understanding, we can't update or edit the #api variable by using splice(), push(), and concat() methods. So, you have to replicate the #api variable in another temp variable and implement your logic on that temp variable. Assign back the temp variable to the #api variable. Have a look at the below code for reference:
export default class MakeYourTeamChild extends LightningElement {
#api unselectedPlayerList=[];
**let tempUnsltPlList = [];**
SelectPlayer(event)
{
for(let index = 0 ; index < this.tempUnsltPlList.length; index++)
{
if(this.tempUnsltPlList[index].Name == event.target.title)
{
this.selectedPlayer = this.tempUnsltPlList[index].Name;
this.tempUnsltPlList.splice(index,1);
console.log('After Splice',this.tempUnsltPlList.length);
}
}
**this.unselectedPlayerList = [...tempUnsltPlList];**
}
}
I hope it will help you. If yes, mark it as the best answer.
Feel free to reach out to me!
You have a parent / child communication issue as I understand. As always, the best know approach is "props down, event up". This will ensure you easily understand how & when a variable change within a component and component behavior will not be changed by it's inner child component.
In your case, unselectedPlayerList is a prop on the child component filled by its parent component. This means parent component is owner of the data and controlling this prop value. If child component wants to modify the value of this value, it needs to ask the parent to do so, this is done by emitting an event telling the parent component what to do.
export default class ParentComponent extends LightningElement {
unselectedPlayerList = []
handleSelectPlayer (event) {
const playerName = event.detail.playerName
const playerIndex = this.unselectedPlayerList.findIndex(player => player.Name === playerName)
const shallowPlayerList = [ ...this.unselectedPlayerList ]
shallowPlayerList.splice(playerIndex, 1)
this.unselectedPlayerList = shallowPlayerList
}
}
<template>
<c-child-component
unselected-player-list={unselectedPlayerList}
onselectplayer={handlePlayerSelect}
></c-child-component>
</template>
export default class ChildComponent extends LightningElement {
#api unselectedPlayerList = []
handleSelectPlayer (event) {
this.dispatchEvent(
new CustomEvent('selectplayer', {
detail: {
playerName: event.target.title,
}
})
)
}
}
There also is another way to write the parent component if you'd like using the #track decorator.
In LWC all component attributes are reactive with a small exception around objects (array, js object). Changing an inner property of an object will not rerender the view unless decorating that object.
You can use this js for the parent component for the exact same result.
export default class ParentComponent extends LightningElement {
#track unselectedPlayerList = []
handleSelectPlayer (event) {
const playerName = event.detail.playerName
const playerIndex = this.unselectedPlayerList.findIndex(player => player.Name === playerName)
this.unselectedPlayerList.splice(playerIndex, 1)
}
}
I would not recommend using the #track version as it's heavier for performance and in any case you do stuff in a loop, every modification to the array will/might trigger view update. In first version you are assured that view will update only once which is also giving you more control on how the view (or dependent properties like getters) behave.
I hope this will help you and I invite you to read these articles:
Reactivity
Events
I am still learning and I got stuck so I need to ask a question. My understanding of Input Output decorators is that I need to add selector to html of parent to be able to use them, but for my case I don't think it's the way to go, but someone can prove me wrong.
CASE: For readability purposes I have split components. I have one component, data-fetch-transform that gets the data form local JSON file and does some adjustments to it, and another one, that wants to take that data for further use.
PROBLEM: I am unsure how to read the data from one component in the other. On the example below, how can I get countryNumber and centerNumber result in my other component. I intend to have data-fetch-transform.component.ts just manipulate the data and used in other components
Target component
project/src/app/data-use/data-use.component.ts
Data Source component
project/src/app/data-fetch-transform/data-fetch-transform.component.ts
import { Component, OnInit } from '#angular/core';
import * as data from '../../../../../data/Data.json';
#Component({
selector: 'app-datafetch-transform',
templateUrl: './datafetch-transform.component.html',
styleUrls: ['./datafetch-transform.component.css'],
})
export class DatafetchComponent implements OnInit {
public dataList: any = (data as any).default;
dataPointCount = this.data.length!!;
uniqueValues = (dt: [], sv: string) => {
var valueList: [] = [];
for (let p = 0; p < this.dataPointCount; p++) {
valueList.push(dt[p][sv]);
}
var uniqueValues = new Set(valueList);
return uniqueValues.size;
};
countryNumber=this.uniqueValues(this.dataList, 'Country')
centerNumber=this.uniqueValues(this.dataList, 'Center Name')
constructor() {}
ngOnInit(): void {}
}
You don't need another component for data manipulation (data-fetch-transform), you need a service (data-fetch-transform-service) where you should do the logic.
HERE IS WHAT YOU SHOULD HAVE IN THE SERVICE
private _dataList = new behaviorSubject([]);
public dataList$ = _dataList.asObservable();
for (let p = 0; p < this.dataPointCount; p++) {
// ... do your thing
_valueList.next(result);
}
and in the component you just subscribe to the service:
declarations:
private _subscription = new Subscription()
in constructor:
private dataService:DataFetchTransformService
and in ngOnInit:
this_subscription.add(this.dataService.dataList$.subscribe((response:any)=>{
this.data = response;
}))
in ngOnDestroy():
ngOnDestroy(){
this._subscription.unsubscribe();
}
I strongly suggest to stop using any since it can bring a lot of bugs up.
Also, as a good pattern, I always suggest use behaviorSubject only in the service as a private variable and user a public observable for data.
WHY IS BETTER TO USE A SERVICE
You can subscribe from 100 components and writing only 4 lines of code you bring the data anywhere.
DON'T FORGET TO UNSUBSRIBE IN ngOnDestroy
If you don't unsubscribe, you'll get unexpected behavior.
I am trying to set a value through component
export class SomeComponent {
constructor(service:SomeService){
}
count =0 ;
onRefresh(){
this.service.count = 1;
}
}
I want to use this value in the service like
SomeService{
count:any;
doSomething(){
//use count value here
console.log('count value saved thru compoent',count);
}
}
All I want to do is use the value stored through component in some service. When I tried this, I am getting an undefined count.
It's hard to give a great answer since I'm not sure what you are trying to achieve, but here is a simple solution based on the information provided without too many tweaks.
I made a button in the AppComponent HTML that will call a function called onRefresh() in order to show how this is passed from a function. Once this button is clicked, here is the function that will be run:
<button type="button" (click)="onRefresh()">Count Button in Service</button>
onRefresh() {
this.service.count = 1;
this.service.doSomething();
}
Here is the code for the service.ts
import { Injectable } from "#angular/core";
#Injectable({
providedIn: "root"
})
export class SomeServiceService {
count: any;
doSomething() {
console.log("count value saved thru compoent", this.count);
}
}
No when you click the button, it will set the service count var to 1, and log it to the console. Hope this helps. Here is a StackBlitz with the same code provided above
When I call my service injected in the constructor, I get undefined.
the service is called in ngOnInit method, From Difference between Constructor and ngOnInit I have seen that constructor run first , but in my case I noted the opposite, so I'm bit confused. have someone more explication about that, thanks.
constructor(private curveService :ProgressCurveService, private util : UtilService) {
this.startPickerOptions = new DatePickerOptions();
this.endPickerOptions = new DatePickerOptions();
//this.datePickerOptions.initialDate = new Date(Date.now());
}
ngOnInit() {
this.curveService.instance.getCurve(this.startDate.formatted,this.endDate.formatted,this.amplutid).
then(res => {
this.lineChartLabels = this.util.dateToShortString(Object.keys(res.progressPlotData))
this.lineChartData = this.util.objectToIntArray(res.progressPlotData);
}).catch(res => console.log('error if date selecting ...'));
}
progress curve service:
import { progressCurveItf } from './progress-curve/progress-curve-interface';
#Injectable()
export class ProgressCurveService {
state : string = 'project';
constructor(private prsCurve : PrsProgressCurveService, private projCurve : ProjProgressCurveService) { }
get instance():progressCurveItf{
if(this.state == 'subproject'){
return this.prsCurve;
} else {
return this.projCurve;
}
}
}
While you're returning an instance of type progressCurveItf that is an interface I think there is something wrong with the instantiation of what you're returning,
check if you provide PrsProgressCurveService and ProjProgressCurveService.
To answer your question Constructor will get invoked since it belongs to ES6, basically which got the first priority. Where as ngOnInit is a life cycle hook designed by angular team which will get invoked after constructor even after ngOnChanges life cycle hook.
Constructor -> ngOnChanges -> ngOnInit -> followed by other life cycle hooks
I have component like below which is basically a popover:
import {Component, Input, ViewChild} from 'angular2/core'
declare var $: any;
#Component({
selector: 'popover',
template: `
<div id="temp" [ngStyle]="{'position':'absolute', 'z-index':'10000', 'top': y + 'px', left: x + 'px'}"
[hidden]="hidden" #temp>
<ng-content></ng-content>
</div>
`
})
export class Popover {
#ViewChild("temp") temp;
private hidden: boolean = true;
private y: number = 0;
private x: number = 0;
show(target, shiftx = 0, shifty = 0){
let position = $(target).offset();
this.x = position.left + shiftx;
this.y = position.top + shifty;
this.hidden = false;
console.log("#temp", this.temp.nativeElement.getBoundingClientRect()); //all 0s
console.log("temp id", document.getElementById('temp').getBoundingClientRect()); //all 0s
}
hide(){
this.hidden = true;
}
}
Inside the show() method I am trying to get the result of getBoundingClientRect() but its returning 0 for all properties but when I type in document.getElementById("temp").getBoundingClientRect() from Chrome's console I get proper result with actual values in the properties. Why the difference and what can I do to get the actual value from my component?
Instead of using setTimeout or lifecycle hooks that can be triggered more than once, I solved it with setting the Renderer to watch the window load event.
import { OnInit, Renderer2} from '#angular/core';
...
export class MyComponent implements OnInit {
constructor(private el: ElementRef, private render: Renderer2) {}
ngOnInit() {
this.render.listen('window', 'load', () => {
const rect = this.el.nativeElement.getBoundingClientRect().top;
})
}
}
Maybe this helps someone's usecase.
I would use ngAfterContentChecked(). It works perfectly fine for me.
import { AfterContentChecked } from '#angular/core';
export class ModelComponent implements AfterContentChecked {
ngAfterContentChecked() {
//your code
}
}
I hope it helps. :)
For some reason, the DOM was not updated right after it was shown so, a setTimeout e.g. 10 did the trick.
In this context, ngAfterContentChecked works. As answered above.
ngAfterContentChecked hook method runs last in the lifecycle before the ngOnDestroy. You can read more about Lifecycle Hooks here.
Also be careful what you do in here, as it may result into performance issue.
Timing
Called after the ngAfterViewInit() and every subsequent
ngAfterContentChecked().