Skip to content
compodocx
Skip to article content

Guide 07

Playground Blocks

cngxjs (opens in new tab) Updated Guide 07 of 13

Built on StackBlitz SDK lazy-loaded · ~7 KB chunk

Components can ship author-curated, runnable demos that open in a fresh StackBlitz tab. Add a @playground JSDoc block, and compodocx renders a dedicated Playground tab on the component page with one launch button per block. The StackBlitz project is assembled at build time. The SDK is lazy-loaded on first click, so static doc pages stay light.

Three authoring modes

Pick the mode that matches the demo. All three share the same scaffold — only the AppComponent body differs.

  • Inline snippet. @playground <title> followed by a fenced HTML or TS code block in the JSDoc. Best for short single-template demos.
  • HTML file reference. @playground <title> ./path/to/file.html — the file body becomes the AppComponent template literal. Best for longer markup demos.
  • TS component file reference. @playground <title> ./path/to/file.component.ts — a real @Component class replaces the AppComponent. Best for stateful demos with signals, event handlers, or composed widgets.

Mode 1 — Inline snippet

The original authoring mode. The full demo body lives inside a fenced code block in the JSDoc comment.

ts
/**
 * Reusable button.
 *
 * @playground Default
 * ```html
 * <my-button label="Save changes"></my-button>
 * ```
 *
 * @playground Disabled
 * ```html
 * <my-button label="Save changes" [disabled]="true"></my-button>
 * ```
 */
@Component({ /* ... */ })
export class MyButton { /* ... */ }

Multiple @playground blocks render as a vertical stack of sections in source order, each with its own “Open in StackBlitz” button. The tab is hidden automatically when no block is parsed. Zero-config when you don’t need it.

Mode 2 — HTML file reference

Append a relative path ending in .html to the title line. The file’s contents become the AppComponent template body. Path is resolved against the file holding the JSDoc comment.

ts
/**
 * Reusable button.
 *
 * @playground Showcase ./examples/button-showcase.html
 */
@Component({ /* ... */ })
export class MyButton { /* ... */ }
html button-showcase.html
<section class="demo">
  <my-button label="Solid">Solid</my-button>
  <my-button label="Ghost" tone="ghost">Ghost</my-button>
  <my-button label="Disabled" [disabled]="true">Disabled</my-button>
</section>

Best for demos long enough that JSDoc gets visually noisy. Editor lints the HTML, syntax highlighting works as expected, diffs stay clean.

Mode 3 — TS component file reference

Append a relative path ending in .ts. The file must export a real standalone @Component class — it REPLACES the AppComponent in the StackBlitz project entirely. Optional templateUrl, styleUrl, styleUrls siblings and relative imports are walked and packed automatically.

ts
/**
 * Reusable button.
 *
 * @playground Counter ./examples/counter/counter-example.component.ts
 */
@Component({ /* ... */ })
export class MyButton { /* ... */ }
ts counter-example.component.ts
import { Component, signal } from '@angular/core';
import { MyButton } from '../../button.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [MyButton],
  templateUrl: './counter-example.component.html',
  styleUrl: './counter-example.component.css'
})
export class CounterExample {
  readonly count = signal(0);
  increment(): void {
    this.count.update(n => n + 1);
  }
}

Use selector: ‘app-root’ on the entry — that is the host the scaffold bootstraps. The class name itself can be anything; compodocx appends export { YourClass as AppComponent } automatically so the import in src/main.ts resolves.

Recommended folder layout for a library component with multiple demos:

text
src/lib/button/
├── button.component.ts          ← @playground tags
├── button.component.html
├── button.component.css
└── examples/
    ├── default/
    │   ├── default-example.component.ts        (mode 3 entry)
    │   ├── default-example.component.html      (templateUrl sibling)
    │   └── default-example.component.css       (styleUrl sibling)
    ├── disabled/
    │   └── disabled-example.component.ts       (template inline)
    └── inline-html.html                        (mode 2 — bare HTML)

Authoring rules

  • Title (text between @playground and the optional path) is required. Path-only tags (no title) are dropped with a build-time warning.
  • Mutual exclusion: a tag with both a fileRef AND a fenced body is dropped with a “mutually exclusive” warning. Pick one.
  • File paths must be relative (./ or ../ prefix), end in .html or .ts, and resolve against the JSDoc’s host file.
  • templateUrl, styleUrl, styleUrls in the entry must use string literals — template literals are not parsed.
  • Titles can contain slashes (e.g. “A / B comparison”) — only a trailing path with .html/.ts extension triggers file-ref mode.
  • The Playground tab is component-only. Other entity types ignore the tag.
  • Runs alongside @example on the Info tab and <example-url> on the Example tab. All three can coexist on the same component.

@playground vs. @example vs. <example-url>

Three tags, three intents:

  • @example: static, syntax-highlighted snippet on the Info tab. Use for code readers should copy.
  • <example-url>: iframe demo of an externally hosted page on the Example tab. Use when you already host the demo.
  • @playground: runnable StackBlitz project assembled from your component’s source. Use when you want readers to fork and try.

How the manifest is built

At build time compodocx assembles a StackBlitz WebContainer-templated (template: 'node') Angular CLI 21 project per block: a minimal main.ts, app.component.ts that hosts your snippet (or IS your TS-mode entry), the documented component’s source plus any transitive imports, and a package.json with the dependency table.

The manifest’s dependencies map starts with the eight Angular peers (@angular/core, @angular/common, @angular/compiler, @angular/forms, @angular/router, @angular/animations, and the platform-browser pair) all aligned on a single major derived from your @angular/core. Non-Angular runtime peers zone.js, rxjs, tslib are always present. @angular/material and @angular/cdk are forwarded only when your package.json declares them OR when Material auto-detection scans recognise widget selectors in your demo.

Beyond those tables, every bare-specifier import seen in any walked source or in your snippet is auto-forwarded with the version your package.json declares. Missing roots are skipped silently. The playgroundDependencies config-only key (no CLI flag) lets you inject extra packages or pin a specific version per build — useful for libraries the consumer ships but doesn’t install directly.

Dependency walking is bounded: depth 3, 25 files maximum, 8000 characters per file. When the caps trip, the section renders a static fallback instead of a launcher and the build log explains why. @internal-tagged exports are filtered out before serialisation, so private code never leaks into a public StackBlitz.

Library-author workflow

Component-library authors who want first-class runnable demos — the way Angular Material ships them — pair compodocx with a sibling dev-app inside the workspace. The library lives at projects/<library>/; a dev-app at projects/dev-app/ imports the SAME example components compodocx embeds in the docs. Single source of truth, no copy-paste drift.

The example folder layout sits inside each component:

text
projects/ui-kit/src/lib/button/
├── button.component.ts          ← @playground tags
├── examples/
│   ├── default/
│   │   ├── default-example.component.ts        ← imported by both compodocx AND dev-app
│   │   ├── default-example.component.html
│   │   └── default-example.component.css
│   └── states-showcase.html                    ← Mode 2 sibling

projects/dev-app/src/app/app.component.ts:
  import { DefaultExample } from '@my-org/ui-kit/button/examples/default/default-example.component';
  // composed alongside other example components for local preview

Run compodocx against the library’s tsconfig (not the dev-app), and pin the library’s version in playgroundDependencies if your published package should appear in the StackBlitz package.json alongside the inline sources:

json compodocx.config.json
{
  "tsconfig": "projects/ui-kit/tsconfig.lib.json",
  "output": "docs/",
  "publicApiOnly": true,
  "playgroundDependencies": {
    "@my-org/ui-kit": "^1.0.0"
  }
}

See the options guide for every flag and config key that touches the playground pipeline.

Opt-out

Hide the tab globally with --disablePlaygroundTab or disablePlaygroundTab: true in your config file. Independent of --disableDependenciesTab: one suppresses the per-component standalone-import graph, the other suppresses the Playground tab. The flag has no effect on components that declare no @playground blocks (the tab is already absent).

Browser path

Each block emits an inert manifest into the static HTML as <script type="application/json" data-cdx-stackblitz-manifest-data>. On click, the launcher dynamic-imports @stackblitz/sdk. The SDK ships as a separate ~7 KB gzipped chunk that is never loaded until needed. The SDK call is openProject(manifest, { newWindow: true }): a fresh tab opens, no iframe overlays the doc page.