5
\$\begingroup\$

The goal is to list out some data, but the methods vary on options, and whether the user is on a touch device.

This is my current function declaration. Very non-DRY. Values of isTouch and headlinesOnly don't change during loop, so I don't want to make if tests inside the loop.

Edit: This is primarily meant for a memory-sensitive environment which, when not handled properly, reboots to safe mode, not a regular browser. (But I'm making it for both.)

var makeElement;

if (isTouch) {
    if (headlinesOnly) {
        makeElement = function(item, top, left) {
            var a = document.createElement("a");
            a.setAttribute('ontouchstart', 'itemTouchStart()');
            a.setAttribute('ontouchmove', 'itemTouchMove()');
            a.setAttribute('ontouchend', 'itemTouchEnd(\'' + item.link + '\')');
            a.style.top = top + 'px';
            a.style.left = left + 'px';
            a.innerHTML = '<h1>' + item.title + '</h1>';
            return a;
        };
    }
    else {
        makeElement = function(item, top, left) {
            var a = document.createElement("a");
            a.setAttribute('ontouchstart', 'itemTouchStart()');
            a.setAttribute('ontouchmove', 'itemTouchMove()');
            a.setAttribute('ontouchend', 'itemTouchEnd(\'' + item.link + '\')');
            a.style.top = top + 'px';
            a.style.left = left + 'px';
            a.innerHTML = '<h1>' + item.title + '</h1>'
                        + '<div>' + item.content + '</h1>';
            return a;
        };
    }

    var dragging;

    window['itemTouchStart'] = function() {
        dragging = false;
    };

    window['itemTouchMove'] = function() {
        dragging = true;
    };

    window['itemTouchEnd'] = function(url) {
        if (dragging) return;

        if (cycript) {
            tryOpenUrl(url);
        }
        else {
            window.open(url, "_blank");
        }
    };
}
else {
    if (headlinesOnly) {
        makeElement = function(item, top, left) {
            var a = document.createElement("a");
            a.setAttribute('onmouseup', 'itemMouseUp(\'' + item.link + '\')');
            a.style.top = top + 'px';
            a.style.left = left + 'px';
            a.innerHTML = '<h1>' + item.title + '</h1>';
            return a;
        };
    }
    else {
        makeElement = function(item, top, left) {
            var a = document.createElement("a");
            a.setAttribute('onmouseup', 'itemMouseUp(\'' + item.link + '\')');
            a.style.top = top + 'px';
            a.style.left = left + 'px';
            a.innerHTML = '<h1>' + item.title + '</h1>'
                        + '<div>' + item.content + '</h1>';
            return a;
        };
    }

    window['itemMouseUp'] = function(url) {
        window.open(url, "_blank");
    };
}


var item = myCollection.shift();

container.appendChild(makeElement(item));

var columnWidth = container.childNodes[0].offsetWidth;
var containerWidth = container.offsetWidth;
var numberOfColumns = containerWidth / columnWidth;
var columns = [];

for (var i = 0; i < numberOfColumns; i++) {
    this.columns.push(0);
}

while (item = myCollection.shift()) {
    var sh = getShortestColumn();
    var itemElement = makeElement(item, sh.height, sh.index * columnWidth);
    container.appendChild(itemElement);
    columns[sh.index] += itemElement.offsetHeight;
}
\$\endgroup\$
0

2 Answers 2

2
\$\begingroup\$

The two makeElement functions are the same, except for the innerHTML. You could move the common part to its own function, and make it take innerHTML as parameter:

function createAnchor(item, top, left, innerHTML) {
    var a = document.createElement("a");
    a.setAttribute('ontouchstart', 'itemTouchStart()');
    a.setAttribute('ontouchmove', 'itemTouchMove()');
    a.setAttribute('ontouchend', 'itemTouchEnd(\'' + item.link + '\')');
    a.style.top = top + 'px';
    a.style.left = left + 'px';
    a.innerHTML = innerHTML;
    return a;
}

And then the code can be a bit simpler:

if (headlinesOnly) {
    makeElement = function(item, top, left) {
        var innerHTML = '<h1>' + item.title + '</h1>';
        return createAnchor(item, top, left, innerHTML);
    };
} else {
    makeElement = function(item, top, left) {
        var innerHTML = '<h1>' + item.title + '</h1>'
                    + '<div>' + item.content + '</h1>';
        return createAnchor(item, top, left, innerHTML);
    };
}

You can do likewise for the other functions too. You can have a chain of these helper functions: one function that creates a very primitive anchor, setting only top and left, and other helpers can build on top of it to set the event handlers and inner html that they need.

\$\endgroup\$
3
\$\begingroup\$

Two things right off the bat:

  • You're assuming that item.title and item.content are "safe" to insert. I.e. that they don't contain HTML, something that could be mistaken for HTML, or that if they contain HTML, it's meant to be there.

    For instance, if title is "How to use the <a> tag", then the "<a>" will be interpreted as HTML by the browser, and you won't get the right result.

  • Putting block elements (h1 and div) inside an inline element (a) is invalid HTML. (Edit: I was wrong - it's allowed, if you're using HTML5. Still invalid in earlier version, though. Tip 'o the hat to @dev-null for the correction). Anyway, your a elements hardly need to be a elements; you're handling all the interaction in JavaScript. They could just as well be div elements, or article elements or something else entirely.

Moving on. Janos already touched on how to improve makeElement, but I'd take it a step further.

I don't like the idea of defining a global function in an if/else to begin with, when said if/else could just as well be inside the function.

Using Janos' extracted createAnchor, you could simply define makeElement as:

function makeElement(item, top, left, includeContent) {
    var innerHTML = '<h1>' + item.title + '</h1>';
    if(includeContent) {
        innerHTML += '<div>' + item.content + '</h1>';
    }
    return createAnchor(item, top, left, innerHTML);
}

The includeContent argument can probably be left out, since headlinesOnly seems to be a global (which is not a good thing, by the way). Here, I've made the argument "positive", as in "yes, do include content", since that's the part you're deciding on. Headlines are always present, so headlinesOnly is a bit of a double negation in the sense that true actually means "Yes, don't include content".

Overall, however, makeElement is poor name. Everything on the page is an element, so "make element" doesn't explain anything about the function's purpose. I don't know what your page is displaying, but I'm sure there's some more descriptive name for those elements; forget that they're HTML elements and consider instead what they're representing.

And while I used Janos' createAnchor, I'm not a huge fan of it. Using onevent-style attributes is not good practice. It's been outmoded for many years now.

Instead you should use addEventListener to attach event handling functions to your elements.

This would also mitigate the other big no-no you're committing: window['something'] = function ....

First of all, why not just say window.something = ... instead of the bracket/subscript? Using brackets is only useful if you want to access a property using a dynamic string. Here, the strings are hardcoded so it makes no difference.

But much more importantly, why are you declaring so many global functions? It's plain bad practice to pollute the global namespace like that.

Again, using addEventListener would avoid all that.

I'm also wondering why the behavior is different for touch vs mouse interaction. If I drag on a touch screen, the touchend event is ignored. But if I drag with a mouse, the mouseup event is not ignored. So why not use the click event instead?

Of course, click does have a delay on many touch screen clients, since the device is waiting to see if there's a 2nd tap, i.e. if it's a double-tap that's meant to zoom or some similar "meta-interaction". Whereas on with a mouse and keyboard there's no inherent meaning to a double-click, hence there's no delay.

So maybe keep the touchstart/move/end handlers for touch screens, but just use click for mouse events.

Lastly, as idle speculation, do you need to absolutely position your elements? You could make "column" elements instead, arranged side-by-side with CSS, and simply add your item elements to those. The browser would figure out how to do the layout. But I don't know the particulars of your page.


Here's one way to factor the code. Not saying it's perfect (far from it), but there's no duplication:

function createItemElement(item, includeContent) { // still not a great name
  var element = document.createElement('div'), // not an <a>
      heading = document.createElement('h1'),
      content;

  heading.innerHTML = item.title; // NOTE: I'm *assuming* the title is "safe" to insert as HTML
  element.appendChild(heading);

  if(includeContent) {
    content = document.createElement('div');
    content.innerHTML = item.content; // NOTE: See the other note
    element.appendChild(content);
  }

  addItemClickListener(item, element);

  return element;
}

function addItemClickListener(item, element) {
  var touchmove;

  // define what should happen on click/tap
  function openItemUrl(event) {
    isTouch && cycript ? tryOpenUrl(item.url) : window.open(url, '_blank');
  }

  if(isTouch) {
    touchmove = function (event) {
      // remove move/end listeners in case of touchmove
      element.removeEventListener('touchend', openItemUrl);
      element.removeEventListener('touchmove', touchmove);
    };

    element.addEventListener('touchstart', function () {
      // only add the move/end listeners when a touchstart occurs
      element.addEventListener('touchmove', touchmove);
      element.addEventListener('touchend', openItemUrl);
    });
  } else {
    element.addEventListener('click', openItemUrl);
  }
}

// ....

// basic usage
var itemElement = createItemElement(item, !headlinesOnly);
itemElement.style.top = ...;
itemElement.style.left = ...;
container.appendChild(itemElement);

Here, createItemElement doesn't care about positioning; it just creates an element, and then it's up to you to do something with it. Likewise, it knows nothing about touch vs mouse; that's all contained in addItemClickListener. And the event handling doesn't use an explicit drag boolean; it just adds and removes the event listeners as needed.

Either function knows as little as possible, while the things that always change (positioning) is simply handled directly without being passed via arguments.

Optimally, the isTouch and cycript (whatever that is) variables wouldn't need to be global either, as they're what's creating the most coupling right now; addItemClickListener depends on both being present, while createItemElement is much better off just relying only on its arguments (and, well, on document, but that's a pretty safe thing to rely on in a browser).

I've also been a bit pedantic in building the elements, explicitly creating them with document.createElement. I'm just not a fan of defining chunks of HTML as strings. Besides, with this, you can more easily control how the title and content is added to the elements.

\$\endgroup\$
6
  • \$\begingroup\$ Just as a best practices thing, in this case I'd delegate the event listeners to the root list element which the list items are sitting in. Not really doable here since the element creation function doesn't actually know anything about the context the created elements will be sitting in. Though now that I'm thinking about it I wonder if the event listeners should be added inside the element creation function or if that should be done after element creation, within the myCollection loop. \$\endgroup\$
    – moberemk
    Commented Jun 13, 2015 at 17:34
  • \$\begingroup\$ @moberemk I was thinking the same thing. However, doing so would require a bit more work in the event handlers themselves, to a) figure out what was actually clicked (this will be the container, and event.target can be several things, as there are several levels of nesting), and b) somehow marry that with the correct item.url. It can all be done of course, but I didn't want to wander too far from OP's code. \$\endgroup\$
    – Flambino
    Commented Jun 13, 2015 at 17:40
  • \$\begingroup\$ Yeah it's a little out of scope from the original question; at the very least though this suggests there should be some kind of an <a> element involved in this markup if they're links unless there's some very specific reason to only rely on Javascript's window.location, in which case maybe attach it with a data- attribute? \$\endgroup\$
    – moberemk
    Commented Jun 13, 2015 at 19:17
  • \$\begingroup\$ @moberemk The original code's odd in that regard. It's adding anchor elements, but it doesn't use them for anything, nor are their href attributes set to anything, ever. All they do is (incorrectly) wrap some block elements. True, you could fix the structure by nesting tags properly, and/or use the dataset API, and have the events bubble to the container. But again, it's out of scope. There are plenty of things that could/should be done differently, but it's a long answer already, and just deals with the immediate code. But by all means add your thoughts as an answer (or leave an upvote). \$\endgroup\$
    – Flambino
    Commented Jun 13, 2015 at 21:18
  • \$\begingroup\$ @Flambino In HTML5 it's allowed to suround block levels elements with a tags: davidwalsh.name/html5-elements-links \$\endgroup\$ Commented Jun 15, 2015 at 8:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.