Introducing Mutation Events
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?
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.
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.
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.
XML.com Copyright © 1998-2006 O'Reilly Media, Inc.