Trigger cloned component when click on original in Angular - javascript

I have a loop on a component which represents a list of graph cards on my real app.
I have copied this component ( and loop it ) as the original
Hello Component
export class HelloComponent {
message:string;
printedMessage:string
#Input() elm:string;
constructor(private data: DataService, private router : Router) { }
ngOnInit() {
this.message = this.data.messageSource.value;
this.data.messageSource.subscribe(message => this.message = message)
}
updateService(){
this.data.changeMessage(this.message);
this.printedMessage=this.data.messageSource.value
}
navigateToSibling(){
this.router.navigate(['/sibling']);
}
}
app component
<div *ngFor="let elm of [1,2,3,4]">
<hello [elm]= "elm"></hello>
</div>
<h1>Copy </h1>
<div *ngFor="let elm of [1,2,3,4]">
<hello [elm]= "elm"></hello>
</div>
DataService component
export class DataService {
messageSource = new BehaviorSubject<string>("default message");
constructor() { }
changeMessage(message: string) {
this.messageSource.next(message)
}
}
Expected behaviour
What I would is when change the input value on the component 1 for example , only the value on the input of the copied component 1 changes.
Actual behaviour
Actually when I change a value inside an input all the other inputs are changings.
Here's a stackblitz example

Below is a solution that will solve you issue. This may not be a perfect solution but you need something similar.
hello.html
<h1>App component {{elm}}</h1>
<input type="text" [(ngModel)]="message">
<button (click)="updateService()" type="button">Save</button> {{printedMessage}}
Data Service
import {
Injectable
} from '#angular/core';
import {
BehaviorSubject
} from 'rxjs/BehaviorSubject';
#Injectable()
export class DataService {
messageSource = new BehaviorSubject < any > ("default message");
constructor() {}
changeMessage(message: string, elem: any) {
this.messageSource.next({
message: message,
elem: elem
});
}
}
HelloComponent
import {
Component,
Input
} from '#angular/core';
import {
DataService
} from "./dataService";
import {
Router
} from '#angular/router';
#Component({
selector: 'hello',
templateUrl: './hello.html',
styles: [`h1 { font-family: Lato; }`]
})
export class HelloComponent {
message: string;
printedMessage: string
#Input() elm: string;
constructor(private data: DataService, private router: Router) {}
ngOnInit() {
this.message = this.data.messageSource.value;
this.data.messageSource.subscribe(message => this.message = message.elem === this.elm ? message.message : this.message);
}
updateService() {
debugger
this.data.changeMessage(this.message, this.elm);
this.printedMessage = this.data.messageSource.value.message;
}
navigateToSibling() {
this.router.navigate(['/sibling']);
}
}
Have also updated the Stackblitz Demo. Hope this helps :)

Related

Angular - Structural directive with embedded view does not pass children to ng-template

I've got a structural directive that creates an embedded view by looking up a template ref using ng-template. My problem is that from this parent component (with structural directive), I cannot pass down children.
Parent component with structural directive
import { ViewChild, Component, OnInit, ElementRef } from "#angular/core";
import { TestJsonService } from "../../services/test-json.service";
#Component({
selector: "xfr-json-renderer",
template: `
<template-lookup></template-lookup>
<div class="NA-TEMPLATE-CHOOSER" *replaceWith="'flexCol'">
<div>Why can't i pass this down to the child?</div>
</div>
`,
styleUrls: ["./json-renderer.component.css"],
})
export class JsonRendererComponent implements OnInit {
#ViewChild("childTemplate") childTemplate;
constructor(el: ElementRef, json: TestJsonService) {}
ngOnInit(): void {}
ngAfterViewInit() {}
}
Child component
import { Injectable, TemplateRef, Component, ViewChild } from "#angular/core";
#Injectable()
export class TemplateStore {
templates = new Map<string, TemplateRef<any>>();
}
#Component({
selector: "template-lookup",
template: `
<ng-template #flexRow></ng-template>
<ng-template #flexCol><xfr-flex-col>
// I want to pass the children into here
</xfr-flex-col></ng-template>
`,
})
export class TemplateLookup {
#ViewChild("flexRow") flexRowTemplate;
#ViewChild("flexCol") flexColTemplate;
constructor(private service: TemplateStore) {}
ngAfterViewInit() {
this.service.templates.set("flexRow", this.flexRowTemplate);
this.service.templates.set("flexCol", this.flexColTemplate);
}
}
Structural directive
import { ViewContainerRef } from "#angular/core";
import { TemplateStore } from "./../services/composite-template.service";
import { Directive, Input } from "#angular/core";
#Directive({
selector: "[replaceWith]",
})
export class CompositeTemplateDirective {
#Input() replaceWith: "flex-col" | "flex-row";
constructor(private service: TemplateStore, private view: ViewContainerRef) {}
ngAfterViewInit() {
this.view.createEmbeddedView(this.service.templates.get(this.replaceWith));
}
}
The problem is that you need to use internal API for that, what is not the best thing. I would use it until I stay with the same angular version and would test it before every update - then should work stable.
I was able to do the injection with Angular 9, quite sure a similar solution (but different internal API) can be applied for other angular versions.
The main thing for the injection - where to inject the content, in components we could use ng-content, but here it wouldn't work, because we have different component contexts. In this case we could use <ng-template [ngTemplateOutlet]></ng-template> to tell the script where we want the injection.
here you can find a live demo: https://codesandbox.io/s/nifty-wright-335bm?file=/src/app/json-renderer.component.ts
CompositeTemplateDirective
import {NgTemplateOutlet} from '#angular/common';
import {AfterViewInit, Directive, Input, TemplateRef, ViewContainerRef} from '#angular/core';
import {TemplateStore} from 'src/app/TemplateLookup/TemplateLookup';
#Directive({
selector: '[replaceWith]',
})
export class CompositeTemplateDirective implements AfterViewInit {
#Input() replaceWith: 'flex-col' | 'flex-row';
constructor(
private service: TemplateStore,
private view: ViewContainerRef,
private templateRef: TemplateRef<any>,
) {
}
public ngAfterViewInit(): void {
const wrapper = this.service.templates.get(this.replaceWith);
const source = this.templateRef;
const view: any = this.view.createEmbeddedView(wrapper);
let directive: NgTemplateOutlet;
const nodes: Array<any> = view._lView ? view._lView : view._view && view._view.nodes ? view._view.nodes : [];
for (const node of nodes) {
if (typeof node !== 'object') {
continue;
}
if (node instanceof NgTemplateOutlet) {
directive = node;
}
if (typeof node.instance === 'object' && node.instance instanceof NgTemplateOutlet) {
directive = node.instance;
}
}
if (directive) {
directive.ngTemplateOutlet = source;
directive.ngOnChanges({
ngTemplateOutlet: {
previousValue: null,
currentValue: source,
firstChange: true,
isFirstChange: () => true,
},
});
}
}
}
TemplateLookup
import {AfterViewInit, Component, Injectable, TemplateRef, ViewChild} from '#angular/core';
#Injectable()
export class TemplateStore {
templates = new Map<string, TemplateRef<any>>();
}
#Component({
selector: 'template-lookup',
template: `
<ng-template #flexRow>
<div>
flexRow template
</div>
</ng-template>
<ng-template #flexCol>
<div>
<div>wrap</div>
<ng-template [ngTemplateOutlet]></ng-template>
<div>wrap</div>
</div>
</ng-template>
`,
})
export class TemplateLookup implements AfterViewInit {
#ViewChild('flexRow', {static: false}) flexRowTemplate;
#ViewChild('flexCol', {static: false}) flexColTemplate;
constructor(
private service: TemplateStore,
) {
}
ngAfterViewInit() {
console.log('TemplateLookup:ngAfterViewInit');
this.service.templates.set('flexRow', this.flexRowTemplate);
this.service.templates.set('flexCol', this.flexColTemplate);
}
}
so the most pragmatic thing here seems to be to just put the child you want to pass as a child of the template-lookup component and use ng-content...
do this in the parent:
<template-lookup>
<div>I will pass to child</div>
</template-lookup>
<div class="NA-TEMPLATE-CHOOSER" *replaceWith="'flexCol'">
</div>
and this in the child:
<ng-template #flexRow></ng-template>
<ng-template #flexCol>
<xfr-flex-col>
<ng-content></ng-content>
</xfr-flex-col>
</ng-template>
and that will solve your problem / fulfill the stated requirements.
You could also consider a rewrite to your service to solve timing problems between templates being set and gotten once and for all:
import { Injectable, TemplateRef } from "#angular/core";
import {ReplaySubject} from 'rxjs';
import {map, filter, distinctUntilChanged} from 'rxjs/operators';
#Injectable({providedIn: 'root'}) // provide appropriately, root for example
export class TemplateStore {
private templates = new Map<string, TemplateRef<any>>();
private tmpSource = new ReplaySubject<Map<string, TemplateRef<any>>>(1);
setTemplate(key: string, template: TemplateRef<any>) {
this.templates.set(key, template);
this.tmpSource.next(this.templates)
}
getTemplate(key: string) {
return this.tmpSource.pipe(
map(tmpMap => tmpMap.get(key)),
filter(tmp => !!tmp),
distinctUntilChanged()
)
}
}
and make the associated changes in the directive and child components...
export class CompositeTemplateDirective implements OnInit, OnDestroy {
#Input() replaceWith: "flex-col" | "flex-row";
private sub: Subscription;
constructor(private service: TemplateStore, private viewContainer: ViewContainerRef) { }
ngOnInit() {
this.sub = this.service.getTemplate(this.replaceWith).subscribe(t => {
this.viewContainer.clear()
this.viewContainer.createEmbeddedView(t)
})
}
ngOnDestroy() {
this.sub.unsubscribe()
}
}
export class TemplateLookup {
#ViewChild("flexRow") flexRowTemplate;
#ViewChild("flexCol") flexColTemplate;
constructor(private service: TemplateStore) {}
ngAfterViewInit() {
this.service.setTemplate("flexRow", this.flexRowTemplate);
this.service.setTemplate("flexCol", this.flexColTemplate);
}
}
functioning example: https://stackblitz.com/edit/angular-ygdveu
it's been pointed out that this doesn't support nesting... so make the following adjustments and you can nest. in template lookup, you'll need to use the SkipSelf modifier in your constructor, and also provide the TemplateStore... in the case of no nesting, this will have no effect, SkipSelf just tells the injector to start looking for the service at the parent rather than at the component:
#Component({
selector: "template-lookup",
template: `
<ng-template #flexRow>FLEX ROW</ng-template>
<ng-template #flexCol>
FLEX COL
<div class="flex-col">
<ng-content></ng-content>
</div>
</ng-template>
`,
providers: [TemplateStore]
})
export class TemplateLookup {
#ViewChild("flexRow") flexRowTemplate;
#ViewChild("flexCol") flexColTemplate;
constructor(#SkipSelf() private service: TemplateStore) {}
ngAfterViewInit() {
this.service.setTemplate("flexRow", this.flexRowTemplate);
this.service.setTemplate("flexCol", this.flexColTemplate);
}
}
then you can nest to your hearts content like so:
<template-lookup>
<div>I can pass this to the child!</div>
<template-lookup>NESTED</template-lookup>
<div class="nested-content" *replaceWith="'flexCol'"></div>
</template-lookup>
<div class="NA-TEMPLATE-CHOOSER" *replaceWith="'flexCol'">
</div>
which is a little ugly, as you need to repeat the template-lookup component, but it does get the job done. This works by allowing the directive and template lookup to communicate with a different copy of the TemplateStore so you can nest different content.
working example of this variant: https://stackblitz.com/edit/angular-lpner2

Angular - communication from child-component to parent

I don't get i, how to communicate between components and services.. :(
I have read and tried a lot about even if some examples somehow work, I do not understand why (?)
what I want to achieve:
I have one parent and two child-components:
dashboard
toolbar
graph
in the toolbar-component I have a searchfield, which gets it's result from a external source (works via service).. when the result arrives, I need to trigger the updateGraph()-Method in the graph-component
toolbar.component.ts
import { Component, OnInit, Output, EventEmitter } from '#angular/core';
import { FormControl } from '#angular/forms';
import { WebsocketsService } from '../../../services/websockets/websockets.service';
import { DataService } from '../../../services/data/data.service';
#Component({
selector: 'toolbar',
templateUrl: './toolbar.component.html',
styleUrls: ['./toolbar.component.scss'],
providers: [WebsocketsService, DataService]
})
export class ToolbarComponent implements OnInit {
#Output() newGraphData: EventEmitter<boolean> = new EventEmitter();
searchField: FormControl;
search: string;
private isNewGraph = false;
constructor(private _websocketsService: WebsocketsService, private _dataService: DataService) {
}
ngOnInit() {
this.searchField = new FormControl();
this.searchField.valueChanges
.subscribe(term => {
this.search = term;
});
}
private applySearch() {
const res = this._websocketsService.sendQuery(this.search);
this._dataService.setGraphData(res);
this.newGraphData.emit(true);
this.search = '';
this.searchField.reset();
}
}
graph-component.ts
import { Component, OnInit} from '#angular/core';
import { HttpService } from '../../../services/http/http.service';
import { DataService } from '../../../services/data/data.service';
#Component({
selector: 'graph',
templateUrl: './graph.component.html',
styleUrls: ['./graph.component.scss'],
providers: [HttpService, DataService]
})
export class GraphComponent implements OnInit, AfterViewInit {
constructor( private _httpService: HttpService, private _dataService: DataService ) {
}
ngOnInit() {
}
public renderResult() {
console.log( this._dataService.getGraphData() );
}
}
data.service.ts
import { Injectable } from '#angular/core';
import { Subject } from 'rxjs/Subject';
#Injectable()
export class DataService {
private graphData: Subject<string> = new Subject<string>();
public setGraphData(data) {
this.graphData.next( data );
}
public getGraphData() {
return this.graphData;
}
constructor() { }
}
I simply want ´renderResult()´to be executed after the searchresult has been written to ´graphData´. please help i am confused.
If I understand, you want communication between components and service.
A[component] (make a information) -----(notification)-----> B[service] ----(send)----> C[component] (consume the information)
It's correct? Let's go.
You need create a subscription of graphData(data.service.ts) in GraphComponent.
import { Subscription } from 'rxjs/Subscription';
export class GraphComponent implements OnInit, AfterViewInit {
constructor( private _httpService: HttpService, private _dataService: DataService ) {
}
private subscription: Subscription;
ngOnInit() {
this.subscription = this._dataService.getGraphData().asObservable().subscribe((data) => {
console.log(data);
});
}
}
Look here to help you.
http://jasonwatmore.com/post/2016/12/01/angular-2-communicating-between-components-with-observable-subject
Short answer, I think you need to subscribe to the getGraphData subject, something like this (NOT RECOMMENDED):
public renderResult() {
this._dataService.getGraphData().subscribe(d => {
console.log(d)
});
}
It is not recommended as per the lead of RxJS says: https://medium.com/#benlesh/on-the-subject-of-subjects-in-rxjs-2b08b7198b93
Better answer, create an observable in your service and subscribe to that instead.
data.service.ts
graphObservable = this.graphData.asObservable();
graph-component.ts
public renderResult() {
this._dataService.graphObservable().subscribe(d => {
console.log(d)
});
}

Angular 4 - (onclick) pass parameter to a service

I am using Angular 4 and I was wondering how to pass a parameter value to a Service.
For example:
<button (onClick)="doSomething('myParameter')">Send this to Service</button>
Then the service would get it.
I currently have this:
import { Injectable } from '#angular/core';
#Injectable()
export class MessageService {
constructor() { }
message() {
return 'This data goes to the component';
}
}
and then get is like this:
export class AppComponent implements OnInit {
constructor(private messageService: MessageService) {}
ngOnInit() {
console.log(this.messageService.message);
}
}
but this only sends data to the component.
How do I do this?
Your template should talk to your component class and your component class should talk to your service.
I see you have a doSomething method in your template that is not defined in your component?
You need something like this:
Component 1
export class AppComponent implements OnInit {
constructor(private messageService: MessageService) {}
ngOnInit() {
}
doSomething(message: string): void {
this.messageService.message = message;
}
}
Service
import { Injectable } from '#angular/core';
#Injectable()
export class MessageService {
message: string;
constructor() { }
}
Component 2
export class AppComponent implements OnInit {
get message(): string {
return this.messageService.message;
}
constructor(private messageService: MessageService) {}
ngOnInit() {
}
}

Can't Bind Property in Angular 4

Why is there a problem in binding a property on the same component? I already added Input() but still doesn't work. Do i need to put Input() even though it is on the same component when binding?
//output.component.ts
import { Component, OnInit} from '#angular/core';
import { DataService } from '../data.service';
#Component({
selector: 'app-output',
templateUrl: './output.component.html',
styleUrls: ['./output.component.css']
})
export class OutputComponent implements OnInit {
data: {name: string};
datas = [];
constructor(private dataService: DataService) { }
ngOnInit(){
this.datas = this.dataService.datas;
}
}
//output.component.html
<p *ngFor="let data of datas"></p>
<p>{{data.name}}</p>
//data.service.ts
export class DataService {
datas= [];
addData(name: string){
return this.datas.push({name: name});
}
}
For same component #input API is not required. It is used when you want to pass the data from Parentcomponent to a child component.
//output.component.html
<p *ngFor="let data of dataService.datas" > // removed [data]="data" and added dataService.datas
<p>{{data?.name}}</p>
</p> //changed the position of </p>
export class OutputComponent implements OnInit {
constructor(private dataService: DataService) {}
}
export class DataService {
datas= [];
addData(name: string){
return this.datas.push({name: name}); //return keyword was missing
}
}
Just for your reference
DEMO: https://plnkr.co/edit/XlJM2LHFwlAYpQe2ancM?p=preview

Angular2: Send data from one component to other and act with the data

I'm learning Angular2. In order to that, I have 2 components, on the click of one of the components, the other component should be notified and act with that.
This is my code so far:
export class JsonTextInput {
#Output() renderNewJson: EventEmitter<Object> = new EventEmitter()
json: string = '';
process () {
this.renderNewJson.next(this.json)
}
}
The process function is being called on the click on the first component.
On the second component I have this code:
export class JsonRendered {
#Input() jsonObject: Object
ngOnChanges () {
console.log(1)
console.log(this.jsonObject)
}
}
The ngOnChanges is never runned, I dont get how to pass the info from one component to other
EDIT
There is an app component which is parent of those 2 components. None of both is parent of the other
This is how my clasess look now:
export class JsonRendered {
private jsonObject: Object
constructor (private jsonChangeService: JsonChangeService) {
this.jsonChangeService = jsonChangeService
this.jsonObject = jsonChangeService.jsonObject
jsonChangeService.stateChange.subscribe(json => { this.jsonObject = json; console.log('Change made!') })
}
}
export class JsonTextInput {
json: string = '';
constructor (private jsonChangeService: JsonChangeService) {
this.jsonChangeService = jsonChangeService
}
process () {
this.jsonChangeService.jsonChange(this.json)
}
}
And the service
import {Injectable, EventEmitter} from '#angular/core';
#Injectable()
export default class JsonChangeService {
public jsonObject: Object;
stateChange: EventEmitter<Object> = new EventEmitter<Object>();
constructor(){
this.jsonObject = {};
}
jsonChange(obj) {
console.log('sending', obj)
this.jsonObject = obj
this.stateChange.next(this.jsonObject)
}
}
Create a service like so...
import {Injectable, EventEmitter} from 'angular2/core';
#Injectable()
export class MyService {
private searchParams: string[];
stateChange: EventEmitter<any> = new EventEmitter<any>();
constructor(){
this.searchParams = [{}];
}
change(value) {
this.searchParams = value;
this.stateChange.next(this.searchParams);
}
}
Then in your component...
import {Component} from 'angular2/core';
import {MyService} from './myService';
#Component({
selector: 'my-directive',
pipes: [keyValueFilterPipe],
templateUrl: "./src/someTemplate.html",
providers: [MyService]
})
export class MyDirective {
public searchParams: string[];
constructor(private myService: MyService) {
this.myService = myService;
myService.stateChange.subscribe(value => { this.searchParams = value; console.log('Change made!') })
}
change(){
this.myService.change(this.searchParams);
}
}
You have to subscribe to the eventemitter, then update your variable. The change event in the service would get fired of from something like...
(click)="change()"

Categories

Resources