"I like JavaScript"

@cowboyd

"I do not like JavaScript"

-- many many people

"The JavaScript Problem"

-- The Haskell Community
https://wiki.haskell.org/The_JavaScript_Problem

You're not wrong Walter

You're just a Hask-hole

FP

The truth is that we've been gradually doing more and more functional-style programming for years.

MVC

The view is a pure function of the model

The New MVC

  • One-Way
  • Side-Effect Free
  • Idempotent
Layer 1

Just One-Way?

In real-world applications data flow is not one-way

What's Up?

How does data make its way back to the model/server?

?

Layer 1

Number One Answer:

callback

Callbacks

Data Down. Actions Up.

Layer 1

Is there a better way?

on('input')

http://frontside.io
@cowboyd

Hello.

I believe we've already met.

Simple.


$('form').on('submit', function() {
  $.post('/api/cards', $(this).serializeJSON());
});
          

Requirement:

I don't want to submit the form if it isn't valid

Easy.


$('form').validator({
  "input[name=card-number]": {
    validations: {
      lunCheck: function(value) {/* isValidLuhn(value)*/ }
    },
    errorMessageSelector: ".card-number-errors"
  } //etc.
}).on('submit', function(e) {
  const validation = $(this).validate(); //validation plugin
  if (validation.passed) {
    $.post('/api/cards', $(this).serializeJSON());
  } else {
    e.preventDefault();
  }
});
          

Requirement

I want validation to happen every time a field changes

Easy: Part 2


$('form').validator({
  "input[name=card-number]": {
    validations: {
      lunCheck: function(value) {/* isValidLuhn(value)*/ }
    },
    errorMessageSelector: ".card-name-errors"
  } //etc.
}).on('submit', function(e) {
  const validation = $(this).validate(); //validation plugin
  if (validation.passed) {
    $.post('/api/cards', $(this).serializeJSON());
  } else {
    e.preventDefault();
  }
}).on('change', function() {
  const validation = $(this).validate();
});
          

Requirement

Don't enable the submit button unless the form is actually valid.

Easy Part 2.1


$('form').validator({
  "input[name=card-number]": {
    validations: {
      lunCheck: function(value) {/* isValidLuhn(value)*/ }
    },
    errorMessageSelector: ".card-name-errors"
  } //etc.
}).on('submit', function(e) {
  const validation = $(this).validate(); //validation plugin
  if (validation.passed) {
    $.post('/api/cards', $(this).serializeJSON());
  } else {
    e.preventDefault();
  }
}).on('change', function() {
  const validation = $(this).validate();
  $(this).find('input[type=submit]').prop('disabled', !validation.passed);
});
          

Requirement

Let's verify with the server that this card isn't in our fraud database.

Part 3: Revenge of the Sith


let isSafeCardNumber = false;

$('form').validator({
  "input[name=card-number]": {
    validations: {
      lunCheck: function(value) {/* isValidLuhn(value)*/ }
    },
    errorMessageSelector: ".card-name-errors"
  } //etc.
}).on('submit', function(e) {
  const validation = $(this).validate(); //validation plugin
  if (validation.passed && isSafeCardNumber) {
    $.post('/api/cards', $(this).serializeJSON());
  } else {
    e.preventDefault();
  }
}).on('change', function() {
  const validation = $(this).validate();
  $(this).find('input[type=submit]').prop('disabled', !validation.passed);
}).on('change', function(e) {
  const validation = $(this).validate();
  $(this).find('input[type=submit]').prop('disabled', !validation.passed);
  const creditCardNumber = this.elements['card-number'];
  if (e.target === creditCardNumber) {
    $.get(`/api/fraud-detector/cards/${creditCardNumber.value}`).then(()=> {
      isSafeCardNumber = true;
    }).fail(()=> {
      $(this).find('input[type=submit]').prop('disabled', !validation.passed);
      isSafeCardNumber = false;
    });
  }
});
          

Requirement

Can we throw up a loading spinner while we're doing the fraud detection?

umm...


let isSafeCardNumber = false;

$('form').validator({
  "input[name=card-number]": {
    validations: {
      lunCheck: function(value) {/* isValidLuhn(value)*/ }
    },
    errorMessageSelector: ".card-name-errors"
  } //etc.
}).on('submit', function(e) {
  const validation = $(this).validate(); //validation plugin
  if (validation.passed) {
    $.post('/api/cards', $(this).serializeJSON());
  } else {
    e.preventDefault();
  }
}).on('change', function() {
  const validation = $(this).validate();
  $(this).find('input[type=submit]').prop('disabled', !validation.passed);
}).on('change', function(e) {
  const validation = $(this).validate();
  $(this).find('input[type=submit]').prop('disabled', !validation.passed);
  const creditCardNumber = this.elements['card-number'];
  if (e.target === creditCardNumber) {
    $(this).find('.card-name-check-spinner').toggleClass('visible', true);
    $.get(`/api/fraud-detector/cards/${creditCardNumber.value}`).then(()=> {
      isSafeCardNumber = true;
    }).fail(()=> {
      $(this).find('input[type=submit]').prop('disabled', !validation.passed);
      isSafeCardNumber = false;
    }).always(() => {
      $(this).find('.card-name-check-spinner').toggleClass('visible', false);
    });
  }
});
          

I just want to go home!


$('form').on('submit', function() {
  $.post('/api/cards', $(this).serializeJSON());
});
          

Remember this guy?

Requirement:

I don't want to submit the form if it isn't valid

Use MVC

A form's values are a pure function of its inputs

let form = new CardForm({
  number: '4242424242424242'
});

form.type  /* visa */
form.isValid /* true */
form.isInvalid /* false */

          

//form.js
  get type() {
    const number = this.number;
    if (number.match(VISA_REGEX)) {return "visa";}
    else if (number.match(MASTERCARD_REGEX)) {return "mastercard";}
    else if (number.match(AMEX_REGEX)) {return "amex";}
    else if (number.match(DISCOVER_REGEX)) {return "discover";}
    else if (number.match(DINERS_CLUB_REGEX)) {return "diners";}
    else if (number.match(JCB_REGEX)) {return "jcb";}
    else {return undefined;}
  },

  get isValid() {
    const rules = this.rules;
    return rules.reduce(function(currentValue, rule) {
      return currentValue && rule.isFulfilled;
    }, true);
  },
  get isInvalid() {
    return !this.isValid;
  }
          

Card Type: {{form.type}}
            
            
          

Requirement

I want validation to happen every time a field changes

Eliminate State

Create a new form for your computation every time


$('form').on('input', function() {
  const form = new Form($(this).serialize());
  update(form);
})
          

Requirement

Let's verify with the server that this card isn't in our fraud database.


get isValid() {
  const rules = this.rules;
  return rules.reduce(function(currentValue, rule) {
    return currentValue && rule.isFulfilled;
  }, true);
},
          

Solution

Every validation rule is a promise

Promises

Compose Asynchronous And Synchronous Operations

Ansynchrony

Promises

flexible messaging


if (!number || number.length === 0) {
  reject("can't be blank");
} else if (!_this.type) {
   reject("not enough digits");
}
switch(_this.type) {
 case 'diners':
 case 'amex':
   if (number.length === 15) {
     resolve();
   } else {
     reject(`(${number.length}/15)`);
   }
 default:
   if (number.length === 16) {
     resolve();
   } else {
     reject(`(${number.length}/16)`);
   }
 }
        


{{#if rules.numberLongEnough.isRejected}}
  {{rules.numberLongEnough.reason}}
{{/if}}
{{#if rules.numberPassesLuhnCheck.isRejected}}
  {{rules.numberPassesLuhnCheck.reason}}
{{/if}}
          

Flexible Messaging

Promises

Models dependencies of operations with then()

Length -> Luhn Check -> Fraud Check

Sequencing

Promises

  • Asynchrony a breeze
  • Flexible Messaging
  • Natural Sequencing of constraints

This is just a taste

What now?

Data Down. Data Up.

Layer 1 Layer 1

Action in between.

User

Layer 1

Developer

The End