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

Commit

Permalink
Add styling page to the docs
Browse files Browse the repository at this point in the history
Adds a "Styling" page to each component that tells users how to customize the component's styles.
  • Loading branch information
crisbeto committed Oct 30, 2024
1 parent 0cc5af9 commit 8c0f840
Show file tree
Hide file tree
Showing 12 changed files with 357 additions and 61 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"@angular/cdk-experimental": "^19.0.0-next.9",
"@angular/common": "^19.0.0-next.10",
"@angular/compiler": "^19.0.0-next.10",
"@angular/components-examples": "https://github.com/angular/material2-docs-content.git#4334c0c112b4a55c7257a5837ac94f5464938764",
"@angular/components-examples": "https://github.com/angular/material2-docs-content.git#3e3187172e1edc005a6669fa214e7b4bdc6a230b",
"@angular/core": "^19.0.0-next.10",
"@angular/forms": "^19.0.0-next.10",
"@angular/google-maps": "^19.0.0-next.9",
Expand Down
4 changes: 3 additions & 1 deletion src/app/pages/component-sidenav/component-sidenav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
ComponentViewer,
ComponentViewerModule
} from '../component-viewer/component-viewer';
import {ComponentStyling} from '../component-viewer/component-styling';

// These constants are used by the ComponentSidenav for orchestrating the MatSidenav in a responsive
// way. This includes hiding the sidenav, defaulting it to open, changing the mode from over to
Expand Down Expand Up @@ -165,7 +166,8 @@ const routes: Routes = [{
{path: '', redirectTo: 'overview', pathMatch: 'full'},
{path: 'overview', component: ComponentOverview, pathMatch: 'full'},
{path: 'api', component: ComponentApi, pathMatch: 'full'},
{path: 'examples', component: ComponentExamples, pathMatch: 'full'}
{path: 'styling', component: ComponentStyling, pathMatch: 'full'},
{path: 'examples', component: ComponentExamples, pathMatch: 'full'},
],
},
{path: '**', redirectTo: '/404'}
Expand Down
34 changes: 34 additions & 0 deletions src/app/pages/component-viewer/component-styling.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
@let item = docItem | async;
@let data = dataStream | async;
@let example = exampleStream | async;
@let hasData = hasDataStream | async;

@if (!item || !data) {
Loading...
} @else if (!hasData) {
This component does not support style overrides
} @else {
<h2 class="cdk-visually-hidden" tabindex="-1">How to style {{item.id}}</h2>
Styles from the <code>{{item.packageName}}/{{item.id}}</code> package can be customized using
@if (data.length === 1) {
the <code>{{data[0].overridesMixin}}</code> mixin.
} @else {
the @for (current of data; track current.name) {{{$last ? ' and ' : ($first ? '' : ', ')}}<code>{{current.overridesMixin}}</code>} mixins.
}
{{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:' : ''}}

@if (example) {
<div class="docs-markdown">
<pre>{{example}}</pre>
</div>
}

You can find the full list of supported mixins and tokens below.

<div class="docs-markdown">
@for (current of data; track current.name) {
<h3>Tokens supported by <code>{{current.overridesMixin}}</code></h3>
<token-table [tokens]="current.tokens"/>
}
</div>
}
79 changes: 79 additions & 0 deletions src/app/pages/component-viewer/component-styling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {Component, inject, Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {AsyncPipe} from '@angular/common';
import {Observable} from 'rxjs';
import {map, shareReplay, switchMap} from 'rxjs/operators';
import {ComponentViewer} from './component-viewer';
import {DocItem} from '../../shared/documentation-items/documentation-items';
import {Token, TokenTable} from './token-table';

interface StyleOverridesData {
name: string;
overridesMixin: string;
tokens: Token[];
}

@Injectable({providedIn: 'root'})
class TokenService {
private _cache: Record<string, Observable<StyleOverridesData[]>> = {};

constructor(private _http: HttpClient) {}

getTokenData(item: DocItem): Observable<StyleOverridesData[]> {
const url = `/docs-content/tokens/${item.packageName}/${item.id}/${item.id}.json`;

if (this._cache[url]) {
return this._cache[url];
}

const stream = this._http.get<StyleOverridesData[]>(url).pipe(shareReplay(1));
this._cache[url] = stream;
return stream;
}
}

@Component({
selector: 'component-styling',
templateUrl: './component-styling.html',
standalone: true,
imports: [AsyncPipe, TokenTable],
})
export class ComponentStyling {
private componentViewer = inject(ComponentViewer);
private tokenService = inject(TokenService);
protected docItem = this.componentViewer.componentDocItem;
protected dataStream =
this.docItem.pipe(switchMap(item => this.tokenService.getTokenData(item)));
protected hasDataStream = this.dataStream.pipe(
map(data => data.length > 0 && data.some(d => d.tokens.length > 0)));

protected exampleStream = this.dataStream.pipe(map(data => {
const mixin = data.find(d => d.tokens.length > 0);

if (!mixin) {
return null;
}

// Pick out a couple of color tokens to show as examples.
const firstToken = mixin.tokens.find(token => token.type === 'color');
const secondToken = mixin.tokens.find(token => token.type === 'color' && token !== firstToken);

if (!firstToken) {
return null;
}

const lines = [
`@use '@angular/material' as mat;`,
``,
`// Customize the entire app. Change :root to your selector if you want to scope the styles.`,
`:root {`,
` @include mat.${mixin.overridesMixin}((`,
` ${firstToken.overridesName}: orange,`,
...(secondToken ? [` ${secondToken.overridesName}: red,`] : []),
` ));`,
`}`,
];

return lines.join('\n');
}));
}
2 changes: 1 addition & 1 deletion src/app/pages/component-viewer/component-viewer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe('ComponentViewer', () => {
throw Error(`Unable to find DocItem: '${docItemsId}' in section: 'material'.`);
}
const expected = `${docItem.name}`;
expect(component._componentPageTitle.title).toEqual(expected);
expect(component.componentPageTitle.title).toEqual(expected);
});
});

Expand Down
83 changes: 37 additions & 46 deletions src/app/pages/component-viewer/component-viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,32 +51,47 @@ export class ComponentViewer implements OnDestroy {
sections: Set<string> = new Set(['overview', 'api']);
private _destroyed = new Subject<void>();

constructor(_route: ActivatedRoute, private router: Router,
public _componentPageTitle: ComponentPageTitle,
public docItems: DocumentationItems) {
const routeAndParentParams = [_route.params];
if (_route.parent) {
routeAndParentParams.push(_route.parent.params);
constructor(
route: ActivatedRoute,
private router: Router,
public componentPageTitle: ComponentPageTitle,
readonly docItems: DocumentationItems) {
const routeAndParentParams = [route.params];
if (route.parent) {
routeAndParentParams.push(route.parent.params);
}
// Listen to changes on the current route for the doc id (e.g. button/checkbox) and the
// parent route for the section (material/cdk).
combineLatest(routeAndParentParams).pipe(
map((params: Params[]) => ({id: params[0]['id'], section: params[1]['section']})),
map((docIdAndSection: {id: string, section: string}) =>
({doc: docItems.getItemById(docIdAndSection.id, docIdAndSection.section),
section: docIdAndSection.section}), takeUntil(this._destroyed))
).subscribe((docItemAndSection: {doc: DocItem | undefined, section: string}) => {
if (docItemAndSection.doc !== undefined) {
this.componentDocItem.next(docItemAndSection.doc);
this._componentPageTitle.title = `${docItemAndSection.doc.name}`;

if (docItemAndSection.doc.examples && docItemAndSection.doc.examples.length) {
this.sections.add('examples');
} else {
this.sections.delete('examples');
}
map((params: Params[]) => {
const id = params[0]['id'];
const section = params[1]['section'];

return ({
doc: docItems.getItemById(id, section),
section: section
});
},
takeUntil(this._destroyed))
).subscribe(({doc, section}) => {
if (!doc) {
this.router.navigate(['/' + section]);
return;
}

this.componentDocItem.next(doc);
componentPageTitle.title = `${doc.name}`;

if (doc.hasStyling) {
this.sections.add('styling');
} else {
this.router.navigate(['/' + docItemAndSection.section]);
this.sections.delete('styling');
}

if (doc.examples && doc.examples.length) {
this.sections.add('examples');
} else {
this.sections.delete('examples');
}
});
}
Expand Down Expand Up @@ -159,14 +174,6 @@ export class ComponentBaseView implements OnInit, OnDestroy {
],
})
export class ComponentOverview extends ComponentBaseView {
constructor(
componentViewer: ComponentViewer,
breakpointObserver: BreakpointObserver,
changeDetectorRef: ChangeDetectorRef
) {
super(componentViewer, breakpointObserver, changeDetectorRef);
}

getOverviewDocumentUrl(doc: DocItem) {
// Use the explicit overview path if specified. Otherwise, compute an overview path based
// on the package name and doc item id. Overviews for components are commonly stored in a
Expand All @@ -191,14 +198,6 @@ export class ComponentOverview extends ComponentBaseView {
],
})
export class ComponentApi extends ComponentBaseView {
constructor(
componentViewer: ComponentViewer,
breakpointObserver: BreakpointObserver,
changeDetectorRef: ChangeDetectorRef
) {
super(componentViewer, breakpointObserver, changeDetectorRef);
}

getApiDocumentUrl(doc: DocItem) {
const apiDocId = doc.apiDocId || `${doc.packageName}-${doc.id}`;
return `/docs-content/api-docs/${apiDocId}.html`;
Expand All @@ -215,15 +214,7 @@ export class ComponentApi extends ComponentBaseView {
AsyncPipe,
],
})
export class ComponentExamples extends ComponentBaseView {
constructor(
componentViewer: ComponentViewer,
breakpointObserver: BreakpointObserver,
changeDetectorRef: ChangeDetectorRef
) {
super(componentViewer, breakpointObserver, changeDetectorRef);
}
}
export class ComponentExamples extends ComponentBaseView {}

@NgModule({
imports: [
Expand Down
42 changes: 42 additions & 0 deletions src/app/pages/component-viewer/token-name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {Component, input, inject} from '@angular/core';
import {MatIconButton} from '@angular/material/button';
import {Clipboard} from '@angular/cdk/clipboard';
import {MatIcon} from '@angular/material/icon';
import {MatSnackBar} from '@angular/material/snack-bar';
import {MatTooltip} from '@angular/material/tooltip';

@Component({
selector: 'token-name',
standalone: true,
template: `
<code>{{name()}}</code>
<button
mat-icon-button
matTooltip="Copy name to the clipboard"
(click)="copy(name())">
<mat-icon>content_copy</mat-icon>
</button>
`,
styles: `
:host {
display: flex;
align-items: center;
button {
margin-left: 8px;
}
}
`,
imports: [MatIconButton, MatIcon, MatTooltip],
})
export class TokenName {
private clipboard = inject(Clipboard);
private snackbar = inject(MatSnackBar);

name = input.required<string>();

protected copy(name: string): void {
const message = this.clipboard.copy(name) ? 'Copied token name' : 'Failed to copy token name';
this.snackbar.open(message, undefined, {duration: 2500});
}
}
53 changes: 53 additions & 0 deletions src/app/pages/component-viewer/token-table.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<div class="filters">
<mat-form-field class="name-field" subscriptSizing="dynamic" appearance="outline">
<mat-label>Filter by name</mat-label>
<input
#nameInput
matInput
[value]="nameFilter()"
(input)="nameFilter.set(nameInput.value)"/>
</mat-form-field>

<mat-form-field subscriptSizing="dynamic" appearance="outline">
<mat-label>Filter by type</mat-label>
<mat-select (selectionChange)="typeFilter.set($event.value)">
@for (type of types; track $index) {
<mat-option [value]="type">{{type | titlecase}}</mat-option>
}
</mat-select>
</mat-form-field>

<button mat-button (click)="reset()">Reset filters</button>
</div>

<div class="docs-markdown">
<table>
<thead>
<tr>
<th>Name</th>
<th class="type-header">Type</th>
<th class="system-header">Based on system token</th>
</tr>
</thead>

<tbody>
@for (token of filteredTokens(); track token.overridesName) {
<tr>
<td><token-name [name]="token.overridesName"/></td>
<td>{{token.type | titlecase}}</td>
<td>
@if (token.derivedFrom) {
<token-name [name]="token.derivedFrom"/>
} @else {
None
}
</td>
</tr>
} @empty {
<tr>
<td>No tokens match the current set of filters</td>
</tr>
}
</tbody>
</table>
</div>
Loading

0 comments on commit 8c0f840

Please sign in to comment.