Login form with Bacon.js

bacon.js (GitHub: baconjs/bacon.js, License: MIT) by Juha Paananen (that we’ve talked about last year) let’s you use so called functional reactive (frp) approach to coding your applications. Some say that using standard event handlers instead going reactive is like using for loop to iterate over an array compared to using mapreduce or other functional methods. This article is by no means trying to be a complete introduction to bacon.js capabilities, or a thorough presentation of what a frp is. Treat it more as a sneak peak of what can be done with these tools.

I am going to compare a ‘standard’ observer approach to implementing a simple login form client side validations with how it can be done using bacon.js. All the code is available in github repo. You are more than welcome to fork and tinker with it yourself.

In both examples we are going to work on this form:

<form>
  <input id='login' name='login' type='text' class='invalid' placeholder='Login'><br>
  <input id='password' name='password' type='password' class='invalid' placeholder='Password'><br>
  <input id='button' type='submit' disabled='disabled'>
</form>

Both login and password have to be longer than 5 in order to enable the submit button. When any of the values is valid its text field background is set to green. Otherwise it is red.

Observer approach

The way I would do it ‘normally’ is:

$(function(){
  var loginValid;
  var passwordValid;

  var isValidLogin = function(text) {
    return text.length > 5;
  };

  var isValidPassword = function(text) {
    return text.length > 5;
  };

  $('#login').on('keyup', function() {
    loginValid = isValidLogin($(this).val());
    if(loginValid){
      $(this).addClass('valid');
      $(this).removeClass('invalid');
      if(passwordValid) {
        $('#button').removeAttr('disabled');
      }
    } else {
      $(this).addClass('invalid');
      $(this).removeClass('valid');
      $('#button').attr('disabled', 'disabled');
    }
  });

  $('#password').on('keyup', function() {
    passwordValid = isValidPassword($(this).val());
    if(passwordValid){
      $(this).addClass('valid');
      $(this).removeClass('invalid');
      if(loginValid) {
        $('#button').removeAttr('disabled');
      }
    } else {
      $(this).addClass('invalid');
      $(this).removeClass('valid');
      $('#button').attr('disabled', 'disabled');
    }
  });
});

Because each of the event handlers is only aware of the state of its own text field, we have to use shared mutating variables (loginValidpasswordValid). To determine if we want to disable or enable button, nested if statements have to be used. If you have any ideas how it could be done ‘nicer’ do not hesitate to leave a comment. Anyway this example was supposed to be ‘the ugly one’ 🙂

Bacon.js approach

Here is the code for the same case using bacon.js:

$(function(){
  // Non-functional helper methods responsible for side effects
  var setTextFieldClass = function(selector, valid) {
    if(valid) {
      $(selector).addClass('valid');
      $(selector).removeClass('invalid');
    } else {
      $(selector).addClass('invalid');
      $(selector).removeClass('valid');
    }
  };

  var setButtonEnabled = function(enabled) {
    if(enabled) {
      $('#button').removeAttr('disabled');
    } else {
      $('#button').attr('disabled', 'disabled');
    }
  };

  // Functional code
  // Login input field events stream
  var loginValid = $('#login').asEventStream('keyup')
  .map(function(e) {
    return e.target.value.length > 5;
  });

  loginValid.onValue(function(valid) {
    setTextFieldClass('#login', valid);
  });

  // Password input field events stream
  var passwordValid = $('#password').asEventStream('keyup')
  .map(function(e) {
    return e.target.value.length > 5;
  });

  passwordValid.onValue(function(valid) {
    setTextFieldClass('#password', valid);
  });

  // Combine two streams to determine button state
  loginValid.combine(passwordValid, function(loginVal, passVal) {
    return loginVal && passVal;
  }).onValue(setButtonEnabled);
});

This code can be divided into two parts: non-functional and functional. There are two helper methods (setTextFieldClasssetButtonEnabled) responsible for side effects. Literally changing the state of UI elements on the basis of functional methods return values.

Bacon.js operates on so called ‘event streams’ instead of using event handlers. After creating one you can subscribe to it using onValue method which accepts a callback. It will be executed every time a stream returns a value. In our case it happens when user types something into the text field.

You can use all the functional magic like mapreduce etc. to transform the values returned from the stream. In our case loginValid and passwordValid streams return true or false on the basis of text fields current input validness.

Here comes the best part. State of the submit button depends on the combined validness of both of text fields. To create a stream which will dispatch values on the basis of combined values from both of the previous streams we use (you guessed it !) combine method. Its subscriber executes a setButtonEnabled callback method which sets the button state.

Streams flow looks like this:

Conclusion

Without mutating variables, nested if statements and with separation of methods responsible for side effects code seems much cleaner for me. This is just scratching the surface of what can be done with bacon.js. If you have any ideas how these examples could be improved or disagree with something, then leave a comment or hit me up on the twitter.

Post navigation

Leave a comment

Leave a Reply

Your email address will not be published. Required fields are marked *