Menu

Introducing Mutation Events

October 9, 2002

Antoine Quint

Last month we completed work on a simple yet functional implementation of text wrapping for SVG. My only complaint is that the text-wrapping component lies about its XML integration. Last month I raved about how this text-wrapping component was a clean extension of SVG 1.0; but, looking closer, it appears that it only does so in a superficial way.

In the attached demo, I had little buttons that would allow you to change the alignment and font size. The code looked something like this:

function updateLayout (frame) {

  var text = TextWrap._instances[0];

  var action = frame.parentNode.getElementsByTagNameNS(SVG.ns, 'use').

       item(1).getAttribute('xlink:href').substr(1);

  if (action == 'plus') {

    text.setFontSize( (parseInt(text.getFontSize()) + 2) + 'px' );

  } else if (action == 'minus') {

    text.setFontSize( (parseInt(text.getFontSize()) - 2) + 'px' );

  } else {

    text.setTextAlign(action);

  }

}

Do you see what's wrong. The point of an XML grammar to define the component was for the developer experience to be the same with this <text:wrap> element as with any other SVG element. When it comes to changing an SVG element's features, the DOM is the only choice. But in the code above I don't use any of the DOM API to tweak my component, although it is expressed as XML. I resorted to some dodgy custom methods instead, and I accessed my component though a Class variable. All of which is suspicious as far as XML integration is concerned. Developers shouldn't have to learn new APIs for every component they use, especially since they already know the DOM API and XML principles. Why not leverage this knowledge?

Mutate!

You've probably used DOM Events, at least in some simple way. For instance, the little buddy that is sent as a parameter to your event handler functions is a DOM Event. This API tells you all you need to know about who fired the event, what the target is, mouse coordinates, etc. Every user interaction on an SVG document triggers a DOM Event of some sort. The DOM Events specification also includes something called Mutation Events.

DOM Events are not only fired when you interact with the document -- with mouse or keyboard -- but also when something is changed within the DOM. For example, when I change the x position of a rectangle,

myRect.setAttribute('x', newPosition);

the SVG implementation fires a Mutation Event, which informs developers of every kind of mutation that happens in the document, not simply those occuring through the user interface. Suppose I wanted to allow the user to use the DOM instead of a custom method to change the width of a <text:wrap> element. What I have now is a method, TextWrap.setWidth(). I am actually going to keep this method around. What I am going to do though is to add a little more code to my component. Each instance of this component has a pointer to the actual DOM Element that represents it in the XML world. What I want is to be told when the width attribute gets changed. Looking at the Interactivity chapter of the SVG spec, which contains a list of all supported events, I see that DOMAttrModified does exactly what I want. In order to be called when the attribute is changed, I need to register an event listener on my element:

this._node.addEventListener('DOMAttrModified', TextWrap.mutate, false);

The new TextWrap.mutate() method will thus act like a bridge between the XML and its corresponding TextWrap object. Let's examine some of the information available from the MutationEvent we receive:

interface MutationEvent : Event {

  readonly attribute DOMString        prevValue;

  readonly attribute DOMString        newValue;

  readonly attribute DOMString        attrName;

};

MutationEvent inherits from the Event interface, which provides goodies like target. From this interface we can get to the name of the attribute that was changed (attrName), its value before the mutation occurred (prevValue), and the new value (newValue). Having old and new values is useful: while a MutationEvent cannot be cancelled -- the new value has already been applied to the attribute within the DOM -- we can always have a method that will check if the new value given actually makes any sense, which means that, if something goes wrong, the attribute can be set back to its original value.

Gripes

All of which is fine, but there is a major drawback to using mutation events. In short, no shipped version of the Adobe SVG Viewer actually implements mutation events. It is a sad state of affairs to have such an amazing feature not available, but the good news is that Adobe has recently called for feedback about which missing features are most wanted by the community. If you care about mutation events, make yourself heard by voting for mutation events in an upcoming release of the viewer at the SVG-Developers YahooGroups poll.

But Adobe's SVG Viewer is not the only game in town. Apache Batik implements mutation events, though it doesn't implement some features necessary for the TextWrap class. So I cooked up a simpler triangle component that uses mutations. If you followed previous columns, the code should be easy enough to understand (Take a look -- this was tested against Batik 1.5b4).

There are two other problems when working with mutation events. Having them addresses the DOM integration part of the problem of components design, but there are two other technologies that you might want to integrate with: SMIL and CSS.

Suppose I wanted an SVG button to control the width of my text-wrapped text. Someone accustomed to SVG might want to use the SMIL <set> attribute to apply a new value to the width attribute of our component when the button is clicked. Most people think <set> works like calling the DOM setAttribute() method. Hence people expect a mutation event to be fired once the <set> has been applied. Alas, this is not going to happen. In SVG two DOMs exist in parallel. The first is the one you know best, namely, the core DOM that's a representation of your document. The other is something called the animated DOM, which stores animated values for each attribute which may be animated. Thus when an attribute is being animated, you can access its core or underlying value as well as the animated value that is the one currently applied visually to your attribute. So <set> only makes a change to the animated DOM. There is no way to monitor changes made to the animated DOM.

Also in Sacré SVG

Big Lists in Small Spaces

SVG At the Movies

Mobile SVG

SVG and Typography: Animation

Going Mobile With SVG: Standards

There is a similar issue with CSS. Just as DOM Core prevents you from parsing XML data back and forth to access your structured XML data, DOM CSS (or CSSOM) eases the manipulation of CSS data. In SVG all elements can be styled using the style attribute, even my text-wrapping component. SVG developers are most likely to use the CSSOM CSSStyleDeclaration object attached to their element, rather than actually doing a DOM setAttribute() call on the style attribute. Alas, here again, using the helpful setProperty() method from CSSStyleDeclaration will not trigger a mutation event. This issue has long been known and Microsoft even came up with some proprietary solution within IE. It's another head-scratching limitation.

Wrapping it All Up

You should have a better understanding of what mutation events are and how they can help you in designing SVG extensions that behave much like other SVG elements. You've also seen that all is not perfect: there are limitations to using mutation events with SMIL and CSS. But all in all mutation events can be very useful for component design.