Emerald Hand Wiki

Sider - Dev - Tutorials - View - Notes

Modified: 2007/03/21 18:55 by Ornus - Categorized as: Development, Sider, Tutorial
Back to tutorials section


Edit

Introduction

This tutorial describes how to create a complex view to show and manage notes. I'm going to make tutorial iterative. First we'll create a skeleton of the view, enough for the Sider to load it and show a document. Then we'll build on top of that adding a feature after feature.

When creating a new view you can often consult existing views and pull pieces out from them. In this case I'm creating the view with a tree and tinyMce editor control, and it's going to be based largely on DojoTree and tinyMce views. Most of the time I copy/paste code with minor adjustments. Often, you could do the same, but remember to be careful and read your code. People often tend to forget to do all modifications required when pasting the code, and it can be a source of many errors if not reviewed.

Edit

Analysis

To write successful view you need to understand its goal and what the user expectations. The point of analysis is to have a clear picture of what needs to be done, what functionality view provides, how it looks, etc. With good vision of what needs to be done and how to do it creating a view is going to be a breeze. Without it can be hard process, almost as if you were stumbling in the darkness. However, sitting down and writing out all little details and steps is boring. You want analysis to help you, not get in your way. To do that that keep it very brief, enough to ensure you understand how you are going to do it, but without repeating all steps twice, first by writing them out and then by actually doing them.

The main point of the analysis is to get us to think how to implement the view and what problems we might encounter that we need to solve. You might not be able to see all problems, but should try to address as many of them early.

Edit

Description

In this example I'm want to write a view to allow the user use Sider to store personal notes. In addition I want to write a tutorial on how the view was created.

Edit

Requirements

The view needs to be simple in code so that you, the reader, could figure it out. It also needs to be rich enough to be usable for information management.

Edit

Interface

What should the view look like?
I'm going to show an explorer-like tree with each node representing a note. When a node is clicked the note is shown next to it. Both the tree and the note are visible next to each other.

The tree provides all commonly expected operations: expand, collapse, rename, etc.
The note is shown using rich WYSIWYG editor.

Edit

Right/wrong

I always try to write out what should go right and what could go wrong. In general right things need to be easy and wrong hard. Again, this is very brief, just to get us thinking about how to the view design.

Edit

What should go right?

The view is used both for reading and managing notes. The tree could support sorting, different icons and so on, but we are going to keep it simple for now. What it needs to support well is hiding/showing notes, highlight the current node, support operations (add, remove, rename, drag and drop).

When a note is visible, it should be automatically saved when user clicks on a different node. All notes are stored automatically when user navigates the tree and click on different nodes.

Edit

What could go wrong?

All of the nodes in the tree could get deleted and the tree could become invisible. It should either be impossible to delete all nodes or when all nodes are deleted a tree place holder is shown.

If note WYSIWYG editor is reused it could be possible to go back to the old text by undo. Undo/redo buffer needs to be cleared somehow (if possible) when text for a different node is shown.

Edit

Development

After analysis is done we can get down to writing the code. Some people might find it useful to plan steps required to take to have a completed view. I do that and it helps me a lot, even though most often I don't follow the plan exactly. Just like analysis, the plan can be helpful to understand what to do, how to do and in what order. It can be especially helpful if the view is quite large and will take several days to write.

For now, I'm going to skip most of the plan and just describe the actual actions to take to create the view.

Edit

Plan

Before you can write a view you need to be aware of how you are going to implement it, what view components you will need and what types the view can support. This particular view will use Dojo and tinyMce components. Both are free JavaScript components. You can use any other components or libraries with the Sider, as long as their license permits you to do that. As for the type, the view will show documents of the HtmlTree type ({FA706485-6B3B-438B-9320-DE21872011B8}).

Edit

New view

Follow @parent/NewView tutorial to create a blank new view. The view name is "Notes" and its version is 0.5.0.0. Add appropriate view components and type references.

Sider and Dojo components are included automatically with each view (they are needed for the Sider to function correctly). However, Sider component also provides Sider specific transformations. To use them you do need to include the component and create a reference to the transformation.

Add reference to the Sider view component and import its widget/tree.xslt.

Edit

notes.xslt

This is the view transformation file. It transforms document from XML to HTML for the browser. I assume you have already created and did minor adjustments to the transformation (using @parent/NewView tutorial). I'm going to describe what to de next.

  1. In place of the TODO comment in the head we are going to place initialization scripts.

  2. Replace TODO comment with {}. This calls template from the Sider tree transformation to insert the code needed to initialize the page for the tree.

  3. Now we add the code to generate the tree and HTML note that will be shown to the user.

  4. In place of TODO comment in generateView template add body tags:

    <body>
    </body>

    • Insert the following code inside of the body tags. It calls Sider component tree transformation to generate HTML to show the tree.


    <xsl:call-template name="sider.xslt.tree.GenerateTree">
    <xsl:with-param name="tree" select="/" />
    <xsl:with-param name="id" select="'treeId'"/>
    </xsl:call-template>


  5. Add this code after above code. This will generate tinyMce HTML.

    <xsl:call-template name="insertTinyMce">
    <xsl:with-param name="content" select="" />
    <xsl:with-param name="id" select="mceEditorId" />
    </xsl:call-template>



This is all we need for now. Your notes.xslt should look something like this:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:siderTree="http://www.emeraldhand.com/Sider/tree/v20060325"
exclude-result-prefixes="siderTree">

<xsl:strip-space elements="*"/>


<xsl:template name="generateHead">
<title>Notes view</title>
<xsl:call-template name="sider.xslt.tree.Init" />
</xsl:template>


<xsl:template name="generateView">
<xsl:call-template name="sider.xslt.tree.GenerateTree">
<xsl:with-param name="tree" select="/" />
<xsl:with-param name="id" select="'treeId'"/>
</xsl:call-template>

<xsl:call-template name="insertTinyMce">
<xsl:with-param name="id" select="mceEditorId" />
</xsl:call-template>
</xsl:template>
</xsl:stylesheet>


We should take a look at the result of our work, but Sider might not let us see the view if view script files are missing. If it can't find them go ahead and create them from view.js. Try launching Sider now. If everything is fine it should launch without any error dialogs.

Open new document window (File->New) and select HTML Tree document type on the left. Select Notes view in the drop down and you should be able to see new document with the view you just have created.

Edit

Adding layout

With the current view you should see the tree above the TinyMce editor. It would be much more convenient to show them next to each other. Let's modify our code to do just that. We are going to use DojoToolkit SplitExtension widget.

  1. To begin we need to request the widget we will be using from the Dojo. Add the following code to the generateHead, right before closing tag.

    <xsl:call-template name="genScript">
    <xsl:with-param name="body">
    dojo.require( "dojo.widget.ContentPane" );
    dojo.require( "dojo.widget.SplitExtension" );
    </xsl:with-param>
    </xsl:call-template>


  2. Next we need to declare the widgets to wrap our tree and editor. Replace code inside of the body tags with this:

    <div dojoType="SplitExtension"
    orientation="horizontal"
    sizerWidth="5"
    activeSizing="0" style="width: 100%; height: 100%">

    <div dojoType="ContentPane" sizeShare="20" style="overflow: auto">
    <xsl:call-template name="sider.xslt.tree.GenerateTree">
    <xsl:with-param name="tree" select="/" />
    <xsl:with-param name="id" select="'treeId'"/>
    </xsl:call-template>
    </div>

    <div dojoType="ContentPane" sizeShare="50">
    <xsl:call-template name="insertTinyMce">
    <xsl:with-param name="id" select="mceEditorId" />
    </xsl:call-template>
    </div>
    </div>



This should be enough to layout tree and editor controls. To see the updated view restart Sider, create a new HTML tree document and select Notes as its view.

Edit

Showing note text

Hooray, the tree and the editor are visible. We have controls we are going to use and you can select different nodes in the tree, but there's no logic to handle them. Node associated with a node is not shown when you click on the node. You can modify the tree using its context menu, but XML isn't being changed. If you switch to a different view you will see that everything is the same. Let's add tree and HTML editor controllers.

We are going to start by adding support to show text for the selected node. You should have created all script files when you were creating the blank view. If you haven't you can create both now, or as you need them.

Edit

html.js


  1. Start by adding code to initialize the TineMce handler.

    1. When TinyMce loads it will fire tinyMceEvents.init every time new instance of the editor is created. Since there will be only one editor we can freely handle this event and assume passed editor is the editor we will handle. Add this code inside of the Html class in html.js. This is the method that we will need to connect to the new editor instance event.

      /*
      Function: newEditorInit
      Handles new instances of the editor.
      */
      newEditorInit: function( inst )
      {
      this.editorId = inst.editorId;
      }


    2. Now we need to subscribe to the event. initializer is the class constructor. It registers connectToEvents to run when the page has loaded. If we try to connect to the event earlier we could get an error if it has not been loaded yet.

      /*
      Constructor: initializer
      Initializes a new instance of the <sider.views.tinyEditor.0.5>.
      */
      initializer: function()
      {
      dojo.addOnLoad( this, "connectToEvents" );
      },
      /*
      Function: connectToEvents
      Connects to the editor events.
      */
      connectToEvents: function()
      {
      dojo.event.connect( tinyMceEvents, "init", this, "newEditorInit" );
      }


      Your whole class should look like this:

      /*
      Class: sider.views.Html.0.5.0.0
      Wraps *tinyMce* for the view.
      */
      sider.views.addView( null, "Html",
      {
      /*
      Constructor: initializer
      Initializes a new instance of the <sider.views.tinyEditor.0.5>.
      */
      initializer: function()
      {
      dojo.addOnLoad( this, "connectToEvents" );
      },
      /*
      Function: connectToEvents
      Connects to the editor events.
      */
      connectToEvents: function()
      {
      dojo.event.connect( tinyMceEvents, "init", this, "newEditorInit" );
      },
      /*
      Function: newEditorInit
      Handles new instances of the editor.
      */
      newEditorInit: function( inst )
      {
      this.editorId = inst.editorId;
      }
      });

      Notice that I'm using anonymous object notation. That means that each member of the class is separated from the next member with a comma (visible after each closing }). The last member should have no commas.

    3. This is enough to initialize the object, but not enough to do anything useful. Let's add code to set the content in the editor, which by the way will be the note. As you insert this code after newEditorInit don't forget to add a coma after closing } of the newEditorInit method.

      /*
      Function: setContent
      Sets *tinyMce* content.
      */
      setContent: function( content )
      {
      tinyMCE.execInstanceCommand( this.editorId, 'mceSetContent', false, content );
      }


    4. Before we finish we need one last method. Sider stores data in XML form, and so types and views must also work with XML. Our handler should be able to show the view note at the specified XPath. This method can do just that:


      /*
      Function: showHtml
      Shows HTML at the specified XPath.
      */
      showHtml: function( xpath )
      {
      var html = sider.types.htmlTree.getHtml( xpath );
      if( html != null )
      {
      this.setContent( html );
      }
      }


    We are done with html.js for now. Let's have some fun with the tree (tree.js).


Edit

tree.js


  1. Just like with html.js we need initialization code. We are going to take a little bit different approach. To connect to a tree we need to know its ID. There're no global events to connect to. ID is known at the time when the view is being generated (in the transformation) and that's the best place to add call to our initialization code that will supply the tree ID. But first we need a method that will be invoked.

    /*
    Constructor: init
    Initializers a current instance of the <tree>.

    Parameters:
    treeId - Tree widgit id.
    */
    init: function( treeId )
    {
    var tree = dojo.widget.byId( treeId );
    }


  2. We now need to make sure this method will be invoked with the tree ID. Open notes.xslt and add the following parameter to the sider.xslt.tree.GenerateTree template call. If you launch Sider now the view will be shown, but there's still no functionality. This is all about to change.


    <xsl:with-param name="initScript">sider.views.tree.init( "$TREEID$" );</xsl:with-param>



Path handling

View doesn't interact with the document directly. Instead it uses type objects. To reference different parts of the document XPath is used.

For example, in our case, when user clicks on a node, view generates XPath to the node clicked to get its HTML. In other words the view needs to have a link between a control in the view and corresponding piece of data in document. Often type objects provide methods to assist with generating correct XPath.

Tree type has a method to convert index path ( {0, 5, 2}, with each number corresponding to the index of consecutive child node) to the XPath to access corresponding tree node in the document.

Dojo tree we are using provides the method to get an index path to the node. So we have all the methods we need and getting XPath for the node is quite trivial.

var nodePath = node.getIndexPath();
var xpath = sider.types.tree.fixPath( nodePath );


There's one small issue to address. We use Sider component to generate the tree HTML (component also provides custom tree node objects that support index path). Generated tree has an additional root node under which all notes will go. It is required to allow user delete all notes and still have a node to add notes to. Without that additional node when all notes are deleted the tree is no longer visible and is inaccessible.

We need that additional root node, but when it is present, index path returned by the tree widget will be too long. We need to remove that node from the index path (since document doesn't have that node) before we can convert it to the XPath.

This is our final function that we will use everywhere in our code to get XPath to the node we are currently are working with. Go ahead and add it to the tree.js.

/*
Function: getXPath
Gets *indexPath* from the specified node and converts it to the XPath.
*/
getXPath: function( node )
{
var nodePath = node.getIndexPath();

// Remove root node used by the tree to provide consistent interface
nodePath = nodePath.slice( 1 );
return sider.types.tree.fixPath( nodePath );
}


Node selection

Now we can finally get to handling node selection and showing a note.

  1. By default the tree doesn't provide any handling logic. Instead it is added through extensions. There's a selector extension that is responsible for selecting different nodes. So, to handle node selection we don't work with the tree, but with the selector. We need to connect to it. Replace init with the code. It's almost the same method, but it accepts selector ID and connects to it.

    /*
    Constructor: init
    Initializers a current instance of the <tree>.

    Parameters:
    treeId - Tree widgit id.
    treeSelectorId - Tree selector widget id.
    */
    init: function( treeId, treeSelectorId )
    {
    var tree = dojo.widget.byId( treeId );

    var treeSelector = dojo.widget.byId( treeSelectorId );
    dojo.event.topic.subscribe( treeSelector.eventNames.select, this, "onSelect" );
    }


  2. We also need to update the transformation to ensure selector ID is supplied.

    <xsl:with-param name="initScript">sider.views.tree.init( "$TREEID$" , "$TREESELECTORID$" );</xsl:with-param>


  3. Now we need onSelect method. It verifies that that an actual node with a note was clicked (and not the root extra note). If it was, a note for the node is shown.

    /*
    Function: onSelect
    Shows the note for the selected node.
    */
    onSelect: function( eventArgs )
    {
    var node = eventArgs.node;
    var path = node.getIndexPath();

    var xpath;
    if( path.length > 1 )
    {
    xpath = this.getXPath( eventArgs.node );
    sider.views.html.showHtml( xpath );
    }
    }


  4. This is it. You now have a view that should show different notes when you click on different nodes.


Edit

Saving note

We can show the note for each node now, but if we edit the note, it will not be saved. We are going to address that right now.

  1. Open html.js for editing.

  2. We are going to assume only one note is edited at a time, so we can save the path to the current node. We will use it to store the note. Add this code after {this.setContent( html );} in the showHtml method.

    this.notePath = xpath;

    • Now we add a method to save HTML for the current note.


    /*
    Function: saveHtml
    Saves HTML for the current note.
    */
    saveHtml: function()
    {
    if( this.notePath )
    {
    var html = this.getContent();
    sider.types.htmlTree.setHtml( this.notePath, html );
    }
    }


  3. In the above code we are using getContent method to get current HTML. Let's implement it. Add it just above setContent member. Remember to add comas to separate the class members.

    /*
    Function: getContent
    Retrieves HTML from *tinyMce*.
    */
    getContent: function()
    {
    return tinyMCE.getContent( this.editorId );
    }


  4. Now we need to save HTML every time a request is made to show a different note. Add call to SaveHtml right above {this.setContent( html );}. Your new showHtml should look something like this:

    /*
    Function: showHtml
    Shows HTML at the specified XPath.
    */
    showHtml: function( xpath )
    {
    var html = sider.types.htmlTree.getHtml( xpath );
    if( html != null )
    {
    this.saveHtml();
    this.setContent( html );
    this.notePath = xpath;
    }
    }


  5. Sider accepts changes only from the documents marked as changed. We need code to leave such mark. Add the following code to the connectToEvents.

    dojo.event.connect( tinyMceEvents, "onchange", function() {
    sider.doc.setChanged( true );
    });


  6. As the last step we need to make sure changes to the current node will be saved when the user decides to save the document. With above code the change is only saved when user clicks on a different node. When a document is being saved synchronize method is invoked for each view object (in sider.views). We need to supply our own method to save the note.

    /*
    Function: synchronize
    Updates the document.
    */
    synchronize: function()
    {
    this.saveHtml();
    }


  7. That's it. We are done with html.js. You should now be able to see note for each node, change it and save it.


Edit

Adding new notes

It's possible to only edit and view existing nodes, but there's no way to add a new node. We will now add code to add create new notes.

  1. Open tree.js for editing.

  2. Add the following method to the Tree class. It creates new tree node and then adds default HTML note to it.

    /*
    Function: createNode
    Creates a new node in response to the tree event.
    */
    createNode: function( eventArgs )
    {
    if( eventArgs.childWidgetCreated )
    {
    // Node was created for lazy instancing. It already is in the document
    return;
    }
    var xpath = this.getXPath( eventArgs.parent );
    var newNode = eventArgs.child;

    var newNodePath = sider.types.tree.addNode( xpath, newNode.labelNode.innerHTML, eventArgs.index );
    sider.types.htmlTree.createHtml( newNodePath );

    sider.doc.setChanged();
    }


  3. Now, we need to connect this method to handle new node event from the tree. Add this code right under the line where we get tree widget in the init method.

    dojo.event.topic.subscribe( tree.eventNames.afterAddChild, this, "createNode" );


  4. We should also go ahead select new node to show the new note.

  5. Convert init method to store treeSelector as a member of the class.

    this.treeSelector = dojo.widget.byId( treeSelectorId );
    dojo.event.topic.subscribe( this.treeSelector.eventNames.select, this, "onSelect" );


  6. Update createNode to select new node by adding the following code to the end of the method.

    this.treeSelector.deselectAll();
    this.treeSelector.select( eventArgs.child );


  7. That's it. You can now create new notes through the tree context menu.


Edit

Deleting notes

Sometimes a note loses its value and needs to be deleted. We are going to make sure the user can do that. All changes will be done to the tree.js file.

  1. Open tree.js for editing.

  2. Add the following method to the Tree class.

    /*
    Function: destroyNode
    Removes the node.
    */
    destroyNode: function( eventArgs )
    {
    var xpath = this.getXPath( eventArgs.source );

    sider.types.tree.removeNode( xpath );
    sider.doc.setChanged();
    }


  3. All we have left is to connect this method to the destroy node event in the tree. Add the code to the tree init method.

    dojo.event.topic.subscribe( tree.eventNames.beforeNodeDestroy, this, "destroyNode" );



Edit

Renaming nodes

I often associate note content with the note title. Sometimes I might want to rename existing nodes and when I add a new note, I definitely want to give it a meaningful title. Adding support for node rename is just a walk in a park for us. Right?

  1. Open tree.js for editing.

  2. Add this method. It can change the title of any node in response to the tree events.

    /*
    Function: setNodeTitle
    Sets node title in response to the tree event.
    */
    setNodeTitle: function( eventArgs )
    {
    var xpath = this.getXPath( eventArgs.source );
    sider.types.tree.setNodeName( xpath, eventArgs.title );
    sider.doc.setChanged();
    }


  3. Now we need to connect this method to the title changed event. This code goes to the init method.

  4. This is it for rename


Edit

Drag and drop

The tree widget supports DnD operation. We are going to add support for it and move the notes around when user drags them in the tree.

  1. To begin with we need to subscribe to the move events from the tree. Add this code to the init.

    dojo.event.topic.subscribe( tree.eventNames.beforeMoveFrom, this, "moveNodeStart" );
    dojo.event.topic.subscribe( tree.eventNames.afterMoveTo, this, "moveNodeEnd" );


  2. In the declaration above we are connecting two methods: one before the move operation and one after. Before the move starts we need to capture which node is going to be moved, and where. After the node is moved in the tree, the tree model changes and it will be impossible to get accurate location of the node. Add this method to make sure that doesn't happen.

    /*
    Function: moveNodeStart
    Starts moving a node to a new location.

    Remarks:
    Captures exact node position to move later.
    */
    moveNodeStart: function( eventArgs )
    {
    var nodeXPath = this.getXPath( eventArgs.child );
    var parentXPath = this.getXPath( eventArgs.newParent );

    this.movePaths = {
    nodeXPath: nodeXPath,
    parentXPath: parentXPath
    };
    }


  3. Now let's add the code to complete move operation.

    /*
    Function: moveNodeEnd
    Finishes the node move to a new location.

    Remarks:
    Uses position captured by the <moveNodeStart> to correctly move the node.
    */
    moveNodeEnd: function( eventArgs )
    {
    if( this.movePaths )
    {
    sider.types.tree.moveNode(
    this.movePaths.parentXPath,
    this.movePaths.nodeXPath,
    eventArgs.child.getParentIndex() );

    sider.doc.setChanged();
    this.movePaths = null;
    }
    }


  4. Drag and Drop is done. Everything should work now.


Edit

Conclusion

This tutorial demonstrates creation of one of the more complicated views. There are two independent controls (tree and HTML editor) that need to interact together to work on the same document. It is possible to create even more complicated views, but it's better to create several simple views and let user pick an appropriate view, than try to accommodate every user need.

Copyright © 2006-2008 Emerald Hand, Inc. All rights reserved.
Powered by ScrewTurn Wiki version 2.0.34. Some of the icons created by FamFamFam.