Skip to content

Playgrounds

A playground renders a component, a directive or a pipe next to an inspector with a control for each of its inputs. Readers change the values and see the result and its code at once, without writing a demo for every state.

 See it

Tag Label

Playground

Settings

The demo sits on the left, and the inspector on the right. When the playground is narrower than 640 pixels, the inspector moves below the demo.

  • Settings has a row for each input: its name, a chip with the type of its value, and a control. Hover the name to read the input's description, taken from its doc comment.
  • Recreate creates the demo again every time an input changes, instead of updating the inputs of the same instance. Use it for components that read an input only once, and turn it on from the start with the recreate option (see "Options").
  • Reset appears as soon as a value differs from its default, and sets every input back.
  • The code button beside the demo shows, below it, the template of the current state.

 Use it

  1. Add the playground to the playgrounds of the page. The key is the name of the playground:

    ng-doc.page.tsTypeScript
    import { NgDocPage } from '@ng-doc/core';
    import { NgDocTagComponent } from '@ng-doc/ui-kit';
    
    const MyPage: NgDocPage = {
      title: 'Tag',
      mdFile: './index.md',
      playgrounds: {
        TagPlayground: {
          target: NgDocTagComponent,
          template: `<ng-doc-selector>Tag Label</ng-doc-selector>`,
        },
      },
    };
    
    export default MyPage;
  2. Render it in the Markdown with the playground action:

    index.mdTwig
    {{ NgDocActions.playground("TagPlayground") }}

target is the class to play with. template is the Angular template that renders it, and <ng-doc-selector> stands for the target's selector. Prefer it to the real selector: the playground keeps working when you rename the selector. A standalone target needs no imports; for a target declared in an NgModule, add the module to the imports of the page.

The template is static text: it can't read variables or call functions of the page. Use data for values (see below).

 Inputs and controls

NgDoc reads the inputs of the target when it builds the page: signal inputs (input() and model(), required or not), @Input() properties, and the inputs of its base classes. An alias becomes the name of the row. The type of the input chooses the control:

Type of the inputControl
stringA text field.
numberA number field.
booleanA checkbox.
A union of literals, or an alias of itA list of the values, such as 'small' | 'medium' | 'large'.
An enumA list of the member names, such as Good, that sets the member's value.
string[] or number[]A chip field: the items are chips that you add, edit, remove and reorder.
An array of a union of literalsA chip field that picks the values of the union, such as NgDocSize[].
Any other array, or a tupleA JSON field.
A type with a type controlYour control (Custom type controls).
Any other typeNo control. In development, the browser console names the skipped input.

For a signal input, the type is the type argument: size = input<NgDocSize>('small') gets the list of NgDocSize. An input with a transform, such as rounded = input<boolean, unknown>(false, { transform: booleanAttribute }), gets the control of its first type argument, here a checkbox. An optional or nullable string, number or boolean input, such as label = input<string>(), gets the control of its type too.

The list of a union shows the values in the order the type is written. NgDocSize is written 'small' | 'medium' | 'large', so its list starts with small. The default value of the input is marked in the list.

The list of an enum shows the names of its members, and choosing one sets the member's value:

status.component.tsTypeScript
export enum Status {
  Good = 'good',
  Bad = 'bad',
}

@Component({ selector: 'app-status', template: '' })
export class StatusComponent {
  // The list shows Good and Bad; choosing Good sets 'good'.
  readonly status = input<Status>(Status.Good);
}
Warning

Only the new engine lists the members of an enum. With the legacy builders, an enum input gets no control: list its values in controls with the type NgDocTypeAlias (see "Controls for other inputs").

Arrays

An array of strings or numbers gets a chip field: each item is a chip, and a text field follows them, all in one box. An array of a union of literals gets the same field, which picks the values of the union. Any other array gets a JSON field. Try them with the tags, sizes, palette and accents inputs of this component:

tag-cloud.component.tsTypeScript
export interface TagCloudAccent {
  tag: string;
  color: NgDocColor;
}

export class TagCloudComponent {
  readonly tags = input<string[]>(['Angular', 'NgDoc', 'Playground']);
  readonly sizes = input<number[]>();
  readonly palette = input<NgDocColor[]>(['primary', 'info', 'warning']);
  readonly accents = input<TagCloudAccent[]>([{ tag: 'NgDoc', color: 'success' }]);
}
AngularNgDocPlayground

Playground

Settings
tags
  • Angular
  • NgDoc
  • Playground
sizes
undefined
palette
  • primary
  • info
  • warning

The chip field edits string[] and number[] inputs, however they are written: Array<string>, readonly string[] and ReadonlyArray<string> too, and optional or nullable ones, such as input<number[]>().

  • Add: type a value and press Enter. While you type, a list shows the matching suggestions, and a row that adds the text itself when no suggestion is that text. The arrow keys move through the list, and Enter or a click picks a row; Escape closes the list.
  • Suggestions: the items of the input's default value and the items you removed, so a removed item can come back. An item that the array holds is not suggested, but you can still type it: an array of strings or numbers may hold an item twice.
  • Remove: click the × of a chip, or press Backspace in the empty text field to remove the last chip.
  • Reorder: drag a chip to another place. With the keyboard, the arrow keys move between the chips and the text field, Alt+Left and Alt+Right move the focused chip, and Delete removes it.
  • Edit: double-click a chip, or press Enter or F2 on it, to edit its value in place. Enter keeps the new value, and Escape the old one.
  • Numbers: the items of a number[] are numbers. Text that is not a number is marked as invalid and isn't added.
  • Empty values: undefined, null and an empty array are different values, and the field shows which one the input has. The first item you add makes an array, and removing the last item leaves []. The × at the end of the field resets the input to its default value, such as undefined for an optional input.

An array of a union of literals, such as palette above, gets a multi-select: NgDocSize[] (an alias of a union), ('a' | 'b')[], Array<'x' | 'y'>, an array of an enum, and optional or nullable ones. Its list shows every value of the union, as the list of a union does, and marks the values that the array holds. Picking a value adds it, picking a marked value removes it, and the text you type filters the values instead of adding text. It moves and removes chips like the other chip fields. Both engines give such an input its values; an array of boolean gets the JSON field.

The JSON field edits every other array: arrays of objects (Item[], Array<{ x: number }>), nested arrays (string[][]), arrays of unions that are not only literals ((string | number)[]) and tuples ([string, number]), optional or not. It shows the value as JSON, and sets the input when the text is valid JSON. While it is not, the field is marked as invalid and the input keeps its last valid value. An empty field sets undefined.

The type of an input is read as TypeScript prints it, so an input typed with an alias of an array, such as type Tags = string[], is typed Tags and gets no control. Register one of the built-in controls for the alias (Custom type controls [Reuse the built-in controls]). In controls, name an array type such as 'string[]', 'NgDocArray' for the JSON field, or 'NgDocTypeAlias[]' with options for the multi-select:

ng-doc.page.tsTypeScript
controls: {
  sizes: { type: 'NgDocTypeAlias[]', options: ['small', 'medium', 'large'] },
},

Multiple selectors

When the target has several selectors, NgDoc renders a demo for each of them. To render only some, set selectors in the configuration or in the action:

index.mdTwig
{{ NgDocActions.playground("ButtonPlayground", { selectors: "button[ng-doc-button-flat]" }) }}

Playground

Settings

Inputs set in the template

An input that the template sets gets no control. So you can bind an input to something that a control can't edit, such as a reference to another element:

ng-doc.page.tsTypeScript
import { NgDocPage } from '@ng-doc/core';

import { TooltipComponent } from './tooltip.component';

const MyPage: NgDocPage = {
  title: 'Tooltip',
  mdFile: './index.md',
  playgrounds: {
    TooltipPlayground: {
      target: TooltipComponent,
      template: `
        <ng-doc-selector [anchor]="anchor">Hello</ng-doc-selector>
        <input #anchor value="Hover me" />
      `,
    },
  },
};

export default MyPage;

To start with a value and keep the control, use the inputs or defaults options instead (see "Options").

Optional content

content adds toggles for parts of the template, for example the content that a component projects. Each key is a slot: write {{ content.<key> }} where it goes in the template. The inspector lists the slots under Content:

ng-doc.page.tsTypeScript
import { NgDocPage } from '@ng-doc/core';
import { NgDocIconComponent, NgDocTagComponent } from '@ng-doc/ui-kit';

const MyPage: NgDocPage = {
  title: 'Tag',
  mdFile: './index.md',
  imports: [NgDocIconComponent],
  playgrounds: {
    TagIconPlayground: {
      target: NgDocTagComponent,
      template: `
        <ng-doc-selector>
          {{ content.icon }}
          Tag Label
        </ng-doc-selector>`,
      content: {
        icon: {
          label: 'email icon',
          template: '<ng-doc-icon icon="at-sign" [size]="16"></ng-doc-icon>',
        },
      },
    },
  },
};

export default MyPage;

A component in a slot template must be in the imports of the page.

Tag Label

Playground

Settings
Content

Data

data passes values to the template, which reads them as data:

ng-doc.page.tsTypeScript
import { NgDocPage } from '@ng-doc/core';
import { NgDocTagComponent } from '@ng-doc/ui-kit';

const MyPage: NgDocPage = {
  title: 'Tag',
  mdFile: './index.md',
  playgrounds: {
    TagDataPlayground: {
      target: NgDocTagComponent,
      template: `<ng-doc-selector>{{ data.array | json }}</ng-doc-selector>`,
      data: {
        array: [1, 2, 3],
      },
    },
  },
};

export default MyPage;
[ 1, 2, 3 ]

Playground

Settings

Directives and pipes

A directive works like a component. The template applies it to an element:

ng-doc.page.tsTypeScript
import { NgDocPage } from '@ng-doc/core';
import { NgDocRotatorDirective } from '@ng-doc/ui-kit';

const MyPage: NgDocPage = {
  title: 'Rotator',
  mdFile: './index.md',
  playgrounds: {
    RotatorPlayground: {
      target: NgDocRotatorDirective,
      template: `<button ngDocRotator>Button</button>`,
    },
  },
};

export default MyPage;

Playground

Settings

For a pipe, the parameters of transform after the value are the inputs. Take this pipe:

format-date.pipe.tsTypeScript
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'formatDate',
  standalone: true,
})
export class FormatDatePipe implements PipeTransform {
  /**
   * Transform pipe method
   * @param value - ISO date string
   * @param display - Display format
   * @param utc - Determines if the date should be displayed in UTC
   */
  transform(
    value: string,
    display: 'date' | 'time' | 'datetime' = 'datetime',
    utc: boolean,
  ): string {
    const date: Date = new Date(value);
    const timeZone: 'UTC' | undefined = utc ? 'UTC' : undefined;

    if (display === 'date') {
      return date.toLocaleDateString('en-US', { timeZone });
    } else if (display === 'time') {
      return date.toLocaleTimeString('en-US', { timeZone });
    } else {
      return date.toLocaleString('en-US', { timeZone });
    }
  }
}

The template applies the pipe to a value, without parameters: NgDoc binds them to the controls.

ng-doc.page.tsTypeScript
import { NgDocPage } from '@ng-doc/core';

import { FormatDatePipe } from './format-date.pipe';

const MyPage: NgDocPage = {
  title: 'Format date',
  mdFile: './index.md',
  playgrounds: {
    DatePipePlayground: {
      target: FormatDatePipe,
      template: `{{ '2023-06-05T08:00:00.000Z' | formatDate }}`,
    },
  },
};

export default MyPage;
6/5/2023, 8:00:00 AM

Playground

Settings

The descriptions come from the @param tags of transform, and the defaults from its default parameter values.

Isolated playgrounds

An isolated playground shows its demos in iframes, as isolated demos do (Isolated demos). Each target runs on a page of its own, which loads only Angular and the target: the documentation page's styles and providers don't reach it, and its media queries see the width of its iframe. The inspector stays on the documentation page. Isolated playgrounds are part of the new engine.

Loading the demo. Open it in a new tab

Change a value: the inspector writes the values into the iframe's URL, after the #, and the target takes them without reloading. Open in a new tab, beside the demo, opens the target's page with the current values.

Isolate one playground with the isolated option of the action, or of its configuration in ng-doc.page.ts:

index.mdTwig
{{ NgDocActions.playground("TagPlayground", { isolated: true }) }}

Or isolate every playground of the site in ng-doc.config.ts, and keep one in the page with isolated: false:

ng-doc.config.tsTypeScript
import { NgDocConfiguration } from '@ng-doc/builder';

const config: NgDocConfiguration = {
  isolatedPlaygrounds: true,
};

export default config;

The action's option wins over the configuration's, which wins over isolatedPlaygrounds.

The theme option shows the demos of an isolated playground in another theme than the page, as for isolated demos (Isolated demos [Theme]), and the theme button beside each demo switches it for all of them.

The page of a target is <base href>demo-preview/<page route>/playground/<name>/<target>/. The target is the selector, or the pipe name, in lower case, with - for other characters: button[ngDocTag] is button-ngdoctag. The pages get the providers of their page and its categories, and demoProviders, as demo pages do (Isolated demos [Providers]). A page with an isolated playground gives its demos their pages too, so none of its demos may be named playground.

Values that can be sent

The URL carries plain data: strings, numbers, booleans, null, undefined, dates, and arrays and plain objects of those. A playground with another value, such as a function or a class instance like a TemplateRef, renders in the page, and a note above the inspector names the input. So does a playground whose values don't fit in 8 KB of URL. The action's data travels with the values, so the same applies to it; the data of the configuration is part of the page and can hold anything.

The note also tells when the playground renders in the page because:

  • the control of an input has cloneable: false (Custom type controls [Options]);
  • its selectors option names a selector that the target doesn't have;
  • the page in the iframe failed, or didn't answer within 10 seconds.

 Options

Set options in the configuration of the playground, or as the second argument of the action. The action's options win. They follow NgDocPlaygroundOptions:

OptionTypeDefaultDescription
selectorsstring | string[]AllThe selectors to render a demo for.
expandedbooleanfalseShows the code under the demo when the playground opens.
hideSidePanelbooleanfalseHides the inspector and shows only the demo.
inspectorPosition'right' | 'bottom''right'Puts the inspector right of the demo, or below it so the demo gets the full width.
recreateboolean | 'always'falseStarts with Recreate on. 'always' keeps it on and hides the setting.
inputsRecord<string, unknown>–Values for the inputs when the playground opens. Reset returns to the defaults.
defaultsRecord<string, unknown>–Defaults for the controls, instead of the target's own. Reset returns to them.
hiddenInputsstring[]–Inputs that get no control.
dataRecord<string, unknown>–Values for the template. The action's data extends the configuration's.
isolatedbooleanfalseShows the demos in iframes of their own pages (new engine only, see above).
themestring–The theme of the demos of an isolated playground, such as 'dark' (see above).

This playground hides the inspector, and starts with a rounded button:

index.mdTwig
{{ NgDocActions.playground("ButtonPlayground", { hideSidePanel: true, selectors: "button[ng-doc-button-flat]", inputs: { rounded: true }, data: { label: "Rounded Button" } }) }}

For a large component, put the inspector below the demo. The demo gets the full width, and the controls flow into columns:

index.mdTwig
{{ NgDocActions.playground("TagPlayground", { inspectorPosition: "bottom" }) }}
Tag Label

Playground

Settings

Controls for other inputs

NgDoc doesn't see inputs declared in the inputs of the @Component decorator. Add them with controls, keyed by property name. The value is the type of the input, or an NgDocPlaygroundControlConfig:

ng-doc.page.tsTypeScript
import { NgDocPage } from '@ng-doc/core';

import { BadgeComponent } from './badge.component';

const MyPage: NgDocPage = {
  title: 'Badge',
  mdFile: './index.md',
  playgrounds: {
    BadgePlayground: {
      target: BadgeComponent,
      template: `<ng-doc-selector>New</ng-doc-selector>`,
      controls: {
        label: 'string',
        count: { type: 'number', alias: 'badgeCount', description: 'The number to show.' },
        size: { type: 'NgDocTypeAlias', options: ['small', 'medium', 'large'] },
      },
    },
  },
};

export default MyPage;

An entry in controls also replaces the control that NgDoc chose for an input of the same name. The type NgDocTypeAlias shows a list of options, and NgDocTypeAlias[] a multi-select of them for an array; a type registered with provideTypeControl shows your control.

Labels, groups and order

An entry in controls can also change how the inspector shows a row. Leave out type to keep the control NgDoc chose for the input:

ng-doc.page.tsTypeScript
controls: {
  primaryColor: { label: 'Primary color', group: 'Colors', order: 1 },
  accentColor: { label: 'Accent color', group: 'Colors', order: 2 },
  rounded: { label: 'Rounded corners', group: 'Shape' },
},
  • label: '…' replaces the input's name in the row. A control that shows the name itself, such as the checkbox of a boolean, shows the label too.
  • group: '…' lists the inputs of a group together, under its name. Inputs without a group stay under Settings, above the groups. The groups follow the order of their first input.
  • order: <number> puts the input before the inputs without one, lowest first. The others keep the order of their controls (Custom type controls [Options]), then their names.

An entry with a type takes the same fields.

Warning

Only the new engine reads these fields. The legacy builders ignore them, and an entry without a type leaves the input's row as it is (Legacy builders).

 Gotchas

Warning

In the action's options, put a space between two closing braces, such as { inputs: { a: 1 } }. Without it, Nunjucks reads }} as the end of the expression.

Warning

Only the new engine builds playground pages. With the legacy builders, an isolated playground renders in the page, without a note (Legacy builders).

Warning

An overlay that a target opens, such as a menu or a tooltip, can't leave its iframe.

Note

The legacy builders list the values of a union in the order the TypeScript checker created them, which can differ from the written order (Upgrade to 22.0 [7. The new engine]).

Next: Custom type controls

Edit this page