@ng-doc/app 22.1.0 → 22.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/LICENSE +21 -0
  2. package/fesm2022/ng-doc-app-classes-content-controller.mjs +22 -4
  3. package/fesm2022/ng-doc-app-classes-content-controller.mjs.map +1 -1
  4. package/fesm2022/ng-doc-app-classes-default-search-engine.mjs +36 -12
  5. package/fesm2022/ng-doc-app-classes-default-search-engine.mjs.map +1 -1
  6. package/fesm2022/ng-doc-app-classes-root-page.mjs.map +1 -1
  7. package/fesm2022/ng-doc-app-components-api-list.mjs +7 -5
  8. package/fesm2022/ng-doc-app-components-api-list.mjs.map +1 -1
  9. package/fesm2022/ng-doc-app-components-breadcrumb.mjs +5 -3
  10. package/fesm2022/ng-doc-app-components-breadcrumb.mjs.map +1 -1
  11. package/fesm2022/ng-doc-app-components-code.mjs +6 -4
  12. package/fesm2022/ng-doc-app-components-code.mjs.map +1 -1
  13. package/fesm2022/ng-doc-app-components-command-palette.mjs +35 -27
  14. package/fesm2022/ng-doc-app-components-command-palette.mjs.map +1 -1
  15. package/fesm2022/ng-doc-app-components-copy-button.mjs +6 -4
  16. package/fesm2022/ng-doc-app-components-copy-button.mjs.map +1 -1
  17. package/fesm2022/ng-doc-app-components-demo-displayer.mjs +19 -4
  18. package/fesm2022/ng-doc-app-components-demo-displayer.mjs.map +1 -1
  19. package/fesm2022/ng-doc-app-components-demo-frame.mjs +195 -12
  20. package/fesm2022/ng-doc-app-components-demo-frame.mjs.map +1 -1
  21. package/fesm2022/ng-doc-app-components-demo-pane.mjs +2 -2
  22. package/fesm2022/ng-doc-app-components-demo-pane.mjs.map +1 -1
  23. package/fesm2022/ng-doc-app-components-demo.mjs +49 -12
  24. package/fesm2022/ng-doc-app-components-demo.mjs.map +1 -1
  25. package/fesm2022/ng-doc-app-components-fullscreen-button.mjs +5 -3
  26. package/fesm2022/ng-doc-app-components-fullscreen-button.mjs.map +1 -1
  27. package/fesm2022/ng-doc-app-components-fullscreen-toggle.mjs +4 -2
  28. package/fesm2022/ng-doc-app-components-fullscreen-toggle.mjs.map +1 -1
  29. package/fesm2022/ng-doc-app-components-heading-anchor.mjs +5 -3
  30. package/fesm2022/ng-doc-app-components-heading-anchor.mjs.map +1 -1
  31. package/fesm2022/ng-doc-app-components-image-viewer.mjs +5 -3
  32. package/fesm2022/ng-doc-app-components-image-viewer.mjs.map +1 -1
  33. package/fesm2022/ng-doc-app-components-navbar.mjs +4 -2
  34. package/fesm2022/ng-doc-app-components-navbar.mjs.map +1 -1
  35. package/fesm2022/ng-doc-app-components-page-navigation.mjs +5 -3
  36. package/fesm2022/ng-doc-app-components-page-navigation.mjs.map +1 -1
  37. package/fesm2022/ng-doc-app-components-page-wrapper.mjs +2 -2
  38. package/fesm2022/ng-doc-app-components-page-wrapper.mjs.map +1 -1
  39. package/fesm2022/ng-doc-app-components-page.mjs +83 -16
  40. package/fesm2022/ng-doc-app-components-page.mjs.map +1 -1
  41. package/fesm2022/ng-doc-app-components-playground.mjs +743 -108
  42. package/fesm2022/ng-doc-app-components-playground.mjs.map +1 -1
  43. package/fesm2022/ng-doc-app-components-root.mjs +51 -4
  44. package/fesm2022/ng-doc-app-components-root.mjs.map +1 -1
  45. package/fesm2022/ng-doc-app-components-search-result.mjs +8 -6
  46. package/fesm2022/ng-doc-app-components-search-result.mjs.map +1 -1
  47. package/fesm2022/ng-doc-app-components-search.mjs +7 -5
  48. package/fesm2022/ng-doc-app-components-search.mjs.map +1 -1
  49. package/fesm2022/ng-doc-app-components-sidebar.mjs +4 -3
  50. package/fesm2022/ng-doc-app-components-sidebar.mjs.map +1 -1
  51. package/fesm2022/ng-doc-app-components-theme-toggle.mjs +7 -5
  52. package/fesm2022/ng-doc-app-components-theme-toggle.mjs.map +1 -1
  53. package/fesm2022/ng-doc-app-components-toc.mjs +4 -2
  54. package/fesm2022/ng-doc-app-components-toc.mjs.map +1 -1
  55. package/fesm2022/ng-doc-app-constants.mjs +11 -1
  56. package/fesm2022/ng-doc-app-constants.mjs.map +1 -1
  57. package/fesm2022/ng-doc-app-demo-app.mjs +618 -15
  58. package/fesm2022/ng-doc-app-demo-app.mjs.map +1 -1
  59. package/fesm2022/ng-doc-app-helpers.mjs +125 -6
  60. package/fesm2022/ng-doc-app-helpers.mjs.map +1 -1
  61. package/fesm2022/ng-doc-app-providers-ng-doc-app.mjs +29 -18
  62. package/fesm2022/ng-doc-app-providers-ng-doc-app.mjs.map +1 -1
  63. package/fesm2022/ng-doc-app-providers-texts.mjs +53 -0
  64. package/fesm2022/ng-doc-app-providers-texts.mjs.map +1 -0
  65. package/fesm2022/ng-doc-app-providers.mjs +1 -0
  66. package/fesm2022/ng-doc-app-providers.mjs.map +1 -1
  67. package/fesm2022/ng-doc-app-services-content-scroll-intent.mjs +27 -2
  68. package/fesm2022/ng-doc-app-services-content-scroll-intent.mjs.map +1 -1
  69. package/fesm2022/ng-doc-app-services-highlighter.mjs +8 -5
  70. package/fesm2022/ng-doc-app-services-highlighter.mjs.map +1 -1
  71. package/fesm2022/ng-doc-app-services-shortcuts.mjs +38 -4
  72. package/fesm2022/ng-doc-app-services-shortcuts.mjs.map +1 -1
  73. package/fesm2022/ng-doc-app-texts-ar.mjs +238 -0
  74. package/fesm2022/ng-doc-app-texts-ar.mjs.map +1 -0
  75. package/fesm2022/ng-doc-app-texts-de.mjs +205 -0
  76. package/fesm2022/ng-doc-app-texts-de.mjs.map +1 -0
  77. package/fesm2022/ng-doc-app-texts-es.mjs +205 -0
  78. package/fesm2022/ng-doc-app-texts-es.mjs.map +1 -0
  79. package/fesm2022/ng-doc-app-texts-fr.mjs +205 -0
  80. package/fesm2022/ng-doc-app-texts-fr.mjs.map +1 -0
  81. package/fesm2022/ng-doc-app-texts-hi.mjs +219 -0
  82. package/fesm2022/ng-doc-app-texts-hi.mjs.map +1 -0
  83. package/fesm2022/ng-doc-app-texts-ja.mjs +205 -0
  84. package/fesm2022/ng-doc-app-texts-ja.mjs.map +1 -0
  85. package/fesm2022/ng-doc-app-texts-ko.mjs +205 -0
  86. package/fesm2022/ng-doc-app-texts-ko.mjs.map +1 -0
  87. package/fesm2022/ng-doc-app-texts-pt-br.mjs +205 -0
  88. package/fesm2022/ng-doc-app-texts-pt-br.mjs.map +1 -0
  89. package/fesm2022/ng-doc-app-texts-ru.mjs +231 -0
  90. package/fesm2022/ng-doc-app-texts-ru.mjs.map +1 -0
  91. package/fesm2022/ng-doc-app-texts-uk.mjs +231 -0
  92. package/fesm2022/ng-doc-app-texts-uk.mjs.map +1 -0
  93. package/fesm2022/ng-doc-app-texts-zh.mjs +205 -0
  94. package/fesm2022/ng-doc-app-texts-zh.mjs.map +1 -0
  95. package/fesm2022/ng-doc-app-tokens.mjs +214 -1
  96. package/fesm2022/ng-doc-app-tokens.mjs.map +1 -1
  97. package/fesm2022/ng-doc-app-type-controls-json-control.mjs +90 -0
  98. package/fesm2022/ng-doc-app-type-controls-json-control.mjs.map +1 -0
  99. package/fesm2022/ng-doc-app-type-controls-list-control.mjs +792 -0
  100. package/fesm2022/ng-doc-app-type-controls-list-control.mjs.map +1 -0
  101. package/fesm2022/ng-doc-app-type-controls-type-alias-control.mjs +5 -3
  102. package/fesm2022/ng-doc-app-type-controls-type-alias-control.mjs.map +1 -1
  103. package/fesm2022/ng-doc-app-type-controls.mjs +2 -0
  104. package/fesm2022/ng-doc-app-type-controls.mjs.map +1 -1
  105. package/package.json +59 -3
  106. package/styles/global.css +1 -0
  107. package/styles/main.css +1 -0
  108. package/types/ng-doc-app-classes-default-search-engine.d.ts +11 -0
  109. package/types/ng-doc-app-classes-root-page.d.ts +5 -0
  110. package/types/ng-doc-app-components-api-list.d.ts +3 -1
  111. package/types/ng-doc-app-components-breadcrumb.d.ts +2 -1
  112. package/types/ng-doc-app-components-code.d.ts +2 -0
  113. package/types/ng-doc-app-components-command-palette.d.ts +3 -1
  114. package/types/ng-doc-app-components-copy-button.d.ts +3 -1
  115. package/types/ng-doc-app-components-demo-displayer.d.ts +13 -1
  116. package/types/ng-doc-app-components-demo-frame.d.ts +62 -3
  117. package/types/ng-doc-app-components-demo.d.ts +27 -8
  118. package/types/ng-doc-app-components-fullscreen-button.d.ts +2 -0
  119. package/types/ng-doc-app-components-fullscreen-toggle.d.ts +2 -0
  120. package/types/ng-doc-app-components-heading-anchor.d.ts +2 -0
  121. package/types/ng-doc-app-components-image-viewer.d.ts +2 -0
  122. package/types/ng-doc-app-components-navbar.d.ts +2 -0
  123. package/types/ng-doc-app-components-page-navigation.d.ts +2 -1
  124. package/types/ng-doc-app-components-page.d.ts +10 -1
  125. package/types/ng-doc-app-components-playground.d.ts +241 -44
  126. package/types/ng-doc-app-components-root.d.ts +18 -0
  127. package/types/ng-doc-app-components-search-result.d.ts +2 -0
  128. package/types/ng-doc-app-components-search.d.ts +2 -0
  129. package/types/ng-doc-app-components-sidebar.d.ts +2 -1
  130. package/types/ng-doc-app-components-theme-toggle.d.ts +4 -1
  131. package/types/ng-doc-app-components-toc.d.ts +2 -1
  132. package/types/ng-doc-app-constants.d.ts +11 -1
  133. package/types/ng-doc-app-demo-app.d.ts +211 -3
  134. package/types/ng-doc-app-helpers.d.ts +56 -3
  135. package/types/ng-doc-app-interfaces.d.ts +621 -3
  136. package/types/ng-doc-app-providers-ng-doc-app.d.ts +6 -0
  137. package/types/ng-doc-app-providers-texts.d.ts +28 -0
  138. package/types/ng-doc-app-providers.d.ts +1 -0
  139. package/types/ng-doc-app-services-content-scroll-intent.d.ts +11 -1
  140. package/types/ng-doc-app-services-highlighter.d.ts +3 -1
  141. package/types/ng-doc-app-services-shortcuts.d.ts +16 -0
  142. package/types/ng-doc-app-texts-ar.d.ts +13 -0
  143. package/types/ng-doc-app-texts-de.d.ts +12 -0
  144. package/types/ng-doc-app-texts-es.d.ts +12 -0
  145. package/types/ng-doc-app-texts-fr.d.ts +12 -0
  146. package/types/ng-doc-app-texts-hi.d.ts +12 -0
  147. package/types/ng-doc-app-texts-ja.d.ts +12 -0
  148. package/types/ng-doc-app-texts-ko.d.ts +12 -0
  149. package/types/ng-doc-app-texts-pt-br.d.ts +12 -0
  150. package/types/ng-doc-app-texts-ru.d.ts +12 -0
  151. package/types/ng-doc-app-texts-uk.d.ts +12 -0
  152. package/types/ng-doc-app-texts-zh.d.ts +12 -0
  153. package/types/ng-doc-app-tokens.d.ts +21 -2
  154. package/types/ng-doc-app-type-controls-json-control.d.ts +38 -0
  155. package/types/ng-doc-app-type-controls-list-control.d.ts +313 -0
  156. package/types/ng-doc-app-type-controls-type-alias-control.d.ts +2 -1
  157. package/types/ng-doc-app-type-controls.d.ts +2 -0
@@ -1,29 +1,124 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, viewChild, ViewContainerRef, signal, inject, Injector, DestroyRef, PendingTasks, ChangeDetectionStrategy, Component, computed, effect, untracked, reflectComponentType, model, output, isDevMode, linkedSignal, ChangeDetectorRef, Directive } from '@angular/core';
2
+ import { InjectionToken, input, inject, ChangeDetectorRef, Directive, output, model, viewChild, computed, ViewContainerRef, signal, Injector, DestroyRef, PendingTasks, ChangeDetectionStrategy, Component, effect, untracked, reflectComponentType, isDevMode, linkedSignal, PLATFORM_ID, DOCUMENT, afterNextRender } from '@angular/core';
3
3
  import { extractFunctionDefaults } from '@ng-doc/core/helpers/extract-function-defaults';
4
4
  import { Subject, take } from 'rxjs';
5
+ import { NgTemplateOutlet, KeyValuePipe, PlatformLocation, APP_BASE_HREF, isPlatformBrowser } from '@angular/common';
6
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
5
7
  import * as i1 from '@angular/forms';
6
8
  import { FormsModule, FormBuilder } from '@angular/forms';
7
9
  import { NgDocRootPage } from '@ng-doc/app/classes/root-page';
10
+ import { ɵwithDemoTheme as _withDemoTheme, ɵplaygroundStateToFragment as _playgroundStateToFragment, ɵdecodePlaygroundValue as _decodePlaygroundValue, ɵencodePlaygroundValue as _encodePlaygroundValue, ɵplaygroundStateFromFragment as _playgroundStateFromFragment, ɵNG_DOC_DEMO_MESSAGE_SOURCE as _NG_DOC_DEMO_MESSAGE_SOURCE } from '@ng-doc/app/demo-app';
11
+ import { getTokenForType } from '@ng-doc/app/providers/type-control';
12
+ import { NG_DOC_TEXTS } from '@ng-doc/app/tokens';
8
13
  import { isSameObject } from '@ng-doc/core/helpers/is-same-object';
9
14
  import { objectKeys } from '@ng-doc/core/helpers/object-keys';
15
+ import { ɵplaygroundTargetSlugs as _playgroundTargetSlugs } from '@ng-doc/core/helpers/playground-target-slugs';
10
16
  import { NgDocLabelComponent, NgDocTooltipDirective, NgDocCheckboxComponent, NgDocIconComponent, NgDocBindPipe, NgDocExecutePipe, NgDocAsArrayPipe } from '@ng-doc/ui-kit';
11
- import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
12
17
  import { NgDocDemoDisplayerComponent } from '@ng-doc/app/components/demo-displayer';
18
+ import { NgDocDemoFrameComponent, NgDocDemoThemeButtonComponent } from '@ng-doc/app/components/demo-frame';
13
19
  import { formatHtml, isPlaygroundProperty, resolvePlaygroundOption } from '@ng-doc/app/helpers';
14
20
  import { getPlaygroundDemoToken } from '@ng-doc/app/providers/playground-demo';
15
21
  import { buildPlaygroundDemoPipeTemplate, buildPlaygroundDemoTemplate } from '@ng-doc/core/helpers/build-playground-demo-template';
16
22
  import { stringify } from '@ng-doc/core/helpers/stringify';
17
23
  import { startWith, takeUntil } from 'rxjs/operators';
18
- import { NgTemplateOutlet, KeyValuePipe } from '@angular/common';
19
- import { getTokenForType } from '@ng-doc/app/providers/type-control';
20
24
  import { isPresent } from '@ng-doc/core/helpers/is-present';
21
25
  import { NgDocKindIconComponent } from '@ng-doc/app/components/kind-icon';
22
26
  import { NgDocSanitizeHtmlPipe } from '@ng-doc/app/pipes';
27
+ import { ActivatedRoute } from '@angular/router';
28
+
29
+ /**
30
+ * The playground that shows a generated playground class.
31
+ * @internal
32
+ */
33
+ const ɵNG_DOC_PLAYGROUND_CONTAINER = new InjectionToken('NG_DOC_PLAYGROUND_CONTAINER');
34
+
35
+ /**
36
+ * Base class of the playground classes the builder generates, one per playground and selector.
37
+ * A generated class renders the playground template with the target's inputs bound to
38
+ * `properties()`, and queries the target with `viewChild()`.
39
+ */
40
+ class NgDocBasePlayground {
41
+ static { this.selector = 'unknown'; }
42
+ constructor() {
43
+ /** Values of the target's inputs, by property name. */
44
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
+ this.properties = input({}, /* @ts-ignore */
46
+ ...(ngDevMode ? [{ debugName: "properties" }] : /* istanbul ignore next */ []));
47
+ /** The `data` given to the playground action; it extends the configuration's `data`. */
48
+ this.actionData = input({}, /* @ts-ignore */
49
+ ...(ngDevMode ? [{ debugName: "actionData" }] : /* istanbul ignore next */ []));
50
+ /** Whether each content slot is shown, by slot name. */
51
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
+ this.content = input({}, /* @ts-ignore */
53
+ ...(ngDevMode ? [{ debugName: "content" }] : /* istanbul ignore next */ []));
54
+ /** The default values of the target's inputs, read before the template binds them. */
55
+ this.defaultValues = {};
56
+ this.reattached = new Subject();
57
+ this.playgroundContainer = inject(ɵNG_DOC_PLAYGROUND_CONTAINER);
58
+ this.changeDetectorRef = inject(ChangeDetectorRef);
59
+ // The view is not checked until the default values of the target are read, so the
60
+ // template's bindings cannot overwrite them first.
61
+ this.changeDetectorRef.detach();
62
+ }
63
+ /** Emits once, after the view is attached again and checks the template's bindings. */
64
+ get onReattached() {
65
+ return this.reattached.pipe(take(1));
66
+ }
67
+ /** The playground `data`: the configuration's data extended by the action's data. */
68
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
+ get data() {
70
+ return Object.assign({}, this.configData, this.actionData());
71
+ }
72
+ ngOnInit() {
73
+ // The target is created with the view, but the view has not been checked yet, so its inputs
74
+ // still hold their defaults. Signal inputs are read by calling them.
75
+ const playground = this.playground();
76
+ if (playground) {
77
+ this.defaultValues = Object.keys(playground).reduce((values, key) => {
78
+ try {
79
+ const value = playground[key];
80
+ values[key] = typeof value === 'function' ? value.call(playground) : value;
81
+ }
82
+ catch {
83
+ // Some properties are getters or functions that throw without their context.
84
+ }
85
+ return values;
86
+ }, {});
87
+ }
88
+ else if (this.target) {
89
+ // A pipe: the defaults of its `transform` parameters after the value itself.
90
+ const defaults = extractFunctionDefaults(this.target.prototype.transform);
91
+ this.defaultValues = Object.keys(this.playgroundContainer.properties() ?? {}).reduce((values, key, i) => {
92
+ values[key] = defaults[i + 1];
93
+ return values;
94
+ }, {});
95
+ }
96
+ else {
97
+ throw new Error('Playground is not defined or initialized');
98
+ }
99
+ if (!this.playgroundContainer.defaultValues()) {
100
+ this.playgroundContainer.defaultValues.set(this.defaultValues);
101
+ }
102
+ // The demo sets the playground's inputs right after it creates this view; the view is
103
+ // attached again once they are set, so its first check renders the playground's values.
104
+ Promise.resolve().then(() => {
105
+ this.changeDetectorRef.reattach();
106
+ this.changeDetectorRef.markForCheck();
107
+ this.reattached.next();
108
+ });
109
+ }
110
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.1", ngImport: i0, type: NgDocBasePlayground, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
111
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.2.1", type: NgDocBasePlayground, isStandalone: true, inputs: { properties: { classPropertyName: "properties", publicName: "properties", isSignal: true, isRequired: false, transformFunction: null }, actionData: { classPropertyName: "actionData", publicName: "actionData", isSignal: true, isRequired: false, transformFunction: null }, content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
112
+ }
113
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.1", ngImport: i0, type: NgDocBasePlayground, decorators: [{
114
+ type: Directive
115
+ }], ctorParameters: () => [], propDecorators: { properties: [{ type: i0.Input, args: [{ isSignal: true, alias: "properties", required: false }] }], actionData: [{ type: i0.Input, args: [{ isSignal: true, alias: "actionData", required: false }] }], content: [{ type: i0.Input, args: [{ isSignal: true, alias: "content", required: false }] }] } });
23
116
 
24
117
  /**
25
118
  * One demo of a playground: it creates the generated playground class of its selector (or pipe),
26
- * passes it the form's values and shows the code of the current state.
119
+ * passes it the form's values and shows the code of the current state. In an isolated playground
120
+ * the class runs on a page of the demo application in an iframe, which gets the values from its
121
+ * URL fragment, and reports the target's defaults back for the code view.
27
122
  */
28
123
  class NgDocPlaygroundDemoComponent {
29
124
  constructor() {
@@ -51,6 +146,42 @@ class NgDocPlaygroundDemoComponent {
51
146
  /** Whether the code of the demo is shown. */
52
147
  this.expanded = input(false, /* @ts-ignore */
53
148
  ...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
149
+ /** The iframe of an isolated playground; without it, the class is created in the page. */
150
+ this.frame = input(undefined, /* @ts-ignore */
151
+ ...(ngDevMode ? [{ debugName: "frame" }] : /* istanbul ignore next */ []));
152
+ /** The defaults of the target's inputs that the iframe reported, for the code view. */
153
+ this.frameDefaults = input(undefined, /* @ts-ignore */
154
+ ...(ngDevMode ? [{ debugName: "frameDefaults" }] : /* istanbul ignore next */ []));
155
+ /** The messages of the iframe of an isolated playground. */
156
+ this.frameMessage = output();
157
+ /** The iframe's page loaded but did not report the target in time. */
158
+ this.frameSilent = output();
159
+ /** The theme of the iframe, which the theme button beside the demo changes. */
160
+ this.frameTheme = model(undefined, /* @ts-ignore */
161
+ ...(ngDevMode ? [{ debugName: "frameTheme" }] : /* istanbul ignore next */ []));
162
+ /** The `theme` option of the playground, which the theme button offers too. */
163
+ this.configuredTheme = input(undefined, /* @ts-ignore */
164
+ ...(ngDevMode ? [{ debugName: "configuredTheme" }] : /* istanbul ignore next */ []));
165
+ /** The iframe of an isolated playground, while it is shown. */
166
+ this.frameComponent = viewChild(NgDocDemoFrameComponent, /* @ts-ignore */
167
+ ...(ngDevMode ? [{ debugName: "frameComponent" }] : /* istanbul ignore next */ []));
168
+ /**
169
+ * The background of the canvas while the iframe shows a theme of its own: the background its
170
+ * page reported, so the whole canvas reads in that theme.
171
+ */
172
+ this.canvasBackground = computed(() => this.frame() && this.frameTheme() !== undefined
173
+ ? this.frameComponent()?.background()
174
+ : undefined, /* @ts-ignore */
175
+ ...(ngDevMode ? [{ debugName: "canvasBackground" }] : /* istanbul ignore next */ []));
176
+ /** The dots of the canvas while the iframe shows a theme of its own, in that theme's color. */
177
+ this.canvasDots = computed(() => this.canvasBackground() ? this.frameComponent()?.dots() : undefined, /* @ts-ignore */
178
+ ...(ngDevMode ? [{ debugName: "canvasDots" }] : /* istanbul ignore next */ []));
179
+ /** The iframe's page with the current state, which the toolbar opens in a new tab. */
180
+ this.frameUrl = computed(() => {
181
+ const frame = this.frame();
182
+ return frame ? `${frame.url}#${frame.fragment}` : undefined;
183
+ }, /* @ts-ignore */
184
+ ...(ngDevMode ? [{ debugName: "frameUrl" }] : /* istanbul ignore next */ []));
54
185
  /** Where the playground class is created. */
55
186
  this.demoOutlet = viewChild('demoOutlet', { ...(ngDevMode ? { debugName: "demoOutlet" } : /* istanbul ignore next */ {}), read: ViewContainerRef });
56
187
  this.code = signal('', /* @ts-ignore */
@@ -60,7 +191,11 @@ class NgDocPlaygroundDemoComponent {
60
191
  this.pendingTasks = inject(PendingTasks);
61
192
  this.unsubscribe$ = new Subject();
62
193
  }
63
- async ngOnChanges({ form, id }) {
194
+ async ngOnChanges({ form, id, frame }) {
195
+ // A playground that falls back from its iframe to the page creates its class in the page.
196
+ if (frame && !frame.firstChange && !this.frame()) {
197
+ await this.updateDemo(this.form()?.value);
198
+ }
64
199
  if (form || id) {
65
200
  this.unsubscribe$.next();
66
201
  const demoInjector = getPlaygroundDemoToken(this.id());
@@ -68,14 +203,25 @@ class NgDocPlaygroundDemoComponent {
68
203
  const demos = this.injector.get(demoInjector, []);
69
204
  this.playgroundDemo = demos.find((demo) => demo.selector === this.selector() || demo.selector === this.pipeName());
70
205
  }
71
- await this.updateDemo();
72
206
  const form = this.form();
73
- form?.valueChanges
207
+ if (!form) {
208
+ // No values yet: the class is created so that it reports the target's defaults.
209
+ await this.updateDemo();
210
+ return;
211
+ }
212
+ // The form's values reach the class synchronously, in this check: the class may already be
213
+ // attached again, and checked with no values its target would get `undefined` for every
214
+ // input instead of its default.
215
+ form.valueChanges
74
216
  .pipe(startWith(form.value), takeUntil(this.unsubscribe$), takeUntilDestroyed(this.destroyRef))
75
217
  .subscribe((data) => this.updateDemo(data));
76
218
  }
77
219
  }
78
220
  async updateDemo(data) {
221
+ if (this.frame()) {
222
+ await this.updateCodeView();
223
+ return;
224
+ }
79
225
  if (this.recreateDemo() || !this.demoRef) {
80
226
  this.createDemo();
81
227
  }
@@ -124,7 +270,7 @@ class NgDocPlaygroundDemoComponent {
124
270
  return objectKeys(formData).reduce((result, key) => {
125
271
  const inputName = this.properties()?.[key]?.inputName ?? key;
126
272
  const value = formData[key];
127
- const property = this.demoRef?.instance?.defaultValues[key];
273
+ const property = this.defaultValue(key);
128
274
  if (property !== value) {
129
275
  result[inputName] = stringify(value).replace(/"/g, `'`);
130
276
  }
@@ -137,7 +283,7 @@ class NgDocPlaygroundDemoComponent {
137
283
  return objectKeys(formData)
138
284
  .map((key, i) => {
139
285
  const value = formData[key];
140
- const defaultValue = this.demoRef?.instance?.defaultValues[key];
286
+ const defaultValue = this.defaultValue(key);
141
287
  if (defaultValue !== value) {
142
288
  changedInputIndex = i;
143
289
  }
@@ -153,16 +299,175 @@ class NgDocPlaygroundDemoComponent {
153
299
  this.unsubscribe$.next();
154
300
  this.unsubscribe$.complete();
155
301
  }
302
+ /**
303
+ * The default of an input: from the class in the page, or as the iframe reported it.
304
+ * @param key
305
+ */
306
+ defaultValue(key) {
307
+ return this.frame() ? this.frameDefaults()?.[key] : this.demoRef?.instance?.defaultValues[key];
308
+ }
156
309
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.1", ngImport: i0, type: NgDocPlaygroundDemoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
157
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.2.1", type: NgDocPlaygroundDemoComponent, isStandalone: true, selector: "ng-doc-playground-demo", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, pipeName: { classPropertyName: "pipeName", publicName: "pipeName", isSignal: true, isRequired: false, transformFunction: null }, selector: { classPropertyName: "selector", publicName: "selector", isSignal: true, isRequired: false, transformFunction: null }, configuration: { classPropertyName: "configuration", publicName: "configuration", isSignal: true, isRequired: false, transformFunction: null }, properties: { classPropertyName: "properties", publicName: "properties", isSignal: true, isRequired: false, transformFunction: null }, recreateDemo: { classPropertyName: "recreateDemo", publicName: "recreateDemo", isSignal: true, isRequired: false, transformFunction: null }, form: { classPropertyName: "form", publicName: "form", isSignal: true, isRequired: false, transformFunction: null }, expanded: { classPropertyName: "expanded", publicName: "expanded", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "demoOutlet", first: true, predicate: ["demoOutlet"], descendants: true, read: ViewContainerRef, isSignal: true }], usesOnChanges: true, ngImport: i0, template: "<ng-doc-demo-displayer\n [code]=\"code()\"\n [border]=\"false\"\n [expanded]=\"expanded()\"\n language=\"angular-html\">\n <ng-container #demoOutlet></ng-container>\n</ng-doc-demo-displayer>\n", styles: [":host{display:flex;flex-direction:column}ng-doc-demo-displayer{flex:1 1 auto}\n"], dependencies: [{ kind: "component", type: NgDocDemoDisplayerComponent, selector: "ng-doc-demo-displayer", inputs: ["codeContent", "code", "language", "container", "border", "expanded"], outputs: ["expandedChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
310
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.1", type: NgDocPlaygroundDemoComponent, isStandalone: true, selector: "ng-doc-playground-demo", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, pipeName: { classPropertyName: "pipeName", publicName: "pipeName", isSignal: true, isRequired: false, transformFunction: null }, selector: { classPropertyName: "selector", publicName: "selector", isSignal: true, isRequired: false, transformFunction: null }, configuration: { classPropertyName: "configuration", publicName: "configuration", isSignal: true, isRequired: false, transformFunction: null }, properties: { classPropertyName: "properties", publicName: "properties", isSignal: true, isRequired: false, transformFunction: null }, recreateDemo: { classPropertyName: "recreateDemo", publicName: "recreateDemo", isSignal: true, isRequired: false, transformFunction: null }, form: { classPropertyName: "form", publicName: "form", isSignal: true, isRequired: false, transformFunction: null }, expanded: { classPropertyName: "expanded", publicName: "expanded", isSignal: true, isRequired: false, transformFunction: null }, frame: { classPropertyName: "frame", publicName: "frame", isSignal: true, isRequired: false, transformFunction: null }, frameDefaults: { classPropertyName: "frameDefaults", publicName: "frameDefaults", isSignal: true, isRequired: false, transformFunction: null }, frameTheme: { classPropertyName: "frameTheme", publicName: "frameTheme", isSignal: true, isRequired: false, transformFunction: null }, configuredTheme: { classPropertyName: "configuredTheme", publicName: "configuredTheme", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { frameMessage: "frameMessage", frameSilent: "frameSilent", frameTheme: "frameThemeChange" }, viewQueries: [{ propertyName: "frameComponent", first: true, predicate: NgDocDemoFrameComponent, descendants: true, isSignal: true }, { propertyName: "demoOutlet", first: true, predicate: ["demoOutlet"], descendants: true, read: ViewContainerRef, isSignal: true }], usesOnChanges: true, ngImport: i0, template: "<ng-doc-demo-displayer\n [code]=\"code()\"\n [border]=\"false\"\n [expanded]=\"expanded()\"\n [newTabUrl]=\"frameUrl()\"\n [canvasBackground]=\"canvasBackground()\"\n [canvasDots]=\"canvasDots()\"\n language=\"angular-html\">\n @if (frame()) {\n <ng-doc-demo-theme-button\n ngDocDemoControl\n [(theme)]=\"frameTheme\"\n [configured]=\"configuredTheme()\" />\n }\n @if (frame(); as frame) {\n <ng-doc-demo-frame\n [src]=\"frame.url\"\n [fragment]=\"frame.fragment\"\n [playground]=\"true\"\n [frameTitle]=\"frame.title\"\n (playgroundMessage)=\"frameMessage.emit($event)\"\n (playgroundSilent)=\"frameSilent.emit()\" />\n }\n <!-- Static, so the class can be created before the first check (in ngOnChanges). An isolated\n playground leaves it empty. -->\n <ng-container #demoOutlet></ng-container>\n</ng-doc-demo-displayer>\n", styles: [":host{display:flex;flex-direction:column}ng-doc-demo-displayer{flex:1 1 auto}\n"], dependencies: [{ kind: "component", type: NgDocDemoDisplayerComponent, selector: "ng-doc-demo-displayer", inputs: ["codeContent", "code", "language", "container", "canvasBackground", "canvasDots", "border", "expanded", "newTabUrl"], outputs: ["expandedChange"] }, { kind: "component", type: NgDocDemoFrameComponent, selector: "ng-doc-demo-frame", inputs: ["src", "frameTitle", "fill", "fragment", "playground"], outputs: ["playgroundMessage", "playgroundSilent"] }, { kind: "component", type: NgDocDemoThemeButtonComponent, selector: "ng-doc-demo-theme-button", inputs: ["theme", "configured"], outputs: ["themeChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
158
311
  }
159
312
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.1", ngImport: i0, type: NgDocPlaygroundDemoComponent, decorators: [{
160
313
  type: Component,
161
- args: [{ selector: 'ng-doc-playground-demo', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgDocDemoDisplayerComponent], template: "<ng-doc-demo-displayer\n [code]=\"code()\"\n [border]=\"false\"\n [expanded]=\"expanded()\"\n language=\"angular-html\">\n <ng-container #demoOutlet></ng-container>\n</ng-doc-demo-displayer>\n", styles: [":host{display:flex;flex-direction:column}ng-doc-demo-displayer{flex:1 1 auto}\n"] }]
162
- }], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], pipeName: [{ type: i0.Input, args: [{ isSignal: true, alias: "pipeName", required: false }] }], selector: [{ type: i0.Input, args: [{ isSignal: true, alias: "selector", required: false }] }], configuration: [{ type: i0.Input, args: [{ isSignal: true, alias: "configuration", required: false }] }], properties: [{ type: i0.Input, args: [{ isSignal: true, alias: "properties", required: false }] }], recreateDemo: [{ type: i0.Input, args: [{ isSignal: true, alias: "recreateDemo", required: false }] }], form: [{ type: i0.Input, args: [{ isSignal: true, alias: "form", required: false }] }], expanded: [{ type: i0.Input, args: [{ isSignal: true, alias: "expanded", required: false }] }], demoOutlet: [{ type: i0.ViewChild, args: ['demoOutlet', { ...{
314
+ args: [{ selector: 'ng-doc-playground-demo', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgDocDemoDisplayerComponent, NgDocDemoFrameComponent, NgDocDemoThemeButtonComponent], template: "<ng-doc-demo-displayer\n [code]=\"code()\"\n [border]=\"false\"\n [expanded]=\"expanded()\"\n [newTabUrl]=\"frameUrl()\"\n [canvasBackground]=\"canvasBackground()\"\n [canvasDots]=\"canvasDots()\"\n language=\"angular-html\">\n @if (frame()) {\n <ng-doc-demo-theme-button\n ngDocDemoControl\n [(theme)]=\"frameTheme\"\n [configured]=\"configuredTheme()\" />\n }\n @if (frame(); as frame) {\n <ng-doc-demo-frame\n [src]=\"frame.url\"\n [fragment]=\"frame.fragment\"\n [playground]=\"true\"\n [frameTitle]=\"frame.title\"\n (playgroundMessage)=\"frameMessage.emit($event)\"\n (playgroundSilent)=\"frameSilent.emit()\" />\n }\n <!-- Static, so the class can be created before the first check (in ngOnChanges). An isolated\n playground leaves it empty. -->\n <ng-container #demoOutlet></ng-container>\n</ng-doc-demo-displayer>\n", styles: [":host{display:flex;flex-direction:column}ng-doc-demo-displayer{flex:1 1 auto}\n"] }]
315
+ }], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], pipeName: [{ type: i0.Input, args: [{ isSignal: true, alias: "pipeName", required: false }] }], selector: [{ type: i0.Input, args: [{ isSignal: true, alias: "selector", required: false }] }], configuration: [{ type: i0.Input, args: [{ isSignal: true, alias: "configuration", required: false }] }], properties: [{ type: i0.Input, args: [{ isSignal: true, alias: "properties", required: false }] }], recreateDemo: [{ type: i0.Input, args: [{ isSignal: true, alias: "recreateDemo", required: false }] }], form: [{ type: i0.Input, args: [{ isSignal: true, alias: "form", required: false }] }], expanded: [{ type: i0.Input, args: [{ isSignal: true, alias: "expanded", required: false }] }], frame: [{ type: i0.Input, args: [{ isSignal: true, alias: "frame", required: false }] }], frameDefaults: [{ type: i0.Input, args: [{ isSignal: true, alias: "frameDefaults", required: false }] }], frameMessage: [{ type: i0.Output, args: ["frameMessage"] }], frameSilent: [{ type: i0.Output, args: ["frameSilent"] }], frameTheme: [{ type: i0.Input, args: [{ isSignal: true, alias: "frameTheme", required: false }] }, { type: i0.Output, args: ["frameThemeChange"] }], configuredTheme: [{ type: i0.Input, args: [{ isSignal: true, alias: "configuredTheme", required: false }] }], frameComponent: [{ type: i0.ViewChild, args: [i0.forwardRef(() => NgDocDemoFrameComponent), { isSignal: true }] }], demoOutlet: [{ type: i0.ViewChild, args: ['demoOutlet', { ...{
163
316
  read: ViewContainerRef,
164
317
  }, isSignal: true }] }] } });
165
318
 
319
+ /**
320
+ * Reading an input type as an array type, which the inspector uses to choose the control of an
321
+ * array input and the property row to leave out the type chip of one. Shared inside the playground
322
+ * entry point only; not public API.
323
+ */
324
+ /**
325
+ * The type that the control of array types without a control of their own is registered for.
326
+ * NgDoc registers its JSON field for it, and `controls` can name it for an input.
327
+ * @internal
328
+ */
329
+ const ARRAY_CONTROL_TYPE = 'NgDocArray';
330
+ /**
331
+ * The type that the control of arrays of a union of literals (arrays whose `options` the builder
332
+ * lists) is registered for. NgDoc registers its multi-select for it, as it registers the select
333
+ * of a union for `NgDocTypeAlias`, and `controls` can name it for an input.
334
+ * @internal
335
+ */
336
+ const UNION_ARRAY_CONTROL_TYPE = 'NgDocTypeAlias[]';
337
+ /** The item types whose arrays get the control registered for `<item>[]`, a chip field by default. */
338
+ const LIST_ITEM_TYPES = new Set(['string', 'number']);
339
+ /** Keywords that make a type ending in `[]` another type than an array, as in `keyof Item[]`. */
340
+ const TYPE_OPERATOR = /^(?:keyof|typeof|unique|infer|asserts|new)\s/;
341
+ /**
342
+ * Reads an input type as an array type: `T[]` (as both engines print an array), `Array<T>`,
343
+ * `readonly T[]`, `ReadonlyArray<T>` or a tuple such as `[string, number]`, alone or in a union
344
+ * with `undefined` or `null` (an optional or nullable input). An array of strings or numbers gets
345
+ * the control of `string[]` or `number[]`, so a control registered for that type replaces the
346
+ * built-in list for every spelling; any other array gets the control of `NgDocArray`, the JSON
347
+ * field. A type alias of an array (`type Tags = string[]`) is printed by its name and is not
348
+ * recognized: register a control for the name.
349
+ * @param type - The input type as the builder printed it.
350
+ * @returns What the array type is, or `undefined` when the type is not one.
351
+ * @internal
352
+ */
353
+ function arrayType(type) {
354
+ const members = splitTopLevel(type);
355
+ const rest = members.filter((member) => member !== 'undefined' && member !== 'null');
356
+ if (rest.length !== 1) {
357
+ return undefined;
358
+ }
359
+ const written = unwrapParentheses(rest[0]).replace(/^readonly\s+/, '');
360
+ const item = arrayItem(written);
361
+ if (item !== undefined) {
362
+ return LIST_ITEM_TYPES.has(item) ? { list: `${item}[]` } : {};
363
+ }
364
+ return written.startsWith('[') && written.endsWith(']') && isBalanced(written.slice(1, -1))
365
+ ? {}
366
+ : undefined;
367
+ }
368
+ /**
369
+ * The item type of `T[]`, `Array<T>` or `ReadonlyArray<T>`.
370
+ * @param type - A type that is not a union.
371
+ */
372
+ function arrayItem(type) {
373
+ const generic = /^(?:Readonly)?Array<([\s\S]*)>$/.exec(type);
374
+ if (generic) {
375
+ return isBalanced(generic[1]) ? unwrapParentheses(generic[1].trim()) : undefined;
376
+ }
377
+ if (!type.endsWith('[]') || TYPE_OPERATOR.test(type)) {
378
+ return undefined;
379
+ }
380
+ const item = type.slice(0, -2).trim();
381
+ // `() => string[]` is a function, and `A extends B ? C[] : D[]` a conditional type.
382
+ const text = item ? topLevelText(item) : undefined;
383
+ return text !== undefined && !/=>|\?/.test(text) ? unwrapParentheses(item) : undefined;
384
+ }
385
+ /**
386
+ * Splits a type at the `|` of its top-level union, outside brackets and string literals.
387
+ * @param type - The type.
388
+ */
389
+ function splitTopLevel(type) {
390
+ const text = topLevelText(type);
391
+ if (text === undefined) {
392
+ return [type.trim()];
393
+ }
394
+ const members = [];
395
+ let start = 0;
396
+ for (let index = 0; index < text.length; index++) {
397
+ if (text[index] === '|') {
398
+ members.push(type.slice(start, index).trim());
399
+ start = index + 1;
400
+ }
401
+ }
402
+ return [...members, type.slice(start).trim()];
403
+ }
404
+ /**
405
+ * Removes the parentheses around a whole type, such as those of `(string | number)`.
406
+ * @param type - The type.
407
+ */
408
+ function unwrapParentheses(type) {
409
+ let result = type.trim();
410
+ while (result.startsWith('(') && result.endsWith(')') && isBalanced(result.slice(1, -1))) {
411
+ result = result.slice(1, -1).trim();
412
+ }
413
+ return result;
414
+ }
415
+ /**
416
+ * Whether every bracket and string literal of a type is closed in it.
417
+ * @param type - The type.
418
+ */
419
+ function isBalanced(type) {
420
+ return topLevelText(type) !== undefined;
421
+ }
422
+ /**
423
+ * The type with every character inside brackets or a string literal replaced by a space, so that
424
+ * the operators of the type itself can be found by position; `undefined` when a bracket or a string
425
+ * is not closed. The `>` of an arrow (`=>`) closes no bracket.
426
+ * @param type - The type.
427
+ */
428
+ function topLevelText(type) {
429
+ let depth = 0;
430
+ let quote;
431
+ let text = '';
432
+ for (let index = 0; index < type.length; index++) {
433
+ const char = type[index];
434
+ if (quote) {
435
+ if (char === '\\') {
436
+ index++;
437
+ text += ' ';
438
+ continue;
439
+ }
440
+ if (char === quote) {
441
+ quote = undefined;
442
+ }
443
+ text += ' ';
444
+ continue;
445
+ }
446
+ if (char === "'" || char === '"' || char === '`') {
447
+ quote = char;
448
+ text += ' ';
449
+ }
450
+ else if ('([{<'.includes(char)) {
451
+ depth++;
452
+ text += ' ';
453
+ }
454
+ else if (char === '>' && type[index - 1] === '=') {
455
+ text += depth ? ' ' : char;
456
+ }
457
+ else if (')]}>'.includes(char)) {
458
+ depth--;
459
+ text += ' ';
460
+ if (depth < 0) {
461
+ return undefined;
462
+ }
463
+ }
464
+ else {
465
+ text += depth ? ' ' : char;
466
+ }
467
+ }
468
+ return depth === 0 && !quote ? text : undefined;
469
+ }
470
+
166
471
  let nextId = 0;
167
472
  /** Value types that have a chip colour. */
168
473
  const CHIP_TYPES = new Set([
@@ -269,7 +574,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.1", ngImpor
269
574
  }], ctorParameters: () => [], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], property: [{ type: i0.Input, args: [{ isSignal: true, alias: "property", required: false }] }], typeControl: [{ type: i0.Input, args: [{ isSignal: true, alias: "typeControl", required: false }] }], control: [{ type: i0.Input, args: [{ isSignal: true, alias: "control", required: false }] }], defaultValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultValue", required: false }] }], propertyOutlet: [{ type: i0.ViewChild, args: ['propertyOutlet', { ...{ read: ViewContainerRef }, isSignal: true }] }] } });
270
575
  /**
271
576
  * The value type of an input: its declared type when it is a primitive, else the type of the first
272
- * value of its type alias. Content slots are booleans.
577
+ * value of its type alias; none for an array. Content slots are booleans.
273
578
  * @param property - The input or the content slot.
274
579
  */
275
580
  function valueType(property) {
@@ -282,6 +587,11 @@ function valueType(property) {
282
587
  if (CHIP_TYPES.has(property.type)) {
283
588
  return property.type;
284
589
  }
590
+ // The options of an array are the values of its items, or the members of an optional array's
591
+ // union (`undefined` first): neither is the type of the input.
592
+ if (arrayType(property.type)) {
593
+ return '';
594
+ }
285
595
  const option = property.options?.[0];
286
596
  if (option !== undefined) {
287
597
  try {
@@ -322,6 +632,7 @@ const INPUT_ORDER = new Intl.Collator('en');
322
632
  */
323
633
  class NgDocPlaygroundPropertiesComponent {
324
634
  constructor() {
635
+ this.texts = inject(NG_DOC_TEXTS).playground;
325
636
  this.injector = inject(Injector);
326
637
  /** The form of the playground. */
327
638
  this.form = input.required(/* @ts-ignore */
@@ -338,6 +649,12 @@ class NgDocPlaygroundPropertiesComponent {
338
649
  /** Default values of the inputs; the inspector renders once they are known. */
339
650
  this.defaultValues = input(undefined, /* @ts-ignore */
340
651
  ...(ngDevMode ? [{ debugName: "defaultValues" }] : /* istanbul ignore next */ []));
652
+ /**
653
+ * Whether the demos render before the defaults are known: an isolated playground's iframes
654
+ * report them, so they must render first. The controls render once the defaults are known.
655
+ */
656
+ this.waitingForDefaults = input(false, /* @ts-ignore */
657
+ ...(ngDevMode ? [{ debugName: "waitingForDefaults" }] : /* istanbul ignore next */ []));
341
658
  /** Whether the inspector is hidden, leaving the demos only. */
342
659
  this.hideSidePanel = input(false, /* @ts-ignore */
343
660
  ...(ngDevMode ? [{ debugName: "hideSidePanel" }] : /* istanbul ignore next */ []));
@@ -386,7 +703,7 @@ class NgDocPlaygroundPropertiesComponent {
386
703
  sections.set(group, [...(sections.get(group) ?? []), control]);
387
704
  }
388
705
  return Array.from(sections, ([group, controls]) => ({
389
- title: group ?? 'Settings',
706
+ title: group ?? this.texts.settings,
390
707
  controls,
391
708
  })).filter((section) => section.controls.length > 0);
392
709
  }, /* @ts-ignore */
@@ -405,9 +722,19 @@ class NgDocPlaygroundPropertiesComponent {
405
722
  getTypeControl(property) {
406
723
  const type = property.type;
407
724
  const primitive = optionalPrimitive(type);
725
+ const array = arrayType(type);
726
+ const options = isPlaygroundProperty(property)
727
+ ? property.options
728
+ : undefined;
729
+ // The options of an array are the values of its items, when they are a union of literals.
408
730
  const typeControl = this.getControlForType(type) ??
409
731
  (primitive ? this.getControlForType(primitive) : undefined) ??
410
- this.getControlForTypeAlias(isPlaygroundProperty(property) ? property.options : undefined, property.isManual);
732
+ (array?.list ? this.getControlForType(array.list) : undefined) ??
733
+ (array
734
+ ? this.getControlForTypeAlias(options, property.isManual, UNION_ARRAY_CONTROL_TYPE)
735
+ : undefined) ??
736
+ (array ? this.getControlForType(ARRAY_CONTROL_TYPE) : undefined) ??
737
+ this.getControlForTypeAlias(options, property.isManual, 'NgDocTypeAlias');
411
738
  if (!typeControl && isDevMode()) {
412
739
  console.warn(`NgDocPlayground didn't find the control for the @Input "${property.inputName}", the type "${type}" was not recognized'`);
413
740
  }
@@ -417,25 +744,38 @@ class NgDocPlaygroundPropertiesComponent {
417
744
  const token = getTokenForType(type);
418
745
  return token ? this.injector.get(token) : undefined;
419
746
  }
420
- getControlForTypeAlias(options, isManual) {
421
- if (options && options.length) {
422
- let optionsIsValid = true;
423
- try {
424
- // checking that all values are extractable
425
- options.forEach((item) => resolvePlaygroundOption(item, isManual));
426
- }
427
- catch {
428
- optionsIsValid = false;
429
- }
430
- if (optionsIsValid) {
431
- const token = getTokenForType('NgDocTypeAlias');
432
- return token ? this.injector.get(token) : undefined;
433
- }
747
+ /**
748
+ * The control of the values of a union, when every option is a value: the select of a union
749
+ * (`NgDocTypeAlias`) or the multi-select of an array of one (`NgDocTypeAlias[]`). An optional
750
+ * array input without such options has the members of its own union (`undefined` and the array
751
+ * type), and the text of an array type is not a value of an item.
752
+ * @param options - The options of the input.
753
+ * @param isManual - Whether the options come from the playground's `controls`.
754
+ * @param controlType - The type the control is registered for.
755
+ */
756
+ getControlForTypeAlias(options, isManual, controlType) {
757
+ if (!options?.length) {
758
+ return undefined;
434
759
  }
435
- return undefined;
760
+ const values = [];
761
+ try {
762
+ // checking that all values are extractable
763
+ options.forEach((item) => values.push(resolvePlaygroundOption(item, isManual).value));
764
+ }
765
+ catch {
766
+ return undefined;
767
+ }
768
+ // The items of an array are values: an option that evaluates to an object, such as the text
769
+ // of a tuple type, means the options are not those of the items.
770
+ if (controlType === UNION_ARRAY_CONTROL_TYPE &&
771
+ !(values.every((value) => value === null || ITEM_TYPES.has(typeof value)) &&
772
+ values.some((value) => value !== null && value !== undefined))) {
773
+ return undefined;
774
+ }
775
+ return this.getControlForType(controlType);
436
776
  }
437
777
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.1", ngImport: i0, type: NgDocPlaygroundPropertiesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
438
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.1", type: NgDocPlaygroundPropertiesComponent, isStandalone: true, selector: "ng-doc-playground-properties", inputs: { form: { classPropertyName: "form", publicName: "form", isSignal: true, isRequired: true, transformFunction: null }, properties: { classPropertyName: "properties", publicName: "properties", isSignal: true, isRequired: false, transformFunction: null }, ignoreInputs: { classPropertyName: "ignoreInputs", publicName: "ignoreInputs", isSignal: true, isRequired: false, transformFunction: null }, dynamicContent: { classPropertyName: "dynamicContent", publicName: "dynamicContent", isSignal: true, isRequired: false, transformFunction: null }, defaultValues: { classPropertyName: "defaultValues", publicName: "defaultValues", isSignal: true, isRequired: false, transformFunction: null }, hideSidePanel: { classPropertyName: "hideSidePanel", publicName: "hideSidePanel", isSignal: true, isRequired: false, transformFunction: null }, inspectorPosition: { classPropertyName: "inspectorPosition", publicName: "inspectorPosition", isSignal: true, isRequired: false, transformFunction: null }, recreateDemo: { classPropertyName: "recreateDemo", publicName: "recreateDemo", isSignal: true, isRequired: false, transformFunction: null }, recreateLocked: { classPropertyName: "recreateLocked", publicName: "recreateLocked", isSignal: true, isRequired: false, transformFunction: null }, showResetButton: { classPropertyName: "showResetButton", publicName: "showResetButton", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { recreateDemo: "recreateDemoChange", resetForm: "resetForm" }, ngImport: i0, template: "@if (defaultValues(); as defaultValues) {\n <div\n class=\"ng-doc-playground-properties-wrapper\"\n [class.no-side-panel]=\"hideSidePanel()\"\n [class.inspector-bottom]=\"inspectorPosition() === 'bottom'\">\n <div class=\"ng-doc-playground-demos\">\n <ng-content></ng-content>\n </div>\n @if (!hideSidePanel()) {\n <div class=\"ng-doc-playground-properties\">\n <div class=\"ng-doc-playground-header\">\n <h4>Playground</h4>\n @if (showResetButton()) {\n <button type=\"button\" class=\"ng-doc-playground-reset\" (click)=\"resetForm.emit()\">\n Reset\n </button>\n }\n </div>\n @if (!recreateLocked()) {\n <div class=\"ng-doc-playground-setting\">\n <ng-doc-checkbox [ngModel]=\"recreateDemo()\" (ngModelChange)=\"recreateDemo.set($event)\">\n Recreate\n </ng-doc-checkbox>\n <ng-doc-icon\n class=\"ng-doc-playground-info\"\n icon=\"info\"\n role=\"img\"\n aria-label=\"Recreates the demo every time one of the inputs changes\"\n [ngDocTooltip]=\"'Recreates the demo every time one of the inputs changes'\"\n [positions]=\"['bottom-right', 'left-center']\"></ng-doc-icon>\n </div>\n }\n <div class=\"ng-doc-playground-divider\"></div>\n @for (section of propertySections(); track $index) {\n <h5 class=\"ng-doc-title\">{{ section.title }}</h5>\n <div class=\"ng-doc-playground-property-list\">\n @for (propertyControl of section.controls; track propertyControl.propertyName) {\n <ng-doc-playground-property\n [name]=\"propertyControl.property.label ?? propertyControl.property.inputName\"\n [property]=\"propertyControl.property\"\n [typeControl]=\"propertyControl.typeControl\"\n [defaultValue]=\"defaultValues[propertyControl.propertyName]\"\n [control]=\"\n getFormControl | bind: this | execute: 'properties' : propertyControl.propertyName\n \">\n </ng-doc-playground-property>\n }\n </div>\n }\n @if ((dynamicContent() | keyvalue)?.length) {\n <h5 class=\"ng-doc-title\">Content</h5>\n <div class=\"ng-doc-playground-property-list\">\n @for (content of dynamicContent() | keyvalue; track content.key) {\n <ng-doc-playground-property\n [name]=\"content.value.label\"\n [property]=\"content.value\"\n [typeControl]=\"contentTypeControl\"\n [control]=\"getFormControl | bind: this | execute: 'content' : content.key\">\n </ng-doc-playground-property>\n }\n </div>\n }\n </div>\n }\n </div>\n}\n", styles: [":host{display:block;container:ng-doc-playground/inline-size}.ng-doc-playground-properties-wrapper{display:grid;grid-template-columns:minmax(0,1fr) var(--ng-doc-playground-inspector-width);align-items:stretch;border:var(--ng-doc-demo-displayer-border);border-radius:var(--ng-doc-demo-displayer-border-radius);overflow:clip;background-color:var(--ng-doc-demo-displayer-background, var(--ng-doc-background))}.ng-doc-playground-properties-wrapper.no-side-panel{grid-template-columns:minmax(0,1fr)}.ng-doc-playground-properties-wrapper:not(.no-side-panel):not(.inspector-bottom) .ng-doc-playground-demos{position:sticky;top:calc(var(--ng-doc-page-sticky-offset) + var(--ng-doc-base-gutter));align-self:start;z-index:4}.ng-doc-playground-demos{display:flex;flex-direction:column;min-width:0}.ng-doc-playground-properties-wrapper:not(.no-side-panel):not(.inspector-bottom) .ng-doc-playground-demos{min-height:min(100%,100dvh - var(--ng-doc-page-sticky-offset) - 2 * var(--ng-doc-base-gutter))}.ng-doc-playground-properties{min-width:0;background:var(--ng-doc-base-1);border-left:1px solid var(--ng-doc-border-color);font-family:var(--ng-doc-font-family);font-size:14px;line-height:20px;color:var(--ng-doc-text);--ng-doc-input-height: 36px;--ng-doc-input-border: var(--ng-doc-playground-input-border);--ng-doc-checkbox-border: var(--ng-doc-playground-input-border)}.ng-doc-playground-header{display:flex;align-items:center;gap:var(--ng-doc-space-2);height:52px;padding:0 var(--ng-doc-space-3) 0 var(--ng-doc-space-4)}.ng-doc-playground-header h4{margin:0;font-family:var(--ng-doc-heading-font-family);font-size:15px;line-height:22px;font-weight:600;letter-spacing:0;color:var(--ng-doc-heading-color)}.ng-doc-playground-reset{display:inline-flex;align-items:center;height:28px;margin-left:auto;padding:0 10px;border:1px solid var(--ng-doc-border-color);border-radius:var(--ng-doc-radius-md);background:var(--ng-doc-background);font:inherit;font-size:12px;font-weight:500;color:var(--ng-doc-heading-color);cursor:pointer}.ng-doc-playground-reset:hover{border-color:var(--ng-doc-border-hover-color)}.ng-doc-playground-reset:focus-visible{outline:none;box-shadow:var(--ng-doc-focus-ring)}.ng-doc-playground-setting{display:flex;align-items:center;gap:6px;padding:0 var(--ng-doc-space-4) var(--ng-doc-space-3)}.ng-doc-playground-info{--ng-doc-icon-color: var(--ng-doc-text-muted)}.ng-doc-playground-divider{height:1px;background:var(--ng-doc-border-color)}h5.ng-doc-title{margin:0;padding:var(--ng-doc-space-3) var(--ng-doc-space-4) var(--ng-doc-space-1);font-family:var(--ng-doc-font-family);font-size:11px;line-height:16px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:var(--ng-doc-text-muted)}ng-doc-playground-property{padding:var(--ng-doc-space-2) var(--ng-doc-space-4)}ng-doc-playground-property+ng-doc-playground-property{border-top:1px solid var(--ng-doc-base-2)}.ng-doc-playground-property-list:last-child{padding-bottom:var(--ng-doc-space-2)}.ng-doc-playground-properties-wrapper.inspector-bottom{grid-template-columns:minmax(0,1fr)}.ng-doc-playground-properties-wrapper.inspector-bottom .ng-doc-playground-properties{border-left:0;border-top:1px solid var(--ng-doc-border-color)}.ng-doc-playground-properties-wrapper.inspector-bottom .ng-doc-playground-property-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,320px),1fr));column-gap:var(--ng-doc-space-4)}.ng-doc-playground-properties-wrapper.inspector-bottom ng-doc-playground-property+ng-doc-playground-property{border-top:0}@container ng-doc-playground (max-width: 640px){.ng-doc-playground-properties-wrapper{grid-template-columns:minmax(0,1fr)}.ng-doc-playground-properties-wrapper:not(.no-side-panel) .ng-doc-playground-demos{position:static}.ng-doc-playground-properties{border-left:0;border-top:1px solid var(--ng-doc-border-color)}}\n"], dependencies: [{ kind: "component", type: NgDocCheckboxComponent, selector: "ng-doc-checkbox", inputs: ["color"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: NgDocTooltipDirective, selector: "[ngDocTooltip]", inputs: ["ngDocTooltip", "delay", "displayOrigin", "pointerOrigin", "positions", "canOpen", "panelClass", "minHeight", "maxHeight", "height", "minWidth", "maxWidth", "width"], outputs: ["beforeOpen", "afterOpen", "beforeClose", "afterClose"], exportAs: ["ngDocTooltip"] }, { kind: "component", type: NgDocIconComponent, selector: "ng-doc-icon", inputs: ["icon", "customIcon", "size"] }, { kind: "component", type: NgDocPlaygroundPropertyComponent, selector: "ng-doc-playground-property", inputs: ["name", "property", "typeControl", "control", "defaultValue"] }, { kind: "pipe", type: KeyValuePipe, name: "keyvalue" }, { kind: "pipe", type: NgDocBindPipe, name: "bind" }, { kind: "pipe", type: NgDocExecutePipe, name: "execute" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
778
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.1", type: NgDocPlaygroundPropertiesComponent, isStandalone: true, selector: "ng-doc-playground-properties", inputs: { form: { classPropertyName: "form", publicName: "form", isSignal: true, isRequired: true, transformFunction: null }, properties: { classPropertyName: "properties", publicName: "properties", isSignal: true, isRequired: false, transformFunction: null }, ignoreInputs: { classPropertyName: "ignoreInputs", publicName: "ignoreInputs", isSignal: true, isRequired: false, transformFunction: null }, dynamicContent: { classPropertyName: "dynamicContent", publicName: "dynamicContent", isSignal: true, isRequired: false, transformFunction: null }, defaultValues: { classPropertyName: "defaultValues", publicName: "defaultValues", isSignal: true, isRequired: false, transformFunction: null }, waitingForDefaults: { classPropertyName: "waitingForDefaults", publicName: "waitingForDefaults", isSignal: true, isRequired: false, transformFunction: null }, hideSidePanel: { classPropertyName: "hideSidePanel", publicName: "hideSidePanel", isSignal: true, isRequired: false, transformFunction: null }, inspectorPosition: { classPropertyName: "inspectorPosition", publicName: "inspectorPosition", isSignal: true, isRequired: false, transformFunction: null }, recreateDemo: { classPropertyName: "recreateDemo", publicName: "recreateDemo", isSignal: true, isRequired: false, transformFunction: null }, recreateLocked: { classPropertyName: "recreateLocked", publicName: "recreateLocked", isSignal: true, isRequired: false, transformFunction: null }, showResetButton: { classPropertyName: "showResetButton", publicName: "showResetButton", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { recreateDemo: "recreateDemoChange", resetForm: "resetForm" }, ngImport: i0, template: "@if (defaultValues() || waitingForDefaults()) {\n <div\n class=\"ng-doc-playground-properties-wrapper\"\n [class.no-side-panel]=\"hideSidePanel()\"\n [class.inspector-bottom]=\"inspectorPosition() === 'bottom'\">\n <div class=\"ng-doc-playground-demos\">\n <ng-content></ng-content>\n </div>\n @if (!hideSidePanel() && defaultValues(); as defaultValues) {\n <div class=\"ng-doc-playground-properties\">\n <div class=\"ng-doc-playground-header\">\n <h4>{{ texts.title }}</h4>\n @if (showResetButton()) {\n <button type=\"button\" class=\"ng-doc-playground-reset\" (click)=\"resetForm.emit()\">\n {{ texts.reset }}\n </button>\n }\n </div>\n @if (!recreateLocked()) {\n <div class=\"ng-doc-playground-setting\">\n <ng-doc-checkbox [ngModel]=\"recreateDemo()\" (ngModelChange)=\"recreateDemo.set($event)\">\n {{ texts.recreate }}\n </ng-doc-checkbox>\n <ng-doc-icon\n class=\"ng-doc-playground-info\"\n icon=\"info\"\n role=\"img\"\n [attr.aria-label]=\"texts.recreateHint\"\n [ngDocTooltip]=\"texts.recreateHint\"\n [positions]=\"['bottom-right', 'left-center']\"></ng-doc-icon>\n </div>\n }\n <div class=\"ng-doc-playground-divider\"></div>\n @for (section of propertySections(); track $index) {\n <h5 class=\"ng-doc-title\">{{ section.title }}</h5>\n <div class=\"ng-doc-playground-property-list\">\n @for (propertyControl of section.controls; track propertyControl.propertyName) {\n <ng-doc-playground-property\n [name]=\"propertyControl.property.label ?? propertyControl.property.inputName\"\n [property]=\"propertyControl.property\"\n [typeControl]=\"propertyControl.typeControl\"\n [defaultValue]=\"defaultValues[propertyControl.propertyName]\"\n [control]=\"\n getFormControl | bind: this | execute: 'properties' : propertyControl.propertyName\n \">\n </ng-doc-playground-property>\n }\n </div>\n }\n @if ((dynamicContent() | keyvalue)?.length) {\n <h5 class=\"ng-doc-title\">{{ texts.content }}</h5>\n <div class=\"ng-doc-playground-property-list\">\n @for (content of dynamicContent() | keyvalue; track content.key) {\n <ng-doc-playground-property\n [name]=\"content.value.label\"\n [property]=\"content.value\"\n [typeControl]=\"contentTypeControl\"\n [control]=\"getFormControl | bind: this | execute: 'content' : content.key\">\n </ng-doc-playground-property>\n }\n </div>\n }\n </div>\n }\n </div>\n}\n", styles: [":host{display:block;container:ng-doc-playground/inline-size}.ng-doc-playground-properties-wrapper{display:grid;grid-template-columns:minmax(0,1fr) var(--ng-doc-playground-inspector-width);align-items:stretch;border:var(--ng-doc-demo-displayer-border);border-radius:var(--ng-doc-demo-displayer-border-radius);overflow:clip;background-color:var(--ng-doc-demo-displayer-background, var(--ng-doc-background))}.ng-doc-playground-properties-wrapper.no-side-panel{grid-template-columns:minmax(0,1fr)}.ng-doc-playground-properties-wrapper:not(.no-side-panel):not(.inspector-bottom) .ng-doc-playground-demos{position:sticky;top:calc(var(--ng-doc-page-sticky-offset) + var(--ng-doc-base-gutter));align-self:start;z-index:4}.ng-doc-playground-demos{display:flex;flex-direction:column;min-width:0}.ng-doc-playground-properties-wrapper:not(.no-side-panel):not(.inspector-bottom) .ng-doc-playground-demos{min-height:min(100%,100dvh - var(--ng-doc-page-sticky-offset) - 2 * var(--ng-doc-base-gutter))}.ng-doc-playground-properties{min-width:0;background:var(--ng-doc-base-1);border-left:1px solid var(--ng-doc-border-color);font-family:var(--ng-doc-font-family);font-size:14px;line-height:20px;color:var(--ng-doc-text);--ng-doc-input-height: 36px;--ng-doc-input-border: var(--ng-doc-playground-input-border);--ng-doc-checkbox-border: var(--ng-doc-playground-input-border)}.ng-doc-playground-header{display:flex;align-items:center;gap:var(--ng-doc-space-2);height:52px;padding:0 var(--ng-doc-space-3) 0 var(--ng-doc-space-4)}.ng-doc-playground-header h4{margin:0;font-family:var(--ng-doc-heading-font-family);font-size:15px;line-height:22px;font-weight:600;letter-spacing:0;color:var(--ng-doc-heading-color)}.ng-doc-playground-reset{display:inline-flex;align-items:center;height:28px;margin-left:auto;padding:0 10px;border:1px solid var(--ng-doc-border-color);border-radius:var(--ng-doc-radius-md);background:var(--ng-doc-background);font:inherit;font-size:12px;font-weight:500;color:var(--ng-doc-heading-color);cursor:pointer}.ng-doc-playground-reset:hover{border-color:var(--ng-doc-border-hover-color)}.ng-doc-playground-reset:focus-visible{outline:none;box-shadow:var(--ng-doc-focus-ring)}.ng-doc-playground-setting{display:flex;align-items:center;gap:6px;padding:0 var(--ng-doc-space-4) var(--ng-doc-space-3)}.ng-doc-playground-info{--ng-doc-icon-color: var(--ng-doc-text-muted)}.ng-doc-playground-divider{height:1px;background:var(--ng-doc-border-color)}h5.ng-doc-title{margin:0;padding:var(--ng-doc-space-3) var(--ng-doc-space-4) var(--ng-doc-space-1);font-family:var(--ng-doc-font-family);font-size:11px;line-height:16px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:var(--ng-doc-text-muted)}ng-doc-playground-property{padding:var(--ng-doc-space-2) var(--ng-doc-space-4)}ng-doc-playground-property+ng-doc-playground-property{border-top:1px solid var(--ng-doc-base-2)}.ng-doc-playground-property-list:last-child{padding-bottom:var(--ng-doc-space-2)}.ng-doc-playground-properties-wrapper.inspector-bottom{grid-template-columns:minmax(0,1fr)}.ng-doc-playground-properties-wrapper.inspector-bottom .ng-doc-playground-properties{border-left:0;border-top:1px solid var(--ng-doc-border-color)}.ng-doc-playground-properties-wrapper.inspector-bottom .ng-doc-playground-property-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,320px),1fr));column-gap:var(--ng-doc-space-4)}.ng-doc-playground-properties-wrapper.inspector-bottom ng-doc-playground-property+ng-doc-playground-property{border-top:0}@container ng-doc-playground (max-width: 640px){.ng-doc-playground-properties-wrapper{grid-template-columns:minmax(0,1fr)}.ng-doc-playground-properties-wrapper:not(.no-side-panel) .ng-doc-playground-demos{position:static}.ng-doc-playground-properties{border-left:0;border-top:1px solid var(--ng-doc-border-color)}}\n"], dependencies: [{ kind: "component", type: NgDocCheckboxComponent, selector: "ng-doc-checkbox", inputs: ["color"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: NgDocTooltipDirective, selector: "[ngDocTooltip]", inputs: ["ngDocTooltip", "delay", "displayOrigin", "pointerOrigin", "positions", "canOpen", "panelClass", "minHeight", "maxHeight", "height", "minWidth", "maxWidth", "width"], outputs: ["beforeOpen", "afterOpen", "beforeClose", "afterClose"], exportAs: ["ngDocTooltip"] }, { kind: "component", type: NgDocIconComponent, selector: "ng-doc-icon", inputs: ["icon", "customIcon", "size"] }, { kind: "component", type: NgDocPlaygroundPropertyComponent, selector: "ng-doc-playground-property", inputs: ["name", "property", "typeControl", "control", "defaultValue"] }, { kind: "pipe", type: KeyValuePipe, name: "keyvalue" }, { kind: "pipe", type: NgDocBindPipe, name: "bind" }, { kind: "pipe", type: NgDocExecutePipe, name: "execute" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
439
779
  }
440
780
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.1", ngImport: i0, type: NgDocPlaygroundPropertiesComponent, decorators: [{
441
781
  type: Component,
@@ -448,8 +788,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.1", ngImpor
448
788
  KeyValuePipe,
449
789
  NgDocBindPipe,
450
790
  NgDocExecutePipe,
451
- ], template: "@if (defaultValues(); as defaultValues) {\n <div\n class=\"ng-doc-playground-properties-wrapper\"\n [class.no-side-panel]=\"hideSidePanel()\"\n [class.inspector-bottom]=\"inspectorPosition() === 'bottom'\">\n <div class=\"ng-doc-playground-demos\">\n <ng-content></ng-content>\n </div>\n @if (!hideSidePanel()) {\n <div class=\"ng-doc-playground-properties\">\n <div class=\"ng-doc-playground-header\">\n <h4>Playground</h4>\n @if (showResetButton()) {\n <button type=\"button\" class=\"ng-doc-playground-reset\" (click)=\"resetForm.emit()\">\n Reset\n </button>\n }\n </div>\n @if (!recreateLocked()) {\n <div class=\"ng-doc-playground-setting\">\n <ng-doc-checkbox [ngModel]=\"recreateDemo()\" (ngModelChange)=\"recreateDemo.set($event)\">\n Recreate\n </ng-doc-checkbox>\n <ng-doc-icon\n class=\"ng-doc-playground-info\"\n icon=\"info\"\n role=\"img\"\n aria-label=\"Recreates the demo every time one of the inputs changes\"\n [ngDocTooltip]=\"'Recreates the demo every time one of the inputs changes'\"\n [positions]=\"['bottom-right', 'left-center']\"></ng-doc-icon>\n </div>\n }\n <div class=\"ng-doc-playground-divider\"></div>\n @for (section of propertySections(); track $index) {\n <h5 class=\"ng-doc-title\">{{ section.title }}</h5>\n <div class=\"ng-doc-playground-property-list\">\n @for (propertyControl of section.controls; track propertyControl.propertyName) {\n <ng-doc-playground-property\n [name]=\"propertyControl.property.label ?? propertyControl.property.inputName\"\n [property]=\"propertyControl.property\"\n [typeControl]=\"propertyControl.typeControl\"\n [defaultValue]=\"defaultValues[propertyControl.propertyName]\"\n [control]=\"\n getFormControl | bind: this | execute: 'properties' : propertyControl.propertyName\n \">\n </ng-doc-playground-property>\n }\n </div>\n }\n @if ((dynamicContent() | keyvalue)?.length) {\n <h5 class=\"ng-doc-title\">Content</h5>\n <div class=\"ng-doc-playground-property-list\">\n @for (content of dynamicContent() | keyvalue; track content.key) {\n <ng-doc-playground-property\n [name]=\"content.value.label\"\n [property]=\"content.value\"\n [typeControl]=\"contentTypeControl\"\n [control]=\"getFormControl | bind: this | execute: 'content' : content.key\">\n </ng-doc-playground-property>\n }\n </div>\n }\n </div>\n }\n </div>\n}\n", styles: [":host{display:block;container:ng-doc-playground/inline-size}.ng-doc-playground-properties-wrapper{display:grid;grid-template-columns:minmax(0,1fr) var(--ng-doc-playground-inspector-width);align-items:stretch;border:var(--ng-doc-demo-displayer-border);border-radius:var(--ng-doc-demo-displayer-border-radius);overflow:clip;background-color:var(--ng-doc-demo-displayer-background, var(--ng-doc-background))}.ng-doc-playground-properties-wrapper.no-side-panel{grid-template-columns:minmax(0,1fr)}.ng-doc-playground-properties-wrapper:not(.no-side-panel):not(.inspector-bottom) .ng-doc-playground-demos{position:sticky;top:calc(var(--ng-doc-page-sticky-offset) + var(--ng-doc-base-gutter));align-self:start;z-index:4}.ng-doc-playground-demos{display:flex;flex-direction:column;min-width:0}.ng-doc-playground-properties-wrapper:not(.no-side-panel):not(.inspector-bottom) .ng-doc-playground-demos{min-height:min(100%,100dvh - var(--ng-doc-page-sticky-offset) - 2 * var(--ng-doc-base-gutter))}.ng-doc-playground-properties{min-width:0;background:var(--ng-doc-base-1);border-left:1px solid var(--ng-doc-border-color);font-family:var(--ng-doc-font-family);font-size:14px;line-height:20px;color:var(--ng-doc-text);--ng-doc-input-height: 36px;--ng-doc-input-border: var(--ng-doc-playground-input-border);--ng-doc-checkbox-border: var(--ng-doc-playground-input-border)}.ng-doc-playground-header{display:flex;align-items:center;gap:var(--ng-doc-space-2);height:52px;padding:0 var(--ng-doc-space-3) 0 var(--ng-doc-space-4)}.ng-doc-playground-header h4{margin:0;font-family:var(--ng-doc-heading-font-family);font-size:15px;line-height:22px;font-weight:600;letter-spacing:0;color:var(--ng-doc-heading-color)}.ng-doc-playground-reset{display:inline-flex;align-items:center;height:28px;margin-left:auto;padding:0 10px;border:1px solid var(--ng-doc-border-color);border-radius:var(--ng-doc-radius-md);background:var(--ng-doc-background);font:inherit;font-size:12px;font-weight:500;color:var(--ng-doc-heading-color);cursor:pointer}.ng-doc-playground-reset:hover{border-color:var(--ng-doc-border-hover-color)}.ng-doc-playground-reset:focus-visible{outline:none;box-shadow:var(--ng-doc-focus-ring)}.ng-doc-playground-setting{display:flex;align-items:center;gap:6px;padding:0 var(--ng-doc-space-4) var(--ng-doc-space-3)}.ng-doc-playground-info{--ng-doc-icon-color: var(--ng-doc-text-muted)}.ng-doc-playground-divider{height:1px;background:var(--ng-doc-border-color)}h5.ng-doc-title{margin:0;padding:var(--ng-doc-space-3) var(--ng-doc-space-4) var(--ng-doc-space-1);font-family:var(--ng-doc-font-family);font-size:11px;line-height:16px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:var(--ng-doc-text-muted)}ng-doc-playground-property{padding:var(--ng-doc-space-2) var(--ng-doc-space-4)}ng-doc-playground-property+ng-doc-playground-property{border-top:1px solid var(--ng-doc-base-2)}.ng-doc-playground-property-list:last-child{padding-bottom:var(--ng-doc-space-2)}.ng-doc-playground-properties-wrapper.inspector-bottom{grid-template-columns:minmax(0,1fr)}.ng-doc-playground-properties-wrapper.inspector-bottom .ng-doc-playground-properties{border-left:0;border-top:1px solid var(--ng-doc-border-color)}.ng-doc-playground-properties-wrapper.inspector-bottom .ng-doc-playground-property-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,320px),1fr));column-gap:var(--ng-doc-space-4)}.ng-doc-playground-properties-wrapper.inspector-bottom ng-doc-playground-property+ng-doc-playground-property{border-top:0}@container ng-doc-playground (max-width: 640px){.ng-doc-playground-properties-wrapper{grid-template-columns:minmax(0,1fr)}.ng-doc-playground-properties-wrapper:not(.no-side-panel) .ng-doc-playground-demos{position:static}.ng-doc-playground-properties{border-left:0;border-top:1px solid var(--ng-doc-border-color)}}\n"] }]
452
- }], propDecorators: { form: [{ type: i0.Input, args: [{ isSignal: true, alias: "form", required: true }] }], properties: [{ type: i0.Input, args: [{ isSignal: true, alias: "properties", required: false }] }], ignoreInputs: [{ type: i0.Input, args: [{ isSignal: true, alias: "ignoreInputs", required: false }] }], dynamicContent: [{ type: i0.Input, args: [{ isSignal: true, alias: "dynamicContent", required: false }] }], defaultValues: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultValues", required: false }] }], hideSidePanel: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideSidePanel", required: false }] }], inspectorPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "inspectorPosition", required: false }] }], recreateDemo: [{ type: i0.Input, args: [{ isSignal: true, alias: "recreateDemo", required: false }] }, { type: i0.Output, args: ["recreateDemoChange"] }], recreateLocked: [{ type: i0.Input, args: [{ isSignal: true, alias: "recreateLocked", required: false }] }], showResetButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showResetButton", required: false }] }], resetForm: [{ type: i0.Output, args: ["resetForm"] }] } });
791
+ ], template: "@if (defaultValues() || waitingForDefaults()) {\n <div\n class=\"ng-doc-playground-properties-wrapper\"\n [class.no-side-panel]=\"hideSidePanel()\"\n [class.inspector-bottom]=\"inspectorPosition() === 'bottom'\">\n <div class=\"ng-doc-playground-demos\">\n <ng-content></ng-content>\n </div>\n @if (!hideSidePanel() && defaultValues(); as defaultValues) {\n <div class=\"ng-doc-playground-properties\">\n <div class=\"ng-doc-playground-header\">\n <h4>{{ texts.title }}</h4>\n @if (showResetButton()) {\n <button type=\"button\" class=\"ng-doc-playground-reset\" (click)=\"resetForm.emit()\">\n {{ texts.reset }}\n </button>\n }\n </div>\n @if (!recreateLocked()) {\n <div class=\"ng-doc-playground-setting\">\n <ng-doc-checkbox [ngModel]=\"recreateDemo()\" (ngModelChange)=\"recreateDemo.set($event)\">\n {{ texts.recreate }}\n </ng-doc-checkbox>\n <ng-doc-icon\n class=\"ng-doc-playground-info\"\n icon=\"info\"\n role=\"img\"\n [attr.aria-label]=\"texts.recreateHint\"\n [ngDocTooltip]=\"texts.recreateHint\"\n [positions]=\"['bottom-right', 'left-center']\"></ng-doc-icon>\n </div>\n }\n <div class=\"ng-doc-playground-divider\"></div>\n @for (section of propertySections(); track $index) {\n <h5 class=\"ng-doc-title\">{{ section.title }}</h5>\n <div class=\"ng-doc-playground-property-list\">\n @for (propertyControl of section.controls; track propertyControl.propertyName) {\n <ng-doc-playground-property\n [name]=\"propertyControl.property.label ?? propertyControl.property.inputName\"\n [property]=\"propertyControl.property\"\n [typeControl]=\"propertyControl.typeControl\"\n [defaultValue]=\"defaultValues[propertyControl.propertyName]\"\n [control]=\"\n getFormControl | bind: this | execute: 'properties' : propertyControl.propertyName\n \">\n </ng-doc-playground-property>\n }\n </div>\n }\n @if ((dynamicContent() | keyvalue)?.length) {\n <h5 class=\"ng-doc-title\">{{ texts.content }}</h5>\n <div class=\"ng-doc-playground-property-list\">\n @for (content of dynamicContent() | keyvalue; track content.key) {\n <ng-doc-playground-property\n [name]=\"content.value.label\"\n [property]=\"content.value\"\n [typeControl]=\"contentTypeControl\"\n [control]=\"getFormControl | bind: this | execute: 'content' : content.key\">\n </ng-doc-playground-property>\n }\n </div>\n }\n </div>\n }\n </div>\n}\n", styles: [":host{display:block;container:ng-doc-playground/inline-size}.ng-doc-playground-properties-wrapper{display:grid;grid-template-columns:minmax(0,1fr) var(--ng-doc-playground-inspector-width);align-items:stretch;border:var(--ng-doc-demo-displayer-border);border-radius:var(--ng-doc-demo-displayer-border-radius);overflow:clip;background-color:var(--ng-doc-demo-displayer-background, var(--ng-doc-background))}.ng-doc-playground-properties-wrapper.no-side-panel{grid-template-columns:minmax(0,1fr)}.ng-doc-playground-properties-wrapper:not(.no-side-panel):not(.inspector-bottom) .ng-doc-playground-demos{position:sticky;top:calc(var(--ng-doc-page-sticky-offset) + var(--ng-doc-base-gutter));align-self:start;z-index:4}.ng-doc-playground-demos{display:flex;flex-direction:column;min-width:0}.ng-doc-playground-properties-wrapper:not(.no-side-panel):not(.inspector-bottom) .ng-doc-playground-demos{min-height:min(100%,100dvh - var(--ng-doc-page-sticky-offset) - 2 * var(--ng-doc-base-gutter))}.ng-doc-playground-properties{min-width:0;background:var(--ng-doc-base-1);border-left:1px solid var(--ng-doc-border-color);font-family:var(--ng-doc-font-family);font-size:14px;line-height:20px;color:var(--ng-doc-text);--ng-doc-input-height: 36px;--ng-doc-input-border: var(--ng-doc-playground-input-border);--ng-doc-checkbox-border: var(--ng-doc-playground-input-border)}.ng-doc-playground-header{display:flex;align-items:center;gap:var(--ng-doc-space-2);height:52px;padding:0 var(--ng-doc-space-3) 0 var(--ng-doc-space-4)}.ng-doc-playground-header h4{margin:0;font-family:var(--ng-doc-heading-font-family);font-size:15px;line-height:22px;font-weight:600;letter-spacing:0;color:var(--ng-doc-heading-color)}.ng-doc-playground-reset{display:inline-flex;align-items:center;height:28px;margin-left:auto;padding:0 10px;border:1px solid var(--ng-doc-border-color);border-radius:var(--ng-doc-radius-md);background:var(--ng-doc-background);font:inherit;font-size:12px;font-weight:500;color:var(--ng-doc-heading-color);cursor:pointer}.ng-doc-playground-reset:hover{border-color:var(--ng-doc-border-hover-color)}.ng-doc-playground-reset:focus-visible{outline:none;box-shadow:var(--ng-doc-focus-ring)}.ng-doc-playground-setting{display:flex;align-items:center;gap:6px;padding:0 var(--ng-doc-space-4) var(--ng-doc-space-3)}.ng-doc-playground-info{--ng-doc-icon-color: var(--ng-doc-text-muted)}.ng-doc-playground-divider{height:1px;background:var(--ng-doc-border-color)}h5.ng-doc-title{margin:0;padding:var(--ng-doc-space-3) var(--ng-doc-space-4) var(--ng-doc-space-1);font-family:var(--ng-doc-font-family);font-size:11px;line-height:16px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:var(--ng-doc-text-muted)}ng-doc-playground-property{padding:var(--ng-doc-space-2) var(--ng-doc-space-4)}ng-doc-playground-property+ng-doc-playground-property{border-top:1px solid var(--ng-doc-base-2)}.ng-doc-playground-property-list:last-child{padding-bottom:var(--ng-doc-space-2)}.ng-doc-playground-properties-wrapper.inspector-bottom{grid-template-columns:minmax(0,1fr)}.ng-doc-playground-properties-wrapper.inspector-bottom .ng-doc-playground-properties{border-left:0;border-top:1px solid var(--ng-doc-border-color)}.ng-doc-playground-properties-wrapper.inspector-bottom .ng-doc-playground-property-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,320px),1fr));column-gap:var(--ng-doc-space-4)}.ng-doc-playground-properties-wrapper.inspector-bottom ng-doc-playground-property+ng-doc-playground-property{border-top:0}@container ng-doc-playground (max-width: 640px){.ng-doc-playground-properties-wrapper{grid-template-columns:minmax(0,1fr)}.ng-doc-playground-properties-wrapper:not(.no-side-panel) .ng-doc-playground-demos{position:static}.ng-doc-playground-properties{border-left:0;border-top:1px solid var(--ng-doc-border-color)}}\n"] }]
792
+ }], propDecorators: { form: [{ type: i0.Input, args: [{ isSignal: true, alias: "form", required: true }] }], properties: [{ type: i0.Input, args: [{ isSignal: true, alias: "properties", required: false }] }], ignoreInputs: [{ type: i0.Input, args: [{ isSignal: true, alias: "ignoreInputs", required: false }] }], dynamicContent: [{ type: i0.Input, args: [{ isSignal: true, alias: "dynamicContent", required: false }] }], defaultValues: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultValues", required: false }] }], waitingForDefaults: [{ type: i0.Input, args: [{ isSignal: true, alias: "waitingForDefaults", required: false }] }], hideSidePanel: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideSidePanel", required: false }] }], inspectorPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "inspectorPosition", required: false }] }], recreateDemo: [{ type: i0.Input, args: [{ isSignal: true, alias: "recreateDemo", required: false }] }, { type: i0.Output, args: ["recreateDemoChange"] }], recreateLocked: [{ type: i0.Input, args: [{ isSignal: true, alias: "recreateLocked", required: false }] }], showResetButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showResetButton", required: false }] }], resetForm: [{ type: i0.Output, args: ["resetForm"] }] } });
453
793
  /**
454
794
  * Compares the `order` of two inputs from the playground's `controls`: inputs with one come first,
455
795
  * lowest first.
@@ -489,15 +829,17 @@ function compareByTypeControl(a, b) {
489
829
  }
490
830
  return INPUT_ORDER.compare(a.property.inputName, b.property.inputName);
491
831
  }
832
+ /** The types of the values that an item of the multi-select of a union can have. */
833
+ const ITEM_TYPES = new Set(['string', 'number', 'boolean', 'undefined']);
492
834
  /** The types whose control also edits an optional or nullable input of the type. */
493
835
  const PRIMITIVE_TYPES = new Set(['string', 'number', 'boolean']);
494
836
  /**
495
837
  * The primitive of an optional or nullable primitive input type, such as `string` of
496
838
  * `string | undefined` (an optional `@Input() label?: string`, `input<string>()` or
497
839
  * `model<string>()`) or of `number | null`, so that the input gets the primitive's control.
498
- * Other types are matched by their exact name only: a custom control registered for `Position`
499
- * does not receive the `undefined` of a `Position | undefined` input unless it is registered for
500
- * that type too.
840
+ * Other types are matched by their exact name only (arrays of strings and numbers aside, see
841
+ * `arrayType`): a custom control registered for `Position` does not receive the `undefined` of a
842
+ * `Position | undefined` input unless it is registered for that type too.
501
843
  * @param type - The input type as the builder printed it.
502
844
  * @returns The primitive, or `undefined` when the type is not one.
503
845
  */
@@ -512,11 +854,18 @@ function optionalPrimitive(type) {
512
854
  /**
513
855
  * A playground: an inspector with a control per input and content slot of its target, and a demo
514
856
  * per selector (or the pipe) rendered with the chosen values.
857
+ *
858
+ * An isolated playground (the `isolated` option, or `isolatedPlaygrounds`) shows each demo in an
859
+ * iframe of its page in the demo application, which the Vite engine builds. The inspector stays
860
+ * here and passes the values in the iframe's URL fragment; the page reports the target's defaults
861
+ * back. When a value can't be put in the URL, the playground renders its demos in the page
862
+ * instead, with a note that names the input.
515
863
  */
516
864
  class NgDocPlaygroundComponent {
517
865
  constructor() {
518
866
  this.rootPage = inject(NgDocRootPage);
519
867
  this.formBuilder = inject(FormBuilder);
868
+ this.texts = inject(NG_DOC_TEXTS).playground;
520
869
  /** The name of the playground in the page configuration. */
521
870
  this.id = input.required(/* @ts-ignore */
522
871
  ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
@@ -550,10 +899,169 @@ class NgDocPlaygroundComponent {
550
899
  /** Whether the Recreate setting is fixed on, and hidden, by the `'always'` option. */
551
900
  this.recreateLocked = computed(() => this.configuration().recreate === 'always', /* @ts-ignore */
552
901
  ...(ngDevMode ? [{ debugName: "recreateLocked" }] : /* istanbul ignore next */ []));
902
+ /**
903
+ * The note that says why the playground renders in the page although it is isolated: the input
904
+ * whose value can't be sent to its iframes, or what failed. Once set, it stays for the rest of
905
+ * the visit.
906
+ */
907
+ this.fallback = signal(undefined, /* @ts-ignore */
908
+ ...(ngDevMode ? [{ debugName: "fallback" }] : /* istanbul ignore next */ []));
909
+ /** Whether the demos are shown in iframes of their pages in the demo application. */
910
+ this.isolated = computed(() => !!this.rootPage.demoRoute &&
911
+ !this.fallback() &&
912
+ !this.uncloneableControl() &&
913
+ !this.pagelessSelector() &&
914
+ (this.configuration().isolated ?? this.rootPage.isolatedPlaygrounds ?? false), /* @ts-ignore */
915
+ ...(ngDevMode ? [{ debugName: "isolated" }] : /* istanbul ignore next */ []));
916
+ /** The note that says why an isolated playground renders in the page. */
917
+ this.fallbackNote = computed(() => {
918
+ const note = this.uncloneableControl() ?? this.pagelessSelector() ?? this.fallback();
919
+ return note && (this.configuration().isolated ?? this.rootPage.isolatedPlaygrounds)
920
+ ? note
921
+ : undefined;
922
+ }, /* @ts-ignore */
923
+ ...(ngDevMode ? [{ debugName: "fallbackNote" }] : /* istanbul ignore next */ []));
924
+ /**
925
+ * The theme of the iframes: the `theme` option, until the reader picks another with the theme
926
+ * button beside a demo; `undefined` follows the page.
927
+ */
928
+ this.frameTheme = linkedSignal(() => this.configuration().theme, /* @ts-ignore */
929
+ ...(ngDevMode ? [{ debugName: "frameTheme" }] : /* istanbul ignore next */ []));
930
+ /** The iframe of each target, by selector or pipe name, while the playground is isolated. */
931
+ this.frames = computed(() => {
932
+ const encoded = this.fragment();
933
+ if (!this.isolated() || !encoded.ok)
934
+ return undefined;
935
+ const targets = this.targets();
936
+ const slugs = _playgroundTargetSlugs(targets);
937
+ return new Map(targets.map((target, index) => [
938
+ target,
939
+ {
940
+ url: this.pageUrl(slugs[index]),
941
+ fragment: _withDemoTheme(encoded.fragment, this.frameTheme()),
942
+ title: this.texts.frameTitle({ id: this.id(), target }),
943
+ },
944
+ ]));
945
+ }, /* @ts-ignore */
946
+ ...(ngDevMode ? [{ debugName: "frames" }] : /* istanbul ignore next */ []));
947
+ this.injector = inject(Injector);
948
+ this.platformLocation = inject(PlatformLocation);
949
+ this.appBaseHref = inject(APP_BASE_HREF, { optional: true });
950
+ this.formValue = signal({}, /* @ts-ignore */
951
+ ...(ngDevMode ? [{ debugName: "formValue" }] : /* istanbul ignore next */ []));
553
952
  this.defaultProperties = {};
554
953
  this.defaultContent = {};
954
+ this.viewInitialized = false;
955
+ /** Every target of the playground, in the order of its page's routes: its selectors, or the pipe. */
956
+ this.targets = computed(() => this.pipeName() ? [this.pipeName()] : this.selectors(), /* @ts-ignore */
957
+ ...(ngDevMode ? [{ debugName: "targets" }] : /* istanbul ignore next */ []));
958
+ /** The state of the iframes as a URL fragment. */
959
+ this.fragment = computed(() => {
960
+ const names = objectKeys(this.properties() ?? {}).map(String);
961
+ const value = this.formValue();
962
+ const configuration = this.configuration();
963
+ // Before the iframe reported the defaults, it gets the playground's own values only.
964
+ const properties = value.properties ??
965
+ Object.fromEntries(Object.entries(Object.assign({}, configuration.defaults, configuration.inputs)).filter(([name]) => names.includes(name)));
966
+ return _playgroundStateToFragment({
967
+ inputs: names,
968
+ properties,
969
+ content: Object.entries(value.content ?? {})
970
+ .filter(([, shown]) => shown)
971
+ .map(([slot]) => slot),
972
+ recreate: this.recreateDemo(),
973
+ data: this.options().data,
974
+ });
975
+ }, /* @ts-ignore */
976
+ ...(ngDevMode ? [{ debugName: "fragment" }] : /* istanbul ignore next */ []));
977
+ /**
978
+ * An input whose control is declared not cloneable (`cloneable: false` of `provideTypeControl`):
979
+ * the playground renders in the page from the start.
980
+ */
981
+ this.uncloneableControl = computed(() => {
982
+ const controls = this.configuration().controls ?? {};
983
+ for (const [name, property] of Object.entries(this.properties() ?? {})) {
984
+ const control = controls[name];
985
+ const type = typeof control === 'string' ? control : control?.type ?? property.type;
986
+ const token = getTokenForType(type);
987
+ if (token && this.injector.get(token, null)?.options?.cloneable === false) {
988
+ return this.texts.inPageControl({ input: name });
989
+ }
990
+ }
991
+ return undefined;
992
+ }, /* @ts-ignore */
993
+ ...(ngDevMode ? [{ debugName: "uncloneableControl" }] : /* istanbul ignore next */ []));
994
+ /**
995
+ * A selector of the `selectors` option that is not one of the target's: it has no page, so the
996
+ * playground renders in the page from the start.
997
+ */
998
+ this.pagelessSelector = computed(() => {
999
+ if (this.pipeName())
1000
+ return undefined;
1001
+ const option = this.configuration().selectors;
1002
+ const rendered = option === undefined ? [] : Array.isArray(option) ? option : [option];
1003
+ const selector = rendered.find((item) => !this.selectors().includes(item));
1004
+ return selector === undefined ? undefined : this.texts.inPageSelector({ selector });
1005
+ }, /* @ts-ignore */
1006
+ ...(ngDevMode ? [{ debugName: "pagelessSelector" }] : /* istanbul ignore next */ []));
1007
+ // A value that can't be put in the URL makes the playground render in the page.
1008
+ effect(() => {
1009
+ const encoded = this.fragment();
1010
+ if (!encoded.ok && this.isolated()) {
1011
+ untracked(() => this.fallback.set(encoded.path
1012
+ ? this.texts.inPageValue({ input: encoded.path, reason: encoded.reason })
1013
+ : this.texts.inPageTooLong));
1014
+ }
1015
+ });
1016
+ // A playground that fell back before its form existed creates it once a demo in the page has
1017
+ // reported the defaults. A playground in the page from the start creates it in
1018
+ // `ngAfterViewInit`, as before.
1019
+ effect(() => {
1020
+ if (this.fallback() && this.defaultValues() && !this.formGroup() && this.viewInitialized) {
1021
+ untracked(() => this.createForm());
1022
+ }
1023
+ });
1024
+ }
1025
+ /**
1026
+ * Takes a message of a demo's iframe: the target's defaults, which create the form, or a
1027
+ * failure, which makes the playground render in the page.
1028
+ * @param message - The message.
1029
+ * @internal
1030
+ */
1031
+ onFrameMessage(message) {
1032
+ if (message.type === 'playground-error') {
1033
+ this.fallback.set(this.texts.inPageFrameError({ message: message.message }));
1034
+ return;
1035
+ }
1036
+ if (message.type !== 'playground-ready' || this.formGroup())
1037
+ return;
1038
+ if (message.uncloneable.length) {
1039
+ this.fallback.set(this.texts.inPageDefault({ input: message.uncloneable[0] }));
1040
+ return;
1041
+ }
1042
+ this.defaultValues.set(Object.fromEntries(Object.entries(message.defaults).map(([name, value]) => [
1043
+ name,
1044
+ _decodePlaygroundValue(value),
1045
+ ])));
1046
+ this.createForm();
1047
+ }
1048
+ /**
1049
+ * Makes the playground render in the page when the page in an iframe loaded but did not report
1050
+ * the target, such as a page the build didn't make.
1051
+ * @internal
1052
+ */
1053
+ onFrameSilent() {
1054
+ if (!this.fallback())
1055
+ this.fallback.set(this.texts.inPageFrameSilent);
555
1056
  }
556
1057
  ngAfterViewInit() {
1058
+ this.viewInitialized = true;
1059
+ // An isolated playground creates its form once its iframe reported the defaults.
1060
+ if (this.isolated())
1061
+ return;
1062
+ this.createForm();
1063
+ }
1064
+ createForm() {
557
1065
  this.defaultProperties = this.getPropertiesFormValues();
558
1066
  this.defaultContent = this.getContentFormValues();
559
1067
  // `FormBuilder` reads an array as `[value, validators]`, so each value is wrapped: the default
@@ -570,6 +1078,30 @@ class NgDocPlaygroundComponent {
570
1078
  content: this.defaultContent,
571
1079
  });
572
1080
  this.formGroup.set(formGroup);
1081
+ formGroup.valueChanges
1082
+ .pipe(takeUntilDestroyed(this.injector.get(DestroyRef)))
1083
+ .subscribe(() => this.formValue.set(formGroup.getRawValue()));
1084
+ this.formValue.set(formGroup.getRawValue());
1085
+ }
1086
+ /**
1087
+ * The URL of a target's page in the demo application, under the document's base href.
1088
+ * @param slug
1089
+ */
1090
+ pageUrl(slug) {
1091
+ const baseHref = (this.appBaseHref ??
1092
+ this.platformLocation.getBaseHrefFromDOM() ??
1093
+ '/').replace(/^\/+|\/+$/g, '');
1094
+ const path = [
1095
+ ...baseHref.split('/'),
1096
+ ...(this.rootPage.demoRoute ?? '').split('/'),
1097
+ 'playground',
1098
+ this.id(),
1099
+ slug,
1100
+ ]
1101
+ .filter(Boolean)
1102
+ .map((segment) => encodeURIComponent(segment))
1103
+ .join('/');
1104
+ return `/${path}`;
573
1105
  }
574
1106
  isDefaultState() {
575
1107
  const formGroup = this.formGroup();
@@ -603,98 +1135,201 @@ class NgDocPlaygroundComponent {
603
1135
  });
604
1136
  }
605
1137
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.1", ngImport: i0, type: NgDocPlaygroundComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
606
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.1", type: NgDocPlaygroundComponent, isStandalone: true, selector: "ng-doc-playground", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: true, transformFunction: null }, pipeName: { classPropertyName: "pipeName", publicName: "pipeName", isSignal: true, isRequired: false, transformFunction: null }, selectors: { classPropertyName: "selectors", publicName: "selectors", isSignal: true, isRequired: false, transformFunction: null }, properties: { classPropertyName: "properties", publicName: "properties", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (configuration(); as configuration) {\n <ng-doc-playground-properties\n [form]=\"formGroup()!\"\n [properties]=\"properties()\"\n [ignoreInputs]=\"configuration.hiddenInputs\"\n [dynamicContent]=\"configuration.content\"\n [hideSidePanel]=\"configuration.hideSidePanel ?? false\"\n [inspectorPosition]=\"configuration.inspectorPosition ?? 'right'\"\n [defaultValues]=\"defaultValues()\"\n [showResetButton]=\"!isDefaultState()\"\n [(recreateDemo)]=\"recreateDemo\"\n [recreateLocked]=\"recreateLocked()\"\n (resetForm)=\"resetForm()\">\n @for (selector of configuration.selectors ?? selectors() | asArray; track selector) {\n <ng-doc-playground-demo\n [id]=\"id()\"\n [selector]=\"selector\"\n [properties]=\"properties()\"\n [configuration]=\"configuration\"\n [recreateDemo]=\"recreateDemo()\"\n [form]=\"formGroup()\"\n [expanded]=\"configuration.expanded ?? false\">\n </ng-doc-playground-demo>\n }\n @if (pipeName()) {\n <ng-doc-playground-demo\n [id]=\"id()\"\n [pipeName]=\"pipeName()\"\n [properties]=\"properties()\"\n [configuration]=\"configuration\"\n [recreateDemo]=\"recreateDemo()\"\n [form]=\"formGroup()\">\n </ng-doc-playground-demo>\n }\n </ng-doc-playground-properties>\n}\n", styles: [":host{display:block;margin:var(--ng-doc-playground-margin)}ng-doc-playground-demo:last-child{flex:1 1 auto}ng-doc-playground-demo+ng-doc-playground-demo{border-top:1px solid color-mix(in srgb,var(--ng-doc-border-color) 70%,transparent)}\n"], dependencies: [{ kind: "component", type: NgDocPlaygroundPropertiesComponent, selector: "ng-doc-playground-properties", inputs: ["form", "properties", "ignoreInputs", "dynamicContent", "defaultValues", "hideSidePanel", "inspectorPosition", "recreateDemo", "recreateLocked", "showResetButton"], outputs: ["recreateDemoChange", "resetForm"] }, { kind: "component", type: NgDocPlaygroundDemoComponent, selector: "ng-doc-playground-demo", inputs: ["id", "pipeName", "selector", "configuration", "properties", "recreateDemo", "form", "expanded"] }, { kind: "pipe", type: NgDocAsArrayPipe, name: "asArray" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1138
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.1", type: NgDocPlaygroundComponent, isStandalone: true, selector: "ng-doc-playground", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: true, transformFunction: null }, pipeName: { classPropertyName: "pipeName", publicName: "pipeName", isSignal: true, isRequired: false, transformFunction: null }, selectors: { classPropertyName: "selectors", publicName: "selectors", isSignal: true, isRequired: false, transformFunction: null }, properties: { classPropertyName: "properties", publicName: "properties", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null } }, providers: [{ provide: ɵNG_DOC_PLAYGROUND_CONTAINER, useExisting: NgDocPlaygroundComponent }], ngImport: i0, template: "@if (configuration(); as configuration) {\n @if (fallbackNote(); as note) {\n <p class=\"ng-doc-playground-note\" role=\"note\">{{ note }}</p>\n }\n <ng-doc-playground-properties\n [form]=\"formGroup()!\"\n [properties]=\"properties()\"\n [ignoreInputs]=\"configuration.hiddenInputs\"\n [dynamicContent]=\"configuration.content\"\n [hideSidePanel]=\"configuration.hideSidePanel ?? false\"\n [inspectorPosition]=\"configuration.inspectorPosition ?? 'right'\"\n [defaultValues]=\"defaultValues()\"\n [waitingForDefaults]=\"isolated()\"\n [showResetButton]=\"!isDefaultState()\"\n [(recreateDemo)]=\"recreateDemo\"\n [recreateLocked]=\"recreateLocked()\"\n (resetForm)=\"resetForm()\">\n @for (selector of configuration.selectors ?? selectors() | asArray; track selector) {\n <ng-doc-playground-demo\n [id]=\"id()\"\n [selector]=\"selector\"\n [properties]=\"properties()\"\n [configuration]=\"configuration\"\n [recreateDemo]=\"recreateDemo()\"\n [form]=\"formGroup()\"\n [expanded]=\"configuration.expanded ?? false\"\n [frame]=\"frames()?.get(selector)\"\n [frameDefaults]=\"defaultValues()\"\n [(frameTheme)]=\"frameTheme\"\n [configuredTheme]=\"configuration.theme\"\n (frameMessage)=\"onFrameMessage($event)\"\n (frameSilent)=\"onFrameSilent()\">\n </ng-doc-playground-demo>\n }\n @if (pipeName()) {\n <ng-doc-playground-demo\n [id]=\"id()\"\n [pipeName]=\"pipeName()\"\n [properties]=\"properties()\"\n [configuration]=\"configuration\"\n [recreateDemo]=\"recreateDemo()\"\n [form]=\"formGroup()\"\n [frame]=\"frames()?.get(pipeName())\"\n [frameDefaults]=\"defaultValues()\"\n [(frameTheme)]=\"frameTheme\"\n [configuredTheme]=\"configuration.theme\"\n (frameMessage)=\"onFrameMessage($event)\"\n (frameSilent)=\"onFrameSilent()\">\n </ng-doc-playground-demo>\n }\n </ng-doc-playground-properties>\n}\n", styles: [":host{display:block;margin:var(--ng-doc-playground-margin)}ng-doc-playground-demo:last-child{flex:1 1 auto}ng-doc-playground-demo+ng-doc-playground-demo{border-top:1px solid color-mix(in srgb,var(--ng-doc-border-color) 70%,transparent)}.ng-doc-playground-note{margin:0;padding:var(--ng-doc-space-2) var(--ng-doc-space-3);color:var(--ng-doc-text-muted)}\n"], dependencies: [{ kind: "component", type: NgDocPlaygroundPropertiesComponent, selector: "ng-doc-playground-properties", inputs: ["form", "properties", "ignoreInputs", "dynamicContent", "defaultValues", "waitingForDefaults", "hideSidePanel", "inspectorPosition", "recreateDemo", "recreateLocked", "showResetButton"], outputs: ["recreateDemoChange", "resetForm"] }, { kind: "component", type: NgDocPlaygroundDemoComponent, selector: "ng-doc-playground-demo", inputs: ["id", "pipeName", "selector", "configuration", "properties", "recreateDemo", "form", "expanded", "frame", "frameDefaults", "frameTheme", "configuredTheme"], outputs: ["frameMessage", "frameSilent", "frameThemeChange"] }, { kind: "pipe", type: NgDocAsArrayPipe, name: "asArray" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
607
1139
  }
608
1140
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.1", ngImport: i0, type: NgDocPlaygroundComponent, decorators: [{
609
1141
  type: Component,
610
- args: [{ selector: 'ng-doc-playground', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgDocPlaygroundPropertiesComponent, NgDocPlaygroundDemoComponent, NgDocAsArrayPipe], template: "@if (configuration(); as configuration) {\n <ng-doc-playground-properties\n [form]=\"formGroup()!\"\n [properties]=\"properties()\"\n [ignoreInputs]=\"configuration.hiddenInputs\"\n [dynamicContent]=\"configuration.content\"\n [hideSidePanel]=\"configuration.hideSidePanel ?? false\"\n [inspectorPosition]=\"configuration.inspectorPosition ?? 'right'\"\n [defaultValues]=\"defaultValues()\"\n [showResetButton]=\"!isDefaultState()\"\n [(recreateDemo)]=\"recreateDemo\"\n [recreateLocked]=\"recreateLocked()\"\n (resetForm)=\"resetForm()\">\n @for (selector of configuration.selectors ?? selectors() | asArray; track selector) {\n <ng-doc-playground-demo\n [id]=\"id()\"\n [selector]=\"selector\"\n [properties]=\"properties()\"\n [configuration]=\"configuration\"\n [recreateDemo]=\"recreateDemo()\"\n [form]=\"formGroup()\"\n [expanded]=\"configuration.expanded ?? false\">\n </ng-doc-playground-demo>\n }\n @if (pipeName()) {\n <ng-doc-playground-demo\n [id]=\"id()\"\n [pipeName]=\"pipeName()\"\n [properties]=\"properties()\"\n [configuration]=\"configuration\"\n [recreateDemo]=\"recreateDemo()\"\n [form]=\"formGroup()\">\n </ng-doc-playground-demo>\n }\n </ng-doc-playground-properties>\n}\n", styles: [":host{display:block;margin:var(--ng-doc-playground-margin)}ng-doc-playground-demo:last-child{flex:1 1 auto}ng-doc-playground-demo+ng-doc-playground-demo{border-top:1px solid color-mix(in srgb,var(--ng-doc-border-color) 70%,transparent)}\n"] }]
611
- }], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: true }] }], pipeName: [{ type: i0.Input, args: [{ isSignal: true, alias: "pipeName", required: false }] }], selectors: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectors", required: false }] }], properties: [{ type: i0.Input, args: [{ isSignal: true, alias: "properties", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }] } });
1142
+ args: [{ selector: 'ng-doc-playground', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgDocPlaygroundPropertiesComponent, NgDocPlaygroundDemoComponent, NgDocAsArrayPipe], providers: [{ provide: ɵNG_DOC_PLAYGROUND_CONTAINER, useExisting: NgDocPlaygroundComponent }], template: "@if (configuration(); as configuration) {\n @if (fallbackNote(); as note) {\n <p class=\"ng-doc-playground-note\" role=\"note\">{{ note }}</p>\n }\n <ng-doc-playground-properties\n [form]=\"formGroup()!\"\n [properties]=\"properties()\"\n [ignoreInputs]=\"configuration.hiddenInputs\"\n [dynamicContent]=\"configuration.content\"\n [hideSidePanel]=\"configuration.hideSidePanel ?? false\"\n [inspectorPosition]=\"configuration.inspectorPosition ?? 'right'\"\n [defaultValues]=\"defaultValues()\"\n [waitingForDefaults]=\"isolated()\"\n [showResetButton]=\"!isDefaultState()\"\n [(recreateDemo)]=\"recreateDemo\"\n [recreateLocked]=\"recreateLocked()\"\n (resetForm)=\"resetForm()\">\n @for (selector of configuration.selectors ?? selectors() | asArray; track selector) {\n <ng-doc-playground-demo\n [id]=\"id()\"\n [selector]=\"selector\"\n [properties]=\"properties()\"\n [configuration]=\"configuration\"\n [recreateDemo]=\"recreateDemo()\"\n [form]=\"formGroup()\"\n [expanded]=\"configuration.expanded ?? false\"\n [frame]=\"frames()?.get(selector)\"\n [frameDefaults]=\"defaultValues()\"\n [(frameTheme)]=\"frameTheme\"\n [configuredTheme]=\"configuration.theme\"\n (frameMessage)=\"onFrameMessage($event)\"\n (frameSilent)=\"onFrameSilent()\">\n </ng-doc-playground-demo>\n }\n @if (pipeName()) {\n <ng-doc-playground-demo\n [id]=\"id()\"\n [pipeName]=\"pipeName()\"\n [properties]=\"properties()\"\n [configuration]=\"configuration\"\n [recreateDemo]=\"recreateDemo()\"\n [form]=\"formGroup()\"\n [frame]=\"frames()?.get(pipeName())\"\n [frameDefaults]=\"defaultValues()\"\n [(frameTheme)]=\"frameTheme\"\n [configuredTheme]=\"configuration.theme\"\n (frameMessage)=\"onFrameMessage($event)\"\n (frameSilent)=\"onFrameSilent()\">\n </ng-doc-playground-demo>\n }\n </ng-doc-playground-properties>\n}\n", styles: [":host{display:block;margin:var(--ng-doc-playground-margin)}ng-doc-playground-demo:last-child{flex:1 1 auto}ng-doc-playground-demo+ng-doc-playground-demo{border-top:1px solid color-mix(in srgb,var(--ng-doc-border-color) 70%,transparent)}.ng-doc-playground-note{margin:0;padding:var(--ng-doc-space-2) var(--ng-doc-space-3);color:var(--ng-doc-text-muted)}\n"] }]
1143
+ }], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: true }] }], pipeName: [{ type: i0.Input, args: [{ isSignal: true, alias: "pipeName", required: false }] }], selectors: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectors", required: false }] }], properties: [{ type: i0.Input, args: [{ isSignal: true, alias: "properties", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }] } });
612
1144
 
1145
+ const EMPTY_STATE = {
1146
+ inputs: [],
1147
+ properties: {},
1148
+ content: [],
1149
+ recreate: false,
1150
+ };
613
1151
  /**
614
- * Base class of the playground classes the builder generates, one per playground and selector.
615
- * A generated class renders the playground template with the target's inputs bound to
616
- * `properties()`, and queries the target with `viewChild()`.
1152
+ * Shows one target of a playground on a page of the demo application, for an isolated playground:
1153
+ * the generated playground class of the route's playground and target, without the inspector,
1154
+ * which stays on the documentation page. The state comes from the URL fragment, and a new
1155
+ * fragment applies a new state without a reload (the documentation page replaces it with
1156
+ * `location.replace`). The page tells the documentation page, with `postMessage`, the target's
1157
+ * default input values once it exists, each state it applied, and what failed.
1158
+ *
1159
+ * Like the demo host, it skips hydration: the server renders the target with its defaults, and
1160
+ * the browser renders it again with the fragment's state, which only the browser has.
617
1161
  */
618
- class NgDocBasePlayground {
619
- static { this.selector = 'unknown'; }
1162
+ class NgDocPlaygroundHostComponent {
620
1163
  constructor() {
621
- /** Values of the target's inputs, by property name. */
622
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
623
- this.properties = input({}, /* @ts-ignore */
1164
+ /** The target's inputs, by name, from the state: a pipe's parameters are positional. */
1165
+ this.properties = signal(undefined, /* @ts-ignore */
624
1166
  ...(ngDevMode ? [{ debugName: "properties" }] : /* istanbul ignore next */ []));
625
- /** The `data` given to the playground action; it extends the configuration's `data`. */
626
- this.actionData = input({}, /* @ts-ignore */
627
- ...(ngDevMode ? [{ debugName: "actionData" }] : /* istanbul ignore next */ []));
628
- /** Whether each content slot is shown, by slot name. */
629
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
630
- this.content = input({}, /* @ts-ignore */
631
- ...(ngDevMode ? [{ debugName: "content" }] : /* istanbul ignore next */ []));
632
- /** The default values of the target's inputs, read before the template binds them. */
633
- this.defaultValues = {};
634
- this.reattached = new Subject();
635
- this.playgroundContainer = inject(NgDocPlaygroundComponent);
636
- this.changeDetectorRef = inject(ChangeDetectorRef);
637
- // The view is not checked until the default values of the target are read, so the
638
- // template's bindings cannot overwrite them first.
639
- this.changeDetectorRef.detach();
1167
+ /** The default values of the target's inputs, reported by the generated class. */
1168
+ this.defaultValues = signal(undefined, /* @ts-ignore */
1169
+ ...(ngDevMode ? [{ debugName: "defaultValues" }] : /* istanbul ignore next */ []));
1170
+ this.texts = inject(NG_DOC_TEXTS).playground;
1171
+ this.injector = inject(Injector);
1172
+ this.outlet = viewChild.required('outlet', { ...(ngDevMode ? { debugName: "outlet" } : /* istanbul ignore next */ {}), read: ViewContainerRef });
1173
+ this.window = isPlatformBrowser(inject(PLATFORM_ID))
1174
+ ? inject(DOCUMENT).defaultView
1175
+ : null;
1176
+ const data = inject(ActivatedRoute).snapshot.data['ngDocPlayground'];
1177
+ const target = data?.selector ?? data?.pipeName;
1178
+ const token = data ? getPlaygroundDemoToken(data.id) : undefined;
1179
+ const classes = token ? this.injector.get(token, []) : [];
1180
+ this.playground = classes.find((type) => type.selector === target);
1181
+ this.missing = !this.playground;
1182
+ // The router updates the URL after it activates the route, so the fragment is read after
1183
+ // the first render.
1184
+ afterNextRender(() => this.start(this.readState() ?? EMPTY_STATE));
1185
+ const window = this.window;
1186
+ if (window) {
1187
+ const onHashChange = () => {
1188
+ const state = this.readState();
1189
+ // A malformed fragment is reported and leaves the target as it is.
1190
+ if (state)
1191
+ this.apply(state);
1192
+ };
1193
+ window.addEventListener('hashchange', onHashChange);
1194
+ inject(DestroyRef).onDestroy(() => window.removeEventListener('hashchange', onHashChange));
1195
+ }
640
1196
  }
641
- /** Emits once, after the view is attached again and checks the template's bindings. */
642
- get onReattached() {
643
- return this.reattached.pipe(take(1));
1197
+ /**
1198
+ * Creates the target with the first state, and reports its defaults once it has rendered.
1199
+ * @param state
1200
+ */
1201
+ start(state) {
1202
+ this.properties.set(Object.fromEntries(state.inputs.map((name) => [name, true])));
1203
+ if (!this.playground) {
1204
+ this.post({
1205
+ type: 'playground-error',
1206
+ phase: 'create',
1207
+ message: this.texts.missing,
1208
+ });
1209
+ return;
1210
+ }
1211
+ if (!this.create(state))
1212
+ return;
1213
+ // The generated class reads the target's defaults when it is first checked, which the next
1214
+ // render has done.
1215
+ afterNextRender(() => {
1216
+ const defaults = {};
1217
+ const uncloneable = [];
1218
+ for (const [name, value] of Object.entries(this.defaultValues() ?? {})) {
1219
+ const encoded = _encodePlaygroundValue(value);
1220
+ if (encoded.ok)
1221
+ defaults[name] = encoded.value;
1222
+ else
1223
+ uncloneable.push(name);
1224
+ }
1225
+ this.post({ type: 'playground-ready', defaults, uncloneable });
1226
+ this.applied();
1227
+ }, { injector: this.injector });
644
1228
  }
645
- /** The playground `data`: the configuration's data extended by the action's data. */
646
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
647
- get data() {
648
- return Object.assign({}, this.configData, this.actionData());
1229
+ /**
1230
+ * Creates the target and sets the state; reports a failure instead of throwing.
1231
+ * @param state
1232
+ */
1233
+ create(state) {
1234
+ if (!this.playground)
1235
+ return false;
1236
+ try {
1237
+ this.reference?.destroy();
1238
+ this.reference = this.outlet().createComponent(this.playground);
1239
+ this.setState(state);
1240
+ return true;
1241
+ }
1242
+ catch (error) {
1243
+ this.post({ type: 'playground-error', phase: 'create', message: describe(error) });
1244
+ return false;
1245
+ }
649
1246
  }
650
- ngOnInit() {
651
- // The target is created with the view, but the view has not been checked yet, so its inputs
652
- // still hold their defaults. Signal inputs are read by calling them.
653
- const playground = this.playground();
654
- if (playground) {
655
- this.defaultValues = Object.keys(playground).reduce((values, key) => {
656
- try {
657
- const value = playground[key];
658
- values[key] = typeof value === 'function' ? value.call(playground) : value;
659
- }
660
- catch {
661
- // Some properties are getters or functions that throw without their context.
662
- }
663
- return values;
664
- }, {});
1247
+ /**
1248
+ * Applies a state to the existing target, or creates it again when the state asks to.
1249
+ * @param state
1250
+ */
1251
+ apply(state) {
1252
+ if (!this.reference)
1253
+ return;
1254
+ if (state.recreate) {
1255
+ if (this.create(state))
1256
+ this.applied();
1257
+ return;
665
1258
  }
666
- else if (this.target) {
667
- // A pipe: the defaults of its `transform` parameters after the value itself.
668
- const defaults = extractFunctionDefaults(this.target.prototype.transform);
669
- this.defaultValues = Object.keys(this.playgroundContainer.properties() ?? {}).reduce((values, key, i) => {
670
- values[key] = defaults[i + 1];
671
- return values;
672
- }, {});
1259
+ try {
1260
+ this.setState(state);
1261
+ this.applied();
673
1262
  }
674
- else {
675
- throw new Error('Playground is not defined or initialized');
1263
+ catch (error) {
1264
+ this.post({ type: 'playground-error', phase: 'apply', message: describe(error) });
676
1265
  }
677
- if (!this.playgroundContainer.defaultValues()) {
678
- this.playgroundContainer.defaultValues.set(this.defaultValues);
1266
+ }
1267
+ setState(state) {
1268
+ this.reference?.setInput('properties', state.properties);
1269
+ this.reference?.setInput('actionData', state.data ?? {});
1270
+ this.reference?.setInput('content', Object.fromEntries(state.content.map((slot) => [slot, true])));
1271
+ this.reference?.changeDetectorRef.markForCheck();
1272
+ }
1273
+ /** The state of the URL fragment; a malformed one is reported and reads as `undefined`. */
1274
+ readState() {
1275
+ const fragment = this.window?.location.hash ?? '';
1276
+ if (!fragment || fragment === '#')
1277
+ return EMPTY_STATE;
1278
+ try {
1279
+ return _playgroundStateFromFragment(fragment);
679
1280
  }
680
- // The demo sets the playground's inputs right after it creates this view; the view is
681
- // attached again once they are set, so its first check renders the playground's values.
682
- Promise.resolve().then(() => {
683
- this.changeDetectorRef.reattach();
684
- this.changeDetectorRef.markForCheck();
685
- this.reattached.next();
1281
+ catch (error) {
1282
+ this.post({ type: 'playground-error', phase: 'apply', message: describe(error) });
1283
+ return undefined;
1284
+ }
1285
+ }
1286
+ applied() {
1287
+ this.post({
1288
+ type: 'playground-applied',
1289
+ fragment: (this.window?.location.hash ?? '').replace(/^#/, ''),
686
1290
  });
687
1291
  }
688
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.1", ngImport: i0, type: NgDocBasePlayground, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
689
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.2.1", type: NgDocBasePlayground, isStandalone: true, inputs: { properties: { classPropertyName: "properties", publicName: "properties", isSignal: true, isRequired: false, transformFunction: null }, actionData: { classPropertyName: "actionData", publicName: "actionData", isSignal: true, isRequired: false, transformFunction: null }, content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
1292
+ post(message) {
1293
+ const window = this.window;
1294
+ // Only a page of the same origin may receive it: the demo pages are part of the site.
1295
+ if (window && window.parent !== window) {
1296
+ window.parent.postMessage({ source: _NG_DOC_DEMO_MESSAGE_SOURCE, version: 1, ...message }, window.location.origin);
1297
+ }
1298
+ }
1299
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.1", ngImport: i0, type: NgDocPlaygroundHostComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1300
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.1", type: NgDocPlaygroundHostComponent, isStandalone: true, selector: "ng-doc-playground-host", host: { attributes: { "ngSkipHydration": "true" } }, providers: [{ provide: ɵNG_DOC_PLAYGROUND_CONTAINER, useExisting: NgDocPlaygroundHostComponent }], viewQueries: [{ propertyName: "outlet", first: true, predicate: ["outlet"], descendants: true, read: ViewContainerRef, isSignal: true }], ngImport: i0, template: `
1301
+ <ng-container #outlet />
1302
+ @if (missing) {
1303
+ <p role="alert">{{ texts.missing }}</p>
1304
+ }
1305
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1306
+ }
1307
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.1", ngImport: i0, type: NgDocPlaygroundHostComponent, decorators: [{
1308
+ type: Component,
1309
+ args: [{
1310
+ selector: 'ng-doc-playground-host',
1311
+ template: `
1312
+ <ng-container #outlet />
1313
+ @if (missing) {
1314
+ <p role="alert">{{ texts.missing }}</p>
1315
+ }
1316
+ `,
1317
+ changeDetection: ChangeDetectionStrategy.OnPush,
1318
+ host: { ngSkipHydration: 'true' },
1319
+ providers: [{ provide: ɵNG_DOC_PLAYGROUND_CONTAINER, useExisting: NgDocPlaygroundHostComponent }],
1320
+ }]
1321
+ }], ctorParameters: () => [], propDecorators: { outlet: [{ type: i0.ViewChild, args: ['outlet', { ...{ read: ViewContainerRef }, isSignal: true }] }] } });
1322
+ /**
1323
+ *
1324
+ * @param error
1325
+ */
1326
+ function describe(error) {
1327
+ return error instanceof Error ? error.message : String(error);
690
1328
  }
691
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.1", ngImport: i0, type: NgDocBasePlayground, decorators: [{
692
- type: Directive
693
- }], ctorParameters: () => [], propDecorators: { properties: [{ type: i0.Input, args: [{ isSignal: true, alias: "properties", required: false }] }], actionData: [{ type: i0.Input, args: [{ isSignal: true, alias: "actionData", required: false }] }], content: [{ type: i0.Input, args: [{ isSignal: true, alias: "content", required: false }] }] } });
694
1329
 
695
1330
  /**
696
1331
  * Generated bundle index. Do not edit.
697
1332
  */
698
1333
 
699
- export { NgDocBasePlayground, NgDocPlaygroundComponent, NgDocPlaygroundDemoComponent, NgDocPlaygroundPropertiesComponent, NgDocPlaygroundPropertyComponent };
1334
+ export { NgDocBasePlayground, NgDocPlaygroundComponent, NgDocPlaygroundDemoComponent, NgDocPlaygroundHostComponent, NgDocPlaygroundPropertiesComponent, NgDocPlaygroundPropertyComponent, ɵNG_DOC_PLAYGROUND_CONTAINER };
700
1335
  //# sourceMappingURL=ng-doc-app-components-playground.mjs.map