hyperapp: Namespacing wrong when adding children to SVG tag

I have an app which makes random svg circles (test case for a bigger project). Initially there are three circles, but a button lets you add more.

The new circle elements are added, but with the wrong namespace: xhtml rather than svg (tested in Chrome 59.0.3071.115). This can be seen by opening the developer tools, going to elements, and then clicking “Add Circle”. A new circle tag is added but the namespace URI is wrong, so svg ignores it and it is not rendered.

The “Move” button is not part of the bug report; it was added to see if altering attributes (position) works - it does.

The code is below:

[Note also usage of Object.assign in h( ) call - if I just pass this.attr, the change in attributes isn’t picked up]

<script src="https://unpkg.com/hyperapp"></script>
<body></body>
<script>
var h = hyperapp.h;
var app = hyperapp.app;

function randInt(a,b) { // random integer in [a,b]
    return Math.floor(Math.random()*(b+1-a))+a;
}

// random circle object

function Circle() {
    this.attr = {
        cx: randInt(1,400),
        cy: randInt(1,400),
        r:  randInt(10,30)
    }
}

Circle.prototype = {
    h: function() {
        // a change in attributes isn't picked up unless Object.assign is used!
        return h("circle", Object.assign({}, this.attr))
    },
    move: function() {
        this.attr.cx = randInt(1,400);
        this.attr.cy = randInt(1,400);
        return this;
    }
}

// the app

app({
    state: { // just a list of objects
        objects: [],
    },

    view: function v(state, actions) {
        return h("div",{},[
            // buttons
            h("button", {onclick: actions.addObj}, 'Add circle'),
            h("button", {onclick: actions.move}, 'Move'),
            h("hr"),
            // svg area
            h("svg", {width:400, height:400, style:{backgroundColor: 'lavender'}}, 
                state.objects.map(obj => obj.h())
            )
        ])
    },

    actions: {
        addObj: function(state) {
            // add a new object
            return {
                objects: state.objects.concat(new Circle())
            }
        },
        move: function(state) {
            // moves all the objects
            return {
                objects: state.objects.map(obj => obj.move())
            }
        }
    },

    events: {
        init: function(state, actions) {
            // put 3 random objects in the state
            actions.addObj(state)
            actions.addObj(state)
            actions.addObj(state)
        }
    }
})

</script>

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Comments: 19 (10 by maintainers)

Commits related to this issue

Most upvoted comments

@w-mcilhagga Thanks! I’ll refactor this patch a bit and create a commit soon with other small changes I am currently working on (unless you really want to send me a PR 😏).

Anyway, I’ll make sure to thoroughly thank you in the commit message!

Good job! Also got the SVG fractal example to finally work too! 😅 🎉

2017-07-15 22 01 26

OK, patched the patch function. This works:

      function patch(parent, element, oldNode, node, isSVG) {
        if (oldNode == null) {
          element = parent.insertBefore(createElement(node, isSVG), element)
        } else if (node.tag != null && node.tag === oldNode.tag) {
          isSVG = isSVG || (node.tag==='svg')
          updateElementData(element, oldNode.data, node.data)
          
          var len = node.children.length
          var oldLen = oldNode.children.length
          var reusableChildren = {}
          var oldElements = []
          var newKeys = {}

          for (var i = 0; i < oldLen; i++) {
            var oldElement = element.childNodes[i]
            oldElements[i] = oldElement

            var oldChild = oldNode.children[i]
            var oldKey = getKey(oldChild)

            if (null != oldKey) {
              reusableChildren[oldKey] = [oldElement, oldChild]
            }
          }

          var i = 0
          var j = 0

          while (j < len) {
            var oldElement = oldElements[i]
            var oldChild = oldNode.children[i]
            var newChild = node.children[j]

            var oldKey = getKey(oldChild)
            if (newKeys[oldKey]) {
              i++
              continue
            }

            var newKey = getKey(newChild)

            var reusableChild = reusableChildren[newKey] || []

            if (null == newKey) {
              if (null == oldKey) {
                patch(element, oldElement, oldChild, newChild, isSVG)
                j++
              }
              i++
            } else {
              if (oldKey === newKey) {
                patch(element, reusableChild[0], reusableChild[1], newChild, isSVG)
                i++
              } else if (reusableChild[0]) {
                element.insertBefore(reusableChild[0], oldElement)
                patch(element, reusableChild[0], reusableChild[1], newChild, isSVG)
              } else {
                patch(element, oldElement, null, newChild, isSVG)
              }

              j++
              newKeys[newKey] = newChild
            }
          }

          while (i < oldLen) {
            var oldChild = oldNode.children[i]
            var oldKey = getKey(oldChild)
            if (null == oldKey) {
              removeElement(element, oldElements[i], oldChild)
            }
            i++
          }

          for (var i in reusableChildren) {
            var reusableChild = reusableChildren[i]
            var reusableNode = reusableChild[1]
            if (!newKeys[reusableNode.data.key]) {
              removeElement(element, reusableChild[0], reusableNode)
            }
          }
        } else if (
          element != null &&
          node !== oldNode &&
          node !== element.nodeValue
        ) {
          var i = element
          parent.replaceChild((element = createElement(node, isSVG)), i)
        }

        return element
      }
    }