Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(form controls): mark as touched #864

Merged
merged 9 commits into from
Oct 11, 2018
15 changes: 11 additions & 4 deletions src/framework/theme/components/checkbox/checkbox.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*/

import { Component, Input, HostBinding, forwardRef } from '@angular/core';
import { Component, Input, HostBinding, forwardRef, ChangeDetectorRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { convertToBoolProperty } from '../helpers';

Expand Down Expand Up @@ -58,7 +58,8 @@ import { convertToBoolProperty } from '../helpers';
<input type="checkbox" class="customised-control-input"
[disabled]="disabled"
[checked]="value"
(change)="value = !value">
(change)="value = !value"
(blur)="setTouched()">
<span class="customised-control-indicator"></span>
<span class="customised-control-description">
<ng-content></ng-content>
Expand Down Expand Up @@ -123,9 +124,10 @@ export class NbCheckboxComponent implements ControlValueAccessor {
set value(val) {
this._value = val;
this.onChange(val);
this.onTouched();
}

constructor(private changeDetector: ChangeDetectorRef) {}

registerOnChange(fn: any) {
this.onChange = fn;
}
Expand All @@ -135,10 +137,15 @@ export class NbCheckboxComponent implements ControlValueAccessor {
}

writeValue(val: any) {
this.value = val;
this._value = val;
this.changeDetector.detectChanges();
}

setDisabledState(val: boolean) {
this.disabled = convertToBoolProperty(val);
}

setTouched() {
this.onTouched();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ export abstract class NbBasePicker<D, T, P> extends NbDatepicker<T> implements O
* */
protected queue: T;

protected blur$: Subject<void> = new Subject<void>();

constructor(@Inject(NB_DOCUMENT) protected document,
protected positionBuilder: NbPositionBuilderService,
protected overlay: NbOverlayService,
Expand All @@ -172,6 +174,17 @@ export abstract class NbBasePicker<D, T, P> extends NbDatepicker<T> implements O
return this.onChange$.asObservable();
}

get isShown(): boolean {
return this.ref && this.ref.hasAttached();
}

/**
* Emits when datepicker looses focus.
*/
get blur(): Observable<void> {
return this.blur$.asObservable();
}

protected abstract get pickerValueChange(): Observable<T>;

ngOnDestroy() {
Expand Down Expand Up @@ -250,7 +263,10 @@ export abstract class NbBasePicker<D, T, P> extends NbDatepicker<T> implements O
protected subscribeOnTriggers() {
const triggerStrategy = this.createTriggerStrategy();
triggerStrategy.show$.pipe(takeWhile(() => this.alive)).subscribe(() => this.show());
triggerStrategy.hide$.pipe(takeWhile(() => this.alive)).subscribe(() => this.hide());
triggerStrategy.hide$.pipe(takeWhile(() => this.alive)).subscribe(() => {
this.blur$.next();
this.hide();
});
}

protected instantiatePicker() {
Expand Down
24 changes: 20 additions & 4 deletions src/framework/theme/components/datepicker/datepicker.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import {
Validators,
} from '@angular/forms';
import { Type } from '@angular/core/src/type';
import { fromEvent, Observable } from 'rxjs';
import { map, takeWhile } from 'rxjs/operators';
import { fromEvent, Observable, merge } from 'rxjs';
import { map, takeWhile, filter, take } from 'rxjs/operators';

import { NB_DOCUMENT } from '../../theme.options';
import { NbDateService } from '../calendar-kit';
Expand Down Expand Up @@ -99,6 +99,10 @@ export abstract class NbDatepicker<T> {
abstract hide();

abstract shouldHide(): boolean;

abstract get isShown(): boolean;

abstract get blur(): Observable<void>;
}

export const NB_DATE_ADAPTER = new InjectionToken<NbDatepickerAdapter<any>>('Datepicker Adapter');
Expand Down Expand Up @@ -225,8 +229,8 @@ export class NbDatepickerDirective<D> implements OnDestroy, ControlValueAccessor
* */
protected picker: NbDatepicker<D>;
protected alive: boolean = true;
protected onChange: (D) => void = () => {
};
protected onChange: (D) => void = () => {};
protected onTouched: () => void = () => {};

/**
* Form control validators will be called in validators context, so, we need to bind them.
Expand Down Expand Up @@ -276,9 +280,11 @@ export class NbDatepickerDirective<D> implements OnDestroy, ControlValueAccessor
}

registerOnTouched(fn: any): void {
this.onTouched = fn;
}

setDisabledState(isDisabled: boolean): void {
this.input.disabled = isDisabled;
}

/**
Expand Down Expand Up @@ -367,6 +373,16 @@ export class NbDatepickerDirective<D> implements OnDestroy, ControlValueAccessor
this.hidePicker();
}
});

merge(
this.picker.blur,
fromEvent(this.input, 'blur').pipe(
filter(() => !this.picker.isShown && this.document.activeElement !== this.input),
),
).pipe(
takeWhile(() => this.alive),
take(1),
Tibing marked this conversation as resolved.
Show resolved Hide resolved
).subscribe(() => this.onTouched());
}

protected writePicker(value: D) {
Expand Down
17 changes: 16 additions & 1 deletion src/framework/theme/components/radio/radio-group.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
} from '@angular/core';
import { NbRadioComponent } from './radio.component';
import { merge } from 'rxjs';
import { takeWhile } from 'rxjs/operators';
import { filter, take, takeWhile, delay } from 'rxjs/operators';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { convertToBoolProperty } from '../helpers';

Expand Down Expand Up @@ -104,6 +104,7 @@ export class NbRadioGroupComponent implements AfterContentInit, OnDestroy, Contr
protected name: string;
protected alive: boolean = true;
protected onChange = (value: any) => {};
protected onTouched = () => {};

constructor(protected cd: ChangeDetectorRef) {}

Expand All @@ -112,6 +113,7 @@ export class NbRadioGroupComponent implements AfterContentInit, OnDestroy, Contr
this.updateValues();
this.updateDisabled();
this.subscribeOnRadiosValueChange();
this.subscribeOnRadiosBlur();
}

ngOnDestroy() {
Expand All @@ -123,6 +125,7 @@ export class NbRadioGroupComponent implements AfterContentInit, OnDestroy, Contr
}

registerOnTouched(fn: any): void {
this.onTouched = fn;
}

writeValue(value: any): void {
Expand Down Expand Up @@ -171,4 +174,16 @@ export class NbRadioGroupComponent implements AfterContentInit, OnDestroy, Contr
protected markRadiosForCheck() {
this.radios.forEach((radio: NbRadioComponent) => radio.markForCheck());
}

protected subscribeOnRadiosBlur() {
merge(...this.radios.map(radio => radio.blur))
.pipe(
takeWhile(() => this.alive),
// wait for focus to be applied to another radio in case it moved within radio group.
delay(100),
filter(() => !this.radios.some(radio => radio.hasFocus)),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delay(100) in this case may lead to unexpected behavior. So, maybe, we may remove it?

take(1),
Tibing marked this conversation as resolved.
Show resolved Hide resolved
)
.subscribe(() => this.onTouched());
}
}
21 changes: 20 additions & 1 deletion src/framework/theme/components/radio/radio.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ import { convertToBoolProperty } from '../helpers';
[checked]="checked"
[disabled]="disabled"
(change)="onChange($event)"
(click)="onClick($event)">
(click)="onClick($event)"
(focus)="setFocus()"
(blur)="removeFocus()">
<span class="radio-indicator"></span>
<span class="radio-description">
<ng-content></ng-content>
Expand All @@ -87,6 +89,12 @@ import { convertToBoolProperty } from '../helpers';
styleUrls: ['./radio.component.scss'],
})
export class NbRadioComponent {

private focus: boolean = false;
get hasFocus(): boolean {
return this.focus;
}

@Input() name: string;

@Input() checked: boolean;
Expand All @@ -100,6 +108,8 @@ export class NbRadioComponent {

@Output() valueChange: EventEmitter<any> = new EventEmitter();

@Output() blur: EventEmitter<void> = new EventEmitter();

disabled: boolean;

constructor(protected cd: ChangeDetectorRef) {}
Expand All @@ -118,4 +128,13 @@ export class NbRadioComponent {
onClick(event: Event) {
event.stopPropagation();
}

setFocus() {
this.focus = true;
}

removeFocus() {
this.focus = false;
this.blur.emit();
}
}
3 changes: 2 additions & 1 deletion src/framework/theme/components/select/select.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
[fullWidth]="fullWidth"
[outline]="outline"
[class.opened]="isOpened"
[ngClass]="overlayPosition">
[ngClass]="overlayPosition"
(blur)="trySetTouched()">

<ng-container *ngIf="selectionModel?.length">

Expand Down
34 changes: 31 additions & 3 deletions src/framework/theme/components/select/select.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
NbTriggerStrategyBuilder,
} from '../cdk';
import { NbOptionComponent } from './option.component';
import { NbButtonComponent } from '../button/button.component';
import { NB_DOCUMENT } from '../../theme.options';
import { convertToBoolProperty } from '../helpers';

Expand Down Expand Up @@ -112,7 +113,7 @@ export class NbSelectLabelComponent {
*
* @stacked-example(Select statuses, select/select-status.component)
*
* There are three select sizes:
* There are four select sizes:
*
* @stacked-example(Select sizes, select/select-sizes.component)
*
Expand Down Expand Up @@ -233,6 +234,8 @@ export class NbSelectComponent<T> implements OnInit, AfterViewInit, AfterContent
* */
@ViewChild(NbPortalDirective) portal: NbPortalDirective;

@ViewChild(NbButtonComponent, { read: ElementRef }) button: ElementRef<HTMLButtonElement>;

multiple: boolean = false;

/**
Expand Down Expand Up @@ -270,10 +273,11 @@ export class NbSelectComponent<T> implements OnInit, AfterViewInit, AfterContent
* Function passed through control value accessor to propagate changes.
* */
protected onChange: Function = () => {};
protected onTouched: Function = () => {};

constructor(@Inject(NB_DOCUMENT) protected document,
protected overlay: NbOverlayService,
protected hostRef: ElementRef,
protected hostRef: ElementRef<HTMLElement>,
protected positionBuilder: NbPositionBuilderService,
protected cd: ChangeDetectorRef) {
}
Expand Down Expand Up @@ -349,9 +353,12 @@ export class NbSelectComponent<T> implements OnInit, AfterViewInit, AfterContent
}

registerOnTouched(fn: any): void {
this.onTouched = fn;
}

setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
this.cd.detectChanges();
}

writeValue(value: T | T[]): void {
Expand Down Expand Up @@ -386,6 +393,7 @@ export class NbSelectComponent<T> implements OnInit, AfterViewInit, AfterContent
this.selectionModel.forEach((option: NbOptionComponent<T>) => option.deselect());
this.selectionModel = [];
this.hide();
this.button.nativeElement.focus();
this.emitSelected(null);
}

Expand Down Expand Up @@ -413,6 +421,7 @@ export class NbSelectComponent<T> implements OnInit, AfterViewInit, AfterContent
this.selectionModel = [option];
option.select();
this.hide();
this.button.nativeElement.focus();

this.emitSelected(option.value);
}
Expand Down Expand Up @@ -464,7 +473,12 @@ export class NbSelectComponent<T> implements OnInit, AfterViewInit, AfterContent

triggerStrategy.hide$
.pipe(takeWhile(() => this.alive))
.subscribe(() => this.hide());
.subscribe(($event: Event) => {
this.hide();
if (!this.isClickedWithinComponent($event)) {
this.onTouched();
}
});
}

protected subscribeOnPositionChange() {
Expand Down Expand Up @@ -541,4 +555,18 @@ export class NbSelectComponent<T> implements OnInit, AfterViewInit, AfterContent
this.selectionModel.push(corresponding);
}
}

/**
* Sets touched if focus moved outside of button and overlay,
* ignoring the case when focus moved to options overlay.
*/
trySetTouched() {
if (this.isHidden) {
this.onTouched();
}
}

protected isClickedWithinComponent($event: Event) {
return this.hostRef.nativeElement === $event.target || this.hostRef.nativeElement.contains($event.target as Node);
}
}