Step 7 – Parent – Child Checkbox Relationship

Up till this point we have been laying the foundation for our checkbox tree, created the model, put in the plumbing and added some attributes that make the checkbox tree pretty configurable but now there is only one of the original requirements left:

The tree must be able to maintain a parent-child checkbox relationship. What I mean by this is that for example if a parent checkbox is checked all child and grandchild checkboxes will automatically be checked as well or if one child is unchecked the parent and potentially its parent is unchecked automatically.

The key is to keep the parent-child relations consistent, a quick analysis of the situation reveals we need a couple of methods to be successful:

  1. We must be able to update the store. (_setCheckboxState)
  2. We must be able to update child checkboxes if a parent is checked. (_updateChildCheckbox).
  3. We must be able to update the parent checkbox if a child changes state (_updateParentCheckbox and _getParentItem).
  4. We must be able to validate the store before we begin. (validateData)
  5. Receive checkbox status change updates from the tree. (updateCheckbox)

And finally in case we do not want to maintain a strict parent-child checkbox relationship I’m going to introduce one last attribute checkboxStrict.

Because of the tight relationship between the above mentioned methods I’m going to take a slightly different approach in this step. I’m going to address each method separately one-by-one instead of doing a couple of line in each function at the time. The at the end we are going to put everything together to build our final solution. With that out of the way lets get started with the most fundamental function of them all _setCheckboxState.

111   _setCheckboxState: function(storeItem, newState ) {
112     var stateChanged = true;
113     var currState = false;
114 
114     if( storeItem != this.root ) {
115       currState = this.store.getValue(storeItem, this.checkboxIdent);
116       if( currState != newState && (currState !== undefined || this.checkboxAll ) ) {
117         this.store.setValue(storeItem, this.checkboxIdent, newState);
118         currState = newState;
119       } else {
120         stateChanged = false;  // No changes to the checkbox
121       }
122     } else {  // Tree root instance
123       if( this.root.checkbox != newState && ( this.root.checkbox !== undefined || this.checkboxRoot ) ) {
124         currState = this.root.checkbox = newState;
125       } else {
126         stateChanged = false;
127       }
128     }
129     if( stateChanged ) {  // In case of any changes trigger the update event.
130       this.onCheckboxChange(storeItem);
131     }
132     return currState;  // the new state of the checkbox
133   },

On line 114, the first thing we do again is check if we are dealing with the tree root. If it’s not the root we get the current checkbox state from the store and see if the state has actually changed. If the current state is not undefined (we do have a checkbox in the store for this item) we update the store using the setValue method of the store. If the current state is undefined we check the checkboxAll attribute at line 116 and if true we create the missing checkbox in the store and update the current state. The one thing you need to know about the stores setValue method is that if the attribute does not exist it will create it for you therefore there is no method like createValue or createAttribute.

Starting at line 122 we do pretty much the same for the root element with the only exception that we do not use the actual store and validate the checkboxRoot attribute to see if a root checkbox is required.

Finally at line 129 we check if a state change did happen. If so, we call the by now infamous onCheckboxChange method to trigger the update event for the store, Yippy, we finally closed all loops.

Lets go make our final changes to the getCheckboxState:

 18   getCheckboxState: function( storeItem) {
 19     var currState = undefined;
 20 
 20     if ( storeItem == this.root ) {
 21       if( typeof(storeItem.checkbox) == "undefined" ) {
 22         this.root.checkbox = undefined;    
 23         if( this.checkboxRoot ) {
 24           currState = this.root.checkbox = this.checkboxState;
 25         }
 26       } else {
 27         currState = this.root.checkbox;
 28       }
 29     } else {  
 30       currState = this.store.getValue(storeItem, this.checkboxIdent);
 31       if( currState == undefined && this.checkboxAll) {
 32         currState = this._setCheckboxState( storeItem, this.checkboxState );
 33       }
 34     }
 35     return currState  
 36   },

On line 31 we add the check if all store item require a checkbox and on line 32 we call the storeCheckboxState method to create the checkbox state on the store for this item.

The next method we need is to update child checkboxes. Whenever a parent checkbox is checked or unchecked all children need to be checked or unchecked as well

 76   _updateChildCheckbox: function( parentItem,  newState ) {
 77     if( this.mayHaveChildren( parentItem ) ) {
 78       this.getChildren( parentItem, dojo.hitch( this,
 79         function( children ) {
 80           dojo.forEach( children, function(child) {
 81             this._setCheckboxState(child, newState);
 82             if( this.mayHaveChildren( child )) {
 83               this._updateChildCheckbox( child, newState )
 84             }
 85           }, this );
 86         }),
 87         function(err) {
 88           console.error(_this, ": updating child checkboxes: ", err);
 89         }
 90       );
 91     }
 92   },

On line 77 we perform a lightweight check to see if the parent item potentially has children. The reason why I use potentially here is because the mayHaveChildren method only checks if the specified item has the so called ‘children’ attribute. However, the children attribute doesn’t always mean the item actually does have any children.

On line 78 we are going to fetch the children for the specified item from the data store. Once the child items are fetched the getChildren method will call our callback function which we wrap in the dojo hitch function because the callback normally executes in the scope of the store. Dojo hitch allows the callback to execute in any specified scope. In our case we tell dojo hitch to use this as the scope. Be aware that the function on line 79 is a callback and thus executed asynchronous. The callback function will iterate all children using the dojo.forEach function.

Because we already know the parent checkbox has changed state we can immediately update the child checkbox state on line 81. The last thing we have to do it to see if the child potentially has children of its own. If true we just make a recursive call to _updateChildCheckbox again.

If you are not familiar with dojo you may want to look into dojo.hitch(), dojo.forEach() and dojo.some() as we will be using these functions a couple more times.

Now that we are able to update child checkboxes we also need a method to update a parent checkbox in case any of its children changes state. When a checkbox is unchecked the theory is simple, just uncheck the parent and then its parent all the way up to the root. If a checkbox gets checked on the other hand, we need to validate the state of all siblings and if all of them are checked as well we need to check the parent checkbox and so on.

The challenge here is that the store doesn’t offers a method to fetch an items parent. As you already have seen it is fairly simple to traverse down the tree using the getChildren method but up is a completely different story so let’s go ahead and write our own _getParentItem method first. This is one of the area’s that’s going to change for dojo 1.4.

119   _getParentItem: function( storeItem ) {
120     var parent = null;
121 
121     if( storeItem != this.root ) {
122       var references = storeItem[this.store._reverseRefMap];
123       for(itemId in references ) {
124         parent = this.store._itemsByIdentity[itemId];
125         break;
126       }
127       if (!parent) {
128         parent = this.root;
129       }
130     }
131     return parent 
132   },

If an item in the store is referenced by another item (its parent) the child will have a reverse reference to its parent. In other words: A child will have a parent reference if the parent specified the ‘_reference’ attribute in our Json source file. For example: children:[{_reference:’Mexico’}, {_reference:’Canada’}, … This BTW implies that any store item can have multiple parents but because the dojo 1.3.2. Tree implementation does NOT support multiple parents we will have to stop as soon as we get the first parent and break out of our loop started at line 123. Ok, let’s go do the _updateParentCheckbox next.

 93   _updateParentCheckbox: function( storeItem,  newState ) {
 94     var parentItem = this._getParentItem(storeItem);
 95 
 95     if( !parentItem ) {
 96       return;
 97     }
 98     if( newState ) { 
 99       this.getChildren( parentItem, dojo.hitch( this,
100         function(siblings) {
101           var allChecked  = true;
102           dojo.some( siblings, function(sibling) {
103             return !(allChecked = this.store.getValue(sibling,this.checkboxIdent));
104           }, this );
105           if( allChecked ) {
106             this._setCheckboxState( parentItem, true );
107             this._updateParentCheckbox( parentItem, true );
108           }
109         }),
110         function(err) {
111           console.error(_this, ": updating parent checkboxes: ", err);
112         }
113       );
114     } else {   
115       this._setCheckboxState( parentItem, false );
116       this._updateParentCheckbox( parentItem, false );
117     }
118   },

On line 94 we call our newly created _getParentItem method, if we don’t get anything back the store item apparently doesn’t have a parent. This should only be true in case of our Tree root, take another look at our _getParentItem method as to why. Once we set the parent checkbox state to true ,on line 106, we move up one level in our tree (line 107) and repeat the process, remember the parent is always somebody’s else’s child. We keep doing this until we reach the Tree root and voila, there you have it. Notice that if a checkbox gets unchecked (line 114) we don’t care about any of the siblings because it only takes one unchecked child to uncheck the parent.

As per our analysis there are only two more things to do:

  1. We must be able to validate the store before we begin. (validateData)
  2. Receive checkbox status change updates from the tree. (updateCheckbox)

In order to guarantee a consistent data store we have to start-off with one. This means we need to validate that the checkbox data provided in our Json source file is correct to begin with. For example: does the parent checkbox state accurately reflect the state of all its children. If not, we will have to make changes to our store BEFORE we even start rendering the tree. Simple right….

Now think of the following scenario: while we modify the store, update events will be sent to our tree but there are no tree nodes yet. This is exactly why the _CheckboxChange method of our tree checks if a node exists before trying to change the checkbox state on the tree. Again, there is the checkbox state maintained by our store and there is the matching state maintained by the tree and both need to be in sync.

133   validateData: function( storeItem,  scope ) {
134     if( !scope.checkboxStrict ) {
135       return;
136     }
137     var parentState = scope.getCheckboxState( storeItem );
138     scope.getChildren( storeItem, dojo.hitch( scope,
139       function(children) {
140         var allChecked = true;
141         var parentState = this.getCheckboxState( storeItem );
142         dojo.some( children, function( child ) {
143           if( this.mayHaveChildren( child )) {
144             this.validateData( child, this );
145           }
146           return !(allChecked = this.getCheckboxState( child ));
147         }, this);
148 
148         if( parentState != allChecked ) {
149           this._setCheckboxState( storeItem, allChecked);
150           this._updateParentCheckbox( storeItem, allChecked);
151         }
152       }),
153       function(err) {
154         console.error(this, ": validating checkbox data: ", err);
155       }
156     );
157   },

On line 134 we check if a strict checkbox relationship is requested, this by the way, is our last attribute for our model. The default for checkboxStrict is true. Because our validation method is called from the store “this” equates to our tree. In order to get the proper scope the tree passes our model as the scope. On line 138 we use dojo.hitch to adjust the scope for our getChildren callback function so now we can start using “this” again. Basically, we traverse the store and update checkbox states if needed. There is however a downside to the implementation of the strict checkbox relationships and that is that all the data needs to be loaded first before we can start rendering the tree. This is not an issue with dojo version 1.3.2 but version 1.4 offers a feature called deferred loading. What that means is that the data is only loaded from the file when needed for example when you expand the tree nodes.

The last bit we need to do is to make sure our validateData method gets called, we do that by adding one line to our Tree postCreate method:

210   postCreate: function() {
211     this.connect(this.model, "onCheckboxChange", "_onCheckboxChange");
212     this.model.validateData( this.model.root, this.model );
213     this.inherited(arguments);
214   },

On line 212 we call the validateData method and pass “this.model.root” as the starting point in our tree and “this.model” as the scope. At this point there is only one thing left to do. We talked about the updateCheckbox method before, we even declared the method on our model but just to recap, the updateCheckbox method is called by our Tree to inform the model that the user has checked/unchecked one of our checkboxes. With all the plumbing in place updateCheckbox has to perform three steps:

  1. Change the status of the checkbox in our data store.
  2. Update any child checkboxes (in case the tree node is a parent)
  3. Update any parent checkboxes (incase all siblings are checked/unchecked)

The final updateCheckbox method looks as follows:

 11   updateCheckbox: function( storeItem,  newState ) {
 12     this._setCheckboxState( storeItem, newState );
 13     if( this.checkboxStrict ) {
 14       this._updateChildCheckbox( storeItem, newState );
 15       this._updateParentCheckbox( storeItem, newState );
 16     }
 17   },

Believe it or not but we are done, this basically concludes our CheckBoxTree tutorial. We have implemented all of the requirements we wanted to accomplish. Sit back relax and take a look at your new shiny dijit CheckBoxTree widget.

The last thing I want to do is to explain the differences between dojo version 1.3.2 and version 1.4. The last and final step, STEP 8

3 Replies to “Step 7 – Parent – Child Checkbox Relationship”

  1. Thank you so much for this very detailed tutorial. It really helped me to understand some of the inner workings of dojo and digit. I downloaded the code and it works like a charm. I have one question: are you planning on provide this Checkbox tree with multi state checkboxes? What I mean by multi state checkboxes is a parent checkbox that can be a) checked b) unchecked c) mixed. Mixed in this context means some child checkboxes are checked whereas others are unchecked.

    Again, thanks for this great tutorial.

    1. As a matter of fact I do have a newer version available that supports triple state checkboxes. I’m currently running all the test scenarios and expect to release this version somewhere next week.

  2. Hi,

    I just went quickly through your tutorial, downloaded and used your code with dojo 1.4.3 and it works so nicely. You saved me a lot of time.

    great work!
    cheers,

Leave a Reply

Your email address will not be published. Required fields are marked *