Blog-Archiv

Sonntag, 25. Juni 2017

JS Layout Reality versus CSS Reqirement

The difference between attributes and properties does not look very clear from the JavaScript point of view. The property textField.value delivers the current user input, while textField.getAttribute('value') delivers just the initial value? On the Internet you find answers:

An HTML attribute is one item in the "attributes" property of the DOM-node.

Attributes and properties are connected in different ways. Find details in the w3c specification.

Similar it is for read/writable style properties like height and their read-only reflection offsetHeight or clientHeight. If you set style.height of an DOM element, that value will be reflected into offsetHeight as soon as you request that property. With one exception: there was an animation (transition) defined on that property. This Blog is about what happens then.

Example

Height em Seconds


height:
offsetHeight:

Set "Height" to 8 em, and then press "Set Height". This will trigger

grayRectangle.style.transition = "";
grayRectangle.style.height = 8+"em";

By the control output inside the gray rectangle you can see that grayRectangle.offsetHeight is set to the according pixel-height immediately.

Now set "Height" to 12 em, and then press "Animate Height". This will do

grayRectangle.style.transition = "height 2s";
grayRectangle.style.height = 12+"em";

A timer is used to track the changes in grayRectangle.offsetHeight. You see that the CSS height value is not immediately reflected into that DOM node property, instead it is subsequently updated by the animation.

Consequences

When you use animation on a layout property like width or height, CSS style requirements differ from DOM reality for a while. A geometry calculation done in that time will not be accurate when it uses DOM properties like offsetWidth or offsetHeight.

So be warned when you use geometry calculations like I introduced in my Blog about get and set on element width / height. They won't work when you use animations.

Workaround

In such a case you need to buffer all affected elements, set and get values in a custom-property on them, and provide a flush() function that you call at end of all layout calculations. Here is an example of what I mean.

 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
      var bufferedHeights = [];
      
      var getOverallHeight = function(element) {
        if (element.deferredHeight === undefined) {
          element.deferredHeight = elementDimensions.getOverallHeight(element);
          addToBufferedHeights(element, false);
        }
        return element.deferredHeight;
      };
      
      var setOverallHeight = function(element, numericHeightValue) {
        element.deferredHeight = numericHeightValue;
        addToBufferedHeights(element, true);
      };
      
      var addToBufferedHeights = function(element, modified) {
        for (var i = 0; i < bufferedHeights.length; i++) {
          if (bufferedHeights[i].element === element) {
            if (modified) /* modification overwrites read-only access */
              bufferedHeights[i].modified = modified;
            
            return; /* already in list */
          }
        }

        bufferedHeights.push({
          element: element,
          modified: modified
        });
      };
      
      var flushBufferedHeights = function() {
        for (var i = 0; i < bufferedHeights.length; i++) {
          var bufferedHeight = bufferedHeights[i];
          
          if (bufferedHeight.modified)
            elementDimensions.setOverallHeight(bufferedHeight.element, bufferedHeight.element.deferredHeight);
          
          bufferedHeight.element.deferredHeight = undefined;
        }
        bufferedHeights = [];
      };

You can use getOverallHeight() and setOverallHeight() all the time during layout calculations. When all elements are sized, call flushBufferedHeights() to close the gap between CSS requirements and DOM reality. That way you work around both calculation mistakes and repaint / reflow problems.




Mittwoch, 21. Juni 2017

Stringify HTML Elements in JS

When debugging JavaScript, you may find out that one thing slows you down particularly:

  • What and where is the DOM element I am having in this variable?

Even if the debugger would highlight the hovered variable in the browser view, this may not be helpful in certain situations, as the page might be in an intermediate state. Instead, when you hover a variable, normally a big popup shows up, containing all properties of the element. You need to scroll to bottom just to see tagName or textContent.


This Blog is about a little debugging helper that I recently wrote to cope with this problem. It outputs a text representation of a DOM element to the browser console (that can be viewed e.g. in the debugger).

Element toString()

Most important information about a DOM element is surely the tag name.

Second is the text inside the element. For better readability I decided to not render nested markup.

Debugging output must not be too long. The string representation of an element should fit into a line of 72 characters (e-mail line length, printer page). So I must shorten the embedded text. Best is to take head and tail.

Another important information may be the id of the element, or its CSS class attribute. I decided to not render the class when there is an id, because the id may be sufficient for identification.
This concept could be extended to also search for attributes like href (links), src (images) or type (input fields).

So here comes my proposal.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
      var elementToString = function(element) {
        var idOrClass =
          element.id ? " id='"+element.id+"'" :
          element.className ? " class='"+element.className+"'" :
          "";
        
        var text = element.textContent;
        if (text) {
          text = text.replace(/\n/g, " "); /* turn newlines into spaces */
          text = text.trim().replace(/  +/g, " "); /* normalize space */
          
          var maxLength = 48;
          if (text.length > maxLength) {
            var cutLength = maxLength / 2;
            var substitute = "....";
            var head = text.slice(0, cutLength).trim();
            var tail = text.slice(text.length - cutLength + substitute.length).trim();
            text = head + substitute + tail;
          }
        }
        
        return "<"+element.tagName + idOrClass + ">" + text + "</"+element.tagName+">";
      };

First I create a string representation of the id if it exists. When not, I try to find the CSS class attribute.

The textContent property gives us the text representation of an element and all its nested elements, without HTML tags, but with newlines. I replace all newlines by spaces. Then I replace all occurrences of more than one space by just one space ("normalize space").

The maxLength could be a parameter to this function. When the text is longer than 48 characters, I just keep its head and tail, and put a "...." in between. The JS slice() function does what substring() does in Java.

Finally the element is printed like it was written in HTML, enclosed in its tags. This gives a nice output that helps a lot to recognize DOM elements in JS when debugging! You can try it out by hovering any element in this Blog.