diff --git a/.gitignore b/.gitignore index 4b4897a..31cb7a9 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,6 @@ openfeature # Build output directory /bin + +# test output +**/obj/ \ No newline at end of file diff --git a/README.md b/README.md index 71bdca3..d763d55 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,9 @@ openfeature generate typescript --output ./src/flags See [here](./docs/commands/openfeature_generate.md) for all available options. +> **_NOTE:_** +> Angular generated code requires `@openfeature/angular-sdk` version `1.1.0` or newer. + ### `pull` Fetch feature flag configurations from a remote source. diff --git a/internal/cmd/testdata/success_angular.golden b/internal/cmd/testdata/success_angular.golden index c3daa3d..b0deeed 100644 --- a/internal/cmd/testdata/success_angular.golden +++ b/internal/cmd/testdata/success_angular.golden @@ -4,25 +4,28 @@ * This file contains generated typesafe Angular services and directives * for feature flags defined in your OpenFeature flag manifest. * - * @openfeature/angular-sdk is required as a peer dependency. + * Requires @openfeature/angular-sdk >= 1.1.0. * - * @see https://openfeature.dev + * @see https://openfeature.dev/docs/reference/other-technologies/cli */ import { + ChangeDetectorRef, Directive, inject, Injectable, + Input, + OnChanges, + TemplateRef, + ViewContainerRef, } from '@angular/core'; import { AngularFlagEvaluationOptions, - BooleanFeatureFlagDirective, EvaluationDetails, + FeatureFlagDirective, + FeatureFlagDirectiveContext, FeatureFlagService, JsonValue, - NumberFeatureFlagDirective, - ObjectFeatureFlagDirective, - StringFeatureFlagDirective, } from '@openfeature/angular-sdk'; import { Observable } from 'rxjs'; @@ -251,376 +254,702 @@ export class GeneratedFeatureFlagService { // ============================================================================ + /** * Structural directive for the `discountPercentage` feature flag. * * Discount percentage applied to purchases. * - * This directive wraps the `NumberFeatureFlagDirective` from @openfeature/angular-sdk - * with pre-configured flag key and default value. + * This directive extends `FeatureFlagDirective` from @openfeature/angular-sdk + * with a pre-configured flag key and default value. * * **Details:** * - Flag key: `discountPercentage` * - Type: `number` * - Default value: `0.15` * + * * @example - * Basic usage: + * Explicit `ng-template` (no `*`), bind inputs directly * ```html - *
- * Content shown when flag is matched. - *
+ * + *
Content shown when flag is matched.
+ *
+ * + * Content shown when flag is not matched. + * * ``` * * @example - * With else template: + * Microsyntax `*` form, start with `let` * ```html - *
+ *
* Content shown when flag is matched. *
- * - * Content shown when flag is not matched. - * * ``` * * @example - * With all options: + * Simple `*` usage (no else/initializing/reconciling) * ```html - *
- * Flag value: {{ value }} + *
+ * Content shown when flag is matched. *
* ``` + * + * @remarks + * Note: Angular's microsyntax parser requires the first segment to be a primary + * expression or a `let` declaration. Because the flag key is preconfigured, start with + * `let v` or `let details = evaluationDetails` (or `let _` if you do not need the value) + * before any `else`/`initializing`/`reconciling`/`default` segments. + * This is an Angular microsyntax parsing constraint, not a directive limitation. + * `*discountPercentage="else elseTemplate"` will not parse because `else` is a + * secondary segment. If you do not need the value, use `let _`, or use the + * explicit `ng-template` form above. */ @Directive({ selector: '[discountPercentage]', standalone: true, - hostDirectives: [ - { - directive: NumberFeatureFlagDirective, - inputs: [ - 'numberFeatureFlag: discountPercentage', - 'numberFeatureFlagDefault: discountPercentageDefault', - 'numberFeatureFlagDomain: discountPercentageDomain', - 'numberFeatureFlagUpdateOnConfigurationChanged: discountPercentageUpdateOnConfigurationChanged', - 'numberFeatureFlagUpdateOnContextChanged: discountPercentageUpdateOnContextChanged', - 'numberFeatureFlagElse: discountPercentageElse', - 'numberFeatureFlagInitializing: discountPercentageInitializing', - 'numberFeatureFlagReconciling: discountPercentageReconciling', - 'numberFeatureFlagValue: discountPercentageValue', - ], - }, - ], }) -export class DiscountPercentageDirective { - private readonly hostDirective = inject(NumberFeatureFlagDirective); +export class DiscountPercentageFeatureFlagDirective extends FeatureFlagDirective implements OnChanges { + override _changeDetectorRef = inject(ChangeDetectorRef); + override _viewContainerRef = inject(ViewContainerRef); + override _thenTemplateRef = inject>>(TemplateRef); + + /** + * The expected value of this number feature flag, for which the `then` template should be rendered. + */ + @Input({ required: false }) discountPercentageValue?: number; constructor() { - // Set the pre-configured flag key and default value - this.hostDirective.numberFeatureFlag = "discountPercentage"; - this.hostDirective.numberFeatureFlagDefault = 0.15; + super(); + + this._featureFlagKey = "discountPercentage"; + this._featureFlagDefault = 0.15; + + } + + override ngOnChanges() { + super.ngOnChanges(); + + this._featureFlagValue = this.discountPercentageValue; + } + + /** + * The domain of the number feature flag. + */ + @Input({ required: false }) + set discountPercentageDomain(domain: string | undefined) { + super.featureFlagDomain = domain; + } + + /** + * Update the component if the provider emits a ConfigurationChanged event. + * Set to false to prevent components from re-rendering when flag value changes + * are received by the associated provider. + * Defaults to true. + */ + @Input({ required: false }) + set discountPercentageUpdateOnConfigurationChanged(enabled: boolean | undefined) { + this._updateOnConfigurationChanged = enabled ?? true; + } + + /** + * Update the component when the OpenFeature context changes. + * Set to false to prevent components from re-rendering when attributes which + * may be factors in flag evaluation change. + * Defaults to true. + */ + @Input({ required: false }) + set discountPercentageUpdateOnContextChanged(enabled: boolean | undefined) { + this._updateOnContextChanged = enabled ?? true; + } + + /** + * Template to be displayed when the feature flag does not match value. + */ + @Input() + set discountPercentageElse(tpl: TemplateRef>) { + this._elseTemplateRef = tpl; + } + + /** + * Template to be displayed when the provider is not ready. + */ + @Input() + set discountPercentageInitializing(tpl: TemplateRef>) { + this._initializingTemplateRef = tpl; + } + + /** + * Template to be displayed when the provider is reconciling. + */ + @Input() + set discountPercentageReconciling(tpl: TemplateRef>) { + this._reconcilingTemplateRef = tpl; } } + + /** * Structural directive for the `enableFeatureA` feature flag. * * Controls whether Feature A is enabled. * - * This directive wraps the `BooleanFeatureFlagDirective` from @openfeature/angular-sdk - * with pre-configured flag key and default value. + * This directive extends `FeatureFlagDirective` from @openfeature/angular-sdk + * with a pre-configured flag key and default value. * * **Details:** * - Flag key: `enableFeatureA` * - Type: `boolean` * - Default value: `false` * + * * @example - * Basic usage: + * Explicit `ng-template` (no `*`), bind inputs directly * ```html - *
- * Content shown when flag is enabled. - *
+ * + *
Content shown when flag is enabled.
+ *
+ * + * Content shown when flag is disabled. + * * ``` * * @example - * With else template: + * Microsyntax `*` form, start with `let` * ```html - *
+ *
* Content shown when flag is enabled. *
- * - * Content shown when flag is disabled. - * * ``` * * @example - * With all options: + * Simple `*` usage (no else/initializing/reconciling) * ```html - *
- * Flag value: {{ value }} + *
+ * Content shown when flag is enabled. *
* ``` + * + * @remarks + * Note: Angular's microsyntax parser requires the first segment to be a primary + * expression or a `let` declaration. Because the flag key is preconfigured, start with + * `let v` or `let details = evaluationDetails` (or `let _` if you do not need the value) + * before any `else`/`initializing`/`reconciling`/`default` segments. + * This is an Angular microsyntax parsing constraint, not a directive limitation. + * `*enableFeatureA="else elseTemplate"` will not parse because `else` is a + * secondary segment. If you do not need the value, use `let _`, or use the + * explicit `ng-template` form above. */ @Directive({ selector: '[enableFeatureA]', standalone: true, - hostDirectives: [ - { - directive: BooleanFeatureFlagDirective, - inputs: [ - 'booleanFeatureFlag: enableFeatureA', - 'booleanFeatureFlagDefault: enableFeatureADefault', - 'booleanFeatureFlagDomain: enableFeatureADomain', - 'booleanFeatureFlagUpdateOnConfigurationChanged: enableFeatureAUpdateOnConfigurationChanged', - 'booleanFeatureFlagUpdateOnContextChanged: enableFeatureAUpdateOnContextChanged', - 'booleanFeatureFlagElse: enableFeatureAElse', - 'booleanFeatureFlagInitializing: enableFeatureAInitializing', - 'booleanFeatureFlagReconciling: enableFeatureAReconciling', - ], - }, - ], }) -export class EnableFeatureADirective { - private readonly hostDirective = inject(BooleanFeatureFlagDirective); +export class EnableFeatureAFeatureFlagDirective extends FeatureFlagDirective implements OnChanges { + override _changeDetectorRef = inject(ChangeDetectorRef); + override _viewContainerRef = inject(ViewContainerRef); + override _thenTemplateRef = inject>>(TemplateRef); constructor() { - // Set the pre-configured flag key and default value - this.hostDirective.booleanFeatureFlag = "enableFeatureA"; - this.hostDirective.booleanFeatureFlagDefault = false; + super(); + + this._featureFlagKey = "enableFeatureA"; + this._featureFlagDefault = false; + + this._featureFlagValue = true; + + } + + /** + * The domain of the boolean feature flag. + */ + @Input({ required: false }) + set enableFeatureADomain(domain: string | undefined) { + super.featureFlagDomain = domain; + } + + /** + * Update the component if the provider emits a ConfigurationChanged event. + * Set to false to prevent components from re-rendering when flag value changes + * are received by the associated provider. + * Defaults to true. + */ + @Input({ required: false }) + set enableFeatureAUpdateOnConfigurationChanged(enabled: boolean | undefined) { + this._updateOnConfigurationChanged = enabled ?? true; + } + + /** + * Update the component when the OpenFeature context changes. + * Set to false to prevent components from re-rendering when attributes which + * may be factors in flag evaluation change. + * Defaults to true. + */ + @Input({ required: false }) + set enableFeatureAUpdateOnContextChanged(enabled: boolean | undefined) { + this._updateOnContextChanged = enabled ?? true; + } + + /** + * Template to be displayed when the feature flag is false. + */ + @Input() + set enableFeatureAElse(tpl: TemplateRef>) { + this._elseTemplateRef = tpl; + } + + /** + * Template to be displayed when the provider is not ready. + */ + @Input() + set enableFeatureAInitializing(tpl: TemplateRef>) { + this._initializingTemplateRef = tpl; + } + + /** + * Template to be displayed when the provider is reconciling. + */ + @Input() + set enableFeatureAReconciling(tpl: TemplateRef>) { + this._reconcilingTemplateRef = tpl; } } + + /** * Structural directive for the `greetingMessage` feature flag. * * The message to use for greeting users. * - * This directive wraps the `StringFeatureFlagDirective` from @openfeature/angular-sdk - * with pre-configured flag key and default value. + * This directive extends `FeatureFlagDirective` from @openfeature/angular-sdk + * with a pre-configured flag key and default value. * * **Details:** * - Flag key: `greetingMessage` * - Type: `string` * - Default value: `Hello there!` * + * * @example - * Basic usage: + * Explicit `ng-template` (no `*`), bind inputs directly * ```html - *
- * Content shown when flag is matched. - *
+ * + *
Content shown when flag is matched.
+ *
+ * + * Content shown when flag is not matched. + * * ``` * * @example - * With else template: + * Microsyntax `*` form, start with `let` * ```html - *
+ *
* Content shown when flag is matched. *
- * - * Content shown when flag is not matched. - * * ``` * * @example - * With all options: + * Simple `*` usage (no else/initializing/reconciling) * ```html - *
- * Flag value: {{ value }} + *
+ * Content shown when flag is matched. *
* ``` + * + * @remarks + * Note: Angular's microsyntax parser requires the first segment to be a primary + * expression or a `let` declaration. Because the flag key is preconfigured, start with + * `let v` or `let details = evaluationDetails` (or `let _` if you do not need the value) + * before any `else`/`initializing`/`reconciling`/`default` segments. + * This is an Angular microsyntax parsing constraint, not a directive limitation. + * `*greetingMessage="else elseTemplate"` will not parse because `else` is a + * secondary segment. If you do not need the value, use `let _`, or use the + * explicit `ng-template` form above. */ @Directive({ selector: '[greetingMessage]', standalone: true, - hostDirectives: [ - { - directive: StringFeatureFlagDirective, - inputs: [ - 'stringFeatureFlag: greetingMessage', - 'stringFeatureFlagDefault: greetingMessageDefault', - 'stringFeatureFlagDomain: greetingMessageDomain', - 'stringFeatureFlagUpdateOnConfigurationChanged: greetingMessageUpdateOnConfigurationChanged', - 'stringFeatureFlagUpdateOnContextChanged: greetingMessageUpdateOnContextChanged', - 'stringFeatureFlagElse: greetingMessageElse', - 'stringFeatureFlagInitializing: greetingMessageInitializing', - 'stringFeatureFlagReconciling: greetingMessageReconciling', - 'stringFeatureFlagValue: greetingMessageValue', - ], - }, - ], }) -export class GreetingMessageDirective { - private readonly hostDirective = inject(StringFeatureFlagDirective); +export class GreetingMessageFeatureFlagDirective extends FeatureFlagDirective implements OnChanges { + override _changeDetectorRef = inject(ChangeDetectorRef); + override _viewContainerRef = inject(ViewContainerRef); + override _thenTemplateRef = inject>>(TemplateRef); + + /** + * The expected value of this string feature flag, for which the `then` template should be rendered. + */ + @Input({ required: false }) greetingMessageValue?: string; constructor() { - // Set the pre-configured flag key and default value - this.hostDirective.stringFeatureFlag = "greetingMessage"; - this.hostDirective.stringFeatureFlagDefault = "Hello there!"; + super(); + + this._featureFlagKey = "greetingMessage"; + this._featureFlagDefault = "Hello there!"; + + } + + override ngOnChanges() { + super.ngOnChanges(); + + this._featureFlagValue = this.greetingMessageValue; + } + + /** + * The domain of the string feature flag. + */ + @Input({ required: false }) + set greetingMessageDomain(domain: string | undefined) { + super.featureFlagDomain = domain; + } + + /** + * Update the component if the provider emits a ConfigurationChanged event. + * Set to false to prevent components from re-rendering when flag value changes + * are received by the associated provider. + * Defaults to true. + */ + @Input({ required: false }) + set greetingMessageUpdateOnConfigurationChanged(enabled: boolean | undefined) { + this._updateOnConfigurationChanged = enabled ?? true; + } + + /** + * Update the component when the OpenFeature context changes. + * Set to false to prevent components from re-rendering when attributes which + * may be factors in flag evaluation change. + * Defaults to true. + */ + @Input({ required: false }) + set greetingMessageUpdateOnContextChanged(enabled: boolean | undefined) { + this._updateOnContextChanged = enabled ?? true; + } + + /** + * Template to be displayed when the feature flag does not match value. + */ + @Input() + set greetingMessageElse(tpl: TemplateRef>) { + this._elseTemplateRef = tpl; + } + + /** + * Template to be displayed when the provider is not ready. + */ + @Input() + set greetingMessageInitializing(tpl: TemplateRef>) { + this._initializingTemplateRef = tpl; + } + + /** + * Template to be displayed when the provider is reconciling. + */ + @Input() + set greetingMessageReconciling(tpl: TemplateRef>) { + this._reconcilingTemplateRef = tpl; } } + + /** * Structural directive for the `themeCustomization` feature flag. * * Allows customization of theme colors. * - * This directive wraps the `ObjectFeatureFlagDirective` from @openfeature/angular-sdk - * with pre-configured flag key and default value. + * This directive extends `FeatureFlagDirective` from @openfeature/angular-sdk + * with a pre-configured flag key and default value. * * **Details:** * - Flag key: `themeCustomization` * - Type: `JsonValue` * - Default value: `{"primaryColor":"#007bff","secondaryColor":"#6c757d"}` * + * * @example - * Basic usage: + * Explicit `ng-template` (no `*`), bind inputs directly * ```html - *
- * Content shown when flag is matched. - *
+ * + *
Content shown when flag is matched.
+ *
+ * + * Content shown when flag is not matched. + * * ``` * * @example - * With else template: + * Microsyntax `*` form, start with `let` * ```html - *
+ *
* Content shown when flag is matched. *
- * - * Content shown when flag is not matched. - * * ``` * * @example - * With all options: + * Simple `*` usage (no else/initializing/reconciling) * ```html - *
- * Flag value: {{ value }} + *
+ * Content shown when flag is matched. *
* ``` + * + * @remarks + * Note: Angular's microsyntax parser requires the first segment to be a primary + * expression or a `let` declaration. Because the flag key is preconfigured, start with + * `let v` or `let details = evaluationDetails` (or `let _` if you do not need the value) + * before any `else`/`initializing`/`reconciling`/`default` segments. + * This is an Angular microsyntax parsing constraint, not a directive limitation. + * `*themeCustomization="else elseTemplate"` will not parse because `else` is a + * secondary segment. If you do not need the value, use `let _`, or use the + * explicit `ng-template` form above. */ @Directive({ selector: '[themeCustomization]', standalone: true, - hostDirectives: [ - { - directive: ObjectFeatureFlagDirective, - inputs: [ - 'objectFeatureFlag: themeCustomization', - 'objectFeatureFlagDefault: themeCustomizationDefault', - 'objectFeatureFlagDomain: themeCustomizationDomain', - 'objectFeatureFlagUpdateOnConfigurationChanged: themeCustomizationUpdateOnConfigurationChanged', - 'objectFeatureFlagUpdateOnContextChanged: themeCustomizationUpdateOnContextChanged', - 'objectFeatureFlagElse: themeCustomizationElse', - 'objectFeatureFlagInitializing: themeCustomizationInitializing', - 'objectFeatureFlagReconciling: themeCustomizationReconciling', - 'objectFeatureFlagValue: themeCustomizationValue', - ], - }, - ], }) -export class ThemeCustomizationDirective { - private readonly hostDirective = inject(ObjectFeatureFlagDirective); +export class ThemeCustomizationFeatureFlagDirective extends FeatureFlagDirective implements OnChanges { + override _changeDetectorRef = inject(ChangeDetectorRef); + override _viewContainerRef = inject(ViewContainerRef); + override _thenTemplateRef = inject>>(TemplateRef); + + /** + * The expected value of this object feature flag, for which the `then` template should be rendered. + */ + @Input({ required: false }) themeCustomizationValue?: JsonValue; constructor() { - // Set the pre-configured flag key and default value - this.hostDirective.objectFeatureFlag = "themeCustomization"; - this.hostDirective.objectFeatureFlagDefault = {"primaryColor":"#007bff","secondaryColor":"#6c757d"}; + super(); + + this._featureFlagKey = "themeCustomization"; + this._featureFlagDefault = {"primaryColor":"#007bff","secondaryColor":"#6c757d"}; + + } + + override ngOnChanges() { + super.ngOnChanges(); + + this._featureFlagValue = this.themeCustomizationValue; + } + + /** + * The domain of the object feature flag. + */ + @Input({ required: false }) + set themeCustomizationDomain(domain: string | undefined) { + super.featureFlagDomain = domain; + } + + /** + * Update the component if the provider emits a ConfigurationChanged event. + * Set to false to prevent components from re-rendering when flag value changes + * are received by the associated provider. + * Defaults to true. + */ + @Input({ required: false }) + set themeCustomizationUpdateOnConfigurationChanged(enabled: boolean | undefined) { + this._updateOnConfigurationChanged = enabled ?? true; + } + + /** + * Update the component when the OpenFeature context changes. + * Set to false to prevent components from re-rendering when attributes which + * may be factors in flag evaluation change. + * Defaults to true. + */ + @Input({ required: false }) + set themeCustomizationUpdateOnContextChanged(enabled: boolean | undefined) { + this._updateOnContextChanged = enabled ?? true; + } + + /** + * Template to be displayed when the feature flag does not match value. + */ + @Input() + set themeCustomizationElse(tpl: TemplateRef>) { + this._elseTemplateRef = tpl; + } + + /** + * Template to be displayed when the provider is not ready. + */ + @Input() + set themeCustomizationInitializing(tpl: TemplateRef>) { + this._initializingTemplateRef = tpl; + } + + /** + * Template to be displayed when the provider is reconciling. + */ + @Input() + set themeCustomizationReconciling(tpl: TemplateRef>) { + this._reconcilingTemplateRef = tpl; } } + + /** * Structural directive for the `usernameMaxLength` feature flag. * * Maximum allowed length for usernames. * - * This directive wraps the `NumberFeatureFlagDirective` from @openfeature/angular-sdk - * with pre-configured flag key and default value. + * This directive extends `FeatureFlagDirective` from @openfeature/angular-sdk + * with a pre-configured flag key and default value. * * **Details:** * - Flag key: `usernameMaxLength` * - Type: `number` * - Default value: `50` * + * * @example - * Basic usage: + * Explicit `ng-template` (no `*`), bind inputs directly * ```html - *
- * Content shown when flag is matched. - *
+ * + *
Content shown when flag is matched.
+ *
+ * + * Content shown when flag is not matched. + * * ``` * * @example - * With else template: + * Microsyntax `*` form, start with `let` * ```html - *
+ *
* Content shown when flag is matched. *
- * - * Content shown when flag is not matched. - * * ``` * * @example - * With all options: + * Simple `*` usage (no else/initializing/reconciling) * ```html - *
- * Flag value: {{ value }} + *
+ * Content shown when flag is matched. *
* ``` + * + * @remarks + * Note: Angular's microsyntax parser requires the first segment to be a primary + * expression or a `let` declaration. Because the flag key is preconfigured, start with + * `let v` or `let details = evaluationDetails` (or `let _` if you do not need the value) + * before any `else`/`initializing`/`reconciling`/`default` segments. + * This is an Angular microsyntax parsing constraint, not a directive limitation. + * `*usernameMaxLength="else elseTemplate"` will not parse because `else` is a + * secondary segment. If you do not need the value, use `let _`, or use the + * explicit `ng-template` form above. */ @Directive({ selector: '[usernameMaxLength]', standalone: true, - hostDirectives: [ - { - directive: NumberFeatureFlagDirective, - inputs: [ - 'numberFeatureFlag: usernameMaxLength', - 'numberFeatureFlagDefault: usernameMaxLengthDefault', - 'numberFeatureFlagDomain: usernameMaxLengthDomain', - 'numberFeatureFlagUpdateOnConfigurationChanged: usernameMaxLengthUpdateOnConfigurationChanged', - 'numberFeatureFlagUpdateOnContextChanged: usernameMaxLengthUpdateOnContextChanged', - 'numberFeatureFlagElse: usernameMaxLengthElse', - 'numberFeatureFlagInitializing: usernameMaxLengthInitializing', - 'numberFeatureFlagReconciling: usernameMaxLengthReconciling', - 'numberFeatureFlagValue: usernameMaxLengthValue', - ], - }, - ], }) -export class UsernameMaxLengthDirective { - private readonly hostDirective = inject(NumberFeatureFlagDirective); +export class UsernameMaxLengthFeatureFlagDirective extends FeatureFlagDirective implements OnChanges { + override _changeDetectorRef = inject(ChangeDetectorRef); + override _viewContainerRef = inject(ViewContainerRef); + override _thenTemplateRef = inject>>(TemplateRef); + + /** + * The expected value of this number feature flag, for which the `then` template should be rendered. + */ + @Input({ required: false }) usernameMaxLengthValue?: number; constructor() { - // Set the pre-configured flag key and default value - this.hostDirective.numberFeatureFlag = "usernameMaxLength"; - this.hostDirective.numberFeatureFlagDefault = 50; + super(); + + this._featureFlagKey = "usernameMaxLength"; + this._featureFlagDefault = 50; + + } + + override ngOnChanges() { + super.ngOnChanges(); + + this._featureFlagValue = this.usernameMaxLengthValue; + } + + /** + * The domain of the number feature flag. + */ + @Input({ required: false }) + set usernameMaxLengthDomain(domain: string | undefined) { + super.featureFlagDomain = domain; + } + + /** + * Update the component if the provider emits a ConfigurationChanged event. + * Set to false to prevent components from re-rendering when flag value changes + * are received by the associated provider. + * Defaults to true. + */ + @Input({ required: false }) + set usernameMaxLengthUpdateOnConfigurationChanged(enabled: boolean | undefined) { + this._updateOnConfigurationChanged = enabled ?? true; + } + + /** + * Update the component when the OpenFeature context changes. + * Set to false to prevent components from re-rendering when attributes which + * may be factors in flag evaluation change. + * Defaults to true. + */ + @Input({ required: false }) + set usernameMaxLengthUpdateOnContextChanged(enabled: boolean | undefined) { + this._updateOnContextChanged = enabled ?? true; + } + + /** + * Template to be displayed when the feature flag does not match value. + */ + @Input() + set usernameMaxLengthElse(tpl: TemplateRef>) { + this._elseTemplateRef = tpl; + } + + /** + * Template to be displayed when the provider is not ready. + */ + @Input() + set usernameMaxLengthInitializing(tpl: TemplateRef>) { + this._initializingTemplateRef = tpl; + } + + /** + * Template to be displayed when the provider is reconciling. + */ + @Input() + set usernameMaxLengthReconciling(tpl: TemplateRef>) { + this._reconcilingTemplateRef = tpl; } } + // ============================================================================ // EXPORTS // ============================================================================ @@ -640,9 +969,9 @@ export class UsernameMaxLengthDirective { * ``` */ export const GeneratedFeatureFlagDirectives = [ - DiscountPercentageDirective, - EnableFeatureADirective, - GreetingMessageDirective, - ThemeCustomizationDirective, - UsernameMaxLengthDirective, + DiscountPercentageFeatureFlagDirective, + EnableFeatureAFeatureFlagDirective, + GreetingMessageFeatureFlagDirective, + ThemeCustomizationFeatureFlagDirective, + UsernameMaxLengthFeatureFlagDirective, ] as const; diff --git a/internal/generators/angular/angular.go b/internal/generators/angular/angular.go index 6e57e50..1b3003b 100644 --- a/internal/generators/angular/angular.go +++ b/internal/generators/angular/angular.go @@ -38,24 +38,6 @@ func openFeatureType(t flagset.FlagType) string { } } -// sdkDirectiveType returns the corresponding Angular SDK directive name for a flag type. -func sdkDirectiveType(t flagset.FlagType) string { - switch t { - case flagset.IntType: - fallthrough - case flagset.FloatType: - return "numberFeatureFlag" - case flagset.BoolType: - return "booleanFeatureFlag" - case flagset.StringType: - return "stringFeatureFlag" - case flagset.ObjectType: - return "objectFeatureFlag" - default: - return "" - } -} - // sdkServiceMethod returns the corresponding FeatureFlagService method name for a flag type. func sdkServiceMethod(t flagset.FlagType) string { switch t { @@ -87,7 +69,6 @@ func toJSONString(value any) string { func (g *AngularGenerator) Generate(params *generators.Params[Params]) error { funcs := template.FuncMap{ "OpenFeatureType": openFeatureType, - "SdkDirectiveType": sdkDirectiveType, "SdkServiceMethod": sdkServiceMethod, "ToJSONString": toJSONString, } diff --git a/internal/generators/angular/angular.tmpl b/internal/generators/angular/angular.tmpl index 8849d32..f0766ff 100644 --- a/internal/generators/angular/angular.tmpl +++ b/internal/generators/angular/angular.tmpl @@ -4,25 +4,28 @@ * This file contains generated typesafe Angular services and directives * for feature flags defined in your OpenFeature flag manifest. * - * @openfeature/angular-sdk is required as a peer dependency. + * Requires @openfeature/angular-sdk >= 1.1.0. * - * @see https://openfeature.dev + * @see https://openfeature.dev/docs/reference/other-technologies/cli */ import { + ChangeDetectorRef, Directive, inject, Injectable, + Input, + OnChanges, + TemplateRef, + ViewContainerRef, } from '@angular/core'; import { AngularFlagEvaluationOptions, - BooleanFeatureFlagDirective, EvaluationDetails, + FeatureFlagDirective, + FeatureFlagDirectiveContext, FeatureFlagService, JsonValue, - NumberFeatureFlagDirective, - ObjectFeatureFlagDirective, - StringFeatureFlagDirective, } from '@openfeature/angular-sdk'; import { Observable } from 'rxjs'; @@ -113,97 +116,154 @@ export class GeneratedFeatureFlagService { // ============================================================================ {{ range .Flagset.Flags }} -{{- $type := .Type | OpenFeatureType -}} -{{- $directiveType := "" -}} -{{- $directivePrefix := "" -}} -{{- if eq $type "boolean" -}} - {{- $directiveType = "BooleanFeatureFlagDirective" -}} - {{- $directivePrefix = "booleanFeatureFlag" -}} -{{- else if eq $type "string" -}} - {{- $directiveType = "StringFeatureFlagDirective" -}} - {{- $directivePrefix = "stringFeatureFlag" -}} -{{- else if eq $type "number" -}} - {{- $directiveType = "NumberFeatureFlagDirective" -}} - {{- $directivePrefix = "numberFeatureFlag" -}} -{{- else -}} - {{- $directiveType = "ObjectFeatureFlagDirective" -}} - {{- $directivePrefix = "objectFeatureFlag" -}} -{{- end }} +{{ $type := .Type | OpenFeatureType }} /** * Structural directive for the `{{ .Key }}` feature flag. * - * {{ if .Description }}{{ .Description }}{{ else }}Feature flag directive.{{ end }} + * {{ if .Description }}{{ .Description }}{{ end }} * - * This directive wraps the `{{ $directiveType }}` from @openfeature/angular-sdk - * with pre-configured flag key and default value. + * This directive extends `FeatureFlagDirective` from @openfeature/angular-sdk + * with a pre-configured flag key and default value. * * **Details:** * - Flag key: `{{ .Key }}` * - Type: `{{ if eq $type "object" }}JsonValue{{ else }}{{ $type }}{{ end }}` * - Default value: `{{ if eq $type "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue }}{{ end }}` * + * * @example - * Basic usage: + * Explicit `ng-template` (no `*`), bind inputs directly * ```html - *
- * Content shown when flag is {{ if eq $type "boolean" }}enabled{{ else }}matched{{ end }}. - *
+ * + *
Content shown when flag is {{ if eq $type "boolean" }}enabled{{ else }}matched{{ end }}.
+ *
+ * + * Content shown when flag is {{ if eq $type "boolean" }}disabled{{ else }}not matched{{ end }}. + * * ``` * * @example - * With else template: + * Microsyntax `*` form, start with `let` * ```html - *
+ *
* Content shown when flag is {{ if eq $type "boolean" }}enabled{{ else }}matched{{ end }}. *
- * - * Content shown when flag is {{ if eq $type "boolean" }}disabled{{ else }}not matched{{ end }}. - * * ``` * * @example - * With all options: + * Simple `*` usage (no else/initializing/reconciling) * ```html - *
- * Flag value: {{ "{{" }} value {{ "}}" }} + *
+ * Content shown when flag is {{ if eq $type "boolean" }}enabled{{ else }}matched{{ end }}. *
* ``` + * + * @remarks + * Note: Angular's microsyntax parser requires the first segment to be a primary + * expression or a `let` declaration. Because the flag key is preconfigured, start with + * `let v` or `let details = evaluationDetails` (or `let _` if you do not need the value) + * before any `else`/`initializing`/`reconciling`/`default` segments. + * This is an Angular microsyntax parsing constraint, not a directive limitation. + * `*{{ .Key | ToCamel }}="else elseTemplate"` will not parse because `else` is a + * secondary segment. If you do not need the value, use `let _`, or use the + * explicit `ng-template` form above. */ @Directive({ selector: '[{{ .Key | ToCamel }}]', standalone: true, - hostDirectives: [ - { - directive: {{ $directiveType }}, - inputs: [ - '{{ $directivePrefix }}: {{ .Key | ToCamel }}', - '{{ $directivePrefix }}Default: {{ .Key | ToCamel }}Default', - '{{ $directivePrefix }}Domain: {{ .Key | ToCamel }}Domain', - '{{ $directivePrefix }}UpdateOnConfigurationChanged: {{ .Key | ToCamel }}UpdateOnConfigurationChanged', - '{{ $directivePrefix }}UpdateOnContextChanged: {{ .Key | ToCamel }}UpdateOnContextChanged', - '{{ $directivePrefix }}Else: {{ .Key | ToCamel }}Else', - '{{ $directivePrefix }}Initializing: {{ .Key | ToCamel }}Initializing', - '{{ $directivePrefix }}Reconciling: {{ .Key | ToCamel }}Reconciling', +}) +export class {{ .Key | ToPascal }}FeatureFlagDirective extends FeatureFlagDirective<{{ if eq $type "object" }}JsonValue{{ else }}{{ $type }}{{ end }}> implements OnChanges { + override _changeDetectorRef = inject(ChangeDetectorRef); + override _viewContainerRef = inject(ViewContainerRef); + override _thenTemplateRef = inject>>(TemplateRef); + {{- if ne $type "boolean" }} - '{{ $directivePrefix }}Value: {{ .Key | ToCamel }}Value', + + /** + * The expected value of this {{ $type }} feature flag, for which the `then` template should be rendered. + */ + @Input({ required: false }) {{ .Key | ToCamel }}Value?: {{ if eq $type "object" }}JsonValue{{ else }}{{ $type }}{{ end }}; {{- end }} - ], - }, - ], -}) -export class {{ .Key | ToPascal }}Directive { - private readonly hostDirective = inject({{ $directiveType }}); constructor() { - // Set the pre-configured flag key and default value - this.hostDirective.{{ $directivePrefix }} = {{ .Key | Quote }}; - this.hostDirective.{{ $directivePrefix }}Default = {{ if eq $type "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue | QuoteString }}{{ end }}; + super(); + + this._featureFlagKey = {{ .Key | ToCamel | QuoteString }}; + this._featureFlagDefault = {{ if eq $type "object"}}{{ .DefaultValue | ToJSONString }}{{ else }}{{ .DefaultValue | QuoteString }}{{ end }}; +{{ if eq $type "boolean" }} + this._featureFlagValue = true; +{{ end }} + } + +{{- if ne $type "boolean" }} + + override ngOnChanges() { + super.ngOnChanges(); + + this._featureFlagValue = this.{{ .Key | ToCamel }}Value; + } +{{- end }} + + /** + * The domain of the {{ $type }} feature flag. + */ + @Input({ required: false }) + set {{ .Key | ToCamel }}Domain(domain: string | undefined) { + super.featureFlagDomain = domain; + } + + /** + * Update the component if the provider emits a ConfigurationChanged event. + * Set to false to prevent components from re-rendering when flag value changes + * are received by the associated provider. + * Defaults to true. + */ + @Input({ required: false }) + set {{ .Key | ToCamel }}UpdateOnConfigurationChanged(enabled: boolean | undefined) { + this._updateOnConfigurationChanged = enabled ?? true; + } + + /** + * Update the component when the OpenFeature context changes. + * Set to false to prevent components from re-rendering when attributes which + * may be factors in flag evaluation change. + * Defaults to true. + */ + @Input({ required: false }) + set {{ .Key | ToCamel }}UpdateOnContextChanged(enabled: boolean | undefined) { + this._updateOnContextChanged = enabled ?? true; + } + + /** + * Template to be displayed when the feature flag {{ if eq $type "boolean" }}is false{{ else }}does not match value{{ end }}. + */ + @Input() + set {{ .Key | ToCamel }}Else(tpl: TemplateRef>) { + this._elseTemplateRef = tpl; + } + + /** + * Template to be displayed when the provider is not ready. + */ + @Input() + set {{ .Key | ToCamel }}Initializing(tpl: TemplateRef>) { + this._initializingTemplateRef = tpl; + } + + /** + * Template to be displayed when the provider is reconciling. + */ + @Input() + set {{ .Key | ToCamel }}Reconciling(tpl: TemplateRef>) { + this._reconcilingTemplateRef = tpl; } } + {{ end }} // ============================================================================ @@ -226,6 +286,6 @@ export class {{ .Key | ToPascal }}Directive { */ export const GeneratedFeatureFlagDirectives = [ {{- range .Flagset.Flags }} - {{ .Key | ToPascal }}Directive, + {{ .Key | ToPascal }}FeatureFlagDirective, {{- end }} ] as const; diff --git a/test/angular-integration/package-lock.json b/test/angular-integration/package-lock.json index d172df0..b86e837 100644 --- a/test/angular-integration/package-lock.json +++ b/test/angular-integration/package-lock.json @@ -13,8 +13,8 @@ "@angular/core": "^19.0.0", "@angular/platform-browser": "^19.0.0", "@angular/platform-browser-dynamic": "^19.0.0", - "@openfeature/angular-sdk": "^1.0.0", - "@openfeature/web-sdk": "^1.7.2", + "@openfeature/angular-sdk": "^1.1.0", + "@openfeature/web-sdk": "^1.7.3", "rxjs": "^7.8.0", "tslib": "^2.6.0", "uuid": "^11.0.0", @@ -1198,9 +1198,9 @@ } }, "node_modules/@openfeature/angular-sdk": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@openfeature/angular-sdk/-/angular-sdk-1.0.0.tgz", - "integrity": "sha512-WTw3dvOkRrYO4HTgOY1J5PYx5ufx7sNUzAFgU33/p402Ez6IZPeKc+EOtBh/uNEBUMopKwE4E9ero2DIKpTwGA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@openfeature/angular-sdk/-/angular-sdk-1.1.0.tgz", + "integrity": "sha512-CgPH7zDhHhTmt8qsB+5taHRL1yzN0yhL+gjijJVdsv3UJSmsCJtD0Xs9FpBJ9Wd/rT2yA0K78BwFriivjodWBw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.3.0" @@ -1212,19 +1212,19 @@ } }, "node_modules/@openfeature/core": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@openfeature/core/-/core-1.9.1.tgz", - "integrity": "sha512-YySPtH4s/rKKnHRU0xyFGrqMU8XA+OIPNWDrlEFxE6DCVWCIrxE5YpiB94YD2jMFn6SSdA0cwQ8vLkCkl8lm8A==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@openfeature/core/-/core-1.9.2.tgz", + "integrity": "sha512-0lX0xYTflLrjiYNlareYmdV98xEddR5+PhcuoGvH+BMIqpZ2icAC7us9Uv86KRVqofXvpAUwpP32wgqmtUFs8Q==", "license": "Apache-2.0", "peer": true }, "node_modules/@openfeature/web-sdk": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@openfeature/web-sdk/-/web-sdk-1.7.2.tgz", - "integrity": "sha512-8QwhoxVNN2bFFkpWjbCyHCdkVjt/UTVn0o+OwcUUQoZnvPn46Oo1BxJQxUTibl/D/dAM/YQhxmg7ep7gYRxX4g==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@openfeature/web-sdk/-/web-sdk-1.7.3.tgz", + "integrity": "sha512-WrerPh3KwtpyNGHtWfWgbBnIv3iyOimnsljXJnx2UMcFzQNmj3xgsUZRvE/gAb+BTgz22+kvks1EXA/DJRmhpg==", "license": "Apache-2.0", "peerDependencies": { - "@openfeature/core": "^1.9.0" + "@openfeature/core": "^1.9.2" } }, "node_modules/@rollup/rollup-android-arm-eabi": { diff --git a/test/angular-integration/package.json b/test/angular-integration/package.json index c29fdc4..1d95dff 100644 --- a/test/angular-integration/package.json +++ b/test/angular-integration/package.json @@ -13,8 +13,8 @@ "@angular/core": "^19.0.0", "@angular/platform-browser": "^19.0.0", "@angular/platform-browser-dynamic": "^19.0.0", - "@openfeature/angular-sdk": "^1.0.0", - "@openfeature/web-sdk": "^1.7.2", + "@openfeature/angular-sdk": "^1.1.0", + "@openfeature/web-sdk": "^1.7.3", "rxjs": "^7.8.0", "tslib": "^2.6.0", "uuid": "^11.0.0", diff --git a/test/angular-integration/specs/compilation.spec.ts b/test/angular-integration/specs/compilation.spec.ts index 8664b6d..9c04637 100644 --- a/test/angular-integration/specs/compilation.spec.ts +++ b/test/angular-integration/specs/compilation.spec.ts @@ -32,18 +32,18 @@ describe("Compilation Tests", () => { it("should import all generated directives", async () => { const { - DiscountPercentageDirective, - EnableFeatureADirective, - GreetingMessageDirective, - ThemeCustomizationDirective, - UsernameMaxLengthDirective, + DiscountPercentageFeatureFlagDirective, + EnableFeatureAFeatureFlagDirective, + GreetingMessageFeatureFlagDirective, + ThemeCustomizationFeatureFlagDirective, + UsernameMaxLengthFeatureFlagDirective, } = await import("../generated/openfeature.generated"); - expect(DiscountPercentageDirective).toBeDefined(); - expect(EnableFeatureADirective).toBeDefined(); - expect(GreetingMessageDirective).toBeDefined(); - expect(ThemeCustomizationDirective).toBeDefined(); - expect(UsernameMaxLengthDirective).toBeDefined(); + expect(DiscountPercentageFeatureFlagDirective).toBeDefined(); + expect(EnableFeatureAFeatureFlagDirective).toBeDefined(); + expect(GreetingMessageFeatureFlagDirective).toBeDefined(); + expect(ThemeCustomizationFeatureFlagDirective).toBeDefined(); + expect(UsernameMaxLengthFeatureFlagDirective).toBeDefined(); }); it("should import GeneratedFeatureFlagDirectives array", async () => { diff --git a/test/angular-integration/specs/directives.spec.ts b/test/angular-integration/specs/directives.spec.ts index ee0ad92..cd6ef5c 100644 --- a/test/angular-integration/specs/directives.spec.ts +++ b/test/angular-integration/specs/directives.spec.ts @@ -6,11 +6,11 @@ import { OpenFeature, InMemoryProvider } from "@openfeature/web-sdk"; import { v4 as uuid } from "uuid"; import { GeneratedFeatureFlagDirectives, - EnableFeatureADirective, - GreetingMessageDirective, - DiscountPercentageDirective, - UsernameMaxLengthDirective, - ThemeCustomizationDirective, + EnableFeatureAFeatureFlagDirective, + GreetingMessageFeatureFlagDirective, + DiscountPercentageFeatureFlagDirective, + UsernameMaxLengthFeatureFlagDirective, + ThemeCustomizationFeatureFlagDirective, } from "../generated/openfeature.generated"; import type { JsonValue } from "@openfeature/angular-sdk"; @@ -18,7 +18,7 @@ import type { JsonValue } from "@openfeature/angular-sdk"; @Component({ selector: "test-boolean", standalone: true, - imports: [EnableFeatureADirective], + imports: [EnableFeatureAFeatureFlagDirective], template: `
@@ -31,11 +31,27 @@ class TestBooleanComponent { @Input() domain?: string; } +// Test component for boolean directive with simple microsyntax +@Component({ + selector: "test-boolean-simple", + standalone: true, + imports: [EnableFeatureAFeatureFlagDirective], + template: ` +
+
+ Feature A is enabled +
+
+ `, +}) +class TestBooleanSimpleComponent { +} + // Test component for string directive with domain input @Component({ selector: "test-string", standalone: true, - imports: [GreetingMessageDirective], + imports: [GreetingMessageFeatureFlagDirective], template: `
@@ -52,7 +68,7 @@ class TestStringComponent { @Component({ selector: "test-number", standalone: true, - imports: [DiscountPercentageDirective], + imports: [DiscountPercentageFeatureFlagDirective], template: `
@@ -69,7 +85,7 @@ class TestNumberComponent { @Component({ selector: "test-username", standalone: true, - imports: [UsernameMaxLengthDirective], + imports: [UsernameMaxLengthFeatureFlagDirective], template: `
@@ -86,7 +102,7 @@ class TestUsernameComponent { @Component({ selector: "test-object", standalone: true, - imports: [ThemeCustomizationDirective, JsonPipe], + imports: [ThemeCustomizationFeatureFlagDirective, JsonPipe], template: `
@@ -140,7 +156,7 @@ class TestAllDirectivesComponent { @Component({ selector: "test-boolean-else", standalone: true, - imports: [EnableFeatureADirective], + imports: [EnableFeatureAFeatureFlagDirective], template: `
{ }); describe("EnableFeatureADirective (boolean)", () => { + it("should render content with simple microsyntax", async () => { + provider = new InMemoryProvider({ + enableFeatureA: { + variants: { on: true }, + defaultVariant: "on", + disabled: false, + }, + }); + await OpenFeature.setProviderAndWait(provider); + + const fixture = TestBed.configureTestingModule({ + imports: [TestBooleanSimpleComponent], + }).createComponent(TestBooleanSimpleComponent); + fixture.detectChanges(); + await fixture.whenStable(); + + const content = fixture.nativeElement.querySelector(".flag-content"); + expect(content).not.toBeNull(); + expect(content.textContent.trim()).toBe("Feature A is enabled"); + }); + it("should render content when flag is true", async () => { provider = new InMemoryProvider({ enableFeatureA: { diff --git a/test/angular-integration/specs/ngtsc-compile.spec.ts b/test/angular-integration/specs/ngtsc-compile.spec.ts new file mode 100644 index 0000000..6cf90d8 --- /dev/null +++ b/test/angular-integration/specs/ngtsc-compile.spec.ts @@ -0,0 +1,209 @@ +import { describe, it, expect } from "vitest"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import ts from "typescript"; +import { fileURLToPath } from "node:url"; +import { + NgtscProgram, + createCompilerHost, + readConfiguration, +} from "@angular/compiler-cli"; + +describe("ngtsc compilation", () => { + const compileAndAssert = (componentSource: string) => { + const baseDir = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "..", + ); + const tmpDir = fs.mkdtempSync(path.join(baseDir, ".tmp-ngtsc-")); + const componentPath = path.join(tmpDir, "component.ts"); + const tsconfigPath = path.join(tmpDir, "tsconfig.json"); + + try { + const tsconfig = { + compilerOptions: { + target: "ES2022", + module: "ESNext", + moduleResolution: "bundler", + lib: ["ES2022", "DOM"], + strict: true, + esModuleInterop: true, + experimentalDecorators: true, + emitDecoratorMetadata: true, + useDefineForClassFields: false, + skipLibCheck: true, + baseUrl: baseDir, + paths: { + "@generated/*": ["generated/*"], + }, + types: ["node"], + }, + angularCompilerOptions: { + enableI18nLegacyMessageIdFormat: false, + strictInjectionParameters: true, + strictInputAccessModifiers: true, + strictTemplates: true, + }, + files: [componentPath], + }; + + fs.writeFileSync(componentPath, componentSource, "utf8"); + fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2), "utf8"); + + const parsed = readConfiguration(tsconfigPath); + + const host = createCompilerHost({ options: parsed.options }); + const ngProgram = new NgtscProgram( + parsed.rootNames, + parsed.options, + host, + ); + + const diagnostics = [ + ...parsed.errors, + ...ngProgram.getTsOptionDiagnostics(), + ...ngProgram.getTsSyntacticDiagnostics(), + ...ngProgram.getTsSemanticDiagnostics(), + ...ngProgram.getNgOptionDiagnostics(), + ...ngProgram.getNgStructuralDiagnostics(), + ...ngProgram.getNgSemanticDiagnostics(), + ]; + + const errors = diagnostics.filter( + (diag) => diag.category === ts.DiagnosticCategory.Error, + ); + + if (errors.length > 0) { + const formatted = ts.formatDiagnosticsWithColorAndContext(errors, { + getCanonicalFileName: (fileName) => fileName, + getCurrentDirectory: () => tmpDir, + getNewLine: () => "\n", + }); + throw new Error(`ngtsc reported errors:\n${formatted}`); + } + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }; + + it("should compile structural directive usage without requiring a default input", () => { + const componentSource = ` +import { Component } from "@angular/core"; +import { EnableFeatureAFeatureFlagDirective } from "@generated/openfeature.generated"; + +@Component({ + standalone: true, + selector: "test-host", + imports: [EnableFeatureAFeatureFlagDirective], + template: \` + +
Feature A enabled
+
+ \`, +}) +export class RequiredInputHostComponent {} +`; + compileAndAssert(componentSource); + }); + + it("should compile structural directive usage with else template binding", () => { + const componentSource = ` +import { Component } from "@angular/core"; +import { EnableFeatureAFeatureFlagDirective } from "@generated/openfeature.generated"; + +@Component({ + standalone: true, + selector: "test-host", + imports: [EnableFeatureAFeatureFlagDirective], + template: \` + Else + + +
Feature A enabled
+
+ \`, +}) +export class ElseTemplateHostComponent {} +`; + compileAndAssert(componentSource); + }); + + it("should compile simple microsyntax usage without else templates", () => { + const componentSource = ` +import { Component } from "@angular/core"; +import { EnableFeatureAFeatureFlagDirective } from "@generated/openfeature.generated"; + +@Component({ + standalone: true, + selector: "test-host", + imports: [EnableFeatureAFeatureFlagDirective], + template: \` +
Feature A enabled
+ \`, +}) +export class SimpleMicrosyntaxHostComponent {} +`; + compileAndAssert(componentSource); + }); + + it("should compile structural directive usage with templates and all options", () => { + const componentSource = ` +import { Component } from "@angular/core"; +import { GreetingMessageFeatureFlagDirective } from "@generated/openfeature.generated"; + +@Component({ + standalone: true, + selector: "test-host", + imports: [GreetingMessageFeatureFlagDirective], + template: \` + Else + Init + Reconciling + +
+ Flag value: {{ value }} +
+ \`, +}) +export class AllOptionsHostComponent { + expectedValue = "hello"; +} +`; + compileAndAssert(componentSource); + }); + + it("should compile structural directive usage on a custom component with inputs", () => { + const componentSource = ` +import { Component, Input } from "@angular/core"; +import { GreetingMessageFeatureFlagDirective } from "@generated/openfeature.generated"; + +@Component({ + standalone: true, + selector: "custom-widget", + template: "{{ label }}", +}) +export class CustomWidgetComponent { + @Input() label = ""; +} + +@Component({ + standalone: true, + selector: "test-host", + imports: [CustomWidgetComponent, GreetingMessageFeatureFlagDirective], + template: + \` + \`, +}) +export class CustomComponentHostComponent { + expectedValue = "hello"; +} +`; + compileAndAssert(componentSource); + }); +});