Introducing Mutation Events
October 9, 2002
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 |
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.