The selected value in a mat-select is not sent to parent - angular-material2

I created a dropdown in an angular library to be used in our applications. I used angular-material2 for the dropdown (mat-select and mat-autocomplete).
I must be doing something wrong since I don't get the value when I use the dropdown in an app. I already tried pretty much everything I found on the net, with no result.
I commented most of it and I'm trying to solve the simplest version, but even in this case I'm not getting the value. Here is what I have now:
DropdownComponent.html library:
<mat-form-field appearance="outline">
<mat-select disableOptionCentering (selectionChange)="writeValue($event)" [multiple]="multi">
<mat-option *ngFor="let item of list" [value]="item">
{{ item }}
</mat-option>
</mat-select>
</mat-form-field>
DropdownComponent.ts library:
import {Component, OnInit, ViewEncapsulation, Input, forwardRef} from '#angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR, NG_VALIDATORS, FormControl} from '#angular/forms';
import {Observable} from 'rxjs';
#Component({
selector: 'pux-dropdown',
templateUrl: './dropdown.component.html',
styleUrls: ['./dropdown.component.scss'],
encapsulation: ViewEncapsulation.None,
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DropdownComponent), multi: true },
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => DropdownComponent), multi: true }
]
})
export class DropdownComponent implements OnInit, ControlValueAccessor {
#Input() list: any[] = [];
#Input() selected: any;
#Input() multi = false;
#Input() search = false;
items: any[] = [];
propagateChange = (_: any) => {};
validateFn: any = () => {};
constructor() { }
ngOnInit() {
this.items = this.list;
}
// Form
get value(): any { return this.selected; }
set value(newValue: any) {
if (newValue !== this.selected) {
this.writeValue(newValue);
this.registerOnChange(newValue);
this.selected = newValue;
}
}
registerOnChange(fn: any): void { this.propagateChange = fn; }
registerOnTouched(fn: any): void {}
setDisabledState(isDisabled: boolean): void {}
writeValue(obj: any): void {
if (obj !== null) {
this.selected = obj.value;
this.registerOnChange(this.selected);
console.log(this.selected);
}
}
validate(c: FormControl) { return this.validateFn(c); }
}
DropDownComponent.html application:
<div>
<form [formGroup]="selectForm" (ngSubmit)="saveSelect(selectForm)" #form1="ngForm">
<div>
<pux-dropdown formControlName="selectValue" [list]="list1"> </pux-dropdown>
</div> <br>
<button mat-flat-button="primary" type="submit" class="btn btn-primary">Save</button>
</form> <br>
<div>
Saved Value: {{selectValue | json}}
</div>
</div>
DropdownComponent.ts application:
import {Component, OnInit} from '#angular/core';
import {FormGroup, FormBuilder} from '#angular/forms';
const states = [
'Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut', 'Delaware',
'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky',
'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi',
'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey', 'New Mexico',
'New York', 'North Carolina', 'North Dakota', 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania',
'Rhode Island', 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont',
'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming'
];
#Component({
selector: 'app-dropdown',
templateUrl: './dropdown.component.html',
styleUrls: ['./dropdown.component.scss']
})
export class DropdownComponent implements OnInit {
list1;
multi: boolean;
selected: any;
search: boolean;
// Form
selectForm: FormGroup;
selectValue: string;
constructor(private fb: FormBuilder) { }
ngOnInit() {
this.list1 = states;
// Form
this.selectForm = this.fb.group({
selectValue: this.selected
});
}
saveSelect(formValues) {
console.log(formValues.value.selectValue);
this.selectValue = formValues.value.selectValue;
}
}
The console.log in writeValue in the library gives me the value I select in the dropdown, but the console.log in saveSelect shows me null. So the value isn't sent to the parent. Any idea what I'm doing wrong? Thank you in advance.

Your writeValue implementation needs to call the change function, but instead it is calling the registerOnChange function which is there for the form control to register its change function. Try something like this:
propagateChange: (value: any) => void = () => {};
registerOnChange(fn: (value: any) => void) { this.propagateChange = fn; }
writeValue(obj: any): void {
if (obj !== null && obj !== this.selected) {
this.selected = obj.value;
this.propagateChange(this.selected);
}
}

Related

Trouble rendering react components that import google-maps-react on Heroku only

I have a react-in-rails application that utilizes the google-maps-react api. The app works fine locally but when deployed to heroku, any component that imports google-maps-react does not render. Since this is generally the landing page for most users, the app is not accessible at all.
When all the components that import or render google-maps-react are removed, the app deploys correctly.
import React from "react"
import MapContainer from "./MapContainer"
import StoreList from './StoreList'
class FindBar extends React.Component {
render () {
const {stores, openTab, success} = this.props
return (
<div className="findbar" >
<div className="mapcomponent">
<MapContainer
stores={stores}
openTab={openTab}
success={success}
/>
</div>
<br/>
<StoreList
stores={stores}
openTab={openTab}
/>
{this.props.success &&
<Redirect to="/user_home/opentabs" />
}
</div>
);
}
}
export default FindBar
import React, { Component } from 'react';
import { Button, Card } from 'reactstrap';
import { Map, GoogleApiWrapper, Marker, InfoWindow } from 'google-maps-react';
import UserHome from './UserHome.js'
import StoreMarkerWindow from './StoreMarkerWindow.js'
import InfoWindowEx from './InfoWindowEx.js'
const mapStyles = {
width: '100%',
height: '100vh',
};
class MapContainer extends Component {
constructor(props) {
super(props)
this.state = {
showingInfoWindow: false,
activeMarker: {},
selectedPlace: {},
address: [],
location: {},
displayMarkers: [],
success: false,
}
}
componentDidMount = () => {
this.fetchMarkers()
}
componentDidUpdate = (prevProps) => {
if (prevProps.stores === this.props.stores){
return true
}
this.fetchMarkers()
}
openTab = () => {
console.log(this.state.selectedPlace.storeId)
// this.props.openTab(this.state.selectedPlace.storeId)
}
onClick = (props, marker, e) => {
this.setState({
selectedPlace: props,
activeMarker: marker,
showingInfoWindow: true
})
}
onClose = props => {
if (this.state.showingInfoWindow) {
this.setState({
showingInfoWindow: false,
activeMarker: null
});
}
}
fetchMarkers = () => {
const newMarkers = []
this.props.stores.map((store, index) => {
const location = `${store.address1}, ${store.city}, ${store.state}, ${store.zip}`
this.geocodeAddress(location)
.then((geoco)=>{
newMarkers.push({lat: geoco.lat,
lng: geoco.lng,
storeId: store.id,
name: store.establishmentname,
location: location,
info: store.additionalinfo,
})
this.setState({ displayMarkers:newMarkers})
})
})
}
// create a function that maps stores.address, stores.city, stores.state, stores.zipcode
// and returns it to the geocodeAddress and then geocodeAddress returns it to
// the displayMarkers
geocodeAddress = (address) => {
const geocoder = new google.maps.Geocoder()
return new Promise((resolve, reject) => {
geocoder.geocode({'address': address}, function(results, status) {
if (status === google.maps.GeocoderStatus.OK) {
resolve(results[0].geometry.location.toJSON())
} else {
reject()
}
})
})
}
render() {
const{
activeMarker,
showingInfoWindow,
selectedPlace,
onMapOver,
}=this.props
return (
<div className="mapContainer" style={mapStyles}>
<Map
google={this.props.google}
onMouseover={this.onMapOver}
zoom={14}
style={mapStyles}
initialCenter={{
lat: 32.7091,
lng: -117.1580
}}
>
{this.state.displayMarkers.map((coordinates, index) => {
const{storeId, lat, lng, name, location, info} = coordinates
return (
<Marker onClick={this.onClick}
key={index}
id={storeId}
name={name}
position = {{lat, lng}}
location={location}
info= {info}
>
</Marker>
)
})}
<InfoWindowEx
marker={this.state.activeMarker}
visible={this.state.showingInfoWindow}
onClose={this.onClose}
>
<div>
<StoreMarkerWindow
name={this.state.selectedPlace.name}
location={this.state.selectedPlace.location}
info={this.state.selectedPlace.info}
id={this.state.selectedPlace.id}
openTab={this.props.openTab}
/>
</div>
</InfoWindowEx>
</Map>
</div>
);
}
}
export default GoogleApiWrapper({
apiKey: 'xxxx'
})(MapContainer);
TypeError: t is not a function
at Object.a (windowOrGlobal.js:18)
at Object.<anonymous> (windowOrGlobal.js:5)
at Object.<anonymous> (windowOrGlobal.js:5)
at n (bootstrap:19)
at Object.<anonymous> (ScriptCache.js:3)
at n (bootstrap:19)
at Object.<anonymous> (GoogleApiComponent.js:5)
at n (bootstrap:19)
at Object.<anonymous> (index.js:5)
at n (bootstrap:19)

Select All mat option and deselect All

I have scenario as below:
I want to achieve is:
When user click on All then all options shall be selected and when user click All again then all options shall be deselcted.
If All option is checked and user click any other checkbox than All then All and clicked checkbox shall be deselected.
When user selects 4 options one by one then All shall be selected.
HTML file
<mat-select placeholder="User Type" formControlName="UserType" multiple>
<mat-option *ngFor="let filters of userTypeFilters" [value]="filters.key">
{{filters.value}}
</mat-option>
<mat-option #allSelected (click)="toggleAllSelection()" [value]="0">All</mat-option>
</mat-select>
TS file
this.searchUserForm = this.fb.group({
userType: new FormControl('')
});
userTypeFilters = [
{
key: 1, value: 'Value 1',
},
{
key: 2, value: 'Value 2',
},
{
key: 3, value: 'Value 3',
},
{
key: 4, value: 'Value 4',
}
]
toggleAllSelection() {
if (this.allSelected.selected) {
this.searchUserForm.controls.userType
.patchValue([...this.userTypeFilters.map(item => item.key), 0]);
} else {
this.searchUserForm.controls.userType.patchValue([]);
}
}
Now, how to achieve 2nd and 3rd point
Stackblitz is: https://stackblitz.com/edit/angular-material-with-angular-v5-znfehg?file=app/app.component.html
Use code as below create function on click each mat-option and select()/deselect() all option:
See stackblitz:https://stackblitz.com/edit/angular-material-with-angular-v5-jsgvx6?file=app/app.component.html
TS:
togglePerOne(all){
if (this.allSelected.selected) {
this.allSelected.deselect();
return false;
}
if(this.searchUserForm.controls.userType.value.length==this.userTypeFilters.length)
this.allSelected.select();
}
toggleAllSelection() {
if (this.allSelected.selected) {
this.searchUserForm.controls.userType
.patchValue([...this.userTypeFilters.map(item => item.key), 0]);
} else {
this.searchUserForm.controls.userType.patchValue([]);
}
}
HTML:
<form [formGroup]="searchUserForm" fxFlex fxLayout="column" autocomplete="off" style="margin: 30px">
<mat-select placeholder="User Type" formControlName="userType" multiple>
<mat-option *ngFor="let filters of userTypeFilters" [value]="filters.key" (click)="togglePerOne(allSelected.viewValue)">
{{filters.value}}
</mat-option>
<mat-option #allSelected (click)="toggleAllSelection()" [value]="0">All</mat-option>
</mat-select>
</form>
Simply you can do it without adding a new option to your data source by adding a checkbox.
See the: Demo
import { Component, VERSION, ViewChild } from '#angular/core';
import { FormControl } from '#angular/forms';
import { MatSelect } from '#angular/material/select';
import { MatOption } from '#angular/material/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
#ViewChild('select') select: MatSelect;
allSelected=false;
foods: any[] = [
{value: 'steak-0', viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
{value: 'tacos-2', viewValue: 'Tacos'}
];
toggleAllSelection() {
if (this.allSelected) {
this.select.options.forEach((item: MatOption) => item.select());
} else {
this.select.options.forEach((item: MatOption) => item.deselect());
}
}
optionClick() {
let newStatus = true;
this.select.options.forEach((item: MatOption) => {
if (!item.selected) {
newStatus = false;
}
});
this.allSelected = newStatus;
}
}
.select-all{
margin: 5px 17px;
}
<mat-form-field>
<mat-label>Favorite food</mat-label>
<mat-select #select multiple>
<div class="select-all">
<mat-checkbox [(ngModel)]="allSelected"
[ngModelOptions]="{standalone: true}"
(change)="toggleAllSelection()">Select All</mat-checkbox>
</div>
<mat-option (click)="optionClick()" *ngFor="let food of foods" [value]="food.value">
{{food.viewValue}}
</mat-option>
</mat-select>
</mat-form-field>
Another way to do this is with the #ViewChild selector to get the mat-select component and troggle the mat-options items selected or unselected. We need also a variable to save the selected actual status to select or unselect all the elements on every click. Hope will help.
import {MatOption, MatSelect} from "#angular/material";
export class ExampleAllSelector {
myFormControl = new FormControl();
elements: any[] = [];
allSelected = false;
#ViewChild('mySel') skillSel: MatSelect;
constructor() {}
toggleAllSelection() {
this.allSelected = !this.allSelected; // to control select-unselect
if (this.allSelected) {
this.skillSel.options.forEach( (item : MatOption) => item.select());
} else {
this.skillSel.options.forEach( (item : MatOption) => {item.deselect()});
}
this.skillSel.close();
}
}
<mat-select #mySel placeholder="Example" [formControl]="myFormControl" multiple>
<mat-option [value]="0" (click)="toggleAllSelection()">All items</mat-option>
<mat-option *ngFor="let element of elements" [value]="element">{{skill.name}}</mat-option>
</mat-select>
Here is an example of how to extend a material option component.
See stackblitz Demo
Component:
import { ChangeDetectorRef, Component, ElementRef, HostListener, HostBinding, Inject, Input, OnDestroy, OnInit, Optional } from '#angular/core';
import { MAT_OPTION_PARENT_COMPONENT, MatOptgroup, MatOption, MatOptionParentComponent } from '#angular/material/core';
import { AbstractControl } from '#angular/forms';
import { MatPseudoCheckboxState } from '#angular/material/core/selection/pseudo-checkbox/pseudo-checkbox';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
#Component({
selector: 'app-select-all-option',
templateUrl: './select-all-option.component.html',
styleUrls: ['./select-all-option.component.css']
})
export class SelectAllOptionComponent extends MatOption implements OnInit, OnDestroy {
protected unsubscribe: Subject<any>;
#Input() control: AbstractControl;
#Input() title: string;
#Input() values: any[] = [];
#HostBinding('class') cssClass = 'mat-option';
#HostListener('click') toggleSelection(): void {
this. _selectViaInteraction();
this.control.setValue(this.selected ? this.values : []);
}
constructor(elementRef: ElementRef<HTMLElement>,
changeDetectorRef: ChangeDetectorRef,
#Optional() #Inject(MAT_OPTION_PARENT_COMPONENT) parent: MatOptionParentComponent,
#Optional() group: MatOptgroup) {
super(elementRef, changeDetectorRef, parent, group);
this.title = 'Select All';
}
ngOnInit(): void {
this.unsubscribe = new Subject<any>();
this.refresh();
this.control.valueChanges
.pipe(takeUntil(this.unsubscribe))
.subscribe(() => {
this.refresh();
});
}
ngOnDestroy(): void {
super.ngOnDestroy();
this.unsubscribe.next();
this.unsubscribe.complete();
}
get selectedItemsCount(): number {
return this.control && Array.isArray(this.control.value) ? this.control.value.filter(el => el !== null).length : 0;
}
get selectedAll(): boolean {
return this.selectedItemsCount === this.values.length;
}
get selectedPartially(): boolean {
const selectedItemsCount = this.selectedItemsCount;
return selectedItemsCount > 0 && selectedItemsCount < this.values.length;
}
get checkboxState(): MatPseudoCheckboxState {
let state: MatPseudoCheckboxState = 'unchecked';
if (this.selectedAll) {
state = 'checked';
} else if (this.selectedPartially) {
state = 'indeterminate';
}
return state;
}
refresh(): void {
if (this.selectedItemsCount > 0) {
this.select();
} else {
this.deselect();
}
}
}
HTML:
<mat-pseudo-checkbox class="mat-option-pseudo-checkbox"
[state]="checkboxState"
[disabled]="disabled"
[ngClass]="selected ? 'bg-accent': ''">
</mat-pseudo-checkbox>
<span class="mat-option-text">
{{title}}
</span>
<div class="mat-option-ripple" mat-ripple
[matRippleTrigger]="_getHostElement()"
[matRippleDisabled]="disabled || disableRipple">
</div>
CSS:
.bg-accent {
background-color: #2196f3 !important;
}
Another possible solution:
using <mat-select [(value)]="selectedValues" in the template and set the selectedValues via toggle function in the component.
Working Stackblitz Demo.
Component
export class AppComponent {
selectedValues: any;
allSelected = false;
public displayDashboardValues = [
{key:'0', valuePositionType: 'undefined', viewValue:'Select all'},
{key:'1', valuePositionType: 'profit-loss-area', viewValue:'result'},
{key:'2', valuePositionType: 'cash-area', viewValue:'cash'},
{key:'3', valuePositionType: 'balance-area', viewValue:'balance'},
{key:'4', valuePositionType: 'staff-area' ,viewValue:'staff'},
{key:'5', valuePositionType: 'divisions-area', viewValue:'divisions'},
{key:'6', valuePositionType: 'commisions-area', viewValue:'commisions'},
];
toggleAllSelection() {
this.allSelected = !this.allSelected;
this.selectedValues = this.allSelected ? this.displayDashboardValues : [];
}
}
Template
<mat-select [(value)]="selectedValues" (selectionChange)="selectionChange($event)" formControlName="dashboardValue" multiple>
<mat-option [value]="displayDashboardValues[0]" (click)="toggleAllSelection()">{{ displayDashboardValues[0].viewValue }}</mat-option>
<mat-divider></mat-divider>
<div *ngFor="let dashboardPosition of displayDashboardValues">
<mat-option class="dashboard-select-option" *ngIf="dashboardPosition.key>0" [value]="dashboardPosition">
{{ dashboardPosition.viewValue }}
</mat-option>
</div>
</mat-select>
There are some problems with other answers. The most important one is that they're listening to the click event which is not complete (user can select an option via space key on the keyboard).
I've created a component that solves all the problems:
#Component({
selector: 'app-multi-select',
templateUrl: './multi-select.component.html',
styleUrls: ['./multi-select.component.scss'],
})
export class MultiSelectComponent<V> implements OnInit {
readonly _ALL_SELECTED = '__ALL_SELECTED__' as const;
#Input() options: ReadonlyArray<{ value: V; name: string }> = [];
#Input('selectControl') _selectControl!: FormControl | AbstractControl | null | undefined;
get selectControl(): FormControl {
return this._selectControl as FormControl;
}
#Input() label: string = '';
#Input() hasSelectAllOption = false;
selectedValues: (V | '__ALL_SELECTED__')[] = [];
constructor() {}
ngOnInit(): void {}
onSelectAllOptions({ isUserInput, source: { selected } }: MatOptionSelectionChange) {
if (!isUserInput) return;
this.setValues(selected ? this.options.map(o => o.value) : []);
}
private setValues(values: (V | '__ALL_SELECTED__')[]) {
const hasAllOptions = ArrayUtils.arraysAreSame(
values,
this.options.map(o => o.value),
);
if (!values.includes(this._ALL_SELECTED)) {
if (hasAllOptions) {
values = [...values, this._ALL_SELECTED];
}
} else if (!hasAllOptions) {
values = values.filter(o => o !== this._ALL_SELECTED);
}
setTimeout(() => {
this.selectedValues = values;
});
this.selectControl.setValue(values.filter(o => (o as any) !== this._ALL_SELECTED));
}
onSelectOtherOptions({ isUserInput, source: { selected, value } }: MatOptionSelectionChange) {
if (!isUserInput) return;
this.setValues(
selected ? [...this.selectedValues, value] : this.selectedValues.filter(o => o !== value),
);
}
}
<mat-form-field>
<mat-label>Choose some options</mat-label>
<mat-select multiple [value]="selectedValues">
<mat-option
*ngFor="let d of options"
[value]="d.value"
(onSelectionChange)="onSelectOtherOptions($event)"
>
{{ d.name }}
</mat-option>
<mat-option
*ngIf="hasSelectAllOption"
[value]="_ALL_SELECTED"
(onSelectionChange)="onSelectAllOptions($event)"
>
Select all
</mat-option>
</mat-select>
</mat-form-field>

Use ngx-translate with ng2-select in Angular 5 app

I want to use ngx-translate for items in ng2-select. The only way I can think of is using the translate service and mutate the items' text in ts file prior to binding.
Is there a way to use pipe or directive as I want to make it consistent.
Thanks in advance.
My solution was to create a pipe and use it on the items of the select:
<ng-select [items]="listOfTimeOfExecution | selectOptionsTranslate" ...
and pipe code:
import { Pipe, PipeTransform } from '#angular/core';
import { TranslateService } from 'ng2-translate/ng2-translate';
import { SelectOption } from 'app/shared/entities';
#Pipe({name: 'selectOptionsTranslate'})
export class SelectOptionsTranslatePipe implements PipeTransform {
constructor(public translateService: TranslateService){}
transform(items: Array<SelectOption>) : Array<SelectOption> {
for(let item of items) {
item.text = this.translateService.instant(item.text);
}
return items;
}
}
My solution is inspired by Iosif's for which I can't make it work properly becuz of the async characteristics of the translateService 😣
Secondly, I also need to translate the options when user changes language.
So here's my solution (I wrote a pipe as well) for handling the above 2 issues:
Dependencies:
Angular 6
"#ng-select/ng-select": "2.5.1",
"#ngx-translate/core": "^10.0.2",
translate-options.pipe.ts (Rmbr to import it to the declaration array of app.module.ts)
// ... Rmbr to import the libs ...
#Pipe({
name: 'translateOptions',
})
export class TranslateOptionsPipe implements PipeTransform, OnDestroy {
constructor(private translateService: TranslateService) { }
transform(items: any) {
const observable = Observable.create(observer => {
this.translateService.get(items).subscribe(result => {
// result will be an object
// e.g. { 'JOBS.UX': 'UX Designer', 'JOBS.DEVELOPER': 'Developer' }
observer.next(result);
});
this.translateService.onLangChange.subscribe(event => {
this.translateService.get(items).subscribe(result => {
observer.next(result);
});
})
});
return observable;
}
ngOnDestroy() {
this.translateService.onLangChange.unsubscribe();
}
}
app.component.html
items in the code below will be an array of your translation keys, something like:
['JOBS.DEVELOPER', 'JOBS.UX', 'JOBS.PM']
<ng-select
[addTag]="true"
[addTagText]="to.addTagText || 'Create item: '"
[multiple]="to.multiple"
[closeOnSelect]="!to.multiple"
(change)="onAutoCompleteChange($event)"
>
<ng-option
*ngFor="let item of items | translateOptions | async | keyvalue"
[value]="item.key"
>
{{ item.value }}
</ng-option>
</ng-select>
Hope that helps 💪🏻
The parent that use dropdown will pass an object typed as following to the dropdown component.
export interface IDropdownOptions {
items: any[];
itemType: 'action' | 'divider';
itemLabel: (item: any) => string;
itemClicked?: (item: any) => void; // overwriting default onChange function
itemVisible?: (item: any) => boolean;
itemSelectable?: (item: any) => boolean;
selectedText: (() => string) | string;
shortSelectedText?: (() => string) | string;
// can define more for styling and custom purposes...
}
Then I have my dropdown component to implements ControlValueAccessor to be available in angular form
import { Component, forwardRef, Input, } from '#angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '#angular/forms';
import { get } from 'lodash';
#Component({
selector: 'c-dropdown',
templateUrl: './dropdown.component.html',
providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DropdownComponent), multi: true }]
})
export class DropdownComponent implements ControlValueAccessor {
#Input() options: IDropdownOptions;
onChange: any = () => {};
get itemLabel(): (item: any) => string {
return !!get(this.options, 'itemLabel')
? this.options.itemLabel
: () => '';
}
get itemClicked(): (item: any) => void {
!!get(this.options, 'itemClicked')
? this.options.itemClicked
: this.onChange;
}
// Getter functions for itemSelectable, itemVisible, etc.
constructor() {}
// Other inherited functions...
registerOnChange(fn: any): void {
this.onChange = fn;
}
}
Inside template's for loop, you can use translate pipe with itemLabel(item).

Passing an additional value to custom validator in Angular2?

I have a validator that checks if a users email address is unique, to do this I need to also pass in the users id so that it doesn't include itself in the unique checks. What is the best way to achieve this?
From what I can tell the validator only has access to the control value. I'm hooking up my validator like this:
<input #emailAddress="ngForm" type="text" [(ngModel)]="user.emailAddress" ngControl="emailAddress" required userExists />
Currently the only way I've been able to achieve it is by setting a static value on the validator, which is not ideal! Here's my full code for the validator:
import { NG_ASYNC_VALIDATORS, Control } from '#angular/common';
import { Directive, provide, forwardRef, Attribute } from '#angular/core';
import { UserService } from './user.service';
import { User } from './user.model';
interface ValidationResult {
[key: string]: boolean;
}
#Directive({
selector: '[userExists][ngModel]',
providers: [
provide(NG_ASYNC_VALIDATORS, {
useExisting: forwardRef(() => UserExistsValidator),
multi: true
})
]
})
export class UserExistsValidator {
public static user: User;
constructor(private _userService: UserService) { }
validate(control: Control): Promise<ValidationResult> {
return new Promise((resolve, reject) => {
this._userService.exists(control.value, UserExistsValidator.user.id).subscribe(
(response: any) => {
if (response.exists)
return resolve({ userExists: { valid: false } });
else
return resolve(null);
},
(error: any) => { console.log(error); }
)
});
}
}
I would use a shared service
#Injectable()
class ValidatorParam {
value:string; // could also be an observable
}
#Directive({
selector: '[userExists][ngModel]',
providers: [
{ provide: NG_ASYNC_VALIDATORS,
useExisting: forwardRef(() => UserExistsValidator),
multi: true
})
]
})
export class UserExistsValidator {
public static user: User;
constructor(private _userService: UserService, private _param:ValidatorParam) { }
validate(control: Control): Promise<ValidationResult> {
return new Promise((resolve, reject) => {
this._param.... // don't know what you want to do with it
this._userService.exists(control.value, UserExistsValidator.user.id).subscribe(
(response: any) => {
if (response.exists)
return resolve({ userExists: { valid: false } });
else
return resolve(null);
},
(error: any) => { console.log(error); }
)
});
}
}
#Component({
selector: '...',
providers: [ValidatorParam],
template: `
<input #emailAddress="ngForm" type="text" [(ngModel)]="user.emailAddress" ngControl="emailAddress" required userExists />
`})
export class MyComponent {
constructor(private _validatorParam:ValidatorParam) {
this._validatorParam.value = xxx;
}
}
This way you can only have one service per component. If you have several input elements in this component, then they need to share the service.
Caution: not tried myself.

Validation Error Message not getting displayed for custom validation in Angular 2

I have a register form where user need to provide username. When customer enters username, I want to show validation error message if that username already exists in db or not.
register.html
<-- code here-->
<div class="form-group">
<label for="username" class="col-sm-3 control-label">UserName</label>
<div class=" col-sm-6">
<input type="text" ngControl="userName" maxlength="45" class="form-control" [(ngModel)]="parent.userName" placeholder="UserName" #userName="ngForm" required data-is-unique/>
<validation-message control="userName"></validation-message>
</div>
</div>
<--code here-->
register.component.ts
import {Component} from 'angular2/core';
import {NgForm, FormBuilder, Validators, FORM_DIRECTIVES} from 'angular2/common';
import {ValidationService} from '../services/validation.service';
import {ValidationMessages} from './validation-messages.component';
#Component({
selector: 'register',
templateUrl: './views/register.html',
directives: [ROUTER_DIRECTIVES, ValidationMessages, FORM_DIRECTIVES],
providers: []
})
export class ParentSignUpComponent {
parentSignUpForm: any;
constructor(private _formBuilder: FormBuilder) {
this._stateService.isAuthenticatedEvent.subscribe(value => {
this.onAuthenticationEvent(value);
});
this.parent = new ParentSignUpModel();
this.parentSignUpForm = this._formBuilder.group({
'firstName': ['', Validators.compose([Validators.required, Validators.maxLength(45), ValidationService.nameValidator])],
'middleName': ['', Validators.compose([Validators.maxLength(45), ValidationService.nameValidator])],
'lastName': ['', Validators.compose([Validators.required, Validators.maxLength(45), ValidationService.nameValidator])],
'userName': ['', Validators.compose([Validators.required, ValidationService.checkUserName])]
});
}
}
validation-message.component
import {Component, Host} from 'angular2/core';
import {NgFormModel} from 'angular2/common';
import {ValidationService} from '../services/validation.service';
#Component({
selector: 'validation-message',
inputs: ['validationName: control'],
template: `<div *ngIf="errorMessage !== null" class="error-message"> {{errorMessage}}</div>`
})
export class ValidationMessages {
private validationName: string;
constructor (#Host() private _formDir: NgFormModel) {}
get errorMessage() {
let control = this._formDir.form.find(this.validationName);
for (let propertyName in control.errors) {
if (control.errors.hasOwnProperty(propertyName) && control.touched) {
return ValidationService.getValidatorErrorMessage(propertyName);
}
}
return null;
}
}
validation-service.ts
import {Injectable, Injector} from 'angular2/core';
import {Control} from 'angular2/common';
import {Observable} from 'rxjs/Observable';
import {Http, Response, HTTP_PROVIDERS} from 'angular2/http';
import 'rxjs/Rx';
interface ValidationResult {
[key:string]:boolean;
}
#Injectable()
export class ValidationService {
static getValidatorErrorMessage(code: string) {
let config = {
'required': 'This field is required!',
'maxLength': 'Field is too long!',
'invalidName': 'This field can contain only alphabets, space, dot, hyphen, and apostrophe.',
'userAlreadyInUse': 'UserName selected already in use! Please try another.'
};
return config[code];
}
static checkUserName(control: Control): Promise<ValidationResult> {
let injector = Injector.resolveAndCreate([HTTP_PROVIDERS]);
let http = injector.get(Http);
let alreadyExists: boolean;
if (control.value) {
return new Promise((resolve, reject) => {
setTimeout(() => {
http.get('/isUserNameUnique/' + control.value).map(response => response.json()).subscribe(result => {
if (result === false) {
resolve({'userAlreadyInUse': true});
} else {
resolve(null);
}
});
}, 1000);
});
}
}
}
Now, when i run, and give a username that already exists in db, the value of 'result' variable i am getting as false, which is expected and correct. But validation error message is not getting displayed. I am able to run and get validation error message for other custom validation functions. I am using Angular 2.0.0-beta.15. Can somebody help me to understand what could be the issue?
There are some known issues with async validation
https://github.com/angular/angular/issues/1068
https://github.com/angular/angular/issues/7538
https://github.com/angular/angular/issues/8118
https://github.com/angular/angular/issues/8923
https://github.com/angular/angular/issues/8022
This code can be simplified
return new Promise((resolve, reject) => {
setTimeout(() => {
http.get('/isUserNameUnique/' + control.value).map(response => response.json())
.subscribe(result => {
if (result === false) {
resolve({'userAlreadyInUse': true});
} else {
resolve(null);
}
});
}, 1000);
});
to
return http.get('/isUserNameUnique/' + control.value).map(response => response.json())
.timeout(200, new Error('Timeout has occurred.'));
.map(result => {
if (result === false) {
resolve({'userAlreadyInUse': true});
} else {
resolve(null);
}
}).toPromise();
Don't forget to import map, timeout, and toPromise.
If you use subscribe() instead of then() on the caller site, then you can event omit toPromise()
if you look into this -
'userName': ['', Validators.compose([Validators.required, ValidationService.checkUserName])] });
-- you can see I am using both synchronous and asynchronous validations together. When i changed method for checkUserName like 'Validators.composeAsync(ValidationService.checkUserName)' instead of Validators.compose method, error message got displayed.

Resources