fix(web): dynamic() honors xmlns when creating a tag-name element (#3386) - #3436
Conversation
) The compiler resolves a tag's namespace from its parent at build time; the dynamic() runtime path creates the element before it has a parent, so a tag that exists in both HTML and SVG (a, script, style, title) was always an HTML element — `<svg><Link href/></svg>` with `Link = dynamic(() => "a")` produced an HTML anchor inside the SVG tree (#3386 part 2). The instance now says which namespace it means with the attribute compiled JSX already uses for exactly this (`<a xmlns="http://www.w3.org/2000/svg">`). Like `is`, `xmlns` is an attribute of the element that also decides how the node is created: read once, untracked, at creation — the DOM can't re-namespace a node — then applied through spread() as an ordinary attribute, so a client-rendered element carries the same attribute the server serializes and the parser keeps. Server: nothing to do; ssrElement already emits it, and hydration claims the parser-namespaced node. Without xmlns the namespace is still inferred from the tag. Types: dynamic() returns Component<DynamicComponentProps<T>>, adding `xmlns?: string` for tag-name targets only (the ambiguous tags' intrinsic attribute types are the HTML ones, which lack it). Tests: namespaceURI with/without xmlns inside <svg> and <math>, inference still wins for unambiguous tags, xmlns overrides inference both ways, <Dynamic> parity, read-once (later values update the attribute, never the node); server output byte-equal to compiled `<a xmlns>`; parity-harness scenario hydrating a dynamic() <a xmlns> inside <svg>. Also pins ref arrays through spread into a dynamic() tag (the shape Kobalte's solid2 branch works around at rc.3) as working on next. Co-authored-by: Cursor <cursoragent@cursor.com>
🦋 Changeset detectedLatest commit: 919b2ab The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
CI test-types rejected the compiled-JSX comparison `<a xmlns={SVG} href>`:
`AnchorHTMLAttributes` had no `xmlns` — the compiler has honored `<a xmlns=…>`
on any tag all along (it's how it disambiguates a/script/style/title), so
the attribute belongs on the shared `ElementAttributes`, not only SVG/MathML.
Source of truth is jsx-h.d.ts; jsx.d.ts and @solidjs/h's copy are regenerated.
With every intrinsic's attribute type carrying `xmlns`, dynamic()'s tag-name
components inherit it and the conditional `DynamicComponentProps` /
`DynamicElementProps` types from the previous commit are unnecessary — drop
them; dynamic() returns Component<ComponentProps<T>> as before.
Co-authored-by: Cursor <cursoragent@cursor.com>
Coverage Report for CI Build 34901829164Coverage remained the same at 71.842%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Narrow the previous commit: instead of accepting xmlns on every element via ElementAttributes, keep it on SVGAttributes/MathMLAttributes and add it only to the four tags that exist in both HTML and SVG (a, script, style, title). Those are the only tags where the attribute changes what gets created, both in the compiler and in dynamic(). Introduces TitleHTMLAttributes so title has a home for it. Regenerates the web and h JSX types. Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
|
Closes #3386 (part 2; part 1 landed in #3396).
Problem
The compiler resolves a tag's namespace from its parent at build time. The
dynamic()runtime path creates the element before it has a parent, so it can only infer from the tag name — and a tag that exists in both HTML and SVG (a,script,style,title) was always created as HTML.<svg><Link href=…/></svg>withLink = dynamic(() => "a")produced an HTML anchor inside the SVG tree.@yak/solidcarries anambiguousSvgTagsset to route around this.Fix
The instance says which namespace it means with the attribute compiled JSX already uses for exactly this purpose:
xmlnsjoinsisas an attribute of the element that also decides how the node is created: read once, untracked, at creation (the DOM can't re-namespace a node), then applied throughspread()as an ordinary attribute. That keeps a client-rendered element byte-for-byte the same as its own SSR output: the server already serializesxmlns(as compiled SSR does for<a xmlns>), the parser keeps it, and hydration claims the parser-namespaced node — so the server is untouched and hydration was never affected. Withoutxmlnsinference from the tag name still applies.Deliberately not inferring from the mount parent: there is no parent at creation time, and any deferred scheme (create-then-reparent, a context) would tax every
dynamic()instance to fix four tags.Types
dynamic()returnsComponent<DynamicComponentProps<T>>, which addsxmlns?: stringfor tag-name targets only. (The ambiguous tags'IntrinsicElementstypes are the HTML attribute sets, which don't includexmlns; SVG/MathML attribute sets already do.)Tests
test/dynamic-namespace.spec.tsx:namespaceURIwith/withoutxmlnsinside<svg>and<math>; unambiguous tags still inferred;xmlnsoverrides inference in both directions;<Dynamic>parity; read-once (a later value updates the attribute, never recreates the node).test/server/dynamic-xmlns.spec.tsx: server output equals compiled<a xmlns>output.dynamic-xmlns-svg-link: SSR → hydrate adynamic()<a xmlns>inside<svg>, adopt-all, stable identity through an update.dynamic()tag — the shape Kobalte'ssolid2branch works around with aTODOat rc.3 — as working onnext.All three
@solidjs/websuites green (778 / 895 / 181).