Skip to content

Commit 12c6cfd

Browse files
committed
feat: add change emitters to the Intl providers
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.
1 parent 3cb3945 commit 12c6cfd

File tree

11 files changed

+132
-25
lines changed

11 files changed

+132
-25
lines changed

src/lib/datepicker/calendar.spec.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
1+
import {async, ComponentFixture, inject, TestBed} from '@angular/core/testing';
22
import {Component} from '@angular/core';
33
import {MdCalendar} from './calendar';
44
import {By} from '@angular/platform-browser';
@@ -148,6 +148,18 @@ describe('MdCalendar', () => {
148148
expect(testComponent.selected).toEqual(new Date(2017, JAN, 31));
149149
});
150150

151+
it('should re-render when the i18n labels have changed',
152+
inject([MdDatepickerIntl], (intl: MdDatepickerIntl) => {
153+
const button = fixture.debugElement.nativeElement
154+
.querySelector('.mat-calendar-period-button');
155+
156+
intl.switchToYearViewLabel = 'Go to year view?';
157+
intl.changes.emit();
158+
fixture.detectChanges();
159+
160+
expect(button.getAttribute('aria-label')).toBe('Go to year view?');
161+
}));
162+
151163
describe('a11y', () => {
152164
describe('calendar body', () => {
153165
let calendarBodyEl: HTMLElement;

src/lib/datepicker/calendar.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import {
1717
NgZone,
1818
Optional,
1919
Output,
20-
ViewEncapsulation
20+
ViewEncapsulation,
21+
ChangeDetectorRef,
22+
OnDestroy,
2123
} from '@angular/core';
2224
import {
2325
DOWN_ARROW,
@@ -36,6 +38,7 @@ import {createMissingDateImplError} from './datepicker-errors';
3638
import {MD_DATE_FORMATS, MdDateFormats} from '../core/datetime/date-formats';
3739
import {MATERIAL_COMPATIBILITY_MODE} from '../core';
3840
import {first} from '../core/rxjs/index';
41+
import {Subscription} from 'rxjs/Subscription';
3942

4043

4144
/**
@@ -53,7 +56,9 @@ import {first} from '../core/rxjs/index';
5356
encapsulation: ViewEncapsulation.None,
5457
changeDetection: ChangeDetectionStrategy.OnPush,
5558
})
56-
export class MdCalendar<D> implements AfterContentInit {
59+
export class MdCalendar<D> implements AfterContentInit, OnDestroy {
60+
private _intlChanges: Subscription;
61+
5762
/** A date representing the period (month or year) to start the calendar in. */
5863
@Input() startAt: D;
5964

@@ -123,13 +128,18 @@ export class MdCalendar<D> implements AfterContentInit {
123128
private _ngZone: NgZone,
124129
@Optional() @Inject(MATERIAL_COMPATIBILITY_MODE) public _isCompatibilityMode: boolean,
125130
@Optional() private _dateAdapter: DateAdapter<D>,
126-
@Optional() @Inject(MD_DATE_FORMATS) private _dateFormats: MdDateFormats) {
131+
@Optional() @Inject(MD_DATE_FORMATS) private _dateFormats: MdDateFormats,
132+
changeDetectorRef: ChangeDetectorRef) {
133+
127134
if (!this._dateAdapter) {
128135
throw createMissingDateImplError('DateAdapter');
129136
}
137+
130138
if (!this._dateFormats) {
131139
throw createMissingDateImplError('MD_DATE_FORMATS');
132140
}
141+
142+
this._intlChanges = _intl.changes.subscribe(() => changeDetectorRef.markForCheck());
133143
}
134144

135145
ngAfterContentInit() {
@@ -138,6 +148,10 @@ export class MdCalendar<D> implements AfterContentInit {
138148
this._monthView = this.startView != 'year';
139149
}
140150

151+
ngOnDestroy() {
152+
this._intlChanges.unsubscribe();
153+
}
154+
141155
/** Handles date selection in the month view. */
142156
_dateSelected(date: D): void {
143157
if (!this._dateAdapter.sameDate(date, this.selected)) {

src/lib/datepicker/datepicker-intl.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,18 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Injectable} from '@angular/core';
9+
import {Injectable, EventEmitter} from '@angular/core';
1010

1111

1212
/** Datepicker data that requires internationalization. */
1313
@Injectable()
1414
export class MdDatepickerIntl {
15+
/**
16+
* Stream that emits whenever the labels here are changed. Use this to notify
17+
* components if the labels have changed after initialization.
18+
*/
19+
changes: EventEmitter<void> = new EventEmitter<void>();
20+
1521
/** A label for the calendar popup (used by screen readers). */
1622
calendarLabel = 'Calendar';
1723

src/lib/datepicker/datepicker-toggle.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,18 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {ChangeDetectionStrategy, Component, Input, ViewEncapsulation} from '@angular/core';
9+
import {
10+
ChangeDetectionStrategy,
11+
Component,
12+
Input,
13+
ViewEncapsulation,
14+
OnDestroy,
15+
ChangeDetectorRef,
16+
} from '@angular/core';
1017
import {MdDatepicker} from './datepicker';
1118
import {MdDatepickerIntl} from './datepicker-intl';
1219
import {coerceBooleanProperty} from '@angular/cdk/coercion';
20+
import {Subscription} from 'rxjs/Subscription';
1321

1422

1523
@Component({
@@ -27,7 +35,9 @@ import {coerceBooleanProperty} from '@angular/cdk/coercion';
2735
encapsulation: ViewEncapsulation.None,
2836
changeDetection: ChangeDetectionStrategy.OnPush,
2937
})
30-
export class MdDatepickerToggle<D> {
38+
export class MdDatepickerToggle<D> implements OnDestroy {
39+
private _intlChanges: Subscription;
40+
3141
/** Datepicker instance that the button will toggle. */
3242
@Input('mdDatepickerToggle') datepicker: MdDatepicker<D>;
3343

@@ -45,7 +55,13 @@ export class MdDatepickerToggle<D> {
4555
}
4656
private _disabled: boolean;
4757

48-
constructor(public _intl: MdDatepickerIntl) {}
58+
constructor(public _intl: MdDatepickerIntl, changeDetectorRef: ChangeDetectorRef) {
59+
this._intlChanges = _intl.changes.subscribe(() => changeDetectorRef.markForCheck());
60+
}
61+
62+
ngOnDestroy() {
63+
this._intlChanges.unsubscribe();
64+
}
4965

5066
_open(event: Event): void {
5167
if (this.datepicker && !this.disabled) {

src/lib/datepicker/datepicker.spec.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {async, ComponentFixture, TestBed, inject} from '@angular/core/testing';
33
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
44
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
55
import {By} from '@angular/platform-browser';
6-
import {MdDatepickerModule} from './index';
6+
import {MdDatepickerModule, MdDatepickerIntl} from './index';
77
import {MdDatepicker} from './datepicker';
88
import {MdDatepickerInput} from './datepicker-input';
99
import {MdInputModule} from '../input/index';
@@ -532,6 +532,17 @@ describe('MdDatepicker', () => {
532532

533533
expect(document.activeElement).toBe(toggle, 'Expected focus to be restored to toggle.');
534534
});
535+
536+
it('should re-render when the i18n labels change',
537+
inject([MdDatepickerIntl], (intl: MdDatepickerIntl) => {
538+
const toggle = fixture.debugElement.query(By.css('button')).nativeElement;
539+
540+
intl.openCalendarLabel = 'Open the calendar, perhaps?';
541+
intl.changes.emit();
542+
fixture.detectChanges();
543+
544+
expect(toggle.getAttribute('aria-label')).toBe('Open the calendar, perhaps?');
545+
}));
535546
});
536547

537548
describe('datepicker inside input-container', () => {

src/lib/paginator/paginator-intl.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,20 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Injectable} from '@angular/core';
9+
import {Injectable, EventEmitter} from '@angular/core';
1010

1111
/**
1212
* To modify the labels and text displayed, create a new instance of MdPaginatorIntl and
1313
* include it in a custom provider
1414
*/
1515
@Injectable()
1616
export class MdPaginatorIntl {
17+
/**
18+
* Stream that emits whenever the labels here are changed. Use this to notify
19+
* components if the labels have changed after initialization.
20+
*/
21+
changes: EventEmitter<void> = new EventEmitter<void>();
22+
1723
/** A label for the page size selector. */
1824
itemsPerPageLabel = 'Items per page:';
1925

src/lib/paginator/paginator.spec.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
1+
import {async, ComponentFixture, TestBed, inject} from '@angular/core/testing';
22
import {MdPaginatorModule} from './index';
33
import {MdPaginator, PageEvent} from './paginator';
44
import {Component, ElementRef, ViewChild} from '@angular/core';
@@ -90,6 +90,17 @@ describe('MdPaginator', () => {
9090
expect(getPreviousButton(fixture).getAttribute('aria-label')).toBe('Previous page');
9191
expect(getNextButton(fixture).getAttribute('aria-label')).toBe('Next page');
9292
});
93+
94+
it('should re-render when the i18n labels change',
95+
inject([MdPaginatorIntl], (intl: MdPaginatorIntl) => {
96+
const label = fixture.nativeElement.querySelector('.mat-paginator-page-size-label');
97+
98+
intl.itemsPerPageLabel = '1337 items per page';
99+
intl.changes.emit();
100+
fixture.detectChanges();
101+
102+
expect(label.textContent).toBe('1337 items per page');
103+
}));
93104
});
94105

95106
describe('when navigating with the navigation buttons', () => {

src/lib/paginator/paginator.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,19 @@
77
*/
88

99
import {
10-
ChangeDetectionStrategy, ChangeDetectorRef,
10+
ChangeDetectionStrategy,
11+
ChangeDetectorRef,
1112
Component,
1213
EventEmitter,
1314
Input,
1415
OnInit,
1516
Output,
16-
ViewEncapsulation
17+
ViewEncapsulation,
18+
OnDestroy,
1719
} from '@angular/core';
1820
import {MdPaginatorIntl} from './paginator-intl';
1921
import {MATERIAL_COMPATIBILITY_MODE} from '../core';
22+
import {Subscription} from 'rxjs/Subscription';
2023

2124
/** The default page size if there is no page size and there are no provided page size options. */
2225
const DEFAULT_PAGE_SIZE = 50;
@@ -55,8 +58,9 @@ export class PageEvent {
5558
changeDetection: ChangeDetectionStrategy.OnPush,
5659
encapsulation: ViewEncapsulation.None,
5760
})
58-
export class MdPaginator implements OnInit {
61+
export class MdPaginator implements OnInit, OnDestroy {
5962
private _initialized: boolean;
63+
private _intlChanges: Subscription;
6064

6165
/** The zero-based page index of the displayed list of items. Defaulted to 0. */
6266
@Input()
@@ -101,13 +105,19 @@ export class MdPaginator implements OnInit {
101105
_displayedPageSizeOptions: number[];
102106

103107
constructor(public _intl: MdPaginatorIntl,
104-
private _changeDetectorRef: ChangeDetectorRef) { }
108+
private _changeDetectorRef: ChangeDetectorRef) {
109+
this._intlChanges = _intl.changes.subscribe(() => this._changeDetectorRef.markForCheck());
110+
}
105111

106112
ngOnInit() {
107113
this._initialized = true;
108114
this._updateDisplayedPageSizeOptions();
109115
}
110116

117+
ngOnDestroy() {
118+
this._intlChanges.unsubscribe();
119+
}
120+
111121
/** Advances to the next page if it exists. */
112122
nextPage() {
113123
if (!this.hasNextPage()) { return; }

src/lib/sort/sort-header-intl.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Injectable} from '@angular/core';
9+
import {Injectable, EventEmitter} from '@angular/core';
1010
import {SortDirection} from './sort-direction';
1111

1212
/**
@@ -15,6 +15,13 @@ import {SortDirection} from './sort-direction';
1515
*/
1616
@Injectable()
1717
export class MdSortHeaderIntl {
18+
/**
19+
* Stream that emits whenever the labels here are changed. Use this to notify
20+
* components if the labels have changed after initialization.
21+
*/
22+
changes: EventEmitter<void> = new EventEmitter<void>();
23+
24+
/** ARIA label for the sorting button. */
1825
sortButtonLabel = (id: string) => {
1926
return `Change sorting for ${id}`;
2027
}

src/lib/sort/sort-header.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {CdkColumnDef} from '@angular/cdk/table';
2020
import {coerceBooleanProperty} from '../core';
2121
import {getMdSortHeaderNotContainedWithinMdSortError} from './sort-errors';
2222
import {Subscription} from 'rxjs/Subscription';
23+
import {merge} from 'rxjs/observable/merge';
2324

2425
/**
2526
* Applies sorting behavior (click to change sort) and styles to an element, including an
@@ -43,8 +44,7 @@ import {Subscription} from 'rxjs/Subscription';
4344
changeDetection: ChangeDetectionStrategy.OnPush,
4445
})
4546
export class MdSortHeader implements MdSortable {
46-
/** @docs-private */
47-
sortSubscription: Subscription;
47+
private _rerenderSubscription: Subscription;
4848

4949
/**
5050
* 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 {
6969
set _id(v: string) { this.id = v; }
7070

7171
constructor(public _intl: MdSortHeaderIntl,
72-
private _changeDetectorRef: ChangeDetectorRef,
72+
changeDetectorRef: ChangeDetectorRef,
7373
@Optional() public _sort: MdSort,
7474
@Optional() public _cdkColumnDef: CdkColumnDef) {
7575
if (!_sort) {
7676
throw getMdSortHeaderNotContainedWithinMdSortError();
7777
}
7878

79-
this.sortSubscription = _sort.mdSortChange.subscribe(() => _changeDetectorRef.markForCheck());
79+
this._rerenderSubscription = merge(_sort.mdSortChange, _intl.changes).subscribe(() => {
80+
changeDetectorRef.markForCheck();
81+
});
8082
}
8183

8284
ngOnInit() {
@@ -89,7 +91,7 @@ export class MdSortHeader implements MdSortable {
8991

9092
ngOnDestroy() {
9193
this._sort.deregister(this);
92-
this.sortSubscription.unsubscribe();
94+
this._rerenderSubscription.unsubscribe();
9395
}
9496

9597
/** Whether this MdSortHeader is currently sorted in either ascending or descending order. */

src/lib/sort/sort.spec.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
1+
import {async, ComponentFixture, TestBed, inject} from '@angular/core/testing';
22
import {Component, ElementRef, ViewChild} from '@angular/core';
3-
import {MdSort, MdSortHeader, Sort, SortDirection, MdSortModule} from './index';
3+
import {By} from '@angular/platform-browser';
4+
import {MdSort, MdSortHeader, Sort, SortDirection, MdSortModule, MdSortHeaderIntl} from './index';
45
import {CdkTableModule, DataSource, CollectionViewer} from '@angular/cdk/table';
56
import {Observable} from 'rxjs/Observable';
6-
import {dispatchMouseEvent} from '@angular/cdk/testing';
77
import {
88
getMdSortDuplicateMdSortableIdError,
99
getMdSortHeaderMissingIdError,
1010
getMdSortHeaderNotContainedWithinMdSortError
1111
} from './sort-errors';
12-
import {wrappedErrorMessage} from '@angular/cdk/testing';
12+
import {wrappedErrorMessage, dispatchMouseEvent} from '@angular/cdk/testing';
1313
import {map} from '../core/rxjs/index';
1414
import {MdTableModule} from '../table/index';
1515

@@ -141,6 +141,18 @@ describe('MdSort', () => {
141141
const button = fixture.nativeElement.querySelector('#defaultSortHeaderA button');
142142
expect(button.getAttribute('aria-label')).toBe('Change sorting for defaultSortHeaderA');
143143
});
144+
145+
it('should re-render when the i18n labels have changed',
146+
inject([MdSortHeaderIntl], (intl: MdSortHeaderIntl) => {
147+
const header = fixture.debugElement.query(By.directive(MdSortHeader)).nativeElement;
148+
const button = header.querySelector('.mat-sort-header-button');
149+
150+
intl.sortButtonLabel = () => 'Sort all of the things';
151+
intl.changes.emit();
152+
fixture.detectChanges();
153+
154+
expect(button.getAttribute('aria-label')).toBe('Sort all of the things');
155+
}));
144156
});
145157

146158
/**

0 commit comments

Comments
 (0)