Blog-Archiv

Sonntag, 20. August 2017

Expand-All Control for JS Trees

Knowledge is a very long line of text that has been written since thousands of years. To make it available, it has been split into books that provide titles, summarizing the contained knowledge. When you open a book, you find pages, and you need to turn them to find the knowledge you are looking for. Nowadays the Internet has redefined the term "page", and you can find entire books on web pages. Here you can navigate using a search-field, and instead of turning pages you scroll.

Availability increases when knowledge gets structured. That means short books, more accurate and descriptive titles, having tables of contents and indexes, and libraries that provide well-organized catalogues. The Internet is a big library that has no catalogues, but there are search engines and hyperlinks. For web pages, structuring means that you don't want to face full complexity at first sight. In other words, the details should be initially hidden. That's what folding is for.


In this Blog I want to present an idea for tree expand-controls that should make life easier in certain cases. Instead of having just one expand-control there are two, and the new one expands or collapses the whole sub-tree on just one click.

The Idea

Click on the bigger ▶ expand-control to expand just the current folder ("traditional").
Use the new smaller ▸ "expand-all" control to expand the tree fully.

  • root
    • item 1
      • item 1.1
      • item 1.2
    • item 2
    • item 3
      • item 3.1
        • item 3.1.1
        • item 3.1.2
        • item 3.1.3
      • item 3.2
      • item 3.3
        • item 3.3.1
        • item 3.3.2
        • item 3.3.3
          • item 3.3.3.1
          • item 3.3.3.2
      • item 3.4
    • item 4
      • item 4.1
      • item 4.2
      • item 4.3

Mind that the small control nicely indicates the presence of sub-folders!

Of course such a solution is not suitable for all kinds of trees. A dynamically expanding tree that fetches server data any time the user expands a folder may not be able to provide the second expand-control, because it sees just one level down the tree. But for trees that are fully available at page-load-time this could be a nice enhancement.

Design Decisions

A number of things have to be thought over for the second expand control.

  • Should the new "expand-all" control be left or right of the main expand control?
  • Should it be bigger than, smaller than, or same size as the main?

I believe the new control should be smaller, because users will expect the main control to be dominant.

Concerning the question "left or right?", here is a tree where the "expand-all" control is right (and initially fully expanded):

  • item 1
    • item 1.1
    • item 1.2
    • item 1.3
      • item 1.3.1
      • item 1.3.2
      • item 1.3.3
    • item 1.4
  • item 2
    • item 2.1
    • item 2.2
  • item 3

Doesn't look so good, too much space between the main control and the tree label. I think having the new control left is also a good indication about where the sub-trees are. Hence this is the implemented default location, but the following JavaScript lets configure this.

Source Code

The JS code consists of two modules, where the second extends the first. I use functional inheritance. The first module is foldingUtil, this can be used for any kind of expandable disclosure. The second is doubleFoldingUtil, this adds the second expand-control and its functionality. Finally some page-specific adapter code uses these modules to achieve the trees shown on this page.

Put all the JS source shown below inside a <script> tag on bottom of your HTML page's <body>, and then write some adapter code below to catch the trees you want to fold.

I won't explain the following JS source code line by line. I tried to comment everything, so that it should be readable and understandable. When you are completely new to JS programming, step through the code with the browser debugger, this is one of the best ways to get familiar with a piece of software. Set breakpoints in doubleFoldingUtil.initialize(), and doubleFoldingUtil.clickAllListener().

CSS

You will need this CSS in your page's <head> to get rid of <UL> list bullets and paddings.

  <style type="text/css">
    ul {
      list-style-type: none;
      padding-left: 1em;
    }
  </style>

JS Folding Util

 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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
    /**
     * Basic folding utility, suitable for both simple disclosures and complex trees.
     * @param {string} symbolExpand required, the symbol to set on a collapsed folder.
     * @param {string} symbolCollapse required, the symbol to set on an expanded folder.
     * @returns a folding utility to connect() a title with its content.
     */
    var foldingUtil = function(symbolExpand, symbolCollapse)
    {
      "use strict";
    
      var that = {};
      
      /**
       * Connects a title element with a content element for folding.
       * @param titleElement the DOM container that will get an expand-control inserted.
       * @param content optional, the sub-list DOM element to be folded.
       * @param {boolean} initiallyFolded optional, default false, the tree will be collapsed initially when true.
       * @returns the DOM expand-control element that was generated for this fold.
       */
      that.connect = function(titleElement, content, initiallyFolded) {
        var expandControl = that.prependExpandControlContainer(titleElement);
        if (content) {
          expandControl.innerHTML = (initiallyFolded ? symbolExpand : symbolCollapse);
          expandControl.addEventListener("click", function() {
            clickListener(expandControl, content);
          });
          setDisplayed(content, ! initiallyFolded); /* set initial state */
        }
        return expandControl;
      };

      /** This is for calls by sub-classes, do not call from outside! */
      that.prependExpandControlContainer = function(titleElement) {
        var expandControl = document.createElement("div");
        expandControl.style.display = "inline-block";
        expandControl.style.cursor = "pointer";
        expandControl.style["text-align"] = "center";
        
        var width = that.getExpandControlWidth();
        expandControl.style.width = (width !== undefined) ? width : "1em"; /* MUST have a width, placeholder */
        
        var marginRight = that.getExpandControlRightMargin();
        if (marginRight)
          expandControl.style["margin-right"] = marginRight;
        
        titleElement.insertBefore(expandControl, titleElement.childNodes[0]);
        return expandControl;
      };
      
      /** Returns the width value to set onto an expand-control. */
      that.getExpandControlWidth = function() {
        return "1em";
      };
      
      /** Returns the margin-right value to set onto an expand-control. */
      that.getExpandControlRightMargin = function() {
        return "0.5em";
      };
      
      /** Returns true when given expand-control contains an collapse-symbol. */
      that.isControlExpanded = function(expandControl) {
        return expandControl.innerHTML === symbolCollapse;
      };
      
      var clickListener = function(expandControl, content) {
        var displayed = that.isControlExpanded(expandControl);
        setDisplayed(content, displayed === false); /* revert state */
        expandControl.innerHTML = displayed ? symbolExpand : symbolCollapse;
      };
      
      var setDisplayed = function(content, displayed) {
        content.style.display = displayed ? "" : "none";
      };

      return that;
    };

JS Double Folding

The concept here is to mark "single-expand" controls (main) by a CSS class, and then click() programmatically onto all of them when the "expand-all" control is clicked.

  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
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
    /**
     * Folding utility for trees, lets expand and collapse
     * the whole tree with just one click by providing a second expand control.
     * @param foldingUtil required, the folding utility to inherit from, super-object.
     * @param {string} symbolAllExpand required, the expand-all symbol to set on a collapsed folder.
     * @param {string} symbolAllCollapse required, the expand-all symbol to set on an expanded folder.
     * @param {boolean} expandAllSymbolAtRight optional, default false, when true, the expand-all control is second.
     * @returns a folding utility to initialize() a tree structure with double expand-controls.
     */
    var doubleFoldingUtil = function(foldingUtil, symbolAllExpand, symbolAllCollapse, expandAllSymbolAtRight)
    {
      "use strict";
      
      var that = foldingUtil;
      
      /**
       * Call this to install double-folding.
       * This is here to ensure that the treeNodesTopDown list is iterated bottom-up.
       * @param treeNodesTopDown required, array of title-elements to connect with contained sub-lists, in top-down order.
       * @param recognizeSubListFunction required, returns true when given tree-node child is a sub-list.
       * @param {boolean} initiallyFolded optional, default false, the tree will be collapsed initially when true.
       */
      that.initialize = function(treeNodesTopDown, recognizeSubListFunction, initiallyFolded) {
        /* loop bottom-up, so that findSingleExpandControlsBelow() will work correctly */
        for (var i = treeNodesTopDown.length - 1; i >= 0; i--) {
          var treeNode = treeNodesTopDown[i];
          
          var subList = undefined; /* need to initialize this due to variable hoisting */
          var children = treeNode.children;
          for (var c = 0; ! subList && c < children.length; c++)
            if (recognizeSubListFunction(children[c]))
              subList = children[c];

          that.connect(treeNode, subList, initiallyFolded);
        }
      };
      
      
      var singleExpandControlClassName = "singleExpandControl";
      
      
      var superConnect = that.connect; /* will be overridden */
      
      /** Overridden to install a second expand-control. */
      that.connect = function(titleElement, content, initiallyFolded) {
        var singleExpandControl;
        
        if ( ! expandAllSymbolAtRight ) /* insert normal expand-control when being at right */
          singleExpandControl = superConnect(titleElement, content, initiallyFolded);
        
        var expandAllControl = connectAll(titleElement, content, initiallyFolded); /* prepend expand-all-control */
        
        if ( expandAllSymbolAtRight ) /* prepend normal expand-control when being at left */
          singleExpandControl = superConnect(titleElement, content, initiallyFolded);
        
        rightMarginOnExpandControls(singleExpandControl, expandAllControl);
        
        if (content) {
          singleExpandControl.classList.add(singleExpandControlClassName); /* need to find expand-controls */
          
          if (expandAllControl.innerHTML) { /* is not just an indent placeholder */
            singleExpandControl.addEventListener("click", function() { /* synchronize expand-all-control */
              var expanded = that.isControlExpanded(singleExpandControl);
              expandAllControl.innerHTML =
                  (expanded && ! existsCollapsedBelow(content)) ? symbolAllCollapse : symbolAllExpand;
            });
          }
        }
        
        return singleExpandControl;
      };


      var superGetExpandControlRightMargin = that.getExpandControlRightMargin;
      
      /** Overridden to avoid right margin on super's expand-control. */
      that.getExpandControlRightMargin = function() {
        return undefined;
      };
      
      
      var rightMarginOnExpandControls = function(singleExpandControl, expandAllControl) {
        var marginTarget = expandAllSymbolAtRight ? expandAllControl : singleExpandControl;
        marginTarget.style["margin-right"] = superGetExpandControlRightMargin();
      };
      
      var connectAll = function(titleElement, content, initiallyFolded) {
        var expandAllControl = that.prependExpandControlContainer(titleElement);
        /* just nodes that have folder-nodes below them need an expand-all symbol */
        if (findSingleExpandControlsBelow(content).length > 0) {
          expandAllControl.innerHTML = initiallyFolded ? symbolAllExpand : symbolAllCollapse;
          expandAllControl.addEventListener("click", function() {
            clickAllListener(expandAllControl, content);
          });
        }
        return expandAllControl;
      };

      var clickAllListener = function(expandAllControl, content) {
        var singleExpandControl = findSingleExpandControlsBelow(expandAllControl.parentElement)[0];
        var singleExpanded = that.isControlExpanded(singleExpandControl);
        var allExpanded = (expandAllControl.innerHTML === symbolAllCollapse);
        
        if (singleExpanded === allExpanded) /* when both controls have same state, */
          singleExpandControl.click(); /* click the single-expand control */
        
        expandAllControl.innerHTML = allExpanded ? symbolAllExpand : symbolAllCollapse;
        
        clickSingleExpandControlsBelow(content, allExpanded);
      };
      
      var clickSingleExpandControlsBelow = function(content, collapsing) {
        var singleExpandControls = findSingleExpandControlsBelow(content);
        /* loop bottom-up, so that existsCollapsedBelow() will work correctly */
        for (var i = singleExpandControls.length - 1; i >= 0; i--) {
          var singleExpandControl= singleExpandControls[i];
          var collapsed = ! that.isControlExpanded(singleExpandControl);
          if (collapsing !== collapsed) /* needs state change */
            singleExpandControl.click();
        }
      };
      
      var existsCollapsedBelow = function(content) {
        var singleExpandControls = findSingleExpandControlsBelow(content);
        for (var i = 0; i < singleExpandControls.length; i++)
          if ( ! that.isControlExpanded(singleExpandControls[i]) )
            return true;
        return false;
      };
      
      var findSingleExpandControlsBelow = function(content) {
        return content ? content.querySelectorAll("."+singleExpandControlClassName) : [];
      };
      
      return that;
    };

JS Usage Example

Following example assumes that the tree consists of nested <UL> elements, and the root element has an id = "expandable-tree":

 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
    (function() {
      
      /* different expand- and collapse-symbols */
      var blackTriangleRight = "\u25B6";
      var blackTriangleDown = "\u25BC";
      var blackTriangleRightSmall = "\u25B8";
      var blackTriangleDownSmall = "\u25BE";

      /* parameter function for doubleFoldingUtil.initialize(). */
      var recognizeSubListFunction = function(child) {
        return child.tagName === "UL";
      };
      
      /* build for tree */
      var treeFoldingUtil = doubleFoldingUtil(
          foldingUtil(
              blackTriangleRight,
              blackTriangleDown),
          blackTriangleRightSmall,
          blackTriangleDownSmall);

      treeFoldingUtil.initialize(
          document.getElementById("expandable-tree").getElementsByTagName("LI"),
          recognizeSubListFunction,
          true /* initially folded */
      );
      
    })();



Samstag, 12. August 2017

Scrum Standup Meetings

Scrum is a project management technique. It has been around since 1995, on a broader base since 2002, represented by persons like Ken Schwaber and Jeff Sutherland.

Scrum recommends to partition project work into sprints, which are delimited by a planning and a review, plus retrospective. A popular sprint duration is two weeks. During the sprint, stand-up meetings are held every day.

In this Blog I want to focus on stand-up meetings, and why it is so difficult to make them efficient for software development teams. My experiences come from three different Scrum teams I've been in, and include people from all over the world.


Rules

Here is a (hopefully) common sense of the stand-up meeting.

Why Stand Up

We sit in front of our computers the whole day, concentrating on things we must solve on our own, so standing up means that we now concentrate on things that concern us all as a team. Thus standing up transfers us to common sense, which is at the very core of Scrum. When you are disabled, turn your chair towards the meeting point.

What to Say

  1. What have you done since the last meeting?
  2. Is there something that blocks your work?
  3. What will you do until the next meeting?

The "What" here are issues done in sprint planning, accessible for all by some issue tracker. In case you were working on several issues, preparing a "cheat-sheet" for the meeting may be recommendable.

Blockers are usually issues that do not contain enough information to solve them. You should tell what informations exactly you are missing, so that the Scrum Master or the Product Owner can take actions.

Your statement should not be longer than two minutes, maximum three. Use an egg-timer to control that. Because in a Scrum team there are seven members on average, a meeting should last just a quarter of an hour.

What to NOT Say

The stand-up is not for technical surveys and discussions. Do this afterwards, individually. Knowledge about how to solve the planned issues should be well distributed and available, to be done in the sprint-planning.

Of course things not connected to the sprint are not to be mentioned in the meeting, as there is the new coffee machine, or whether copy & paste is a useful programming technique (it is not).

Who Talks

In a stand-up meeting only team members that have been sprint-planned are talking. It doesn't make sense that the product owner tells what the customer expects the next half year, or stake-holders explain what they would like to achieve with the project. Such should be done in separate meetings.

There can be more participants than just the team members, but these others get no speaking time, they just have the right to listen and put up questions. The team doesn't know their interests and goals, thus such information is useless for it.

Roundabout

After a team member has finished telling its state, it passes on to some other member. Lots of teams use a physical token for that. If you have an egg-timer, pass it to the next person after having set three minutes onto it.

No more to say about stand-up meetings. Scrum claims to be simple.


Problems

Reality mostly differs a lot from theory. Following are my experiences of what people really do in stand-up meetings.

Gives No Context

The team member anticipates that everybody knows what it is working on (which is a big mistake). It doesn't feel responsible for creating a common sense. It doesn't even mention the title of the issue it talks about, doesn't tell the module or application it is maintaining. It doesn't verify whether the team understood what it just said, it seamlessly skips to next issue.

Not Related to Issues

The team member describes how to work with the IDE efficiently, or how to tune the computer. It gives an introduction into design patterns, or explains application architectures.

McDiver

The team member immediately dives down to type- and method-names where no one can follow. Consuming all time with loops, conditions and acronyms, it sounds like a compiler participates in the stand-up.

The Unprepped

The team member did not prepare for the meeting, and is not able to do it unprepared. Losing itself in source code mediations, forgetting to formulate potential blockers, the "What To Do Next" is mostly dropped.

The Big Mac

The team member always says the same at the stand-up, for days, sometimes the whole sprint. Reason is that he did not break his big feature down into smaller tasks (which he then could report to be done).

The Acro Guru

Using lots of acronyms and abbreviations will earn you the Guru title. But it will also cause that it is hard to follow you, and thus makes your statements meaningless for a stand-up. Avoid abbreviations and acronyms wherever you can. You are talking to humans, not machines. Besides, times of small memory are also over!

The Time Chaser

The team member explains its issues well, but in a speed that no one can follow. Typically it is never looking at other members while talking, and uses lots of acronyms.

Solutions

Precondition for mitigating these problems is that the sprint has been planned well. That means no issue in the sprint is estimated to last more than one day. Make sure that long lasting features get broken down to smaller tasks. Only then you can avoid that people tell at the stand-up that they are still working on XXX, and there's nothing more to say.

How to Talk

Try to explain your issues in a way so that others can follow and understand you. You can verify this by looking at them from time to time. It is a bad sign if they're all watching the floor, and a good sign if they nod to you.

Go from General to Special

Start with giving the audience a context of what your issue is, and then carefully go deeper, but do not leave the conceptual level. Talking in source code will not be understood, except when your classes and methods have been named really well (which unfortunately rarely is the case).

Briefing

New team members should be told in their first stand-up what are the rules (see above). Regularly also teach Scrum when the team deviates.

Teaching

Make it clear to the team that the stand-up is communication, and find some example from the project's history that shows how important communication is. Mention that there could be conflicting works or duplicate implementations.

Immediately Intervene

The Scrum master must intervene immediately when rules are violated in a stand-up. Don't shift this to later, when you don't do it now, you'll most likely never do it. And it must be done in a common sense, before the team.


Summary

Scrum is a well-accepted project management technique, mixed together from a collection of best practices by experienced engineers. I particularly appreciate its dedication to simplicity.

Stand-up meetings are a way to keep the team members in touch with each other on a daily basis. More common sense will be the revenue, and that's what we're all missing. Don't expect that your stand-up will improve immediately when you explain things once. It will be a long process to become better.