Custom type controls
A playground has built-in controls for strings, numbers, booleans and unions of literals. For an input of any other type, such as an interface, write a type control: a small form component that edits a value of that type. Once you register it, every playground uses it for inputs of that type.
See it
FloatingCircleComponent places a circle with its position input, an object with top and left:
NgDoc can't edit a FloatingCirclePosition by itself, so a playground of this component would skip the input. With a type control for the type, the inspector shows two fields instead:
Playground
Settings
Use it
Write a component that implements
. It is aNgDocTypeControl : the playground writes the value of the input withControlValueAccessor writeValue(), and the control reports changes through the function it gets inregisterOnChange(). The control of this page is below.Register it for the type with
, in the providers of your application:provideTypeControl app.config.tsTypeScriptimport {ApplicationConfig } from '@angular/core'; import {provideTypeControl } from '@ng-doc/app'; import { FloatingCirclePositionControlComponent } from './floating-circle-position-control.component'; export const appConfig:ApplicationConfig = { providers: [provideTypeControl ('FloatingCirclePosition', FloatingCirclePositionControlComponent, { hideLabel: true, }), ], };When only one page uses the control, you can register it in the
providersof that page instead, as this page does.Render a playground of a component with an input of that type (
Playgrounds ).
The control of the playground above:
import { ChangeDetectionStrategy , Component , input } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl , FormGroup , ReactiveFormsModule } from '@angular/forms';
import { NgDocTypeControl } from '@ng-doc/app';
import { EMPTY_FUNCTION } from '@ng-doc/core';
import {
NgDocInputStringDirective ,
NgDocInputWrapperComponent ,
NgDocLabelComponent ,
} from '@ng-doc/ui-kit';
import { FloatingCirclePosition } from '../floating-circle/floating-circle.component';
@Component ({
selector: 'ng-doc-floating-circle-position-control',
templateUrl: './floating-circle-position-control.component.html',
styleUrls: ['./floating-circle-position-control.component.css'],
changeDetection: ChangeDetectionStrategy .OnPush,
imports: [
ReactiveFormsModule ,
NgDocLabelComponent ,
NgDocInputWrapperComponent ,
NgDocInputStringDirective ,
],
})
export class FloatingCirclePositionControlComponent
implements NgDocTypeControl <FloatingCirclePosition>
{
/** The default value of the input, set by the playground. */
readonly default = input <FloatingCirclePosition>();
protected readonly model = new FormGroup ({
top: new FormControl <string | null>(null),
left: new FormControl <string | null>(null),
});
private touched: () => void = EMPTY_FUNCTION ;
private changed: (value: FloatingCirclePosition | null) => void = EMPTY_FUNCTION ;
constructor() {
this.model.valueChanges
.pipe(takeUntilDestroyed ())
.subscribe((value: FloatingCirclePosition) => this.changed(value));
}
writeValue(value: FloatingCirclePosition | null): void {
// `null` means no value: show the default of the input.
this.model.patchValue(
{ top: null, left: null, ...(value ?? this.default()) },
{ emitEvent: false },
);
}
registerOnChange(fn: (value: FloatingCirclePosition | null) => void): void {
this.changed = fn;
}
registerOnTouched(fn: () => void): void {
this.touched = fn;
}
protected blur(): void {
this.touched();
}
}Fields of a type control
Before the control renders, the playground sets these fields of . Declare the ones you need, as signal inputs or as plain fields:
| Field | Type | Value |
|---|---|---|
name | string | The name of the input, or its label from the controls of the playground. |
description | string | The description of the input, from its doc comment, as HTML. |
default | the type of the control | The default value of the input. |
options | Array<string | | The members of the input's union type: code, such as "'small'", or an enum member's label and value. |
isManual | boolean | true when the input comes from controls in the configuration of a playground. |
from @ng-doc/app turns an option into the value it sets and the text that shows it.
The playground sets a field that the control declares as an input, a signal input or an , with setInput(), so ngOnChanges() reports it. It assigns a plain field. With signal inputs, read the fields by calling them, such as this.default().
Type the value of an input
The value that a control edits is the value the input accepts. from @ng-doc/core resolves it from the component: for a signal input, it is the type the input is set with, before any transform; for an property, it is the property type.
import { NgDocTypeControl } from '@ng-doc/app';
import { InputType } from '@ng-doc/core';
import { FloatingCircleComponent } from './floating-circle.component';
// FloatingCirclePosition, read from `position = input<FloatingCirclePosition>(...)`
type Position = InputType <FloatingCircleComponent, 'position'>;
export class PositionControlComponent implements NgDocTypeControl <Position> {
// ...
}The control of this page names FloatingCirclePosition directly. keeps a control in step with the input it edits when the input's type changes.
Registering types
NgDoc finds a type control by the text of the input's type, as TypeScript prints it. For a signal input, that is the type argument of or , required or not. So these inputs need different registrations:
// Registered as 'FloatingCirclePosition'
readonly position = input <FloatingCirclePosition>({ top: '10px', left: '10px' });
readonly position = input .required<FloatingCirclePosition>();
// Registered as 'FloatingCirclePosition | undefined'
readonly position = input <FloatingCirclePosition>();
@Input () position?: FloatingCirclePosition;One control can serve several types. Make sure it handles every value of each type, here undefined:
providers: [
provideTypeControl ('FloatingCirclePosition', FloatingCirclePositionControlComponent, {
hideLabel: true,
}),
provideTypeControl ('FloatingCirclePosition | undefined', FloatingCirclePositionControlComponent, {
hideLabel: true,
}),
],The built-in string, number and boolean controls need no such registration: they also edit optional and nullable inputs of their type, such as , , or . Your own types match by their exact text only.
In development, the browser console names the type of every input that a playground skips, so you can copy the text to register.
Controls for array inputs
An array input, such as tags = , has no built-in control. Register a control for its type text, string[], like for any other type. This one edits the array as comma-separated text:
import { ChangeDetectionStrategy , Component , input , signal } from '@angular/core';
import { NgDocTypeControl } from '@ng-doc/app';
@Component ({
selector: 'app-string-list-control',
template: `<input [value]="text()" (input)="update($event)" (blur)="touched()" />`,
changeDetection: ChangeDetectionStrategy .OnPush,
})
export class StringListControlComponent implements NgDocTypeControl <string[]> {
readonly default = input <string[]>();
protected readonly text = signal ('');
protected touched: () => void = () => {};
private changed: (value: string[]) => void = () => {};
writeValue(value: string[] | null): void {
// `null` means no value: show the default of the input.
this.text.set((value ?? this.default() ?? []).join(', '));
}
registerOnChange(fn: (value: string[]) => void): void {
this.changed = fn;
}
registerOnTouched(fn: () => void): void {
this.touched = fn;
}
protected update(event: Event ): void {
const text = (event.target as HTMLInputElement).value;
this.text.set(text);
this.changed(
text
.split(',')
.map((item: string) => item.trim())
.filter(Boolean),
);
}
}providers: [
provideTypeControl ('string[]', StringListControlComponent),
// For `input<string[]>()` and `@Input() tags?: string[]`.
provideTypeControl ('string[] | undefined', StringListControlComponent),
],The same works for arrays of other types, such as number[] or Item[]: the control parses the text, or offers a form row per item, and reports the new array.
Options
The third argument of takes :
| Option | Type | Default | Description |
|---|---|---|---|
hideLabel | boolean | false | Hides the row's label, with the input's name and type chip, for a control that has its own. |
order | number | โ | The position of the control's rows in the inspector, lowest first. |
labelWrapper | boolean | true | Wraps the row in a <label>. Set false for a control with several interactive parts. |
The inspector lists the inputs whose controls have an order first, by that order, and then the others by name. The built-in controls have these orders:
| Control | Type | Order |
|---|---|---|
| A list of the values of a union | NgDocTypeAlias | 10 |
| A text field | string | 20 |
| A number field | number | 30 |
| A checkbox | boolean | 40 |
Controls with several interactive parts
Each row of the inspector is a <label> around the control. A click anywhere in a label also clicks the first field inside it, so a control with several parts, such as a list with a filter field or a set of buttons, reacts to clicks it shouldn't. Set labelWrapper: false and the row becomes a <div> instead, named by its caption for assistive technology:
provideTypeControl ('Items', ItemsControlComponent, { labelWrapper: false }),The built-in controls keep their label.
Appearance
Build the control from the components of @ng-doc/ui-kit, as the built-in controls do, so it matches the inspector: with or for fields, for labels, and for checkboxes. The inspector sets these variables:
| Variable | Default | Description |
|---|---|---|
--ng-doc-playground-inspector-width | 312px | The width of the inspector beside the demo. |
--ng-doc-playground-input-border | --ng-doc-input-border | The border of the inputs in the inspector. |
Gotchas
WarningType names are compared as text, so a registration of
FloatingCirclePosition|undefined, without the spaces that TypeScript prints, matches no input.
๐ก TipA control registered in the
providersof a page is registered when the code of that page loads. Register controls that the playgrounds of several pages use in the providers of your application.
Next: