Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(material-experimental/mdc-autocomplete): implement MDC-based mat-autocomplete #20247

Merged
merged 1 commit into from
Aug 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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));
}
}