8
\$\begingroup\$

I have two sets of toggles that need to "uncheck" each other. This is my current solution which is working. Can it somehow get more functional/elegant?

let unscheduledLayerToggles = [unscheduledLayerToggle, unscheduledLayerToggleToolbar];
let availableLayerToggles = [availableLayerToggle, availableLayerToggleToolbar];

flipToggles(firstToggles, secondToggles)
flipToggles(secondToggles, firstToggles)

function flipToggles(firstToggles, secondToggles) {
    firstToggles.forEach(function (toggle) {
        on(toggle, "change", function () {
            secondToggles.forEach(function(element) {
                element.checked = false
            })
        })
    })
}
\$\endgroup\$
1
  • \$\begingroup\$ I would have tagged each set of toggles with a class and used that instead. Especially if you are using something like jQuery it is easy to perform an operation on multiple elements matching a class. \$\endgroup\$ Commented Mar 7, 2017 at 2:45

2 Answers 2

5
\$\begingroup\$

The logic itself is simple enough. I see that you are using ECMAScript 6, with it there are some things you could do to simplify the code and make it more robust.

const

You are using let for unscheduledLayerToggles and availableLayerToggles, which is fine, but in the case that you will not be reassigning different values/arrays to those names, you could simply use const instead of let. Note that this will not make the arrays immutable, so you can still manipulate their contents, it only disallows assigning something different to them.

flipToggles function

In the same manner as you can use const to declare function (in the style of var foo = function () { ... }). It helps to make the intentions clear that the function will not change.

Arrow functions

ES6 provides a great way to both shorten/simplify function declarations, as well as isolate their scope better: Arrow Functions.

The syntax is explained well on MDN like I provided. In the case of your flipToggles function, we could write the code this way and eliminate all the unnecessary function() declarations.


const unscheduledLayerToggles = [unscheduledLayerToggle, unscheduledLayerToggleToolbar];
const availableLayerToggles = [availableLayerToggle, availableLayerToggleToolbar];

flipToggles(firstToggles, secondToggles)
flipToggles(secondToggles, firstToggles)

const flipToggles = (firstToggles, secondToggles) => {
  firstToggles.forEach(toggle => {
    on(toggle, "change", () => {
      secondToggles.forEach(element => {
        element.checked = false
      })
    })
  })
}

(mind the indentation, which is a personal preference of mine for 2 spaces indent instead of 4)

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

If you'd want to make it more functional, let's first separate logic from effects (gathering user events and manipulating DOM). And then integrate them back.

Considering logic, it begins with some initial state:

// to make state checks more natural-language-like
const ON = true;
const OFF = false;
const initialState = {
    firstGroup : ON,
    secondGroup : OFF
}

Then we make function that takes previous state and "ACTIVATE_FIRST" or "ACTIVATE_SECOND" command to calculate the next state:

function processCommand (state, command) { 
    // in case there would be more than one command
    switch (command) {
        case "ACTIVATE_FIRST":
            return { 
                firstGroup : ON,
                secondGroup : OFF
            }
        case "ACTIVATE_SECOND":
            return { 
                firstGroup : OFF,
                secondGroup : ON
            }
        default:
            throw new Error("Unknown command")
    }
}

Then, given a stream of commands, starting with initialState, apply reduce (in functional languages it is usually called left fold) RxJS operator to them - and you get stream of states. Conceptually it looks like this: commands -> process -> states. It is your functional logic.

Then you need to connect logic with effects. First, using DOM selector subscribe to events from DOM elements and get two RxJS streams: first producing events that should yield "ACTIVATE_FIRST" command, and second for "ACTIVATE_SECOND". Then map these two streams to commands accordingly and merge into single stream of commands. It would be the left part of the stream chain or sources.

Then for the right part or sinks you should subscribe to a stream of states and finally write simple function that applies given state to DOM.

I don't give more details here, as it is the concept which is important, rather than details. Of course, for such a simple UX this seems like an overkill, but this approach scales great.

If you like it - have a look at CycleJS and Elm.

\$\endgroup\$
4
  • \$\begingroup\$ Very interesting approach, nicely done! \$\endgroup\$
    – Phrancis
    Commented Mar 7, 2017 at 20:14
  • \$\begingroup\$ I would have used a Map instead of switch, though that's mainly because I don't like switch. Also, you should throw an exception, not a string. \$\endgroup\$ Commented Mar 8, 2017 at 1:50
  • \$\begingroup\$ @Brian-McCutchon What we need here is pattern matching. Which is not present in JavaScript spec. Things get especially interesting when commands are no more simple strings, but some objects or data structures. Like a filter command holding filter conditions. This deserves separate implementation on its own. Map wouldn't suffice either. In this case something like PatternMatchingError might be thrown. Not Exception, as it is not supposed to be handled in runtime, but Error. Thanks for pointing that anyway. \$\endgroup\$
    – iTollu
    Commented Mar 8, 2017 at 5:16
  • 1
    \$\begingroup\$ @iTollu I agree about pattern matching. On the subject of errors and exceptions, I don't think JavaScript really differentiates, I just meant that you should put throw new Error("Unknown command") rather than throw "Unknown command", as described here. \$\endgroup\$ Commented Mar 8, 2017 at 6:03

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.