fix(docs-infra): render signal inputs and outputs using initializer s… by MeAkib · Pull Request #70566 · angular/angular · GitHub
Skip to content

fix(docs-infra): render signal inputs and outputs using initializer s… - #70566

Open
MeAkib wants to merge 1 commit into
angular:mainfrom
MeAkib:render-signal-inputs-using-initializer-syntax
Open

fix(docs-infra): render signal inputs and outputs using initializer s…#70566
MeAkib wants to merge 1 commit into
angular:mainfrom
MeAkib:render-signal-inputs-using-initializer-syntax

Conversation

@MeAkib

@MeAkib MeAkib commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

…yntax

The API reference rendered signal-based bindings with a hybrid line mixing decorator syntax and the signal type, e.g. readonly @Input('formField') field: InputSignal<Field<T>>; for a member declared as readonly field = input.required<Field<T>>({alias: 'formField'});. Signal bindings are never declared with @Input() / @Output(), so the rendered code did not match how the source is authored.

Signal inputs and outputs are now rendered in the initializer form the source uses:

readonly field = input.required<Field>({alias: 'formField'});
readonly nameChange = output({alias: 'name'});

PropertyEntry carries no isSignal flag, so the discriminator is the resolved property type: InputSignal / InputSignalWithTransform for inputs and OutputEmitterRef for outputs. outputFromObservable() is deliberately excluded — it is typed as the broader OutputRef and is not declared with an output() initializer. Decorator-based inputs and outputs continue to render with @Input(...) / @Output(...).

What is the current behavior?

https://angular.dev/api/forms/signals/FormRoot
https://angular.dev/api/forms/signals/FormField

What is the new behavior?

image image

@pullapprove
pullapprove Bot requested a review from josephperrott September 4, 2026 05:42
@angular-robot angular-robot Bot added the area: docs-infra Angular.dev application and infrastructure label Sep 4, 2026
@ngbot ngbot Bot added this to the Backlog milestone Sep 4, 2026

@JeanMeche JeanMeche left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great, but there is one missing piece: model() bindings!

Currently, ModelSignal is completely omitted from the new signal prefix checks. Since model() is inherently both an input and an output, the docs extractor applies both MemberTags.Input and MemberTags.Output tags to it. Because it falls through the new checks, the docs generator will incorrectly render it with both legacy decorators like this:

readonly @Input() @Output('myModelChange') myModel: ModelSignal<string>;

To fix this, we need to explicitly detect ModelSignal types and strip both the @Input and @Output tags when generating the code line.

You could add a check for ModelSignal and then handle it similarly inside getPropertyCodeLine:

/** Type-string prefix that identifies a signal-based model (`model()` / `model.required()`). */
const SIGNAL_MODEL_TYPE_PREFIXES = ['ModelSignal<'];

export function isSignalModel(member: PropertyEntry): boolean {
  return (
    (member.memberTags.includes(MemberTags.Input) || member.memberTags.includes(MemberTags.Output)) &&
    SIGNAL_MODEL_TYPE_PREFIXES.some((prefix) => member.type?.startsWith(prefix))
  );
}

// ... inside getPropertyCodeLine(member: PropertyEntry): string
export function getPropertyCodeLine(member: PropertyEntry): string {
  if (isSignalModel(member)) {
    // A model is both an input and an output. We need to strip both tags so they aren't rendered as decorators.
    const memberWithoutTags = {
      ...member,
      memberTags: member.memberTags.filter(t => t !== MemberTags.Input && t !== MemberTags.Output)
    };
    return getSignalBindingCodeLine(
      memberWithoutTags,
      MemberTags.Input, // getSignalBindingCodeLine expects a tag to filter out, but we've already done it
      member.isRequiredInput ? 'model.required' : 'model',
      member.inputAlias,
    );
  }

  if (isSignalInput(member)) {
    // ...

@MeAkib
MeAkib force-pushed the render-signal-inputs-using-initializer-syntax branch from 679c6a1 to 50ae3cc Compare September 5, 2026 02:09
…alizer syntax

The API reference rendered signal-based bindings with a hybrid line mixing decorator
syntax and the signal type, e.g. `readonly @input('formField') field: InputSignal<Field<T>>;`
for a member declared as `readonly field = input.required<Field<T>>({alias: 'formField'});`.
Signal bindings are never declared with `@Input()` / `@Output()`, so the rendered code did
not match how the source is authored.

Signal inputs, outputs and models are now rendered in the initializer form the source uses:

  readonly field = input.required<Field<T>>({alias: 'formField'});
  readonly nameChange = output<string>({alias: 'name'});
  readonly value = model<string>();

`PropertyEntry` carries no `isSignal` flag, so the discriminator is the resolved property
type: `InputSignal` / `InputSignalWithTransform` for inputs, `OutputEmitterRef` for outputs
and `ModelSignal` for models. A model is tagged as both an input and an output, so both tags
are stripped and only its input alias is passed to `model()` — the output alias is always
derived from it. `outputFromObservable()` is deliberately excluded: it is typed as the
broader `OutputRef` and is not declared with an `output()` initializer. Decorator-based
inputs and outputs continue to render with `@Input(...)` / `@Output(...)`.
@MeAkib
MeAkib force-pushed the render-signal-inputs-using-initializer-syntax branch from 50ae3cc to 206248b Compare September 5, 2026 02:10
@MeAkib
MeAkib requested a review from JeanMeche September 5, 2026 03:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: docs-infra Angular.dev application and infrastructure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants