Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions demo/vite-project/src/components/tanstack-form-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import React from 'react';

// Simulating Tanstack Forms pattern
function useForm() {
return {
AppForm: ({ children }: { children: React.ReactNode }) => (
<form style={{ padding: '20px', border: '2px solid blue' }}>{children}</form>
),
CancelButton: ({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) => (
<button
type="button"
onClick={onClick}
style={{
padding: '10px 20px',
backgroundColor: 'red',
color: 'white',
marginRight: '10px',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
{children}
</button>
),
SubmitButton: ({ children }: { children: React.ReactNode }) => (
<button
type="submit"
style={{
padding: '10px 20px',
backgroundColor: 'green',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
{children}
</button>
),
};
}

// Simulating uppercase workaround
function useFormUppercase() {
return {
AppForm: ({ children }: { children: React.ReactNode }) => (
<form style={{ padding: '20px', border: '2px solid green' }}>{children}</form>
),
CancelButton: ({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) => (
<button
type="button"
onClick={onClick}
style={{
padding: '10px 20px',
backgroundColor: 'red',
color: 'white',
marginRight: '10px',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
{children}
</button>
),
SubmitButton: ({ children }: { children: React.ReactNode }) => (
<button
type="submit"
style={{
padding: '10px 20px',
backgroundColor: 'green',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
{children}
</button>
),
};
}

export default function TanstackFormTest() {
const form = useForm();
const FormContent = useFormUppercase();

const handleCancel = () => {
alert('Cancel clicked!');
};

return (
<div style={{ padding: '20px' }}>
<h2>Tanstack Forms + Lingo.dev Compiler Issue #1165</h2>

<div style={{ marginBottom: '40px' }}>
<h3>✅ Previously broken: Lowercase variable name (form) - now fixed</h3>
<p>Using: <code>const form = useForm()</code></p>
<p>Expected: Blue border, styled buttons with click functionality</p>
<form.AppForm>
<div>
<form.CancelButton onClick={handleCancel}>Cancel</form.CancelButton>
<form.SubmitButton>Submit</form.SubmitButton>
</div>
</form.AppForm>
</div>

<div>
<h3>Working: Uppercase variable name (FormContent)</h3>
<p>Using: <code>const FormContent = useFormUppercase()</code></p>
<p>Expected: Green border, styled buttons with click functionality</p>
<FormContent.AppForm>
<div>
<FormContent.CancelButton onClick={handleCancel}>Cancel</FormContent.CancelButton>
<FormContent.SubmitButton>Submit</FormContent.SubmitButton>
</div>
</FormContent.AppForm>
</div>
</div>
);
}

28 changes: 26 additions & 2 deletions packages/compiler/src/jsx-scope-inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,25 @@ import { getJsxExpressions } from "./utils/jsx-expressions";
import { collectJsxScopes, getJsxScopeAttribute } from "./utils/jsx-scope";
import { setJsxAttributeValue } from "./utils/jsx-attribute";

/**
* Creates a proper AST node from a string that may contain dots (member expressions).
* For example, "form.Button" becomes a member expression AST, while "Button" becomes an identifier.
* @param str - The string to convert (e.g., "form.Button" or "Button")
* @returns A Babel AST Expression node
*/
function createMemberExpressionFromString(str: string): t.Expression {
const parts = str.split('.');
if (parts.length === 1) {
return t.identifier(parts[0]);
}

let expr: t.Expression = t.identifier(parts[0]);
for (let i = 1; i < parts.length; i++) {
expr = t.memberExpression(expr, t.identifier(parts[i]));
}
return expr;
}

export const lingoJsxScopeInjectMutation = createCodeMutation((payload) => {
const mode = getModuleExecutionMode(payload.ast, payload.params.rsc);
const jsxScopes = collectJsxScopes(payload.ast);
Expand Down Expand Up @@ -55,8 +74,13 @@ export const lingoJsxScopeInjectMutation = createCodeMutation((payload) => {
} as any;

// Add $as prop
const as = /^[A-Z]/.test(originalJsxElementName)
? t.identifier(originalJsxElementName)
// Check if it's a member expression (contains dot) or starts with uppercase.
// Member expressions (e.g., form.Button) and uppercase names (e.g., Button)
// should be treated as component references, not HTML element strings.
const isMemberExpression = originalJsxElementName.includes(".");
const isComponent = /^[A-Z]/.test(originalJsxElementName);
const as = isMemberExpression || isComponent
? createMemberExpressionFromString(originalJsxElementName)
: originalJsxElementName;
setJsxAttributeValue(newNodePath, "$as", as);

Expand Down