Technique: Storing Arbitrary Objects in HTML Links

June 5th, 2007 by Roger Braunstein

I’d like to share an ActionScript 3.0 technique I’ve been using on a recent project. As any Flash developer will tell you, it’s always a huge time-saver to use formatted HTML text over multiple text fields laid out by hand. However, when you’re creating objects by hand and listening to click events for each one, you can also associate some data with them. I’m sure you’ve done this before: you keep a reference to something you want to select or activate or pass in each movieclip/sprite, and when it receives a click event, the receiver of the event is that individual clip which has the associated reference. You can simulate multiple clickable text clips with one HTML text field, but you lose that one-to-one association of clip to object.

The technique I want to explain lets you store arbitrary objects inside HTML links. Even though you only have one instance that contains the HTML text field, and one kind of event that’s fired from an HTML link, you can interpret the href of the clicked link and retrieve any kind of object back out of it, even custom class instances. I use this to create “links” which navigate within the flash movie: a Link object encodes the destination, is encoded into an HTML link, and placed in an HTML text field; the click triggers an event handler which interprets the link back into the original Link object and then passes it to a navigation class. Read how it’s done below.

The technique is very simple. If you create a link with the event: pseudo-protocol, it will be handled by Flash, and anything after event: will be stored in the TextEvent event object. So all we have to do is encode the object in a way that doesn’t break out of the surrounding HTML:

<a href="event:[[Object is encoded here]]">...</a>

So the object must be encoded in a string that must not contain double quotes, or other HTML entities that could even potentially interfere with parsing like < and >.

Base 64 encoding is the natural way to go. It’s made to stay within the most normal of ASCII ranges: A-Z, 0-9, + and /. So all we have to do is encode the object in binary, and convert the binary to Base 64. Flex comes with Base 64 encoding classes built-in, fortunately. Getting the object to binary just involves making sure it can be serialized with AMF3. In short, to make your class eligible for automatic serialization and deserialization by AMF3:

  1. The constructor must take no arguments
  2. Fields must be public or they won’t be saved
  3. You must register it with a class alias by calling flash.net.registerClassAlias(aliasString, class).

If this is unacceptable, you can easily or implement custom serialization/deserialization functions by making your custom class implement IExternalizable.

Of course, if you’re just saving a basic type, this is overkill — but it works — so you gain a lot by generality.

The Code

In HTMLSerializer.as:

package com.partlyhuman.util
{
	import flash.events.TextEvent;
	import flash.utils.ByteArray;

	import mx.utils.Base64Decoder;
	import mx.utils.Base64Encoder;

	public class HTMLSerializer
	{
		public static function makeObjectLink(object:*, text:String, styleClass:String = null):String
		{
			var bytes:ByteArray = new ByteArray();
			bytes.writeObject(object);
			bytes.position = 0;
			var b:Base64Encoder = new Base64Encoder();
			b.encode(bytes.readUTFBytes(bytes.length));
			var href:String = "event:" + b.drain();

			return HTMLGenerator.a(href, text, styleClass);
		}

		public static function retrieveObjectFromLinkEvent(linkEvent:TextEvent):*
		{
			if (linkEvent.type != TextEvent.LINK)
			{
				throw new Error("Inconsistent event type received. Expected TextEvent.LINK.");
				return null;
			}
			var b:Base64Decoder = new Base64Decoder();
			b.decode(linkEvent.text);
			var bytes:ByteArray = b.drain();

			return bytes.readObject();
		}
	}
}

In HTMLGenerator.as (just a utility class so I can be lazy):

package com.partlyhuman.util
{
	public class HTMLGenerator
	{
		public static function a(href:String, text:String, styleClass:String = null):String
		{
			return '<a href="' + href + '"'
					+ ((styleClass)? ' class="'+styleClass+'"' : "")
					+ ">" + text + "</a>";
		}

		public static function img(src:String, alt:String = "image", width:int = 0, height:int = 0):String
		{
			return '<img src="' + src + '" alt="' + alt + '"'
				+ ((width > 0)? ' width="' + width.toString() + '"' : "")
				+ ((height > 0)? ' height="' + height.toString() + '"' : "")
				+ '/>';
		}
	}
}

Testing it

Here’s an object I will serialize, called TestObject:

package com.partlyhuman.test
{
	public class TestObject
	{
		public var a:Array;
		public var s:String;
		public var i:int;

		public function toString():String
		{
			return "[TestObject\n"
			 + "\ta = " + a.join(", ") + "\n"
			 + "\ts = " + s + "\n"
			 + "\ti = " + i.toString() + "\n"
			 + "]";
		}
	}
}

And here’s a simple test case:

package com.partlyhuman.test
{
	import com.partlyhuman.util.HTMLGenerator;
	import com.partlyhuman.util.HTMLSerializer;

	import flash.display.Sprite;
	import flash.events.TextEvent;
	import flash.net.registerClassAlias;
	import flash.text.TextField;
	import flash.text.TextFieldAutoSize;

	public class TestHTMLSerializer extends Sprite
	{
		public function TestHTMLSerializer()
		{
			registerClassAlias("com.partlyhuman.test.TestObject", TestObject);

			var obj1:TestObject = new TestObject();
			obj1.a = ("This is object number one, hello!").split(" ");
			obj1.s = "Atari";
			obj1.i = 2600;

			var obj2:TestObject = new TestObject();
			obj2.a = ("Hello from object numero dos!").split(" ");
			obj2.s = "Nintendo";
			obj2.i = 64;

			var tf:TextField = new TextField();
			tf.width = 200;
			tf.multiline = true;
			tf.wordWrap = true;
			tf.autoSize = TextFieldAutoSize.LEFT;
			tf.htmlText = "Testing " + HTMLSerializer.makeObjectLink(obj1, "Link to object 1")
				+ " And also " + HTMLSerializer.makeObjectLink(obj2, "Link to object 2")
				+ " And why not " + HTMLSerializer.makeObjectLink("simple string", "A simple string");
			addChild(tf);

			tf.addEventListener(TextEvent.LINK, onLink);
		}

		protected function onLink(event:TextEvent):void
		{
			trace(HTMLSerializer.retrieveObjectFromLinkEvent(event).toString());
		}
	}
}

You can see the results below (if you have flash player 9 and javascript):

If you see this message, you need to install Flash Player 9.


So there you have it!

6 Responses to “Technique: Storing Arbitrary Objects in HTML Links”

  1. Romeo says:

    Really nice … can i use it to build a TagCloud?

  2. gabriel montagné says:

    this technique is really really cool.

    there’s one “silly” shortcoming, not to this technique, but to html links with ‘event:’ in general, pointed here:

    http://www.blog.lessrain.com/?p=584

    you can’t control the context menu for these links, and if you open them not directly but through the “Open” item on the context menu, not only the event will not be dispatched, but the player will pass the broswer the url, which, of course, will not be understood.

    How could this be worked around?

  3. [...] site also uses a bit of the dynamic text wrapping and object serialization techniques I posted about [...]

  4. Art Monstro says:

    I question the use of embedding object data in html. Basically you have a loosely formatted string(html) in which you are embedding arbitrary object data.

    Really you would be better off using XML with nodes within your text data. Consider the circumstance of having to allow an end user (or even a CMS) update the data of your links — there’s no enforcement of standards.

    Also, if you’re still doing new Object() and setting properties, youre already screwed. Its too loosely typed.

  5. Art, this would be used only for your code to link objects at runtime. I’ve used this technique to be lazy about building UIs that use one HTML textfield instead of multiple text fields. I agree that this has serious problems for any sort of human-entered data, and I would never use it for that.

    And whether it’s too loosely typed or not really depends on what you’re using it for. Usually I’m just storing an ID or a few string keys, and I would never give a class outside the one that created this htmlText navigation element direct access to those untyped Objects. That said, I do agree with you that this is a hack… I just happen to think it’s a cute one. I should have made that more clear.

  6. A better solution to storing objects in HTML text (which will run much faster as well) is creating a simple UID hash of the objects you need to reference back to, storing the UID in the event: href, and looking up the original object reference in your hash.

    This approach might be more useful in some cases, for example if you wish to forget about the original objects or collect them or re-use their references for other data; then storing a full copy in the HTML will preserve the objects.

Leave a Reply