@ng-doc/app 22.0.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 (173) 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 +279 -0
  20. package/fesm2022/ng-doc-app-components-demo-frame.mjs.map +1 -0
  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 +97 -14
  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 +838 -141
  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-tabs.mjs +21 -3
  52. package/fesm2022/ng-doc-app-components-tabs.mjs.map +1 -1
  53. package/fesm2022/ng-doc-app-components-theme-toggle.mjs +7 -5
  54. package/fesm2022/ng-doc-app-components-theme-toggle.mjs.map +1 -1
  55. package/fesm2022/ng-doc-app-components-toc.mjs +4 -2
  56. package/fesm2022/ng-doc-app-components-toc.mjs.map +1 -1
  57. package/fesm2022/ng-doc-app-components.mjs +1 -0
  58. package/fesm2022/ng-doc-app-components.mjs.map +1 -1
  59. package/fesm2022/ng-doc-app-constants.mjs +11 -1
  60. package/fesm2022/ng-doc-app-constants.mjs.map +1 -1
  61. package/fesm2022/ng-doc-app-demo-app.mjs +779 -0
  62. package/fesm2022/ng-doc-app-demo-app.mjs.map +1 -0
  63. package/fesm2022/ng-doc-app-helpers.mjs +147 -7
  64. package/fesm2022/ng-doc-app-helpers.mjs.map +1 -1
  65. package/fesm2022/ng-doc-app-processors-processors.mjs +30 -9
  66. package/fesm2022/ng-doc-app-processors-processors.mjs.map +1 -1
  67. package/fesm2022/ng-doc-app-providers-ng-doc-app.mjs +29 -18
  68. package/fesm2022/ng-doc-app-providers-ng-doc-app.mjs.map +1 -1
  69. package/fesm2022/ng-doc-app-providers-texts.mjs +53 -0
  70. package/fesm2022/ng-doc-app-providers-texts.mjs.map +1 -0
  71. package/fesm2022/ng-doc-app-providers-title.mjs +129 -0
  72. package/fesm2022/ng-doc-app-providers-title.mjs.map +1 -0
  73. package/fesm2022/ng-doc-app-providers.mjs +2 -0
  74. package/fesm2022/ng-doc-app-providers.mjs.map +1 -1
  75. package/fesm2022/ng-doc-app-services-content-scroll-intent.mjs +27 -2
  76. package/fesm2022/ng-doc-app-services-content-scroll-intent.mjs.map +1 -1
  77. package/fesm2022/ng-doc-app-services-highlighter.mjs +23 -16
  78. package/fesm2022/ng-doc-app-services-highlighter.mjs.map +1 -1
  79. package/fesm2022/ng-doc-app-services-route-preloader.mjs +34 -10
  80. package/fesm2022/ng-doc-app-services-route-preloader.mjs.map +1 -1
  81. package/fesm2022/ng-doc-app-services-shortcuts.mjs +38 -4
  82. package/fesm2022/ng-doc-app-services-shortcuts.mjs.map +1 -1
  83. package/fesm2022/ng-doc-app-texts-ar.mjs +238 -0
  84. package/fesm2022/ng-doc-app-texts-ar.mjs.map +1 -0
  85. package/fesm2022/ng-doc-app-texts-de.mjs +205 -0
  86. package/fesm2022/ng-doc-app-texts-de.mjs.map +1 -0
  87. package/fesm2022/ng-doc-app-texts-es.mjs +205 -0
  88. package/fesm2022/ng-doc-app-texts-es.mjs.map +1 -0
  89. package/fesm2022/ng-doc-app-texts-fr.mjs +205 -0
  90. package/fesm2022/ng-doc-app-texts-fr.mjs.map +1 -0
  91. package/fesm2022/ng-doc-app-texts-hi.mjs +219 -0
  92. package/fesm2022/ng-doc-app-texts-hi.mjs.map +1 -0
  93. package/fesm2022/ng-doc-app-texts-ja.mjs +205 -0
  94. package/fesm2022/ng-doc-app-texts-ja.mjs.map +1 -0
  95. package/fesm2022/ng-doc-app-texts-ko.mjs +205 -0
  96. package/fesm2022/ng-doc-app-texts-ko.mjs.map +1 -0
  97. package/fesm2022/ng-doc-app-texts-pt-br.mjs +205 -0
  98. package/fesm2022/ng-doc-app-texts-pt-br.mjs.map +1 -0
  99. package/fesm2022/ng-doc-app-texts-ru.mjs +231 -0
  100. package/fesm2022/ng-doc-app-texts-ru.mjs.map +1 -0
  101. package/fesm2022/ng-doc-app-texts-uk.mjs +231 -0
  102. package/fesm2022/ng-doc-app-texts-uk.mjs.map +1 -0
  103. package/fesm2022/ng-doc-app-texts-zh.mjs +205 -0
  104. package/fesm2022/ng-doc-app-texts-zh.mjs.map +1 -0
  105. package/fesm2022/ng-doc-app-tokens.mjs +214 -1
  106. package/fesm2022/ng-doc-app-tokens.mjs.map +1 -1
  107. package/fesm2022/ng-doc-app-type-controls-json-control.mjs +90 -0
  108. package/fesm2022/ng-doc-app-type-controls-json-control.mjs.map +1 -0
  109. package/fesm2022/ng-doc-app-type-controls-list-control.mjs +792 -0
  110. package/fesm2022/ng-doc-app-type-controls-list-control.mjs.map +1 -0
  111. package/fesm2022/ng-doc-app-type-controls-type-alias-control.mjs +45 -8
  112. package/fesm2022/ng-doc-app-type-controls-type-alias-control.mjs.map +1 -1
  113. package/fesm2022/ng-doc-app-type-controls.mjs +2 -0
  114. package/fesm2022/ng-doc-app-type-controls.mjs.map +1 -1
  115. package/package.json +73 -4
  116. package/styles/global.css +4 -0
  117. package/styles/main.css +4 -0
  118. package/styles/themes/dark.css +10 -0
  119. package/types/ng-doc-app-classes-default-search-engine.d.ts +11 -0
  120. package/types/ng-doc-app-classes-root-page.d.ts +13 -0
  121. package/types/ng-doc-app-components-api-list.d.ts +3 -1
  122. package/types/ng-doc-app-components-breadcrumb.d.ts +2 -1
  123. package/types/ng-doc-app-components-code.d.ts +2 -0
  124. package/types/ng-doc-app-components-command-palette.d.ts +3 -1
  125. package/types/ng-doc-app-components-copy-button.d.ts +3 -1
  126. package/types/ng-doc-app-components-demo-displayer.d.ts +13 -1
  127. package/types/ng-doc-app-components-demo-frame.d.ts +107 -0
  128. package/types/ng-doc-app-components-demo.d.ts +47 -10
  129. package/types/ng-doc-app-components-fullscreen-button.d.ts +2 -0
  130. package/types/ng-doc-app-components-fullscreen-toggle.d.ts +2 -0
  131. package/types/ng-doc-app-components-heading-anchor.d.ts +2 -0
  132. package/types/ng-doc-app-components-image-viewer.d.ts +2 -0
  133. package/types/ng-doc-app-components-navbar.d.ts +2 -0
  134. package/types/ng-doc-app-components-page-navigation.d.ts +2 -1
  135. package/types/ng-doc-app-components-page.d.ts +10 -1
  136. package/types/ng-doc-app-components-playground.d.ts +260 -42
  137. package/types/ng-doc-app-components-root.d.ts +18 -0
  138. package/types/ng-doc-app-components-search-result.d.ts +2 -0
  139. package/types/ng-doc-app-components-search.d.ts +2 -0
  140. package/types/ng-doc-app-components-sidebar.d.ts +2 -1
  141. package/types/ng-doc-app-components-tabs.d.ts +13 -3
  142. package/types/ng-doc-app-components-theme-toggle.d.ts +4 -1
  143. package/types/ng-doc-app-components-toc.d.ts +2 -1
  144. package/types/ng-doc-app-components.d.ts +1 -0
  145. package/types/ng-doc-app-constants.d.ts +11 -1
  146. package/types/ng-doc-app-demo-app.d.ts +297 -0
  147. package/types/ng-doc-app-helpers.d.ts +75 -4
  148. package/types/ng-doc-app-interfaces.d.ts +635 -6
  149. package/types/ng-doc-app-processors-processors.d.ts +5 -0
  150. package/types/ng-doc-app-providers-ng-doc-app.d.ts +7 -1
  151. package/types/ng-doc-app-providers-texts.d.ts +28 -0
  152. package/types/ng-doc-app-providers-title.d.ts +59 -0
  153. package/types/ng-doc-app-providers.d.ts +2 -0
  154. package/types/ng-doc-app-services-content-scroll-intent.d.ts +11 -1
  155. package/types/ng-doc-app-services-highlighter.d.ts +15 -6
  156. package/types/ng-doc-app-services-route-preloader.d.ts +13 -1
  157. package/types/ng-doc-app-services-shortcuts.d.ts +16 -0
  158. package/types/ng-doc-app-texts-ar.d.ts +13 -0
  159. package/types/ng-doc-app-texts-de.d.ts +12 -0
  160. package/types/ng-doc-app-texts-es.d.ts +12 -0
  161. package/types/ng-doc-app-texts-fr.d.ts +12 -0
  162. package/types/ng-doc-app-texts-hi.d.ts +12 -0
  163. package/types/ng-doc-app-texts-ja.d.ts +12 -0
  164. package/types/ng-doc-app-texts-ko.d.ts +12 -0
  165. package/types/ng-doc-app-texts-pt-br.d.ts +12 -0
  166. package/types/ng-doc-app-texts-ru.d.ts +12 -0
  167. package/types/ng-doc-app-texts-uk.d.ts +12 -0
  168. package/types/ng-doc-app-texts-zh.d.ts +12 -0
  169. package/types/ng-doc-app-tokens.d.ts +21 -2
  170. package/types/ng-doc-app-type-controls-json-control.d.ts +38 -0
  171. package/types/ng-doc-app-type-controls-list-control.d.ts +313 -0
  172. package/types/ng-doc-app-type-controls-type-alias-control.d.ts +28 -5
  173. package/types/ng-doc-app-type-controls.d.ts +2 -0
@@ -1,30 +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, 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';
13
- import { formatHtml, isPlaygroundProperty } from '@ng-doc/app/helpers';
18
+ import { NgDocDemoFrameComponent, NgDocDemoThemeButtonComponent } from '@ng-doc/app/components/demo-frame';
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 { KeyValuePipe } from '@angular/common';
19
- import { getTokenForType } from '@ng-doc/app/providers/type-control';
20
- import { extractValueOrThrow } from '@ng-doc/core/helpers/extract-value';
21
24
  import { isPresent } from '@ng-doc/core/helpers/is-present';
22
25
  import { NgDocKindIconComponent } from '@ng-doc/app/components/kind-icon';
23
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 }] }] } });
24
116
 
25
117
  /**
26
118
  * One demo of a playground: it creates the generated playground class of its selector (or pipe),
27
- * 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.
28
122
  */
29
123
  class NgDocPlaygroundDemoComponent {
30
124
  constructor() {
@@ -52,6 +146,42 @@ class NgDocPlaygroundDemoComponent {
52
146
  /** Whether the code of the demo is shown. */
53
147
  this.expanded = input(false, /* @ts-ignore */
54
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 */ []));
55
185
  /** Where the playground class is created. */
56
186
  this.demoOutlet = viewChild('demoOutlet', { ...(ngDevMode ? { debugName: "demoOutlet" } : /* istanbul ignore next */ {}), read: ViewContainerRef });
57
187
  this.code = signal('', /* @ts-ignore */
@@ -61,7 +191,11 @@ class NgDocPlaygroundDemoComponent {
61
191
  this.pendingTasks = inject(PendingTasks);
62
192
  this.unsubscribe$ = new Subject();
63
193
  }
64
- 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
+ }
65
199
  if (form || id) {
66
200
  this.unsubscribe$.next();
67
201
  const demoInjector = getPlaygroundDemoToken(this.id());
@@ -69,14 +203,25 @@ class NgDocPlaygroundDemoComponent {
69
203
  const demos = this.injector.get(demoInjector, []);
70
204
  this.playgroundDemo = demos.find((demo) => demo.selector === this.selector() || demo.selector === this.pipeName());
71
205
  }
72
- await this.updateDemo();
73
206
  const form = this.form();
74
- 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
75
216
  .pipe(startWith(form.value), takeUntil(this.unsubscribe$), takeUntilDestroyed(this.destroyRef))
76
217
  .subscribe((data) => this.updateDemo(data));
77
218
  }
78
219
  }
79
220
  async updateDemo(data) {
221
+ if (this.frame()) {
222
+ await this.updateCodeView();
223
+ return;
224
+ }
80
225
  if (this.recreateDemo() || !this.demoRef) {
81
226
  this.createDemo();
82
227
  }
@@ -125,7 +270,7 @@ class NgDocPlaygroundDemoComponent {
125
270
  return objectKeys(formData).reduce((result, key) => {
126
271
  const inputName = this.properties()?.[key]?.inputName ?? key;
127
272
  const value = formData[key];
128
- const property = this.demoRef?.instance?.defaultValues[key];
273
+ const property = this.defaultValue(key);
129
274
  if (property !== value) {
130
275
  result[inputName] = stringify(value).replace(/"/g, `'`);
131
276
  }
@@ -138,7 +283,7 @@ class NgDocPlaygroundDemoComponent {
138
283
  return objectKeys(formData)
139
284
  .map((key, i) => {
140
285
  const value = formData[key];
141
- const defaultValue = this.demoRef?.instance?.defaultValues[key];
286
+ const defaultValue = this.defaultValue(key);
142
287
  if (defaultValue !== value) {
143
288
  changedInputIndex = i;
144
289
  }
@@ -154,16 +299,176 @@ class NgDocPlaygroundDemoComponent {
154
299
  this.unsubscribe$.next();
155
300
  this.unsubscribe$.complete();
156
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
+ }
157
309
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.1", ngImport: i0, type: NgDocPlaygroundDemoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
158
- 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 }); }
159
311
  }
160
312
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.1", ngImport: i0, type: NgDocPlaygroundDemoComponent, decorators: [{
161
313
  type: Component,
162
- 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"] }]
163
- }], 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', { ...{
164
316
  read: ViewContainerRef,
165
317
  }, isSignal: true }] }] } });
166
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
+
471
+ let nextId = 0;
167
472
  /** Value types that have a chip colour. */
168
473
  const CHIP_TYPES = new Set([
169
474
  'string',
@@ -209,25 +514,35 @@ class NgDocPlaygroundPropertyComponent {
209
514
  /** The value type shown as a chip under the name, or an empty string. */
210
515
  this.typeChip = computed(() => valueType(this.property()), /* @ts-ignore */
211
516
  ...(ngDevMode ? [{ debugName: "typeChip" }] : /* istanbul ignore next */ []));
212
- this.propertyOutlet = viewChild.required('propertyOutlet', { ...(ngDevMode ? { debugName: "propertyOutlet" } : /* istanbul ignore next */ {}), read: ViewContainerRef });
517
+ /** Whether the row is a `<label>` around the control, unless its provider opts out. */
518
+ this.labelWrapper = computed(() => this.option()?.labelWrapper !== false, /* @ts-ignore */
519
+ ...(ngDevMode ? [{ debugName: "labelWrapper" }] : /* istanbul ignore next */ []));
520
+ /** ID of the caption that names the control of a row without the `<label>`. */
521
+ this.captionId = `ng-doc-playground-property-caption-${nextId++}`;
522
+ // The outlet is in the `<label>` or in the `<div>` of the row, so the query follows the branch.
523
+ this.propertyOutlet = viewChild('propertyOutlet', { ...(ngDevMode ? { debugName: "propertyOutlet" } : /* istanbul ignore next */ {}), read: ViewContainerRef });
213
524
  // A plain effect, not an after-render hook: it runs during change detection on the server
214
- // too, so server-rendered and prerendered pages include the controls. The outlet is a static
215
- // node of this view, so it exists by then. It runs again only when an input it reads changes.
525
+ // too, so server-rendered and prerendered pages include the controls. The outlet is inside
526
+ // the row's `@if` branch, so the query reports it once the branch renders, and the effect
527
+ // runs again then. It also runs again when an input it reads changes.
216
528
  effect(() => {
529
+ const outlet = this.propertyOutlet();
217
530
  const property = this.property();
218
531
  const typeControl = this.typeControl();
219
532
  const control = this.control();
220
533
  const defaultValue = this.defaultValue();
221
- untracked(() => this.createControl(property, typeControl, control, defaultValue));
534
+ if (outlet) {
535
+ untracked(() => this.createControl(outlet, property, typeControl, control, defaultValue));
536
+ }
222
537
  });
223
538
  inject(DestroyRef).onDestroy(() => this.propertyTypeControl?.destroy());
224
539
  }
225
- createControl(property, typeControl, formControl, defaultValue) {
540
+ createControl(outlet, property, typeControl, formControl, defaultValue) {
226
541
  if (!property || !typeControl) {
227
542
  return;
228
543
  }
229
544
  this.propertyTypeControl?.destroy();
230
- const control = this.propertyOutlet().createComponent(typeControl.control);
545
+ const control = outlet.createComponent(typeControl.control);
231
546
  this.propertyTypeControl = control;
232
547
  setTypeControlField(control, 'name', this.name());
233
548
  setTypeControlField(control, 'description', this.tooltipContent());
@@ -243,7 +558,7 @@ class NgDocPlaygroundPropertyComponent {
243
558
  this.hasPropertyControl.set(true);
244
559
  }
245
560
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.1", ngImport: i0, type: NgDocPlaygroundPropertyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
246
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.1", type: NgDocPlaygroundPropertyComponent, isStandalone: true, selector: "ng-doc-playground-property", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, property: { classPropertyName: "property", publicName: "property", isSignal: true, isRequired: false, transformFunction: null }, typeControl: { classPropertyName: "typeControl", publicName: "typeControl", isSignal: true, isRequired: false, transformFunction: null }, control: { classPropertyName: "control", publicName: "control", isSignal: true, isRequired: false, transformFunction: null }, defaultValue: { classPropertyName: "defaultValue", publicName: "defaultValue", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-has-property-control": "hasPropertyControl()" } }, viewQueries: [{ propertyName: "propertyOutlet", first: true, predicate: ["propertyOutlet"], descendants: true, read: ViewContainerRef, isSignal: true }], ngImport: i0, template: "<label\n class=\"ng-doc-playground-property-label\"\n [class.with-label]=\"!option()?.hideLabel\"\n [ng-doc-label]=\"option()?.hideLabel ? '' : labelContent\">\n <ng-template #labelContent>\n <span\n class=\"ng-doc-playground-property-name\"\n [ngDocTooltip]=\"tooltipTemplate\"\n [canOpen]=\"!!tooltipContent()\"\n [positions]=\"['left-center', 'top-right', 'bottom-right']\"\n >{{ name() }}</span\n >\n @if (typeChip(); as type) {\n <ng-doc-kind-icon [kind]=\"type\" type=\"type\"></ng-doc-kind-icon>\n }\n <ng-template #tooltipTemplate>\n <div [innerHTML]=\"tooltipContent() | ngDocSanitizeHtml\"></div>\n </ng-template>\n </ng-template>\n <ng-container #propertyOutlet></ng-container>\n</label>\n", styles: [":host{display:block}:host[data-has-property-control=false]{display:none}.ng-doc-playground-property-label.with-label{display:grid;grid-template-columns:84px minmax(0,1fr);align-items:center;gap:var(--ng-doc-space-3)}.ng-doc-playground-property-label.with-label ::ng-deep>.ng-doc-label{display:grid;justify-items:start;gap:var(--ng-doc-space-1);min-width:0;margin:0}.ng-doc-playground-property-label.with-label ::ng-deep>.ng-doc-content{width:100%;min-width:0}.ng-doc-playground-property-name{max-width:100%;overflow:hidden;text-overflow:ellipsis;font-family:var(--ng-doc-code-font);font-size:13px;line-height:18px;font-weight:500;color:var(--ng-doc-heading-color)}\n"], dependencies: [{ kind: "component", type: NgDocLabelComponent, selector: "label[ng-doc-label]", inputs: ["ng-doc-label", "align"] }, { 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: NgDocKindIconComponent, selector: "ng-doc-kind-icon", inputs: ["kind", "type", "size"] }, { kind: "pipe", type: NgDocSanitizeHtmlPipe, name: "ngDocSanitizeHtml" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
561
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.1", type: NgDocPlaygroundPropertyComponent, isStandalone: true, selector: "ng-doc-playground-property", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, property: { classPropertyName: "property", publicName: "property", isSignal: true, isRequired: false, transformFunction: null }, typeControl: { classPropertyName: "typeControl", publicName: "typeControl", isSignal: true, isRequired: false, transformFunction: null }, control: { classPropertyName: "control", publicName: "control", isSignal: true, isRequired: false, transformFunction: null }, defaultValue: { classPropertyName: "defaultValue", publicName: "defaultValue", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-has-property-control": "hasPropertyControl()" } }, viewQueries: [{ propertyName: "propertyOutlet", first: true, predicate: ["propertyOutlet"], descendants: true, read: ViewContainerRef, isSignal: true }], ngImport: i0, template: "@if (labelWrapper()) {\n <label\n class=\"ng-doc-playground-property-label\"\n [class.with-label]=\"!option()?.hideLabel\"\n [ng-doc-label]=\"option()?.hideLabel ? '' : labelContent\">\n <ng-container #propertyOutlet></ng-container>\n </label>\n} @else {\n <!-- A control with several interactive parts: a label would forward clicks on them to the first\n of them, so the caption names the control through aria-labelledby instead. -->\n <div\n class=\"ng-doc-playground-property-label ng-doc-playground-property-group\"\n role=\"group\"\n [class.with-label]=\"!option()?.hideLabel\"\n [attr.aria-labelledby]=\"option()?.hideLabel ? null : captionId\">\n @if (!option()?.hideLabel) {\n <span class=\"ng-doc-label\" [id]=\"captionId\">\n <ng-container [ngTemplateOutlet]=\"labelContent\"></ng-container>\n </span>\n }\n <span class=\"ng-doc-content\">\n <ng-container #propertyOutlet></ng-container>\n </span>\n </div>\n}\n<ng-template #labelContent>\n <span\n class=\"ng-doc-playground-property-name\"\n [ngDocTooltip]=\"tooltipTemplate\"\n [canOpen]=\"!!tooltipContent()\"\n [positions]=\"['left-center', 'top-right', 'bottom-right']\"\n >{{ name() }}</span\n >\n @if (typeChip(); as type) {\n <ng-doc-kind-icon [kind]=\"type\" type=\"type\"></ng-doc-kind-icon>\n }\n <ng-template #tooltipTemplate>\n <div [innerHTML]=\"tooltipContent() | ngDocSanitizeHtml\"></div>\n </ng-template>\n</ng-template>\n", styles: [":host{display:block}:host[data-has-property-control=false]{display:none}.ng-doc-playground-property-group{display:flex;flex-direction:column;align-items:flex-start}.ng-doc-playground-property-group>.ng-doc-content{display:flex;flex-direction:column;align-items:flex-start;width:100%}.ng-doc-playground-property-label.with-label{display:grid;grid-template-columns:84px minmax(0,1fr);align-items:center;gap:var(--ng-doc-space-3)}.ng-doc-playground-property-label.with-label ::ng-deep>.ng-doc-label{display:grid;justify-items:start;gap:var(--ng-doc-space-1);min-width:0;margin:0}.ng-doc-playground-property-label.with-label ::ng-deep>.ng-doc-content{width:100%;min-width:0}.ng-doc-playground-property-name{max-width:100%;overflow:hidden;text-overflow:ellipsis;font-family:var(--ng-doc-code-font);font-size:13px;line-height:18px;font-weight:500;color:var(--ng-doc-heading-color)}\n"], dependencies: [{ kind: "component", type: NgDocLabelComponent, selector: "label[ng-doc-label]", inputs: ["ng-doc-label", "align"] }, { 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: NgDocKindIconComponent, selector: "ng-doc-kind-icon", inputs: ["kind", "type", "size"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "pipe", type: NgDocSanitizeHtmlPipe, name: "ngDocSanitizeHtml" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
247
562
  }
248
563
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.1", ngImport: i0, type: NgDocPlaygroundPropertyComponent, decorators: [{
249
564
  type: Component,
@@ -252,15 +567,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.1", ngImpor
252
567
  NgDocTooltipDirective,
253
568
  NgDocSanitizeHtmlPipe,
254
569
  NgDocKindIconComponent,
570
+ NgTemplateOutlet,
255
571
  ], host: {
256
572
  '[attr.data-has-property-control]': 'hasPropertyControl()',
257
- }, template: "<label\n class=\"ng-doc-playground-property-label\"\n [class.with-label]=\"!option()?.hideLabel\"\n [ng-doc-label]=\"option()?.hideLabel ? '' : labelContent\">\n <ng-template #labelContent>\n <span\n class=\"ng-doc-playground-property-name\"\n [ngDocTooltip]=\"tooltipTemplate\"\n [canOpen]=\"!!tooltipContent()\"\n [positions]=\"['left-center', 'top-right', 'bottom-right']\"\n >{{ name() }}</span\n >\n @if (typeChip(); as type) {\n <ng-doc-kind-icon [kind]=\"type\" type=\"type\"></ng-doc-kind-icon>\n }\n <ng-template #tooltipTemplate>\n <div [innerHTML]=\"tooltipContent() | ngDocSanitizeHtml\"></div>\n </ng-template>\n </ng-template>\n <ng-container #propertyOutlet></ng-container>\n</label>\n", styles: [":host{display:block}:host[data-has-property-control=false]{display:none}.ng-doc-playground-property-label.with-label{display:grid;grid-template-columns:84px minmax(0,1fr);align-items:center;gap:var(--ng-doc-space-3)}.ng-doc-playground-property-label.with-label ::ng-deep>.ng-doc-label{display:grid;justify-items:start;gap:var(--ng-doc-space-1);min-width:0;margin:0}.ng-doc-playground-property-label.with-label ::ng-deep>.ng-doc-content{width:100%;min-width:0}.ng-doc-playground-property-name{max-width:100%;overflow:hidden;text-overflow:ellipsis;font-family:var(--ng-doc-code-font);font-size:13px;line-height:18px;font-weight:500;color:var(--ng-doc-heading-color)}\n"] }]
258
- }], 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', { ...{
259
- read: ViewContainerRef,
260
- }, isSignal: true }] }] } });
573
+ }, template: "@if (labelWrapper()) {\n <label\n class=\"ng-doc-playground-property-label\"\n [class.with-label]=\"!option()?.hideLabel\"\n [ng-doc-label]=\"option()?.hideLabel ? '' : labelContent\">\n <ng-container #propertyOutlet></ng-container>\n </label>\n} @else {\n <!-- A control with several interactive parts: a label would forward clicks on them to the first\n of them, so the caption names the control through aria-labelledby instead. -->\n <div\n class=\"ng-doc-playground-property-label ng-doc-playground-property-group\"\n role=\"group\"\n [class.with-label]=\"!option()?.hideLabel\"\n [attr.aria-labelledby]=\"option()?.hideLabel ? null : captionId\">\n @if (!option()?.hideLabel) {\n <span class=\"ng-doc-label\" [id]=\"captionId\">\n <ng-container [ngTemplateOutlet]=\"labelContent\"></ng-container>\n </span>\n }\n <span class=\"ng-doc-content\">\n <ng-container #propertyOutlet></ng-container>\n </span>\n </div>\n}\n<ng-template #labelContent>\n <span\n class=\"ng-doc-playground-property-name\"\n [ngDocTooltip]=\"tooltipTemplate\"\n [canOpen]=\"!!tooltipContent()\"\n [positions]=\"['left-center', 'top-right', 'bottom-right']\"\n >{{ name() }}</span\n >\n @if (typeChip(); as type) {\n <ng-doc-kind-icon [kind]=\"type\" type=\"type\"></ng-doc-kind-icon>\n }\n <ng-template #tooltipTemplate>\n <div [innerHTML]=\"tooltipContent() | ngDocSanitizeHtml\"></div>\n </ng-template>\n</ng-template>\n", styles: [":host{display:block}:host[data-has-property-control=false]{display:none}.ng-doc-playground-property-group{display:flex;flex-direction:column;align-items:flex-start}.ng-doc-playground-property-group>.ng-doc-content{display:flex;flex-direction:column;align-items:flex-start;width:100%}.ng-doc-playground-property-label.with-label{display:grid;grid-template-columns:84px minmax(0,1fr);align-items:center;gap:var(--ng-doc-space-3)}.ng-doc-playground-property-label.with-label ::ng-deep>.ng-doc-label{display:grid;justify-items:start;gap:var(--ng-doc-space-1);min-width:0;margin:0}.ng-doc-playground-property-label.with-label ::ng-deep>.ng-doc-content{width:100%;min-width:0}.ng-doc-playground-property-name{max-width:100%;overflow:hidden;text-overflow:ellipsis;font-family:var(--ng-doc-code-font);font-size:13px;line-height:18px;font-weight:500;color:var(--ng-doc-heading-color)}\n"] }]
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 }] }] } });
261
575
  /**
262
576
  * The value type of an input: its declared type when it is a primitive, else the type of the first
263
- * value of its type alias. Content slots are booleans.
577
+ * value of its type alias; none for an array. Content slots are booleans.
264
578
  * @param property - The input or the content slot.
265
579
  */
266
580
  function valueType(property) {
@@ -273,10 +587,15 @@ function valueType(property) {
273
587
  if (CHIP_TYPES.has(property.type)) {
274
588
  return property.type;
275
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
+ }
276
595
  const option = property.options?.[0];
277
596
  if (option !== undefined) {
278
597
  try {
279
- const type = typeof (property.isManual ? option : extractValueOrThrow(option));
598
+ const type = typeof resolvePlaygroundOption(option, property.isManual).value;
280
599
  return CHIP_TYPES.has(type) ? type : '';
281
600
  }
282
601
  catch {
@@ -313,6 +632,7 @@ const INPUT_ORDER = new Intl.Collator('en');
313
632
  */
314
633
  class NgDocPlaygroundPropertiesComponent {
315
634
  constructor() {
635
+ this.texts = inject(NG_DOC_TEXTS).playground;
316
636
  this.injector = inject(Injector);
317
637
  /** The form of the playground. */
318
638
  this.form = input.required(/* @ts-ignore */
@@ -329,6 +649,12 @@ class NgDocPlaygroundPropertiesComponent {
329
649
  /** Default values of the inputs; the inspector renders once they are known. */
330
650
  this.defaultValues = input(undefined, /* @ts-ignore */
331
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 */ []));
332
658
  /** Whether the inspector is hidden, leaving the demos only. */
333
659
  this.hideSidePanel = input(false, /* @ts-ignore */
334
660
  ...(ngDevMode ? [{ debugName: "hideSidePanel" }] : /* istanbul ignore next */ []));
@@ -338,6 +664,9 @@ class NgDocPlaygroundPropertiesComponent {
338
664
  /** Whether the demo is recreated each time an input changes. */
339
665
  this.recreateDemo = model(false, /* @ts-ignore */
340
666
  ...(ngDevMode ? [{ debugName: "recreateDemo" }] : /* istanbul ignore next */ []));
667
+ /** Whether the Recreate setting is fixed by the playground's options and hidden. */
668
+ this.recreateLocked = input(false, /* @ts-ignore */
669
+ ...(ngDevMode ? [{ debugName: "recreateLocked" }] : /* istanbul ignore next */ []));
341
670
  /** Whether the Reset button is shown. */
342
671
  this.showResetButton = input(false, /* @ts-ignore */
343
672
  ...(ngDevMode ? [{ debugName: "showResetButton" }] : /* istanbul ignore next */ []));
@@ -358,22 +687,27 @@ class NgDocPlaygroundPropertiesComponent {
358
687
  return typeControl ? { propertyName: String(key), property, typeControl } : null;
359
688
  })
360
689
  .filter(isPresent)
361
- .sort((a, b) => {
362
- const aOrder = a.typeControl.options?.order;
363
- const bOrder = b.typeControl.options?.order;
364
- if (isPresent(aOrder) && isPresent(bOrder)) {
365
- return aOrder - bOrder;
366
- }
367
- if (isPresent(aOrder)) {
368
- return -1;
369
- }
370
- if (isPresent(bOrder)) {
371
- return 1;
372
- }
373
- return INPUT_ORDER.compare(a.property.inputName, b.property.inputName);
374
- });
690
+ .sort((a, b) => compareOrder(a.property.order, b.property.order) ?? compareByTypeControl(a, b));
375
691
  }, /* @ts-ignore */
376
692
  ...(ngDevMode ? [{ debugName: "propertyControls" }] : /* istanbul ignore next */ []));
693
+ /**
694
+ * The controls in sections: the inputs without a group first, then a section per group, in the
695
+ * order of the group's first input.
696
+ */
697
+ this.propertySections = computed(() => {
698
+ const sections = new Map([
699
+ [undefined, []],
700
+ ]);
701
+ for (const control of this.propertyControls()) {
702
+ const group = control.property.group || undefined;
703
+ sections.set(group, [...(sections.get(group) ?? []), control]);
704
+ }
705
+ return Array.from(sections, ([group, controls]) => ({
706
+ title: group ?? this.texts.settings,
707
+ controls,
708
+ })).filter((section) => section.controls.length > 0);
709
+ }, /* @ts-ignore */
710
+ ...(ngDevMode ? [{ debugName: "propertySections" }] : /* istanbul ignore next */ []));
377
711
  /** The control of content slots. */
378
712
  this.contentTypeControl = this.getControlForType('boolean');
379
713
  }
@@ -388,9 +722,19 @@ class NgDocPlaygroundPropertiesComponent {
388
722
  getTypeControl(property) {
389
723
  const type = property.type;
390
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.
391
730
  const typeControl = this.getControlForType(type) ??
392
731
  (primitive ? this.getControlForType(primitive) : undefined) ??
393
- 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');
394
738
  if (!typeControl && isDevMode()) {
395
739
  console.warn(`NgDocPlayground didn't find the control for the @Input "${property.inputName}", the type "${type}" was not recognized'`);
396
740
  }
@@ -400,27 +744,38 @@ class NgDocPlaygroundPropertiesComponent {
400
744
  const token = getTokenForType(type);
401
745
  return token ? this.injector.get(token) : undefined;
402
746
  }
403
- getControlForTypeAlias(options, isManual) {
404
- if (options && options.length) {
405
- let optionsIsValid = true;
406
- if (!isManual) {
407
- try {
408
- // checking that all values are extractable
409
- options.forEach((item) => extractValueOrThrow(item));
410
- }
411
- catch {
412
- optionsIsValid = false;
413
- }
414
- }
415
- if (optionsIsValid) {
416
- const token = getTokenForType('NgDocTypeAlias');
417
- return token ? this.injector.get(token) : undefined;
418
- }
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;
419
759
  }
420
- 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);
421
776
  }
422
777
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.1", ngImport: i0, type: NgDocPlaygroundPropertiesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
423
- 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 }, 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 <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 <div class=\"ng-doc-playground-divider\"></div>\n @if (propertyControls().length) {\n <h5 class=\"ng-doc-title\">Settings</h5>\n <div class=\"ng-doc-playground-property-list\">\n @for (propertyControl of propertyControls(); track propertyControl.propertyName) {\n <ng-doc-playground-property\n [name]=\"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 }); }
424
779
  }
425
780
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.1", ngImport: i0, type: NgDocPlaygroundPropertiesComponent, decorators: [{
426
781
  type: Component,
@@ -433,17 +788,58 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.1", ngImpor
433
788
  KeyValuePipe,
434
789
  NgDocBindPipe,
435
790
  NgDocExecutePipe,
436
- ], 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 <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 <div class=\"ng-doc-playground-divider\"></div>\n @if (propertyControls().length) {\n <h5 class=\"ng-doc-title\">Settings</h5>\n <div class=\"ng-doc-playground-property-list\">\n @for (propertyControl of propertyControls(); track propertyControl.propertyName) {\n <ng-doc-playground-property\n [name]=\"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"] }]
437
- }], 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"] }], 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"] }] } });
793
+ /**
794
+ * Compares the `order` of two inputs from the playground's `controls`: inputs with one come first,
795
+ * lowest first.
796
+ * @param a - The first input's order.
797
+ * @param b - The second input's order.
798
+ * @returns The comparison, or `undefined` when neither input has an order or both have the same.
799
+ */
800
+ function compareOrder(a, b) {
801
+ if (isPresent(a) && isPresent(b)) {
802
+ return a - b || undefined;
803
+ }
804
+ if (isPresent(a)) {
805
+ return -1;
806
+ }
807
+ if (isPresent(b)) {
808
+ return 1;
809
+ }
810
+ return undefined;
811
+ }
812
+ /**
813
+ * Compares two inputs by the order of their type controls: controls with an order first, lowest
814
+ * first (equal orders keep the inputs' order), then the others by name.
815
+ * @param a - The first input.
816
+ * @param b - The second input.
817
+ */
818
+ function compareByTypeControl(a, b) {
819
+ const aOrder = a.typeControl.options?.order;
820
+ const bOrder = b.typeControl.options?.order;
821
+ if (isPresent(aOrder) && isPresent(bOrder)) {
822
+ return aOrder - bOrder;
823
+ }
824
+ if (isPresent(aOrder)) {
825
+ return -1;
826
+ }
827
+ if (isPresent(bOrder)) {
828
+ return 1;
829
+ }
830
+ return INPUT_ORDER.compare(a.property.inputName, b.property.inputName);
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']);
438
834
  /** The types whose control also edits an optional or nullable input of the type. */
439
835
  const PRIMITIVE_TYPES = new Set(['string', 'number', 'boolean']);
440
836
  /**
441
837
  * The primitive of an optional or nullable primitive input type, such as `string` of
442
838
  * `string | undefined` (an optional `@Input() label?: string`, `input<string>()` or
443
839
  * `model<string>()`) or of `number | null`, so that the input gets the primitive's control.
444
- * Other types are matched by their exact name only: a custom control registered for `Position`
445
- * does not receive the `undefined` of a `Position | undefined` input unless it is registered for
446
- * 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.
447
843
  * @param type - The input type as the builder printed it.
448
844
  * @returns The primitive, or `undefined` when the type is not one.
449
845
  */
@@ -458,11 +854,18 @@ function optionalPrimitive(type) {
458
854
  /**
459
855
  * A playground: an inspector with a control per input and content slot of its target, and a demo
460
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.
461
863
  */
462
864
  class NgDocPlaygroundComponent {
463
865
  constructor() {
464
866
  this.rootPage = inject(NgDocRootPage);
465
867
  this.formBuilder = inject(FormBuilder);
868
+ this.texts = inject(NG_DOC_TEXTS).playground;
466
869
  /** The name of the playground in the page configuration. */
467
870
  this.id = input.required(/* @ts-ignore */
468
871
  ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
@@ -478,9 +881,6 @@ class NgDocPlaygroundComponent {
478
881
  /** Options of the playground action; they extend the playground configuration. */
479
882
  this.options = input({}, /* @ts-ignore */
480
883
  ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
481
- /** Whether the demo is created again each time a value changes. */
482
- this.recreateDemo = signal(false, /* @ts-ignore */
483
- ...(ngDevMode ? [{ debugName: "recreateDemo" }] : /* istanbul ignore next */ []));
484
884
  /** The values of the inputs and content slots. */
485
885
  this.formGroup = signal(undefined, /* @ts-ignore */
486
886
  ...(ngDevMode ? [{ debugName: "formGroup" }] : /* istanbul ignore next */ []));
@@ -490,13 +890,183 @@ class NgDocPlaygroundComponent {
490
890
  /** The playground configuration of the page, extended by the options. */
491
891
  this.configuration = computed(() => Object.assign({}, this.rootPage.page?.playgrounds?.[this.id()], this.options()), /* @ts-ignore */
492
892
  ...(ngDevMode ? [{ debugName: "configuration" }] : /* istanbul ignore next */ []));
893
+ /**
894
+ * Whether the demo is created again each time a value changes. It starts from the `recreate`
895
+ * option; the reader can change it unless the option is `'always'`.
896
+ */
897
+ this.recreateDemo = linkedSignal(() => !!this.configuration().recreate, /* @ts-ignore */
898
+ ...(ngDevMode ? [{ debugName: "recreateDemo" }] : /* istanbul ignore next */ []));
899
+ /** Whether the Recreate setting is fixed on, and hidden, by the `'always'` option. */
900
+ this.recreateLocked = computed(() => this.configuration().recreate === 'always', /* @ts-ignore */
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 */ []));
493
952
  this.defaultProperties = {};
494
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);
495
1056
  }
496
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() {
497
1065
  this.defaultProperties = this.getPropertiesFormValues();
498
1066
  this.defaultContent = this.getContentFormValues();
499
- const propertiesForm = this.formBuilder.group(this.defaultProperties);
1067
+ // `FormBuilder` reads an array as `[value, validators]`, so each value is wrapped: the default
1068
+ // of an array input is the value of its control.
1069
+ const propertiesForm = this.formBuilder.group(Object.fromEntries(Object.entries(this.defaultProperties).map(([key, value]) => [key, [value]])));
500
1070
  const contentForm = this.formBuilder.group(this.defaultContent);
501
1071
  const formGroup = this.formBuilder.group({
502
1072
  properties: propertiesForm,
@@ -508,6 +1078,30 @@ class NgDocPlaygroundComponent {
508
1078
  content: this.defaultContent,
509
1079
  });
510
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}`;
511
1105
  }
512
1106
  isDefaultState() {
513
1107
  const formGroup = this.formGroup();
@@ -541,98 +1135,201 @@ class NgDocPlaygroundComponent {
541
1135
  });
542
1136
  }
543
1137
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.1", ngImport: i0, type: NgDocPlaygroundComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
544
- 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 (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", "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 }); }
545
1139
  }
546
1140
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.1", ngImport: i0, type: NgDocPlaygroundComponent, decorators: [{
547
1141
  type: Component,
548
- 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 (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"] }]
549
- }], 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 }] }] } });
550
1144
 
1145
+ const EMPTY_STATE = {
1146
+ inputs: [],
1147
+ properties: {},
1148
+ content: [],
1149
+ recreate: false,
1150
+ };
551
1151
  /**
552
- * Base class of the playground classes the builder generates, one per playground and selector.
553
- * A generated class renders the playground template with the target's inputs bound to
554
- * `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.
555
1161
  */
556
- class NgDocBasePlayground {
557
- static { this.selector = 'unknown'; }
1162
+ class NgDocPlaygroundHostComponent {
558
1163
  constructor() {
559
- /** Values of the target's inputs, by property name. */
560
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
561
- 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 */
562
1166
  ...(ngDevMode ? [{ debugName: "properties" }] : /* istanbul ignore next */ []));
563
- /** The `data` given to the playground action; it extends the configuration's `data`. */
564
- this.actionData = input({}, /* @ts-ignore */
565
- ...(ngDevMode ? [{ debugName: "actionData" }] : /* istanbul ignore next */ []));
566
- /** Whether each content slot is shown, by slot name. */
567
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
568
- this.content = input({}, /* @ts-ignore */
569
- ...(ngDevMode ? [{ debugName: "content" }] : /* istanbul ignore next */ []));
570
- /** The default values of the target's inputs, read before the template binds them. */
571
- this.defaultValues = {};
572
- this.reattached = new Subject();
573
- this.playgroundContainer = inject(NgDocPlaygroundComponent);
574
- this.changeDetectorRef = inject(ChangeDetectorRef);
575
- // The view is not checked until the default values of the target are read, so the
576
- // template's bindings cannot overwrite them first.
577
- 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
+ }
578
1196
  }
579
- /** Emits once, after the view is attached again and checks the template's bindings. */
580
- get onReattached() {
581
- 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 });
582
1228
  }
583
- /** The playground `data`: the configuration's data extended by the action's data. */
584
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
585
- get data() {
586
- 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
+ }
587
1246
  }
588
- ngOnInit() {
589
- // The target is created with the view, but the view has not been checked yet, so its inputs
590
- // still hold their defaults. Signal inputs are read by calling them.
591
- const playground = this.playground();
592
- if (playground) {
593
- this.defaultValues = Object.keys(playground).reduce((values, key) => {
594
- try {
595
- const value = playground[key];
596
- values[key] = typeof value === 'function' ? value.call(playground) : value;
597
- }
598
- catch {
599
- // Some properties are getters or functions that throw without their context.
600
- }
601
- return values;
602
- }, {});
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;
603
1258
  }
604
- else if (this.target) {
605
- // A pipe: the defaults of its `transform` parameters after the value itself.
606
- const defaults = extractFunctionDefaults(this.target.prototype.transform);
607
- this.defaultValues = Object.keys(this.playgroundContainer.properties() ?? {}).reduce((values, key, i) => {
608
- values[key] = defaults[i + 1];
609
- return values;
610
- }, {});
1259
+ try {
1260
+ this.setState(state);
1261
+ this.applied();
611
1262
  }
612
- else {
613
- throw new Error('Playground is not defined or initialized');
1263
+ catch (error) {
1264
+ this.post({ type: 'playground-error', phase: 'apply', message: describe(error) });
614
1265
  }
615
- if (!this.playgroundContainer.defaultValues()) {
616
- 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);
617
1280
  }
618
- // The demo sets the playground's inputs right after it creates this view; the view is
619
- // attached again once they are set, so its first check renders the playground's values.
620
- Promise.resolve().then(() => {
621
- this.changeDetectorRef.reattach();
622
- this.changeDetectorRef.markForCheck();
623
- 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(/^#/, ''),
624
1290
  });
625
1291
  }
626
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.1", ngImport: i0, type: NgDocBasePlayground, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
627
- 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);
628
1328
  }
629
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.1", ngImport: i0, type: NgDocBasePlayground, decorators: [{
630
- type: Directive
631
- }], 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 }] }] } });
632
1329
 
633
1330
  /**
634
1331
  * Generated bundle index. Do not edit.
635
1332
  */
636
1333
 
637
- export { NgDocBasePlayground, NgDocPlaygroundComponent, NgDocPlaygroundDemoComponent, NgDocPlaygroundPropertiesComponent, NgDocPlaygroundPropertyComponent };
1334
+ export { NgDocBasePlayground, NgDocPlaygroundComponent, NgDocPlaygroundDemoComponent, NgDocPlaygroundHostComponent, NgDocPlaygroundPropertiesComponent, NgDocPlaygroundPropertyComponent, ɵNG_DOC_PLAYGROUND_CONTAINER };
638
1335
  //# sourceMappingURL=ng-doc-app-components-playground.mjs.map