How to share data between sibling components in Angular - javascript

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.

Related

In LWC JavaScript controller splice method not working with #api variable which is an array

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

How to avoid writing same http subscribe blocks for different components?

Let's assume that there is a service will be used for http request calls.And two different components(could be more than two) which will send same request by using same observables via this service.After getting result that should be assigned to global variable(Components have not relationship like parent-child or child-parent).Below I wrote same code block for all components.Is there any better way to write this function once and call by returning same value?
Service
getStudents() {
const requestUrl = this.apiUrl + 'students/';
return this.httpClient.get(requestUrl);
}
Component1
studentList:Student[]=[];
getStudents.subscribe((students:Student[])=>{
this.studentList=students;
//Some operations
})
Component2
studentList:Student[]=[];
getStudents.subscribe((students:Student[])=>{
//Some operations
this.studentList=students;
})
I'm not a fan of global state, but if you want to maintain the same list of students across components using global state, then that state may as well live in the service (Rather than existing in each component separately)
So, for example:
Service
studentList:Student[] = [];
setStudents(students:Student[]) {
this.studentList = students;
// Operations involved with setting students
}
updateStudents() {
const requestUrl = this.apiUrl + 'students/';
return this.httpClient.get(requestUrl).pipe(
tap(this.setStudents)
);
}
Component
ngOnInit(){
this.service.updateStudents().subscribe();
}
You can have an Observable inside your service,
studentsReceived:Subject = new Subject();
on success of getStundent() you can emit next value of studentsReceived.
Now you can subscribe to the studentsReceived inside your components, after the successful API call you will be notified of each of the subscribed components.
studentRecived.subscribe(data=>{ // do some code })
You must call this getStudent() on some higher component like AppComponent.
2 Important things here:
1) If you dont want to repeat the same block of code, then create a method in the service file,
and call it in the component. Something like this:
SERVICE:
makeSubcription(componentVariableName) {
this.yourObservable.subcribe(subscribedValue => {
componentVariableName = subscribedValue;
})
}
In your Component, you can do this:
yourComponentMethod() {
this.service.makeSubscription(this.studentLists);
}
************
2) If you dont want to make a service call too many times, what you can do is,
use Behavior Subject and try to store the values, so that you are subscribing to the observable and not the actual API call. Something like this:
private initialValuesForObservable: YourObjectModel = {}; // or null;
private observableSource: BehaviorSubject<YourObjectModel> =
new BehaviorSubject<YourObjectModel>(this.initialValuesForObservable);
public subscribableObservable: Observable<YourObjectModel> =
this.observableSource.asObservable();
setObservableValue(data: YourObjectModel) {
this.observableSource.next(data);
}
getObservableData() {
return this.subscribableObservable;
}
In your COMPONENT;
this.service.getObservableData().subscribe(latestObservableData => {
this.schoolList = latestObservableData;
});

How to invoke parent function from child component

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.

How to get ViewRef values in Angular 2

So I'm trying to build a dynamic form in Angular2 (and NativeScript)..
What i've done so far is:
Get JSON from REST API
Add Component (via a template) to the ViewContainerRef
Now I want to "submit" the form. To do this, I thought I could simply use the ViewContainerRef and get its "siblings" (which are dynamically created based on the before mentioned JSON) and loop through them and create a JSON object to post this.. But I don't understand how the Angular2 DOM works..
Here is the (partial) code I've got so far:
addComponent function
//... class DynamicComponent, has id and value properties
public addComponent(container: ViewContainerRef, template: string){
#Component({template: template})
class TemplateComponent{
public value = "test";
}
#NgModule({declarations: [TemplateComponent]})
class TemplateModule {}
const mod = this.compiler.compileModuleAndAllComponentsSync(TemplateModule);
const factory = mod.componentFactories.filter((comp)=>
comp.componentType === TemplateComponent
);
const component = container.createComponent(factory[0]);
return TemplateComponent;
}
Create Components
#ViewChild("container",{ read: ViewContainerRef }) container : ViewContainerRef;
..
//foreach JSON element
let _component = <any>
this._dynamicComponent.addComponent(
this.container, elementObject.render()
);
//the render() returns things like : `<Label text="foo"></Label>`
..
_component.prototype.onTap = () => {
this.submit();
}
Submit Function
submit(){
let json=[];
for(let i=0;i<this.container.length;i++){
let component = this.container.get(i);
//Ideally I want to do this:
/*
json.push({
id:component.id,
val:component.value
});
*/
}
}
Basically, I get "component" in the submit function as a ViewRef object, but how do I continue from there?
Also, I'm fairly new to Angular2, so is this the right way? I've seen the form builder but I didn't get it to work properly with dynamically created elements..

How do I update a value in an AngularJS (with TypeScript) controller when a value on an injected service updates

At the moment I am trying to learn Typescript and AngularJS coming from a background of Actionscript. I have been playing around with some code experiments and van generally achieve what I want to but not always in the way that I would like!
At the moment I have this code:
export class CountingService
{
countScope = { count : 0 };
public increment()
{
this.countScope.count++;
}
}
export class PageController
{
constructor( private service : CountingService )
{
this.countScope = service.countScope;
}
public localCount : number = 0;
countScope;
public increment()
{
this.service.increment();
this.localCount++;
}
}
and I have a few different views that have this code in:
<div ng-controller="PageController as page">
<Button ng-click="page.increment()">Count {{page.localCount}}/{{page.countScope.count}}</Button>
</div>
When I switch between the views the localCount always resets to zero but the count in the countScope from the service does not reset and is incremented by each different controller.
This works but it's a bit messy. I have to have the untyped localScope floating around and the views need to know about the internal structure of the countScope object and bind to page.countScope.count rather than something like page.globalCount.
In Actionscript I would have a read only getting on the controller. The service would dispatch an event each time the value changed and the controller would listen for this and then update it's own property which would then update the view.
My question is what is the best practice way to achieve what I am doing here that does not require the view to have knowledge of the internals of an untyped object.
I am pretty sure that public getters do not exist but can I dispatch and listen for an event?
Many Thanks
Finally answer three. I didn't expect this to work but it does and IMHO this is by far the nicest solution.
It enforces encapsulation with getters, it doesn't need $scope injected and there are no concerns about memory leaks.
I welcome any criticisms that point out issues that I am not aware of.
The only thing that I AM aware of is that you need to enable ECMAScript 5 which means that IE 7 and 8 are not supported. That's fine for me.
export class CountingService
{
private _serviceCount : number = 100;
public increment()
{
this._serviceCount++;
}
get serviceCount() : number
{
return this._serviceCount;
}
}
export class PageController
{
constructor( private countingService : CountingService )
{}
private _localCount : number = 0;
get localCount() : number
{
return this._localCount;
}
get serviceCount() : number
{
return this.countingService.serviceCount;
}
public increment()
{
this.countingService.increment();
this._localCount++;
}
}
This seems like the perfect fit for a reactive library and is exactly what we do. I highly recommend you check out RxJs which has some awesome features. https://github.com/Reactive-Extensions/RxJS/tree/master/doc
https://www.youtube.com/watch?v=XRYN2xt11Ek
You code using Rx
export class CountingService {
//Create a stream of changes. When caller subscribe always give them the most recent value.
private rawCountStream = new Rx.Subject<number>();
public countStream : Rx.Observable<number>;
private count = 0;
constructor() {
this.countStream = this.rawCountStream.replay(1);
}
public increment() {
this.count++;
//Pump the value to callers.
this.rawCountStream.onNext(this.count);
}
}
export class PageController {
constructor(private service: CountingService) {
//Listen to the stream of changes.
service.countStream.subscribe(value => {
//do something
//IMPORTANT If you want angular to update the ui you will need to call $apply on a scope.
//RXJS has a scheduler for this so you look at that here.
//https://github.com/Reactive-Extensions/rx.angular.js
}
);
}
public localCount: number = 0;
public increment() {
this.service.increment();
this.localCount++;
}
}
This is answer number 1 and I think is the approach that John Kurlak was talking about:
export class CountingService
{
serviceCount : number = 100;
public increment()
{
this.serviceCount++;
}
}
export class PageController
{
constructor( $scope, private service : CountingService )
{
this.serviceCount = service.serviceCount;
$scope.$watch( () => this.service.serviceCount, ( newValue : number, oldValue : number ) => this.updateLocal( newValue, oldValue ) );
}
localCount : number = 0;
serviceCount : number;
public increment()
{
this.service.increment();
this.localCount++;
}
private updateLocal( newValue : number, oldValue : number )
{
this.serviceCount = newValue;
}
}
This works and I think is the sort of solution that I was after. There are things that I don't like about it though.
I don't like having to inject the $scope into my service. It seems to be an extra dependency on something that I shouldn't need.
I also really don't like the public members that can be updated by anything and break encapsulation. I want to fix this with getters but I can't figure out how to update to ECMAScript 5 (question here: Targeting ES5 with TypeScript in IntelliJ IDEA 14).
John - is this what you were proposing? If anyone comments with Pros and Cons to this answer I'd be very grateful.
I will mark this as the answer as I think this is what I was after to start with.
This is answer number 2 and I think this is what PSL was getting at (or something similar)
interface CountingCallBack
{
parent : any;
callback : ( value : number ) => void;
}
export class CountingService
{
private _observerCallBacks : Array<CountingCallBack> = [];
private _serviceCount : number = 100;
public increment()
{
this._serviceCount++;
this.notifyObservers();
}
public registerObserverCallback( callbackParent : any, callbackFunction : ( value : number ) => void )
{
var callback : CountingCallBack = { parent : callbackParent, callback : callbackFunction };
this._observerCallBacks.push( callback );
this.updateObserver( callback );
}
private notifyObservers()
{
angular.forEach( this._observerCallBacks, this.updateObserver, this );
}
private updateObserver( callback : CountingCallBack )
{
callback.callback.apply( callback.parent, [ this._serviceCount ] );
}
}
export class PageController
{
constructor( private countingService : CountingService )
{
countingService.registerObserverCallback( this, this.updateLocal );
}
localCount : number = 0;
serviceCount : number;
public increment()
{
this.countingService.increment();
this.localCount++;
}
private updateLocal( newValue : number )
{
this.serviceCount = newValue;
}
}
I'm quite pleased that I managed to implement this as I now understand how the function interface syntax works (and I quite like it) and I managed to fix the issue with the 'this' scope issue in the updateLocal function on the controller.
This solution doesn't require and $scope to be injected which is nice but I find the callback implementation quite messy. I don't like having to pass the controller and the function on the controller to the service to add a callback.
I suppose I could create an interface, ICountListener or something, and just pass that to the service and then call updateCount on the controller. That would be a bit neater but still not that good.
Is there a neater way around setting up the callback than this?
The biggest problem with this code (and a reason that it should not be used) is that it creates a memory leak. If you keep switching views the controllers never get cleaned up so you have many controllers left in memory all responding to the updated count.
It would be relatively easy to clean up the callback and de-register a callback but that would presumably involve injecting a $scope and then listening for a destroy event (I don't know how to do this but I think this was under discussion in the comments above).
Short answer: Services are singletons i.e. the constructor is only called once. If you want it reset you can do that from the constructor of your controller.
this.service.countScope.count = 0
Notes
I see a lot of things I don't like about that code sample. But I am sure its just a sample so don't take it as a personal offence ;)
don't have a service called service. Call it countService.
don't have a member countScope on the service. Just use count. And don't copy countScope to the current controller. Just use this.countService.count
Update
The root of this question is how do I update a displayed value in html when a value in a service has changed
Since you have the controller as page and page.service is the service simply {{page.service.countScope.count}} would display teh count.

Categories

Resources