How would you write an Angular `*appHasRole` structural directive that renders its content only when the current user holds a given role?
answer
- inject what the asterisk created
- a container you add views to
- render once, not on every run
- else is just another prefixed input
basics
~10 sInject TemplateRef and ViewContainerRef, read a role input named after the selector, and call createEmbeddedView when the role is held or clear when it is not, guarding so the view is never created twice.
solid answer
~40 sCreate a directive with selector `[appHasRole]` and an input also called `appHasRole`, so `*appHasRole="'admin'"` binds the role. Inject `TemplateRef` for the content and `ViewContainerRef` for where it goes, plus the service that knows the user's roles. Whenever the role or the user's roles change, decide which template should be showing: if it differs from what is currently rendered, call `viewContainerRef.clear()` and then `createEmbeddedView()` for the new one. That guard matters: calling `createEmbeddedView()` on every update stamps out duplicates. An `else` template is just another prefixed input, `appHasRoleElse`, which the microsyntax `*appHasRole="'admin'; else readOnly"` fills. With signal inputs, an `effect()` in the constructor does the re-evaluation. Remember the directive is only a UI convenience; the server must still enforce the permission.
code
ts · 32 linesimport {
Directive, Injectable, TemplateRef, ViewContainerRef,
effect, inject, input, signal,
} from '@angular/core';
@Injectable({ providedIn: 'root' })
export class Session {
readonly roles = signal<string[]>([]);
}
@Directive({ selector: '[appHasRole]' })
export class HasRoleDirective {
private readonly tpl = inject(TemplateRef);
private readonly vcr = inject(ViewContainerRef);
private readonly session = inject(Session);
readonly appHasRole = input.required<string>();
readonly appHasRoleElse = input<TemplateRef<unknown> | null>(null);
private rendered: TemplateRef<unknown> | null = null;
constructor() {
effect(() => {
const allowed = this.session.roles().includes(this.appHasRole());
const next = allowed ? this.tpl : this.appHasRoleElse();
if (next === this.rendered) return; // already showing the right view
this.vcr.clear();
if (next) this.vcr.createEmbeddedView(next);
this.rendered = next;
});
}
}go deeper
Recall that the directive injects TemplateRef and ViewContainerRef and uses createEmbeddedView to show content and clear to remove it.
Explain why the input must be named after the selector, how else maps to appHasRoleElse, and why the view must not be created twice.
Show the idempotent update logic, the choice between effect and input setters, and that hiding UI is never authorisation.
Discuss where permission rendering belongs: one shared directive and session service keep the policy consistent, while the backend remains the enforcement point.
## The goal A permission directive lets a template say "render this only for admins" in one attribute: `<button *appHasRole="'admin'">Delete</button>`. It is a good example of when to write a **custom structural directive** rather than use `@if`: the rule (look up the current user's roles, react when they change) is reusable across the whole application, and the call site stays declarative. ## The building blocks A structural directive is an ordinary `@Directive` class (standalone by default since v19) that works with the `<ng-template>` produced by the asterisk: - **`TemplateRef`**: injected with `inject(TemplateRef)`, it is a handle to the content the asterisk wrapped, here the `<button>`. - **`ViewContainerRef`**: injected with `inject(ViewContainerRef)`, it is the insertion point at the template's location. Its main methods are `createEmbeddedView(templateRef, context?)`, which instantiates the template and inserts it; `clear()`, which destroys every view in the container; `remove(index?)`; and the `length` getter. - **An input named after the selector.** The microsyntax binds the naked expression to an input whose name equals the selector, so selector `[appHasRole]` needs an input `appHasRole`. Other keys are prefixed: `else readOnly` binds to `appHasRoleElse`. If the directive is used without the asterisk, as a plain attribute on a normal element, there is no `<ng-template>`, so injecting `TemplateRef` fails with an NG0201 no-provider error. That error is a useful hint during reviews. ## Writing it The directive below uses signal inputs and an `effect()`, the current style. The steps it follows: 1. Read the required role and the optional else template from inputs. 2. Ask a root-provided session service for the user's roles, exposed as a signal so changes re-run the effect. 3. Work out which template should be rendered: the main one, the else one, or none. 4. If that is the template already on screen, do nothing. Otherwise `clear()` the container and create the new view. Step 4 is the part weak answers miss. An effect re-runs whenever any signal it read changes, just as an `@Input()` setter in the older style runs on every new value. If it called `createEmbeddedView()` unconditionally, every roles update would add another copy of the button. Tracking what is currently rendered makes the operation idempotent. ## Using it ```html <button *appHasRole="'admin'; else readOnly">Delete project</button> <ng-template #readOnly><span>Read-only access</span></ng-template> ``` The compiler expands this to `<ng-template [appHasRole]="'admin'" [appHasRoleElse]="readOnly">` wrapping the button. The component that uses it lists `HasRoleDirective` in its `imports`. ## Testing it A structural directive is easiest to test through a small host component, because it only makes sense inside a template: 1. Declare a test host whose template uses `*appHasRole="'admin'; else readOnly"` and imports the directive. 2. Provide a stub session whose `roles` signal the test controls. 3. Render the host, set the roles to include or exclude `admin`, let change detection run, and assert which content is in the DOM. 4. Change the roles back and forth several times and assert the button appears **exactly once**, which is the regression test for the duplicate-view bug. Testing the directive class alone, without a template, misses the point: the behaviour under test is what the view container contains. ## Design choices worth mentioning | Choice | Option A | Option B | | --- | --- | --- | | Reacting to changes | `effect()` over signal inputs and a signal of roles | `@Input()` setters plus a subscription to a roles observable | | Else content | An `appHasRoleElse` input taking a `TemplateRef` | A separate `*appLacksRole` directive | | Role source | A service injected with `inject()` | Passing roles in as another input | Either reaction style works; the signal version needs no manual unsubscription because the effect is tied to the directive's lifetime. Whichever you pick, keep these points in mind: - **It is not security.** Hiding a button does not stop a request; the backend must authorise the action. The directive only avoids showing controls the user cannot use. - **Removal destroys state.** When the role is lost, `clear()` destroys the view and any components inside it. - **Context is optional.** A permission directive usually passes no context object; if it did, it would add typing guards for the template type checker.
- What goes wrong if the directive calls `createEmbeddedView()` every time its role input or the user's roles change?Each call inserts another embedded view into the same container, so the button appears twice, three times, and so on. The fix is to track what is currently rendered and only `clear()` and create when the decision actually changes.
- Why does `[appHasRole]="'admin'"` on a plain `<button>` throw at runtime while `*appHasRole` works?Without the asterisk there is no `<ng-template>`, so the node has no template for `TemplateRef` to point at. `inject(TemplateRef)` then fails with an NG0201 no-provider error. The long form works if you write the `<ng-template [appHasRole]>` wrapper yourself.
- Does hiding the Delete button with `*appHasRole` protect the delete endpoint?No. The directive only controls what the template renders; anyone can still send the request. The server must check the permission on every call, and the directive is a usability layer on top of that.
saying these in an interview costs you the question
- Calling createEmbeddedView on every input change without clearing or checking the current view.
- Naming the input role instead of appHasRole, so the microsyntax expression has no input to reach.
- Hiding the element with a style binding and calling that a structural directive.
- Treating the directive as the access-control mechanism instead of server-side checks.
- Expecting the else keyword to work without an appHasRoleElse input on the directive.