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:
Kristiyan Kostadinov
2017-07-27 19:46:05 +03:00
committed by Andrew Seguin
parent 8edbe47e37
commit 0a5489f9c4
11 changed files with 132 additions and 25 deletions
+13 -1
View File
@@ -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 -3
View File
@@ -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)) {
+7 -1
View File
@@ -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';
+19 -3
View File
@@ -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) {
+12 -1
View File
@@ -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', () => {
+7 -1
View File
@@ -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:';
+12 -1
View File
@@ -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', () => {
+14 -4
View File
@@ -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; }
+8 -1
View File
@@ -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}`;
}
+7 -5
View File
@@ -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. */
+16 -4
View File
@@ -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');
}));
});
/**