Angular 2 form validations start date <= end date - validation

I'm trying to add validations such that the end date can't be before the start date. Unfortunately I have no idea how to do that, and I didn't find any helpful advice in the internet so far. My form looks like this:
editAndUpdateForm(tageler: Tageler) {
this.tageler = tageler;
this.tagelerForm = this.fb.group({
title: [this.tageler.title, Validators.required],
text: this.tageler.text,
group: [[this.tageler.group], Validators.required],
date_start: new Date(this.tageler.start).toISOString().slice(0, 10),
date_end: new Date(this.tageler.end).toISOString().slice(0, 10),
...
});
this.tagelerForm.valueChanges
.subscribe(data => this.onValueChanged(data));
}
My validations so far:
onValueChanged(data?: any) {
if (!this.tagelerForm) {
return;
}
const form = this.tagelerForm;
for (const field in this.formErrors) {
// clear previous error message (if any)
this.formErrors[field] = '';
const control = form.get(field);
if (control && control.dirty && !control.valid) {
const messages = this.validationMessages[field];
for (const key in control.errors) {
this.formErrors[field] += messages[key] + ' ';
}
}
}
}
validationMessages = {
'title': {
'required': 'Geben Sie bitte einen Namen ein.',
},
'group': {
'required': 'Wählen Sie bitte eine Gruppe aus.'
},
'bringAlong': {
'required': 'Bitte Feld ausfüllen.'
},
'uniform': {
'required': 'Bitte Feld ausfüllen.'
},
};
formErrors = {
'title': 'Geben Sie bitte einen Namen ein.',
'group': 'Wählen Sie bitte eine Gruppe aus.',
'bringAlong': 'Bitte Feld ausfüllen',
'uniform': 'Bitte Feld ausfüllen',
};
The the form-controls 'date_start' & 'date_end' contain a date-string of the form 'dd.MM.yyyy', and I want 'date_end' to be bigger or equal 'date_start'.
I'd like to directly display the error message (my html code looks like this:)
<label for="formControlName_date_end" class="col-3 col-form-label">Ende:</label>
<div class="col-5">
<input id="formControlName_date_end" class="form-control" formControlName="date_end" type="date" value="{{tageler.end | date: 'yyyy-MM-dd'}}">
</div>
<div *ngIf="formErrors.date_end" class="alert alert-danger">
{{ formErrors.date_end }}
</div>
Could someone help me?
Thanks!

Based on the answer of santiagomaldonado I have created a generic ValidatorFn that can be used in multiple Reactive Forms with a dynamic return value.
export class DateValidators {
static dateLessThan(dateField1: string, dateField2: string, validatorField: { [key: string]: boolean }): ValidatorFn {
return (c: AbstractControl): { [key: string]: boolean } | null => {
const date1 = c.get(dateField1).value;
const date2 = c.get(dateField2).value;
if ((date1 !== null && date2 !== null) && date1 > date2) {
return validatorField;
}
return null;
};
}
}
Import the validator and use it like this in your formgroup validators.
this.form = this.fb.group({
loadDate: null,
deliveryDate: null,
}, { validator: Validators.compose([
DateValidators.dateLessThan('loadDate', 'deliveryDate', { 'loaddate': true }),
DateValidators.dateLessThan('cargoLoadDate', 'cargoDeliveryDate', { 'cargoloaddate': true })
])});
Now you can use the validation in HTML.
<md-error *ngIf="form.hasError('loaddate')">Load date must be before delivery date</md-error>

You can also do it with Reactive Forms.
The FormBuilder API lets you add custom validators.
Valid keys for the extra parameter map are validator and asyncValidator
Example:
import { Component } from '#angular/core';
import { FormGroup, FormBuilder, Validators } from '#angular/forms';
#Component({
selector: 'reactive-form',
templateUrl: './reactive-form.html'
})
export class ReactiveFormComponent {
form: FormGroup
constructor(private fb: FormBuilder){
this.createForm();
}
createForm() {
this.form = this.fb.group({
dateTo: ['', Validators.required ],
dateFrom: ['', Validators.required ]
}, {validator: this.dateLessThan('dateFrom', 'dateTo')});
}
dateLessThan(from: string, to: string) {
return (group: FormGroup): {[key: string]: any} => {
let f = group.controls[from];
let t = group.controls[to];
if (f.value > t.value) {
return {
dates: "Date from should be less than Date to"
};
}
return {};
}
}
}
Note that I'm comparing the values of the inputs date and from with >, but by default this would be comparing strings.
In the live example I'm using angular-date-value-accessor and importing the directive useValueAsDate.
<input formControlName="dateFrom" type="date" useValueAsDate />
With this directive group.controls[from].value and group.controls[to].value returns Date and then I can compare them with <.
Live example in plunkr
Credits to Dave's answer

create a form group . Let the controls be a part of form group .
new FormGroup({
startDate: this.fb.group({
dateInput: [{value: ""}, startDateValidator()]
}),
endDate: this.fb.group({
dateInput: ["", endDateValidator()]
})
}, startDateCannotBeLessThanEndDateValidator());
startDateCannotBeLessThanEndDateValidator(formGroup: FormGroup) {
let startDate = formGroup.get("startDate");
let endDate = formGroup.get("endDate");
// compare 2 dates
}

we cant do it in validation because we need two control values that is startdate and enddate for comparison. So it is better to compare two dates in your component
error:any={isError:false,errorMessage:''};
compareTwoDates(){
if(new Date(this.form.controls['date_end'].value)<new Date(this.form.controls['date_start'].value)){
this.error={isError:true,errorMessage:'End Date can't before start date'};
}
}
In your html
<label for="formControlName_date_end" class="col-3 col-form-label">Ende:</label>
<div class="col-5">
<input id="formControlName_date_end" class="form-control" formControlName="date_end" type="date" value="{{tageler.end | date: 'yyyy-MM-dd'}}" (blur)="compareTwoDates()">
</div>
<div *ngIf="error.isError" class="alert alert-danger">
{{ error.errorMessage }}
</div>

I am using moment, and in angular 7 to compare and validate dates, i use this function:
datesValidator(date1: any, date2: any): {[key:string]:any} | null {
return (group: FormGroup): { [key: string]: any } | null => {
let start = group.controls[date1].value;
let end = group.controls[date2].value;
let datum1 = _moment(start).startOf('day');
let datum2 = _moment(end).startOf('day');
if (_moment(datum1).isSameOrAfter(datum2)) {
this.alert.red("Error: wrong period!"); //popup message
return { 'error': 'Wrong period!' };
}
return null; //period is ok, return null
};
}

Mine is angular7 + ngprime(for calendar)
(*if you don't want ngprime just replace calendar part to others.)
Refer below code for date validation.
My code has additional validation that
once start date is selected, I block previous days in end data's calendar
so that the end date will be always later than that.
if you don't want to block date, delete [minDate] part.
it is also working.
> Component
export class test implements OnInit {
constructor() { }
defaultDate: Date = new Date();
checkDate = true;
form: FormGroup;
today: Date = new Date(); //StartDate for startDatetime
startDate: Date = new Date(); //StartDate for endDatetime
initFormControls(): void {
this.today.setDate(this.today.getDate());
this.startDate.setDate(this.today.getDate()); //or start date + 1 day
this.form = new FormGroup({
startDatetime: new FormControl('', [Validators.required]),
endDatetime: new FormControl('', [Validators.required])
},
{ validators: this.checkDateValidation });
}
checkDateValidation: ValidatorFn = (control: FormGroup): ValidationErrors | null => {
try {
let startingDatefield = control.get('startDatetime').value;
this.startDate = new Date(startingDatefield); //new Date(startingDatefield).getDate()
let endingDatefield = control.get('endDatetime').value;
if (this.today >= startingDatefield) { //compare to current date
console.log("Please set a Start Date that is on or after Current Date and Time.");
return { 'effectiveStartDatetime': false };
} else
if (startingDatefield > endingDatefield && endingDatefield) {
console.log("Please set an End Date and Time that is after the Start Date and Time.");
return {};
} else {
return {};
}
} catch (err) {
}
};
onSubmit() {
//if form is not valid
if (!this.form.valid) {
console.log(" Please fill in all the mandatory fields");
// do sth
return;
}
//if form is valid
//do sth
}
ngOnInit() {
this.initFormControls();
}
> HTML
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div>
<div>
<p-button type="submit" label="submit"></p-button>
</div>
</div>
<div>
<p>Start Date/Time"</p>
<div>
<p-calendar formControlName="startDatetime" appendTo="body" showTime="true" hourFormat="24" stepMinute="30"
showSeconds="false" dateFormat="yy-mm-dd" [minDate]="today"></p-calendar>
<div
*ngIf="form.get('startDatetime').invalid && (form.get('startDatetime').dirty || form.get('startDatetime').touched)">
<div *ngIf="form.get('startDatetime').hasError('required')">
</div>
</div>
</div>
<p>End Date/Time</p>
<div>
<p-calendar formControlName="endDatetime" appendTo="body" showTime="true" hourFormat="24" stepMinute="30"
showSeconds="false" dateFormat="yy-mm-dd" [minDate]="startDate"></p-calendar>
<div *ngIf="form.get('endDatetime').invalid && (form.get('endDatetime').dirty || form.get('endDatetime').touched)">
<div *ngIf="!checkDate || form.get('endDatetime').hasError('required')">
</div>
</div>
</div>
</div>
</form>

Related

Filter lightning:datatable with select-picklist and multiple apex classes

i try to filter datatable results by clicking a lightning:select value. If I have the value "Alle" it should show all values and filter the datatable when clicking any other value. Do you have an idea how to adjust the js controller and the helper so it works? I already tried it by using Case but it isnt supported in the developer console. I made a second apex class to contain all values and the other one which is for the values when a picklist value is selected
public class MediathekSearchController {
#AuraEnabled(cacheable=true)
public static List<List<sObject>> getSearchResult(String searchKey, String KategorieValue){
String findStr = '*'+searchKey+'*';
Boolean isEmptySearch = String.isEmpty(searchKey);
List<List<sObject>> searchResult = [FIND : findStr
IN ALL FIELDS RETURNING
Mediathek__c (Id, Name, Bezeichnung__c, Typ__c, Zielgruppe__c, Umfang__c, Bezeichnung_Link__c, Bezeichnung_Search__c)];
return searchResult;
}
public static List<List<sObject>> getFilteredSearchResult(String searchKey, String KategorieValue){
String findStr = '*'+searchKey+'*';
Boolean isEmptySearch = String.isEmpty(searchKey);
List<List<sObject>> searchResultFiltered = [FIND : findStr
IN ALL FIELDS RETURNING
Mediathek__c (Id, Name, Bezeichnung__c, Typ__c, Zielgruppe__c, Umfang__c, Bezeichnung_Link__c, Bezeichnung_Search__c WHERE Typ__c=:KategorieValue)];
return searchResultFiltered;
}
}
Component:
<aura:component implements="force:appHostable,flexipage:availableForAllPageTypes,flexipage:availableForRecordHome,force:hasRecordId" access="global" controller="MediathekSearchController">
<!-- handlers-->
<aura:handler name="init" value="{!this}" action="{!c.init}"/>
<!-- attributes -->
<aura:attribute name="showSearchResults" type="Boolean" default="false"/>
<aura:attribute name="searchKey" type="String"/>
<aura:attribute name="mediathekList" type="List" default="Mediathek[]"/>
<aura:attribute name="mediathekColumns" type="List"/>
<aura:attribute name="KategorieValue" type="String" default="Alle"/>
<div class= "slds-box">
<div class="slds-grid slds-wrap" >
<div class="slds-size_12-of-12">
<lightning:layout multipleRows="true">
<lightning:layoutItem size="8">
<lightning:input name="searchKey" placeholder="Suchbegriff einfügen" value="{!v.searchKey}" onkeydown="{!c.search}"/>
</lightning:layoutItem>
<lightning:layoutItem size="2">
<lightning:button variant="brand" label="Suchen" title="Search" onclick="{!c.search}" class="customButton" />
</lightning:layoutItem>
<lightning:layoutItem size="2">
<lightning:select aura:id="KategorieValue" name="Kategorie" label="Kategorie" required="false" value="{!v.KategorieValue}">
<option value="Alle">Alle</option>
<option value="Formular">Formular</option>
<option value="Merkblatt">Merkblatt</option>
<option value="Flyer">Flyer</option>
<option value="Infobroschüre">Infobroschüre</option>
</lightning:select>
</lightning:layoutItem>
</lightning:layout>
</div>
</div>
<div class="haha">
<aura:if isTrue="{!v.showSearchResults}">
<lightning:layout multipleRows="true">
<lightning:layoutItem padding="around-small" size="12" >
<lightning:datatable keyField="id"
data="{!v.mediathekList}"
columns="{!v.mediathekColumns}"
hideCheckboxColumn="true"/>
</lightning:layoutItem>
</lightning:layout>
</aura:if>
</div>
</div>
</aura:component>
JS Controller
({
init: function (component, event, helper){
component.set('v.mediathekColumns', [
{label: 'Bezeichnung', fieldName: 'Bezeichnung_Search__c', type: 'url' , fixedWidth: 395,
typeAttributes: {label: { fieldName: 'Bezeichnung__c' }, target: '_blank'}
},
{label: 'Typ', fieldName: 'Typ__c', type: 'text', fixedWidth: 116,
},
{label: 'Zielgruppe', fieldName: 'Zielgruppe__c', type: 'text', fixedWidth: 116,
},
{label: 'Umfang', fieldName: 'Umfang__c', type: 'text', fixedWidth: 112,
},
]);
var action = component.get("c.getSearchResult");
action.setCallback(this, function(response){
var state = response.getState();
if (state === "SUCCESS") {
var records =response.getReturnValue();
records.forEach(function(record){
record.BEZ = record.Bezeichnung__c;
});
component.set("v.mediathekList", records);
}
});
$A.enqueueAction(action);
},
search : function(component, event, helper) {
helper.getSearchResultsFromApex(component, event, helper);
component.set("v.showSearchResults",true);
} })
Helper
({
getSearchResultsFromApex : function(component, event, helper){
var action = component.get("c.getSearchResult");
action.setParams({ searchKey : searchStr, KategorieValue : kategoryVal});
// Create a callback that is executed after
// the server-side action returns
action.setCallback(this, function(response) {
var state = response.getState();
if (state === "SUCCESS") {
var result = response.getReturnValue();
result.forEach(function(result){
result.Name = '/'+result.Id;
});
// SOSL will always return the list in the order they were queried
component.set("v.mediathekList",result[0]);
}
else if (state === "INCOMPLETE") {
// do something
}
else if (state === "ERROR") {
var errors = response.getError();
if (errors) {
if (errors[0] && errors[0].message) {
console.log("Error message: " +
errors[0].message);
}
} else {
console.log("Unknown error");
}
}
});
$A.enqueueAction(action);
}
})
Not sure what the problem is to be honest?
#AuraEnabled(cacheable=true)
public static List<List<sObject>> getSearchResult(String searchKey, String KategorieValue){
String findStr = '*'+searchKey+'*';
List<List<sObject>> searchResult;
if(String.isBlank(kategorieValue)) || kategorieValue == 'Alle'){
searchResult = [FIND :findStr IN ALL FIELDS
RETURNING Mediathek__c (Id, Name, Bezeichnung__c, Typ__c, Zielgruppe__c, Umfang__c, Bezeichnung_Link__c, Bezeichnung_Search__c)];
} else {
searchResult = [FIND :findStr IN ALL FIELDS
RETURNING Mediathek__c (Id, Name, Bezeichnung__c, Typ__c, Zielgruppe__c, Umfang__c, Bezeichnung_Link__c, Bezeichnung_Search__c
WHERE Typ__c=:KategorieValue)];
}
return searchResult;
}
If you feel really fancy you could look into dynamic soql or maybe using real data categories for it (more than just a picklist) but plain old if should be enough?

mapDispatchToProps not updating store

I'm working on a personal project with redux. My mapStateToProps function seems to me properly written. but when I try to use it to send an object to my store nothing works.
Here's my function:
const mapDispatchToProps = dispatch => {
return {
addOrder: (item) => {
dispatch(addOrder(item));
}
}
}
<div className="recordOrder">
<button onclick={() => this.props.addOrder(this.state)}>Enregistrer et lancer la commande</button>
</div>
And my reducer:
const initialState = {
orderList : []
}
console.log(initialState);
export default function rootReducer ( state= initialState, action){
const orderList = [...state.orderList];
let position
switch (action.type){
case ADD_ORDER:
return {
orderList : [...state.orderList, action.payload]
};
case DELETE_ORDER:
position = orderList.indexOf(action.payload)
orderList.splice(position, 1)
return {
orderList
}
default:
return state;
}
console.log(state)
}
My entire component as requested:
import React, { Component } from 'react';
import { NavItem } from 'react-bootstrap';
import menu from './menu';
import { connect } from 'react-redux';
import { addOrder} from '../action'
class getOrder extends Component {
state = {
number: `CMD-${Date.now()}`,
order:[],
total: 0 ,
menu:menu,
isPaid: false
}
addItem = (index) => {
const order = [...this.state.order];
const menu = [...this.state.menu];
let total = this.state.total;
const pizza = menu[index];
console.log(pizza);
let ind = order.findIndex((item) =>
item.article == pizza.name
)
if (ind === -1){
order.push({article: pizza.name, price: pizza.price, volume:1})
total = total + order[order.length-1].price
} else if (ind != -1){
order[ind].volume++
total = total + order[ind].price
}
this.setState({
order:order,
total:total
})
console.log("youpiii");
console.log(this.state.total);
console.log(this.state.order);
}
render() {
const menuDisplay= menu.map( (item) => {
return (
<div>
<img onClick={() => this.addItem(item.number)} src={`${process.env.PUBLIC_URL}${item.picture}`} alt="picture" />
<div className="tagPrice">
<p>{item.name}</p>
<p>{item.price} €</p>
</div>
</div>
)
});
const currentOrder = [...this.state.order]
const orderDisplay = currentOrder.map((item) => {
let price = item.price*item.volume;
console.log(price);
return (
<div>
<h1>{item.volume} × {item.article}</h1>
<p>{price} €</p>
</div>
)
} );
return (
<div className="takeOrder">
<div className="orderban">
<h1>Pizza Reflex</h1>
</div>
<div>
<div className="menuDisplay">
{menuDisplay}
</div>
<div className="orderBoard">
<h1>Détail de la commande N°{this.state.number}</h1>
{orderDisplay}
<div className="total">
<h2>Soit un total de {this.state.total} € </h2>
</div>
<div className="recordOrder">
<button onclick={() => this.props.addOrder(this.state)}>Enregistrer et lancer la commande</button>
</div>
</div>
</div>
</div>
);
}
}
const mapDispatchToProps = dispatch => {
return {
addOrder: (item) => {
dispatch(addOrder(item));
}
}
}
export default connect ( mapDispatchToProps) (getOrder);
Can someone tell me what I've missed ?
Thanks for your help !
What you are missing is more of your code it can not be solved with what you have.
In more details what I need is the this.state , combinedReducer
The easiest fix you can do now is changing yow mapDispatchToProps works better if it is an obj
const mapStateToProps = (state) => {
return {
// here you specified the properties you want to pass yow component fom the state
}
};
const mapDispatchToProps = {action1, action2};
export default connect ( mapDispatchToProps) (getOrder);
connectreceives two params mapStateToProps and mapDispatchToProps,
mapDispatchToProps is optional, but mapStateToProps is mandatory, there for you need to specified, if your are not going to pass anything you need to pass a null value
export default connect (null, mapDispatchToProps) (getOrder);
also avoid exporting components without a name
example
function MyButton () {}
const MyButtonConnect = connect(state, dispatch)(MyButton);
export default MyButtonConnect

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>

Custom Angular2 Validator only evaluates when the page loads, and not when the object is updated

I'm trying to validate a form with tags, where the list must contain at least one tag to be valid. But it's only evaluating when the page loads, and not updating.
https://plnkr.co/edit/umnhybKhNEjswrUJGh3q?p=preview
Validation Function:
function notEmpty(control) {
if(control.value == null || control.value.length===0) {
return {
notEmpty: true
}
}
return null;
}
Component and Template using the Validator:
#Component({
selector: 'my-app',
template: `
<form [formGroup]="myForm">
<div>
(Comma Separated, no duplicates allowed. Enter also submits a tag.)
<div tags formControlName="list" [(ngModel)]="list"> </div>
<div *ngIf="myForm.get('list').valid">List 1 Not Empty.</div>
<div tags formControlName="list2" [(ngModel)]="list2"> </div>
<div *ngIf="myForm.get('list2').valid">List 2 Not Empty.</div>
</div>
</form>
`,
})
export class App {
list:Array<string>;
list2:Array<string>;
myForm:FormGroup;
myList:FormControl;
myList2:FormControl;
constructor(private fb: FormBuilder) {
this.list = [];
this.list2 = ["test"];
this.myList = fb.control('', notEmpty);
this.myList2 = fb.control('', notEmpty);
this.myForm = fb.group({
list: this.myList,
list2: this.myList2
});
}
addItem(item:string) {
this.list.push(item);
}
}
Tags Component and other child components:
const MY_PROVIDER = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(()=> Tags),
multi: true
};
#Component({
selector: 'tags, [tags]',
template: `
<div>
<tag-item *ngFor="let item of tagsList" item="{{item}}" (remove)="removeTag(item)"></tag-item>
<input class="tagInput" #tagInput
(focus)="focus()"
[(ngModel)]="current"
(keydown)="keyDown($event)"
(keyup)="keyUp($event)"
(blur)="blur()"
placeholder="+ Tag"/>
</div>
`,
providers: [MY_PROVIDER]
})
export class Tags implements ControlValueAccessor {
tagsList : Array<string>;
current : string;
#ViewChild('tagInput') child;
inFocus : boolean = false;
constructor() {
this.current = "";
this.tagsList = new Array<string>();
}
focus() {
this.child.nativeElement.focus();
this.inFocus = true;
}
keyDown(event:KeyboardEvent) {
if (event.keyCode === 188 || event.keyCode === 13) { //188 is Comma, 13 is Enter, 32 is Space.
this.pushTag();
} else if (event.keyCode === 8 && this.current.length == 0 && this.tagsList.length > 0){
this.current = this.tagsList.pop();
}
}
keyUp(event:KeyboardEvent) {
if(event.keyCode === 188) {
this.current = '';
}
}
pushTag() {
let str = this.current;
this.current = '';
if(str.trim() != '') {
for(let s of str.split(',')) {
s = this.sanitize(s);
if(s.trim() != '') {
if(!this.tagsList.some(x => x.toLowerCase() === s.toLowerCase()))
this.tagsList.push(s);
}
}
}
}
sanitize(str: string) : string {
let s = str;
s = s.replace('\'', '').replace('"', '').replace(';', '');
return s;
}
blur() {
this.pushTag();
this.inFocus = false;
}
removeTag(value) {
let index = this.tagsList.indexOf(value, 0);
if (index > -1) {
this.tagsList.splice(index, 1);
}
}
clear() {
this.tagsList = new Array<string>();
}
get value(): Array<string> { return this.tagsList; };
set value(v: Array<string>) {
if (v !== this.tagsList) {
this.tagsList = v;
this.onChange(v);
this.onTouched();
}
}
writeValue(value: Array<string>) {
this.tagsList = value;
this.onChange(value);
}
onChange = (_) => {};
onTouched = () => {};
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
}
#Component({
selector: 'tag-item, [tag-item]',
template: `{{item}} <delete-me (click)="removeTag(item)">x</delete-me>`
})
export class TagItem {
#Input() item : string;
#Output() remove : EventEmitter<string> = new EventEmitter();
removeTag(item) {
this.remove.emit(item);
}
}
#Component({
selector:'delete-me',
template:'x'
})
export class DeleteIcon {
}
This seems to work.
...//Tags Component pushMethod
pushTag() {
let str = this.current;
this.current = '';
if(str.trim() != '') {
for(let s of str.split(',')) {
s = this.sanitize(s);
if(s.trim() != '') {
if(!this.tagsList.some(x => x.toLowerCase() === s.toLowerCase())) {
this.tagsList.push(s);
this.pushed.emit(s); // created an EventEmitter<string>
}
}
}
}
}
And in my main component:
#Component({
selector: 'my-app',
template: `
<form [formGroup]="myForm">
<div>
(Comma Separated, no duplicates allowed. Enter also submits a tag.)
<div tags formControlName="list" (pushed)="update()" [(ngModel)]="list"> </div>
<div *ngIf="myForm.get('list').valid">List 1 Not Empty.</div>
<div tags formControlName="list2" (pushed)="update()" [(ngModel)]="list2"> </div>
<div *ngIf="myForm.get('list2').valid">List 2 Not Empty.</div>
</div>
</form>
`,
})
export class App {
list:Array<string>;
list2:Array<string>;
myForm:FormGroup;
myList:FormControl;
myList2:FormControl;
constructor(private fb: FormBuilder) {
this.list = [];
this.list2 = ["test"];
this.myList = fb.control('', notEmpty);
this.myList2 = fb.control('', notEmpty);
this.myForm = fb.group({
list: this.myList,
list2: this.myList2
});
}
update() {
this.myList.updateValueAndValidity();
this.myList2.updateValueAndValidity();
}
addItem(item:string) {
this.list.push(item);
}
}
Seems kind of hacky, though, so I'd still like a better answer, if there is one.

Rendering an object stored in state

I'm trying to render out my calendar's event summaries, using a .map function. I've stored my calendar events object in state, but can't find a way to .map out the different event summaries. Any suggestions?
export default class Container extends React.Component{
calendarID="xxx"
apiKey="zzz";
state = { events: [] };
setEvents = (a) => {
this.setState(a);
}
componentDidMount() {
ajax.get(`https://www.googleapis.com/calendar/v3/calendars/${this.calendarID}/events?fields=items(summary,id,location,start)&key=${this.apiKey}`)
.end((error, response) => {
if(!error && response ) {
this.setEvents({events: response.body});
console.log("success");
console.log(this.state.events);
} else {
console.log("Errors: ", error);
}
});
}
render(){
let lista = this.state.events;
let arr = Object.keys(lista).map(key => lista[key])
return (
<div class = "container">
{arr.map((event, index) => {
const summary = event.summary;
return (<div key={index}>{summary}</div>);
})}
</div>
);
}
}
EDIT:
Thanks for your answers! This is the data that the ajax call returns when I console log this.state.items:
Object {items: Array[1]}
items: Array[1]
0: Object
id: "cmkgsrcohfebl5isa79034h8a4"
start: Object
summary: "Stuff going down"
If I skip the ajax call and create my own state, the mapping works:
state = { items: [
{ items: { summary: "testing"} },
{ items: { summary: "12"} },
{ items: { summary: "3"} }
]};
To get this working, however, I change my render-function to:
render(){
let lista = this.state.items;
let arr = Object.keys(lista).map(key => lista[key])
return (
<div class = "container">
{arr.map((item, index) => {
const summary = item.items.summary;
return (<div key={index}>{summary}</div>);
})}
</div>
);
}
So maybe it has something to do with the object that this.state.items returns from the ajax call?
Edit2: #Andrea Korinski, you were right! I changed my render function to this, and now it works:
render(){
let list = this.state.items;
const arr = (list.items || []).map((item, index) => {
const summary = item.summary;
return (<div key={index}>{summary}</div>);
});
return (
<div class = "container">
{arr}
</div>
);
}
}
The whole component:
export default class Container extends React.Component{
calendarID="xxx";
apiKey="zzz";
state = {items: []};
setEvents = (a) => {
this.setState(a);
}
componentDidMount() {
ajax.get(`https://www.googleapis.com/calendar/v3/calendars/${this.calendarID}/events?fields=items(summary,id,location,start)&key=${this.apiKey}`)
.end((error, response) => {
if(!error && response ) {
this.setEvents({items: response.body});
console.log("success");
console.log(this.state.items);
} else {
console.log("Errors: ", error);
}
});
}
render(){
let list = this.state.items;
const irr = (list.items || []).map((item, index) => {
const summary = item.summary;
return (<div key={index}>{summary}</div>);
});
return (
<div class = "container">
{irr}
</div>
);
}
}

Resources