mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat: add change emitters to the Intl providers (#5867)
Since we're switching all components to OnPush change detection, they won't necessarily react to dynamic changes in the i18n labels. These changes add an `EventEmitter` per provider that allow for components to react to those types of changes. Note that it's up to the consumer to call `provider.changes.emit()` in order to trigger the re-render. Fixes #5738.
This commit is contained in:
committed by
Andrew Seguin
parent
8edbe47e37
commit
0a5489f9c4
@@ -1,4 +1,4 @@
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {async, ComponentFixture, inject, TestBed} from '@angular/core/testing';
|
||||
import {Component} from '@angular/core';
|
||||
import {MdCalendar} from './calendar';
|
||||
import {By} from '@angular/platform-browser';
|
||||
@@ -148,6 +148,18 @@ describe('MdCalendar', () => {
|
||||
expect(testComponent.selected).toEqual(new Date(2017, JAN, 31));
|
||||
});
|
||||
|
||||
it('should re-render when the i18n labels have changed',
|
||||
inject([MdDatepickerIntl], (intl: MdDatepickerIntl) => {
|
||||
const button = fixture.debugElement.nativeElement
|
||||
.querySelector('.mat-calendar-period-button');
|
||||
|
||||
intl.switchToYearViewLabel = 'Go to year view?';
|
||||
intl.changes.emit();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(button.getAttribute('aria-label')).toBe('Go to year view?');
|
||||
}));
|
||||
|
||||
describe('a11y', () => {
|
||||
describe('calendar body', () => {
|
||||
let calendarBodyEl: HTMLElement;
|
||||
|
||||
@@ -17,7 +17,9 @@ import {
|
||||
NgZone,
|
||||
Optional,
|
||||
Output,
|
||||
ViewEncapsulation
|
||||
ViewEncapsulation,
|
||||
ChangeDetectorRef,
|
||||
OnDestroy,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
DOWN_ARROW,
|
||||
@@ -36,6 +38,7 @@ import {createMissingDateImplError} from './datepicker-errors';
|
||||
import {MD_DATE_FORMATS, MdDateFormats} from '../core/datetime/date-formats';
|
||||
import {MATERIAL_COMPATIBILITY_MODE} from '../core';
|
||||
import {first} from '../core/rxjs/index';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
|
||||
|
||||
/**
|
||||
@@ -53,7 +56,9 @@ import {first} from '../core/rxjs/index';
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MdCalendar<D> implements AfterContentInit {
|
||||
export class MdCalendar<D> implements AfterContentInit, OnDestroy {
|
||||
private _intlChanges: Subscription;
|
||||
|
||||
/** A date representing the period (month or year) to start the calendar in. */
|
||||
@Input() startAt: D;
|
||||
|
||||
@@ -123,13 +128,18 @@ export class MdCalendar<D> implements AfterContentInit {
|
||||
private _ngZone: NgZone,
|
||||
@Optional() @Inject(MATERIAL_COMPATIBILITY_MODE) public _isCompatibilityMode: boolean,
|
||||
@Optional() private _dateAdapter: DateAdapter<D>,
|
||||
@Optional() @Inject(MD_DATE_FORMATS) private _dateFormats: MdDateFormats) {
|
||||
@Optional() @Inject(MD_DATE_FORMATS) private _dateFormats: MdDateFormats,
|
||||
changeDetectorRef: ChangeDetectorRef) {
|
||||
|
||||
if (!this._dateAdapter) {
|
||||
throw createMissingDateImplError('DateAdapter');
|
||||
}
|
||||
|
||||
if (!this._dateFormats) {
|
||||
throw createMissingDateImplError('MD_DATE_FORMATS');
|
||||
}
|
||||
|
||||
this._intlChanges = _intl.changes.subscribe(() => changeDetectorRef.markForCheck());
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
@@ -138,6 +148,10 @@ export class MdCalendar<D> implements AfterContentInit {
|
||||
this._monthView = this.startView != 'year';
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._intlChanges.unsubscribe();
|
||||
}
|
||||
|
||||
/** Handles date selection in the month view. */
|
||||
_dateSelected(date: D): void {
|
||||
if (!this._dateAdapter.sameDate(date, this.selected)) {
|
||||
|
||||
@@ -6,12 +6,18 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Injectable, EventEmitter} from '@angular/core';
|
||||
|
||||
|
||||
/** Datepicker data that requires internationalization. */
|
||||
@Injectable()
|
||||
export class MdDatepickerIntl {
|
||||
/**
|
||||
* Stream that emits whenever the labels here are changed. Use this to notify
|
||||
* components if the labels have changed after initialization.
|
||||
*/
|
||||
changes: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
/** A label for the calendar popup (used by screen readers). */
|
||||
calendarLabel = 'Calendar';
|
||||
|
||||
|
||||
@@ -6,10 +6,18 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ChangeDetectionStrategy, Component, Input, ViewEncapsulation} from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
ViewEncapsulation,
|
||||
OnDestroy,
|
||||
ChangeDetectorRef,
|
||||
} from '@angular/core';
|
||||
import {MdDatepicker} from './datepicker';
|
||||
import {MdDatepickerIntl} from './datepicker-intl';
|
||||
import {coerceBooleanProperty} from '@angular/cdk/coercion';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
|
||||
|
||||
@Component({
|
||||
@@ -27,7 +35,9 @@ import {coerceBooleanProperty} from '@angular/cdk/coercion';
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MdDatepickerToggle<D> {
|
||||
export class MdDatepickerToggle<D> implements OnDestroy {
|
||||
private _intlChanges: Subscription;
|
||||
|
||||
/** Datepicker instance that the button will toggle. */
|
||||
@Input('mdDatepickerToggle') datepicker: MdDatepicker<D>;
|
||||
|
||||
@@ -45,7 +55,13 @@ export class MdDatepickerToggle<D> {
|
||||
}
|
||||
private _disabled: boolean;
|
||||
|
||||
constructor(public _intl: MdDatepickerIntl) {}
|
||||
constructor(public _intl: MdDatepickerIntl, changeDetectorRef: ChangeDetectorRef) {
|
||||
this._intlChanges = _intl.changes.subscribe(() => changeDetectorRef.markForCheck());
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._intlChanges.unsubscribe();
|
||||
}
|
||||
|
||||
_open(event: Event): void {
|
||||
if (this.datepicker && !this.disabled) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import {async, ComponentFixture, TestBed, inject} from '@angular/core/testing';
|
||||
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {MdDatepickerModule} from './index';
|
||||
import {MdDatepickerModule, MdDatepickerIntl} from './index';
|
||||
import {MdDatepicker} from './datepicker';
|
||||
import {MdDatepickerInput} from './datepicker-input';
|
||||
import {MdInputModule} from '../input/index';
|
||||
@@ -532,6 +532,17 @@ describe('MdDatepicker', () => {
|
||||
|
||||
expect(document.activeElement).toBe(toggle, 'Expected focus to be restored to toggle.');
|
||||
});
|
||||
|
||||
it('should re-render when the i18n labels change',
|
||||
inject([MdDatepickerIntl], (intl: MdDatepickerIntl) => {
|
||||
const toggle = fixture.debugElement.query(By.css('button')).nativeElement;
|
||||
|
||||
intl.openCalendarLabel = 'Open the calendar, perhaps?';
|
||||
intl.changes.emit();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(toggle.getAttribute('aria-label')).toBe('Open the calendar, perhaps?');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('datepicker inside input-container', () => {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Injectable, EventEmitter} from '@angular/core';
|
||||
|
||||
/**
|
||||
* To modify the labels and text displayed, create a new instance of MdPaginatorIntl and
|
||||
@@ -14,6 +14,12 @@ import {Injectable} from '@angular/core';
|
||||
*/
|
||||
@Injectable()
|
||||
export class MdPaginatorIntl {
|
||||
/**
|
||||
* Stream that emits whenever the labels here are changed. Use this to notify
|
||||
* components if the labels have changed after initialization.
|
||||
*/
|
||||
changes: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
/** A label for the page size selector. */
|
||||
itemsPerPageLabel = 'Items per page:';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {async, ComponentFixture, TestBed, inject} from '@angular/core/testing';
|
||||
import {MdPaginatorModule} from './index';
|
||||
import {MdPaginator, PageEvent} from './paginator';
|
||||
import {Component, ElementRef, ViewChild} from '@angular/core';
|
||||
@@ -90,6 +90,17 @@ describe('MdPaginator', () => {
|
||||
expect(getPreviousButton(fixture).getAttribute('aria-label')).toBe('Previous page');
|
||||
expect(getNextButton(fixture).getAttribute('aria-label')).toBe('Next page');
|
||||
});
|
||||
|
||||
it('should re-render when the i18n labels change',
|
||||
inject([MdPaginatorIntl], (intl: MdPaginatorIntl) => {
|
||||
const label = fixture.nativeElement.querySelector('.mat-paginator-page-size-label');
|
||||
|
||||
intl.itemsPerPageLabel = '1337 items per page';
|
||||
intl.changes.emit();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(label.textContent).toBe('1337 items per page');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('when navigating with the navigation buttons', () => {
|
||||
|
||||
@@ -7,16 +7,19 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
ChangeDetectionStrategy, ChangeDetectorRef,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewEncapsulation
|
||||
ViewEncapsulation,
|
||||
OnDestroy,
|
||||
} from '@angular/core';
|
||||
import {MdPaginatorIntl} from './paginator-intl';
|
||||
import {MATERIAL_COMPATIBILITY_MODE} from '../core';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
|
||||
/** The default page size if there is no page size and there are no provided page size options. */
|
||||
const DEFAULT_PAGE_SIZE = 50;
|
||||
@@ -55,8 +58,9 @@ export class PageEvent {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class MdPaginator implements OnInit {
|
||||
export class MdPaginator implements OnInit, OnDestroy {
|
||||
private _initialized: boolean;
|
||||
private _intlChanges: Subscription;
|
||||
|
||||
/** The zero-based page index of the displayed list of items. Defaulted to 0. */
|
||||
@Input()
|
||||
@@ -101,13 +105,19 @@ export class MdPaginator implements OnInit {
|
||||
_displayedPageSizeOptions: number[];
|
||||
|
||||
constructor(public _intl: MdPaginatorIntl,
|
||||
private _changeDetectorRef: ChangeDetectorRef) { }
|
||||
private _changeDetectorRef: ChangeDetectorRef) {
|
||||
this._intlChanges = _intl.changes.subscribe(() => this._changeDetectorRef.markForCheck());
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._initialized = true;
|
||||
this._updateDisplayedPageSizeOptions();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._intlChanges.unsubscribe();
|
||||
}
|
||||
|
||||
/** Advances to the next page if it exists. */
|
||||
nextPage() {
|
||||
if (!this.hasNextPage()) { return; }
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Injectable, EventEmitter} from '@angular/core';
|
||||
import {SortDirection} from './sort-direction';
|
||||
|
||||
/**
|
||||
@@ -15,6 +15,13 @@ import {SortDirection} from './sort-direction';
|
||||
*/
|
||||
@Injectable()
|
||||
export class MdSortHeaderIntl {
|
||||
/**
|
||||
* Stream that emits whenever the labels here are changed. Use this to notify
|
||||
* components if the labels have changed after initialization.
|
||||
*/
|
||||
changes: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
/** ARIA label for the sorting button. */
|
||||
sortButtonLabel = (id: string) => {
|
||||
return `Change sorting for ${id}`;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {CdkColumnDef} from '@angular/cdk/table';
|
||||
import {coerceBooleanProperty} from '../core';
|
||||
import {getMdSortHeaderNotContainedWithinMdSortError} from './sort-errors';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
import {merge} from 'rxjs/observable/merge';
|
||||
|
||||
/**
|
||||
* Applies sorting behavior (click to change sort) and styles to an element, including an
|
||||
@@ -43,8 +44,7 @@ import {Subscription} from 'rxjs/Subscription';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MdSortHeader implements MdSortable {
|
||||
/** @docs-private */
|
||||
sortSubscription: Subscription;
|
||||
private _rerenderSubscription: Subscription;
|
||||
|
||||
/**
|
||||
* ID of this sort header. If used within the context of a CdkColumnDef, this will default to
|
||||
@@ -69,14 +69,16 @@ export class MdSortHeader implements MdSortable {
|
||||
set _id(v: string) { this.id = v; }
|
||||
|
||||
constructor(public _intl: MdSortHeaderIntl,
|
||||
private _changeDetectorRef: ChangeDetectorRef,
|
||||
changeDetectorRef: ChangeDetectorRef,
|
||||
@Optional() public _sort: MdSort,
|
||||
@Optional() public _cdkColumnDef: CdkColumnDef) {
|
||||
if (!_sort) {
|
||||
throw getMdSortHeaderNotContainedWithinMdSortError();
|
||||
}
|
||||
|
||||
this.sortSubscription = _sort.mdSortChange.subscribe(() => _changeDetectorRef.markForCheck());
|
||||
this._rerenderSubscription = merge(_sort.mdSortChange, _intl.changes).subscribe(() => {
|
||||
changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -89,7 +91,7 @@ export class MdSortHeader implements MdSortable {
|
||||
|
||||
ngOnDestroy() {
|
||||
this._sort.deregister(this);
|
||||
this.sortSubscription.unsubscribe();
|
||||
this._rerenderSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
/** Whether this MdSortHeader is currently sorted in either ascending or descending order. */
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {async, ComponentFixture, TestBed, inject} from '@angular/core/testing';
|
||||
import {Component, ElementRef, ViewChild} from '@angular/core';
|
||||
import {MdSort, MdSortHeader, Sort, SortDirection, MdSortModule} from './index';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {MdSort, MdSortHeader, Sort, SortDirection, MdSortModule, MdSortHeaderIntl} from './index';
|
||||
import {CdkTableModule, DataSource, CollectionViewer} from '@angular/cdk/table';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {dispatchMouseEvent} from '@angular/cdk/testing';
|
||||
import {
|
||||
getMdSortDuplicateMdSortableIdError,
|
||||
getMdSortHeaderMissingIdError,
|
||||
getMdSortHeaderNotContainedWithinMdSortError
|
||||
} from './sort-errors';
|
||||
import {wrappedErrorMessage} from '@angular/cdk/testing';
|
||||
import {wrappedErrorMessage, dispatchMouseEvent} from '@angular/cdk/testing';
|
||||
import {map} from '../core/rxjs/index';
|
||||
import {MdTableModule} from '../table/index';
|
||||
|
||||
@@ -141,6 +141,18 @@ describe('MdSort', () => {
|
||||
const button = fixture.nativeElement.querySelector('#defaultSortHeaderA button');
|
||||
expect(button.getAttribute('aria-label')).toBe('Change sorting for defaultSortHeaderA');
|
||||
});
|
||||
|
||||
it('should re-render when the i18n labels have changed',
|
||||
inject([MdSortHeaderIntl], (intl: MdSortHeaderIntl) => {
|
||||
const header = fixture.debugElement.query(By.directive(MdSortHeader)).nativeElement;
|
||||
const button = header.querySelector('.mat-sort-header-button');
|
||||
|
||||
intl.sortButtonLabel = () => 'Sort all of the things';
|
||||
intl.changes.emit();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(button.getAttribute('aria-label')).toBe('Sort all of the things');
|
||||
}));
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user