Skip to content
This repository was archived by the owner on Dec 18, 2024. It is now read-only.

Commit b82bc86

Browse files
committed
Add styling page to the docs
Adds a "Styling" page to each component that tells users how to customize the component's styles.
1 parent 0cc5af9 commit b82bc86

File tree

12 files changed

+357
-61
lines changed

12 files changed

+357
-61
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"@angular/cdk-experimental": "^19.0.0-next.9",
3838
"@angular/common": "^19.0.0-next.10",
3939
"@angular/compiler": "^19.0.0-next.10",
40-
"@angular/components-examples": "https://github.com/angular/material2-docs-content.git#4334c0c112b4a55c7257a5837ac94f5464938764",
40+
"@angular/components-examples": "https://github.com/angular/material2-docs-content.git#3e3187172e1edc005a6669fa214e7b4bdc6a230b",
4141
"@angular/core": "^19.0.0-next.10",
4242
"@angular/forms": "^19.0.0-next.10",
4343
"@angular/google-maps": "^19.0.0-next.9",

src/app/pages/component-sidenav/component-sidenav.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import {
5151
ComponentViewer,
5252
ComponentViewerModule
5353
} from '../component-viewer/component-viewer';
54+
import {ComponentStyling} from '../component-viewer/component-styling';
5455

5556
// These constants are used by the ComponentSidenav for orchestrating the MatSidenav in a responsive
5657
// way. This includes hiding the sidenav, defaulting it to open, changing the mode from over to
@@ -165,7 +166,8 @@ const routes: Routes = [{
165166
{path: '', redirectTo: 'overview', pathMatch: 'full'},
166167
{path: 'overview', component: ComponentOverview, pathMatch: 'full'},
167168
{path: 'api', component: ComponentApi, pathMatch: 'full'},
168-
{path: 'examples', component: ComponentExamples, pathMatch: 'full'}
169+
{path: 'styling', component: ComponentStyling, pathMatch: 'full'},
170+
{path: 'examples', component: ComponentExamples, pathMatch: 'full'},
169171
],
170172
},
171173
{path: '**', redirectTo: '/404'}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
@let item = docItem | async;
2+
@let data = dataStream | async;
3+
@let example = exampleStream | async;
4+
@let hasData = hasDataStream | async;
5+
6+
@if (!item || !data) {
7+
Loading...
8+
} @else if (!hasData) {
9+
This component does not support style overrides
10+
} @else {
11+
<h2 class="cdk-visually-hidden" tabindex="-1">How to style {{item.id}}</h2>
12+
Styles from the <code>{{item.packageName}}/{{item.id}}</code> package can be customized using
13+
@if (data.length === 1) {
14+
the <code>{{data[0].overridesMixin}}</code> mixin.
15+
} @else {
16+
the @for (current of data; track current.name) {{{$last ? ' and ' : ($first ? '' : ', ')}}<code>{{current.overridesMixin}}</code>} mixins.
17+
}
18+
{{data.length === 1 ? 'This mixin accepts' : 'These mixins accept'}} a set of tokens that control how the components will look, either for the entire app or under a specific selector. {{example ? 'For example:' : ''}}
19+
20+
@if (example) {
21+
<div class="docs-markdown">
22+
<pre>{{example}}</pre>
23+
</div>
24+
}
25+
26+
You can find the full list of supported mixins and tokens below.
27+
28+
<div class="docs-markdown">
29+
@for (current of data; track current.name) {
30+
<h3>Tokens supported by <code>{{current.overridesMixin}}</code></h3>
31+
<token-table [tokens]="current.tokens"/>
32+
}
33+
</div>
34+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import {Component, inject, Injectable} from '@angular/core';
2+
import {HttpClient} from '@angular/common/http';
3+
import {AsyncPipe} from '@angular/common';
4+
import {Observable} from 'rxjs';
5+
import {map, shareReplay, switchMap} from 'rxjs/operators';
6+
import {ComponentViewer} from './component-viewer';
7+
import {DocItem} from '../../shared/documentation-items/documentation-items';
8+
import {Token, TokenTable} from './token-table';
9+
10+
interface StyleOverridesData {
11+
name: string;
12+
overridesMixin: string;
13+
tokens: Token[];
14+
}
15+
16+
@Injectable({providedIn: 'root'})
17+
class TokenService {
18+
private _cache: Record<string, Observable<StyleOverridesData[]>> = {};
19+
20+
constructor(private _http: HttpClient) {}
21+
22+
getTokenData(item: DocItem): Observable<StyleOverridesData[]> {
23+
const url = `/docs-content/tokens/${item.packageName}/${item.id}/${item.id}.json`;
24+
25+
if (this._cache[url]) {
26+
return this._cache[url];
27+
}
28+
29+
const stream = this._http.get<StyleOverridesData[]>(url).pipe(shareReplay(1));
30+
this._cache[url] = stream;
31+
return stream;
32+
}
33+
}
34+
35+
@Component({
36+
selector: 'component-styling',
37+
templateUrl: './component-styling.html',
38+
standalone: true,
39+
imports: [AsyncPipe, TokenTable],
40+
})
41+
export class ComponentStyling {
42+
private componentViewer = inject(ComponentViewer);
43+
private tokenService = inject(TokenService);
44+
protected docItem = this.componentViewer.componentDocItem;
45+
protected dataStream =
46+
this.docItem.pipe(switchMap(item => this.tokenService.getTokenData(item)));
47+
protected hasDataStream = this.dataStream.pipe(
48+
map(data => data.length > 0 && data.some(d => d.tokens.length > 0)));
49+
50+
protected exampleStream = this.dataStream.pipe(map(data => {
51+
const mixin = data.find(d => d.tokens.length > 0);
52+
53+
if (!mixin) {
54+
return null;
55+
}
56+
57+
// Pick out a couple of color tokens to show as examples.
58+
const firstToken = mixin.tokens.find(token => token.type === 'color');
59+
const secondToken = mixin.tokens.find(token => token.type === 'color' && token !== firstToken);
60+
61+
if (!firstToken) {
62+
return null;
63+
}
64+
65+
const lines = [
66+
`@use '@angular/material' as mat;`,
67+
``,
68+
`// Customize the entire app. Change :root to your selector if you want to scope the styles.`,
69+
`:root {`,
70+
` @include mat.${mixin.overridesMixin}((`,
71+
` ${firstToken.overridesName}: orange,`,
72+
...(secondToken ? [` ${secondToken.overridesName}: red,`] : []),
73+
` ));`,
74+
`}`,
75+
];
76+
77+
return lines.join('\n');
78+
}));
79+
}

src/app/pages/component-viewer/component-viewer.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ describe('ComponentViewer', () => {
4444
throw Error(`Unable to find DocItem: '${docItemsId}' in section: 'material'.`);
4545
}
4646
const expected = `${docItem.name}`;
47-
expect(component._componentPageTitle.title).toEqual(expected);
47+
expect(component.componentPageTitle.title).toEqual(expected);
4848
});
4949
});
5050

src/app/pages/component-viewer/component-viewer.ts

Lines changed: 37 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -51,32 +51,47 @@ export class ComponentViewer implements OnDestroy {
5151
sections: Set<string> = new Set(['overview', 'api']);
5252
private _destroyed = new Subject<void>();
5353

54-
constructor(_route: ActivatedRoute, private router: Router,
55-
public _componentPageTitle: ComponentPageTitle,
56-
public docItems: DocumentationItems) {
57-
const routeAndParentParams = [_route.params];
58-
if (_route.parent) {
59-
routeAndParentParams.push(_route.parent.params);
54+
constructor(
55+
route: ActivatedRoute,
56+
private router: Router,
57+
public componentPageTitle: ComponentPageTitle,
58+
readonly docItems: DocumentationItems) {
59+
const routeAndParentParams = [route.params];
60+
if (route.parent) {
61+
routeAndParentParams.push(route.parent.params);
6062
}
6163
// Listen to changes on the current route for the doc id (e.g. button/checkbox) and the
6264
// parent route for the section (material/cdk).
6365
combineLatest(routeAndParentParams).pipe(
64-
map((params: Params[]) => ({id: params[0]['id'], section: params[1]['section']})),
65-
map((docIdAndSection: {id: string, section: string}) =>
66-
({doc: docItems.getItemById(docIdAndSection.id, docIdAndSection.section),
67-
section: docIdAndSection.section}), takeUntil(this._destroyed))
68-
).subscribe((docItemAndSection: {doc: DocItem | undefined, section: string}) => {
69-
if (docItemAndSection.doc !== undefined) {
70-
this.componentDocItem.next(docItemAndSection.doc);
71-
this._componentPageTitle.title = `${docItemAndSection.doc.name}`;
72-
73-
if (docItemAndSection.doc.examples && docItemAndSection.doc.examples.length) {
74-
this.sections.add('examples');
75-
} else {
76-
this.sections.delete('examples');
77-
}
66+
map((params: Params[]) => {
67+
const id = params[0]['id'];
68+
const section = params[1]['section'];
69+
70+
return ({
71+
doc: docItems.getItemById(id, section),
72+
section: section
73+
});
74+
},
75+
takeUntil(this._destroyed))
76+
).subscribe(({doc, section}) => {
77+
if (!doc) {
78+
this.router.navigate(['/' + section]);
79+
return;
80+
}
81+
82+
this.componentDocItem.next(doc);
83+
componentPageTitle.title = `${doc.name}`;
84+
85+
if (doc.hasStyling) {
86+
this.sections.add('styling');
7887
} else {
79-
this.router.navigate(['/' + docItemAndSection.section]);
88+
this.sections.delete('styling');
89+
}
90+
91+
if (doc.examples && doc.examples.length) {
92+
this.sections.add('examples');
93+
} else {
94+
this.sections.delete('examples');
8095
}
8196
});
8297
}
@@ -159,14 +174,6 @@ export class ComponentBaseView implements OnInit, OnDestroy {
159174
],
160175
})
161176
export class ComponentOverview extends ComponentBaseView {
162-
constructor(
163-
componentViewer: ComponentViewer,
164-
breakpointObserver: BreakpointObserver,
165-
changeDetectorRef: ChangeDetectorRef
166-
) {
167-
super(componentViewer, breakpointObserver, changeDetectorRef);
168-
}
169-
170177
getOverviewDocumentUrl(doc: DocItem) {
171178
// Use the explicit overview path if specified. Otherwise, compute an overview path based
172179
// on the package name and doc item id. Overviews for components are commonly stored in a
@@ -191,14 +198,6 @@ export class ComponentOverview extends ComponentBaseView {
191198
],
192199
})
193200
export class ComponentApi extends ComponentBaseView {
194-
constructor(
195-
componentViewer: ComponentViewer,
196-
breakpointObserver: BreakpointObserver,
197-
changeDetectorRef: ChangeDetectorRef
198-
) {
199-
super(componentViewer, breakpointObserver, changeDetectorRef);
200-
}
201-
202201
getApiDocumentUrl(doc: DocItem) {
203202
const apiDocId = doc.apiDocId || `${doc.packageName}-${doc.id}`;
204203
return `/docs-content/api-docs/${apiDocId}.html`;
@@ -215,15 +214,7 @@ export class ComponentApi extends ComponentBaseView {
215214
AsyncPipe,
216215
],
217216
})
218-
export class ComponentExamples extends ComponentBaseView {
219-
constructor(
220-
componentViewer: ComponentViewer,
221-
breakpointObserver: BreakpointObserver,
222-
changeDetectorRef: ChangeDetectorRef
223-
) {
224-
super(componentViewer, breakpointObserver, changeDetectorRef);
225-
}
226-
}
217+
export class ComponentExamples extends ComponentBaseView {}
227218

228219
@NgModule({
229220
imports: [
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {Component, input, inject} from '@angular/core';
2+
import {MatIconButton} from '@angular/material/button';
3+
import {Clipboard} from '@angular/cdk/clipboard';
4+
import {MatIcon} from '@angular/material/icon';
5+
import {MatSnackBar} from '@angular/material/snack-bar';
6+
import {MatTooltip} from '@angular/material/tooltip';
7+
8+
@Component({
9+
selector: 'token-name',
10+
standalone: true,
11+
template: `
12+
<code>{{name()}}</code>
13+
<button
14+
mat-icon-button
15+
matTooltip="Copy name to the clipboard"
16+
(click)="copy(name())">
17+
<mat-icon>content_copy</mat-icon>
18+
</button>
19+
`,
20+
styles: `
21+
:host {
22+
display: flex;
23+
align-items: center;
24+
25+
button {
26+
margin-left: 8px;
27+
}
28+
}
29+
`,
30+
imports: [MatIconButton, MatIcon, MatTooltip],
31+
})
32+
export class TokenName {
33+
private clipboard = inject(Clipboard);
34+
private snackbar = inject(MatSnackBar);
35+
36+
name = input.required<string>();
37+
38+
protected copy(name: string): void {
39+
const message = this.clipboard.copy(name) ? 'Copied token name' : 'Failed to copy token name';
40+
this.snackbar.open(message, undefined, {duration: 2500});
41+
}
42+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<div class="filters">
2+
<mat-form-field class="name-field" subscriptSizing="dynamic" appearance="outline">
3+
<mat-label>Filter by name</mat-label>
4+
<input
5+
#nameInput
6+
matInput
7+
[value]="nameFilter()"
8+
(input)="nameFilter.set(nameInput.value)"/>
9+
</mat-form-field>
10+
11+
<mat-form-field subscriptSizing="dynamic" appearance="outline">
12+
<mat-label>Filter by type</mat-label>
13+
<mat-select (selectionChange)="typeFilter.set($event.value)">
14+
@for (type of types; track $index) {
15+
<mat-option [value]="type">{{type | titlecase}}</mat-option>
16+
}
17+
</mat-select>
18+
</mat-form-field>
19+
20+
<button mat-button (click)="reset()">Reset filters</button>
21+
</div>
22+
23+
<div class="docs-markdown">
24+
<table>
25+
<thead>
26+
<tr>
27+
<th>Name</th>
28+
<th class="type-header">Type</th>
29+
<th class="system-header">Based on system token</th>
30+
</tr>
31+
</thead>
32+
33+
<tbody>
34+
@for (token of filteredTokens(); track token.overridesName) {
35+
<tr>
36+
<td><token-name [name]="token.overridesName"/></td>
37+
<td>{{token.type | titlecase}}</td>
38+
<td>
39+
@if (token.derivedFrom) {
40+
<token-name [name]="token.derivedFrom"/>
41+
} @else {
42+
None
43+
}
44+
</td>
45+
</tr>
46+
} @empty {
47+
<tr>
48+
<td>No tokens match the current set of filters</td>
49+
</tr>
50+
}
51+
</tbody>
52+
</table>
53+
</div>

0 commit comments

Comments
 (0)