Skip to content

Commit

Permalink
feat: add change emitters to the Intl providers
Browse files Browse the repository at this point in the history
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 angular#5738.
  • Loading branch information
crisbeto committed Jul 24, 2017
1 parent 03c0087 commit ef75cb6
Show file tree
Hide file tree
Showing 11 changed files with 130 additions and 22 deletions.
14 changes: 13 additions & 1 deletion src/lib/datepicker/calendar.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
Expand Down
20 changes: 17 additions & 3 deletions src/lib/datepicker/calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import {
NgZone,
Optional,
Output,
ViewEncapsulation
ViewEncapsulation,
ChangeDetectorRef,
OnDestroy,
} from '@angular/core';
import {
DOWN_ARROW,
Expand All @@ -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';


/**
Expand All @@ -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;

Expand Down Expand Up @@ -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() {
Expand All @@ -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)) {
Expand Down
8 changes: 7 additions & 1 deletion src/lib/datepicker/datepicker-intl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
22 changes: 19 additions & 3 deletions src/lib/datepicker/datepicker-toggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
import {Subscription} from 'rxjs/Subscription';


@Component({
Expand All @@ -27,7 +35,9 @@ import {coerceBooleanProperty} from '@angular/cdk';
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>;

Expand All @@ -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) {
Expand Down
13 changes: 12 additions & 1 deletion src/lib/datepicker/datepicker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -498,6 +498,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', () => {
Expand Down
8 changes: 7 additions & 1 deletion src/lib/paginator/paginator-intl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@
* 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
* include it in a custom provider
*/
@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:';

Expand Down
13 changes: 12 additions & 1 deletion src/lib/paginator/paginator.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -90,6 +90,17 @@ describe('MdPaginator', () => {
const nextButton = fixture.nativeElement.querySelector('.mat-paginator-navigation-next');
expect(nextButton.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', () => {
Expand Down
16 changes: 13 additions & 3 deletions src/lib/paginator/paginator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ import {
Input,
OnInit,
Output,
ViewEncapsulation
ViewEncapsulation,
ChangeDetectorRef,
OnDestroy,
} from '@angular/core';
import {MdPaginatorIntl} from './paginator-intl';
import {MATERIAL_COMPATIBILITY_MODE} from '../core';
import {Subscription} from 'rxjs/Subscription';

/**
* Change event object that is emitted when the user selects a
Expand Down Expand Up @@ -52,8 +55,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() pageIndex: number = 0;
Expand Down Expand Up @@ -85,13 +89,19 @@ export class MdPaginator implements OnInit {
/** Displayed set of page size options. Will be sorted and include current page size. */
_displayedPageSizeOptions: number[];

constructor(public _intl: MdPaginatorIntl) { }
constructor(public _intl: MdPaginatorIntl, changeDetectorRef: ChangeDetectorRef) {
this._intlChanges = _intl.changes.subscribe(() => 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; }
Expand Down
9 changes: 8 additions & 1 deletion src/lib/sort/sort-header-intl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand All @@ -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}`;
}
Expand Down
12 changes: 7 additions & 5 deletions src/lib/sort/sort-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {CdkColumnDef} from '@angular/cdk';
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
Expand All @@ -39,8 +40,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
Expand All @@ -65,14 +65,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() {
Expand All @@ -85,7 +87,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. */
Expand Down
17 changes: 15 additions & 2 deletions src/lib/sort/sort.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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';
import {Observable} from 'rxjs/Observable';
import {dispatchMouseEvent} from '@angular/cdk/testing';
Expand Down Expand Up @@ -126,6 +127,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');
}));
});

/**
Expand Down

0 comments on commit ef75cb6

Please sign in to comment.