In Angular(5), I'm trying to write a reusable group of form controls for a template driven form. It needs to be able to take a model from a parent component and pass it to the element for two-way data binding.
Here's what I have.
admin-panel.component.ts (Parent Form)
import { Component, OnInit } from '#angular/core';
import{ NgForm } from '#angular/forms';
#Component({
selector: 'admin-panel',
template: `
<label>Settings</label>
<!-- the below doesn't work, but is an example of how I'd like to use it -->
<settings name="settings" [(ngModel)]="preset.settings"></settings>
`
})
export class AdminPanelComponent implements OnInit
{
preset;
ngOnInit()
{
this.preset = {
name: '',
settings: {
settingOne: 'foo',
settingTwo: false,
settingThree: 14
}
}
}
settings.component.html
For the ngModels below, I've also tried to set it like model.settingOne,model.settingTwo, but this didn't work either.
<div [ngModelGroup]="group">
<select name="settingOne" [(ngModel)]="model.settingOne">
<option value="foo">Foo</option>
<option value="bar">Bar</option>
</select>
<input type="checkbox" name="settingTwo" [(ngModel)]="model.settingTwo">
</div>
settings.component.ts
import { Component, Input } from '#angular/core';
import { ControlContainer, NgForm } from '#angular/forms';
#Component({
selector: 'settings',
templateUrl: './settings.component.html',
viewProviders:[{provide: ControlContainer, useExisting: NgForm}]
})
export class SettingsComponent
{
#Input('name') group: string;
//#Input('model') model;
}
Ah.. silly mistake. The above code indeed works with a couple minor changes.
First, in admin-panel.component.ts, replacing the line
<settings name="settings" [(ngModel)]="preset.settings"></settings>
with
<settings name="settings" [(model)]="preset.settings"></settings>
to match the input variable in settings.component.ts was on the right track.
But, it appeared to not work because in my form I had
[ngFormOptions]="{ updateOn: 'submit' }", so nothing was getting updated. Removing that allowed the updates to come through.
If one implements "ControlValueAccessor" on any component then we can have two-way data binding automatically for your custom component.
Implementation here
Related
I'm attempting to listen to changes on a reactive email form control like this:
import { Component, OnChanges } from '#angular/core';
import { FormGroup, FormControl, Validators } from '#angular/forms';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnChanges {
form: FormGroup = new FormGroup({
email: new FormControl('',[ Validators.email ])
});
get emailInput() { return this.form.get('email'); }
ngOnChanges() {
this.form.get('email').valueChanges.subscribe(val => {
const formattedMessage = `Email is ${val}.`;
console.log(formattedMessage);
});
}
}
The form looks like this:
<form [formGroup]="form">
<input placeholder="Email" type="email" formControlName="email" >
</form>
When typing in the email field nothing gets logged. This is the Stackblitz. Thoughts?
This is the article the question implementation was based on.
Update
The accepted answer is to use the ngOnInitit lifecycle hook. I wanted if perhaps it should be ngAfterViewInit just to make sure the view is entirely initialized or will be form bindings always be complete in ngOnInit?
Didn't notice at first, but your ngOnChanges should not be where you are subscribing to the observable. ngOnChanges is for changes to input parameters to the current component (typically wrapped in []).
Setup your subscription to the observable in the ngOnInit like this and your code will work:
ngOnInit() {
this.emailSubscription = this.form.get('email').valueChanges.subscribe(val => {
const formattedMessage = `Email is ${val}.`;
console.log(formattedMessage);
});
}
Angular does not automatically unsubscribe so typically you'll want to save the value of the description, and then unsubscribe it in the ngOnDestroy:
ngOnDestroy() {
this.emailSubscription.unsubscribe();
}
Since you're writing this code in appComponent there's probably not an explicit need to do this outside it being generally good practice for every other component.
Edit: Updated stackblitz showing this working.
You're using onChanges wrong. OnChanges watches for changes performed on a child component so that the parent component can update information. You're doing this with a form, so nothing will send changes up to the component.
Since the input is an element on the component, you can do it with an (input) listener or a (keypress).
I have completely reworded this question and included a complete code sample.
I have an intermittent issue where clicking the button sometimes shows the validation error message, instead of executing the router.nagivate command. Then, I have to click it a second to work. As I said, this is intermittent. The solution needs to include the focus behavior of the sample below, or an alternative way to focus on input html tags. Sometimes, I only have to click once. Why? And, how can I control this behavior so that it is not random?
I am posting two test components to demonstrate the issue. Any help would be greatly appreciated.
test.component.html
<form novalidate #f="ngForm">
<h2>Scan Part</h2>
<input id="partNum" type="number" class="form-control" required [correctPart]="orderShipDetail?.UPC" name="partNum" [(ngModel)]="model.partNum" #partNum="ngModel" #partNumRef />
<div *ngIf="partNum.invalid && (partNum.dirty || partNum.touched)" class="text-danger">
<p *ngIf="partNum.errors.required">PartNum is required.</p>
<p *ngIf="partNum.errors.correctPart">Please scan the correct part. </p>
</div>
<button type="button" (click)="onClickCartonPartButton()">Carton Parts</button>
</form>
test.component.ts
import { Component, OnInit, AfterViewChecked, ViewChild, ElementRef } from '#angular/core';
import { Router } from '#angular/router';
class TestForm {
constructor(
public partNum: string = '') {
}
}
#Component({
selector: 'test',
templateUrl: './test.component.html',
styleUrls: ['./test.component.scss']
})
export class TestComponent implements OnInit, AfterViewChecked {
#ViewChild('partNumRef') partNumRef: ElementRef;
model: TestForm = new TestForm();
public focusedElement: ElementRef;
constructor(
private router: Router,
private route: ActivatedRoute
) { }
ngAfterViewChecked() {
this.focusedElement.nativeElement.focus();
}
ngOnInit() {
this.focusedElement = this.partNumRef;
}
onClickCartonPartButton() {
try {
this.router.navigate(['/test2', 1006, 1248273, 1234]);
} catch (ex) {
console.log(ex);
}
}
}
test2.component.html
<a [routerLink]="['/test', 1006, 1248273, 1234, 5 ]">click this</a>
test2.component.ts
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'test2',
templateUrl: './test2.component.html',
styleUrls: ['./test2.component.scss']
})
export class Test2Component implements OnInit {
constructor() { }
ngOnInit() {
}
}
Add these routes to the app.module.ts
{ path: 'test/:empId/:orderNumber/:licensePlate/:orderLine', component: TestComponent },
{ path: 'test2/:empId/:orderNumber/:licensePlate', component: Test2Component },
Set type="button" on the two hyperlinks to avoid submit
A button element with no type attribute specified represents the same
thing as a button element with its type attribute set to "submit".
Or put the two hyperlinks outside of the form
Remove partNum.touched from *ngIf in the validation message div, like this...
<div *ngIf="partNum.invalid && partNum.dirty" class="text-danger">
<p *ngIf="partNum.errors.required">PartNum is required.</p>
<p *ngIf="partNum.errors.correctPart">Please scan the correct part.</p>
</div>
Clearly my goal is to make an element as <input type="text" placeholder="example" /> look likes <input type="text" [placeholder]="'example'" /> after using an angular directive as
import { Directive, HostBinding } from '#angular/core';
#Directive({
selector: 'input[placeholder]'
})
export class PlaceholderDirective {
#HostBinding('placeholder') placeholder:string;
constructor() {
}
}
But I don't really now how I could take the firstly set placeholder="A placeholder" in a [placeholder]without having an undefined value.
Okay, I answered myself by asking and writing the question, but here it is for those who'll ask themselves this:
I imported ElementRef to access the original placeholder value and set it in the constructor directly!
import { Directive, HostBinding, ElementRef} from '#angular/core';
#Directive({
selector: 'input[placeholder]'
})
export class PlaceholderDirective{
#HostBinding('placeholder') placeholder:string;
constructor(elementRef: ElementRef) {
this.placeholder = elementRef.nativeElement.placeholder;
}
}
I have created dynamic component instances by selecting pre-existing components. For example,
#Component({
selector: 'dynamic-component',
template: `<div #container><ng-content></ng-content></div>`
})
export class DynamicComponent {
#ViewChild('container', {read: ViewContainerRef}) container: ViewContainerRef;
public addComponent(ngItem: Type<WidgetComponent>,selectedPlugin:Plugin): WidgetComponent {
let factory = this.compFactoryResolver.resolveComponentFactory(ngItem);
const ref = this.container.createComponent(factory);
const newItem: WidgetComponent = ref.instance;
newItem.pluginId = Math.random() + '';
newItem.plugin = selectedPlugin;
this._elements.push(newItem);
return newItem;
}
}
My pre-existed components are ChartWidget and PatientWidget which extended the class WidgetComponent that I wanted to add in the container. For example,
#Component({
selector: 'chart-widget',
templateUrl: 'chart-widget.component.html',
providers: [{provide: WidgetComponent, useExisting: forwardRef(() => ChartWidget) }]
})
export class ChartWidget extends WidgetComponent implements OnInit {
constructor(ngEl: ElementRef, renderer: Renderer) {
super(ngEl, renderer);
}
ngOnInit() {}
close(){
console.log('close');
}
refresh(){
console.log('refresh');
}
...
}
chart-widget.compoment.html (using primeng Panel)
<p-panel [style]="{'margin-bottom':'20px'}">
<p-header>
<div class="ui-helper-clearfix">
<span class="ui-panel-title" style="font-size:14px;display:inline-block;margin-top:2px">Chart Widget</span>
<div class="ui-toolbar-group-right">
<button pButton type="button" icon="fa-window-minimize" (click)="minimize()"</button>
<button pButton type="button" icon="fa-refresh" (click)="refresh()"></button>
<button pButton type="button" icon="fa-expand" (click)="expand()" ></button>
<button pButton type="button" (click)="close()" icon="fa-window-close"></button>
</div>
</div>
</p-header>
some data
</p-panel>
data-widget.compoment.html (same as chart-widget using primeng Panel)
#Component({
selector: 'data-widget',
templateUrl: 'data-widget.component.html',
providers: [{provide: WidgetComponent, useExisting: forwardRef(() =>DataWidget) }]
})
export class DataWidget extends WidgetComponent implements OnInit {
constructor(ngEl: ElementRef, renderer: Renderer) {
super(ngEl, renderer);
}
ngOnInit() {}
close(){
console.log('close');
}
refresh(){
console.log('refresh');
}
...
}
WidgetComponent.ts
#Component({
selector: 'widget',
template: '<ng-content></ng-content>'
})
export class WidgetComponent{
}
Now I added the components by selecting a component from the existed components (e.g. chart-widget and data-widget) in the following way and stored the instances into an array.
#Component({
templateUrl: 'main.component.html',
entryComponents: [ChartWidget, DataWidget],
})
export class MainComponent implements OnInit {
private elements: Array<WidgetComponent>=[];
private WidgetClasses = {
'ChartWidget': ChartWidget,
'DataWidget': DataWidget
}
#ViewChild(DynamicComponent) dynamicComponent: DynamicComponent;
addComponent(): void{
let ref= this.dynamicComponent.addComponent(this.WidgetClasses[this.selectedComponent], this.selectedComponent);
this.elements.push(ref);
this.dynamicComponent.resetContainer();
}
}
Now, I am facing problem to render the components using innerHtml in main.component.html. It render the html but I am not able to use button click event or other event on it. I have also tried to render chart using primeng but its also not working.
main.component.html
<dynamic-component [hidden]="true" ></dynamic-component>
<widget *ngFor="let item of elements">
<div [innerHTML]="item._ngEl.nativeElement.innerHTML | sanitizeHtml">
</div>
</widget>
I have also implemented a sanitizeHtml Pipe but its giving still same result. So, as I understand innerHTML is only showing the html data but I can't use any button event as well as the js chart. I have also tried to show the items like this {{item}} under tag. But it display like a text [object object]. So, could anyone give a solution for it? How can I render the components allowing the button events and js chart? Thanks.
EDIT: See my Plunker here https://plnkr.co/edit/lugU2pPsSBd3XhPHiUP1?p=preview
You can see here, it is possible to add chart or data widget dynamically and I am showing it using innerHTML. So, the button events are not working here. If I coding like {{item}} then it shows [object object] text. You can also see in console the component array data. The main Question is, How can I active the button events on it (e.g. if i click close or refresh button then it will call the related functions)?
I would create structural directive like:
view.directive.ts
import { ViewRef, Directive, Input, ViewContainerRef } from '#angular/core';
#Directive({
selector: '[view]'
})
export class ViewDirective {
constructor(private vcRef: ViewContainerRef) {}
#Input()
set view(view: ViewRef) {
this.vcRef.clear();
this.vcRef.insert(view);
}
ngOnDestroy() {
this.vcRef.clear()
}
}
then
app.component.ts
private elements: Array<{ view: ViewRef, component: WidgetComponent}> = [];
...
addComponent(widget: string ): void{
let component = this.dynamicComponent.addComponent(this.WidgetClasses[widget]);
let view: ViewRef = this.dynamicComponent.container.detach(0);
this.elements.push({view,component});
this.dynamicComponent.resetContainer();
}
and
app.component.html
<widget *ngFor="let item of elements">
<ng-container *view="item.view"></ng-container>
</widget>
So i have just moved view from dynamic component container to desired place.
Plunker Example
Hoping for some help on a more complex example that expands on the examples in angular's Tour of Heroes
Rather than submitting a single string each time, how would you submit multiple values like in the following example e.g.:
export class LittleTourComponent {
heroes = [ {
'name':'Hulk',
'power':'strength'
},{
'name':'Bulk',
'power':'appetite'
}];
I presume a new 'entry' made up of the submitted values should be pushed to the heroes array something like this:
addHero(newHero) {
if (newHero) {
var entry = {
'name': newHero.name,
'power': newHero.power
};
this.heroes.push(entry);
}
}
But what would be required in the template? Would you still use keyup.enter in this case?:
template:
<label>name</label
// how should the inputs be filled out in this scenario?
<input >
<label>power</label>
<input >
<button (click)=addHero(newHero)>Add</button>
<ul *ngFor="let hero of heroes">
<li>name:{{hero.name}}</li>
<li>power:{{hero.power}}</li>
</ul>
example also on plnkr
Any help appreciated. thanks!
Try and do this in your ts file:
import {Component} from '#angular/core';
class Hero {
name: string;
power: string;
}
export class LittleTourComponent {
newHero: Hero;
constructor() {
this.newHero = new Hero();
}
heroes = [{
'name': 'Hulk',
'power': 'strength'
}, {
'name': 'Bulk',
'power': 'appetite'
}];
addHero() {
if (this.newHero) {
var entry = {
'name': this.newHero.name,
'power': this.newHero.power
};
this.heroes.push(entry);
}
}
}
...and this in your html
<label>name</label>
<input [(ngModel)]="newHero.name">
<label >power</label>
<input [(ngModel)]="newHero.power">
<button (click)=addHero()>Add</button>
<ul *ngFor="let hero of heroes">
<li>name:{{hero.name}}</li>
<li>power:{{hero.power}}</li>
</ul>
your click listener is calling what it thinks is a reference to an element in the DOM which u havent defined nor would take paramaters. Trying putting quotes around that callback
<label>name</label
// how should the inputs be filled out in this scenario?
<input >
<label>power</label>
<input >
<button (click)="addHero(newHero)">Add</button>
<ul *ngFor="let hero of heroes">
<li>name:{{hero.name}}</li>
<li>power:{{hero.power}}</li>
</ul>
after further review, i notice ur referencing newHero in the little-tour component which does not exist in that components scope. Also, uve bound correctly to your inputs but i dont believe .value is the correct property to return the input... try
[(ngModel)]="input1"
in your class declaration ad
input1: String;
and then using that variable.
I didnt notice until right now that you arent importing your directive
import { Component } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: 'app/app.component.html'
})
export class AppComponent { }
since u are calling
<little-tour></little-tour>
in your app.component.html then this should be your app component
import { Component } from '#angular/core';
import {LittleTourComponent} from 'path-to-little-tour'
#Component({
selector: 'my-app',
templateUrl: 'app/app.component.html',
directives: [LittleTourComponent]
})
export class AppComponent { }