Skip to content

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

Merged
thetaPC merged 15 commits into
nextfrom
FW-7637
Sep 22, 2026
Merged

thetaPC merged 15 commits into
nextfrom
FW-7637

Conversation

@thetaPC

@thetaPC thetaPC commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Issue number: internal


What is the current behavior?

Angular does not type-check boolean inputs on the Ionic wrappers. The generated wrappers declare no typed members for their inputs, so the compiler has nothing to check a binding against and accepts any expression:

<!-- compiles today, silently -->
<ion-item [button]="items.length"></ion-item>

Attribute presence already works at runtime, because Stencil coerces boolean attributes on the element, but it is incidental rather than declared.

What is the new behavior?

Boolean props declare an Angular input transform, which makes attribute presence explicit and turns on type checking for those inputs.

<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, which covers every boolean prop in the generated wrappers.
  • Applies the same transform by hand to the 17 wrappers excluded from generation. Without it, their boolean inputs would be the only ones left untransformed, including <ion-checkbox checked> and <ion-modal handle>.
  • null and undefined are passed through rather than coerced to false, unlike Angular's own booleanAttribute. 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 computed default.
  • Coercion moves from Stencil to Angular, which changes the result for numbers. 0 and NaN were false and are now true, matching Angular's own booleanAttribute. Everything else coerces identically.
  • Most of the diff is regenerated or copied in by core's build rather than hand-edited.

Does this introduce a breaking change?

  • Yes
  • No

These inputs previously had no declared type, so Angular did not check bindings to them and any expression compiled. Declaring a transform emits ngAcceptInputType_<prop>: boolean | string | null | undefined, which turns checking on for every boolean input in the library. Bindings that pass anything outside that union now fail to compile.

The common pattern this catches is a truthiness binding:

<!-- was accepted, now fails -->
<ion-item [button]="items.length"></ion-item>

<!-- migration: coerce explicitly -->
<ion-item [button]="items.length > 0"></ion-item>
<ion-item [button]="!!items.length"></ion-item>
error TS2322: Type 'number' is not assignable to type 'string | boolean | null | undefined'.

This affects Angular 18 through 22 equally and is compile time only. A binding that already passed a boolean, a string, null or undefined is unaffected.

The narrow accepted type is deliberate upstream. Widening it to unknown would accept any expression again and give up the checking this adds.

Only boolean inputs end up checked. Every other input stays unchecked, for the same reason they all were before, and closing that gap needs the output target's inlineProperties option, which is a separate decision.

Other information

Associated docs: ionic-team/ionic-docs#4744

The version bump is the fix. Generated proxies are overwritten on every core build, so this could not be solved here. The change was made upstream in stenciljs/output-targets#842 and released in 1.5.0.

This started as a fix for #30822, but that report is not reproducible. Neither the compiler nor the Angular language service flags <ion-item button detail> on 9.0.4 or on this branch, and the wrappers have had the same untyped shape since v6.

@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 22, 2026 10:26pm 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated the PR description and BREAKING.md: b8c795c

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

*/

/**
* 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed, you were right. The language service doesn't flag it either, on 9.0.4 or on this branch. I thought I had watched the squiggles go away while testing, but that was a stale editor server.

Dropped the resolves, reframed the description around the type checking, and corrected the BREAKING.md line that described a compile error that never existed.

d3f65a8

* 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!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

'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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

'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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

'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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

keyboardEvents is in, along with the same gap on checkbox, toggle and select: 70c7557.

Confirmed the mirror problem too. event and translucent aren't props on ion-modal, and ion-input has one, accept. I'd rather take those in a follow-up (FW-7774), since removing an input breaks anyone binding it and wants its own breaking change note.

Co-authored-by: Shane <ShaneK@users.noreply.github.com>

@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 good to me! Everything from the last round is addressed, and I checked that the generated proxies reproduce byte-exact from a clean build. A few suggestions inline, all minor apart from the BREAKING.md wording, which describes the wrong outcome.

Comment thread BREAKING.md Outdated
Comment thread docs/component-guide.md Outdated
@@ -0,0 +1,43 @@
/* eslint-disable */

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 angular package's .prettierignore lists **/*/angular-component-lib/utils.ts because the output target emits it, but not this file, so npm run lint owns its formatting. It's a no-op today, though lint and regeneration will fight each other the moment the generator's output drifts. Adding **/*/angular-component-lib/boolean-attribute.ts next to the existing entry covers both the lazy and standalone copies. Just a nit, up to you!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

'labelPlacement',
'mode',
'name',
{ name: 'required', transform: nullableBooleanAttribute },

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.

Following on from the required thread, nothing stops this drifting again. It's still live for the non-boolean props, theme is in all 164 generated proxies and in none of the hand-written wrappers, and there are a few others like alignment here and on checkbox. I did end up writing that verify-wrapper-inputs.js, on the same model as validate.change-detection, and it catches the required case when I revert it. Say the word and I'll open a PR. The non-boolean gaps could go along with FW-7774.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That would be great! Thank you! Please add me as a reviewer.

* That keeps it independently type-checkable without pulling `rxjs` in for `proxyOutputs`.
*/
export function nullableBooleanAttribute(value: boolean | string | null | undefined): boolean | null | undefined {
if (value === null || value === undefined) {

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 undefined passthrough is the reason this exists instead of Angular's booleanAttribute, and nothing pins it right now. One caveat if you do add a test: assertions for attribute presence, 'false' and an absent attribute all still pass with this stubbed out to a no-op, because Stencil's own coercion already covers those, so they'd pass on next too. The undefined case is the one that discriminates, swapping in booleanAttribute semantics breaks that and nothing else. So one test rather than a suite. The number coercion can't be covered this way at all, since the test app has strictTemplates on and [button]="someNumber" doesn't compile.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

thetaPC and others added 3 commits September 22, 2026 11:33
Co-authored-by: Shane <shane.king@outsystems.com>
Co-authored-by: Shane <shane.king@outsystems.com>
@thetaPC
thetaPC merged commit 1c9cd1e into next Sep 22, 2026
53 checks passed
@thetaPC
thetaPC deleted the FW-7637 branch September 22, 2026 22:37

This branch was successfully deployed

1 active deployment
Preview a25ac7cf Deployed Sep 22, 2026 by vercel[bot]
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