Angular 5 - Validating own input components derived from a base componet - javascript

Currently we are going to port our old application to new technologies and get rid of old design failures and improve the overall UX.
At the moment we are wrapping all standard inputs in own components, so we can change them very easily if needed. All our input components could have a validation but the input components can be nested in an input group of bootstrap, like this:
<form #myForm="ngForm">
<div>
<label>Birth Year</label>
<vng-controls-textbox #user="ngModel" name="user" minlength="5" ngModel ></vng-controls-textbox>
<label>Text</label>
<vng-controls-textbox #text="ngModel" name="text" maxlength="5" ngModel ></vng-controls-textbox>
</div>
<div vng-control-group [icon]="['fa','user']">
<vng-controls-textbox required type="text" #username="ngModel" ngModel name="Username">
</vng-controls-textbox>
<vng-controls-textbox required type="password" #password="ngModel" ngModel name="Passwort">
</vng-controls-textbox>
</div>
</form>
The validation works fine for standalone input components, but we want to deactivate the validation for each nested component in our and get all errors of all possible controls with validations. The desired solution should be a generic approach and no specifics, if possible.
Our base component:
import { Component, OnInit, EventEmitter, Input, Output, forwardRef, AfterViewInit, Self } from '#angular/core';
import { Validator, ControlValueAccessor, FormControl, NgControl } from '#angular/forms';
#Component({
selector: 'app-base'
})
export class BaseComponent implements OnInit, ControlValueAccessor, Validator, AfterViewInit//, IControlBase<any>
{
public ngControl: any;
private parseError: boolean;
private _value: any = '';
/** Input value */
#Input() set value(newValue: any) {
if (newValue != this._value && newValue !== undefined) {
console.log("[BaseComponent]: Set value " + newValue);
this.valueChange.emit(newValue);
this._value = newValue;
}
};
get value(): any {
if (this._value) {
return this._value;
}
}
/** Platzhalter */
#Input() placeholder: any;
/** Disabled */
#Input() disabled: boolean;
/** Name des Controls */
#Input() name: string;
/** Optional: Typ des Controls (Text, Password) */
#Input() type?: string;
#Input() hideErrors: boolean = false;
#Output() valueChange: EventEmitter<any> = new EventEmitter<any>();
// this is the initial value set to the component
public writeValue(obj: any) {
if (obj !== undefined && obj) {
console.log("[BaseComponent] writeValue ", obj)
this._value = obj;
}
}
// registers 'fn' that will be fired wheb changes are made
// this is how we emit the changes back to the form
public registerOnChange(fn: any) {
this.propagateChange = fn;
}
// validates the form, returns null when valid else the validation object
// in this case we're checking if the json parsing has passed or failed from the onChange method
public validate(c: FormControl) {
return null;
}
// not used, used for touch input
public registerOnTouched() { }
// change events from the textarea
private onChange(event) {
if (event.target.value !== undefined) {
console.log("[BaseComponent] "+this.name+" OnChange " + event.target.value)
// get value from text area
this._value = event.target.value;
this.propagateChange(this._value);
}
}
// the method set in registerOnChange to emit changes back to the form
private propagateChange = (_: any) => { };
registerOnValidatorChange?(fn: () => void): void {
console.log("[BaseComponent]: registerOnValidatorChange");
}
setDisabledState?(isDisabled: boolean): void {
console.log("[BaseComponent]: setDisabledState");
}
constructor(#Self() ngControl: NgControl) {
ngControl.valueAccessor = this;
if (ngControl) {
this.ngControl = ngControl;
}
}
ngOnInit() {
}
ngAfterViewInit(): void {
//debugger;
//this.placeholder = this.translateService.instant((this.placeholder ? this.placeholder : this.name))
}
}
Our control group component typescript:
import { Component, OnInit, Input, Host, Self } from '#angular/core';
import { ControlContainer, FormControl, AbstractControl, NgControl } from '#angular/forms';
import { BaseComponent } from '../../_bases/base.component'
import { QueryList, ViewChildren, ContentChildren, TemplateRef, ContentChild } from '#angular/core';
import { TextboxComponent } from '../textbox/textbox.component';
/**
* Komponente für Input-Groups
* #example
* <div vng-control-group [config]="textboxUsernameConfig">
* // Kann ein diverses Control sein, was als CSS-Klasse "form-control besitzt"
* <input class="input form-control" />
* </div>
*
* // oder
*
* <div vng-control-group [icon]="['fa','user']" [label]="Test Label" [position]="'append'">
*/
#Component({
selector: 'div[vng-control-group]',
templateUrl: './control-group.component.html'
})
export class ControlGroupComponent {
private controlContainer: ControlContainer;
public formControl: AbstractControl;
#ContentChild(TemplateRef) template: TemplateRef<any>;
ngAfterContentInit() {
}
/** Konfiguration der Inputgroup als Objekt */
#Input() config: InputGroupConfig;
/** Icon als Attribut (z.B. im HTML) */
#Input() icon?: object;
/** Label als Attribut (z.B. im HTML) */
#Input() label?: string;
/** Position als Attribut (z.B. im HTML) */
#Input() position?: GroupPosition;
constructor(#Host() parent: ControlContainer) {
//this.controlContainer = parent;
//this.formControl = this.controlContainer.control;
}
ngOnInit() {
// Wenn kein Konfig-Objekt übergeben worden ist, setze die Attribute bzw. setze Default-Werte
if (!this.config) {
console.log("[ControlGroupComponent]: Keine Config übergeben")
this.config = {
icon: this.icon || ['fa', 'question'],
label: this.label || '',
position: this.position || GroupPosition.prepend
}
}
}
}
export interface InputGroupConfig {
icon?: object,
label?: string,
position?: GroupPosition
}
export enum GroupPosition {
append = 'append',
prepend = 'prepend'
}
Our control-group html:
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text" id="basic-addon1">#</span>
</div>
<ng-content></ng-content>
</div>
Show error component (ts/html):
// show-errors.component.ts
import { Component, Input } from '#angular/core';
import { AbstractControlDirective, AbstractControl } from '#angular/forms';
#Component({
selector: 'show-errors',
template: `
<ul *ngIf="shouldShowErrors()">
<li style="color: red" *ngFor="let error of listOfErrors()">{{error}}</li>
</ul>
`,
})
export class ShowErrorsComponent {
private static readonly errorMessages = {
'required': () => 'This field is required',
'minlength': (params) => 'The min number of characters is ' + params.requiredLength,
'maxlength': (params) => 'The max allowed number of characters is ' + params.requiredLength,
'pattern': (params) => 'The required pattern is: ' + params.requiredPattern,
'years': (params) => params.message,
'countryCity': (params) => params.message,
'uniqueName': (params) => params.message,
'telephoneNumbers': (params) => params.message,
'telephoneNumber': (params) => params.message
};
#Input()
private control: AbstractControlDirective | AbstractControl;
shouldShowErrors(): boolean {
return this.control &&
this.control.errors &&
(this.control.dirty || this.control.touched);
}
listOfErrors(): string[] {
return Object.keys(this.control.errors)
.map(field => this.getMessage(field, this.control.errors[field]));
}
private getMessage(type: string, params: any) {
return ShowErrorsComponent.errorMessages[type](params);
}
}
How can we achieve that? I'm pretty stuck since 3 days and can't get it working.I really don't know how to disable the error messages in the content of and print all errors messages of all nested inputs under the input group.
Besides that, our nested inputs within the control group are looking pretty bad:
Bad bootstrap format on nested inputs
I've created a stackblitz example which demonstrates the behavior of both problems:
https://angular-ng-bootstrap-khjkkhhjkhk.stackblitz.io
Editor:
https://stackblitz.com/edit/angular-ng-bootstrap-khjkkhhjkhk
Do you have any idea how can we fix that too? I guess it's because of the which wraps the controls in an additional div which causes the strange looking inputs. Preferably we want to style them in the manner of bootstrap.

Maybe it's not the case but I see you're not declaring the providers in #Component decorator using ControlValueAccessor.
Like this:
#Component({
selector: 'app-input',
templateUrl: './input.component.html',
styleUrls: ['./input.component.scss'],
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputComponent), multi: true },
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => InputComponent), multi: true }
]
})

Related

How to unselect selected option-element in select-element using Angular 8

I have the following requirements:
I got two FormControl objects for select-elements mainSelect and subSelect that are required.
subSelect changes depending on the value from mainSelect.
When mainSelect changes to a value in which the value from subSelect isn't included subSelect needs to become invalid so the FormGroup both of the FormControl's are part of becomes invalid, too.
But if the value from subSelect is included subSelect needs to hold his actual value.
(A concrete example is described after the StackBlitz link.)
My problem solving this requirement:
If the value of mainSelect changes and the value of subSelect isn't included subSelect takes the first value of the list instead of becoming null/invalid.
So the SOLUTION would be if the selected value of 'subSelect' becomes null and no value is selected in the browser.
What I tried so far:
I tried to create a component and implement the ControlValueAccessor interface. Seems like here lies my problem. I think I don't really understand how that works.
I watched the following video on YouTube and read articles (1, 2) related to ControlValueAccessor, but still couldn't solve my problem.
This is part of my code:
Also you can find it on StackBlitz
Example
If in the browser MainSelect has the value thirdMainSelect and SubSelect has the value fifthSubSelect and MainSelect changes his value to firstMainSelect SubSelect should have no selected value.
select.component.ts
export class SomeObject {
value: string;
parameters: {[parameterName: string]: string} = {};
}
#Component({
selector: "app-select",
templateUrl: "./select.component.html",
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: SelectComponent,
multi: true
}]
})
export class SelectComponent implements ControlValueAccessor, OnChanges {
#ViewChild("select", {static: true}) select: ElementRef;
#Input() tableId: string;
#Input() filter: { [parameterName: string]: string};
returnedTable: SomeObject[];
onChange: (_: any) => void;
onTouched: () => void;
selected: string;
constructor(private tableService: TableService) { }
loadTable(): void {
this.tableService.getTable(this.tableId, this.filter)
.subscribe(table => {
this.returnedTable = table;
});
}
ngOnChanges(): void {
this.loadTable();
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
writeValue(value: string): void {
this.selected = value;
}
}
select.component.html
<select class="form-control" #select (change)="onChange($event.target.value)">
<option *ngFor="let item of returnedTable" [value]="item.value" [selected]="selected === item.value">{{item.value}}</option>
</select>
app.component.ts
#Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.scss"]
})
export class AppComponent implements OnInit {
form: FormGroup;
containerObject: ContainerObject;
selectedMainValue: string;
constructor(private tableService: TableService,
private formBuilder: FormBuilder) {
}
ngOnInit(): void {
this.tableService.getContainerObject()
.subscribe(containerObject => {
this.containerObject = containerObject;
this.selectedMainValue = containerObject.mainSelect;
this.initForm();
});
}
private initForm(): void {
this.form = this.formBuilder.group({
mainSelect: [this.containerObject.mainSelect, Validators.required],
subSelect: [this.containerObject.subSelect, Validators.required]
});
this.subscribeToMainSelectChanged();
this.subscribeToSubSelectChanged();
}
onSubmit(): void {
if (this.form.valid) {
this.containerObject.mainSelect = this.form.get("mainSelect").value;
this.containerObject.subSelect = this.form.get("subSelect").value;
this.tableService.saveContainerObject(this.containerObject);
}
}
private subscribeToMainSelectChanged() {
this.form.get("mainSelect").valueChanges
.subscribe(mainSelect => {
this.selectedMainValue = mainSelect;
console.log(this.form.status);
});
}
private subscribeToSubSelectChanged() {
this.form.get("subSelect").valueChanges
.subscribe(() => {
console.log(this.form.status);
});
}
}
app.component.html
<div>
<form id="wrapper" [formGroup]="form" (ngSubmit)="onSubmit()">
<div id="left" class="form-group row">
<label for="mainSelect" class="col-form-label col-sm-2">MainSelect</label>
<div class="col-sm-6">
<app-select
id="mainSelect"
formControlName="mainSelect"
[tableId]="'mainSelectTable'"
[filter]="{firstType: 'firstParameter'}"
></app-select>
</div>
</div>
<div id="right" class="form-group row">
<label for="subSelect" class="col-form-label col-sm-2">SubSelect</label>
<div class="col-sm-6">
<app-select
id="subSelect"
formControlName="subSelect"
[tableId]="'firstParameter'"
[filter]="{firstType: 'firstParameter', secondType: selectedMainValue}"></app-select>
</div>
</div>
<p></p>
<button id="button" type="submit">Submit</button>
</form>
</div>
I think that it's only use valuesChange. If you has two arrays data and subdata and a form like
form = new FormGroup({
prop1: new FormControl(),
prop2: new FormControl()
});
A simple
this.form.get("prop1").valueChanges.subscribe(res => {
this.dataService.getData(res).subscribe(data=>{
this.subdata=data;
if (!this.subdata.find(x=>x.value==this.form.get("prop2").value))
this.form.get("prop2").setValue(null);
}).unsubscribe()
});
must be enough, see stackblitz
After a while trying to solve this problem only using the SelectComponent and especially writeValue, the following code did the job:
I changed the select.component.ts as following:
export class SomeObject {
value: string;
parameters: {[parameterName: string]: string} = {};
}
#Component({
selector: "app-select",
templateUrl: "./select.component.html",
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: SelectComponent,
multi: true
}]
})
export class SelectComponent implements ControlValueAccessor, OnChanges {
#ViewChild("select", {static: true}) select: ElementRef;
#Input() tableId: string;
#Input() filter: { [parameterName: string]: string};
returnedTable: SomeObject[];
onChange: (_: any) => void;
onTouched: () => void;
selected: string;
constructor(private tableService: TableService) { }
loadTable(): void {
this.tableService.getTable(this.tableId, this.filter)
.subscribe(table => {
this.returnedTable = table;
if (!!this.select && !!this.select.nativeElement.value) {
this.writeValue(this.select.nativeElement.value);
this.onChange(this.selected);
}
});
}
ngOnChanges(): void {
this.loadTable();
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
writeValue(value: string): void {
if (!!this.returnedTable && !this.returnedTable.some(item => item.value === value)) {
this.selected = null;
} else {
this.selected = value;
}
}
}
And the select.component.html like this:
<select class="form-control" #select (change)="onChange($event.target.value)">
<option hidden *ngIf="!selected" value=""></option>
<option *ngFor="let item of returnedTable" [value]="item.value" [selected]="selected === item.value">{{item.value}}</option>
</select>
To deselect an option:— option.selected = false.
Angular library with function decompilation at its core for $scope and Nested Forms, an Angular feature that indicates that Angular team doesn't know how to effectively use HTML. HTML doesn't allow nested forms so why the hell would you try to shoehorn the language to do that? More trouble than it's worth. Of course, you can't expect much better from guys like Bradley Green, former Angular JS manager.

Angular ngx-mat-select-search Custom Component

I am trying to use the ngx-mat-select-search component to put a mat-select style dropdown menu with a search bar in my application.
https://www.npmjs.com/package/ngx-mat-select-search
I have the dropdown working fine, but I am trying to turn it into a custom directive that I can then call and reuse on multiple pages through out the app.
So far I have this: site-dropdown-component.ts
import {AfterViewInit, Component, OnDestroy, OnInit, ViewChild} from '#angular/core';
import {FormControl} from '#angular/forms';
import {ReplaySubject, Subject} from 'rxjs';
import {MatSelect} from '#angular/material';
import {take, takeUntil} from 'rxjs/operators';
#Component({
selector: 'app-site-dropdown',
template: `
<mat-form-field class="w-100">
<mat-select [formControl]="siteCtrl" placeholder="Site" #singleSelect>
<mat-option>
<ngx-mat-select-search [formControl]="siteFilterCtrl" [placeholderLabel]="'Search Sites...'"></ngx-mat-select-search>
</mat-option>
<mat-option *ngFor="let site of filteredSites | async" [value]="site">{{site.name}}</mat-option>
</mat-select>
</mat-form-field>
`
})
export class SiteDropdownComponent implements OnInit, OnDestroy, AfterViewInit {
/** list of sites */
protected sites: Site[] = SITES;
/** control for the selected site */
public siteCtrl: FormControl = new FormControl();
/** control for the MatSelect filter keyword */
public siteFilterCtrl: FormControl = new FormControl();
/** list of sites filtered by search keyword */
public filteredSites: ReplaySubject<Site[]> = new ReplaySubject<Site[]>(1);
#ViewChild('singleSelect') singleSelect: MatSelect;
/** Subject that emits when the component has been destroyed. */
protected onDestroy = new Subject<void>();
constructor() { }
ngOnInit(): void {
// set initial selection
this.siteCtrl.setValue(this.sites);
// load the initial site list
this.filteredSites.next(this.sites.slice());
// listen for search field value changes
this.siteFilterCtrl.valueChanges
.pipe(takeUntil(this.onDestroy))
.subscribe(() => {
this.filterSites();
});
}
ngAfterViewInit(): void {
this.setInitialValue();
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
/**
* Sets the initial value after the filteredBanks are loaded initially
*/
protected setInitialValue() {
this.filteredSites
.pipe(take(1), takeUntil(this.onDestroy))
.subscribe(() => {
// setting the compareWith property to a comparison function
// triggers initializing the selection according to the initial value of
// the form control (i.e. _initializeSelection())
// this needs to be done after the filteredBanks are loaded initially
// and after the mat-option elements are available
this.singleSelect.compareWith = (a: Site, b: Site) => a && b && a.id === b.id;
});
}
protected filterSites() {
if (!this.sites) {
return;
}
// get the search keyword
let search = this.siteFilterCtrl.value;
if (!search) {
this.filteredSites.next(this.sites.slice());
return;
} else {
search = search.toLowerCase();
}
// filter the sites
this.filteredSites.next(
this.sites.filter(site => site.name.toLowerCase().indexOf(search) > -1)
);
}
}
export interface Site {
id: string;
name: string;
}
export const SITES: Site[] = [
{id: 'site1', name: 'Site 1'},
{id: 'site2', name: 'Site 2'},
{id: 'site3', name: 'Site 3'},
];
For the component im trying to use it in, i have:
<app-site-dropdown formControlName="site"></app-site-dropdown>
And inside the component class I have a form:
this.mySearchForm = this.formBuilder.group( {
site: []
});
I can see and interact with the dropdown just fine, but when i submit my form, I cannot get the value of the selected option. It just always returns null when i try mySearchForm.controls['site'].value
What am I missing to be able to inject my custom dropdown component, and retrieve its value upon form submission?
UPDATE:
I was able to make it work by doing the following:
Inside site-dropdown.component.ts, I changed
protected siteCtrl: FormControl;
to
#Input() siteCtrl: FormControl;
And inside my html using the custom dropdown, i added:
<app-site-dropdown [siteCtrl]="myForm.get('site')"></app-site-dropdown>
This allowed me to save the selected value into my form on submission.
you can get the value of the selected option by having your SiteDropdownComponent implement the ControlValueAccessor interface as follows, resulting in your SiteDropdownComponent behaving as a form control and allowing to access the value with e.g. <app-site-dropdown formControlName="site"></app-site-dropdown>:
...
import { forwardRef } from '#angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '#angular/forms';
#Component({
selector: 'app-site-dropdown',
template: ...
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SiteDropdownComponent),
multi: true
}
],
})
export class SiteDropdownComponent implements OnInit, OnDestroy, AfterViewInit, ControlValueAccessor {
...
onChange: Function = (_: any) => {};
onTouched: Function = (_: any) => {};
constructor() { }
ngOnInit() {
...
// call this.onChange to notify the parent component that the value has changed
this.siteCtrl.valueChanges
.pipe(takeUntil(this.onDestroy))
.subscribe(value => this.onChange(value))
}
writeValue(value: string) {
// set the value of siteCtrl when the value is set from outside the component
this.siteCtrl.setValue(value);
}
registerOnChange(fn: Function) {
this.onChange = fn;
}
registerOnTouched(fn: Function) {
this.onTouched = fn;
}
}
See e.g. https://github.com/bithost-gmbh/ngx-mat-select-search/blob/d7ea78d511bbec45143c58c855f013a44d0d5055/src/app/mat-select-search/mat-select-search.component.ts#L134

Email Validation with required overlaps in Angular Reactive Forms

Using Angular Reactive Forms for validation of Email.
I added Validators Required and Validators Email but Its displaying both as shown in below image. I just want one Error to be displayed at a time.
HTML Code :
<form [formGroup]="NamFomNgs">
<label>Email :
<input type="email" name="MylHtm" formControlName="MylNgs">
</label><br>
<div class="ErrMsgCls" *ngIf="(NamFomNgs.controls['MylNgs'].touched || NamFomNgs.controls['MylNgs'].dirty) &&
!NamFomNgs.controls['MylNgs'].valid">
<span *ngIf="NamFomNgs.controls['MylNgs'].errors.required">This field is required</span>
<span *ngIf="NamFomNgs.controls['MylNgs'].errors.email">Enter valid email</span>
</div><br>
<button [disabled]="!NamFomNgs.valid">Submit</button>
</form>
Typescript Code :
NamFomNgs:FormGroup;
constructor(private NavPkjVaj: ActivatedRoute, private HtpCncMgrVaj: HttpClient,private FomNgsPkjVaj: FormBuilder)
{
this.NamFomNgs = FomNgsPkjVaj.group(
{
MylNgs:[null,Validators.compose([
Validators.required,
Validators.email ])]
});
}
I feel it's a bug in angular form.
You can create common component to display validation message:
custom-validation-with-error-message
HTML:
<div *ngIf="errorMessage !== null">
{{errorMessage}}
</div>
constrol-message.component.ts:
import { OnInit } from '#angular/core';
import { Component, Input } from '#angular/core';
import { FormGroup, FormControl, AbstractControl } from '#angular/forms';
import { CustomValidationService } from '../custom-validation.service'
export class ControlMessageComponent implements OnInit {
ngOnInit() {
}
#Input() control: FormControl;
constructor() { }
/**
* This method is use to return validation errors
*/
get errorMessage() {
for (let propertyName in this.control.errors) {
if (this.control.errors.hasOwnProperty(propertyName) && this.control.touched) {
return CustomValidationService.getValidatorErrorMessage(this.getName(this.control), propertyName, this.control.errors[propertyName]);
}
if (this.control.valueChanges) {
return CustomValidationService.showValidatorErrorMessage(propertyName, this.control.errors[propertyName])
}
}
return null;
}
/**
* This method used to find the control name
* #param control - AbstractControl
*/
private getName(control: AbstractControl): string | null {
let group = <FormGroup>control.parent;
if (!group) {
return null;
}
let name: string;
Object.keys(group.controls).forEach(key => {
let childControl = group.get(key);
if (childControl !== control) {
return;
}
name = key;
});
return name;
}
}

Angular custom form input with error message

I'd like to create a custom form input that is just a wrapper around the normal input and that displays an error message in case the formControl isn't valid.
So I made a component implementing ControlValueAccessor but now that I want to display the error message I realize I have no hold of the FormControl, thus I need to pass it as #Input(). The problem is... Why implement the ControlValueAccessor in the first place if I can just pass the FormControl as input ? I'm confused.
html
<input
[placeholder]="placeholder"
[type]="type"
[(ngModel)]="value"
(keyup)="onChange()"
(change)="onChange()"
(blur)="onTouched()"
[disabled]="disabled"/>
ts
#Component({
selector: 'input-app',
templateUrl: './input.component.html',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => InputComponent),
multi: true
}
]
})
export class InputComponent implements ControlValueAccessor {
#Input() type = 'text';
#Input() placeholder = 'placeholder';
#Input() errorMsg = '';
value: string;
disabled: boolean;
private onChangeFn;
private onTouchedFn;
writeValue(val: string | number): void {
this.value = val;
}
registerOnChange(fn: any): void {
this.onChangeFn = fn;
}
registerOnTouched(fn: any): void {
this.onTouchedFn = fn;
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
}
onChange(){
this.onChangeFn(this.value);
}
onTouched(){
this.onTouchedFn();
}
}

Angular 2 How to share variables between components

I'm trying to figure out how to switch a variable in a group of child components
I have this component for a editable form control which switches between view states
import {
Component,
Input,
ElementRef,
ViewChild,
Renderer,
forwardRef,
OnInit
} from '#angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '#angular/forms';
const INLINE_EDIT_CONTROL_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => InlineEditComponent),
multi: true
};
#Component({
selector: 'inline-edit',
templateUrl: 'inline-edit.html',
providers: [INLINE_EDIT_CONTROL_VALUE_ACCESSOR],
})
export class InlineEditComponent implements ControlValueAccessor, OnInit {
#ViewChild('inlineEditControl') inlineEditControl: ElementRef;
#Input() label: string = '';
#Input() type: string = 'text';
#Input() required: boolean = false;
#Input() disabled: boolean = false;
private _value: string = '';
private preValue: string = '';
public editing: boolean = false;
public onChange: any = Function.prototype;
public onTouched: any = Function.prototype;
get value(): any {
return this._value;
}
set value(v: any) {
if (v !== this._value) {
this._value = v;
this.onChange(v);
}
}
writeValue(value: any) {
this._value = value;
}
public registerOnChange(fn: (_: any) => {}): void {
this.onChange = fn;
}
public registerOnTouched(fn: () => {}): void {
this.onTouched = fn;
}
constructor(element: ElementRef, private _renderer: Renderer) {
}
ngOnInit() {
}
}
<div>
<div [hidden]="!editing">
<input #inlineEditControl [required]="required" [name]="value" [(ngModel)]="value" [type]="type" [placeholder]="label" />
</div>
<div [hidden]="editing">
<label class="block bold">{{label}}</label>
<div tabindex="0" class="inline-edit">{{value}} </div>
</div>
</div>
I'm trying to create a simple directive to consume these components and change the editing flag to true
export class EditForm {
//I want to do something like this:
public toggleEdit(fn: () => {}): void {
var editableFormControls = $('#selector: 'inline-edit');
editableFormControls.forEach(control => control.editing = true)
}
}
I want to grab all of the ediiitable form controls and set the editing flag in all of them to true, how can I do this?
You might need to implement a service that keeps the state and all child component subscribe to the state and parent push changes there.
import {Component, NgModule, VERSION, Input} from '#angular/core'
import {BrowserModule} from '#angular/platform-browser'
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
export class EditableService {
subject = new BehaviorSubject(true);
getAsObservable() {
return this.subject.asObservable();
}
}
#Component({
selector:'editable',
template: '<div>i am editable {{ x | async}}</div>'
})
export class Editable {
constructor(private editableService: EditableService) {
this.x = editableService.getAsObservable();
}
}
#Component({
selector: 'my-app',
template: `
<editable></editable>
<editable></editable>
<hr/>
<button (click)="change()">change</button>
`,
providers: [EditableService]
})
export class App {
change() {
this.editableService.subject.next(false);
}
constructor(private editableService: EditableService) {
this.name = `Angular! v${VERSION.full}`;
}
}

Categories