jsTree and Nested XML Data Stores
I happened upon jsTree a few months back while searching for a solid jQuery based tree.
Without a doubt, it is one of the most well implemented and functional Javascript trees I’ve used with perhaps the most powerful feature being the built-in support for client-side XML representations of the tree and the ability to add arbitrary metadata to the tree using the Sarissa library.
While the tree itself is extremely powerful, some of the documentation is actually out of date and made my implementation of the tree a bit more tasking than it should have been.
For example, the option for initializing the tree with a static XML string (as demonstrated here) actually requires using staticData instead of static in the latest version. Adding metadata to the tree is also made far more complicated by the documentation and examples I found online.
In reality, the datastore implementation for the nested XML support is powerful enough to extract arbitrary DOM attributes (online posts seem to indicate that you need to use the custom metadata plugin and/or the jQuery metadata plugin). You can see in the sample below, that I set the attribute “md” to an encoded JSON string (you’ll want to do this for when you reload the tree as a Javascript string) and then retrieve the value as a part of the XML by specifying the attributes to collect:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<span style="font-family: Lucida Console;"><span style="color: #008000;">/*--- test adding data ---*/</span> <span style="color: #000000;">$(</span><span style="color: #a56dbc;">"#test"</span><span style="color: #000000;">).click(</span><span style="color: #4d70ea;">function</span><span style="color: #000000;">() {</span> <span style="color: #4d70ea;">var </span><span style="color: #000000;">t = $.tree.focused();</span></span> <span style="font-family: Lucida Console;"> <span style="color: #4d70ea;">if </span><span style="color: #000000;">(!t.selected) {</span> <span style="color: #4d70ea;">return</span><span style="color: #000000;">;</span> <span style="color: #000000;">}</span></span> <span style="font-family: Lucida Console;"> <span style="color: #008000;">/*--- sets a person on the node ---*/</span> <span style="color: #4d70ea;">var </span><span style="color: #000000;">person = {</span> <span style="color: #a56dbc;">"FirstName"</span><span style="color: #000000;">: $(</span><span style="color: #a56dbc;">"#firstName"</span><span style="color: #000000;">).val(),</span> <span style="color: #a56dbc;">"LastName"</span><span style="color: #000000;">: $(</span><span style="color: #a56dbc;">"#lastName"</span><span style="color: #000000;">).val(),</span> <span style="color: #a56dbc;">"Age"</span><span style="color: #000000;">: $(</span><span style="color: #a56dbc;">"#age"</span><span style="color: #000000;">).val() </span> <span style="color: #000000;">};</span></span> <span style="font-family: Lucida Console;"> <span style="color: #4d70ea;">var </span><span style="color: #000000;">serialized = JSON.stringify(person);</span></span> <span style="font-family: Lucida Console;"> <span style="color: #008000;">/*--- sample of adding an arbitrary attribute at the DOM level ---*/</span> <span style="color: #000000;">t.selected.attr(</span><span style="color: #a56dbc;">"md"</span><span style="color: #000000;">, encodeURI(serialized));</span></span> <span style="font-family: Lucida Console;"> <span style="color: #008000;">/*--- ...and how to retrieve it in XML ---*/</span> <span style="color: #4d70ea;">var </span><span style="color: #000000;">opts = {};</span> <span style="color: #000000;">opts.outer_attrib = [</span><span style="color: #a56dbc;">"id"</span><span style="color: #000000;">, </span><span style="color: #a56dbc;">"rel"</span><span style="color: #000000;">, </span><span style="color: #a56dbc;">"class"</span><span style="color: #000000;">, </span><span style="color: #a56dbc;">"md"</span><span style="color: #000000;">];</span></span> <span style="font-family: Lucida Console;"> <span style="color: #4d70ea;">var </span><span style="color: #000000;">xml = t.get(</span><span style="color: #4d70ea;">null</span><span style="color: #000000;">, </span><span style="color: #a56dbc;">"xml_nested"</span><span style="color: #000000;">, opts)</span></span> <span style="font-family: Lucida Console;"> <span style="color: #000000;">$(</span><span style="color: #a56dbc;">"#xml-d"</span><span style="color: #000000;">).text(xml);</span> <span style="color: #000000;">$(</span><span style="color: #a56dbc;">"#treeXml"</span><span style="color: #000000;">).val(encodeURI(xml)); </span> <span style="color: #000000;">});</span></span> |
In real usage, you’d assign some more meaningful values to the metadata and save it. I find that with a library like this, it’s probably easier to just save the whole XML string and that’s simple enough by just pushing the XML to a hidden input before submitting the form (as I’ve done in the last line).
Mahr Mohyuddin has a much more complex and more generic implementation of ASP.NET integration here, but I think that might be more complexity than is needed. In practice, it makes more sense to use full JSON objects on the client side (as I’ve used above) and embed them into the attribute and then, using the JavaScriptSerializer class, extract the objects into domain objects on the server side. Here’s an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
<span style="font-family: Lucida Console;"><span style="color: #0000ff;">string </span><span style="color: #000000;">xmlString = </span><span style="color: #808000;">Uri</span><span style="color: #000000;">.UnescapeDataString(treeXml.Value);</span></span> <span style="font-family: Lucida Console;"><span style="color: #000000;">XDocument xml = XDocument.Parse(xmlString);</span></span> <span style="font-family: Lucida Console;"><span style="color: #008000;">// Get all the <items/>.</span> <span style="color: #000000;">var items = from x </span><span style="color: #0000ff;">in </span><span style="color: #000000;">xml.Descendants()</span> <span style="color: #000000;">where x.Name == </span><span style="color: #ff00ff;">"item"</span> <span style="color: #000000;">select x;</span></span> <span style="font-family: Lucida Console;"><span style="color: #000000;">List<Person> people = </span><span style="color: #0000ff;">new </span><span style="color: #000000;">List<Person>();</span></span> <span style="font-family: Lucida Console;"><span style="color: #000000;">JavaScriptSerializer serializer = </span><span style="color: #0000ff;">new </span><span style="color: #000000;">JavaScriptSerializer();</span></span> <span style="font-family: Lucida Console;"><span style="color: #008000;">// Resolve the paths and Person instances.</span> <span style="color: #0000ff;">foreach</span><span style="color: #000000;">(var item </span><span style="color: #0000ff;">in </span><span style="color: #000000;">items) {</span> <span style="color: #0000ff;">string</span><span style="color: #000000;">[] parts = item.AncestorsAndSelf()</span> <span style="color: #000000;">.Select(a => a.Descendants(</span><span style="color: #ff00ff;">"name"</span><span style="color: #000000;">).First().Value)</span> <span style="color: #000000;">.Reverse().Skip(</span><span style="color: #800080;">1</span><span style="color: #000000;">).ToArray();</span></span> <span style="font-family: Lucida Console;"> <span style="color: #0000ff;">string </span><span style="color: #000000;">path = </span><span style="color: #0000ff;">string</span><span style="color: #000000;">.Join(</span><span style="color: #ff00ff;">"/"</span><span style="color: #000000;">, parts);</span></span> <span style="font-family: Lucida Console;"> <span style="color: #0000ff;">if</span><span style="color: #000000;">(item.</span><span style="color: #808000;">Attribute</span><span style="color: #000000;">(</span><span style="color: #ff00ff;">"md"</span><span style="color: #000000;">) == </span><span style="color: #0000ff;">null</span><span style="color: #000000;">) {</span> <span style="color: #0000ff;">continue</span><span style="color: #000000;">; </span><span style="color: #008000;">// Next iteration.</span> <span style="color: #000000;">}</span></span> <span style="font-family: Lucida Console;"> <span style="color: #0000ff;">string </span><span style="color: #000000;">serializedPerson = </span> <span style="color: #808000;">Uri</span><span style="color: #000000;">.UnescapeDataString(item.</span><span style="color: #808000;">Attribute</span><span style="color: #000000;">(</span><span style="color: #ff00ff;">"md"</span><span style="color: #000000;">).Value);</span></span> <span style="font-family: Lucida Console;"> <span style="color: #000000;">Person p = serializer.Deserialize<Person>(serializedPerson);</span> <span style="color: #000000;">p.OrgPath = path;</span></span> <span style="font-family: Lucida Console;"> <span style="color: #000000;">people.Add(p);</span> <span style="color: #000000;">}</span></span> <span style="font-family: Lucida Console;"><span style="color: #000000;">_people.DataSource = people;</span> <span style="color: #000000;">_people.DataBind();</span></span> |
Nothing fancy here; the only thing of note is the little LINQ query to resolve the node path (may or may not be useful).
The Person class is also very simple and barebones:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
<span style="font-family: Lucida Console;"><span style="color: #0000ff;">using </span><span style="color: #008080;">System</span><span style="color: #000000;">;</span></span> <span style="font-family: Lucida Console;"><span style="color: #0000ff;">namespace </span><span style="color: #000000;">JsTreeSample {</span> <span style="color: #008000;">/// <summary></span> <span style="color: #008000;">/// Models a person.</span> <span style="color: #008000;">/// </summary></span> <span style="color: #008000;">/// <remarks></span> <span style="color: #008000;">/// Serializable to support deserialization from JSON.</span> <span style="color: #008000;">/// </remarks></span> <span style="color: #000000;">[Serializable]</span> <span style="color: #0000ff;">public class </span><span style="color: #000000;">Person {</span> <span style="color: #0000ff;">private string </span><span style="color: #000000;">_orgPath;</span> <span style="color: #0000ff;">private int </span><span style="color: #000000;">_age;</span> <span style="color: #0000ff;">private string </span><span style="color: #000000;">_firstName;</span> <span style="color: #0000ff;">private string </span><span style="color: #000000;">_lastName;</span></span> <span style="font-family: Lucida Console;"> <span style="color: #0000ff;">public string </span><span style="color: #000000;">FirstName {</span> <span style="color: #000000;">get { </span><span style="color: #0000ff;">return </span><span style="color: #000000;">_firstName; }</span> <span style="color: #000000;">set { _firstName = value; }</span> <span style="color: #000000;">}</span></span> <span style="font-family: Lucida Console;"> <span style="color: #0000ff;">public string </span><span style="color: #000000;">LastName {</span> <span style="color: #000000;">get { </span><span style="color: #0000ff;">return </span><span style="color: #000000;">_lastName; }</span> <span style="color: #000000;">set { _lastName = value; }</span> <span style="color: #000000;">}</span></span> <span style="font-family: Lucida Console;"> <span style="color: #0000ff;">public int </span><span style="color: #000000;">Age {</span> <span style="color: #000000;">get { </span><span style="color: #0000ff;">return </span><span style="color: #000000;">_age; }</span> <span style="color: #000000;">set { _age = value; }</span> <span style="color: #000000;">}</span></span> <span style="font-family: Lucida Console;"> <span style="color: #0000ff;">public string </span><span style="color: #000000;">OrgPath {</span> <span style="color: #000000;">get { </span><span style="color: #0000ff;">return </span><span style="color: #000000;">_orgPath; }</span> <span style="color: #000000;">set { _orgPath = value; }</span> <span style="color: #000000;">}</span> <span style="color: #000000;">}</span> <span style="color: #000000;">}</span></span> |
The full project is included. Some usage notes: select a node first and then enter a first name, last name, and age. Then click “Set Data” to create a Person object at the node (this will also show the full XML of the tree). Then click Submit to send the data (displays in a repeater).
Definitely check out jsTree for your next project; it’s amazingingly versatile and rich in functionality.
Go for it.
I am currently not on Twitter; my mind isn’t interesting enough (or irreverent enough) for it.