createElementNS y el espacio de nombres
Por qué createElement no funciona para SVG, qué es exactamente un HTMLUnknownElement, cuándo hace falta setAttributeNS, y las tres formas de construir marcado SVG desde JavaScript.
El fallo silencioso
// No funcionaconst c = document.createElement('circle');c.setAttribute('cx', 50);c.setAttribute('cy', 50);c.setAttribute('r', 40);c.setAttribute('fill', 'red');svg.appendChild(c);El elemento se añade. svg.children.length aumenta. El inspector lo muestra. Y no hay ningún círculo.
La razón: document.createElement crea elementos en el espacio de nombres de HTML. Un elemento llamado circle en ese espacio de nombres no existe en la especificación de HTML, así que el navegador crea un HTMLUnknownElement: un elemento válido, con todos los métodos del DOM, sin ningún comportamiento y sin caja de renderizado propia. Es lo mismo que ocurriría con document.createElement('pepito').
Un HTMLUnknownElement dentro de un svg no significa nada para el motor de renderizado de SVG, que solo dibuja elementos del espacio de nombres SVG.
La versión correcta:
const NS = 'http://www.w3.org/2000/svg';const c = document.createElementNS(NS, 'circle');c.setAttribute('cx', 50);c.setAttribute('cy', 50);c.setAttribute('r', 40);c.setAttribute('fill', '#f38ba8');svg.appendChild(c);Esa cadena de URL no es una dirección que se visite: es un identificador. No hay nada en ese servidor que el navegador consulte. Es simplemente una cadena única y acordada que distingue el vocabulario SVG del de HTML, de MathML y de cualquier otro.
La manera de verificarlo, útil al depurar:
console.log(c.namespaceURI); // http://www.w3.org/2000/svgconsole.log(c instanceof SVGCircleElement); // true si esta bienSi el segundo da false, ese es el problema y no otro.
Un ayudante mínimo
Escribir la constante en cada creación es tedioso. El ayudante estándar:
const NS = 'http://www.w3.org/2000/svg';function el(nombre, atributos = {}, hijos = []) { const nodo = document.createElementNS(NS, nombre); for (const [k, v] of Object.entries(atributos)) { if (v == null || v === false) continue; nodo.setAttribute(k, String(v)); } for (const h of hijos) { nodo.appendChild(typeof h === 'string' ? document.createTextNode(h) : h); } return nodo;}// Usoconst grafico = el('svg', { viewBox: '0 0 200 100', width: 400 }, [ el('rect', { x: 10, y: 10, width: 60, height: 80, fill: '#89b4fa' }), el('text', { x: 40, y: 100, 'text-anchor': 'middle', 'font-size': 12 }, ['A']),]);Dos detalles del ayudante que se agradecen en uso real. El salto de los valores nulos permite construir atributos condicionales sin ramificar. Y el String(v) evita que un número se convierta en "NaN" sin avisar cuando el cálculo ha fallado; si prefieres detectarlo, añade una comprobación.
setAttributeNS y xlink
setAttribute a secas funciona para casi todos los atributos de SVG, porque casi todos están sin espacio de nombres. viewBox, d, fill, transform, stroke-width: todos con setAttribute normal.
La excepción histórica es xlink:href, que en SVG 1.1 pertenecía al espacio de nombres de XLink:
const XLINK = 'http://www.w3.org/1999/xlink';use.setAttributeNS(XLINK, 'xlink:href', '#simbolo');SVG 2 dejó obsoleto xlink:href en favor de un href normal, sin espacio de nombres:
use.setAttribute('href', '#simbolo'); // asi, en codigo nuevoLos motores actuales soportan las dos formas. En código nuevo, usa href a secas y olvida XLink. Si mantienes código que usa setAttributeNS con XLink, funciona y no hay urgencia en cambiarlo.
Un detalle relacionado: el atributo xmlns no se pone con setAttribute en un SVG que va a vivir dentro de un documento HTML, porque el espacio de nombres ya lo determina createElementNS. Solo hace falta cuando serializas el SVG a un fichero independiente, y ahí se pone como atributo literal en la cadena.
innerHTML sobre un elemento SVG
Esta es la que sorprende: funciona.
const g = document.createElementNS(NS, 'g');g.innerHTML = '<circle cx="20" cy="20" r="10" fill="#a6e3a1"/>';Los elementos creados tienen el espacio de nombres correcto. La razón es que el analizador de fragmentos usa el contexto del elemento sobre el que se llama: como g está en el espacio de nombres SVG, lo que se parsea dentro también.
Lo que no funciona igual de bien:
insertAdjacentHTMLsobre un elemento SVG: su soporte ha sido irregular.- Poner marcado SVG en el
innerHTMLde un elemento HTML sin unsvgque lo envuelva: entonces el contexto es HTML y los elementos salen en el espacio de nombres equivocado. Undiv.innerHTML = '<circle/>'produce el mismoHTMLUnknownElementdel principio.
La vía robusta cuando construyes desde una cadena completa:
function desdeCadena(marcado) { const doc = new DOMParser().parseFromString( `<svg xmlns="${NS}">${marcado}</svg>`, 'image/svg+xml'); const err = doc.querySelector('parsererror'); if (err) throw new Error('SVG mal formado: ' + err.textContent); return [...doc.documentElement.childNodes] .map(n => document.importNode(n, true));}Tres cosas ahí. El xmlns explícito es obligatorio porque image/svg+xml se parsea como XML y el XML no tiene espacios de nombres implícitos. La comprobación de parsererror es necesaria porque el parseo de XML no lanza excepciones: devuelve un documento que contiene un elemento de error, y si no lo miras, insertas basura. Y importNode es imprescindible porque los nodos pertenecen a otro documento; insertarlos directamente falla o produce comportamiento indefinido.