Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/fix-ssr-textarea-spread-value.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/web": patch
---

Render textarea values supplied through SSR spreads as text content rather than invalid HTML attributes.
10 changes: 9 additions & 1 deletion packages/web/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3569,6 +3569,15 @@ export function ssrElement(tag, props, children, needsId) {
let result = `<${tag}${hk} `;
for (let i = 0; i < keys.length; i++) {
const prop = keys[i];
const value = props[prop];
// The compiler moves static textarea values into children, but an
// element with a spread is serialized here instead. Keep the runtime
// path equivalent: textarea value/defaultValue are its text content,
// never HTML attributes.
if (tag === "textarea" && (prop === "value" || prop === "defaultValue")) {
if (value !== null) children = escape(value);
continue;
}
if (ChildProperties.has(prop)) {
if (children === undefined && !skipChildren)
children =
Expand All @@ -3577,7 +3586,6 @@ export function ssrElement(tag, props, children, needsId) {
: escape(props[prop]);
continue;
}
const value = props[prop];
if (prop === "style") {
result += `style="${ssrStyle(value)}"`;
} else if (prop === "class") {
Expand Down
31 changes: 31 additions & 0 deletions packages/web/test/server/spread-function-source.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,37 @@ describe("SSR spread with function source (#2815)", () => {
expect(html).toContain('id="y"');
});

test("textarea value from props merged around a spread becomes text content (#3286)", () => {
const value = () => "something";
const html = renderToString(() => <textarea {...{ "data-x": "x" }} value={value()} />);

expect(html).toMatch(/data-x="x"\s*>something<\/textarea>/);
expect(html).not.toContain(' value="something"');
});

test("textarea value supplied by a spread becomes text content (#3286)", () => {
const html = renderToString(() => <textarea {...{ "data-x": "x", value: "something" }} />);

expect(html).toMatch(/data-x="x"\s*>something<\/textarea>/);
expect(html).not.toContain(' value="something"');
});

test("textarea value before a spread remains text content (#3286)", () => {
const html = renderToString(() => <textarea value="something" {...{ "data-x": "x" }} />);

expect(html).toMatch(/data-x="x"\s*>something<\/textarea>/);
expect(html).not.toContain(' value="something"');
});

test("textarea defaultValue supplied by a spread becomes text content (#3286)", () => {
const html = renderToString(() => (
<textarea {...{ "data-x": "x", defaultValue: "something" }} />
));

expect(html).toMatch(/data-x="x"\s*>something<\/textarea>/);
expect(html).not.toContain(' defaultValue="something"');
});

test("Dynamic routes spreads through mergeProps", () => {
const props = { "data-x": "1", id: "y" };
const html = renderToString(() => <Dynamic component="div" {...props} />);
Expand Down
Loading