refactor(radio): query buttons via @ContentChildren

This commit is contained in:
Adam Bradley
2015-12-19 00:27:30 -06:00
parent 508a872251
commit 1c718b9213
4 changed files with 171 additions and 194 deletions

View File

@ -1,10 +1,10 @@
import {Directive, ElementRef, NgZone} from 'angular2/core'; import {Directive, ElementRef, Renderer, Attribute, NgZone} from 'angular2/core';
import {Ion} from '../ion'; import {Ion} from '../ion';
import {Config} from '../../config/config'; import {Config} from '../../config/config';
import {ListVirtualScroll} from './virtual'; import {ListVirtualScroll} from './virtual';
import {ItemSlidingGesture} from '../item/item-sliding-gesture'; import {ItemSlidingGesture} from '../item/item-sliding-gesture';
import * as util from '../../util'; import {isDefined} from '../../util';
/** /**
* The List is a widely used interface element in almost any mobile app, and can include * The List is a widely used interface element in almost any mobile app, and can include
@ -42,7 +42,7 @@ export class List extends Ion {
ngOnInit() { ngOnInit() {
super.ngOnInit(); super.ngOnInit();
if (util.isDefined(this.virtual)) { if (isDefined(this.virtual)) {
console.log('Content', this.content); console.log('Content', this.content);
console.log('Virtual?', this.virtual); console.log('Virtual?', this.virtual);
console.log('Items?', this.items.length, 'of \'em'); console.log('Items?', this.items.length, 'of \'em');
@ -138,12 +138,21 @@ export class List extends Ion {
* @private * @private
*/ */
@Directive({ @Directive({
selector: 'ion-list-header', selector: 'ion-list-header'
inputs: [
'id'
],
host: {
'[attr.id]': 'id'
}
}) })
export class ListHeader {} export class ListHeader {
constructor(private _renderer: Renderer, private _elementRef: ElementRef, @Attribute('id') id:string){
this._id = id;
}
public get id() {
return this._id;
}
public set id(val) {
this._id = val;
this._renderer.setElementAttribute(this._elementRef, 'id', val);
}
}

View File

@ -1,10 +1,84 @@
import {Component, Directive, ElementRef, Host, Optional, Query, QueryList} from 'angular2/core'; import {Component, Directive, ElementRef, Renderer, Optional, Input, Output, HostListener, ContentChildren, ContentChild, EventEmitter} from 'angular2/core';
import {NgControl} from 'angular2/common'; import {NgControl} from 'angular2/common';
import {Config} from '../../config/config';
import {Ion} from '../ion';
import {ListHeader} from '../list/list'; import {ListHeader} from '../list/list';
import {Form} from '../../util/form'; import {Form} from '../../util/form';
import {isDefined} from '../../util/util';
/**
* @description
* A radio button with a unique value. Note that all `<ion-radio>` components
* must be wrapped within a `<ion-list radio-group>`, and there must be at
* least two `<ion-radio>` components within the radio group.
*
* See the [Angular 2 Docs](https://angular.io/docs/js/latest/api/forms/) for more info on forms and input.
*
* @usage
* ```html
* <ion-radio value="my-value" checked="true">
* Radio Label
* </ion-radio>
* ```
* @demo /docs/v2/demos/radio/
* @see {@link /docs/v2/components#radio Radio Component Docs}
*/
@Component({
selector: 'ion-radio',
host: {
'[attr.id]': 'id',
'[attr.aria-disabled]': 'disabled',
'[attr.aria-labelledby]': 'labelId',
'class': 'item',
'role': 'radio',
'tappable': '',
'tabindex': '0'
},
template:
'<div class="item-inner">' +
'<ion-item-content id="{{labelId}}">' +
'<ng-content></ng-content>' +
'</ion-item-content>' +
'<div class="radio-media">' +
'<div class="radio-icon"></div>' +
'</div>' +
'</div>'
})
export class RadioButton {
@Input() value: string = '';
@Input() checked: boolean = false;
@Input() disabled: boolean = false;
@Input() id: string;
@Output() select: EventEmitter<any> = new EventEmitter();
constructor(private _form: Form, private _renderer: Renderer, private _elementRef: ElementRef) {
this.isChecked = this.checked;
_renderer.setElementAttribute(_elementRef, 'checked', null);
}
/**
* @private
*/
ngOnInit() {
if (!this.id) {
this.id = 'rb-' + this._form.nextId();
}
this.labelId = 'lbl-' + this.id;
}
/**
* @private
*/
@HostListener('click', ['$event'])
private onClick(ev) {
console.debug('RadioButton, select', this.value);
this.select.emit(ev, this.value);
}
public set isChecked(isChecked) {
this._renderer.setElementAttribute(this._elementRef, 'aria-checked', isChecked);
}
}
/** /**
@ -18,26 +92,34 @@ import {Form} from '../../util/form';
* *
* @usage * @usage
* ```html * ```html
* <ion-list radio-group ngControl="clientside"> * <ion-list radio-group ngControl="autoManufactures">
* *
* <ion-list-header> * <ion-list-header>
* Clientside * Auto Manufactures
* </ion-list-header> * </ion-list-header>
* *
* <ion-radio value="ember"> * <ion-radio value="cord">
* Ember * Cord
* </ion-radio> * </ion-radio>
* *
* <ion-radio value="angular1"> * <ion-radio value="duesenberg" checked="true">
* Angular 1 * Duesenberg
* </ion-radio> * </ion-radio>
* *
* <ion-radio value="angular2" checked="true"> * <ion-radio value="hudson">
* Angular 2 * Hudson
* </ion-radio> * </ion-radio>
* *
* <ion-radio value="react"> * <ion-radio value="packard">
* React * Packard
* </ion-radio>
*
* <ion-radio value="studebaker">
* Studebaker
* </ion-radio>
*
* <ion-radio value="tucker">
* Tucker
* </ion-radio> * </ion-radio>
* *
* </ion-list> * </ion-list>
@ -48,79 +130,27 @@ import {Form} from '../../util/form';
@Directive({ @Directive({
selector: '[radio-group]', selector: '[radio-group]',
host: { host: {
'role': 'radiogroup',
'[attr.aria-activedescendant]': 'activeId', '[attr.aria-activedescendant]': 'activeId',
'[attr.aria-describedby]': 'describedById', 'role': 'radiogroup'
} }
}) })
export class RadioGroup extends Ion { export class RadioGroup {
radios: Array<RadioButton> = []; @Output() change: EventEmitter<any> = new EventEmitter();
@ContentChildren(RadioButton) private _buttons;
@ContentChild(ListHeader) private _header;
constructor( constructor(@Optional() ngControl: NgControl, private _renderer: Renderer, private _elementRef: ElementRef) {
elementRef: ElementRef,
config: Config,
@Optional() ngControl: NgControl,
@Query(ListHeader) private _headerQuery: QueryList<ListHeader>
) {
super(elementRef, config);
this.ngControl = ngControl; this.ngControl = ngControl;
this.id = ++radioGroupIds; this.id = ++radioGroupIds;
this.radioIds = -1; this.radioIds = -1;
this.onChange = (_) => {}; this.onChange = (_) => {};
this.onTouched = (_) => {}; this.onTouched = (_) => {};
if (ngControl) this.ngControl.valueAccessor = this; if (ngControl) {
} this.ngControl.valueAccessor = this;
/**
* @private
*/
ngOnInit() {
let header = this._headerQuery.first;
if (header) {
if (!header.id) {
header.id = 'radio-header-' + this.id;
}
this.describedById = header.id;
} }
} }
/**
* @private
* Register the specified radio button with the radio group.
* @param {RadioButton} radio The radio button to register.
*/
registerRadio(radio) {
radio.id = radio.id || ('radio-' + this.id + '-' + (++this.radioIds));
this.radios.push(radio);
if (this.value == radio.value) {
radio.check(this.value);
}
if (radio.checked) {
this.value = radio.value;
this.onChange(this.value);
this.activeId = radio.id;
}
}
/**
* @private
* Update which radio button in the group is checked, unchecking all others.
* @param {RadioButton} checkedRadio The radio button to check.
*/
update(checkedRadio) {
this.value = checkedRadio.value;
this.activeId = checkedRadio.id;
for (let radio of this.radios) {
radio.checked = (radio === checkedRadio);
}
this.onChange(this.value);
}
/** /**
* @private * @private
* Angular2 Forms API method called by the model (Control) on change to update * Angular2 Forms API method called by the model (Control) on change to update
@ -128,9 +158,46 @@ export class RadioGroup extends Ion {
* https://github.com/angular/angular/blob/master/modules/angular2/src/forms/directives/shared.ts#L34 * https://github.com/angular/angular/blob/master/modules/angular2/src/forms/directives/shared.ts#L34
*/ */
writeValue(value) { writeValue(value) {
this.value = value; this.value = isDefined(value) ? value : '';
for (let radio of this.radios) { if (this._buttons) {
radio.checked = (radio.value == value); let buttons = this._buttons.toArray();
for (let button of buttons) {
let isChecked = (button.value === this.value);
button.isChecked = isChecked;
if (isChecked) {
this._renderer.setElementAttribute(this._elementRef, 'aria-activedescendant', button.id);
}
}
}
}
/**
* @private
*/
ngAfterContentInit() {
let header = this._header;
if (header) {
if (!header.id) {
header.id = 'rg-hdr-' + this.id;
}
this._renderer.setElementAttribute(this._elementRef, 'aria-describedby', header.id);
}
let buttons = this._buttons.toArray();
for (let button of buttons) {
button.select.subscribe(() => {
this.writeValue(button.value);
this.onChange(button.value);
this.change.emit(this.value);
});
if (isDefined(this.value)) {
let isChecked = (button.value === this.value) || button.checked;
button.isChecked = isChecked;
if (isChecked) {
this._renderer.setElementAttribute(this._elementRef, 'aria-activedescendant', button.id);
}
}
} }
} }
@ -152,100 +219,4 @@ export class RadioGroup extends Ion {
registerOnTouched(fn) { this.onTouched = fn; } registerOnTouched(fn) { this.onTouched = fn; }
} }
/**
* @description
* A single radio component.
*
* See the [Angular 2 Docs](https://angular.io/docs/js/latest/api/forms/) for more info on forms and input.
*
* @usage
* ```html
* <ion-radio value="isChecked" checked="true">
* Radio Label
* </ion-radio>
* ```
* @demo /docs/v2/demos/radio/
* @see {@link /docs/v2/components#radio Radio Component Docs}
*/
@Component({
selector: 'ion-radio',
inputs: [
'value',
'checked',
'disabled',
'id'
],
host: {
'role': 'radio',
'tappable': '',
'[attr.id]': 'id',
'[tabindex]': 'tabIndex',
'[attr.aria-checked]': 'checked',
'[attr.aria-disabled]': 'disabled',
'[attr.aria-labelledby]': 'labelId',
'(click)': 'click($event)',
'class': 'item'
},
template:
'<div class="item-inner">' +
'<ion-item-content id="{{labelId}}">' +
'<ng-content></ng-content>' +
'</ion-item-content>' +
'<div class="radio-media">' +
'<div class="radio-icon"></div>' +
'</div>' +
'</div>'
})
export class RadioButton extends Ion {
constructor(
@Host() @Optional() group: RadioGroup,
elementRef: ElementRef,
config: Config,
private _form: Form
) {
super(elementRef, config);
this.group = group;
this.tabIndex = 0;
}
/**
* @private
*/
ngOnInit() {
super.ngOnInit();
if (!this.id) {
this.id = 'rb-' + this._form.nextId();
}
this.labelId = 'lbl-' + this.id;
if (this.group) {
this.group.registerRadio(this);
} else {
console.error('<ion-radio> must be within a <ion-list radio-group>');
}
}
/**
* @private
*/
click(ev) {
ev.preventDefault();
ev.stopPropagation();
this.check();
}
/**
* Update the checked state of this radio button.
* TODO: Call this toggle? Since unchecks as well
*/
check() {
this.checked = !this.checked;
this.group.update(this);
}
}
let radioGroupIds = -1; let radioGroupIds = -1;

View File

@ -45,7 +45,7 @@
<form (submit)="doSubmit($event)" [ngFormModel]="currencyForm"> <form (submit)="doSubmit($event)" [ngFormModel]="currencyForm">
<ion-list radio-group ngControl="currenciesControl"> <ion-list radio-group ngControl="currenciesControl">
<ion-list-header> <ion-list-header id="currencies">
Currencies Currencies
</ion-list-header> </ion-list-header>
<ion-radio *ngFor="#currency of currencies" [checked]="currency==selectedCurrency" [value]="currency">{{currency}}</ion-radio> <ion-radio *ngFor="#currency of currencies" [checked]="currency==selectedCurrency" [value]="currency">{{currency}}</ion-radio>

View File

@ -1,7 +1,6 @@
import {Directive, Renderer, ElementRef, Host, Optional, EventEmitter, HostBinding, Input, Output, ContentChildren, HostListener} from 'angular2/core'; import {Directive, ElementRef, Renderer, Optional, EventEmitter, Input, Output, HostListener, ContentChildren} from 'angular2/core';
import {NgControl} from 'angular2/common'; import {NgControl} from 'angular2/common';
import {Config} from '../../config/config';
import {isDefined} from '../../util/util'; import {isDefined} from '../../util/util';
@ -55,7 +54,7 @@ import {isDefined} from '../../util/util';
} }
}) })
export class SegmentButton { export class SegmentButton {
@Input() value: string @Input() value: string;
@Output() select: EventEmitter<any> = new EventEmitter(); @Output() select: EventEmitter<any> = new EventEmitter();
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {} constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
@ -133,9 +132,7 @@ export class SegmentButton {
}) })
export class Segment { export class Segment {
@Output() change: EventEmitter<any> = new EventEmitter(); @Output() change: EventEmitter<any> = new EventEmitter();
@ContentChildren(SegmentButton) _buttons;
@ContentChildren(SegmentButton) buttons;
value: any; value: any;
constructor( constructor(
@ -154,9 +151,9 @@ export class Segment {
* Write a new value to the element. * Write a new value to the element.
*/ */
writeValue(value) { writeValue(value) {
this.value = !value ? '' : value; this.value = isDefined(value) ? value : '';
if (this.buttons) { if (this._buttons) {
let buttons = this.buttons.toArray(); let buttons = this._buttons.toArray();
for (let button of buttons) { for (let button of buttons) {
button.isActive = (button.value === this.value); button.isActive = (button.value === this.value);
} }
@ -167,7 +164,7 @@ export class Segment {
* @private * @private
*/ */
ngAfterViewInit() { ngAfterViewInit() {
let buttons = this.buttons.toArray(); let buttons = this._buttons.toArray();
for (let button of buttons) { for (let button of buttons) {
button.select.subscribe(() => { button.select.subscribe(() => {
this.writeValue(button.value); this.writeValue(button.value);