Skip to content

feat(angular): support setting boolean props by attribute presence - #31442

Open
thetaPC wants to merge 4 commits into
feature-9.1from
FW-7637
Open

thetaPC wants to merge 4 commits into
feature-9.1from
FW-7637

Conversation

@thetaPC

@thetaPC thetaPC commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Issue number: resolves #30822


What is the current behavior?

Boolean props have to be bound explicitly. A bare attribute resolves to the empty string, so an editor reports TS2322: Type 'string' is not assignable to type 'boolean' on markup like this:

<ion-item button detail></ion-item>

It compiles and works at runtime, because Stencil coerces boolean attributes on the element. The problem is what the Angular compiler accepts.

What is the new behavior?

Boolean props can now be set by attribute presence:

<ion-item button detail></ion-item>

<!-- rather than -->
<ion-item [button]="true" [detail]="true"></ion-item>
  • Upgrades @stencil/angular-output-target to 1.5.0 and enables its new booleanAttributes option on both Angular output targets. That declares an input transform on 144 boolean props across 57 components in the lazy output, and 45 components in the standalone output.
  • Applies the same transform by hand to the 17 wrappers that are excluded from generation, covering a further 78 boolean inputs. ion-modal and ion-popover alone account for 19. Without this, <ion-checkbox checked> and <ion-modal handle> would still be rejected.
  • Property bindings are unchanged, and so is runtime behavior.
  • null and undefined are passed through rather than coerced to false, so props that treat them as a third state keep working. ion-item detail, ion-modal handle and ion-content forceOverscroll all resolve undefined to a mode-specific default.
  • 21 of the 69 changed files are hand-edited. The rest are regenerated or copied in by core's build.

Does this introduce a breaking change?

  • Yes
  • No

Other information

Dev build: 9.0.4-dev.11789496656.1690cf55

The dependency bump is the fix rather than an unrelated update. The generated proxies are overwritten on every core build, so this could not be solved in this repo. The change was made upstream in stenciljs/output-targets#842 and released in 1.5.0.

The hand-written wrappers use one mixed input array, with the plain names derived for ProxyCmp:

const CHECKBOX_INPUTS = [{ name: 'checked', transform: nullableBooleanAttribute }, 'color', ...];
const CHECKBOX_PROXY_INPUTS = CHECKBOX_INPUTS.map((input) => (typeof input === 'string' ? input : input.name));

A helper that built the list instead fails with NG1010: Failed to resolve @Directive.inputs to an array, since Angular has to resolve the metadata statically. common/utils/boolean-attribute.ts duplicates the copied helper for the same reason proxy.ts duplicates ProxyCmp; refer to the TODO at the top of proxy.ts.

Verified against packages/angular/test/apps/ng18 with strictTemplates enabled, consuming the built package:

  • <ion-item button detail> compiles (generated wrapper).
  • <ion-checkbox checked indeterminate disabled>, <ion-toggle checked enableOnOffLabels>, <ion-input clearInput counter required readonly> and <ion-modal isOpen handle keepContentsMounted showBackdrop> compile (hand-written wrappers).
  • <ion-modal [handle]="42"> fails with Type '42' is not assignable to type 'string | boolean | null | undefined', which shows type checking genuinely engages rather than silently passing.

@vercel

vercel Bot commented Sep 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
ionic-framework Ready Ready Preview Sep 15, 2026 6:06pm UTC

Request Review

@github-actions github-actions Bot added package: core @ionic/core package package: angular @ionic/angular package labels Sep 11, 2026
@thetaPC
thetaPC changed the base branch from main to feature-9.1 September 14, 2026 17:44
@thetaPC
thetaPC marked this pull request as ready for review September 14, 2026 17:58
@thetaPC
thetaPC requested a review from a team as a code owner September 14, 2026 17:58

@OS-jacobbell OS-jacobbell left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Everything looks good for the auto-generated component wrappers!

Some components are excluded from auto generation, e.g. standalone ion-checkbox. See core/stencil.config.ts for the full list of excluded components wrappers, and packages/angular/src for their manual implementations. The nullableBooleanAttribute will need to be applied to their inputs directly.

@OS-jacobbell OS-jacobbell left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM!

@ShaneK ShaneK 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.

Looks really solid overall, and the sweep across both the generated and hand-written wrappers is thorough.

The main thing is that I think this is a breaking change. These inputs had no type at all before, so narrowing them means bindings like [disabled]="items.length" stop compiling under strictTemplates, and it's the same on Angular 18 through 22. The test cases expect ngAcceptInputType to come out unknown, which would have been harmless, but what actually ships is boolean | string | null | undefined.

The rest are smaller. A couple of boolean props are missing from the hand-written arrays so they only work in the lazy build, and I've left a question on whether the editor warning from #30822 is actually silenced, since your ticket note had that as an open item.

Comment thread core/stencil.config.ts
directivesArrayFile: '../packages/angular/src/lazy/directives/proxies-list.ts',
excludeComponents,
outputType: 'component',
booleanAttributes: true,

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.

The <ion-modal [handle]="42"> failure in the description has a flip side I don't think we've accounted for. These inputs had no type at all before, so anything that compiled and isn't boolean, any or nullable now fails too. Something like [disabled]="items.length" is the common one, and it's the same on Angular 18 through 22.

What gets emitted is ngAcceptInputType_<prop>: boolean | string | null | undefined. Upstream chose the narrow type on purpose, the helper's own comment says widening it to unknown would let any expression through, so this is intended rather than a slip. It does mean every boolean input in the library gets stricter in a minor though.

Could we check the breaking change box, or scope the claim in the description? It probably wants a changelog note either way.

Comment thread core/stencil.config.ts
outputType: 'standalone',
// Emit each component in a separate file rather than putting them all in one large file.
esModules: true,
booleanAttributes: true,

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.

Is there an ionic-docs PR for this one? I couldn't find one linked. The part consumers will get wrong is that null and undefined pass through instead of coercing to false, since that's the opposite of what Angular's own booleanAttribute does.

The "Standalone Directive" section of docs/component-guide.md could use a line too. It's where someone goes to add a hand-written wrapper and it still points at ion-checkbox and ion-toggle for boolean inputs without mentioning the transform, so the next one added won't match its neighbors.

*/

/**
* Transforms a value to a boolean so that boolean properties can be set by attribute presence,

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.

Did this end up confirmed? The original report was an editor warning rather than a build failure, and I'm not sure the new option silences it on its own.

From the compiler side it doesn't. Writing <ion-item button detail> compiles clean with strictTemplates on against both this branch and the base, in both entry points, on Angular 18 through 22. So whether #30822 is actually fixed comes down entirely to the language service. If it isn't silenced there, that's the second Stencil option and probably its own call.

* reach inputs routinely from the `async` pipe before its first emission and from form control
* values:
*
* ```tsx

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 is the only one of the three copies anyone will actually edit, and it's the shortest. It drops the notes the generated copy carries about why Angular's booleanAttribute isn't imported, and about the parameter type being what ngAcceptInputType derives from, so widening it to unknown would quietly loosen template checking everywhere. That second one especially is the kind of thing someone would undo without realizing.

Meanwhile the tsx example and the async pipe paragraph read more like PR description material. I think trading one for the other would leave this better off, but up to you!

if (value === null || value === undefined) {
return value;
}
return typeof value === 'boolean' ? value : value !== 'false';

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.

Anything that isn't a boolean, null or undefined falls into the string branch here, so 0 and NaN come back true. Before this, Angular wrote the raw value straight through and Stencil coerced 0 to false, so [disabled]="items.length" on an empty list flips from enabled to disabled. With strictTemplates on you'd get a compile error instead, without it it's silent. Empty string, null, undefined and objects don't change, so the focusTrap === false cases in the ticket are all fine.

Coercing only strings would keep the feature and the old behavior. The catch is that the two generated copies of this helper are byte-identical to what @stencil/angular-output-target 1.5.0 ships, and the whole angular-component-lib directory gets recopied on every core build, so changing it here alone would leave the hand-written wrappers behaving differently from the generated ones. It'd have to go upstream again.

Mostly flagging it so it's a deliberate call rather than something we notice later, since changing it afterwards is its own behavior change.

'labelPlacement',
'mode',
'name',
'value',

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.

Suggested change
'value',
{ name: 'required', transform: nullableBooleanAttribute },
'value',

The required prop is a public boolean on ion-checkbox and the regenerated lazy proxies picked it up, but this array doesn't list it at all, so it binds in the lazy build and does nothing in standalone. Both ion-toggle and ion-select have the same gap. It predates this PR, but since this one is a sweep over exactly these arrays it seems worth catching here. I added all three and they compile fine with the inputs typed correctly.

The hand-written wrappers are also the part the upstream tests can't reach, so this is where drift shows up. I threw together a script modelled on verify-change-detection.js that checks each one's boolean props against core's types, and it finds exactly these three plus one on popover and nothing else. Happy to share it.

'value',
];

/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */

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 line and the comment above it are byte-identical in 16 files. The NG1010 thing in the PR body is real but it only applies to the @Component/@Directive inputs array, and ProxyCmp is a plain runtime decorator whose inputs never reaches ngtsc, so this could be a shared helper instead. Most of the wrappers already import from @ionic/angular/common or utils/proxy, so 15 of the 16 sites are a one-token change to an existing import. Only the icon wrapper would pick up a new dependency and it can keep its local .map.

One thing to watch if you do take it though. It compiles fine, but changing the line count near these classes makes TypeScript splice unrelated comments into the synthesized ngAcceptInputType members in the emitted .d.ts. Still valid TypeScript and purely cosmetic, and there's none of it on this branch today, but worth a glance at dist afterwards.

'reference',
'size',
'side',
];

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.

Suggested change
];
{ name: 'keyboardEvents', transform: nullableBooleanAttribute },
];

The keyboardEvents prop is a public documented boolean on ion-popover and it's missing from this array entirely, so it's the one popover boolean this PR doesn't reach. Checked that adding it compiles and wires up properly. Pre-existing, same class of thing as the required gap over on checkbox.

While you're in here, the modal array has the mirror problem, it lists translucent and event and neither of those exists on ion-modal. I think the standalone input wrapper has a couple in the same state too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

package: angular @ionic/angular package package: core @ionic/core package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Angular - make certain inputs booleanAttributes

3 participants