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);
+ });
+});