1
\$\begingroup\$

Who has to use enterprise software like for example Microsoft Access knows that control: There a two select lists. The one list offers possible options. With a button you can add one or multiple options to a second list. The second list are your choices (from the offered options).

Moreover, one can move already selected options back into the list of the offered options. So that they aren't part of the choice-list any longer.

I have (re-)build these feature for a Web-GUI with JavaScript and Sass CSS:

(function() {
  'use strict';

  var selects = document.querySelectorAll('.multiple-select');
  var buttons = document.querySelectorAll('.nav-button');
  var selectsSize;

  selects = Array.prototype.slice.call(selects);
  buttons = Array.prototype.slice.call(buttons);
  // Don't want the select to change size (when options added / removed).
  selectsSize = selects[0].children.length +
    selects[1].children.length;

  selects[0].setAttribute('size', selectsSize);
  selects[1].setAttribute('size', selectsSize);

  function shiftOption(columnClassName, toClassName) {
    var fromSelect = // Search within named class for option-elements with state "checked".
      document.querySelectorAll(
        '.' + columnClassName + ' option:checked'
      );
    var toSelect =
      document.querySelector('.' + toClassName);

    fromSelect = Array.prototype.slice.call(fromSelect);
    // Move the option-elements from the one select to the other ...
    fromSelect.forEach(function(option) {
      toSelect.appendChild(option);
      option.selected = false; // Otherwise the option stays marked as selected.
    });
  }

  buttons.forEach(function(button) {
    button.addEventListener('click', function() {
      shiftOption(shiftOption(this.dataset.from, this.dataset.to));
    });
  });
})();
* {
  padding: 0;
  margin: 0;
}
body {
  background-color: powderblue;
  font-family: Futura, Trebuchet MS, Arial, sans-serif;
}
.wrap {
  width: 800px;
  margin: 5px auto;
  border: 1px solid #666666;
  padding: 20px 30px 15px;
  border-radius: 4px;
  box-shadow: inset 1px 1px 0 black;
}
.wrap p:first-of-type {
  text-align: center;
  margin-bottom: 20px;
  font-size: 1.2rem;
}
nav {
  display: flex;
  align-items: center;
}
select {
  min-width: 150px;
  width: 100%;
  padding: 5px 10px;
}
select,
.nav-button {
  border-radius: 4px;
}
.left-column,
.right-column {
  width: 45%;
}
.multiple-select option {
  font-size: 1rem;
}
.middle-column {
  width: 10%;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.nav-button {
  text-decoration: none;
  font-size: 1.6rem;
  font-weight: 900;
  padding: 5px 12px;
  border: 1px solid black;
  color: black;
  background-color: #cdcdcd;
  display: block;
  text-align: center;
  width: calc(100% - 40px);
  margin-bottom: 10px;
}
.nav-button:hover {
  color: white;
  background-color: #323232;
}
.nav-button:active {
  background-color: #ff7807;
  box-shadow: inset 1px 1px 1px black;
  border-right-color: #787878;
  border-bottom-color: #787878;
}
h3 {
  margin-bottom: 15px;
  text-align: center;
  font-family: georgia;
  text-transform: uppercase;
  letter-spacing: 1px;
  font-size: 1.4rem;
}
<div class="wrap">
  <p><strong>Use the ctrl- and shift-key to select multiple options !</strong>
  </p>
  <nav>
    <div class="left-column">
      <h3>Options available</h3>
      <select class="multiple-select left-select" multiple>
        <option>Red</option>
        <option>Yellow</option>
        <option>Blue</option>
        <option>Violet</option>
        <option>Orange</option>
        <option>Green</option>
        <option>Teal</option>
        <option>Magenta</option>
        <option>PowderBlue</option>
      </select>
    </div>
    <div class="middle-column">
      <a href="#" class="nav-button to-right" data-from="left-select" data-to="right-select">>></a>
      <a href="#" class="nav-button to-left" data-from="right-select" data-to="left-select">
        <<</a>
    </div>
    <div class="right-column">
      <h3>Options chosen</h3>
      <select class="multiple-select choices right-select" multiple>
        <option>ALPHA</option>
        <option>BETA</option>
        <option>GAMMA</option>
      </select>
    </div>
  </nav>
</div>

The uncompiled SASS code:

$primaryColor:powderblue;
 $secondaryColor:#cdcdcd;
 $fontStack:Futura,
Trebuchet MS,
Arial,
sans-serif;
 $radius:4px;
 * {
  padding: 0;
  margin: 0;
}
body {
  background-color: $primaryColor;
  font-family: $fontStack;
}
.wrap {
  width: 800px;
  margin: 5px auto;
  border: 1px solid lighten(black, 40%);
  padding: 20px 30px 15px;
  border-radius: $radius;
  box-shadow: inset 1px 1px 0 black;
}
.wrap p:first-of-type {
  text-align: center;
  margin-bottom: 20px;
  font-size: 1.2rem;
}
nav {
  display: flex;
  align-items: center;
}
select {
  min-width: 150px;
  width: 100%;
  padding: 5px 10px;
}
select,
.nav-button {
  border-radius: $radius;
}
.left-column,
.right-column {
  width: 45%;
}
.multiple-select option {
  font-size: 1rem;
}
.middle-column {
  width: 10%;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.nav-button {
  text-decoration: none;
  font-size: 1.6rem;
  font-weight: 900;
  padding: 5px 12px;
  border: 1px solid black;
  color: black;
  background-color: $secondaryColor;
  display: block;
  text-align: center;
  width: calc(100% - 40px);
  margin-bottom: 10px;
}
.nav-button:hover {
  color: white;
  background-color: invert($secondaryColor);
}
.nav-button:active {
  background-color: #ff7807;
  box-shadow: inset 1px 1px 1px black;
  border-right-color: #787878;
  border-bottom-color: #787878;
}
h3 {
  margin-bottom: 15px;
  text-align: center;
  font-family: georgia;
  text-transform: uppercase;
  letter-spacing: 1px;
  font-size: 1.4rem;
}

Is my "rebuild" done in a good way? Are there points which I should change / improve?

\$\endgroup\$
1
\$\begingroup\$

Disclaimer: this answer doesn't examine the SASS aspect. It only focuses on JS code improvement (and to further adaptation for a more general use).

JS code improvement

To precisely answer the question: yes, the "rebuild" is done in a good way, regarding how to achieve what it's intended to.

But its default from my point of view is that it uses too many unneedded variables and statements, so harming performance (though realistically it doesn't matter in this case) and readability.

The most noticeable point is that xxx = Array.prototype.slice.call(xxx); (used three times) is totally useless.
I guess it's intended to turn xxx from NodeList to Array (at least in two cases from three), but it's not needed, since forEach() is also a method of NodeList.
So it can be suppressed.

The second important point is that the shiftOption() function is a bit overkill: its only real task is its embedded forEach().
The rest is merely the previous evaluation of the involved elements, which is currently diluted in a cascading suite of intermediate variables.

BTW I wonder why it's called "twice at once" with: shiftOption(shiftOption(this.dataset.from, this.dataset.to));, the most external call using the internal one (which returns undefined) so doing nothing!

Anyway, taking in account the above points, the whole JS snippet can be reduced like this (tested):

(function() {
  'use strict';

  var selects = document.querySelectorAll('.multiple-select');
  // Don't want the select to change size (when options added / removed).
  var selectsSize = selects[0].children.length + selects[1].children.length;
  selects[0].setAttribute('size', selectsSize);
  selects[1].setAttribute('size', selectsSize);

  document.querySelectorAll('.nav-button').forEach(function(button) {
    button.addEventListener('click', function() {
      var targetBox = document.querySelector('.' + this.dataset.to);
      // Move the option-elements from the one select to the other ...
      document.querySelectorAll('.' + this.dataset.from + ' option:checked')
      .forEach(function(option) {
        targetBox.appendChild(option);
        option.selected = false; // Otherwise the option stays marked as selected.
      });
    });
  });
})();

Generalization

Now another interesting point.

As it seems to be intended for a general use, the snippet might benefit from kind of "configuration", so the developer gets rid of defining options in the HTML part.

This might be achieved by turning the IIFE to a plain function, which can be called with an object embedding the simple list of each (left/right) initial options.
Then, apart from doing its job as already defined, the function previously initializes the two involved <select>s with the passed options.

It might be something like this:

function optionsManager(options) {
  'use strict';

  // populate <select>s:
  var optionsCount = options['left-select'].length + options['right-select'].length;
  for (var target in options) {
    var targetBox = document.querySelector('.' + target);
    targetBox.setAttribute('size', optionsCount);
    options[target].forEach(function(option) {
      targetBox.appendChild(
        document.createElement('option')
        .appendChild(document.createTextNode(option)).parentNode
      );
    });
  }

  // bind buttons:
  document.querySelectorAll('.nav-button').forEach(function(button) {
    button.addEventListener('click', function() {
      var targetBox = document.querySelector('.' + this.dataset.to);
      // Move the option-elements from the one select to the other ...
      document.querySelectorAll('.' + this.dataset.from + ' option:checked')
      .forEach(function(option) {
        targetBox.appendChild(option);
        option.selected = false; // Otherwise the option stays marked as selected.
      });
    });
  });
}

optionsManager({
  'left-select': ['Red', 'Yellow', 'Blue', 'Violet', 'Orange', 'Green', 'Teal',
  'Magenta', 'PowderBlue'],
  'right-select': ['ALPHA', 'BETA', 'GAMMA']
});

Here is a working version:

function optionsManager(options) {
  'use strict';
  
  // populate <select>s:
  var optionsCount = options['left-select'].length + options['right-select'].length;
  for (var target in options) {
    var targetBox = document.querySelector('.' + target);
    targetBox.setAttribute('size', optionsCount);
    options[target].forEach(function(option) {
      targetBox.appendChild(
        document.createElement('option')
        .appendChild(document.createTextNode(option)).parentNode
      );
    });
  }
  
  // bind buttons:
  document.querySelectorAll('.nav-button').forEach(function(button) {
    button.addEventListener('click', function() {
      var targetBox = document.querySelector('.' + this.dataset.to);
      // Move the option-elements from the one select to the other ...
      document.querySelectorAll('.' + this.dataset.from + ' option:checked')
      .forEach(function(option) {
        targetBox.appendChild(option);
        option.selected = false; // Otherwise the option stays marked as selected.
      });
    });
  });
}

optionsManager({
  'left-select': ['Red', 'Yellow', 'Blue', 'Violet', 'Orange', 'Green', 'Teal',
  'Magenta', 'PowderBlue'],
  'right-select': ['ALPHA', 'BETA', 'GAMMA']
});
* {
  padding: 0;
  margin: 0;
}
body {
  background-color: powderblue;
  font-family: Futura, Trebuchet MS, Arial, sans-serif;
}
.wrap {
  width: 800px;
  margin: 5px auto;
  border: 1px solid #666666;
  padding: 20px 30px 15px;
  border-radius: 4px;
  box-shadow: inset 1px 1px 0 black;
}
.wrap p:first-of-type {
  text-align: center;
  margin-bottom: 20px;
  font-size: 1.2rem;
}
nav {
  display: flex;
  align-items: center;
}
select {
  min-width: 150px;
  width: 100%;
  padding: 5px 10px;
}
select,
.nav-button {
  border-radius: 4px;
}
.left-column,
.right-column {
  width: 45%;
}
.multiple-select option {
  font-size: 1rem;
}
.middle-column {
  width: 10%;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.nav-button {
  text-decoration: none;
  font-size: 1.6rem;
  font-weight: 900;
  padding: 5px 12px;
  border: 1px solid black;
  color: black;
  background-color: #cdcdcd;
  display: block;
  text-align: center;
  width: calc(100% - 40px);
  margin-bottom: 10px;
}
.nav-button:hover {
  color: white;
  background-color: #323232;
}
.nav-button:active {
  background-color: #ff7807;
  box-shadow: inset 1px 1px 1px black;
  border-right-color: #787878;
  border-bottom-color: #787878;
}
h3 {
  margin-bottom: 15px;
  text-align: center;
  font-family: georgia;
  text-transform: uppercase;
  letter-spacing: 1px;
  font-size: 1.4rem;
}
<div class="wrap">
  <p><strong>Use the ctrl- and shift-key to select multiple options !</strong>
  </p>
  <nav>
    <div class="left-column">
      <h3>Options available</h3>
      <select class="multiple-select left-select" multiple>
      </select>
    </div>
    <div class="middle-column">
      <a href="#" class="nav-button to-right"
      data-from="left-select" data-to="right-select">&gt;&gt;</a>
      <a href="#" class="nav-button to-left"
      data-from="right-select" data-to="left-select">&lt;&lt;</a>
    </div>
    <div class="right-column">
      <h3>Options chosen</h3>
      <select class="multiple-select choices right-select" multiple>
      </select>
    </div>
  </nav>
</div>

Note that it might be yet more generalized in several ways.

Example: pass to the function the id of the container element (in the current case the <div class="wrap">).
Then the function would automatically recognize the first/second <select>s as left/right ones, so the classNames currently used are no longer needed, and several different containers can be managed independently in the same page.

\$\endgroup\$
  • \$\begingroup\$ forEach() on a nodeList directly works like charm. I haven't indeed not known that nodeList has this method too. Thought it would be array exclusive. :)) Thanks a bunch. \$\endgroup\$ – michael.zech Dec 5 '16 at 16:43
  • \$\begingroup\$ @mizech Glad to help! \$\endgroup\$ – cFreed Dec 5 '16 at 17:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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