I'm creating some components in order to learn Angular2.
I have this basic html:
<h1>test</h1>
<btn [order]="0"></btn>
<btn [order]="1"></btn>
<btn [order]="2"></btn>
And in the ts I have this:
import {Component, Input} from 'angular2/core';
import {DataService} from '../services/DataService';
#Component({
selector: 'btn',
template: '<button>test{{ item }}</button>',
inputs: ['order']
})
export class ButtonComponent {
items: Array<number>;
item: number;
#Input() order;
constructor(dataService: DataService) {
console.log(this.order)
}
}
Doing that I get undefined, what am I doing wrong? how can I read the inputs (or an attribute) in order to send data to the class?
EDIT
import {Component, Input} from 'angular2/core';
import {DataService} from '../services/DataService';
#Component({
selector: 'btn',
template: '<button>test{{ item }}</button>',
inputs: ['order']
})
export class ButtonComponent {
items: Array<number>;
item: number;
#Input() order;
ngOnInit(dataService: DataService) {
this.items = dataService.getItems();
console.log(this.order)
}
constructor() {}
}
You can't access them in the constructor, they are not yet initialized. Use ngOnInit() instead. For more details see https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html
export class ButtonComponent implements OnInit {
items: Array<number>;
item: number;
#Input() order;
constructor(dataService: DataService) { }
ngOnInit() {
console.log(this.order);
}
}
Related
This is parent.html
<child *ngFor="let detail of listContact; let i = index"
[detailsItem]="detail" [index]="i">
</child>
This is child.compenent
export class ChildComponent implements OnInit {
#Input() index: number;
#Input() detailsItem: any;
constructor() {
}
ngOnInit() {
}
saveChild(){
console.log('index', this.index);
}
}
And I want to call the method saveChild in parentComponent like this:
export class ParentComponent implements OnInit {
private child: ChildComponent;
#Input() index: number;
#Input() detailsItem: any;
constructor() {
}
ngOnInit() {
}
save(){
this.child.saveChild();
}
}
you can use #ViewChild to run that.
import { AfterViewInit, Component, ViewChild } from '#angular/core';
import { ChildComponent } from '../child/child.component';
#Component({
selector: 'app-parent',
templateUrl: './parent.component.html',
styleUrls: ['./parent.component.css']
})
export class ParentComponent implements AfterViewInit {
#ViewChild(ChildComponent) public child!:ChildComponent
constructor() { }
ngAfterViewInit(): void {
this.child.childMethod();
}
}
It's going to let you access the properties and methods inside the ChildComponent. However remember to use ngAfterViewInit so that the child is initialised when you try and access it.
Whatever i do angular does not detect change on talks array. I have a handleSubmit function to send the toolbar. Toolbar use it to send the changes to parent from input field.
My app.component.ts file
import { Component, Type, OnChanges, SimpleChanges } from '#angular/core';
import { getResponse } from '../api/API';
declare module '../api/API' {
export interface NlpAPI {
getResponse(data: any): Promise<any>;
}
}
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnChanges {
talks: string[];
title: string;
ngOnChanges(changes: SimpleChanges): void {
console.log(changes);
}
constructor() {
this.talks = [];
this.title = 'Talks';
}
ngOnInit() {
this.talks.push('Welcome to ProjectX! How can I help you?');
this.talks.push('I am a chatbot. I can help you with your queries.');
}
handleSubmit(data: any): void {
this.talks.push(data.talk);
}
messageResponse() {
// #ts-ignore: Object is possibly 'null'.
const x = document.getElementById('txt').value;
// #ts-ignore: Object is possibly 'null'.
document.getElementById('output').innerHTML =
'Your message is ' + '"' + x + '"';
}
}
My app.component.html
<!-- Toolbar -->
<app-custom-toolbar [handleSubmit]="handleSubmit"></app-custom-toolbar>
<!-- Highlight Card -->
<app-text-area [talksFromUser]="talks" [title]="title"></app-text-area>
<!-- Bottombar -->
<router-outlet></router-outlet>
My text-area.component.ts file
import { Component, Input, OnChanges, SimpleChanges } from '#angular/core';
#Component({
selector: 'app-text-area',
templateUrl: './text-area.component.html',
styleUrls: ['./text-area.component.css'],
})
export class TextAreaComponent implements OnChanges {
#Input() talksFromUser: string[] = [];
#Input() title: string = '';
constructor() {}
ngOnChanges(changes: SimpleChanges): void {
console.log(changes);
}
}
My text-area.component.html
<div class="main-text-area">
<div *ngFor="let item of talksFromUser">{{ item }}</div>
</div>
custom-toolbar.component.ts file
import { Component, Input, OnInit } from '#angular/core';
import { NgForm } from '#angular/forms';
#Component({
selector: 'app-custom-toolbar',
templateUrl: './custom-toolbar.component.html',
styleUrls: ['./custom-toolbar.component.css'],
})
export class CustomToolbarComponent implements OnInit {
talks: string[] = [];
#Input() handleSubmit!: (args: any) => void;
constructor() {}
ngOnInit(): void {}
onSubmit(f: NgForm) {
this.handleSubmit(f.value);
f.resetForm();
}
}
I tried also
this.talks = [...this.talks, data.talk]
Thank you all.
There are two issues in your code:
First one, you are calling handleSubmit("string") (so data is a string), but you are pushing data.talk, which is undefined (so talks will be [undefined, undefined, ...]). To fix it, use data:
handleSubmit(data: any): void {
this.talks.push(data); // use "data" instead of "data.talk"
}
Second one, you are using a AppComponent method into CustomToolbarComponent class. You need to keep the this scope of AppComponent. Also, you should use arrow functions:
handleSubmit = (data: any): void => {
this.talks.push(data);
}
I'm not sure how can I use a custom component if it's wrapper under another component.
Like:
ComponentA_withForm
|
--ComponentA1_withWrapperOfCustomInput
|
--ComponentA11_withCustomInput
if I have a structure like this:
ComponentA_withForm
|
--ComponentA11_withCustomInput
Everything's fine
But for my case (tons of async data) I need a wrapper... Is it possible somehow to do this?
Here is my fiddle code:
ComponentA:
import { Component } from '#angular/core';
import { FormBuilder } from '#angular/forms';
#Component({
selector: 'my-app',
template: `<form [formGroup]="form"><custom-input-wrapper formControlName="someInput"></custom-input-wrapper></form> <p>value is: {{formVal | json}}</p>`
})
export class AppComponent {
form = this.fb.group({
someInput: [],
});
get formVal() {
return this.form.getRawValue();
}
constructor(private fb: FormBuilder) { }
}
ComponentA1:
import { Component } from '#angular/core';
#Component({
selector: 'custom-input-wrapper',
template: '<custom-input></custom-input>',
})
export class CustomInputWrapperComponent {
constructor() { }
}
ComponentA11:
import { Component, forwardRef } from '#angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '#angular/forms';
#Component({
selector: 'custom-input',
template: `Hey there! <button (click)="inc()">Value: {{ value }}</button>`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true,
}],
})
export class CustomInputComponent implements ControlValueAccessor {
private value = 0;
writeValue(value: number): void {
this.value = value;
}
registerOnChange(fn: (_: any) => void): void {
this.onChangeFn = fn;
}
registerOnTouched(fn: any): void {
}
inc() {
this.value = this.value + 1;
this.onChangeFn(this.value);
}
onChangeFn = (_: any) => { };
}
And here I have a working sample:
https://stackblitz.com/edit/angular-qmrj3a
so: basically removing & refactoring code not to use CustomInputWrapperComponent makes my code working. But I need this wrapper and I'm not sure how to pass formControlName then.
I don't want a dirty solution with passing parent formGroup :)
Since you don't want a dirty solution ;) , you could just implement ControlValueAccessor in the CustomInputWrapperComponent also. That way any change in the parent will be reflected in the child, any change in the child will be reflected in the parent as well with just few lines of code.
Wrapper Component
#Component({
selector: 'custom-input-wrapper',
template: '<custom-input [formControl]="value"></custom-input>',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputWrapperComponent),
multi: true,
}]
})
export class CustomInputWrapperComponent implements AfterViewInit, ControlValueAccessor {
public value = new FormControl();
constructor() { }
ngAfterViewInit() {
this.value.valueChanges.subscribe((x) => {
this.onChangeFn(x);
});
}
writeValue(value: number): void {
this.value.setValue(value);
}
registerOnChange(fn: (_: any) => void): void {
this.onChangeFn = fn;
}
registerOnTouched(fn: any): void {
}
onChangeFn = (_: any) => { };
}
Parent Template
<form [formGroup]="form"><custom-input-wrapper formControlName="someInput"></custom-input-wrapper></form> <p>value is: {{formVal | json}}</p>
I have made a stackbitz demo here - https://stackblitz.com/edit/angular-csaxcz
you cannot use formControlName on custom-input-wrapper because it doesn't implement ControlValueAccessor. implementing ControlValueAccessor on custom-input-wrapper might be a solution but it seems to be overkill. Instead pass the control from formGroup to custom-input-wrapper as an #Input() and pass the inputed formControl to custom-input
app.component
#Component({
selector: 'my-app',
template: `<form [formGroup]="form"><custom-input-wrapper [formCtrl]="form.get('someInput')"></custom-input-wrapper></form> <p>value is: {{formVal | json}}</p>`
})
export class AppComponent {
form = this.fb.group({
someInput: [],
});
get formVal() {
return this.form.getRawValue();
}
constructor(private fb: FormBuilder) { }
}
custom-input-wrapper.component
#Component({
selector: 'custom-input-wrapper',
template: '<custom-input [formControl]="formCtrl"></custom-input>',
})
export class CustomInputWrapperComponent {
#Input() formCtrl: AbstractControl;
constructor() { }
}
here is a working demo https://stackblitz.com/edit/angular-3lrfqv
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 :)
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