-
Notifications
You must be signed in to change notification settings - Fork 13.3k
feat(angular): support setting boolean props by attribute presence #31442
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature-9.1
Are you sure you want to change the base?
Changes from all commits
62770a8
811bd2a
9e2c23a
690cf55
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,6 +34,7 @@ const getAngularOutputTargets = () => { | |
| directivesArrayFile: '../packages/angular/src/lazy/directives/proxies-list.ts', | ||
| excludeComponents, | ||
| outputType: 'component', | ||
| booleanAttributes: true, | ||
| }), | ||
| angularOutputTarget({ | ||
| componentCorePackage, | ||
|
|
@@ -66,6 +67,7 @@ const getAngularOutputTargets = () => { | |
| outputType: 'standalone', | ||
| // Emit each component in a separate file rather than putting them all in one large file. | ||
| esModules: true, | ||
| booleanAttributes: true, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 The "Standalone Directive" section of |
||
| }) | ||
| ]; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -9,6 +9,7 @@ import { | |||||||
| } from '@angular/core'; | ||||||||
| import type { Components } from '@ionic/core/components'; | ||||||||
|
|
||||||||
| import { nullableBooleanAttribute } from '../utils/boolean-attribute'; | ||||||||
| import { ProxyCmp, proxyOutputs } from '../utils/proxy'; | ||||||||
|
|
||||||||
| export declare interface IonPopover extends Components.IonPopover { | ||||||||
|
|
@@ -48,32 +49,35 @@ export declare interface IonPopover extends Components.IonPopover { | |||||||
|
|
||||||||
| const POPOVER_INPUTS = [ | ||||||||
| 'alignment', | ||||||||
| 'animated', | ||||||||
| 'arrow', | ||||||||
| 'keepContentsMounted', | ||||||||
| 'backdropDismiss', | ||||||||
| { name: 'animated', transform: nullableBooleanAttribute }, | ||||||||
| { name: 'arrow', transform: nullableBooleanAttribute }, | ||||||||
| { name: 'keepContentsMounted', transform: nullableBooleanAttribute }, | ||||||||
| { name: 'backdropDismiss', transform: nullableBooleanAttribute }, | ||||||||
| 'cssClass', | ||||||||
| 'dismissOnSelect', | ||||||||
| { name: 'dismissOnSelect', transform: nullableBooleanAttribute }, | ||||||||
| 'enterAnimation', | ||||||||
| 'event', | ||||||||
| 'focusTrap', | ||||||||
| 'isOpen', | ||||||||
| 'keyboardClose', | ||||||||
| { name: 'focusTrap', transform: nullableBooleanAttribute }, | ||||||||
| { name: 'isOpen', transform: nullableBooleanAttribute }, | ||||||||
| { name: 'keyboardClose', transform: nullableBooleanAttribute }, | ||||||||
| 'leaveAnimation', | ||||||||
| 'mode', | ||||||||
| 'showBackdrop', | ||||||||
| 'translucent', | ||||||||
| { name: 'showBackdrop', transform: nullableBooleanAttribute }, | ||||||||
| { name: 'translucent', transform: nullableBooleanAttribute }, | ||||||||
| 'trigger', | ||||||||
| 'triggerAction', | ||||||||
| 'reference', | ||||||||
| 'size', | ||||||||
| 'side', | ||||||||
| ]; | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
The While you're in here, the modal array has the mirror problem, it lists |
||||||||
|
|
||||||||
| /* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ | ||||||||
| const POPOVER_PROXY_INPUTS = POPOVER_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); | ||||||||
|
|
||||||||
| const POPOVER_METHODS = ['present', 'dismiss', 'onDidDismiss', 'onWillDismiss']; | ||||||||
|
|
||||||||
| @ProxyCmp({ | ||||||||
| inputs: POPOVER_INPUTS, | ||||||||
| inputs: POPOVER_PROXY_INPUTS, | ||||||||
| methods: POPOVER_METHODS, | ||||||||
| }) | ||||||||
| /** | ||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| /* | ||
| * Duplicates `angular-component-lib/boolean-attribute.ts`, which the output | ||
| * target copies next to each generated proxies file and so is not reachable | ||
| * from here. `proxy.ts` duplicates `ProxyCmp` for the same reason. Refer to | ||
| * the TODO at the top of `proxy.ts`. | ||
| */ | ||
|
|
||
| /** | ||
| * Transforms a value to a boolean so that boolean properties can be set by attribute presence, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| * e.g. `<ion-modal handle>` instead of `<ion-modal [handle]="true">`. | ||
| * | ||
| * Strings are coerced the same way Angular's `booleanAttribute` coerces them, so `''` (a bare | ||
| * attribute) becomes `true` and `'false'` becomes `false`. | ||
| * | ||
| * Unlike Angular's `booleanAttribute`, `null` and `undefined` are passed through rather than | ||
| * coerced to `false`. Components frequently treat them as a state distinct from `false`, and both | ||
| * reach inputs routinely from the `async` pipe before its first emission and from form control | ||
| * values: | ||
| * | ||
| * ```tsx | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Meanwhile the tsx example and the |
||
| * // `undefined` means "decide based on the mode", which is not the same as `false` | ||
| * const showDetail = detail !== undefined ? detail : mode === 'ios'; | ||
| * | ||
| * // a strict comparison also behaves differently for `null` than it does for `false` | ||
| * const showHandle = handle !== false; | ||
| * ``` | ||
| * | ||
| * Declared as a function rather than an arrow constant because Angular has to resolve input | ||
| * transforms statically when compiling a library in partial compilation mode. | ||
| */ | ||
| export function nullableBooleanAttribute(value: boolean | string | null | undefined): boolean | null | undefined { | ||
| if (value === null || value === undefined) { | ||
| return value; | ||
| } | ||
| return typeof value === 'boolean' ? value : value !== 'false'; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Anything that isn't a boolean, 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 Mostly flagging it so it's a deliberate call rather than something we notice later, since changing it afterwards is its own behavior change. |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| /* eslint-disable */ | ||
| /* tslint:disable */ | ||
|
|
||
| /** | ||
| * Transforms a value to a boolean so that boolean properties can be set by attribute presence, | ||
| * e.g. `<my-component disabled>` instead of `<my-component [disabled]="true">`. | ||
| * | ||
| * Strings are coerced the same way Angular's `booleanAttribute` coerces them, so `''` (a bare | ||
| * attribute) becomes `true` and `'false'` becomes `false`. | ||
| * | ||
| * Unlike Angular's `booleanAttribute`, `null` and `undefined` are passed through rather than | ||
| * coerced to `false`. Components frequently treat them as a state distinct from `false`: | ||
| * | ||
| * ```tsx | ||
| * // `undefined` means "decide based on the mode", which is not the same as `false` | ||
| * const showDetail = detail !== undefined ? detail : mode === 'ios'; | ||
| * | ||
| * // a strict comparison also behaves differently for `null` than it does for `false` | ||
| * const showHandle = handle !== false; | ||
| * ``` | ||
| * | ||
| * Both values reach inputs routinely in Angular templates, from the `async` pipe before its | ||
| * first emission and from form control values, so coercing them would change the behavior of | ||
| * bindings that work today. | ||
| * | ||
| * This is implemented here rather than imported from `@angular/core` so that consumers on | ||
| * Angular versions without `booleanAttribute` are unaffected by this file being generated. | ||
| * | ||
| * The parameter type is what Angular derives `ngAcceptInputType_*` from, so it decides which | ||
| * template bindings compile. Widening it to `unknown` would let any expression through. | ||
| * | ||
| * Declared as a function rather than an arrow constant because Angular has to resolve input | ||
| * transforms statically when compiling a library in partial compilation mode. | ||
| * | ||
| * This lives in its own file, separate from `utils.ts`, so that it carries no runtime imports. | ||
| * 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) { | ||
| return value; | ||
| } | ||
| return typeof value === 'boolean' ? value : value !== 'false'; | ||
| } |
There was a problem hiding this comment.
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'tboolean,anyor 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 tounknownwould 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.