Search code examples
htmlcssangularsvgsvg-filters

Why is <feDropShadow> not displaying?


Edit: trying to create a mcve I wasn't able to repro the issue. Now I'm completely baffled. Works on codesandbox, not in my project.


Initial question
I want to create a dynamic inline SVG element and map its rotation to an [(ngModel)]. Nothing fancy.

The fancy part is that I want to use a <filter> with a <feDropShadow>. And I want the shadow to be dynamic (always point up, regardless of the needle's rotation). It's something I've done before using Vue.

Here's a fiddle demonstrating the effect: https://jsfiddle.net/websiter/y4ghan0k/

But, for the life of me, I can't get the <feDropShadow> to work in Angular when the <svg> is inlined in the template. It just won't display. No error or warning. If I insert it as <img src="path/to/svg"> it works as expected (the shadow is displayed), but then I can't rotate the path anymore, as the element transformed needs to be a child of the element bearing the filter.

Note it's not because of this url() filter issue - I am prefixing the filter with this.location.path().

Here's the gist of my Angular code:

component.ts:

import { Location } from '@angular/common';

export class SomeComponent {
  constructor(private location: Location) {}
  dsLink = `url(${this.location.path()}#drop-shadow)`;
}

component.html:

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="180" y="100"
     viewBox="0 0 180 100" xml:space="preserve">
  <defs>
    <filter xmlns="http://www.w3.org/2000/svg" id="drop-shadow" height="130%">
      <feDropShadow dx="0" dy="-4" flood-color="rgba(0,0,0,.65)"/>
    </filter>
  </defs>
  <g [attr.filter]="dsLink">
    <path fill="#fff" d="M102.2,89.5c0-0.1,0-0.1,0-0.2c0-0.2,0-0.4-0.1-0.6L92.9,6.8c-0.1-0.8-3.2-0.9-3.3,0
      L78.7,88.5c-0.1,0.2-0.1,0.4-0.1,0.6c0,0.1,0,0.1,0,0.2l0,0.1c0,0,0,0.1,0.1,0.1c0.5,2.4,5.6,4.4,11.7,4.4
      c6.2,0.1,11.2-1.8,11.8-4.2c0,0,0.1-0.1,0.1-0.1L102.2,89.5z">
    </path>
  </g>
</svg>

For simplicity, I've removed the [(ngModel)] from path which is supposed to rotate the needle.

The filter url() appears to be correct, there's no error. But the shadow is not displayed.

Is there anything special I need to do/know in order to make Angular handle <svg> elements inline?

What am I missing?


Solution

  • I finally cracked it so I'm posting it here, hoping it will help others.

    In short: use unique ids for filters in each of your component instances. Otherwise, each instance will use the first filter found in DOM (with that id) and if that filter happens to be inside a parent with display: none, visibility:hidden or opacity: 0, applying the filter will make whatever you apply it to invisible as well.


    The issue had to do with the fact I was using the same component in different tabs. This created separate instances of the component, each of them using the same id (#drop-shadow). While having duplicate ids is obviously invalid HTML, this wouldn't actually have been a problem if we weren't dealing with filters. Because, since the <defs> are identical, it wouldn't really matter if the component on the 4th tab would use the <defs> defined by the component on the first tab.

    Except when dealing with <filter>s, because they are actually calculating, pixel by pixel, the rendering result, dynamically. Which means that, when the <svg> defining the <filter> is not rendered, using the filter will make the browser calculate (pixel by pixel) the result applying the filter and it always result in all the pixels being invisible.

    So the solution is to assign a unique id in each separate instance of the component.