Svg binding and limitations


#1

Due to the valid markup of the binding syntax, aurelia is well suited to binding to svg. We’ve created some modular visualisation components using aurelia so have quite a bit of experience trying it out.

Firstly, we found out very quickly that binding to large path strings was very slow:

<path d.one-way="humongousPathString" ... ></path>

Aurelia bindings should obviously not be used to pass huge primitive types, so it was considerably faster to simply set the attribute manually:

this.pathElement.setAttribute('d', newPath);

svg in component template causes a few issues

Aurelia svg components have to be wrapped in an svg element:

<template>
   <svg>
    ... component here
   </svg>
</template>

To get anything complex to display correctly, you’ll probably need to include the following CSS. This is because the default for the non-root svg is to hide the overflow:

svg {
  overflow: visible !important;
}

We spent ages trying to work out why the above approach was not working when we exported to png. It turns out you have to inline styles in every component for that to work. That gives us:

<template>
  <svg style="overflow: visible">

  </svg>
</template>

The nestled svg element also causes issues if we want to use a clipPath from a parent component as they don’t appear to work in the child svg. This means we currently have to copy the clip dimensions into some components and render the <clipPath> within it:

<template>
  <svg style="overflow: visible">
    <clipPath id="clip">
      <rect id="clip-rect" x.bind="rect.x" y.bind="rect.y" width.bind="rect.width" height.bind="rect.height"></rect>
    </clipPath>

    <line clip-path="url(#clip)" stroke="#000" ...
  </svg>
</template>

Update

See below for a possible solution


Composed SVG elements missing attributes
#2

The overflow issue seems like a browser bug. Did you test in multiple browsers?


#3

The default in Chrome is to hide the overflow:
image

So we have no choice but to include it inline if we want it to display correctly when converting to png.


#4

I’m guessing that the wrapping <svg> in the templates is so that the elements are created with the correct namespace?

It looks like it’s possible to remove the wrapping svg using a custom attribute which simply copies the elements out when it’s attached. This fixes all the above issues.

<template>
  <svg svg-remove>
    <rect x.bind="rect.x" y.bind="rect.y" width.bind="rect.width" height.bind="rect.height" style="background-color:red"></rect>
  </svg>
</template>
import {inject, customAttribute} from 'aurelia-framework';

@customAttribute('svg-remove')
@inject(Element)
export class SvgRemove {
  constructor(element){
    this.element = element;
  }
  
  attached(){
    for(var i = 0; i< this.element.children.length; i++){
      this.element.parentNode.appendChild(this.element.children[i])
    }
    this.element.remove();
  }
}

#5

I’ve been trying out my svg-remove attribute with some of our more complicated visualisations.

Unfortunately, the DOM manipulation the attribute does on attached breaks aurelias binding to the elements and they’re not removed properly when the contents of a repeat.for change.


#6

I’ve finally had some time to look into this and find a solution. The following code removes the wrapping <svg svg-remove> in the beforeCreate hook which seems to work.

My next step is to create a @containerlessSVG decorator which adds the existing containerless metadata and also applies the below hook without requiring the svg-remove attribute.

Does anybody know how I would apply this selectively from a decorator?

const viewResources = container.get(ViewResources);

viewResources.registerViewEngineHooks({
  beforeCreate: (view, container, fragment) => {
    if(fragment.children.length === 1 && 
      fragment.children[0].tagName === 'svg' && 
      fragment.children[0].attributes.getNamedItem('svg-remove')
    ) {
    const root = fragment.children[0];   
  
    for(var i = 0; i < root.children.length; i++){
      fragment.appendChild(root.children[i])
    };
           
    fragment.removeChild(root)
  }
 }
});

#7

Nice that you figured that out, but you shouldn’t be hooking into create phase unnecessarily, here is an example of how to do it in compile phase: https://github.com/bigopon/aurelia-fractals/blob/9e8cb952c0fc737fde7406e0c59d1f0e1cb592ec/src/pythagoras.ts#L81-L95

@viewEngineHooks()
export class RemoveSvg {
  beforeCompile(fragment: DocumentFragment) {
    let svg = fragment.querySelector('svg');
    if (svg) {
      let remove = svg.hasAttribute('remove');
      if (remove) {
        while (svg.childElementCount) {
          (svg.parentNode as Element).insertBefore(svg.firstElementChild, svg);
        }
        (svg.parentNode as Element).removeChild(svg);
      }
    }
  }
}

And the html looks very similar to yours: https://github.com/bigopon/aurelia-fractals/blob/master/src/pythagoras.html

<template transform.bind='getTransform(x, y, w, A, B)'>
  <svg remove>
      <!-- ... -->
  </svg>
</template>

#8

Thanks @bigopon.

I looked at beforeCompile but couldn’t get it working.


#9

probably because you forgot to remove <svg-rect/> via containerness ? I saw you commented it out


#10

I just noticed in your pythagoras code you have containerless commented out too so assumed that was the way to go.

If I use containerless, the <svg> never gets removed.

image


#11

Yes, it’s there because it’s the svg of <svg-rect/> we want to remove, not svg inside <app/> so we need to declare view engine hooks for <svg-rect/>


#12

Doh, I have to import it where it’s required! Thanks for the pointers.
I might try and do a docs PR because this this is kinda essential knowledge for doing anything complex with SVG and Aurelia.

By the way, that fractals demo is awesome!


#13

Please do :wink:

POST REQUIRES AT LEAST 20 CHARS