Skip to content

Commit 937d051

Browse files
committed
feat(material/timepicker): add option template
close #31209
1 parent f9042f1 commit 937d051

File tree

9 files changed

+140
-53
lines changed

9 files changed

+140
-53
lines changed

goldens/material/timepicker/index.api.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export class MatTimepicker<D> implements OnDestroy, MatOptionParentComponent {
3838
constructor();
3939
readonly activeDescendant: Signal<string | null>;
4040
// (undocumented)
41-
protected _animationsDisabled: boolean;
41+
protected readonly _animationsDisabled: boolean;
4242
readonly ariaLabel: InputSignal<string | null>;
4343
readonly ariaLabelledby: InputSignal<string | null>;
4444
close(): void;
@@ -55,12 +55,12 @@ export class MatTimepicker<D> implements OnDestroy, MatOptionParentComponent {
5555
readonly opened: OutputEmitterRef<void>;
5656
readonly options: InputSignal<readonly MatTimepickerOption<D>[] | null>;
5757
// (undocumented)
58-
protected _options: Signal<readonly MatOption<any>[]>;
58+
protected readonly _options: Signal<readonly MatOption<any>[]>;
5959
// (undocumented)
60-
protected _optionTemplate: Signal<TemplateRef<MatTimepickerOption<D>> | undefined>;
60+
protected readonly _optionTemplate: Signal<MatTimepickerOptionTemplate<D> | undefined>;
6161
readonly panelId: string;
6262
// (undocumented)
63-
protected _panelTemplate: Signal<TemplateRef<unknown>>;
63+
protected readonly _panelTemplate: Signal<TemplateRef<unknown>>;
6464
registerInput(input: MatTimepickerConnectedInput<D>): void;
6565
readonly selected: OutputEmitterRef<MatTimepickerSelected<D>>;
6666
protected _selectValue(option: MatOption<D>): void;
@@ -131,7 +131,7 @@ export class MatTimepickerModule {
131131
// (undocumented)
132132
static ɵinj: i0.ɵɵInjectorDeclaration<MatTimepickerModule>;
133133
// (undocumented)
134-
static ɵmod: i0.ɵɵNgModuleDeclaration<MatTimepickerModule, never, [typeof MatTimepicker, typeof MatTimepickerInput, typeof MatTimepickerToggle], [typeof i1.CdkScrollableModule, typeof MatTimepicker, typeof MatTimepickerInput, typeof MatTimepickerToggle]>;
134+
static ɵmod: i0.ɵɵNgModuleDeclaration<MatTimepickerModule, never, [typeof MatTimepicker, typeof MatTimepickerInput, typeof MatTimepickerOptionTemplate, typeof MatTimepickerToggle], [typeof i1.CdkScrollableModule, typeof MatTimepicker, typeof MatTimepickerInput, typeof MatTimepickerOptionTemplate, typeof MatTimepickerToggle]>;
135135
}
136136

137137
// @public
@@ -140,6 +140,16 @@ export interface MatTimepickerOption<D = unknown> {
140140
value: D;
141141
}
142142

143+
// @public
144+
export class MatTimepickerOptionTemplate<D> {
145+
// (undocumented)
146+
readonly template: TemplateRef<MatTimepickerOption<D>>;
147+
// (undocumented)
148+
static ɵdir: i0.ɵɵDirectiveDeclaration<MatTimepickerOptionTemplate<any>, "ng-template[matTimepickerOption]", never, {}, {}, never, never, true, never>;
149+
// (undocumented)
150+
static ɵfac: i0.ɵɵFactoryDeclaration<MatTimepickerOptionTemplate<any>, never>;
151+
}
152+
143153
// @public
144154
export interface MatTimepickerSelected<D> {
145155
// (undocumented)

src/components-examples/material/timepicker/timepicker-option-template/timepicker-option-template-example.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<input matInput [matTimepicker]="picker">
44
<mat-timepicker-toggle matIconSuffix [for]="picker"/>
55
<mat-timepicker #picker>
6-
<ng-template let-option>
6+
<ng-template matTimepickerOption let-option>
77
<mat-icon>
88
{{ (dateAdapter.compareTime(option.value, sunrise) > 0 && dateAdapter.compareTime(option.value, sunset) < 0) ? 'sunny' : 'bedtime' }}
99
</mat-icon>

src/material/timepicker/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ ng_project(
6868
"timepicker.ts",
6969
"timepicker-input.ts",
7070
"timepicker-module.ts",
71+
"timepicker-option.ts",
7172
"timepicker-toggle.ts",
7273
"util.ts",
7374
],

src/material/timepicker/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
export * from './timepicker';
1010
export * from './timepicker-input';
11+
export * from './timepicker-option';
1112
export * from './timepicker-toggle';
1213
export * from './timepicker-module';
1314
export {MatTimepickerOption, MAT_TIMEPICKER_CONFIG, MatTimepickerConfig} from './util';

src/material/timepicker/timepicker-module.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,17 @@ import {NgModule} from '@angular/core';
1010
import {CdkScrollableModule} from '@angular/cdk/scrolling';
1111
import {MatTimepicker} from './timepicker';
1212
import {MatTimepickerInput} from './timepicker-input';
13+
import {MatTimepickerOptionTemplate} from './timepicker-option';
1314
import {MatTimepickerToggle} from './timepicker-toggle';
1415

1516
@NgModule({
16-
imports: [MatTimepicker, MatTimepickerInput, MatTimepickerToggle],
17-
exports: [CdkScrollableModule, MatTimepicker, MatTimepickerInput, MatTimepickerToggle],
17+
imports: [MatTimepicker, MatTimepickerInput, MatTimepickerOptionTemplate, MatTimepickerToggle],
18+
exports: [
19+
CdkScrollableModule,
20+
MatTimepicker,
21+
MatTimepickerInput,
22+
MatTimepickerOptionTemplate,
23+
MatTimepickerToggle,
24+
],
1825
})
1926
export class MatTimepickerModule {}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {Directive, TemplateRef, inject} from '@angular/core';
10+
import {MatTimepickerOption} from './util';
11+
12+
/** Template to be used to override the timepicker's option labels. */
13+
@Directive({
14+
selector: 'ng-template[matTimepickerOption]',
15+
})
16+
export class MatTimepickerOptionTemplate<D> {
17+
readonly template = inject<TemplateRef<MatTimepickerOption<D>>>(TemplateRef);
18+
}

src/material/timepicker/timepicker.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<mat-option
1515
[value]="option.value"
1616
(onSelectionChange)="_selectValue($event.source)">
17-
<ng-container [ngTemplateOutlet]="_optionTemplate() || defaultOptionTemplate"
17+
<ng-container [ngTemplateOutlet]="_optionTemplate()?.template || defaultOptionTemplate"
1818
[ngTemplateOutletContext]="{ $implicit: option }">
1919
</ng-container>
2020
</mat-option>

src/material/timepicker/timepicker.spec.ts

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1-
import {Component, Injector, Provider, signal, ViewChild, ViewEncapsulation} from '@angular/core';
1+
import {
2+
Component,
3+
inject,
4+
Injector,
5+
Provider,
6+
signal,
7+
ViewChild,
8+
ViewEncapsulation,
9+
} from '@angular/core';
210
import {ComponentFixture, fakeAsync, flush, TestBed} from '@angular/core/testing';
3-
import {DateAdapter, MATERIAL_ANIMATIONS, provideNativeDateAdapter} from '../core';
411
import {
512
clearElement,
613
dispatchFakeEvent,
@@ -20,16 +27,18 @@ import {
2027
TAB,
2128
UP_ARROW,
2229
} from '@angular/cdk/keycodes';
23-
import {MatInput} from '../input';
30+
import {createCloseScrollStrategy} from '@angular/cdk/overlay';
31+
import {ScrollDispatcher} from '@angular/cdk/scrolling';
32+
import {FormControl, ReactiveFormsModule, Validators} from '@angular/forms';
33+
import {Subject} from 'rxjs';
34+
import {DateAdapter, MATERIAL_ANIMATIONS, provideNativeDateAdapter} from '../core';
2435
import {MatFormField, MatLabel, MatSuffix} from '../form-field';
36+
import {MatInput} from '../input';
2537
import {MatTimepickerInput} from './timepicker-input';
26-
import {MAT_TIMEPICKER_SCROLL_STRATEGY, MatTimepicker} from './timepicker';
38+
import {MatTimepickerOptionTemplate} from './timepicker-option';
2739
import {MatTimepickerToggle} from './timepicker-toggle';
40+
import {MAT_TIMEPICKER_SCROLL_STRATEGY, MatTimepicker} from './timepicker';
2841
import {MAT_TIMEPICKER_CONFIG, MatTimepickerOption} from './util';
29-
import {FormControl, ReactiveFormsModule, Validators} from '@angular/forms';
30-
import {ScrollDispatcher} from '@angular/cdk/scrolling';
31-
import {createCloseScrollStrategy} from '@angular/cdk/overlay';
32-
import {Subject} from 'rxjs';
3342

3443
describe('MatTimepicker', () => {
3544
let adapter: DateAdapter<Date>;
@@ -652,6 +661,24 @@ describe('MatTimepicker', () => {
652661
fixture.detectChanges();
653662
expect(fixture.componentInstance.timepicker.interval()).toBe(null);
654663
});
664+
665+
it('should be able to apply custom options template', () => {
666+
const fixture = TestBed.createComponent(TimepickerOptionTemplate);
667+
fixture.detectChanges();
668+
669+
getInput(fixture).click();
670+
fixture.detectChanges();
671+
expect(getOptions().map(o => o.textContent.trim())).toEqual([
672+
'12:00 AM (off-hours)',
673+
'3:00 AM (off-hours)',
674+
'6:00 AM (off-hours)',
675+
'9:00 AM (working hours)',
676+
'12:00 PM (working hours)',
677+
'3:00 PM (working hours)',
678+
'6:00 PM (off-hours)',
679+
'9:00 PM (off-hours)',
680+
]);
681+
});
655682
});
656683

657684
describe('mat-form-field integration', () => {
@@ -1512,3 +1539,28 @@ class TimepickerWithoutInput {
15121539
encapsulation: ViewEncapsulation.ShadowDom,
15131540
})
15141541
class TimepickerInShadowDom {}
1542+
1543+
@Component({
1544+
template: `
1545+
<input [matTimepicker]="picker" />
1546+
<mat-timepicker #picker interval="3h">
1547+
<ng-template matTimepickerOption let-option>
1548+
{{ option.label }}
1549+
{{ (dateAdapter.compareTime(option.value, workStartTime) > 0 && dateAdapter.compareTime(option.value, workEndTime) < 0) ? '(working hours)' : '(off-hours)' }}
1550+
</ng-template>
1551+
</mat-timepicker>
1552+
`,
1553+
imports: [MatTimepicker, MatTimepickerInput, MatTimepickerOptionTemplate],
1554+
})
1555+
class TimepickerOptionTemplate {
1556+
readonly dateAdapter = inject<DateAdapter<Date>>(DateAdapter);
1557+
readonly workStartTime: Date;
1558+
readonly workEndTime: Date;
1559+
1560+
constructor() {
1561+
this.workStartTime = new Date();
1562+
this.workStartTime.setHours(8, 30, 0);
1563+
this.workEndTime = new Date();
1564+
this.workEndTime.setHours(17, 0, 0);
1565+
}
1566+
}

src/material/timepicker/timepicker.ts

Lines changed: 34 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,15 @@ import {TemplatePortal} from '@angular/cdk/portal';
5555
import {_getEventTarget} from '@angular/cdk/platform';
5656
import {ENTER, ESCAPE, hasModifierKey, TAB} from '@angular/cdk/keycodes';
5757
import {_IdGenerator, ActiveDescendantKeyManager} from '@angular/cdk/a11y';
58+
import {Subscription} from 'rxjs';
5859
import {
5960
generateOptions,
6061
MAT_TIMEPICKER_CONFIG,
6162
MatTimepickerOption,
6263
parseInterval,
6364
validateAdapter,
6465
} from './util';
65-
import {Subscription} from 'rxjs';
66+
import {MatTimepickerOptionTemplate} from './timepicker-option';
6667

6768
/** Event emitted when a value is selected in the timepicker. */
6869
export interface MatTimepickerSelected<D> {
@@ -129,31 +130,33 @@ export interface MatTimepickerConnectedInput<D> {
129130
],
130131
})
131132
export class MatTimepicker<D> implements OnDestroy, MatOptionParentComponent {
132-
private _dir = inject(Directionality, {optional: true});
133-
private _viewContainerRef = inject(ViewContainerRef);
134-
private _injector = inject(Injector);
135-
private _defaultConfig = inject(MAT_TIMEPICKER_CONFIG, {optional: true});
136-
private _dateAdapter = inject<DateAdapter<D>>(DateAdapter, {optional: true})!;
137-
private _dateFormats = inject(MAT_DATE_FORMATS, {optional: true})!;
138-
private _scrollStrategyFactory = inject(MAT_TIMEPICKER_SCROLL_STRATEGY);
139-
protected _animationsDisabled = _animationsDisabled();
140-
141-
private _isOpen = signal(false);
142-
private _activeDescendant = signal<string | null>(null);
143-
144-
private _input = signal<MatTimepickerConnectedInput<D> | null>(null);
133+
private readonly _dir = inject(Directionality, {optional: true});
134+
private readonly _viewContainerRef = inject(ViewContainerRef);
135+
private readonly _injector = inject(Injector);
136+
private readonly _defaultConfig = inject(MAT_TIMEPICKER_CONFIG, {optional: true});
137+
private readonly _dateAdapter = inject<DateAdapter<D>>(DateAdapter, {optional: true})!;
138+
private readonly _dateFormats = inject(MAT_DATE_FORMATS, {optional: true})!;
139+
private readonly _scrollStrategyFactory = inject(MAT_TIMEPICKER_SCROLL_STRATEGY);
140+
protected readonly _animationsDisabled = _animationsDisabled();
141+
142+
private readonly _isOpen = signal(false);
143+
private readonly _activeDescendant = signal<string | null>(null);
144+
145+
private readonly _input = signal<MatTimepickerConnectedInput<D> | null>(null);
145146
private _overlayRef: OverlayRef | null = null;
146147
private _portal: TemplatePortal<unknown> | null = null;
147148
private _optionsCacheKey: string | null = null;
148149
private _localeChanges: Subscription;
149150
private _onOpenRender: AfterRenderRef | null = null;
150151

151-
protected _panelTemplate = viewChild.required<TemplateRef<unknown>>('panelTemplate');
152152
protected _timeOptions: readonly MatTimepickerOption<D>[] = [];
153-
protected _options = viewChildren(MatOption);
154-
protected _optionTemplate = contentChild<TemplateRef<MatTimepickerOption<D>>>(TemplateRef);
153+
protected readonly _panelTemplate = viewChild.required<TemplateRef<unknown>>('panelTemplate');
154+
protected readonly _options = viewChildren(MatOption);
155+
protected readonly _optionTemplate = contentChild<MatTimepickerOptionTemplate<D>>(
156+
MatTimepickerOptionTemplate,
157+
);
155158

156-
private _keyManager = new ActiveDescendantKeyManager(this._options, this._injector)
159+
private readonly _keyManager = new ActiveDescendantKeyManager(this._options, this._injector)
157160
.withHomeAndEnd(true)
158161
.withPageUpDown(true)
159162
.withVerticalOrientation(true);
@@ -172,48 +175,43 @@ export class MatTimepicker<D> implements OnDestroy, MatOptionParentComponent {
172175
* Array of pre-defined options that the user can select from, as an alternative to using the
173176
* `interval` input. An error will be thrown if both `options` and `interval` are specified.
174177
*/
175-
readonly options: InputSignal<readonly MatTimepickerOption<D>[] | null> = input<
176-
readonly MatTimepickerOption<D>[] | null
177-
>(null);
178+
readonly options = input<readonly MatTimepickerOption<D>[] | null>(null);
178179

179180
/** Whether the timepicker is open. */
180-
readonly isOpen: Signal<boolean> = this._isOpen.asReadonly();
181+
readonly isOpen = this._isOpen.asReadonly();
181182

182183
/** Emits when the user selects a time. */
183-
readonly selected: OutputEmitterRef<MatTimepickerSelected<D>> = output();
184+
readonly selected = output<MatTimepickerSelected<D>>();
184185

185186
/** Emits when the timepicker is opened. */
186-
readonly opened: OutputEmitterRef<void> = output();
187+
readonly opened = output();
187188

188189
/** Emits when the timepicker is closed. */
189-
readonly closed: OutputEmitterRef<void> = output();
190+
readonly closed = output();
190191

191192
/** ID of the active descendant option. */
192-
readonly activeDescendant: Signal<string | null> = this._activeDescendant.asReadonly();
193+
readonly activeDescendant = this._activeDescendant.asReadonly();
193194

194195
/** Unique ID of the timepicker's panel */
195-
readonly panelId: string = inject(_IdGenerator).getId('mat-timepicker-panel-');
196+
readonly panelId = inject(_IdGenerator).getId('mat-timepicker-panel-');
196197

197198
/** Whether ripples within the timepicker should be disabled. */
198-
readonly disableRipple: InputSignalWithTransform<boolean, unknown> = input(
199-
this._defaultConfig?.disableRipple ?? false,
200-
{
201-
transform: booleanAttribute,
202-
},
203-
);
199+
readonly disableRipple = input(this._defaultConfig?.disableRipple ?? false, {
200+
transform: booleanAttribute,
201+
});
204202

205203
/** ARIA label for the timepicker panel. */
206-
readonly ariaLabel: InputSignal<string | null> = input<string | null>(null, {
204+
readonly ariaLabel = input<string | null>(null, {
207205
alias: 'aria-label',
208206
});
209207

210208
/** ID of the label element for the timepicker panel. */
211-
readonly ariaLabelledby: InputSignal<string | null> = input<string | null>(null, {
209+
readonly ariaLabelledby = input<string | null>(null, {
212210
alias: 'aria-labelledby',
213211
});
214212

215213
/** Whether the timepicker is currently disabled. */
216-
readonly disabled: Signal<boolean> = computed(() => !!this._input()?.disabled());
214+
readonly disabled = computed(() => !!this._input()?.disabled());
217215

218216
constructor() {
219217
if (typeof ngDevMode === 'undefined' || ngDevMode) {

0 commit comments

Comments
 (0)