How can I get Knockout.js to set the namespaceURI

2019-05-18 05:35发布

In an svg, if I use knockout to set the xlink:href attribute for an a node, the attribute's namespace isn't set correctly, so the a doesn't work as a link when clicked.

For example, consider the following svg that contains two linked ellipses. One has its xlink:href attribute hardcoded, the other is set by knockout via the data-bind attribute:

<svg width="5cm" height="6cm" viewBox="0 0 5 6" version="1.1" 
     xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> 
  <rect x=".01" y=".01" width="4.98" height="5.98"  
        fill="none" stroke="blue"  stroke-width=".03"/> 
  <a xlink:href="#hardcoded"> 
    <ellipse data-bind="attr: blue" /> 
  </a> 
  <a data-bind="attr: { 'xlink:href': href }"> 
    <ellipse data-bind="attr: red" /> 
  </a> 
</svg>

Getting knockout to run is pretty easy:

ko.applyBindings( { 
  blue: { cx:2.5, cy:1.5, rx:2, ry:1, fill:"blue" },
  href: '#foo', 
  red: { cx:2.5, cy:4.5, rx:2, ry:1, fill:"red" },
});

But only the link for the hardcoded one works. If I add some code to view the namespaceURI value for the attribute node, I can see that the xlink:href attribute set by knockout has a null namespaceURI, as opposed to the hardcoded one, which is set to http://www.w3.org/1999/xlink.

Array.forEach( document.getElementsByTagName('a'), function(a){
  a.setAttribute('title', 'xlink:href namespaceURI = ' + a.getAttributeNode('xlink:href').namespaceURI);
});

You can view all this in a fiddle.

Is there an easy way to tell knockout what the correct namespace should be for an attribute, or do I need to write a custom binding?

2条回答
爷、活的狠高调
2楼-- · 2019-05-18 06:06

My fallback solution is to add this custom binding:

ko.bindingHandlers['attr-ns'] = {
  update: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
    ko.utils.objectForEach( ko.unwrap( valueAccessor() ), function(name, value){
      var prefixLen = name.indexOf(':');
      var prefix    = name.substr( 0, prefixLen );
      var namespace = prefixLen < 0 ? null : element.lookupNamespaceURI( prefix );

      element.setAttributeNS( namespace, name, ko.unwrap( value ) );
    });
  }
};

and change the template to use the attr-ns binding I defined:

<a data-bind="attr-ns: { 'xlink:href': href }">
  <ellipse data-bind="attr: red" />
</a>

This seems to work ok, but I'd rather not do this if I don't have to. More custom code = more that could go wrong.

You can view this solution in a fiddle.

查看更多
We Are One
3楼-- · 2019-05-18 06:24

You'd have to override the default attr binding handler with a namespace-aware version.

FWIW, here is my take on it, use it as a transparent drop-in, it even supports namespace prefixes:

var attrHtmlToJavascriptMap = { 'class': 'className', 'for': 'htmlFor' },
    namespaceDecl = {
        svg: "http://www.w3.org/2000/svg",
        xlink: "http://www.w3.org/1999/xlink"
    };

ko.bindingHandlers['attr'] = {
    'update': function(element, valueAccessor, allBindings) {
        var value = ko.utils.unwrapObservable(valueAccessor()) || {};
        ko.utils.objectForEach(value, function(attrName, attrValue) {
            var attrRawValue = ko.utils.unwrapObservable(attrValue),
                toRemove = (attrRawValue === false) || (attrRawValue === null) || (attrRawValue === undefined),
                attrStrValue = attrRawValue.toString(),
                attrNameParts = attrName.split(":"),
                attrNsUri = (attrNameParts.length === 2) ? namespaceDecl[attrNameParts[0]] : null,
                attrName = (attrNsUri) ? attrNameParts[1] : attrNameParts[0];

            if (toRemove) {
                if (attrNsUri) {
                    element.removeAttributeNS(attrNsUri, attrName);
                } else {
                    element.removeAttribute(attrName);
                }
            }

            if (ko.utils.ieVersion <= 8 && attrName in attrHtmlToJavascriptMap && !attrNsUri) {
                attrName = attrHtmlToJavascriptMap[attrName];
                if (toRemove)
                    element.removeAttribute(attrName);
                else
                    element[attrName] = attrValue;
            } else if (!toRemove) {
                if (attrNsUri) {
                    element.setAttributeNS(attrNsUri, attrName, attrStrValue);
                } else {
                    element.setAttribute(attrName, attrStrValue);
                }
            }

            if (attrName === "name") {
                ko.utils.setElementName(element, toRemove ? "" : attrValue.toString());
            }
        });
    }
};

http://jsfiddle.net/ZghP7/1/

查看更多
登录 后发表回答