feat(forms) range values need to be numbers instead of strings (#11792)
This commit is contained in:
		
							parent
							
								
									f77ab6a2d2
								
							
						
					
					
						commit
						0e9503b500
					
				| @ -16,6 +16,7 @@ import {NgModel} from './directives/ng_model'; | |||||||
| import {NgModelGroup} from './directives/ng_model_group'; | import {NgModelGroup} from './directives/ng_model_group'; | ||||||
| import {NumberValueAccessor} from './directives/number_value_accessor'; | import {NumberValueAccessor} from './directives/number_value_accessor'; | ||||||
| import {RadioControlValueAccessor} from './directives/radio_control_value_accessor'; | import {RadioControlValueAccessor} from './directives/radio_control_value_accessor'; | ||||||
|  | import {RangeValueAccessor} from './directives/range_value_accessor'; | ||||||
| import {FormControlDirective} from './directives/reactive_directives/form_control_directive'; | import {FormControlDirective} from './directives/reactive_directives/form_control_directive'; | ||||||
| import {FormControlName} from './directives/reactive_directives/form_control_name'; | import {FormControlName} from './directives/reactive_directives/form_control_name'; | ||||||
| import {FormGroupDirective} from './directives/reactive_directives/form_group_directive'; | import {FormGroupDirective} from './directives/reactive_directives/form_group_directive'; | ||||||
| @ -34,6 +35,7 @@ export {NgModel} from './directives/ng_model'; | |||||||
| export {NgModelGroup} from './directives/ng_model_group'; | export {NgModelGroup} from './directives/ng_model_group'; | ||||||
| export {NumberValueAccessor} from './directives/number_value_accessor'; | export {NumberValueAccessor} from './directives/number_value_accessor'; | ||||||
| export {RadioControlValueAccessor} from './directives/radio_control_value_accessor'; | export {RadioControlValueAccessor} from './directives/radio_control_value_accessor'; | ||||||
|  | export {RangeValueAccessor} from './directives/range_value_accessor'; | ||||||
| export {FormControlDirective} from './directives/reactive_directives/form_control_directive'; | export {FormControlDirective} from './directives/reactive_directives/form_control_directive'; | ||||||
| export {FormControlName} from './directives/reactive_directives/form_control_name'; | export {FormControlName} from './directives/reactive_directives/form_control_name'; | ||||||
| export {FormGroupDirective} from './directives/reactive_directives/form_group_directive'; | export {FormGroupDirective} from './directives/reactive_directives/form_group_directive'; | ||||||
| @ -44,9 +46,9 @@ export {MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValida | |||||||
| 
 | 
 | ||||||
| export const SHARED_FORM_DIRECTIVES: Type<any>[] = [ | export const SHARED_FORM_DIRECTIVES: Type<any>[] = [ | ||||||
|   NgSelectOption, NgSelectMultipleOption, DefaultValueAccessor, NumberValueAccessor, |   NgSelectOption, NgSelectMultipleOption, DefaultValueAccessor, NumberValueAccessor, | ||||||
|   CheckboxControlValueAccessor, SelectControlValueAccessor, SelectMultipleControlValueAccessor, |   RangeValueAccessor, CheckboxControlValueAccessor, SelectControlValueAccessor, | ||||||
|   RadioControlValueAccessor, NgControlStatus, NgControlStatusGroup, RequiredValidator, |   SelectMultipleControlValueAccessor, RadioControlValueAccessor, NgControlStatus, | ||||||
|   MinLengthValidator, MaxLengthValidator, PatternValidator |   NgControlStatusGroup, RequiredValidator, MinLengthValidator, MaxLengthValidator, PatternValidator | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| export const TEMPLATE_DRIVEN_DIRECTIVES: Type<any>[] = [NgModel, NgModelGroup, NgForm]; | export const TEMPLATE_DRIVEN_DIRECTIVES: Type<any>[] = [NgModel, NgModelGroup, NgForm]; | ||||||
|  | |||||||
| @ -0,0 +1,57 @@ | |||||||
|  | /** | ||||||
|  |  * @license | ||||||
|  |  * Copyright Google Inc. All Rights Reserved. | ||||||
|  |  * | ||||||
|  |  * Use of this source code is governed by an MIT-style license that can be | ||||||
|  |  * found in the LICENSE file at https://angular.io/license
 | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import {Directive, ElementRef, Provider, Renderer, forwardRef} from '@angular/core'; | ||||||
|  | 
 | ||||||
|  | import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor'; | ||||||
|  | 
 | ||||||
|  | export const RANGE_VALUE_ACCESSOR: Provider = { | ||||||
|  |   provide: NG_VALUE_ACCESSOR, | ||||||
|  |   useExisting: forwardRef(() => RangeValueAccessor), | ||||||
|  |   multi: true | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * The accessor for writing a range value and listening to changes that is used by the | ||||||
|  |  * {@link NgModel}, {@link FormControlDirective}, and {@link FormControlName} directives. | ||||||
|  |  * | ||||||
|  |  *  ### Example | ||||||
|  |  *  ``` | ||||||
|  |  *  <input type="range" [(ngModel)]="age" > | ||||||
|  |  *  ``` | ||||||
|  |  */ | ||||||
|  | @Directive({ | ||||||
|  |   selector: | ||||||
|  |       'input[type=range][formControlName],input[type=range][formControl],input[type=range][ngModel]', | ||||||
|  |   host: { | ||||||
|  |     '(change)': 'onChange($event.target.value)', | ||||||
|  |     '(input)': 'onChange($event.target.value)', | ||||||
|  |     '(blur)': 'onTouched()' | ||||||
|  |   }, | ||||||
|  |   providers: [RANGE_VALUE_ACCESSOR] | ||||||
|  | }) | ||||||
|  | export class RangeValueAccessor implements ControlValueAccessor { | ||||||
|  |   onChange = (_: any) => {}; | ||||||
|  |   onTouched = () => {}; | ||||||
|  | 
 | ||||||
|  |   constructor(private _renderer: Renderer, private _elementRef: ElementRef) {} | ||||||
|  | 
 | ||||||
|  |   writeValue(value: any): void { | ||||||
|  |     this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', parseFloat(value)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   registerOnChange(fn: (_: number) => void): void { | ||||||
|  |     this.onChange = (value) => { fn(value == '' ? null : parseFloat(value)); }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   registerOnTouched(fn: () => void): void { this.onTouched = fn; } | ||||||
|  | 
 | ||||||
|  |   setDisabledState(isDisabled: boolean): void { | ||||||
|  |     this._renderer.setElementProperty(this._elementRef.nativeElement, 'disabled', isDisabled); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -22,6 +22,7 @@ import {NgControl} from './ng_control'; | |||||||
| import {normalizeAsyncValidator, normalizeValidator} from './normalize_validator'; | import {normalizeAsyncValidator, normalizeValidator} from './normalize_validator'; | ||||||
| import {NumberValueAccessor} from './number_value_accessor'; | import {NumberValueAccessor} from './number_value_accessor'; | ||||||
| import {RadioControlValueAccessor} from './radio_control_value_accessor'; | import {RadioControlValueAccessor} from './radio_control_value_accessor'; | ||||||
|  | import {RangeValueAccessor} from './range_value_accessor'; | ||||||
| import {FormArrayName} from './reactive_directives/form_group_name'; | import {FormArrayName} from './reactive_directives/form_group_name'; | ||||||
| import {SelectControlValueAccessor} from './select_control_value_accessor'; | import {SelectControlValueAccessor} from './select_control_value_accessor'; | ||||||
| import {SelectMultipleControlValueAccessor} from './select_multiple_control_value_accessor'; | import {SelectMultipleControlValueAccessor} from './select_multiple_control_value_accessor'; | ||||||
| @ -130,6 +131,7 @@ export function isPropertyUpdated(changes: {[key: string]: any}, viewModel: any) | |||||||
| export function isBuiltInAccessor(valueAccessor: ControlValueAccessor): boolean { | export function isBuiltInAccessor(valueAccessor: ControlValueAccessor): boolean { | ||||||
|   return ( |   return ( | ||||||
|       hasConstructor(valueAccessor, CheckboxControlValueAccessor) || |       hasConstructor(valueAccessor, CheckboxControlValueAccessor) || | ||||||
|  |       hasConstructor(valueAccessor, RangeValueAccessor) || | ||||||
|       hasConstructor(valueAccessor, NumberValueAccessor) || |       hasConstructor(valueAccessor, NumberValueAccessor) || | ||||||
|       hasConstructor(valueAccessor, SelectControlValueAccessor) || |       hasConstructor(valueAccessor, SelectControlValueAccessor) || | ||||||
|       hasConstructor(valueAccessor, SelectMultipleControlValueAccessor) || |       hasConstructor(valueAccessor, SelectMultipleControlValueAccessor) || | ||||||
|  | |||||||
| @ -22,11 +22,26 @@ export function main() { | |||||||
|       TestBed.configureTestingModule({ |       TestBed.configureTestingModule({ | ||||||
|         imports: [FormsModule, ReactiveFormsModule], |         imports: [FormsModule, ReactiveFormsModule], | ||||||
|         declarations: [ |         declarations: [ | ||||||
|           FormControlComp, FormGroupComp, FormArrayComp, FormArrayNestedGroup, |           FormControlComp, | ||||||
|           FormControlNameSelect, FormControlNumberInput, FormControlRadioButtons, WrappedValue, |           FormGroupComp, | ||||||
|           WrappedValueForm, MyInput, MyInputForm, FormGroupNgModel, FormControlNgModel, |           FormArrayComp, | ||||||
|           LoginIsEmptyValidator, LoginIsEmptyWrapper, ValidationBindingsForm, UniqLoginValidator, |           FormArrayNestedGroup, | ||||||
|           UniqLoginWrapper, NestedFormGroupComp |           FormControlNameSelect, | ||||||
|  |           FormControlNumberInput, | ||||||
|  |           FormControlRangeInput, | ||||||
|  |           FormControlRadioButtons, | ||||||
|  |           WrappedValue, | ||||||
|  |           WrappedValueForm, | ||||||
|  |           MyInput, | ||||||
|  |           MyInputForm, | ||||||
|  |           FormGroupNgModel, | ||||||
|  |           FormControlNgModel, | ||||||
|  |           LoginIsEmptyValidator, | ||||||
|  |           LoginIsEmptyWrapper, | ||||||
|  |           ValidationBindingsForm, | ||||||
|  |           UniqLoginValidator, | ||||||
|  |           UniqLoginWrapper, | ||||||
|  |           NestedFormGroupComp | ||||||
|         ] |         ] | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| @ -1126,6 +1141,57 @@ export function main() { | |||||||
| 
 | 
 | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|  |       describe('should support <type=range>', () => { | ||||||
|  |         it('with basic use case', () => { | ||||||
|  |           const fixture = TestBed.createComponent(FormControlRangeInput); | ||||||
|  |           const control = new FormControl(10); | ||||||
|  |           fixture.componentInstance.control = control; | ||||||
|  |           fixture.detectChanges(); | ||||||
|  | 
 | ||||||
|  |           // model -> view
 | ||||||
|  |           const input = fixture.debugElement.query(By.css('input')); | ||||||
|  |           expect(input.nativeElement.value).toEqual('10'); | ||||||
|  | 
 | ||||||
|  |           input.nativeElement.value = '20'; | ||||||
|  |           dispatchEvent(input.nativeElement, 'input'); | ||||||
|  | 
 | ||||||
|  |           // view -> model
 | ||||||
|  |           expect(control.value).toEqual(20); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         it('when value is cleared in the UI', () => { | ||||||
|  |           const fixture = TestBed.createComponent(FormControlNumberInput); | ||||||
|  |           const control = new FormControl(10, Validators.required); | ||||||
|  |           fixture.componentInstance.control = control; | ||||||
|  |           fixture.detectChanges(); | ||||||
|  | 
 | ||||||
|  |           const input = fixture.debugElement.query(By.css('input')); | ||||||
|  |           input.nativeElement.value = ''; | ||||||
|  |           dispatchEvent(input.nativeElement, 'input'); | ||||||
|  | 
 | ||||||
|  |           expect(control.valid).toBe(false); | ||||||
|  |           expect(control.value).toEqual(null); | ||||||
|  | 
 | ||||||
|  |           input.nativeElement.value = '0'; | ||||||
|  |           dispatchEvent(input.nativeElement, 'input'); | ||||||
|  | 
 | ||||||
|  |           expect(control.valid).toBe(true); | ||||||
|  |           expect(control.value).toEqual(0); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         it('when value is cleared programmatically', () => { | ||||||
|  |           const fixture = TestBed.createComponent(FormControlNumberInput); | ||||||
|  |           const control = new FormControl(10); | ||||||
|  |           fixture.componentInstance.control = control; | ||||||
|  |           fixture.detectChanges(); | ||||||
|  | 
 | ||||||
|  |           control.setValue(null); | ||||||
|  | 
 | ||||||
|  |           const input = fixture.debugElement.query(By.css('input')); | ||||||
|  |           expect(input.nativeElement.value).toEqual(''); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|       describe('custom value accessors', () => { |       describe('custom value accessors', () => { | ||||||
|         it('should support basic functionality', () => { |         it('should support basic functionality', () => { | ||||||
|           const fixture = TestBed.createComponent(WrappedValueForm); |           const fixture = TestBed.createComponent(WrappedValueForm); | ||||||
| @ -1852,6 +1918,16 @@ class FormControlNumberInput { | |||||||
|   control: FormControl; |   control: FormControl; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @Component({ | ||||||
|  |   selector: 'form-control-range-input', | ||||||
|  |   template: ` | ||||||
|  |     <input type="range" [formControl]="control"> | ||||||
|  |   ` | ||||||
|  | }) | ||||||
|  | class FormControlRangeInput { | ||||||
|  |   control: FormControl; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'form-control-radio-buttons', |   selector: 'form-control-radio-buttons', | ||||||
|   template: ` |   template: ` | ||||||
|  | |||||||
| @ -20,7 +20,7 @@ export function main() { | |||||||
|       TestBed.configureTestingModule({ |       TestBed.configureTestingModule({ | ||||||
|         declarations: [ |         declarations: [ | ||||||
|           StandaloneNgModel, NgModelForm, NgModelGroupForm, NgModelValidBinding, NgModelNgIfForm, |           StandaloneNgModel, NgModelForm, NgModelGroupForm, NgModelValidBinding, NgModelNgIfForm, | ||||||
|           NgModelRadioForm, NgModelSelectForm, NgNoFormComp, InvalidNgModelNoName, |           NgModelRadioForm, NgModelRangeForm, NgModelSelectForm, NgNoFormComp, InvalidNgModelNoName, | ||||||
|           NgModelOptionsStandalone, NgModelCustomComp, NgModelCustomWrapper, |           NgModelOptionsStandalone, NgModelCustomComp, NgModelCustomWrapper, | ||||||
|           NgModelValidationBindings, NgModelMultipleValidators, NgAsyncValidator, |           NgModelValidationBindings, NgModelMultipleValidators, NgAsyncValidator, | ||||||
|           NgModelAsyncValidation |           NgModelAsyncValidation | ||||||
| @ -503,6 +503,26 @@ export function main() { | |||||||
| 
 | 
 | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |     describe('range control', () => { | ||||||
|  |       it('should support <type=range>', fakeAsync(() => { | ||||||
|  |            const fixture = TestBed.createComponent(NgModelRangeForm); | ||||||
|  |            // model -> view
 | ||||||
|  |            fixture.componentInstance.val = 4; | ||||||
|  |            fixture.detectChanges(); | ||||||
|  |            tick(); | ||||||
|  |            let input = fixture.debugElement.query(By.css('input')); | ||||||
|  |            expect(input.nativeElement.value).toBe('4'); | ||||||
|  |            fixture.detectChanges(); | ||||||
|  |            tick(); | ||||||
|  |            let newVal = '4'; | ||||||
|  |            input.triggerEventHandler('input', {target: {value: newVal}}); | ||||||
|  |            tick(); | ||||||
|  |            // view -> model
 | ||||||
|  |            fixture.detectChanges(); | ||||||
|  |            expect(typeof(fixture.componentInstance.val)).toBe('number'); | ||||||
|  |          })); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     describe('radio controls', () => { |     describe('radio controls', () => { | ||||||
|       it('should support <type=radio>', fakeAsync(() => { |       it('should support <type=radio>', fakeAsync(() => { | ||||||
|            const fixture = TestBed.createComponent(NgModelRadioForm); |            const fixture = TestBed.createComponent(NgModelRadioForm); | ||||||
| @ -1023,6 +1043,11 @@ class NgModelOptionsStandalone { | |||||||
|   two: string; |   two: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @Component({selector: 'ng-model-range-form', template: '<input type="range" [(ngModel)]="val">'}) | ||||||
|  | class NgModelRangeForm { | ||||||
|  |   val: any; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'ng-model-radio-form', |   selector: 'ng-model-radio-form', | ||||||
|   template: ` |   template: ` | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user