Skip to content

Commit ed409cd

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 03c0087 commit ed409cd

File tree

11 files changed

+139
-22
lines changed

11 files changed

+139
-22
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: 19 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,12 @@ export class MdCalendar<D> implements AfterContentInit {
138148
this._monthView = this.startView != 'year';
139149
}
140150

151+
ngOnDestroy() {
152+
if (this._intlChanges) {
153+
this._intlChanges.unsubscribe();
154+
}
155+
}
156+
141157
/** Handles date selection in the month view. */
142158
_dateSelected(date: D): void {
143159
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: 21 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';
20+
import {Subscription} from 'rxjs/Subscription';
1321

1422

1523
@Component({
@@ -27,7 +35,9 @@ import {coerceBooleanProperty} from '@angular/cdk';
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,15 @@ 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+
if (this._intlChanges) {
64+
this._intlChanges.unsubscribe();
65+
}
66+
}
4967

5068
_open(event: Event): void {
5169
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';
@@ -498,6 +498,17 @@ describe('MdDatepicker', () => {
498498

499499
expect(document.activeElement).toBe(toggle, 'Expected focus to be restored to toggle.');
500500
});
501+
502+
it('should re-render when the i18n labels change',
503+
inject([MdDatepickerIntl], (intl: MdDatepickerIntl) => {
504+
const toggle = fixture.debugElement.query(By.css('button')).nativeElement;
505+
506+
intl.openCalendarLabel = 'Open the calendar, perhaps?';
507+
intl.changes.emit();
508+
fixture.detectChanges();
509+
510+
expect(toggle.getAttribute('aria-label')).toBe('Open the calendar, perhaps?');
511+
}));
501512
});
502513

503514
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
const nextButton = fixture.nativeElement.querySelector('.mat-paginator-navigation-next');
9191
expect(nextButton.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: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@ import {
1313
Input,
1414
OnInit,
1515
Output,
16-
ViewEncapsulation
16+
ViewEncapsulation,
17+
ChangeDetectorRef,
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
/**
2225
* Change event object that is emitted when the user selects a
@@ -52,8 +55,9 @@ export class PageEvent {
5255
changeDetection: ChangeDetectionStrategy.OnPush,
5356
encapsulation: ViewEncapsulation.None,
5457
})
55-
export class MdPaginator implements OnInit {
58+
export class MdPaginator implements OnInit, OnDestroy {
5659
private _initialized: boolean;
60+
private _intlChanges: Subscription;
5761

5862
/** The zero-based page index of the displayed list of items. Defaulted to 0. */
5963
@Input() pageIndex: number = 0;
@@ -85,13 +89,21 @@ export class MdPaginator implements OnInit {
8589
/** Displayed set of page size options. Will be sorted and include current page size. */
8690
_displayedPageSizeOptions: number[];
8791

88-
constructor(public _intl: MdPaginatorIntl) { }
92+
constructor(public _intl: MdPaginatorIntl, changeDetectorRef: ChangeDetectorRef) {
93+
_intl.changes.subscribe(() => changeDetectorRef.markForCheck());
94+
}
8995

9096
ngOnInit() {
9197
this._initialized = true;
9298
this._updateDisplayedPageSizeOptions();
9399
}
94100

101+
ngOnDestroy() {
102+
if (this._intlChanges) {
103+
this._intlChanges.unsubscribe();
104+
}
105+
}
106+
95107
/** Advances to the next page if it exists. */
96108
nextPage() {
97109
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: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {CdkColumnDef} from '@angular/cdk';
1616
import {coerceBooleanProperty} from '../core';
1717
import {getMdSortHeaderNotContainedWithinMdSortError} from './sort-errors';
1818
import {Subscription} from 'rxjs/Subscription';
19+
import {merge} from 'rxjs/observable/merge';
1920

2021
/**
2122
* Applies sorting behavior (click to change sort) and styles to an element, including an
@@ -39,8 +40,7 @@ import {Subscription} from 'rxjs/Subscription';
3940
changeDetection: ChangeDetectionStrategy.OnPush,
4041
})
4142
export class MdSortHeader implements MdSortable {
42-
/** @docs-private */
43-
sortSubscription: Subscription;
43+
private _rerenderSubscription: Subscription;
4444

4545
/**
4646
* ID of this sort header. If used within the context of a CdkColumnDef, this will default to
@@ -65,14 +65,16 @@ export class MdSortHeader implements MdSortable {
6565
set _id(v: string) { this.id = v; }
6666

6767
constructor(public _intl: MdSortHeaderIntl,
68-
private _changeDetectorRef: ChangeDetectorRef,
68+
changeDetectorRef: ChangeDetectorRef,
6969
@Optional() public _sort: MdSort,
7070
@Optional() public _cdkColumnDef: CdkColumnDef) {
7171
if (!_sort) {
7272
throw getMdSortHeaderNotContainedWithinMdSortError();
7373
}
7474

75-
this.sortSubscription = _sort.mdSortChange.subscribe(() => _changeDetectorRef.markForCheck());
75+
this._rerenderSubscription = merge(_sort.mdSortChange, _intl.changes).subscribe(() => {
76+
changeDetectorRef.markForCheck();
77+
});
7678
}
7779

7880
ngOnInit() {
@@ -85,7 +87,10 @@ export class MdSortHeader implements MdSortable {
8587

8688
ngOnDestroy() {
8789
this._sort.deregister(this);
88-
this.sortSubscription.unsubscribe();
90+
91+
if (this._rerenderSubscription) {
92+
this._rerenderSubscription.unsubscribe();
93+
}
8994
}
9095

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

src/lib/sort/sort.spec.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
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';
56
import {Observable} from 'rxjs/Observable';
67
import {dispatchMouseEvent} from '@angular/cdk/testing';
@@ -126,6 +127,18 @@ describe('MdSort', () => {
126127
const button = fixture.nativeElement.querySelector('#defaultSortHeaderA button');
127128
expect(button.getAttribute('aria-label')).toBe('Change sorting for defaultSortHeaderA');
128129
});
130+
131+
it('should re-render when the i18n labels have changed',
132+
inject([MdSortHeaderIntl], (intl: MdSortHeaderIntl) => {
133+
const header = fixture.debugElement.query(By.directive(MdSortHeader)).nativeElement;
134+
const button = header.querySelector('.mat-sort-header-button');
135+
136+
intl.sortButtonLabel = () => 'Sort all of the things';
137+
intl.changes.emit();
138+
fixture.detectChanges();
139+
140+
expect(button.getAttribute('aria-label')).toBe('Sort all of the things');
141+
}));
129142
});
130143

131144
/**

0 commit comments

Comments
 (0)