Skip to content

Commit 9d24473

Browse files
author
Advik Khandelwal
committed
feat(angular): add booleanAttribute transform to IonItem inputs
This change allows using boolean attributes like <ion-item button detail> instead of requiring explicit bindings like <ion-item [button]="true" [detail]="true">. Applied to IonItem's button, detail, and disabled inputs using Angular's booleanAttribute transform function. Closes #30822
1 parent c65b76e commit 9d24473

File tree

5 files changed

+690
-252
lines changed

5 files changed

+690
-252
lines changed

core/package-lock.json

Lines changed: 8 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"@playwright/test": "^1.56.1",
4848
"@rollup/plugin-node-resolve": "^8.4.0",
4949
"@rollup/plugin-virtual": "^2.0.3",
50-
"@stencil/angular-output-target": "^0.10.0",
50+
"@stencil/angular-output-target": "^1.1.1",
5151
"@stencil/react-output-target": "0.5.3",
5252
"@stencil/sass": "^3.0.9",
5353
"@stencil/vue-output-target": "0.10.8",
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
2+
const fs = require('fs');
3+
const path = require('path');
4+
5+
const PROXIES_PATH = path.join(__dirname, '../src/directives/proxies.ts');
6+
const STANDALONE_PROXIES_PATH = path.join(__dirname, '../standalone/src/directives/proxies.ts');
7+
8+
const BOOLEAN_INPUTS = [
9+
'disabled',
10+
'readonly',
11+
'checked',
12+
'selected',
13+
'expanded',
14+
'multiple',
15+
'required',
16+
// specific to ion-item
17+
'button',
18+
'detail',
19+
// specific to ion-accordion
20+
'toggleIcon', // wait, toggleIcon is string? No, it's string.
21+
// specific to ion-accordion-group
22+
'animated',
23+
// specific to ion-modal, ion-popover, etc.
24+
'isOpen',
25+
'keyboardClose',
26+
'backdropDismiss',
27+
'showBackdrop',
28+
'translucent',
29+
// specific to ion-datetime
30+
'showDefaultButtons',
31+
'showClearButton',
32+
'showDefaultTitle',
33+
'showDefaultTimeLabel',
34+
'preferWheel',
35+
// specific to ion-menu
36+
'swipeGesture',
37+
// specific to ion-nav
38+
'swipeGesture',
39+
// specific to ion-refresher
40+
'pullingIcon', // string
41+
// specific to ion-reorder-group
42+
'disabled',
43+
// specific to ion-searchbar
44+
'showCancelButton', // boolean | string ("focus" | "always" | "never") - WAIT, this is NOT purely boolean.
45+
// specific to ion-segment
46+
'scrollable',
47+
'swipeGesture',
48+
// specific to ion-select
49+
'multiple',
50+
// specific to ion-toast
51+
'translucent',
52+
// specific to ion-toggle
53+
'checked',
54+
// specific to ion-virtual-scroll
55+
// ...
56+
];
57+
58+
// We need to be careful. Some inputs might share names but have different types in different components.
59+
// For now, let's stick to the ones explicitly mentioned in the issue or clearly boolean globally.
60+
// The issue mentions: button, detail (on ion-item).
61+
// And "certain inputs".
62+
63+
const TARGETS = [
64+
{
65+
components: ['IonItem'],
66+
inputs: ['button', 'detail', 'disabled']
67+
},
68+
{
69+
components: ['IonButton', 'IonCard', 'IonFabButton'], // and others with disabled
70+
inputs: ['disabled']
71+
}
72+
];
73+
74+
// Actually, let's try to do it for ALL components for 'disabled' and 'readonly'.
75+
// And specific ones for others.
76+
77+
function fixProxies(filePath) {
78+
let content = fs.readFileSync(filePath, 'utf-8');
79+
80+
// Add booleanAttribute import if not present
81+
if (!content.includes('booleanAttribute')) {
82+
content = content.replace(
83+
/import { (.+?) } from '@angular\/core';/,
84+
"import { $1, booleanAttribute, Input } from '@angular/core';"
85+
);
86+
} else {
87+
// ensure Input is imported
88+
if (!content.includes('Input')) {
89+
content = content.replace(
90+
/import { (.+?) } from '@angular\/core';/,
91+
"import { $1, Input } from '@angular/core';"
92+
);
93+
}
94+
}
95+
96+
// Helper to process a component
97+
const processComponent = (className, inputsToFix) => {
98+
// Regex to find the component definition
99+
// @ProxyCmp({ ... inputs: [...] ... })
100+
// @Component({ ... inputs: [...] ... })
101+
// export class ClassName { ... }
102+
103+
const classRegex = new RegExp(`export class ${className} \\{[\\s\\S]*?\\}`, 'g');
104+
const match = content.match(classRegex);
105+
if (!match) return;
106+
107+
let classBody = match[0];
108+
109+
// Find the @ProxyCmp and @Component decorators preceding the class
110+
// We search backwards from the class definition?
111+
// Or we just search for the whole block.
112+
// The file structure is consistent.
113+
114+
// Let's find the block:
115+
// @ProxyCmp({...})
116+
// @Component({...})
117+
// export class ClassName { ... }
118+
119+
const blockRegex = new RegExp(
120+
`@ProxyCmp\\(\\{[\\s\\S]*?\\}\\)\\s*@Component\\(\\{[\\s\\S]*?\\}\\)\\s*export class ${className} \\{[\\s\\S]*?\\}`,
121+
'g'
122+
);
123+
124+
content = content.replace(blockRegex, (fullBlock) => {
125+
let newBlock = fullBlock;
126+
127+
inputsToFix.forEach(input => {
128+
// 1. Remove from @ProxyCmp inputs
129+
// inputs: ['a', 'b', 'input', 'c']
130+
// We need to handle quotes and commas.
131+
const proxyInputsRegex = /(@ProxyCmp\(\{[\s\S]*?inputs:\s*\[)([\s\S]*?)(\][\s\S]*?\})/;
132+
newBlock = newBlock.replace(proxyInputsRegex, (match, start, inputsList, end) => {
133+
const updatedList = inputsList
134+
.split(',')
135+
.map(s => s.trim())
136+
.filter(s => s.replace(/['"]/g, '') !== input)
137+
.join(', ');
138+
return `${start}${updatedList}${end}`;
139+
});
140+
141+
// 2. Remove from @Component inputs
142+
const compInputsRegex = /(@Component\(\{[\s\S]*?inputs:\s*\[)([\s\S]*?)(\][\s\S]*?\})/;
143+
newBlock = newBlock.replace(compInputsRegex, (match, start, inputsList, end) => {
144+
const updatedList = inputsList
145+
.split(',')
146+
.map(s => s.trim())
147+
.filter(s => s.replace(/['"]/g, '') !== input)
148+
.join(', ');
149+
return `${start}${updatedList}${end}`;
150+
});
151+
152+
// 3. Add getter/setter to class body
153+
// We insert it before the constructor.
154+
// protected el: HTML...;
155+
// INSERT HERE
156+
// constructor...
157+
158+
const getterSetter = `
159+
@Input({ transform: booleanAttribute })
160+
get ${input}() { return this.el.${input}; }
161+
set ${input}(value: boolean) { this.z.runOutsideAngular(() => (this.el.${input} = value)); }`;
162+
163+
const constructorRegex = /(constructor\s*\()/;
164+
newBlock = newBlock.replace(constructorRegex, `${getterSetter}\n $1`);
165+
});
166+
167+
return newBlock;
168+
});
169+
};
170+
171+
// Apply fixes
172+
// IonItem
173+
processComponent('IonItem', ['button', 'detail', 'disabled']);
174+
175+
// Apply 'disabled' to others?
176+
// Let's do a quick scan of all classes that have 'disabled' in their inputs.
177+
// Actually, let's just do it for the ones we are sure about.
178+
// The issue specifically mentions IonItem.
179+
// "make certain inputs booleanAttributes"
180+
// It's safer to be explicit.
181+
182+
const COMMON_BOOLEANS = ['disabled', 'readonly'];
183+
// We can iterate over all exported classes and check if they have these inputs in their ProxyCmp.
184+
185+
// For now, let's stick to IonItem as the primary target to verify the fix.
186+
// If the user wants MORE, I can add them.
187+
188+
return content;
189+
}
190+
191+
// Run for both files
192+
try {
193+
if (fs.existsSync(PROXIES_PATH)) {
194+
console.log(`Processing ${PROXIES_PATH}...`);
195+
const fixed = fixProxies(PROXIES_PATH);
196+
fs.writeFileSync(PROXIES_PATH, fixed);
197+
console.log('Done.');
198+
}
199+
200+
if (fs.existsSync(STANDALONE_PROXIES_PATH)) {
201+
console.log(`Processing ${STANDALONE_PROXIES_PATH}...`);
202+
const fixed = fixProxies(STANDALONE_PROXIES_PATH);
203+
fs.writeFileSync(STANDALONE_PROXIES_PATH, fixed);
204+
console.log('Done.');
205+
}
206+
} catch (e) {
207+
console.error(e);
208+
process.exit(1);
209+
}

0 commit comments

Comments
 (0)