Skip to content

Commit

Permalink
feat(material-experimental/mdc-autocomplete): implement MDC-based mat…
Browse files Browse the repository at this point in the history
…-autocomplete (#20247)

* Moves all of the autocomplete logic into base classes so that it can be reused between the standard and MDC components.
* Re-implements `mat-autocomplete` using the logic from the existing one and the styling from MDC.

The MDC-based autocomplete behaves identically to the existing one, with the only minor difference being that MDC one fixes a long-standing issue where we expect a hardcoded height for each of the options. It was easier to fix the bug and add logic to support arbitrary option heights than to add more logic to account for MDC's styles.
  • Loading branch information
crisbeto committed Aug 13, 2020
1 parent cb8de61 commit c8f03c7
Show file tree
Hide file tree
Showing 34 changed files with 3,988 additions and 194 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -144,6 +144,7 @@
/src/dev-app/baseline/** @mmalerba
/src/dev-app/bottom-sheet/** @jelbourn @crisbeto
/src/dev-app/button-toggle/** @jelbourn
/src/dev-app/mdc-autocomplete/** @crisbeto
/src/dev-app/button/** @jelbourn
/src/dev-app/card/** @jelbourn
/src/dev-app/cdk-experimental-listbox/** @jelbourn @nielsr98
Expand Down
1 change: 1 addition & 0 deletions src/dev-app/BUILD.bazel
Expand Up @@ -46,6 +46,7 @@ ng_module(
"//src/dev-app/input",
"//src/dev-app/list",
"//src/dev-app/live-announcer",
"//src/dev-app/mdc-autocomplete",
"//src/dev-app/mdc-button",
"//src/dev-app/mdc-card",
"//src/dev-app/mdc-checkbox",
Expand Down
1 change: 1 addition & 0 deletions src/dev-app/dev-app/dev-app-layout.ts
Expand Up @@ -78,6 +78,7 @@ export class DevAppLayout {
{name: 'Typography', route: '/typography'},
{name: 'Virtual Scrolling', route: '/virtual-scroll'},
{name: 'YouTube Player', route: '/youtube-player'},
{name: 'MDC Autocomplete', route: '/mdc-autocomplete'},
{name: 'MDC Button', route: '/mdc-button'},
{name: 'MDC Card', route: '/mdc-card'},
{name: 'MDC Checkbox', route: '/mdc-checkbox'},
Expand Down
4 changes: 4 additions & 0 deletions src/dev-app/dev-app/routes.ts
Expand Up @@ -69,6 +69,10 @@ export const DEV_APP_ROUTES: Routes = [
path: 'menubar',
loadChildren: 'menubar/mat-menubar-demo-module#MatMenuBarDemoModule'
},
{
path: 'mdc-autocomplete',
loadChildren: 'mdc-autocomplete/mdc-autocomplete-demo-module#MdcAutocompleteDemoModule'
},
{path: 'mdc-button', loadChildren: 'mdc-button/mdc-button-demo-module#MdcButtonDemoModule'},
{path: 'mdc-card', loadChildren: 'mdc-card/mdc-card-demo-module#MdcCardDemoModule'},
{
Expand Down
26 changes: 26 additions & 0 deletions src/dev-app/mdc-autocomplete/BUILD.bazel
@@ -0,0 +1,26 @@
load("//tools:defaults.bzl", "ng_module", "sass_binary")

package(default_visibility = ["//visibility:public"])

ng_module(
name = "mdc-autocomplete",
srcs = glob(["**/*.ts"]),
assets = [
"mdc-autocomplete-demo.html",
":mdc_autocomplete_demo_scss",
],
deps = [
"//src/material-experimental/mdc-autocomplete",
"//src/material-experimental/mdc-button",
"//src/material-experimental/mdc-card",
"//src/material-experimental/mdc-form-field",
"//src/material-experimental/mdc-input",
"@npm//@angular/forms",
"@npm//@angular/router",
],
)

sass_binary(
name = "mdc_autocomplete_demo_scss",
src = "mdc-autocomplete-demo.scss",
)
35 changes: 35 additions & 0 deletions src/dev-app/mdc-autocomplete/mdc-autocomplete-demo-module.ts
@@ -0,0 +1,35 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MatAutocompleteModule} from '@angular/material-experimental/mdc-autocomplete';
import {MatButtonModule} from '@angular/material-experimental/mdc-button';
import {MatCardModule} from '@angular/material-experimental/mdc-card';
import {MatFormFieldModule} from '@angular/material-experimental/mdc-form-field';
import {MatInputModule} from '@angular/material-experimental/mdc-input';
import {RouterModule} from '@angular/router';
import {MdcAutocompleteDemo} from './mdc-autocomplete-demo';

@NgModule({
imports: [
CommonModule,
FormsModule,
MatAutocompleteModule,
MatButtonModule,
MatCardModule,
MatFormFieldModule,
MatInputModule,
ReactiveFormsModule,
RouterModule.forChild([{path: '', component: MdcAutocompleteDemo}]),
],
declarations: [MdcAutocompleteDemo],
})
export class MdcAutocompleteDemoModule {
}
76 changes: 76 additions & 0 deletions src/dev-app/mdc-autocomplete/mdc-autocomplete-demo.html
@@ -0,0 +1,76 @@
Space above cards: <input type="number" [formControl]="topHeightCtrl">
<div [style.height.px]="topHeightCtrl.value"></div>
<div class="demo-autocomplete">
<mat-card *ngIf="(reactiveStates | async) as tempStates">
Reactive length: {{ tempStates?.length }}
<div>Reactive value: {{ stateCtrl.value | json }}</div>
<div>Reactive dirty: {{ stateCtrl.dirty }}</div>

<mat-form-field>
<mat-label>State</mat-label>
<input matInput [matAutocomplete]="reactiveAuto" [formControl]="stateCtrl">
<mat-autocomplete #reactiveAuto="matAutocomplete" [displayWith]="displayFn">
<mat-option *ngFor="let state of tempStates" [value]="state">
<span>{{ state.name }}</span>
<span class="demo-secondary-text"> ({{ state.code }}) </span>
</mat-option>
</mat-autocomplete>
</mat-form-field>

<mat-card-actions>
<button mat-button (click)="stateCtrl.reset()">RESET</button>
<button mat-button (click)="stateCtrl.setValue(states[10])">SET VALUE</button>
<button mat-button (click)="stateCtrl.enabled ? stateCtrl.disable() : stateCtrl.enable()">
TOGGLE DISABLED
</button>
</mat-card-actions>

</mat-card>

<mat-card>

<div>Template-driven value (currentState): {{ currentState }}</div>
<div>Template-driven dirty: {{ modelDir ? modelDir.dirty : false }}</div>

<!-- Added an ngIf below to test that autocomplete works with ngIf -->
<mat-form-field *ngIf="true">
<mat-label>State</mat-label>
<input matInput [matAutocomplete]="tdAuto" [(ngModel)]="currentState"
(ngModelChange)="tdStates = filterStates(currentState)" [disabled]="tdDisabled">
<mat-autocomplete #tdAuto="matAutocomplete">
<mat-option *ngFor="let state of tdStates" [value]="state.name">
<span>{{ state.name }}</span>
</mat-option>
</mat-autocomplete>
</mat-form-field>

<mat-card-actions>
<button mat-button (click)="modelDir.reset()">RESET</button>
<button mat-button (click)="currentState='California'">SET VALUE</button>
<button mat-button (click)="tdDisabled=!tdDisabled">
TOGGLE DISABLED
</button>
</mat-card-actions>

</mat-card>

<mat-card>
<div>Option groups (currentGroupedState): {{ currentGroupedState }}</div>

<mat-form-field>
<mat-label>State</mat-label>
<input
matInput
[matAutocomplete]="groupedAuto"
[(ngModel)]="currentGroupedState"
(ngModelChange)="filteredGroupedStates = filterStateGroups(currentGroupedState)">
</mat-form-field>
</mat-card>
</div>

<mat-autocomplete #groupedAuto="matAutocomplete">
<mat-optgroup *ngFor="let group of filteredGroupedStates"
[label]="'States starting with ' + group.letter">
<mat-option *ngFor="let state of group.states" [value]="state.name">{{ state.name }}</mat-option>
</mat-optgroup>
</mat-autocomplete>
21 changes: 21 additions & 0 deletions src/dev-app/mdc-autocomplete/mdc-autocomplete-demo.scss
@@ -0,0 +1,21 @@
.demo-autocomplete {
display: flex;
flex-flow: row wrap;

.mat-mdc-card {
width: 400px;
margin: 24px;
padding: 16px;
}

.mat-mdc-form-field {
margin-top: 16px;
min-width: 200px;
max-width: 100%;
}
}

.demo-secondary-text {
color: rgba(0, 0, 0, 0.54);
margin-left: 8px;
}
145 changes: 145 additions & 0 deletions src/dev-app/mdc-autocomplete/mdc-autocomplete-demo.ts
@@ -0,0 +1,145 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Component, ViewChild} from '@angular/core';
import {FormControl, NgModel} from '@angular/forms';
import {Observable} from 'rxjs';
import {map, startWith} from 'rxjs/operators';


export interface State {
code: string;
name: string;
}

export interface StateGroup {
letter: string;
states: State[];
}

@Component({
selector: 'mdc-autocomplete-demo',
templateUrl: 'mdc-autocomplete-demo.html',
styleUrls: ['mdc-autocomplete-demo.css']
})
export class MdcAutocompleteDemo {
stateCtrl: FormControl;
currentState = '';
currentGroupedState = '';
topHeightCtrl = new FormControl(0);

reactiveStates: Observable<State[]>;
tdStates: State[];

tdDisabled = false;

@ViewChild(NgModel) modelDir: NgModel;

groupedStates: StateGroup[];
filteredGroupedStates: StateGroup[];
states: State[] = [
{code: 'AL', name: 'Alabama'},
{code: 'AK', name: 'Alaska'},
{code: 'AZ', name: 'Arizona'},
{code: 'AR', name: 'Arkansas'},
{code: 'CA', name: 'California'},
{code: 'CO', name: 'Colorado'},
{code: 'CT', name: 'Connecticut'},
{code: 'DE', name: 'Delaware'},
{code: 'FL', name: 'Florida'},
{code: 'GA', name: 'Georgia'},
{code: 'HI', name: 'Hawaii'},
{code: 'ID', name: 'Idaho'},
{code: 'IL', name: 'Illinois'},
{code: 'IN', name: 'Indiana'},
{code: 'IA', name: 'Iowa'},
{code: 'KS', name: 'Kansas'},
{code: 'KY', name: 'Kentucky'},
{code: 'LA', name: 'Louisiana'},
{code: 'ME', name: 'Maine'},
{code: 'MD', name: 'Maryland'},
{code: 'MA', name: 'Massachusetts'},
{code: 'MI', name: 'Michigan'},
{code: 'MN', name: 'Minnesota'},
{code: 'MS', name: 'Mississippi'},
{code: 'MO', name: 'Missouri'},
{code: 'MT', name: 'Montana'},
{code: 'NE', name: 'Nebraska'},
{code: 'NV', name: 'Nevada'},
{code: 'NH', name: 'New Hampshire'},
{code: 'NJ', name: 'New Jersey'},
{code: 'NM', name: 'New Mexico'},
{code: 'NY', name: 'New York'},
{code: 'NC', name: 'North Carolina'},
{code: 'ND', name: 'North Dakota'},
{code: 'OH', name: 'Ohio'},
{code: 'OK', name: 'Oklahoma'},
{code: 'OR', name: 'Oregon'},
{code: 'PA', name: 'Pennsylvania'},
{code: 'RI', name: 'Rhode Island'},
{code: 'SC', name: 'South Carolina'},
{code: 'SD', name: 'South Dakota'},
{code: 'TN', name: 'Tennessee'},
{code: 'TX', name: 'Texas'},
{code: 'UT', name: 'Utah'},
{code: 'VT', name: 'Vermont'},
{code: 'VA', name: 'Virginia'},
{code: 'WA', name: 'Washington'},
{code: 'WV', name: 'West Virginia'},
{code: 'WI', name: 'Wisconsin'},
{code: 'WY', name: 'Wyoming'},
];

constructor() {
this.tdStates = this.states;
this.stateCtrl = new FormControl({code: 'CA', name: 'California'});
this.reactiveStates = this.stateCtrl.valueChanges
.pipe(
startWith(this.stateCtrl.value),
map(val => this.displayFn(val)),
map(name => this.filterStates(name))
);

this.filteredGroupedStates = this.groupedStates =
this.states.reduce<StateGroup[]>((groups, state) => {
let group = groups.find(g => g.letter === state.name[0]);

if (!group) {
group = { letter: state.name[0], states: [] };
groups.push(group);
}

group.states.push({ code: state.code, name: state.name });

return groups;
}, []);
}

displayFn(value: any): string {
return value && typeof value === 'object' ? value.name : value;
}

filterStates(val: string) {
return val ? this._filter(this.states, val) : this.states;
}

filterStateGroups(val: string) {
if (val) {
return this.groupedStates
.map(group => ({ letter: group.letter, states: this._filter(group.states, val) }))
.filter(group => group.states.length > 0);
}

return this.groupedStates;
}

private _filter(states: State[], val: string) {
const filterValue = val.toLowerCase();
return states.filter(state => state.name.toLowerCase().startsWith(filterValue));
}
}

0 comments on commit c8f03c7

Please sign in to comment.