Skip to content

Commit 1160a8c

Browse files
committed
fix(transpiler_ui): fix value prop, event handler prefix, and JS state/action emission
1 parent 0d2448e commit 1160a8c

4 files changed

Lines changed: 124 additions & 3 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
function app() {
2+
return {
3+
count: 0,
4+
increment() {
5+
this.count = count + 1;
6+
},
7+
};
8+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>MyLibraryApp</title>
7+
<link rel="stylesheet" href="style.css">
8+
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
9+
</head>
10+
<body x-data="app()">
11+
<div id="window-MainWindow" class="prox-window">
12+
<div class="hero">
13+
<span>Welcome to ProXPL UI</span>
14+
<button @click="increment">
15+
<span>Click Me</span>
16+
</button>
17+
<span x-text="count"></span>
18+
</div>
19+
</div>
20+
<script src="app.js"></script>
21+
</body>
22+
</html>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/* ============================================
2+
ProXPL UI — Generated style.css
3+
App: MyLibraryApp
4+
============================================ */
5+
6+
/* === CSS Reset === */
7+
*, *::before, *::after {
8+
box-sizing: border-box;
9+
margin: 0;
10+
padding: 0;
11+
}
12+
13+
/* === Base === */
14+
html {
15+
font-size: 16px;
16+
scroll-behavior: smooth;
17+
}
18+
19+
body {
20+
font-family: 'Inter', system-ui, -apple-system, sans-serif;
21+
font-size: 1rem;
22+
line-height: 1.6;
23+
color: #1f2937;
24+
background: #f9fafb;
25+
min-height: 100vh;
26+
}
27+
28+
img, video, canvas {
29+
max-width: 100%;
30+
display: block;
31+
}
32+
33+
input, button, textarea, select {
34+
font: inherit;
35+
}
36+
37+
/* === ProXPL Window Component === */
38+
.prox-window {
39+
background: #ffffff;
40+
border-radius: 1rem;
41+
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
42+
border: 1px solid rgba(0, 0, 0, 0.05);
43+
padding: 2rem;
44+
margin: 2rem;
45+
}
46+
47+
/* === Animations === */
48+
@keyframes fadeIn {
49+
from { opacity: 0; }
50+
to { opacity: 1; }
51+
}
52+

src/compiler/transpiler_ui.c

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,16 +198,40 @@ static void transpileStmt(Stmt *stmt, FILE *html, FILE *js, int indent) {
198198

199199
// Emit props/attributes
200200
DictPairList *props = stmt->as.ui_component.props;
201+
Expr *innerTextValue = NULL;
202+
201203
for (int i = 0; i < props->count; i++) {
202204
const char *rawKey = (props->items[i].key && props->items[i].key->type == EXPR_VARIABLE)
203205
? props->items[i].key->as.variable.name : NULL;
204206
const char *mappedKey = rawKey ? mapPropName(rawKey) : NULL;
205207
bool isVariableValue = (props->items[i].value && props->items[i].value->type == EXPR_VARIABLE);
206208

209+
// Special handling for 'value' prop on non-form elements
210+
if (rawKey && strcmp(rawKey, "value") == 0) {
211+
if (strcmp(htmlTag, "input") != 0 && strcmp(htmlTag, "option") != 0 &&
212+
strcmp(htmlTag, "textarea") != 0 && strcmp(htmlTag, "select") != 0 &&
213+
strcmp(htmlTag, "progress") != 0 && strcmp(htmlTag, "meter") != 0) {
214+
215+
// If literal string, inject as inner text
216+
if (props->items[i].value && props->items[i].value->type == EXPR_LITERAL && IS_STRING(props->items[i].value->as.literal.value)) {
217+
innerTextValue = props->items[i].value;
218+
continue; // Skip emitting this attribute
219+
} else {
220+
// Any other expression becomes a reactive x-text attribute
221+
mappedKey = "x-text";
222+
isVariableValue = false; // x-text is natively reactive, do not prefix with ':'
223+
}
224+
}
225+
}
226+
207227
fprintf(html, " ");
208228
if (mappedKey) {
209-
// Reactive prefix for Alpine
210-
if (isVariableValue) {
229+
// Only add ':' reactive prefix for plain HTML attribute bindings.
230+
// Event handlers (@click etc.) and Alpine directives (x-model, x-show etc.)
231+
// must NOT be prefixed with ':'.
232+
bool isEventHandler = (mappedKey[0] == '@');
233+
bool isAlpineDirective = (mappedKey[0] == 'x' && mappedKey[1] == '-');
234+
if (isVariableValue && !isEventHandler && !isAlpineDirective) {
211235
fprintf(html, ":%s", mappedKey);
212236
} else {
213237
fprintf(html, "%s", mappedKey);
@@ -227,9 +251,14 @@ static void transpileStmt(Stmt *stmt, FILE *html, FILE *js, int indent) {
227251

228252
fprintf(html, ">");
229253

254+
if (innerTextValue) {
255+
transpileExpr(innerTextValue, html);
256+
}
257+
230258
// Children
231259
if (stmt->as.ui_component.children && stmt->as.ui_component.children->count > 0) {
232-
fprintf(html, "\n");
260+
if (!innerTextValue) fprintf(html, "\n");
261+
else fprintf(html, "\n");
233262
for (int i = 0; i < stmt->as.ui_component.children->count; i++) {
234263
transpileStmt(stmt->as.ui_component.children->items[i], html, js, indent + 1);
235264
}
@@ -409,10 +438,20 @@ void transpileUIApp(Stmt *appStmt, const char *outputDir) {
409438
fprintf(js, "function app() {\n return {\n");
410439

411440
StmtList *body = appStmt->as.ui_app.body;
441+
442+
// Pass 1: emit Window/Component HTML structure (no JS)
412443
for (int i = 0; i < body->count; i++) {
413444
transpileStmt(body->items[i], html, NULL, 1);
414445
}
415446

447+
// Pass 2: emit State + Action into app.js
448+
for (int i = 0; i < body->count; i++) {
449+
Stmt *s = body->items[i];
450+
if (s->type == STMT_UI_STATE || s->type == STMT_UI_ACTION) {
451+
transpileStmt(s, NULL, js, 2);
452+
}
453+
}
454+
416455
fprintf(js, " };\n}\n");
417456
fprintf(html, " <script src=\"app.js\"></script>\n");
418457
fprintf(html, "</body>\n</html>\n");

0 commit comments

Comments
 (0)