mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
docs(docs-infra): Add package filter to the API list
This commit also introduces the usage of signal forms on adev
(cherry picked from commit 15308149db)
This commit is contained in:
parent
834bae1331
commit
1748c8984c
10 changed files with 123 additions and 215 deletions
|
|
@ -3,7 +3,7 @@
|
|||
<docs-text-field
|
||||
[autofocus]="true"
|
||||
[hideIcon]="true"
|
||||
[formControl]="searchControl"
|
||||
[field]="searchForm"
|
||||
[resetLabel]="'Clear the search'"
|
||||
class="docs-search-input"
|
||||
placeholder="Search docs"
|
||||
|
|
@ -59,7 +59,7 @@
|
|||
</li>
|
||||
}
|
||||
</ul>
|
||||
} @else if (history.hasItems() && !this.searchControl.value.length) {
|
||||
} @else if (history.hasItems() && !this.searchForm().value().length) {
|
||||
<docs-search-history />
|
||||
} @else {
|
||||
<div class="docs-search-results docs-mini-scroll-track">
|
||||
|
|
|
|||
|
|
@ -20,19 +20,19 @@ import {
|
|||
viewChildren,
|
||||
} from '@angular/core';
|
||||
|
||||
import {WINDOW} from '../../providers';
|
||||
import {ClickOutside, SearchItem} from '../../directives';
|
||||
import {WINDOW} from '../../providers';
|
||||
import {Search, SearchHistory} from '../../services';
|
||||
|
||||
import {TextField} from '../text-field/text-field.component';
|
||||
import {FormControl, ReactiveFormsModule} from '@angular/forms';
|
||||
import {ActiveDescendantKeyManager} from '@angular/cdk/a11y';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {Field, form} from '@angular/forms/signals';
|
||||
import {Router, RouterLink} from '@angular/router';
|
||||
import {fromEvent} from 'rxjs';
|
||||
import {AlgoliaIcon} from '../algolia-icon/algolia-icon.component';
|
||||
import {RelativeLink} from '../../pipes';
|
||||
import {AlgoliaIcon} from '../algolia-icon/algolia-icon.component';
|
||||
import {SearchHistoryComponent} from '../search-history/search-history.component';
|
||||
import {TextField} from '../text-field/text-field.component';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-search-dialog',
|
||||
|
|
@ -40,7 +40,7 @@ import {SearchHistoryComponent} from '../search-history/search-history.component
|
|||
imports: [
|
||||
ClickOutside,
|
||||
TextField,
|
||||
ReactiveFormsModule,
|
||||
Field,
|
||||
SearchItem,
|
||||
AlgoliaIcon,
|
||||
RelativeLink,
|
||||
|
|
@ -67,22 +67,14 @@ export class SearchDialog {
|
|||
this.injector,
|
||||
).withWrap();
|
||||
|
||||
readonly searchQuery = this.search.searchQuery;
|
||||
readonly resultsResource = this.search.resultsResource;
|
||||
readonly searchResults = this.search.searchResults;
|
||||
|
||||
// We use a FormControl instead of relying on NgModel+signal to avoid
|
||||
// the issue https://github.com/angular/angular/issues/13568
|
||||
// TODO: Use signal forms when available
|
||||
searchControl = new FormControl(this.searchQuery(), {nonNullable: true});
|
||||
searchForm = form(this.search.searchQuery);
|
||||
|
||||
constructor() {
|
||||
inject(DestroyRef).onDestroy(() => this.keyManager.destroy());
|
||||
|
||||
this.searchControl.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => {
|
||||
this.searchQuery.set(value);
|
||||
});
|
||||
|
||||
// Thinking about refactoring this to a single afterRenderEffect ?
|
||||
// Answer: It won't have the same behavior
|
||||
effect(() => {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<select
|
||||
[attr.id]="id()"
|
||||
[attr.name]="name()"
|
||||
[ngModel]="selectedOption()"
|
||||
(ngModelChange)="setOption($event)"
|
||||
[value]="value()"
|
||||
(change)="setOption($any($event.target).value)"
|
||||
>
|
||||
@for (item of options(); track item) {
|
||||
<option [value]="item.value">{{ item.label }}</option>
|
||||
<option [value]="item.value" [selected]="value() === item.value">{{ item.label }}</option>
|
||||
}
|
||||
</select>
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule} from '@angular/forms';
|
||||
import {ChangeDetectionStrategy, Component, model, forwardRef, input, signal} from '@angular/core';
|
||||
import {ChangeDetectionStrategy, Component, input, model} from '@angular/core';
|
||||
import {FormValueControl} from '@angular/forms/signals';
|
||||
|
||||
type SelectOptionValue = string | number | boolean;
|
||||
|
||||
|
|
@ -19,59 +19,25 @@ export interface SelectOption {
|
|||
@Component({
|
||||
selector: 'docs-select',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [FormsModule],
|
||||
templateUrl: './select.component.html',
|
||||
styleUrls: ['./select.component.scss'],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => Select),
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
host: {
|
||||
class: 'docs-form-element',
|
||||
},
|
||||
})
|
||||
export class Select implements ControlValueAccessor {
|
||||
export class Select implements FormValueControl<string | null> {
|
||||
readonly value = model<string | null>(null);
|
||||
|
||||
readonly id = input.required<string>({alias: 'selectId'});
|
||||
readonly name = input.required<string>();
|
||||
readonly options = input.required<SelectOption[]>();
|
||||
readonly disabled = model(false);
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
private onChange: (value: SelectOptionValue) => void = (_: SelectOptionValue) => {};
|
||||
private onTouched: () => void = () => {};
|
||||
|
||||
protected readonly selectedOption = signal<SelectOptionValue | null>(null);
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
writeValue(value: SelectOptionValue): void {
|
||||
this.selectedOption.set(value);
|
||||
}
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
registerOnChange(fn: any): void {
|
||||
this.onChange = fn;
|
||||
}
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
registerOnTouched(fn: any): void {
|
||||
this.onTouched = fn;
|
||||
}
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
setDisabledState?(isDisabled: boolean): void {
|
||||
this.disabled.set(isDisabled);
|
||||
}
|
||||
readonly disabled = input(false);
|
||||
|
||||
setOption($event: SelectOptionValue): void {
|
||||
if (this.disabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedOption.set($event);
|
||||
this.onChange($event);
|
||||
this.onTouched();
|
||||
this.value.set($event as string);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,27 +31,14 @@ describe('SlideToggle', () => {
|
|||
expect(component['checked']()).toBeTrue();
|
||||
});
|
||||
|
||||
it('should call onChange and onTouched when toggled', () => {
|
||||
const onChangeSpy = jasmine.createSpy('onChangeSpy');
|
||||
const onTouchedSpy = jasmine.createSpy('onTouchedSpy');
|
||||
component.registerOnChange(onChangeSpy);
|
||||
component.registerOnTouched(onTouchedSpy);
|
||||
|
||||
component.toggle();
|
||||
|
||||
expect(onChangeSpy).toHaveBeenCalled();
|
||||
expect(onChangeSpy).toHaveBeenCalledWith(true);
|
||||
expect(onTouchedSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set active class for button when is checked', async () => {
|
||||
component.writeValue(true);
|
||||
await fixture.whenStable();
|
||||
it('should set active class for button when is checked', () => {
|
||||
component.checked.set(true);
|
||||
fixture.detectChanges();
|
||||
const buttonElement: HTMLButtonElement = fixture.nativeElement.querySelector('input');
|
||||
expect(buttonElement.classList.contains('docs-toggle-active')).toBeTrue();
|
||||
|
||||
component.writeValue(false);
|
||||
await fixture.whenStable();
|
||||
component.checked.set(false);
|
||||
fixture.detectChanges();
|
||||
expect(buttonElement.classList.contains('docs-toggle-active')).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,52 +6,21 @@
|
|||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {ChangeDetectionStrategy, Component, forwardRef, model, input, signal} from '@angular/core';
|
||||
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
|
||||
import {ChangeDetectionStrategy, Component, input, model} from '@angular/core';
|
||||
import {FormCheckboxControl} from '@angular/forms/signals';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-slide-toggle',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: './slide-toggle.component.html',
|
||||
styleUrls: ['./slide-toggle.component.scss'],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => SlideToggle),
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class SlideToggle implements ControlValueAccessor {
|
||||
export class SlideToggle implements FormCheckboxControl {
|
||||
readonly buttonId = input.required<string>();
|
||||
readonly label = input.required<string>();
|
||||
readonly disabled = model(false);
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
private onChange: (value: boolean) => void = (_: boolean) => {};
|
||||
private onTouched: () => void = () => {};
|
||||
|
||||
protected readonly checked = signal(false);
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
writeValue(value: boolean): void {
|
||||
this.checked.set(value);
|
||||
}
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
registerOnChange(fn: any): void {
|
||||
this.onChange = fn;
|
||||
}
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
registerOnTouched(fn: any): void {
|
||||
this.onTouched = fn;
|
||||
}
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
setDisabledState(isDisabled: boolean): void {
|
||||
this.disabled.set(isDisabled);
|
||||
}
|
||||
readonly checked = model(false);
|
||||
|
||||
// Toggles the checked state of the slide-toggle.
|
||||
toggle(): void {
|
||||
|
|
@ -60,7 +29,5 @@ export class SlideToggle implements ControlValueAccessor {
|
|||
}
|
||||
|
||||
this.checked.update((checked) => !checked);
|
||||
this.onChange(this.checked());
|
||||
this.onTouched();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,91 +7,58 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
afterNextRender,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ElementRef,
|
||||
input,
|
||||
afterNextRender,
|
||||
forwardRef,
|
||||
signal,
|
||||
model,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import {ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR} from '@angular/forms';
|
||||
import {FormValueControl} from '@angular/forms/signals';
|
||||
import {IconComponent} from '../icon/icon.component';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-text-field',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [FormsModule, IconComponent],
|
||||
imports: [IconComponent],
|
||||
templateUrl: './text-field.component.html',
|
||||
styleUrls: ['./text-field.component.scss'],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => TextField),
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
host: {
|
||||
class: 'docs-form-element',
|
||||
},
|
||||
})
|
||||
export class TextField implements ControlValueAccessor {
|
||||
export class TextField implements FormValueControl<string | null> {
|
||||
readonly input = viewChild.required<ElementRef<HTMLInputElement>>('inputRef');
|
||||
|
||||
readonly name = input<string | null>(null);
|
||||
readonly name = input<string>('');
|
||||
readonly value = model<string | null>(null);
|
||||
readonly placeholder = input<string | null>(null);
|
||||
readonly disabled = model<boolean>(false);
|
||||
readonly hideIcon = input<boolean>(false);
|
||||
readonly autofocus = input<boolean>(false);
|
||||
readonly resetLabel = input<string | null>(null);
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
private onChange: (value: string) => void = (_: string) => {};
|
||||
private onTouched: () => void = () => {};
|
||||
|
||||
protected readonly value = signal<string | null>(null);
|
||||
|
||||
constructor() {
|
||||
afterNextRender(() => {
|
||||
if (this.autofocus()) {
|
||||
this.input().nativeElement.focus();
|
||||
this.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
writeValue(value: unknown): void {
|
||||
this.value.set(typeof value === 'string' ? value : null);
|
||||
}
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
registerOnChange(fn: any): void {
|
||||
this.onChange = fn;
|
||||
}
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
registerOnTouched(fn: any): void {
|
||||
this.onTouched = fn;
|
||||
}
|
||||
|
||||
// Implemented as part of ControlValueAccessor.
|
||||
setDisabledState?(isDisabled: boolean): void {
|
||||
this.disabled.set(isDisabled);
|
||||
}
|
||||
|
||||
setValue(value: string): void {
|
||||
if (this.disabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.value.set(value);
|
||||
this.onChange(value);
|
||||
this.onTouched();
|
||||
}
|
||||
|
||||
clearTextField() {
|
||||
this.setValue('');
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.input().nativeElement.focus();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,13 @@
|
|||
|
||||
<div class="adev-reference-list-filter">
|
||||
<div class="adev-reference-list-query-filter">
|
||||
<docs-text-field name="query" placeholder="Filter" [(ngModel)]="query" />
|
||||
<docs-text-field placeholder="Filter" [field]="form.query" />
|
||||
|
||||
<docs-select
|
||||
selectId="package-entry-filter"
|
||||
[options]="packageOptions()"
|
||||
[field]="form.selectedPackage"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="adev-reference-list-status">
|
||||
|
|
@ -23,7 +29,7 @@
|
|||
@for (itemType of itemTypes; track itemType) {
|
||||
<li
|
||||
class="adev-reference-list-type-filter-item"
|
||||
[class.adev-reference-list-type-filter-item-active]="type() === itemType"
|
||||
[class.adev-reference-list-type-filter-item-active]="form.type().value() === itemType"
|
||||
(click)="setItemType(itemType)"
|
||||
>
|
||||
<docs-api-item-label [type]="itemType" mode="short" class="docs-api-item-label" />
|
||||
|
|
|
|||
|
|
@ -8,16 +8,16 @@
|
|||
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {Location} from '@angular/common';
|
||||
import {signal} from '@angular/core';
|
||||
import {TextField} from '@angular/docs';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {provideRouter} from '@angular/router';
|
||||
import {RouterTestingHarness} from '@angular/router/testing';
|
||||
import {ApiItem} from '../interfaces/api-item';
|
||||
import {ApiItemType} from '../interfaces/api-item-type';
|
||||
import ApiReferenceList, {ALL_TYPES_KEY, STATUSES} from './api-reference-list.component';
|
||||
import {ApiReferenceManager} from './api-reference-manager.service';
|
||||
import {signal} from '@angular/core';
|
||||
import {ApiItemType} from '../interfaces/api-item-type';
|
||||
import {RouterTestingHarness} from '@angular/router/testing';
|
||||
import {provideRouter} from '@angular/router';
|
||||
import {Location} from '@angular/common';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {TextField} from '@angular/docs';
|
||||
import {ApiItem} from '../interfaces/api-item';
|
||||
|
||||
describe('ApiReferenceList', () => {
|
||||
let component: ApiReferenceList;
|
||||
|
|
@ -114,7 +114,7 @@ describe('ApiReferenceList', () => {
|
|||
fixture.componentRef.setInput('query', 'Item1');
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(component.filteredGroups()![0].items).toEqual([fakeItem1]);
|
||||
expect(component.filteredGroups()[0].items).toEqual([fakeItem1]);
|
||||
});
|
||||
|
||||
it('should display items which match the query by group title', async () => {
|
||||
|
|
@ -122,33 +122,33 @@ describe('ApiReferenceList', () => {
|
|||
await fixture.whenStable();
|
||||
|
||||
// Should find all items whose group title contains the query
|
||||
expect(component.filteredGroups()![0].items.length).toBeGreaterThan(0);
|
||||
expect(component.filteredGroups()![0].items).toContain(fakeItem1);
|
||||
expect(component.filteredGroups()[0].items.length).toBeGreaterThan(0);
|
||||
expect(component.filteredGroups()[0].items).toContain(fakeItem1);
|
||||
});
|
||||
|
||||
it('should display only class items when user selects Class in the Type select', async () => {
|
||||
fixture.componentRef.setInput('type', ApiItemType.CLASS);
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(component.type()).toEqual(ApiItemType.CLASS);
|
||||
expect(component.filteredGroups()![0].items).toEqual([fakeItem2]);
|
||||
expect(component.form.type().value()).toEqual(ApiItemType.CLASS);
|
||||
expect(component.filteredGroups()[0].items).toEqual([fakeItem2]);
|
||||
});
|
||||
|
||||
it('should set selected type when provided type is different than selected', async () => {
|
||||
expect(component.type()).toBe(ALL_TYPES_KEY);
|
||||
expect(component.form.type().value()).toBe(ALL_TYPES_KEY);
|
||||
component.setItemType(ApiItemType.BLOCK);
|
||||
await RouterTestingHarness.create(`/api?type=${ApiItemType.BLOCK}`);
|
||||
expect(component.type()).toBe(ApiItemType.BLOCK);
|
||||
expect(component.form.type().value()).toBe(ApiItemType.BLOCK);
|
||||
});
|
||||
|
||||
it('should reset selected type when provided type is equal to selected', async () => {
|
||||
component.setItemType(ApiItemType.BLOCK);
|
||||
const harness = await RouterTestingHarness.create(`/api?type=${ApiItemType.BLOCK}`);
|
||||
expect(component.type()).toBe(ApiItemType.BLOCK);
|
||||
expect(component.form.type().value()).toBe(ApiItemType.BLOCK);
|
||||
|
||||
component.setItemType(ApiItemType.BLOCK);
|
||||
harness.navigateByUrl(`/api`);
|
||||
expect(component.type()).toBe(ALL_TYPES_KEY);
|
||||
expect(component.form.type().value()).toBe(ALL_TYPES_KEY);
|
||||
});
|
||||
|
||||
it('should set the value of the queryParam equal to the query text field', async () => {
|
||||
|
|
@ -164,15 +164,15 @@ describe('ApiReferenceList', () => {
|
|||
const location = TestBed.inject(Location);
|
||||
|
||||
const textField = fixture.debugElement.query(By.directive(TextField));
|
||||
(textField.componentInstance as TextField).setValue('item1');
|
||||
fixture.componentInstance.form.query().value.set('item1');
|
||||
await fixture.whenStable();
|
||||
expect(location.path()).toBe(`?query=item1`);
|
||||
|
||||
component.setItemType(ApiItemType.BLOCK);
|
||||
fixture.componentInstance.form.type().value.set(ApiItemType.BLOCK);
|
||||
await fixture.whenStable();
|
||||
expect(location.path()).toBe(`?query=item1&type=${ApiItemType.BLOCK}`);
|
||||
|
||||
fixture.componentRef.setInput('status', STATUSES.experimental);
|
||||
fixture.componentInstance.form.status().value.set(STATUSES.experimental);
|
||||
await fixture.whenStable();
|
||||
expect(location.path()).toBe(
|
||||
`?query=item1&type=${ApiItemType.BLOCK}&status=${STATUSES.experimental}`,
|
||||
|
|
|
|||
|
|
@ -6,33 +6,33 @@
|
|||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {CdkMenuModule} from '@angular/cdk/menu';
|
||||
import {KeyValuePipe} from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ElementRef,
|
||||
EnvironmentInjector,
|
||||
afterNextRender,
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
input,
|
||||
linkedSignal,
|
||||
viewChild,
|
||||
afterNextRender,
|
||||
EnvironmentInjector,
|
||||
effect,
|
||||
input,
|
||||
} from '@angular/core';
|
||||
import ApiItemsSection from '../api-items-section/api-items-section.component';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {TextField} from '@angular/docs';
|
||||
import {KeyValuePipe} from '@angular/common';
|
||||
import {Params, Router} from '@angular/router';
|
||||
import {ApiItemType} from '../interfaces/api-item-type';
|
||||
import {ApiReferenceManager} from './api-reference-manager.service';
|
||||
import ApiItemLabel from '../api-item-label/api-item-label.component';
|
||||
import {ApiLabel} from '../pipes/api-label.pipe';
|
||||
import {ApiItemsGroup} from '../interfaces/api-items-group';
|
||||
import {CdkMenuModule} from '@angular/cdk/menu';
|
||||
import {Select, SelectOption, TextField} from '@angular/docs';
|
||||
import {Field, form} from '@angular/forms/signals';
|
||||
import {MatChipsModule} from '@angular/material/chips';
|
||||
import {Params, Router} from '@angular/router';
|
||||
import ApiItemLabel from '../api-item-label/api-item-label.component';
|
||||
import ApiItemsSection from '../api-items-section/api-items-section.component';
|
||||
import {ApiItemType} from '../interfaces/api-item-type';
|
||||
import {ApiItemsGroup} from '../interfaces/api-items-group';
|
||||
import {ApiLabel} from '../pipes/api-label.pipe';
|
||||
import {ApiReferenceManager} from './api-reference-manager.service';
|
||||
|
||||
export const ALL_TYPES_KEY = 'All';
|
||||
export const ALL_PACKAGES = 'All';
|
||||
export const STATUSES = {
|
||||
stable: 1,
|
||||
developerPreview: 2,
|
||||
|
|
@ -46,12 +46,13 @@ export const DEFAULT_STATUS = STATUSES.stable | STATUSES.developerPreview | STAT
|
|||
imports: [
|
||||
ApiItemsSection,
|
||||
ApiItemLabel,
|
||||
FormsModule,
|
||||
TextField,
|
||||
ApiLabel,
|
||||
CdkMenuModule,
|
||||
MatChipsModule,
|
||||
KeyValuePipe,
|
||||
Select,
|
||||
Field,
|
||||
],
|
||||
templateUrl: './api-reference-list.component.html',
|
||||
styleUrls: ['./api-reference-list.component.scss'],
|
||||
|
|
@ -61,19 +62,33 @@ export default class ApiReferenceList {
|
|||
// services
|
||||
private readonly apiReferenceManager = inject(ApiReferenceManager);
|
||||
private readonly router = inject(Router);
|
||||
private readonly filterInput = viewChild.required(TextField, {read: ElementRef});
|
||||
private readonly filterInput = viewChild.required(TextField);
|
||||
private readonly injector = inject(EnvironmentInjector);
|
||||
|
||||
// inputs
|
||||
readonly queryInput = input<string | undefined>('', {alias: 'query'});
|
||||
readonly typeInput = input<string | undefined>(ALL_TYPES_KEY, {alias: 'type'});
|
||||
readonly statusInput = input<number | undefined>(DEFAULT_STATUS, {alias: 'status'});
|
||||
protected selectedPackageInput = input<string | undefined>(ALL_PACKAGES, {alias: 'package'});
|
||||
|
||||
// inputs are route binded, they can reset to undefined
|
||||
// also we want a writable state, so we use a linked signal
|
||||
protected query = linkedSignal(() => this.queryInput() ?? '');
|
||||
public type = linkedSignal(() => this.typeInput() ?? ALL_TYPES_KEY);
|
||||
private status = linkedSignal(() => this.statusInput() ?? DEFAULT_STATUS);
|
||||
public form = form(
|
||||
linkedSignal(() => ({
|
||||
query: this.queryInput() ?? '',
|
||||
status: this.statusInput() ?? DEFAULT_STATUS,
|
||||
type: this.typeInput() ?? ALL_TYPES_KEY,
|
||||
selectedPackage: this.selectedPackageInput() ?? ALL_PACKAGES,
|
||||
})),
|
||||
);
|
||||
|
||||
protected packageOptions = computed<SelectOption[]>(() => [
|
||||
{label: 'All Packages', value: ALL_PACKAGES},
|
||||
...this.apiReferenceManager.apiGroups().map((group) => ({
|
||||
label: group.title,
|
||||
value: group.id,
|
||||
})),
|
||||
]);
|
||||
|
||||
// const state
|
||||
protected readonly itemTypes = Object.values(ApiItemType);
|
||||
|
|
@ -87,9 +102,9 @@ export default class ApiReferenceList {
|
|||
};
|
||||
|
||||
readonly filteredGroups = computed((): ApiItemsGroup[] => {
|
||||
const query = this.query().toLocaleLowerCase();
|
||||
const status = this.status();
|
||||
const type = this.type();
|
||||
const query = this.form.query().value().toLocaleLowerCase();
|
||||
const status = this.form.status().value();
|
||||
const type = this.form.type().value();
|
||||
return this.apiReferenceManager
|
||||
.apiGroups()
|
||||
.map((group) => ({
|
||||
|
|
@ -112,7 +127,12 @@ export default class ApiReferenceList {
|
|||
);
|
||||
}),
|
||||
}))
|
||||
.filter((group) => group.items.length > 0);
|
||||
.filter(
|
||||
(group) =>
|
||||
group.items.length > 0 &&
|
||||
(this.form.selectedPackage().value() === ALL_PACKAGES ||
|
||||
group.id === this.form.selectedPackage().value()),
|
||||
);
|
||||
});
|
||||
|
||||
constructor() {
|
||||
|
|
@ -121,10 +141,9 @@ export default class ApiReferenceList {
|
|||
afterNextRender(
|
||||
{
|
||||
write: () => {
|
||||
// Lord forgive me for I have sinned
|
||||
// Use the CVA to focus when https://github.com/angular/angular/issues/31133 is implemented
|
||||
// Why not improve this once https://github.com/orgs/angular/projects/60?pane=issue&itemId=143488813 is done
|
||||
if (matchMedia('(hover: hover) and (pointer:fine)').matches) {
|
||||
scheduleOnIdle(() => filterInput.nativeElement.querySelector('input').focus());
|
||||
scheduleOnIdle(() => filterInput.focus());
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
@ -135,9 +154,13 @@ export default class ApiReferenceList {
|
|||
effect(() => {
|
||||
// We'll only set the params if we deviate from the default values
|
||||
const params: Params = {
|
||||
'query': this.query() || null,
|
||||
'type': this.type() === ALL_TYPES_KEY ? null : this.type(),
|
||||
'status': this.status() === DEFAULT_STATUS ? null : this.status(),
|
||||
'query': this.form.query().value() || null,
|
||||
'type': this.form.type().value() === ALL_TYPES_KEY ? null : this.form.type().value(),
|
||||
'status': this.form.status().value() === DEFAULT_STATUS ? null : this.form.status().value(),
|
||||
'package':
|
||||
this.form.selectedPackage().value() === ALL_PACKAGES
|
||||
? null
|
||||
: this.form.selectedPackage().value(),
|
||||
};
|
||||
|
||||
this.router.navigate([], {
|
||||
|
|
@ -152,11 +175,11 @@ export default class ApiReferenceList {
|
|||
}
|
||||
|
||||
setItemType(itemType: ApiItemType): void {
|
||||
this.type.update((type) => (type === itemType ? ALL_TYPES_KEY : itemType));
|
||||
this.form.type().value.update((type) => (type === itemType ? ALL_TYPES_KEY : itemType));
|
||||
}
|
||||
|
||||
setStatus(status: number): void {
|
||||
this.status.update((previousStatus) => {
|
||||
this.form.status().value.update((previousStatus) => {
|
||||
if (this.isStatusSelected(status)) {
|
||||
return previousStatus & ~status; // Clear the bit
|
||||
} else {
|
||||
|
|
@ -166,7 +189,7 @@ export default class ApiReferenceList {
|
|||
}
|
||||
|
||||
isStatusSelected(status: number): boolean {
|
||||
return (this.status() & status) === status;
|
||||
return (this.form.status().value() & status) === status;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue