Skip to content

Commit 0a5489f

Browse files
crisbetoandrewseguin
authored andcommitted
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.
1 parent 8edbe47 commit 0a5489f

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)