Manipulating a reusable component based on input - javascript

I have created a reusable component and using it twice inside a component. But I need two buttons, that can manipulate the component individually.
In my case the button for component1 should not update both the instance of the component.
I think I'm doing something wrong by design, but any suggestion will help me.
Stackblitz
Reusable Component:-
import { Component, OnInit,Input } from '#angular/core';
import { AppService } from '../app.service';
#Component({
selector: 'app-reusable',
templateUrl: './reusable.component.html',
styleUrls: ['./reusable.component.css']
})
export class ReusableComponent implements OnInit {
#Input() items:any;
constructor(
private service:AppService
) { }
ngOnInit() {
this.service.addFruit.subscribe(()=>{
this.items.unshift({fruit:'Blackberry'});
});
}
}
Usage:-
<button type="button" (click)="Add()">Add Component1</button>
<app-reusable [items]="fruitList1"></app-reusable>
<hr/>
<button type="button" (click)="Add()">Add Component2</button>
<app-reusable [items]="fruitList2"></app-reusable>
I want to update only one instance of reusable component at once.
Either instance 1 or 2.

You have to let the service know which component you are calling from.
Try the changes I made in the demo.
app.component.html
<button type="button" (click)="Add(1)">Add Component1</button>
<app-reusable [items]="fruitList1" [componentNumber]="1"></app-reusable>
app.component.ts:
Add(componentNumber:number){
this.service.addFruit.next(componentNumber);
}
reusable.component.ts:
#Input() componentNumber: number;
ngOnInit() {
this.service.addFruit.subscribe((x) => {
if (x == this.componentNumber)
this.items.unshift({ fruit: 'Blackberry' });
});
}
Working Stackbiltz

More cleaner approch would be simply pass the component instance and call related method so create a method in your reusable component something like
addFruit(){
this.items.unshift({fruit:'Blackberry'});
}
Modify your add method to get component as instance and call this method
Add(instance:ReusableComponent){
instance.addFruit();
}
Then add hash to seprate each instance and pass instance in method
<button type="button" (click)="Add(instance1)">Add Component1</button>
<app-reusable [items]="fruitList1" #instance1></app-reusable>
Working demo

Each instance of ReusableComponent is subscribed to the Subject addFruit. Clicking on the button will update Subject value which will trigger all subscriptions.
In order to avoid this, you will need to add a filter in a subscription which ignores values from other components by adding some when doing this.service.addFruit.next();. You can do filtering with RXJS filter operator. https://rxjs-dev.firebaseapp.com/api/operators/filter
Another idea is to create a subscription for each component in service and save them in some map/object in service. When a component requests a subscription from service it would add an entry to map which would be subjectId: new Subject(). You would return that new subject for the component to subscribe. Instead of doing next() directly you would call service method addNewFruit(subjectId: string, newFruit: string): void.
Map would be:
{
'firstId': Subject,
'secondId': Subject,
}
The most simple idea for this case is to use ViewChild and call method addFruit from the parent component.

Instead of having subscription of APP service in the reusable component , on click of button you should modified the input provided to your components. If you update the fruitList1 or fruitList2 at a time then it would not update the another instance of the component.

Related

I want to create a custom event which can trigger from any component and get listened to any component within my angular 7 app

I want to create a custom event which can trigger from any component and get listened to any component within my angular 7 app
Suppose I have 1 component in which I have a button on click on which I want to trigger my custom event with some data. Next, there will be another component which will constantly listening for that event when it triggers it will execute some code and update the ui accordingly.
How should I implement it?
Well, well, well, what you're looking for is a Shared Service. This shared service will have a BehaviorSubject that will act as a source for the data. Through this, you will be able to push new data streams. And then you will expose this BehaviorSubject asObservable.
You will then subscribe to this Observable from all the components where you want to listen for data changes and then react accordingly.
This is what this is going to look like in code:
import { Injectable } from '#angular/core';
import { Observable, BehaviorSubject } from 'rxjs';
#Injectable()
export class SharedService {
private data: BehaviorSubject<any> = new BehaviorSubject<any>(null);
data$: Observable<any> = this.data.asObservable();
constructor() { }
setData(newData) {
this.data.next(newData);
}
}
You can now inject the SharedService in any controller you want and call setData from the component that you want to push new data from(see the AppComponent from the Sample StackBlitz for more details). And then you'll also be injecting the SharedService in other components and in there, you'll subscribe to data$ in their ngOnInit(see the HelloComponent from the Sample StackBlitz for details)
Here's a Sample StackBlitz for your ref.

How to define a "global" variable in a component? (Angular4)

I have an app-editor-component which contains a lot of nested components inside it's structure. When the component is loaded, it creates an array: obj_list : MyObject[]
Many of the nested components will contain a <select> element, where one of the elements in obj_list has to be selected in each.
How is it possible to share this list with all the elements in the structure?
One way to share data between nested components is Input/Output+EventEmitter system.
Or, you can use shared service to transfer data between components.
Here are some links to official docs and good post from firebase about component interaction:
Docs
Post
In your case, for example, you can pass obj_list from app-editor-component down to child components through Inputs, then, in child components, observe <select>’s change event and emit changes back to app-editor-component.
But, if you have deep nesting, using service is better approach
The entity that is shared by several nesting components should be a service. This is naturally provided by Angular hierarchical injectors.
More importantly, if data is supposed to be changed asynchronously, components should be notified of this somehow. This is conveniently done with RxJS observables:
import { Subject } from 'rxjs/Subject';
#Injectable()
class Foo {
objListSubject = new Subject();
objList$ = this.objListSubject.asObservable();
}
#Component({
providers: [Foo] // belongs to NgModule providers if the service is global
...
})
class ParentComponent {
constructor(private foo: Foo) {
...
this.foo.next(['bar']);
}
}
#Component({
template: `
<select ...>
<option *ngFor="foo.objList$ | async">
</select>
`,
...
});
class ChildComponent {
constructor(public foo: Foo) {}
}

Angular.js 2: Access component of a directive

Consider the following snippet of Parent's template:
<div *ngFor= "let event of events" >
<event-thumbnail [theEvent] = 'event'></event-thumbnail>
</div>
Also event-thumbnail component definition is:
export class EventThumbnailComponent{
intoroduceYourself(){
console.log('I am X');
}
}
In Parent component class, I want to iterate over all generated event-thumbnail elements, access the component beneath each, and call introduceYourself function on single one of them.
You want to use the #ViewChildren() decorator to get a list of all instances of a specific component type within the view:
class ParentComponent implements AfterViewInit {
#ViewChildren(EventThumbnailComponent)
eventThumbnails: QueryList<EventThumbnailComponent>;
ngAfterViewInit(): void {
// Loop over your components and call the method on each one
this.eventThumbnails.forEach(component => component.introduceYourself());
// You can also subscribe to changes...
this.eventThumbnails.changes.subscribe(r => {
// Do something when the QueryList changes
});
}
}
The eventThumbnails property will be updated whenever an instance of this component is added to or removed from the view. Notice the eventThumbnails is not set until ngAfterViewInit.
See the docs here for more information:
https://angular.io/docs/ts/latest/api/core/index/ViewChildren-decorator.html
Your child component should have #Input() theEvent to get access to the event you are passing. Then you can use the following lifecycle hook:
ngOnInit(){
introduceYourself(){
console.log('I am X');
}
}

How to pass data from parrent constructor to child component in angular 2

I have a angular 2 page where i need to show 2 different components using same array of data from external API. Parent is regular component, and child is shared among several other components using same functionality.
In parent component class i have output property declared:
public weatherList: WeatherForecast[];
#Output() public weatherListData: any;
Inside constructor of parent component, i populate weatherListData property with data from an external API
http.get(url)
.subscribe(result => {
this.weatherList= result.json() as WeatherForecast[];
this.weatherListData = this.weatherList;
});
and i'm using it inside parent template with success, something like: {{ weatherList.someValue }}
Also, inside parent component template, i have a call to a child component
<daily-temperature-chart [weatherListData]='weatherListData'></daily-temperature-chart>
In child component class i have declared property
#Input() weatherListData: any;
but, when i try to access weatherListData property in constructor, or init of child component, i get undefined result.
EDIT: I have played with console.log() and noticed that child component Constructor and OnInit() methods return before http.get() from parent component. Maybe this is problem, but i'm still new to angular and can't tell.
Can someone point me how to solve this?
You've a service call so you can't go for constructor or OnInit because component initialization is not dependent on your service call for this situation angular provides OnChanges whenever your input value is updated OnChanges fired.
ngOnChanges(changes: any){
console.log(changes);
console.log(this.weatherListData);
}
OnChanges passes as well an argument which informs about the current state and pervious state now you are able to use input values. If your components are bases on input and input is based on any other operation you can handle it in this block.

Angular2: Creating child components programmatically

Question
How to create child components inside a parent component and display them in the view afterwards using Angular2? How to make sure the injectables are injected correctly into the child components?
Example
import {Component, View, bootstrap} from 'angular2/angular2';
import {ChildComponent} from './ChildComponent';
#Component({
selector: 'parent'
})
#View({
template: `
<div>
<h1>the children:</h1>
<!-- ??? three child views shall be inserted here ??? -->
</div>`,
directives: [ChildComponent]
})
class ParentComponent {
children: ChildComponent[];
constructor() {
// when creating the children, their constructors
// shall still be called with the injectables.
// E.g. constructor(childName:string, additionalInjectable:SomeInjectable)
children.push(new ChildComponent("Child A"));
children.push(new ChildComponent("Child B"));
children.push(new ChildComponent("Child C"));
// How to create the components correctly?
}
}
bootstrap(ParentComponent);
Edit
I found the DynamicComponentLoader in the API docs preview. But I get the following error when following the example: There is no dynamic component directive at element 0
This is generally not the approach I would take. Instead I would rely on databinding against an array that will render out more child components as objects are added to the backing array. Essentially child components wrapped in an ng-for
I have an example here that is similar in that it renders a dynamic list of children. Not 100% the same, but seems like the concept is still the same:
http://www.syntaxsuccess.com/viewarticle/recursive-treeview-in-angular-2.0
Warning: DynamicComponentLoader has been deprecated in RC.4
In Angular 2.0, loadIntoLocation method of DynamicComponentLoader serve this purpose of creating parent-child relationship. By using this approach you can dynamically create relationship between two components.
Here is the sample code in which paper is my parent and bulletin is my child component.
paper.component.ts
import {Component,DynamicComponentLoader,ElementRef,Inject,OnInit} from 'angular2/core';
import { BulletinComponent } from './bulletin.component';
#Component({
selector: 'paper',
templateUrl: 'app/views/paper.html'
}
})
export class PaperComponent {
constructor(private dynamicComponentLoader:DynamicComponentLoader, private elementRef: ElementRef) {
}
ngOnInit(){
this.dynamicComponentLoader.loadIntoLocation(BulletinComponent, this.elementRef,'child');
}
}
bulletin.component.ts
import {Component} from 'angular2/core';
#Component({
selector: 'bulletin',
template: '<div>Hi!</div>'
}
})
export class BulletinComponent {}
paper.html
<div>
<div #child></div>
</div>
Few things you needs to be take care of are mentioned in this answer
You should use ComponentFactoryResolver and ViewElementRef to add component at runtime.Let's have a look at below code.
let factory = this.componentFactoryResolver.resolveComponentFactory(SpreadSheetComponent);
let res = this.viewContainerRef.createComponent(factory);
Put the above code inside your ngOnInit function and replace "SpreadSheetComponent" by your component name.
Hope this will work.
Programmatically add components to DOM in Angular 2/4 app
We need to use ngAfterContentInit() lifecycle method from AfterContentInit. It is called after the directive content has been fully initialized.
In the parent-component.html, add the a div like this:
<div #container> </div>
The parent-component.ts file looks like this:
class ParentComponent implements AfterContentInit {
#ViewChild("container", { read: ViewContainerRef }) divContainer
constructor(private componentFactoryResolver: ComponentFactoryResolver) { }
ngAfterContentInit() {
let childComponentFactory = this.componentFactoryResolver.resolveComponentFactory(childComponent);
this.divContainer.createComponent(childComponentFactory);
let childComponentRef = this.divContainer.createComponent(childComponentFactory);
childComponentRef.instance.someInputValue = "Assigned value";
}
}
Inside src\app\app.module.ts, add the following entry to the #NgModule() method parameters:
entryComponents:[
childComponent
],
Notice that we're not accessing the div#container using the #ViewChild("container") divContainer approach. We need it's reference instead of the nativeElement. We will access it as ViewContainerRef:
#ViewChild("container", {read: ViewContainerRef}) divContainer
The ViewContainerRef has a method called createComponent() which requires a component factory to be passed as a parameter. For the same, we need to inject a ComponentFactoryResolver. It has a method which basically loads a component.
The right approach depends on the situation you're trying to solve.
If the number of children is unknown then NgFor is the right approach.
If it is fixed, as you mentioned, 3 children, you can use the DynamicComponentLoader to load them manually.
The benefits of manual loading is better control over the elements and a reference to them within the Parent (which can also be gained using templating...)
If you need to populate the children with data, this can also be done via injection, the Parent is injected with a data object populating the children in place...
Again, a lot of options.
I have used 'DynamicComponentLoader' in my modal example, https://github.com/shlomiassaf/angular2-modal

Categories

Resources