mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
committed by
Jeremy Elbourn
parent
ad0c3eb801
commit
d76465b12b
@@ -0,0 +1,129 @@
|
||||
@import "default-theme";
|
||||
|
||||
$md-radio-width: 20px !default;
|
||||
|
||||
// Top-level host container.
|
||||
md-radio-button {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
// Inner label container, wrapping entire element.
|
||||
// Enables focus by click.
|
||||
.md-radio-label {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
padding: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// Container for radio circles and ripple.
|
||||
.md-radio-container {
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
height: $md-radio-width;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
width: $md-radio-width;
|
||||
}
|
||||
|
||||
// TODO(mtlin): Replace when ink ripple component is implemented.
|
||||
// A placeholder ink ripple, shown when keyboard focused.
|
||||
.md-ink-ripple {
|
||||
background-color: md-color($md-accent);
|
||||
border-radius: 50%;
|
||||
height: 48px;
|
||||
left: 10px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
transform: translate(-50%,-50%);
|
||||
transition: opacity ease 0.28s, background-color ease 0.28s;
|
||||
width: 48px;
|
||||
overflow: hidden;
|
||||
|
||||
// Fade in when radio focused.
|
||||
.md-radio-focused & {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
// TODO(mtlin): This corresponds to disabled focus state, but it's unclear how to enter into
|
||||
// this state.
|
||||
.md-radio-disabled & {
|
||||
background: #000;
|
||||
}
|
||||
}
|
||||
|
||||
// The outer circle for the radio, always present.
|
||||
.md-radio-outer-circle {
|
||||
border-color: md-color($md-foreground, secondary-text);
|
||||
border: solid 2px;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
height: $md-radio-width;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transition: border-color ease 0.28s;
|
||||
width: $md-radio-width;
|
||||
|
||||
.md-radio-checked & {
|
||||
border-color: md-color($md-accent);
|
||||
}
|
||||
|
||||
.md-radio-disabled & {
|
||||
border-color: md-color($md-foreground, disabled);
|
||||
}
|
||||
}
|
||||
|
||||
// The inner circle for the radio, shown when checked.
|
||||
.md-radio-inner-circle {
|
||||
background-color: md-color($md-accent);
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
height: $md-radio-width;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transition: transform ease 0.28s, background-color ease 0.28s;
|
||||
transform: scale(0);
|
||||
width: $md-radio-width;
|
||||
|
||||
.md-radio-checked & {
|
||||
transform: scale(0.5);
|
||||
}
|
||||
|
||||
.md-radio-disabled & {
|
||||
background-color: md-color($md-foreground, disabled);
|
||||
}
|
||||
}
|
||||
|
||||
// Text label next to radio.
|
||||
.md-radio-label-content {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
line-height: 24px;
|
||||
// Equal padding on both sides, for RTL.
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
position: relative;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
// Underlying native input element.
|
||||
// Visually hidden but still able to respond to focus.
|
||||
.md-radio-input {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
opacity: 0;
|
||||
appearance: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
// Basic disabled state.
|
||||
.md-radio-disabled, .md-radio-disabled .md-radio-label {
|
||||
cursor: default;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<!-- TODO(jelbourn): render the radio on either side of the content -->
|
||||
<!-- TODO(mtlin): Evaluate trade-offs of using native radio vs. cost of additional bindings. -->
|
||||
<label [attr.for]="inputId" class="md-radio-label">
|
||||
<!-- The actual `radio` part of the control. -->
|
||||
<div class="md-radio-container">
|
||||
<div class="md-radio-outer-circle"></div>
|
||||
<div class="md-radio-inner-circle"></div>
|
||||
<div class="md-ink-ripple"></div>
|
||||
</div>
|
||||
|
||||
<input #input class="md-radio-input" type="radio"
|
||||
[id]="inputId"
|
||||
[checked]="checked"
|
||||
[disabled]="disabled"
|
||||
[name]="name"
|
||||
(change)="onInputChange()"
|
||||
(focus)="onInputFocus()"
|
||||
(blur)="onInputBlur()" />
|
||||
|
||||
<!-- The label content for radio control. -->
|
||||
<div class="md-radio-label-content">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</label>
|
||||
@@ -0,0 +1,292 @@
|
||||
import {
|
||||
fakeAsync,
|
||||
inject,
|
||||
tick,
|
||||
TestComponentBuilder
|
||||
} from 'angular2/testing';
|
||||
import {
|
||||
it,
|
||||
describe,
|
||||
expect,
|
||||
beforeEach,
|
||||
} from '../../core/facade/testing';
|
||||
import {Component, DebugElement} from 'angular2/core';
|
||||
import {By} from 'angular2/platform/browser';
|
||||
|
||||
import {MdRadioButton, MdRadioGroup, MdRadioChange} from './radio_button';
|
||||
import {MdRadioDispatcher} from './radio_dispatcher';
|
||||
|
||||
export function main() {
|
||||
describe('MdRadioButton', () => {
|
||||
let builder: TestComponentBuilder;
|
||||
|
||||
beforeEach(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
|
||||
builder = tcb;
|
||||
}));
|
||||
|
||||
it('should have same name as radio group', (done: () => void) => {
|
||||
builder
|
||||
.overrideTemplate(TestApp, `
|
||||
<md-radio-group name="my_group">
|
||||
<md-radio-button></md-radio-button>
|
||||
</md-radio-group>`)
|
||||
.createAsync(TestApp)
|
||||
.then((fixture) => {
|
||||
let button = fixture.debugElement.query(By.css('md-radio-button'));
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(button.componentInstance.name).toBe('my_group');
|
||||
}).then(done);
|
||||
});
|
||||
|
||||
it('should not allow click selection if disabled', (done: () => void) => {
|
||||
builder
|
||||
.overrideTemplate(TestApp, '<md-radio-button disabled></md-radio-button>')
|
||||
.createAsync(TestApp)
|
||||
.then((fixture) => {
|
||||
let button = fixture.debugElement.query(By.css('md-radio-button'));
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(button.componentInstance.checked).toBe(false);
|
||||
|
||||
button.nativeElement.click();
|
||||
expect(button.componentInstance.checked).toBe(false);
|
||||
}).then(done);
|
||||
});
|
||||
|
||||
it('should be disabled if radio group disabled', (done: () => void) => {
|
||||
builder
|
||||
.overrideTemplate(TestApp, `
|
||||
<md-radio-group disabled>
|
||||
<md-radio-button></md-radio-button>
|
||||
</md-radio-group>`)
|
||||
.createAsync(TestApp)
|
||||
.then((fixture) => {
|
||||
let button = fixture.debugElement.query(By.css('md-radio-button'));
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(button.componentInstance.disabled).toBe(true);
|
||||
}).then(done);
|
||||
});
|
||||
|
||||
it('updates parent group value when selected and value changed', (done: () => void) => {
|
||||
builder
|
||||
.overrideTemplate(TestApp, `
|
||||
<md-radio-group>
|
||||
<md-radio-button value="1"></md-radio-button>
|
||||
</md-radio-group>`)
|
||||
.createAsync(TestApp)
|
||||
.then((fixture) => {
|
||||
let button = fixture.debugElement.query(By.css('md-radio-button'));
|
||||
let group = fixture.debugElement.query(By.css('md-radio-group'));
|
||||
|
||||
group.componentInstance.selected = button.componentInstance;
|
||||
fixture.detectChanges();
|
||||
expect(group.componentInstance.value).toBe('1');
|
||||
|
||||
button.componentInstance.value = '2';
|
||||
fixture.detectChanges();
|
||||
expect(group.componentInstance.value).toBe('2');
|
||||
}).then(done);
|
||||
});
|
||||
|
||||
it('should be checked after input change event', (done: () => void) => {
|
||||
builder
|
||||
.overrideTemplate(TestApp, '<md-radio-button></md-radio-button>')
|
||||
.createAsync(TestApp)
|
||||
.then((fixture) => {
|
||||
let button = fixture.debugElement.query(By.css('md-radio-button'));
|
||||
let input = button.query(By.css('input'));
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(button.componentInstance.checked).toBe(false);
|
||||
|
||||
let event = createEvent('change');
|
||||
input.nativeElement.dispatchEvent(event);
|
||||
expect(button.componentInstance.checked).toBe(true);
|
||||
}).then(done);
|
||||
});
|
||||
|
||||
it('should emit event when checked', (done: () => void) => {
|
||||
builder
|
||||
.overrideTemplate(TestApp, '<md-radio-button></md-radio-button>')
|
||||
.createAsync(TestApp)
|
||||
.then((fixture) => {
|
||||
fakeAsync(function() {
|
||||
let button = fixture.debugElement.query(By.css('md-radio-button'));
|
||||
let changeEvent: MdRadioChange = null;
|
||||
button.componentInstance.change.subscribe((evt: MdRadioChange) => {
|
||||
changeEvent = evt;
|
||||
});
|
||||
button.componentInstance.checked = true;
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
expect(changeEvent).not.toBe(null);
|
||||
expect(changeEvent.source).toBe(button.componentInstance);
|
||||
});
|
||||
}).then(done);
|
||||
});
|
||||
|
||||
it('should be focusable', (done: () => void) => {
|
||||
builder
|
||||
.overrideTemplate(TestApp, '<md-radio-button></md-radio-button>')
|
||||
.createAsync(TestApp)
|
||||
.then((fixture) => {
|
||||
let button = fixture.debugElement.query(By.css('md-radio-button'));
|
||||
let input = button.query(By.css('input'));
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(button.nativeElement.classList.contains('md-radio-focused')).toBe(false);
|
||||
|
||||
let event = createEvent('focus');
|
||||
input.nativeElement.dispatchEvent(event);
|
||||
fixture.detectChanges();
|
||||
expect(button.nativeElement.classList.contains('md-radio-focused')).toBe(true);
|
||||
|
||||
event = createEvent('blur');
|
||||
input.nativeElement.dispatchEvent(event);
|
||||
fixture.detectChanges();
|
||||
expect(button.nativeElement.classList.contains('md-radio-focused')).toBe(false);
|
||||
}).then(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MdRadioDispatcher', () => {
|
||||
let builder: TestComponentBuilder;
|
||||
let dispatcher: MdRadioDispatcher;
|
||||
|
||||
beforeEach(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
|
||||
builder = tcb;
|
||||
dispatcher = new MdRadioDispatcher();
|
||||
}));
|
||||
|
||||
it('notifies listeners', () => {
|
||||
let notificationCount = 0;
|
||||
const numListeners = 2;
|
||||
|
||||
for (let i = 0; i < numListeners; i++) {
|
||||
dispatcher.listen(() => {
|
||||
notificationCount++;
|
||||
});
|
||||
}
|
||||
|
||||
dispatcher.notify('hello');
|
||||
|
||||
expect(notificationCount).toBe(numListeners);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MdRadioGroup', () => {
|
||||
let builder: TestComponentBuilder;
|
||||
|
||||
beforeEach(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
|
||||
builder = tcb;
|
||||
}));
|
||||
|
||||
it('can select by value', (done: () => void) => {
|
||||
builder
|
||||
.overrideTemplate(TestApp, `
|
||||
<md-radio-group>
|
||||
<md-radio-button value="1"></md-radio-button>
|
||||
<md-radio-button value="2"></md-radio-button>
|
||||
</md-radio-group>`)
|
||||
.createAsync(TestApp)
|
||||
.then((fixture) => {
|
||||
let buttons = fixture.debugElement.queryAll(By.css('md-radio-button'));
|
||||
let group = fixture.debugElement.query(By.css('md-radio-group'));
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(group.componentInstance.selected).toBe(null);
|
||||
|
||||
group.componentInstance.value = '2';
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(group.componentInstance.selected).toBe(buttons[1].componentInstance);
|
||||
}).then(done);
|
||||
});
|
||||
|
||||
it('should select uniquely', (done: () => void) => {
|
||||
builder
|
||||
.overrideTemplate(TestApp, `
|
||||
<md-radio-group>
|
||||
<md-radio-button></md-radio-button>
|
||||
<md-radio-button></md-radio-button>
|
||||
</md-radio-group>`)
|
||||
.createAsync(TestApp)
|
||||
.then((fixture) => {
|
||||
let buttons = fixture.debugElement.queryAll(By.css('md-radio-button'));
|
||||
let group = fixture.debugElement.query(By.css('md-radio-group'));
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(group.componentInstance.selected).toBe(null);
|
||||
|
||||
group.componentInstance.selected = buttons[0].componentInstance;
|
||||
fixture.detectChanges();
|
||||
expect(isSinglySelected(buttons[0], buttons)).toBe(true);
|
||||
|
||||
group.componentInstance.selected = buttons[1].componentInstance;
|
||||
fixture.detectChanges();
|
||||
expect(isSinglySelected(buttons[1], buttons)).toBe(true);
|
||||
}).then(done);
|
||||
});
|
||||
|
||||
it('should emit event when value changes', (done: () => void) => {
|
||||
builder
|
||||
.overrideTemplate(TestApp, `
|
||||
<md-radio-group>
|
||||
<md-radio-button></md-radio-button>
|
||||
<md-radio-button></md-radio-button>
|
||||
</md-radio-group>`)
|
||||
.createAsync(TestApp)
|
||||
.then((fixture) => {
|
||||
fakeAsync(function() {
|
||||
let buttons = fixture.debugElement.queryAll(By.css('md-radio-button'));
|
||||
let group = fixture.debugElement.query(By.css('md-radio-group'));
|
||||
|
||||
let changeEvent: MdRadioChange = null;
|
||||
group.componentInstance.change.subscribe((evt: MdRadioChange) => {
|
||||
changeEvent = evt;
|
||||
});
|
||||
|
||||
group.componentInstance.selected = buttons[1].componentInstance;
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
expect(changeEvent).not.toBe(null);
|
||||
expect(changeEvent.source).toBe(buttons[1].componentInstance);
|
||||
});
|
||||
}).then(done);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Checks whether a given button is uniquely selected from a group of buttons. */
|
||||
function isSinglySelected(button: DebugElement, buttons: DebugElement[]): boolean {
|
||||
let component = button.componentInstance;
|
||||
let otherSelectedButtons =
|
||||
buttons.filter((e: DebugElement) =>
|
||||
e.componentInstance != component && e.componentInstance.checked);
|
||||
return component.checked && otherSelectedButtons.length == 0;
|
||||
}
|
||||
|
||||
/** Browser-agnostic function for creating an event. */
|
||||
function createEvent(name: string): Event {
|
||||
let ev: Event;
|
||||
try {
|
||||
ev = createEvent(name);
|
||||
} catch (e) {
|
||||
ev = document.createEvent('Event');
|
||||
ev.initEvent(name, true, true);
|
||||
}
|
||||
return ev;
|
||||
}
|
||||
|
||||
|
||||
/** Test component. */
|
||||
@Component({
|
||||
directives: [MdRadioButton, MdRadioGroup],
|
||||
providers: [MdRadioDispatcher],
|
||||
template: ''
|
||||
})
|
||||
class TestApp {}
|
||||
@@ -0,0 +1,306 @@
|
||||
import {
|
||||
AfterContentInit,
|
||||
Component,
|
||||
ContentChildren,
|
||||
Directive,
|
||||
EventEmitter,
|
||||
HostBinding,
|
||||
HostListener,
|
||||
Input,
|
||||
OnInit,
|
||||
Optional,
|
||||
Output,
|
||||
QueryList,
|
||||
ViewEncapsulation,
|
||||
forwardRef
|
||||
} from 'angular2/core';
|
||||
|
||||
import {Event} from 'angular2/src/facade/browser';
|
||||
|
||||
import {MdRadioDispatcher} from './radio_dispatcher';
|
||||
|
||||
// TODO(mtlin):
|
||||
// Ink ripple is currently placeholder.
|
||||
// Determine motion spec for button transitions.
|
||||
// Design review.
|
||||
// RTL
|
||||
// Support forms API.
|
||||
// Use ChangeDetectionStrategy.OnPush
|
||||
|
||||
var _uniqueIdCounter = 0;
|
||||
|
||||
/** A simple change event emitted by either MdRadioButton or MdRadioGroup. */
|
||||
export class MdRadioChange {
|
||||
source: MdRadioButton;
|
||||
value: any;
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: 'md-radio-group',
|
||||
host: {
|
||||
'role': 'radiogroup',
|
||||
},
|
||||
})
|
||||
export class MdRadioGroup implements AfterContentInit {
|
||||
/** The value for the radio group. Should match currently selected button. */
|
||||
private _value: any = null;
|
||||
|
||||
/** The HTML name attribute applied to radio buttons in this group. */
|
||||
private _name: string = null;
|
||||
|
||||
/** Disables all individual radio buttons assigned to this group. */
|
||||
private _disabled: boolean = false;
|
||||
|
||||
/** The currently selected radio button. Should match value. */
|
||||
private _selected: MdRadioButton = null;
|
||||
|
||||
/** Event emitted when the group value changes. */
|
||||
@Output()
|
||||
change: EventEmitter<MdRadioChange> = new EventEmitter();
|
||||
|
||||
/** Child radio buttons. */
|
||||
@ContentChildren(forwardRef(() => MdRadioButton))
|
||||
private _radios: QueryList<MdRadioButton> = null;
|
||||
|
||||
/**
|
||||
* Initialize properties once content children are available.
|
||||
* This allows us to propagate relevant attributes to associated buttons.
|
||||
*/
|
||||
ngAfterContentInit() {
|
||||
if (this._name == null) {
|
||||
this.name = `md-radio-group-${_uniqueIdCounter++}`;
|
||||
} else {
|
||||
this._updateChildRadioNames();
|
||||
}
|
||||
}
|
||||
|
||||
@Input()
|
||||
get name(): string {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
set name(value: string) {
|
||||
this._name = value;
|
||||
|
||||
this._updateChildRadioNames();
|
||||
}
|
||||
|
||||
/** Propagate name attribute to radio buttons. */
|
||||
private _updateChildRadioNames(): void {
|
||||
if (this._radios != null) {
|
||||
this._radios.forEach((radio) => {
|
||||
radio.name = this._name;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Input()
|
||||
get disabled(): boolean {
|
||||
return this._disabled;
|
||||
}
|
||||
|
||||
set disabled(value) {
|
||||
// The presence of *any* disabled value makes the component disabled, *except* for false.
|
||||
this._disabled = (value != null && value !== false) ? true : null;
|
||||
}
|
||||
|
||||
@Input()
|
||||
get value(): any {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
set value(newValue: any) {
|
||||
if (this._value != newValue) {
|
||||
// Set this before proceeding to ensure no circular loop occurs with selection.
|
||||
this._value = newValue;
|
||||
|
||||
this._updateSelectedRadioFromValue();
|
||||
this._emitChangeEvent();
|
||||
}
|
||||
}
|
||||
|
||||
private _updateSelectedRadioFromValue(): void {
|
||||
// Update selected if different from current value.
|
||||
let isAlreadySelected = this._selected != null && this._selected.value == this._value;
|
||||
if (this._radios != null && !isAlreadySelected) {
|
||||
let matched = this._radios.filter((radio) => {
|
||||
return radio.value == this._value;
|
||||
});
|
||||
|
||||
if (matched.length == 0) {
|
||||
// Didn't find a button that matches this value, return early without setting.
|
||||
return;
|
||||
}
|
||||
|
||||
// Change the selection immediately.
|
||||
this.selected = matched[0];
|
||||
}
|
||||
}
|
||||
|
||||
/** Dispatch change event with current selection and group value. */
|
||||
private _emitChangeEvent(): void {
|
||||
let event = new MdRadioChange();
|
||||
event.source = this._selected;
|
||||
event.value = this._value;
|
||||
this.change.emit(event);
|
||||
}
|
||||
|
||||
@Input()
|
||||
get selected() {
|
||||
return this._selected;
|
||||
}
|
||||
|
||||
set selected(selected: MdRadioButton) {
|
||||
this._selected = selected;
|
||||
this.value = selected.value;
|
||||
|
||||
selected.checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'md-radio-button',
|
||||
templateUrl: './components/radio/radio_button.html',
|
||||
styleUrls: ['./components/radio/radio-button.css'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class MdRadioButton implements OnInit {
|
||||
@HostBinding('class.md-radio-focused')
|
||||
private _isFocused: boolean;
|
||||
|
||||
/** Whether this radio is checked. */
|
||||
private _checked: boolean = false;
|
||||
|
||||
/** The unique ID for the radio button. */
|
||||
@HostBinding('id')
|
||||
@Input()
|
||||
id: string;
|
||||
|
||||
/** Analog to HTML 'name' attribute used to group radios for unique selection. */
|
||||
@Input()
|
||||
name: string;
|
||||
|
||||
/** Whether this radio is disabled. */
|
||||
private _disabled: boolean;
|
||||
|
||||
/** Value assigned to this radio.*/
|
||||
private _value: any = null;
|
||||
|
||||
/** The parent radio group. May or may not be present. */
|
||||
radioGroup: MdRadioGroup;
|
||||
|
||||
/** Event emitted when the group value changes. */
|
||||
@Output()
|
||||
change: EventEmitter<MdRadioChange> = new EventEmitter();
|
||||
|
||||
constructor(@Optional() radioGroup: MdRadioGroup, public radioDispatcher: MdRadioDispatcher) {
|
||||
// Assertions. Ideally these should be stripped out by the compiler.
|
||||
// TODO(jelbourn): Assert that there's no name binding AND a parent radio group.
|
||||
|
||||
this.radioGroup = radioGroup;
|
||||
|
||||
radioDispatcher.listen((name: string) => {
|
||||
if (name == this.name) {
|
||||
this.checked = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.id == null) {
|
||||
this.id = `md-radio-${_uniqueIdCounter++}`;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* We use a hidden native input field to handle changes to focus state via keyboard navigation,
|
||||
* with visual rendering done separately. The native element is kept in sync with the overall
|
||||
* state of the component.
|
||||
*/
|
||||
onInputFocus() {
|
||||
this._isFocused = true;
|
||||
}
|
||||
|
||||
onInputBlur() {
|
||||
this._isFocused = false;
|
||||
}
|
||||
|
||||
/** Input change handler, called only on keyboard selection. */
|
||||
onInputChange() {
|
||||
this.checked = true;
|
||||
}
|
||||
|
||||
get inputId(): string {
|
||||
return `${this.id}-input`;
|
||||
}
|
||||
|
||||
@HostBinding('class.md-radio-checked')
|
||||
@Input()
|
||||
get checked(): boolean {
|
||||
return this._checked;
|
||||
}
|
||||
|
||||
set checked(value: boolean) {
|
||||
if (value) {
|
||||
// Notify all radio buttons with the same name to un-check.
|
||||
this.radioDispatcher.notify(this.name);
|
||||
|
||||
if (!this._checked) {
|
||||
this._emitChangeEvent();
|
||||
}
|
||||
}
|
||||
this._checked = value;
|
||||
}
|
||||
|
||||
/** MdRadioGroup reads this to assign its own value. */
|
||||
@Input()
|
||||
get value(): any {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
set value(value: any) {
|
||||
if (this._value != value) {
|
||||
if (this.radioGroup != null && this.checked) {
|
||||
this.radioGroup.value = value;
|
||||
}
|
||||
this._value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Dispatch change event with current value. */
|
||||
private _emitChangeEvent(): void {
|
||||
let event = new MdRadioChange();
|
||||
event.source = this;
|
||||
event.value = this._value;
|
||||
this.change.emit(event);
|
||||
}
|
||||
|
||||
@HostBinding('class.md-radio-disabled')
|
||||
@Input()
|
||||
get disabled(): boolean {
|
||||
return this._disabled || (this.radioGroup != null && this.radioGroup.disabled);
|
||||
}
|
||||
|
||||
set disabled(value: boolean) {
|
||||
// The presence of *any* disabled value makes the component disabled, *except* for false.
|
||||
this._disabled = (value != null && value !== false) ? true : null;
|
||||
}
|
||||
|
||||
@HostListener('click', ['$event'])
|
||||
onClick(event: Event) {
|
||||
if (this.disabled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.radioGroup != null) {
|
||||
// Propagate the change one-way via the group, which will in turn mark this
|
||||
// button as checked.
|
||||
this.radioGroup.selected = this;
|
||||
} else {
|
||||
this.checked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import {Injectable} from 'angular2/core';
|
||||
|
||||
/**
|
||||
* Class for radio buttons to coordinate unique selection based on name.
|
||||
* Intended to be consumed as an Angular service.
|
||||
* This service is needed because native radio change events are only fired on the item currently
|
||||
* being selected, and we still need to uncheck the previous selection.
|
||||
*/
|
||||
@Injectable()
|
||||
export class MdRadioDispatcher {
|
||||
// TODO(jelbourn): Change this to TypeScript syntax when supported.
|
||||
private _listeners: Function[];
|
||||
|
||||
constructor() {
|
||||
this._listeners = [];
|
||||
}
|
||||
|
||||
/** Notify other radio buttons that selection for the given name has been set. */
|
||||
notify(name: string) {
|
||||
this._listeners.forEach(listener => listener(name));
|
||||
}
|
||||
|
||||
/** Listen for future changes to radio button selection. */
|
||||
listen(listener: (name: string) => void) {
|
||||
this._listeners.push(listener);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
<li><a [routerLink]="['PortalDemo']">Portal demo</a></li>
|
||||
<li><a [routerLink]="['CheckboxDemo']">Checkbox demo</a></li>
|
||||
<li><a [routerLink]="['ToolbarDemo']">Toolbar demo</a></li>
|
||||
<li><a [routerLink]="['RadioDemo']">Radio demo</a></li>
|
||||
</ul>
|
||||
<button md-raised-button (click)="root.dir = (root.dir == 'rtl' ? 'ltr' : 'rtl')">
|
||||
{{root.dir.toUpperCase()}}
|
||||
|
||||
@@ -2,6 +2,7 @@ import {Component} from 'angular2/core';
|
||||
import {Route, RouteConfig, ROUTER_DIRECTIVES} from 'angular2/router';
|
||||
import {CardDemo} from './card/card-demo';
|
||||
import {ButtonDemo} from './button/button-demo';
|
||||
import {RadioDemo} from './radio/radio-demo';
|
||||
import {SidenavDemo} from './sidenav/sidenav-demo';
|
||||
import {ProgressCircleDemo} from './progress-circle/progress-circle-demo';
|
||||
import {CheckboxDemo} from './checkbox/checkbox-demo';
|
||||
@@ -29,6 +30,7 @@ export class Home {}
|
||||
new Route({path: '/', name: 'Home', component: Home, useAsDefault: true}),
|
||||
new Route({path: '/button', name: 'ButtonDemo', component: ButtonDemo}),
|
||||
new Route({path: '/card', name: 'CardDemo', component: CardDemo}),
|
||||
new Route({path: '/radio', name: 'RadioDemo', component: RadioDemo}),
|
||||
new Route({path: '/sidenav', name: 'SidenavDemo', component: SidenavDemo}),
|
||||
new Route({path: '/progress-circle', name: 'ProgressCircleDemo', component: ProgressCircleDemo}),
|
||||
new Route({path: '/portal', name: 'PortalDemo', component: PortalDemo}),
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<section class="demo-section">
|
||||
<md-radio-button name="group1">Option 1</md-radio-button>
|
||||
<md-radio-button name="group1">Option 2</md-radio-button>
|
||||
<md-radio-button name="group1" disabled="true">Option 3 (disabled)</md-radio-button>
|
||||
</section>
|
||||
<section class="demo-section">
|
||||
<div>
|
||||
<span>isDisabled: {{isDisabled}}</span>
|
||||
<button md-raised-button (click)="isDisabled=!isDisabled" class="demo-button">
|
||||
Disable buttons
|
||||
</button>
|
||||
</div>
|
||||
<md-radio-group name="my_options" [disabled]="isDisabled">
|
||||
<md-radio-button value="option_1">Option 1</md-radio-button>
|
||||
<md-radio-button value="option_2">Option 2</md-radio-button>
|
||||
<md-radio-button value="option_3">Option 3</md-radio-button>
|
||||
</md-radio-group>
|
||||
</section>
|
||||
@@ -0,0 +1,10 @@
|
||||
.demo-button {
|
||||
margin: 8px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
background-color: #f7f7f7;
|
||||
margin: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import {Component} from 'angular2/core';
|
||||
import {MdRadioButton, MdRadioGroup} from '../../components/radio/radio_button';
|
||||
import {MdRadioDispatcher} from '../../components/radio/radio_dispatcher';
|
||||
|
||||
@Component({
|
||||
selector: 'radio-demo',
|
||||
templateUrl: 'demo-app/radio/radio-demo.html',
|
||||
styleUrls: ['demo-app/radio/radio-demo.css'],
|
||||
providers: [MdRadioDispatcher],
|
||||
directives: [MdRadioButton, MdRadioGroup]
|
||||
})
|
||||
export class RadioDemo {
|
||||
isDisabled: boolean = false;
|
||||
}
|
||||
Reference in New Issue
Block a user