-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
3 changed files
with
332 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
import {it, iit, expect, inject, async, tick, fakeAsync, describe, ddescribe} from '@angular/core/testing'; | ||
import {TestComponentBuilder} from '@angular/compiler/testing'; | ||
import {Component} from '@angular/core'; | ||
import {NGB_RADIO_DIRECTIVES} from './radio'; | ||
|
||
|
||
function expectRadios(element: HTMLElement, states: number[]) { | ||
var labels = element.querySelectorAll('label'); | ||
expect(labels.length).toEqual(states.length); | ||
|
||
for (let i = 0; i < states.length; i++) { | ||
let state = states[i]; | ||
|
||
if (state === 1) { | ||
expect(labels[i]).toHaveCssClass('active'); | ||
} else if (state === 0) { | ||
expect(labels[i]).not.toHaveCssClass('active'); | ||
} | ||
} | ||
} | ||
|
||
function getInput(nativeEl: HTMLElement, idx: number): HTMLInputElement { | ||
return <HTMLInputElement>nativeEl.querySelectorAll('input')[idx]; | ||
} | ||
|
||
describe('ng-radio-group', () => { | ||
|
||
const defaultHtml = `<div [(ngModel)]="model" ngb-radio-group> | ||
<label class="btn"> | ||
<input type="radio" name="radio" [value]="values[0]" ngb-radio/> {{ values[0] }} | ||
</label> | ||
<label class="btn"> | ||
<input type="radio" name="radio" [value]="values[1]" ngb-radio/> {{ values[1] }} | ||
</label> | ||
</div>`; | ||
|
||
it('toggles radio inputs based on model changes', async(inject([TestComponentBuilder], (tcb) => { | ||
tcb.overrideTemplate(TestComponent, defaultHtml).createAsync(TestComponent).then((fixture) => { | ||
|
||
let values = fixture.componentInstance.values; | ||
|
||
fixture.detectChanges(); | ||
expectRadios(fixture.nativeElement, [0, 0]); | ||
|
||
// checking null | ||
fixture.componentInstance.model = null; | ||
fixture.detectChanges(); | ||
expectRadios(fixture.nativeElement, [0, 0]); | ||
|
||
// checking first radio | ||
fixture.componentInstance.model = values[0]; | ||
fixture.detectChanges(); | ||
expectRadios(fixture.nativeElement, [1, 0]); | ||
|
||
// checking second radio | ||
fixture.componentInstance.model = values[1]; | ||
fixture.detectChanges(); | ||
expectRadios(fixture.nativeElement, [0, 1]); | ||
|
||
// checking non-matching value | ||
fixture.componentInstance.model = values[3]; | ||
fixture.detectChanges(); | ||
expectRadios(fixture.nativeElement, [0, 0]); | ||
}); | ||
}))); | ||
|
||
it('updates model based on radio input clicks', fakeAsync(inject([TestComponentBuilder], (tcb) => { | ||
tcb.overrideTemplate(TestComponent, defaultHtml).createAsync(TestComponent).then((fixture) => { | ||
|
||
fixture.detectChanges(); | ||
expectRadios(fixture.nativeElement, [0, 0]); | ||
|
||
// clicking first radio | ||
getInput(fixture.nativeElement, 0).click(); | ||
tick(); | ||
fixture.detectChanges(); | ||
expectRadios(fixture.nativeElement, [1, 0]); | ||
expect(fixture.componentInstance.model).toBe('one'); | ||
|
||
// clicking second radio | ||
getInput(fixture.nativeElement, 1).click(); | ||
tick(); | ||
fixture.detectChanges(); | ||
expectRadios(fixture.nativeElement, [0, 1]); | ||
expect(fixture.componentInstance.model).toBe('two'); | ||
}); | ||
}))); | ||
|
||
it('can be used with objects as values', fakeAsync(inject([TestComponentBuilder], (tcb) => { | ||
tcb.overrideTemplate(TestComponent, defaultHtml).createAsync(TestComponent).then((fixture) => { | ||
|
||
let [one, two] = [{one: 'one'}, {two: 'two'}]; | ||
|
||
fixture.componentInstance.values[0] = one; | ||
fixture.componentInstance.values[1] = two; | ||
|
||
fixture.detectChanges(); | ||
expectRadios(fixture.nativeElement, [0, 0]); | ||
|
||
// checking model -> radio input | ||
fixture.componentInstance.model = one; | ||
fixture.detectChanges(); | ||
expectRadios(fixture.nativeElement, [1, 0]); | ||
|
||
// checking radio click -> model | ||
getInput(fixture.nativeElement, 1).click(); | ||
tick(); | ||
fixture.detectChanges(); | ||
expectRadios(fixture.nativeElement, [0, 1]); | ||
expect(fixture.componentInstance.model).toBe(two); | ||
}); | ||
}))); | ||
|
||
it('updates radio input values dynamically', fakeAsync(inject([TestComponentBuilder], (tcb) => { | ||
tcb.overrideTemplate(TestComponent, defaultHtml).createAsync(TestComponent).then((fixture) => { | ||
|
||
let values = fixture.componentInstance.values; | ||
|
||
// checking first radio | ||
fixture.componentInstance.model = values[0]; | ||
fixture.detectChanges(); | ||
expectRadios(fixture.nativeElement, [1, 0]); | ||
|
||
// updating radio value | ||
fixture.componentInstance.values[0] = 'ten'; | ||
fixture.detectChanges(); | ||
expectRadios(fixture.nativeElement, [1, 0]); | ||
tick(); | ||
expect(fixture.componentInstance.model).toEqual('ten'); | ||
}); | ||
}))); | ||
|
||
it('can be used with ngFor', async(inject([TestComponentBuilder], (tcb) => { | ||
|
||
const forHtml = `<div [(ngModel)]="model" ngb-radio-group> | ||
<label *ngFor="let v of values" class="btn"> | ||
<input type="radio" name="radio" [value]="v" ngb-radio/> {{ v }} | ||
</label> | ||
</div>`; | ||
|
||
tcb.overrideTemplate(TestComponent, forHtml).createAsync(TestComponent).then((fixture) => { | ||
|
||
let values = fixture.componentInstance.values; | ||
|
||
fixture.detectChanges(); | ||
expectRadios(fixture.nativeElement, [0, 0, 0]); | ||
|
||
fixture.componentInstance.model = values[1]; | ||
fixture.detectChanges(); | ||
expectRadios(fixture.nativeElement, [0, 1, 0]); | ||
}); | ||
}))); | ||
|
||
it('cleans up the model when radio inputs are hidden', fakeAsync(inject([TestComponentBuilder], (tcb) => { | ||
|
||
const ifHtml = `<div [(ngModel)]="model" ngb-radio-group> | ||
<label class="btn"> | ||
<input type="radio" name="radio" [value]="values[0]" ngb-radio/> {{ values[0] }} | ||
</label> | ||
<label *ngIf="shown" class="btn"> | ||
<input type="radio" name="radio" [value]="values[1]" ngb-radio/> {{ values[1] }} | ||
</label> | ||
</div>`; | ||
|
||
tcb.overrideTemplate(TestComponent, ifHtml).createAsync(TestComponent).then((fixture) => { | ||
|
||
let values = fixture.componentInstance.values; | ||
|
||
// hiding/showing non-selected radio -> expecting initial model value | ||
fixture.detectChanges(); | ||
expectRadios(fixture.nativeElement, [0, 0]); | ||
|
||
fixture.componentInstance.shown = false; | ||
fixture.detectChanges(); | ||
expectRadios(fixture.nativeElement, [0]); | ||
tick(); | ||
expect(fixture.componentInstance.model).toBeUndefined(); | ||
|
||
fixture.componentInstance.shown = true; | ||
fixture.detectChanges(); | ||
expectRadios(fixture.nativeElement, [0, 0]); | ||
tick(); | ||
expect(fixture.componentInstance.model).toBeUndefined(); | ||
|
||
// hiding/showing selected radio -> expecting model to become null | ||
fixture.componentInstance.model = values[1]; | ||
fixture.detectChanges(); | ||
expectRadios(fixture.nativeElement, [0, 1]); | ||
|
||
fixture.componentInstance.shown = false; | ||
fixture.detectChanges(); | ||
expectRadios(fixture.nativeElement, [0]); | ||
tick(); | ||
expect(fixture.componentInstance.model).toBeNull(); | ||
|
||
fixture.componentInstance.shown = true; | ||
fixture.detectChanges(); | ||
expectRadios(fixture.nativeElement, [0, 0]); | ||
tick(); | ||
expect(fixture.componentInstance.model).toBeNull(); | ||
}); | ||
}))); | ||
|
||
}); | ||
|
||
@Component({selector: 'test-cmp', directives: [NGB_RADIO_DIRECTIVES], template: ''}) | ||
class TestComponent { | ||
model; | ||
values = ['one', 'two', 'three']; | ||
shown = true; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import {Directive, forwardRef, Provider, Host, Optional, Input, Renderer, ElementRef, OnDestroy} from '@angular/core'; | ||
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/common'; | ||
|
||
const NGB_RADIO_VALUE_ACCESSOR = | ||
new Provider(NG_VALUE_ACCESSOR, {useExisting: forwardRef(() => NgbRadioGroup), multi: true}); | ||
|
||
@Directive({selector: '[ngb-radio-group][ngModel]', bindings: [NGB_RADIO_VALUE_ACCESSOR]}) | ||
export class NgbRadioGroup implements ControlValueAccessor { | ||
private _radios: Set<NgbRadio> = new Set<NgbRadio>(); | ||
private _selectedRadio: NgbRadio = null; | ||
private _value = null; | ||
|
||
onChange = (_: any) => {}; | ||
onTouched = () => {}; | ||
|
||
onRadioChange(radio: NgbRadio) { this._setGroupValue(radio); } | ||
|
||
onRadioValueUpdate(radio: NgbRadio) { | ||
if (this._selectedRadio === radio) { | ||
this.onRadioChange(radio); | ||
} else { | ||
this._updateRadios(); | ||
} | ||
} | ||
|
||
register(radio: NgbRadio) { | ||
this._radios.add(radio); | ||
this._updateRadios(); | ||
} | ||
|
||
registerOnChange(fn: (value: any) => any): void { this.onChange = fn; } | ||
|
||
registerOnTouched(fn: () => any): void { this.onTouched = fn; } | ||
|
||
unregister(radio: NgbRadio) { | ||
this._radios.delete(radio); | ||
if (this._selectedRadio === radio) { | ||
this._setGroupValue(null); | ||
} | ||
} | ||
|
||
writeValue(value) { | ||
this._value = value; | ||
this._updateRadios(); | ||
} | ||
|
||
private _setGroupValue(radio: NgbRadio) { | ||
this._selectedRadio = radio; | ||
var value = radio ? radio.value : null; | ||
this.writeValue(value); | ||
this.onChange(value); | ||
} | ||
|
||
private _updateRadios() { | ||
this._selectedRadio = null; | ||
this._radios.forEach((radio) => { | ||
if (radio.markChecked(this._value)) { | ||
this._selectedRadio = radio; | ||
} | ||
}); | ||
} | ||
} | ||
|
||
|
||
@Directive({selector: 'label.btn', host: {'[class.active]': 'checked'}}) | ||
export class NgbRadioLabel { | ||
@Input() checked: boolean; | ||
} | ||
|
||
|
||
@Directive({selector: 'input[type=radio]', host: {'(change)': 'onChange($event.target.value)', '[checked]': 'checked'}}) | ||
export class NgbRadio implements OnDestroy { | ||
private _value = null; | ||
|
||
@Input() checked: boolean; | ||
|
||
@Input('value') | ||
set value(value) { | ||
this._value = value; | ||
var stringValue = value ? value.toString() : ''; | ||
this.renderer.setElementProperty(this.element.nativeElement, 'value', stringValue); | ||
|
||
if (this.group) { | ||
this.group.onRadioValueUpdate(this); | ||
} | ||
} | ||
|
||
get value() { return this._value; } | ||
|
||
constructor( | ||
@Optional() @Host() private group: NgbRadioGroup, @Optional() @Host() private label: NgbRadioLabel, | ||
private renderer: Renderer, private element: ElementRef) { | ||
if (this.group) { | ||
this.group.register(this); | ||
} | ||
} | ||
|
||
markChecked(value): boolean { | ||
this.checked = (this.value === value && value !== null); | ||
this.label.checked = this.checked; | ||
|
||
return this.checked; | ||
} | ||
|
||
ngOnDestroy() { | ||
if (this.group) { | ||
this.group.unregister(this); | ||
} | ||
} | ||
|
||
onChange() { | ||
if (this.group) { | ||
this.group.onRadioChange(this); | ||
} | ||
} | ||
} | ||
|
||
export const NGB_RADIO_DIRECTIVES = [NgbRadio, NgbRadioLabel, NgbRadioGroup]; |