diff --git a/CHANGELOG.md b/CHANGELOG.md index c4be44b08e4..5ea22e526e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Ignite UI for Angular Change Log All notable changes for each version of this project will be documented in this file. +## 6.0.4 +- **igxRadioGroup** directive introduced. It allows better control over its child `igxRadio` components and support template-driven and reactive forms. ## 6.0.3 - **igxGrid** exposing the `filteredSortedData` method publicly - returns the grid data with current filtering and sorting applied. diff --git a/projects/igniteui-angular/src/lib/directives/radio/radio-group.directive.spec.ts b/projects/igniteui-angular/src/lib/directives/radio/radio-group.directive.spec.ts new file mode 100644 index 00000000000..050266ada05 --- /dev/null +++ b/projects/igniteui-angular/src/lib/directives/radio/radio-group.directive.spec.ts @@ -0,0 +1,269 @@ +import { Component, ViewChild } from '@angular/core'; +import { async, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { IgxRadioModule, IgxRadioGroupDirective } from './radio-group.directive'; +import { FormsModule, ReactiveFormsModule, FormGroup, FormBuilder } from '@angular/forms'; + +describe('IgxRadioGroupDirective', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + RadioGroupComponent, + RadioGroupWithModelComponent, + RadioGroupReactiveFormsComponent + ], + imports: [ + IgxRadioModule, + FormsModule, + ReactiveFormsModule + ] + }) + .compileComponents(); + })); + + it('Properly initialize the radio group buttons\' properties.', fakeAsync(() => { + const fixture = TestBed.createComponent(RadioGroupComponent); + const radioInstance = fixture.componentInstance.radioGroup; + + fixture.detectChanges(); + tick(); + + expect(radioInstance.radioButtons).toBeDefined(); + expect(radioInstance.radioButtons.length).toEqual(3); + + const allRequiredButtons = radioInstance.radioButtons.filter((btn) => btn.required); + expect(allRequiredButtons.length).toEqual(radioInstance.radioButtons.length); + + const allButtonsWithGroupName = radioInstance.radioButtons.filter((btn) => btn.name === radioInstance.name); + expect(allButtonsWithGroupName.length).toEqual(radioInstance.radioButtons.length); + + const allButtonsWithGroupLabelPos = radioInstance.radioButtons.filter((btn) => btn.labelPosition === radioInstance.labelPosition); + expect(allButtonsWithGroupLabelPos.length).toEqual(radioInstance.radioButtons.length); + + const buttonWithGroupValue = radioInstance.radioButtons.find((btn) => btn.value === radioInstance.value); + expect(buttonWithGroupValue).toBeDefined(); + expect(buttonWithGroupValue).toEqual(radioInstance.selected); + })); + + it('Setting radioGroup\'s properties should affect all radio buttons.', fakeAsync(() => { + const fixture = TestBed.createComponent(RadioGroupComponent); + const radioInstance = fixture.componentInstance.radioGroup; + + fixture.detectChanges(); + tick(); + + expect(radioInstance.radioButtons).toBeDefined(); + + // name + radioInstance.name = 'newGroupName'; + fixture.detectChanges(); + + const allButtonsWithNewName = radioInstance.radioButtons.filter((btn) => btn.name === 'newGroupName'); + expect(allButtonsWithNewName.length).toEqual(radioInstance.radioButtons.length); + + // required + radioInstance.required = true; + fixture.detectChanges(); + + const allRequiredButtons = radioInstance.radioButtons.filter((btn) => btn.required); + expect(allRequiredButtons.length).toEqual(radioInstance.radioButtons.length); + + // labelPosition + radioInstance.labelPosition = 'after'; + fixture.detectChanges(); + + const allAfterButtons = radioInstance.radioButtons.filter((btn) => btn.labelPosition === 'after'); + expect(allAfterButtons.length).toEqual(radioInstance.radioButtons.length); + + // disabled + radioInstance.disabled = true; + fixture.detectChanges(); + + const allDisabledButtons = radioInstance.radioButtons.filter((btn) => btn.disabled); + expect(allDisabledButtons.length).toEqual(radioInstance.radioButtons.length); + })); + + it('Set value should change selected property and emit change event.', fakeAsync(() => { + const fixture = TestBed.createComponent(RadioGroupComponent); + const radioInstance = fixture.componentInstance.radioGroup; + + fixture.detectChanges(); + tick(); + + expect(radioInstance.value).toBeDefined(); + expect(radioInstance.value).toEqual('Baz'); + + expect(radioInstance.selected).toBeDefined(); + expect(radioInstance.selected).toEqual(radioInstance.radioButtons.last); + + spyOn(radioInstance.change, 'emit'); + + radioInstance.value = 'Foo'; + fixture.detectChanges(); + + expect(radioInstance.value).toEqual('Foo'); + expect(radioInstance.selected).toEqual(radioInstance.radioButtons.first); + expect(radioInstance.change.emit).toHaveBeenCalled(); + })); + + it('Set selected property should change value and emit change event.', fakeAsync(() => { + const fixture = TestBed.createComponent(RadioGroupComponent); + const radioInstance = fixture.componentInstance.radioGroup; + + fixture.detectChanges(); + tick(); + + expect(radioInstance.value).toBeDefined(); + expect(radioInstance.value).toEqual('Baz'); + + expect(radioInstance.selected).toBeDefined(); + expect(radioInstance.selected).toEqual(radioInstance.radioButtons.last); + + spyOn(radioInstance.change, 'emit'); + + radioInstance.selected = radioInstance.radioButtons.first; + fixture.detectChanges(); + + expect(radioInstance.value).toEqual('Foo'); + expect(radioInstance.selected).toEqual(radioInstance.radioButtons.first); + expect(radioInstance.change.emit).toHaveBeenCalled(); + })); + + it('Clicking on a radio button should update the model.', fakeAsync(() => { + const fixture = TestBed.createComponent(RadioGroupWithModelComponent); + const radioInstance = fixture.componentInstance.radioGroup; + + fixture.detectChanges(); + tick(); + + radioInstance.radioButtons.first.nativeLabel.nativeElement.click(); + fixture.detectChanges(); + tick(); + + expect(radioInstance.value).toEqual('Winter'); + expect(radioInstance.selected).toEqual(radioInstance.radioButtons.first); + })); + + it('Updating the model should select a radio button.', fakeAsync(() => { + const fixture = TestBed.createComponent(RadioGroupWithModelComponent); + const radioInstance = fixture.componentInstance.radioGroup; + + fixture.detectChanges(); + tick(); + + fixture.componentInstance.personBob.favoriteSeason = 'Winter'; + fixture.detectChanges(); + tick(); + + expect(radioInstance.value).toEqual('Winter'); + expect(radioInstance.selected).toEqual(radioInstance.radioButtons.first); + })); + + it('Properly update the model when radio group is hosted in Reactive forms.', fakeAsync(() => { + const fixture = TestBed.createComponent(RadioGroupReactiveFormsComponent); + + fixture.detectChanges(); + tick(); + + expect(fixture.componentInstance.personForm).toBeDefined(); + expect(fixture.componentInstance.model).toBeDefined(); + expect(fixture.componentInstance.newModel).toBeUndefined(); + + fixture.componentInstance.personForm.patchValue({ favoriteSeason: fixture.componentInstance.seasons[0] }); + fixture.componentInstance.updateModel(); + fixture.detectChanges(); + tick(); + + expect(fixture.componentInstance.newModel).toBeDefined(); + expect(fixture.componentInstance.newModel.name).toEqual(fixture.componentInstance.model.name); + expect(fixture.componentInstance.newModel.favoriteSeason).toEqual(fixture.componentInstance.seasons[0]); + })); +}); + +@Component({ + template: ` + + {{item}} + + +` +}) +class RadioGroupComponent { + @ViewChild('radioGroup', { read: IgxRadioGroupDirective }) public radioGroup: IgxRadioGroupDirective; +} + +class Person { + name: string; + favoriteSeason: string; +} + +@Component({ + template: ` + + {{item}} + + +` +}) +class RadioGroupWithModelComponent { + seasons = [ + 'Winter', + 'Spring', + 'Summer', + 'Autumn', + ]; + + @ViewChild('radioGroupSeasons', { read: IgxRadioGroupDirective }) public radioGroup: IgxRadioGroupDirective; + + personBob: Person = { name: 'Bob', favoriteSeason: 'Summer' }; +} + +@Component({ + template: ` +
+ + + {{item}} + + +
+` +}) +class RadioGroupReactiveFormsComponent { + seasons = [ + 'Winter', + 'Spring', + 'Summer', + 'Autumn', + ]; + + newModel: Person; + model: Person = { name: 'Kirk', favoriteSeason: this.seasons[1] }; + personForm: FormGroup; + + constructor(private _formBuilder: FormBuilder) { + this._createForm(); + } + + updateModel() { + const formModel = this.personForm.value; + + this.newModel = { + name: formModel.name as string, + favoriteSeason: formModel.favoriteSeason as string + }; + } + + private _createForm() { + // create form + this.personForm = this._formBuilder.group({ + name: '', + favoriteSeason: '' + }); + + // simulate model loading from service + this.personForm.setValue({ + name: this.model.name, + favoriteSeason: this.model.favoriteSeason + }); + } +} diff --git a/projects/igniteui-angular/src/lib/directives/radio/radio-group.directive.ts b/projects/igniteui-angular/src/lib/directives/radio/radio-group.directive.ts new file mode 100644 index 00000000000..7225f124a94 --- /dev/null +++ b/projects/igniteui-angular/src/lib/directives/radio/radio-group.directive.ts @@ -0,0 +1,350 @@ +import { Directive, NgModule, Input, QueryList, Output, EventEmitter, AfterContentInit, ContentChildren } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { IgxRadioComponent, RadioLabelPosition, IChangeRadioEventArgs } from '../../radio/radio.component'; +import { IgxRippleModule } from '../ripple/ripple.directive'; + +const noop = () => { }; +let nextId = 0; + +/** + * **Ignite UI for Angular Radio Group** - + * [Documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/radio_group.html) + * + * The Ignite UI Radio Group allows the user to select a single option from an available set of options that are listed side by side. + * + * Example: + * ```html + * + * + * {{item}} + * + * + * ``` + */ +@Directive({ + selector: 'igx-radio-group, [igxRadioGroup]', + providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: IgxRadioGroupDirective, multi: true }] +}) +export class IgxRadioGroupDirective implements AfterContentInit, ControlValueAccessor { + /** + * Returns reference to the child radio buttons. + * ```typescript + * let radioButtons = this.radioGroup.radioButtons; + * ``` + * @memberof IgxRadioGroupDirective + */ + @ContentChildren(IgxRadioComponent) public radioButtons: QueryList; + + /** + * Sets/gets the `value` attribute. + * ```html + * + * ``` + * ```typescript + * let value = this.radioGroup.value; + * ``` + * @memberof IgxRadioGroupDirective + */ + @Input() + get value(): any { return this._value; } + set value(newValue: any) { + if (this._value !== newValue) { + this._value = newValue; + this._selectRadioButton(); + } + } + + /** + * Sets/gets the `name` attribute of the radio group component. All child radio buttons inherits this name. + * ```html + * + * ``` + * ```typescript + * let name = this.radioGroup.name; + * ``` + * @memberof IgxRadioGroupDirective + */ + @Input() + get name(): string { return this._name; } + set name(newValue: string) { + if (this._name !== newValue) { + this._name = newValue; + this._setRadioButtonNames(); + } + } + + /** + * Sets/gets whether the radio group is required. + * If not set, `required` will have value `false`. + * ```html + * + * ``` + * ```typescript + * let isRequired = this.radioGroup.required; + * ``` + * @memberof IgxRadioGroupDirective + */ + @Input() + get required(): boolean { return this._required; } + set required(newValue: boolean) { + if (this._required !== newValue) { + this._required = newValue; + this._setRadioButtonsRequired(); + } + } + + /** + * An @Input property that allows you to disable the radio group. By default it's false. + * ```html + * + * ``` + * @memberof IgxRadioGroupDirective + */ + @Input() + get disabled(): boolean { return this._disabled; } + set disabled(newValue: boolean) { + if (this._disabled !== newValue) { + this._disabled = newValue; + this._disableRadioButtons(); + } + } + + /** + * Sets/gets the position of the `label` in the child radio buttons. + * If not set, `labelPosition` will have value `"after"`. + * ```html + * + * ``` + * ```typescript + * let labelPosition = this.radioGroup.labelPosition; + * ``` + * @memberof IgxRadioGroupDirective + */ + @Input() + get labelPosition(): RadioLabelPosition | string { return this._labelPosition; } + set labelPosition(newValue: RadioLabelPosition | string) { + if (this._labelPosition !== newValue) { + this._labelPosition = newValue === RadioLabelPosition.BEFORE ? RadioLabelPosition.BEFORE : RadioLabelPosition.AFTER; + this._setRadioButtonLabelPosition(); + } + } + + /** + * Sets/gets the selected child radio button. + * ```typescript + * let selectedButton = this.radioGroup.selected; + * this.radioGroup.selected = selectedButton; + * ``` + * @memberof IgxRadioGroupDirective + */ + @Input() + get selected() { return this._selected; } + set selected(selected: IgxRadioComponent | null) { + if (this._selected !== selected) { + this._selected = selected; + this.value = selected ? selected.value : null; + } + } + + /** + * An event that is emitted after the radio group `value` is changed. + * Provides references to the selected `IgxRadioComponent` and the `value` property as event arguments. + * @memberof IgxRadioGroupDirective + */ + @Output() + readonly change: EventEmitter = new EventEmitter(); + + /** + *@hidden + */ + private _onChangeCallback: (_: any) => void = noop; + /** + *@hidden + */ + private _name = `igx-radio-group-${nextId++}`; + /** + *@hidden + */ + private _value: any = null; + /** + *@hidden + */ + private _selected: IgxRadioComponent | null = null; + /** + *@hidden + */ + private _isInitialized = false; + /** + *@hidden + */ + private _labelPosition: RadioLabelPosition | string = 'after'; + /** + *@hidden + */ + private _disabled = false; + /** + *@hidden + */ + private _required = false; + + ngAfterContentInit() { + // The initial value can possibly be set by NgModel and it is possible that + // the OnInit of the NgModel occurs after the OnInit of this class. + this._isInitialized = true; + + setTimeout(() => { this._initRadioButtons(); }); + } + + /** + * Checks whether the provided value is consistent to the current radio button. + * If it is, the checked attribute will have value `true` and selected property will contain the selected `IgxRadioComponent`. + * ```typescript + * this.radioGroup.writeValue('radioButtonValue'); + * ``` + */ + public writeValue(value: any) { + this.value = value; + } + + /** + *@hidden + */ + public registerOnChange(fn: (_: any) => void) { this._onChangeCallback = fn; } + + /** + *@hidden + */ + public registerOnTouched(fn: () => void) { + if (this.radioButtons) { + this.radioButtons.forEach((button) => { + button.registerOnTouched(fn); + }); + } + } + + /** + *@hidden + */ + private _initRadioButtons() { + if (this.radioButtons) { + this.radioButtons.forEach((button) => { + button.name = this._name; + button.labelPosition = this._labelPosition; + button.disabled = this._disabled; + button.required = this._required; + + if (this._value && button.value === this._value) { + button.checked = true; + this._selected = button; + } + + button.change.subscribe((ev) => this._selectedRadioButtonChanged(ev)); + }); + } + } + + /** + *@hidden + */ + private _selectedRadioButtonChanged(args: IChangeRadioEventArgs) { + if (this._selected !== args.radio) { + if (this._selected) { + this._selected.checked = false; + } + this._selected = args.radio; + } + + this._value = args.value; + + if (this._isInitialized) { + this.change.emit(args); + this._onChangeCallback(this.value); + } + } + + /** + *@hidden + */ + private _setRadioButtonNames() { + if (this.radioButtons) { + this.radioButtons.forEach((button) => { + button.name = this._name; + }); + } + } + + /** + *@hidden + */ + private _selectRadioButton() { + if (this.radioButtons) { + this.radioButtons.forEach((button) => { + if (!this._value) { + // no value - uncheck all radio buttons + if (button.checked) { + button.checked = false; + } + } else { + if (this._value === button.value) { + // selected button + if (this._selected !== button) { + this._selected = button; + } + + if (!button.checked) { + button.select(); + } + } else { + // non-selected button + if (button.checked) { + button.checked = false; + } + } + } + }); + } + } + + /** + *@hidden + */ + private _setRadioButtonLabelPosition() { + if (this.radioButtons) { + this.radioButtons.forEach((button) => { + button.labelPosition = this._labelPosition; + }); + } + } + + /** + *@hidden + */ + private _disableRadioButtons() { + if (this.radioButtons) { + this.radioButtons.forEach((button) => { + button.disabled = this._disabled; + }); + } + } + + /** + *@hidden + */ + private _setRadioButtonsRequired() { + if (this.radioButtons) { + this.radioButtons.forEach((button) => { + button.required = this._required; + }); + } + } +} + +/** + *The IgxRadioModule provides the {@link IgxRadioGroupDirective} and {@link IgxRadioComponent} inside your application. + */ +@NgModule({ + declarations: [IgxRadioGroupDirective, IgxRadioComponent], + exports: [IgxRadioGroupDirective, IgxRadioComponent], + imports: [IgxRippleModule] +}) +export class IgxRadioModule { } diff --git a/projects/igniteui-angular/src/lib/radio/radio.component.ts b/projects/igniteui-angular/src/lib/radio/radio.component.ts index 624747051a9..86868c21985 100644 --- a/projects/igniteui-angular/src/lib/radio/radio.component.ts +++ b/projects/igniteui-angular/src/lib/radio/radio.component.ts @@ -1,15 +1,12 @@ import { Component, EventEmitter, - forwardRef, HostBinding, Input, - NgModule, Output, ViewChild } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { IgxRippleModule } from '../directives/ripple/ripple.directive'; export interface IChangeRadioEventArgs { value: any; @@ -43,64 +40,250 @@ const noop = () => { }; }) export class IgxRadioComponent implements ControlValueAccessor { + /** + * Returns reference to native radio element. + * ```typescript + * let radioElement = this.radio.nativeRadio; + * ``` + * @memberof IgxSwitchComponent + */ @ViewChild('radio') public nativeRadio; + /** + * Returns reference to native label element. + * ```typescript + * let labelElement = this.radio.nativeLabel; + * ``` + * @memberof IgxSwitchComponent + */ @ViewChild('nativeLabel') public nativeLabel; + /** + * Returns reference to the label placeholder element. + * ```typescript + * let labelPlaceholder = this.radio.placeholderLabel; + * ``` + * @memberof IgxSwitchComponent + */ @ViewChild('placeholderLabel') public placeholderLabel; - /** ID of the component */ + /** + * Sets/gets the `id` of the radio component. + * If not set, the `id` of the first radio component will be `"igx-radio-0"`. + * ```html + * + * ``` + * ```typescript + * let radioId = this.radio.id; + * ``` + * @memberof IgxRadioComponent + */ @HostBinding('attr.id') @Input() public id = `igx-radio-${nextId++}`; + /** + * Sets/gets the id of the `label` element in the radio component. + * If not set, the id of the `label` in the first radio component will be `"igx-radio-0-label"`. + * ```html + * + * ``` + * ```typescript + * let labelId = this.radio.labelId; + * ``` + * @memberof IgxRadioComponent + */ @Input() public labelId = `${this.id}-label`; + /** + * Sets/gets the position of the `label` in the radio component. + * If not set, `labelPosition` will have value `"after"`. + * ```html + * + * ``` + * ```typescript + * let labelPosition = this.radio.labelPosition; + * ``` + * @memberof IgxRadioComponent + */ @Input() public labelPosition: RadioLabelPosition | string = 'after'; + /** + * Sets/gets the `value` attribute. + * ```html + * + * ``` + * ```typescript + * let value = this.radio.value; + * ``` + * @memberof IgxRadioComponent + */ @Input() public value: any; - @Input() public name: string; - @Input() public tabindex: number = null; - @Input() public disableRipple = false; - @Input() public required = false; - - @Input('aria-labelledby') +/** + * Sets/gets the `name` attribute of the radio component. + * ```html + * + * ``` + * ```typescript + * let name = this.radio.name; + * ``` + * @memberof IgxRadioComponent + */ +@Input() public name: string; +/** + * Sets the value of the `tabindex` attribute. + * ```html + * + * ``` + * ```typescript + * let tabIndex = this.radio.tabindex; + * ``` + * @memberof IgxRadioComponent + */ +@Input() public tabindex: number = null; +/** + * Enables/disables the ripple effect on the radio button.. + * If not set, the `disableRipple` will have value `false`. + * ```html + * + * ``` + * ```typescript + * let isDisabledRipple = this.radio.disableRipple; + * ``` + * @memberof IgxRadioComponent + */ +@Input() public disableRipple = false; +/** + * Sets/gets whether the radio button is required. + * If not set, `required` will have value `false`. + * ```html + * + * ``` + * ```typescript + * let isRequired = this.radio.required; + * ``` + * @memberof IgxRadioComponent + */ +@Input() public required = false; +/** + * Sets/gets the `aria-labelledby` attribute of the radio component. + * If not set, the `aria-labelledby` will be equal to the value of `labelId` attribute. + * ```html + * + * ``` + * ```typescript + * let ariaLabelledBy = this.radio.ariaLabelledBy; + * ``` + * @memberof IgxRadioComponent + */ +@Input('aria-labelledby') public ariaLabelledBy = this.labelId; - - @Input('aria-label') +/** + * Sets/gets the `aria-label` attribute of the radio component. + * ```html + * + * ``` + * ```typescript + * let ariaLabel = this.radio.ariaLabel; + * ``` + * @memberof IgxRadioComponent + */ +@Input('aria-label') public ariaLabel: string | null = null; - - @Output() +/** + * An event that is emitted after the radio `value` is changed. + * Provides references to the `IgxRadioComponent` and the `value` property as event arguments. + * @memberof IgxRadioComponent + */ +@Output() readonly change: EventEmitter = new EventEmitter(); - - @HostBinding('class.igx-radio') +/** + * Returns the class of the radio component. + * ```typescript + * let radioClass = this.radio.cssClass; + * ``` + * @memberof IgxRadioComponent + */ +@HostBinding('class.igx-radio') public cssClass = 'igx-radio'; - - @HostBinding('class.igx-radio--checked') +/** + * Sets/gets the `checked` attribute. + * Default value is `false`. + * ```html + * + * ``` + * ```typescript + * let isChecked = this.radio.checked; + * ``` + * @memberof IgxRadioComponent + */ +@HostBinding('class.igx-radio--checked') @Input() public checked = false; - - @HostBinding('class.igx-radio--disabled') +/** + * Sets/gets the `disabled` attribute. + * Default value is `false`. + * ```html + * + * ``` + * ```typescript + * let isDisabled = this.radio.disabled; + * ``` + * @memberof IgxRadioComponent + */ +@HostBinding('class.igx-radio--disabled') @Input() public disabled = false; - - @HostBinding('class.igx-radio--focused') +/** + * Sets/gets whether the radio component is on focus. + * Default value is `false`. + * ```typescript + * this.radio.focus = true; + * ``` + * ```typescript + * let isFocused = this.radio.focused; + * ``` + * @memberof IgxRadioComponent + */ +@HostBinding('class.igx-radio--focused') public focused = false; - - public inputId = `${this.id}-input`; +/** + *@hidden + */ +public inputId = `${this.id}-input`; + /** + *@hidden + */ protected _value: any = null; constructor() { } - - private _onTouchedCallback: () => void = noop; - private _onChangeCallback: (_: any) => void = noop; - - public _onRadioChange(event) { +/** + *@hidden + */ +private _onTouchedCallback: () => void = noop; +/** + *@hidden + */ +private _onChangeCallback: (_: any) => void = noop; +/** + *@hidden + */ +public _onRadioChange(event) { event.stopPropagation(); } - - public _onRadioClick(event) { +/** + *@hidden + */ +public _onRadioClick(event) { event.stopPropagation(); this.select(); } - - public _onLabelClick() { +/** + *@hidden + */ +public _onLabelClick() { this.select(); } - - public select() { +/** + * Selects the current radio button. + * ```typescript + * this.radio.select(); + * ``` + * @memberof IgxRadioComponent + */ +public select() { if (this.disabled) { return; } @@ -110,13 +293,21 @@ export class IgxRadioComponent implements ControlValueAccessor { this.change.emit({ value: this.value, radio: this }); this._onChangeCallback(this.value); } - - public writeValue(value: any) { +/** + * Checks whether the provided value is consistent to the current radio button. + * If it is, the checked attribute will have value `true`; + * ```typescript + * this.radio.writeValue('radioButtonValue'); + * ``` + */ +public writeValue(value: any) { this._value = value; this.checked = (this._value === this.value); } - - public get labelClass(): string { +/** + *@hidden + */ +public get labelClass(): string { switch (this.labelPosition) { case RadioLabelPosition.BEFORE: return `${this.cssClass}__label--before`; @@ -125,23 +316,25 @@ export class IgxRadioComponent implements ControlValueAccessor { return `${this.cssClass}__label`; } } - - public onFocus(event) { +/** + *@hidden + */ +public onFocus(event) { this.focused = true; } - - public onBlur(event) { +/** + *@hidden + */ +public onBlur(event) { this.focused = false; this._onTouchedCallback(); } - - public registerOnChange(fn: (_: any) => void) { this._onChangeCallback = fn; } - public registerOnTouched(fn: () => void) { this._onTouchedCallback = fn; } +/** + *@hidden + */ +public registerOnChange(fn: (_: any) => void) { this._onChangeCallback = fn; } +/** + *@hidden + */ +public registerOnTouched(fn: () => void) { this._onTouchedCallback = fn; } } - -@NgModule({ - declarations: [IgxRadioComponent], - exports: [IgxRadioComponent], - imports: [IgxRippleModule] -}) -export class IgxRadioModule { } diff --git a/projects/igniteui-angular/src/public_api.ts b/projects/igniteui-angular/src/public_api.ts index 68a92590360..eef4acab225 100644 --- a/projects/igniteui-angular/src/public_api.ts +++ b/projects/igniteui-angular/src/public_api.ts @@ -19,6 +19,7 @@ export * from './lib/directives/focus/focus.directive'; export * from './lib/directives/for-of/for_of.directive'; export * from './lib/directives/layout/layout.directive'; export * from './lib/directives/mask/mask.directive'; +export * from './lib/directives/radio/radio-group.directive'; export * from './lib/directives/ripple/ripple.directive'; export * from './lib/directives/text-highlight/text-highlight.directive'; export * from './lib/directives/text-selection/text-selection.directive'; diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 375e88ed8ea..c78140d35a8 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -140,6 +140,11 @@ export class AppComponent implements OnInit { icon: 'poll', name: 'Progress Indicators' }, + { + link: '/radio', + icon: 'pol', + name: 'Radio Group' + }, { link: '/slider', icon: 'linear_scale', diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 2ca40c1b1fd..3dcdecc6e9e 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,7 +1,7 @@ import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; -import { FormsModule } from '@angular/forms'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { NgModule } from '@angular/core'; import { IgxIconModule, IgxGridModule, IgxExcelExporterService, IgxCsvExporterService } from 'igniteui-angular'; import { SharedModule } from './shared/shared.module'; @@ -48,6 +48,7 @@ import { GridPerformanceSampleComponent } from './grid-performance/grid-performa import { GridSelectionComponent } from './grid-selection/grid-selection.sample'; import { GridVirtualizationSampleComponent } from './grid-remote-virtualization/grid-remote-virtualization.sample'; import { ButtonGroupSampleComponent } from './buttonGroup/buttonGroup.sample'; +import { RadioSampleComponent } from './radio/radio.sample'; const components = [ @@ -91,7 +92,7 @@ const components = [ CustomContentComponent, ColorsSampleComponent, ShadowsSampleComponent, - TypographySampleComponent + RadioSampleComponent ]; @NgModule({ @@ -100,6 +101,7 @@ const components = [ BrowserModule, BrowserAnimationsModule, FormsModule, + ReactiveFormsModule, HttpClientModule, IgxIconModule.forRoot(), IgxGridModule.forRoot(), diff --git a/src/app/radio/radio.sample.css b/src/app/radio/radio.sample.css new file mode 100644 index 00000000000..ce2d920573c --- /dev/null +++ b/src/app/radio/radio.sample.css @@ -0,0 +1,8 @@ +.sample-content { + flex-flow: column nowrap; +} + +.radio-sample { + padding: 6px; +} + diff --git a/src/app/radio/radio.sample.html b/src/app/radio/radio.sample.html new file mode 100644 index 00000000000..e05c2075c58 --- /dev/null +++ b/src/app/radio/radio.sample.html @@ -0,0 +1,68 @@ +
+ + Demonstrates a group of radio buttons. + +
+
+

Radio group without data model

+ + + {{item}} + + + + + Disabled = '{{radioGroup.disabled}}'
+ Required = '{{radioGroup.required}}'
+ SelectedValue = '{{selectedValue}}'
+ +
+
+

Radio group in template-driven form

+
+
+ + +
+
+ + + + {{item}} + + +
+ + {{diagnostic}}
+ +
+
+
+
+

Radio group in reactive form

+ +
+ + + + + {{item}} + + + +
+ +
+

Form value: {{ personKirkForm.value | json }}

+

Model value: {{ personKirk | json }}

+

Updated model: {{ newPerson | json }}

+
+
+
diff --git a/src/app/radio/radio.sample.ts b/src/app/radio/radio.sample.ts new file mode 100644 index 00000000000..cef225cec24 --- /dev/null +++ b/src/app/radio/radio.sample.ts @@ -0,0 +1,79 @@ +import { Component, ViewChild, AfterContentInit } from '@angular/core'; +import { IgxRadioGroupDirective } from 'igniteui-angular'; +import { FormGroup, FormBuilder } from '@angular/forms'; + +class Person { + favoriteSeason: string; + + constructor(public name: string, season?: string) { + if (season) { + this.favoriteSeason = season; + } + } +} + +@Component({ + selector: 'app-radio-sample', + styleUrls: ['radio.sample.css'], + templateUrl: 'radio.sample.html' +}) +export class RadioSampleComponent implements AfterContentInit { + @ViewChild('radioGroupZZ', { read: IgxRadioGroupDirective }) public radioGroup: IgxRadioGroupDirective; + + selectedValue: any; + + seasons = [ + 'Winter', + 'Spring', + 'Summer', + 'Autumn', + ]; + + personBob: Person = new Person('Bob', this.seasons[2]); + + newPerson: Person; + personKirk: Person = new Person('Kirk', this.seasons[1]); + personKirkForm: FormGroup; + + constructor(private _formBuilder: FormBuilder) { + this._createPersonKirkForm(); + } + + get diagnostic() { + return JSON.stringify(this.personBob); + } + + ngAfterContentInit(): void { + setTimeout(() => this.selectedValue = this.radioGroup.value); + } + + onBtnClick(evt) { + this.radioGroup.value = 'Baz'; + } + + onUpdateBtnClick(evt) { + const formModel = this.personKirkForm.value; + + this.newPerson = new Person(formModel.name as string, formModel.favoriteSeason as string); + } + + onRadioChange(evt) { + this.selectedValue = evt.value; + } + + onSubmit() { + this.personBob.favoriteSeason = this.seasons[1]; + } + + private _createPersonKirkForm() { + this.personKirkForm = this._formBuilder.group({ + name: '', + favoriteSeason: '' + }); + + this.personKirkForm.setValue({ + name: this.personKirk.name, + favoriteSeason: this.personKirk.favoriteSeason + }); + } +} diff --git a/src/app/routing.ts b/src/app/routing.ts index 99e80d1e33f..5c9dd53966a 100644 --- a/src/app/routing.ts +++ b/src/app/routing.ts @@ -36,6 +36,7 @@ import { GridPerformanceSampleComponent } from './grid-performance/grid-performa import { GridSelectionComponent } from './grid-selection/grid-selection.sample'; import { GridVirtualizationSampleComponent } from './grid-remote-virtualization/grid-remote-virtualization.sample'; import { ButtonGroupSampleComponent } from './buttonGroup/buttonGroup.sample'; +import { RadioSampleComponent } from './radio/radio.sample'; const appRoutes = [ { @@ -115,6 +116,10 @@ const appRoutes = [ path: 'progressbar', component: ProgressbarSampleComponent }, + { + path: 'radio', + component: RadioSampleComponent + }, { path: 'ripple', component: RippleSampleComponent