CoffeeScript and the Road to ES2015

Who am I?

  • Josh Finnie
  • Senior Software Maven @ TrackMaven
  • NodeDC co-Organizer
  • Lover of JavaScript
  • Coder of Python (and CoffeeScript)

CoffeeScript

CoffeeScript is a little language that compiles into JavaScript. Underneath that awkward Java-esque patina, JavaScript has always had a gorgeous heart. CoffeeScript is an attempt to expose the good parts of JavaScript in a simple way. [1]

Why CoffeeScript?

class BaseFeedModel extends Model

	...

    fromServer: (attributes, share) ->
        @id = attributes.id
        @type = attributes.type
        @shareToken = attributes.share_token

        shareParams = {}
        if share
            @share = true
            shareParams = {
                'share_id': @id
                'share_token': @shareToken
                'share_type': "activity"
            }

Background

  • At TrackMaven, we have a very large Angular.js application.
  • We chose CoffeeScript as it looks very Pythonic (the language our backend application is written in).
  • It allowed for less context switching between front-end and back-end code which allowed for quicker development in the early days.

CoffeeScript vs JavaScript

The next few slides, we have some examples from our code base. First is what they look like in CoffeeScript, then I converted them to JavaScript. Let's compare!

CoffeeScript 1

angular.module('common.services')

.factory('helpers', ->

    camelize: (str) ->
        str = str.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) ->
            if index == 0
                letter.toLowerCase()
            else
                letter.toUpperCase())
        return str.split(" ").join("")

    snakify: (str) ->
        regexp = /[A-Z]/g
        separator = '_'
        str =  str.replace(regexp, (letter, index) ->
            if index == 0
                return letter.toLowerCase()
            else
                return separator + letter.toLowerCase()
        )
        return str

JavaScript 1

angular.module('common.services').factory('helpers', function() {
  return {
    camelize: function(str) {
      str = str.replace(/(?:^\w|[A-Z]|\b\w)/g, function(letter, index) {
        if (index === 0) {
          return letter.toLowerCase();
        } else {
          return letter.toUpperCase();
        }
      });
      return str.split(" ").join("");
    },
    snakify: function(str) {
      var regexp, separator;
      regexp = /[A-Z]/g;
      separator = '_';
      str = str.replace(regexp, function(letter, index) {
        if (index === 0) {
          return letter.toLowerCase();
        } else {
          return separator + letter.toLowerCase();
        }
      });
      return str;
    }
  };
});

CoffeeScript 2


angular.module('common.services')

    .service('browser', ['$window', ($window) ->
        get: ->
            userAgent = $window.navigator.userAgent
            browsers =
                chrome: /chrome/i,
                safari: /safari/i,
                firefox: /firefox/i,
                ie: /internet explorer/i

            for key of browsers
                return key if browsers[key].test(userAgent)

            return 'unknown'
    ])

JavaScript 2

angular.module('common.services').service('browser', [
  '$window', function($window) {
    return {
      get: function() {
        var browsers, key, userAgent;
        userAgent = $window.navigator.userAgent;
        browsers = {
          chrome: /chrome/i,
          safari: /safari/i,
          firefox: /firefox/i,
          ie: /internet explorer/i
        };
        for (key in browsers) {
          if (browsers[key].test(userAgent)) {
            return key;
          }
        }
        return 'unknown';
      }
    };
  }
]);

CoffeeScript 3

angular.module('tags.services')

.factory('Tag', (Model) ->
    class Tag extends Model
        @field 'tagName', default: null
        @field 'count', default: 0
        @field 'id', default: null
        @field 'isDeleted', default: false

        fromServer: (attributes) ->
            @tagName = attributes.name
            if attributes.count
                @count = attributes.count
            @id = attributes.id
            @isDeleted = attributes.marked_as_deleted


    return Tag
)

JavaScript 3 (Part 1)

var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
  hasProp = {}.hasOwnProperty;

angular.module('tags.services').factory('Tag', function(Model) {
  var Tag;
  Tag = (function(superClass) {
    extend(Tag, superClass);

    function Tag() {
      return Tag.__super__.constructor.apply(this, arguments);
    }

    Tag.field('tagName', {
      "default": null
    });

    Tag.field('count', {
      "default": 0
    });

    ...

JavaScript 3 (Part 2)

    ...

    Tag.field('id', {
      "default": null
    });

    Tag.field('isDeleted', {
      "default": false
    });

    Tag.prototype.fromServer = function(attributes) {
      this.tagName = attributes.name;
      if (attributes.count) {
        this.count = attributes.count;
      }
      this.id = attributes.id;
      return this.isDeleted = attributes.marked_as_deleted;
    };

    return Tag;

  })(Model);
  return Tag;
});

So What's Wrong?

  • CoffeeScript is a programming language that transcompiles into JavaScript.
    • This transcompiling process requires a lot of extra work.
    • There are added complications to your workflow when using CoffeeScript.
    • Once transcompiled, your code no longer looks like the code you written making debugging difficult.

However

  • We cannot rewrite our whole application
  • We cannot rewrite our whole application
  • And, most importantly, we cannot rewrite our whole application

I had an Idea

With the adoption of ES2015 and the promise of more regular updates to the EcmaScript standards, I took it upon myself to research how we would go about switching to ES2015 without rewriting our whole application.

 

This was challenging...

What's ES2015?

  • It's JavaScript! New, shiny, better JavaScript!

Thanks Google! (and Meteor...)

Why?

  • With ES2015, CoffeeScript is becoming a bit irrelevant
    • A lot of the neat features of CoffeeScript have been ported over to ES2015
      • Classes
      • Fat Arrows
      • `this` scoping
      • and more!
  • Also, we are moving to less full-stack engineering as we grow, and want to hire JavaScript developers, not CoffeeScript developers. It's easier!

My idea

  • Add Babel to our Gulp workflow
    • We're already transpiling CS, why not ES2015
  • Slowly add new features to TrackMaven written in ES2015 and not written in CoffeeScript
  • Transpile both into "old" (really web-safe) JavaScript
  • Marry the two into web-safe super JavaScript file.

In Practice

  • It worked! Sort of...
    • We had some major issues with tests
    • We had some major issues with imports
  • But small, individual pieces of ES2015 were working in our codebase of CoffeeScript.

 

That was pretty cool!

Next Steps

  • Figure out how to get CoffeeScript and ES2015 to talk to each other.
    • Thoughts on this is that the ES2015 module imports are confusing CoffeeScript...
  • Convert more CoffeeScript on ES2015 (using tools??)
  • Never speak of CoffeeScript again

Cool Comparisons

Here are some examples of why the switch from CoffeeScript to ES2015 is the way forward.

CoffeeScript 1

angular.module('common.filters')

.filter('humanizeNumber', ->
    (number) ->
        [before_decimal, after_decimal] = String(number).split(".")
        length = before_decimal.length

        if not number?
            value = 0
            text = ''

        else if 4 <= length <= 5
            text = 'K'
            value = (number/1000).toFixed(1)

        else if 6 <= length < 7
            text = 'K'
            value = (number/1000).toFixed()

        else if length >= 7
            text = 'M'
            value = (number/1000000).toFixed(1)

        else if before_decimal == '0' or length == 0
            text = ''
            if after_decimal and after_decimal.length > 0
                value = parseFloat(number).toFixed(2)
            else
                value = Math.round(number, 0)
        else
            text = ''
            value = Math.round(number, 1)

        return "#{value}#{text}"
)

ES2015 1

angular.module('common.filters')
.filter('humanizeNumber', function() {
    return function(number) {
        let [before_decimal, after_decimal] = String(number).split(".");
        let length = before_decimal.length;
		let value = 0.0;
        let text = '';
        if (!(typeof number !== "undefined" && number !== null)) {
            value = 0.0;
			text = '';
        } else if (4 <= length && length <= 5) {
            text = 'K';
            value = (number/1000).toFixed(1);

        } else if (6 <= length && length < 7) {
            text = 'K';
            value = (number/1000).toFixed();

        } else if (length >= 7) {
            text = 'M';
            value = (number/1000000).toFixed(1);

        } else if (before_decimal === '0' || length === 0) {
            text = '';
            if (after_decimal && after_decimal.length > 0) {
                value = parseFloat(number).toFixed(2);
            } else {
                value = Math.round(number, 0);
            }
        } else {
            text = '';
            value = Math.round(number, 1);
        }

        return `${value}${text}`;
    };
});

CoffeeScript 2

angular.module('common.filters')

.filter('linkify', ($sanitize) ->
    (text, serviceName) ->
        regexs =
            link:
              regex: /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig,
              template: '<a href=\"$1\" target="_blank">$1</a>'
            ...

        services =
            twitter:
                hash:
                  regex: /(^|\s)#(\w+)/g,
                  template: "$1<a href='http://twitter.com/#!/search?q=%23$2' target='_blank'>#$2</a>"
                user:
                  regex: /((?:^|[^a-zA-Z0-9_!#$%&*@@]|RT:?))([@@])([a-zA-Z0-9_]{1,20})(\/[a-zA-Z][a-zA-Z0-9_-]{0,24})?/g,
                  template: "$1<a href='http://twitter.com/#!/$3$4' target='_blank'>@$3$4</a>"
            ...

        if text
            for name, type of regexs
                text = text.replace(type.regex, type.template)

            service = services[serviceName]
            if service
                for name, type of service
                    text = text.replace(type.regex, type.template)
        return text
)

ES2015 2

angular.module('common.filters')

.filter('linkify', function($sanitize) {
    return function(text, serviceName) {
        let regexs = {
            link: {
                regex: /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig,
                template: '<a href=\"$1\" target="_blank">$1</a>'
            },
            ...
        };

        let services = {
            twitter: {
                hash: {
                    regex: /(^|\s)#(\w+)/g,
                    template: "$1<a href='http://twitter.com/#!/search?q=%23$2' target='_blank'>#$2</a>"
                },
                user: {
                    regex: /((?:^|[^a-zA-Z0-9_!#$%&*@@]|RT:?))([@@])([a-zA-Z0-9_]{1,20})(\/[a-zA-Z][a-zA-Z0-9_-]{0,24})?/g,
                    template: "$1<a href='http://twitter.com/#!/$3$4' target='_blank'>@$3$4</a>"
                }
            },
            ...
        };

        if (text) {
        	let type = '';
            for (let name in regexs) {
                type = regexs[name];
                text = text.replace(type.regex, type.template);
            }

            let service = services[serviceName];
            if (service) {
                for (let name in service) {
                    type = service[name];
                    text = text.replace(type.regex, type.template);
                }
            }
        }
        return text;
    };
});

CoffeeScript 3

angular.module('common.services')

.factory("Paginator", ->
    class Paginator
        constructor: (itemsPerPage=1, totalPages=3) ->
            @page = 0
            @itemsPerPage = itemsPerPage
            @totalPages = totalPages
        previousPage: ->
            if @page > 0
                @page -= 1
        nextPage: ->
            if @page < (@totalPages - 1)
                @page += 1
        atStart: ->
            @page == 0

        atEnd: ->
            @page + 1 >= @totalPages

    return Paginator
)

ES2015 3

angular.module('common.services')
.factory("Paginator", function() {
    class Paginator {
        constructor(itemsPerPage=1, totalPages=3) {
            this.page = 0;
            this.itemsPerPage = itemsPerPage;
            this.totalPages = totalPages;
        }
        previousPage() {
            if (this.page > 0) {
                return this.page -= 1;
            }
        }
        nextPage() {
            if (this.page < (this.totalPages - 1)) {
                return this.page += 1;
            }
        }
        atStart(){
        	return this.page === 0;
        }
        atEnd(){
        	return this.page + 1 >= this.totalPages;
        }
    }

    return Paginator;
});

Tooling

Questions?

Thoughts?

Thanks!

Find me! @joshfinnie (almost) everywhere on the internetz...