Foundation-sites: The Great ES2015 Shift

Created on 4 Jan 2016  路  24Comments  路  Source: foundation/foundation-sites

We missed our chance to ship Foundation 6 in ES2015, but we're interested in upgrading the codebase.

To compile ES6 to 5 we'd use Babel, but _only_ features that don't require polyfills. A basic list includes:

  • Fat arrows
  • Classes #8086
  • Multiline strings
  • Template strings #8018
  • Destructuring
  • Default function parameters
  • Trailing function parameters
  • Spread parameters
  • let and const

This will also get us easier UMD support, by using Babel's module transpiler.

We need the shift to be entirely backwards compatible, which should be totally doable.

If you're interested in contributing, there's a 6.2 branch you can work on.

Edit: I also remembered there's some weirdness with how Babel's module transpiler works regarding browser globals, so we need to work around that. It doesn't mesh with the way we assign browser globals, since all of our plugins add to window.Foundation, not window.

help wanted javascript new feature

All 24 comments

We have Babel in our build process now! Next step is to do some experimenting with hooking up the import and export syntax, since the UMD support is arguably the most important thing to square away initially.

One tricky thing is with the Babel transforms available. We need UMD, but UMD transformers tend to assign each module to a variable on the window. We have our own process for loading plugins into window.Foundation, and we also don't want each little utility library to get its own variable in the global namespace, i.e. window.foundationUtilMotion.

Instead, in each plugin, we can do a check for an absence of an AMD/CommonJS environment, and then assign things the way we want to. For example:

if (typeof module !== 'object' && typeof define !== 'function') {
  // For plugins
  Foundation.plugin(PluginClass);

  // For utilities
  Foundation.util.motion = Motion;
}

_However_, this doesn't work unless we transform to _UMD without browser globals_, which doesn't appear to be a thing that Babel's UMD transformer does, unless it's configurable in some way.

Where can i start? What's some low hanging fruit? Can i start creating an issue for each component?

@chrisjlee Some high-level stuff:

  • Convert plugins to classes
  • Use shorthand object literals
  • Use arrow functions when possible (remember in some cases we use jQuery's this inside of callbacks)
  • Use template strings instead of string concatenation

Remember that it's just the basic ES2015 stuff, nothing wild like generators or async functions. Our .babelrc has a list of every feature we're using.

Don't worry about module stuff just yet, but we will be looking to use import and export so we can compile to CommonJS and AMD.

Edit: Also remember that it has to be entirely backwards-compatible, which should be possible. We aren't changing the APIs themselves, just how they're written. Go one plugin at a time and ensure that the functionality is the same.

@chrisjlee just so you are aware and so you don't waste time redoing what has already been done, template strings are taken care of and have been merged into the 6.2 branch.

@gakimball I would be willing to convert the plugins to classes if somebody would convert one of them (post in a gist or something like that) so I could use it as a reference.

@colin-marshall I can do that this week.

:+1:

I did Interchange: b5f93a8d403923f346da590921a09f8ad9f953f8 As it turns out, it only takes like 10 minutes to do the conversion.

Here's the general format:

export default class Plugin {
  constructor() { }
  _init() { }
  // ...and more
}

Plugin.defaults = {}

// Window exports
if (window.Foundation) {
  window.Foundation.plugin(Plugin, 'Plugin');
}

Let me know which ones you're working on so we don't overlap. I think I'll start with the utility libraries.

Edit: Also note that the IIFE around the whole file has been removed. There's still some kinks to work out with how module loaders will work, but everything will work the same for now.

Nice! I was mostly unsure about the IIFE part of it, this helps a lot.

I will start with the smaller ones:

  • Accordion
  • Accordion Menu
  • Magellan
  • Responsive Menu
  • Responsive Toggle
  • Tabs
  • Toggler

Just a short question and maybe a hint what you should insert into your release notes and documentation to make people feel save about the update.
Using single JS components like foundation.accordion.js or the complete foundation.js there is nothing I need to change?
On my current project I have bower_components/foundation-sites/dist/foundation.js included in my build process to be concatenated and minified together with other JS files. So as I understand it, this version should be ES5 compatible in 6.2 as well. I just to have this clear to not have any bad surprises when 6.2 is released.

@DaSchTour The files in dist/ will continue to be compiled assets, so the JavaScript files will be ES5 JS compiled from Babel.

Now, the files in the js/ folder will be written in ES6, so users of the ZURB Template will need to add Babel to their Gulpfile. We'll be providing instructions on how to do that.

@gakimball I was playing around with arrow functions and have a question. I'm using the _events() function from foundation.abide.js as an example.

Here is the original function:

_events() {
    var _this = this;

    this.$element.off('.abide')
        .on('reset.zf.abide', function(e){
          _this.resetForm();
        })
        .on('submit.zf.abide', function(e){
          return _this.validateForm();
        });

    if(this.options.validateOn === 'fieldChange'){
        this.$inputs.off('change.zf.abide')
            .on('change.zf.abide', function(e){
              _this.validateInput($(this));
            });
    }

    if(this.options.liveValidate){
      this.$inputs.off('input.zf.abide')
          .on('input.zf.abide', function(e){
            _this.validateInput($(this));
          });
    }
  }

Here is events with arrow functions:

function _events() {

  this.$element.off('.abide')
      .on('reset.zf.abide', (e) => { this.resetForm() })
      .on('submit.zf.abide', (e) => this.validateForm());

  if(this.options.validateOn === 'fieldChange'){
      this.$inputs.off('change.zf.abide')
          .on('change.zf.abide', (e) => { this.validateInput($(this)) });
  }

  if(this.options.liveValidate){
    this.$inputs.off('input.zf.abide')
        .on('input.zf.abide', (e) => { this.validateInput($(this)) });
  }
}

And here is what it transpiles to:

'use strict';

function _events() {
    var _this = this;

    this.$element.off('.abide').on('reset.zf.abide', function (e) {
        _this.resetForm();
    }).on('submit.zf.abide', function (e) {
        return _this.validateForm();
    });

    if (this.options.validateOn === 'fieldChange') {
        this.$inputs.off('change.zf.abide').on('change.zf.abide', function (e) {
            _this.validateInput($(_this));
        });
    }

    if (this.options.liveValidate) {
        this.$inputs.off('input.zf.abide').on('input.zf.abide', function (e) {
            _this.validateInput($(_this));
        });
    }
}

Note that in the transpiled code it comes out with _this.validateInput($(_this)) while the original code was _this.validateInput($(this)). Is $(_this) pointing at the wrong "this" in the transpiled code?

Yeah, this is a weird one. For jQuery events, changing this makes writing code more convenient, sometimes. So you can't mix this referring to the plugin class, and this referring to the element that fired the event.

So for jQuery events, there's two things we can do:

  • Don't use arrow functions and continue using _this.
  • Use $(event.target) instead of $(this) inside of event handlers, and then we can use arrow functions everywhere

However, from the jQuery .on() documentation:

When jQuery calls a handler, the this keyword is a reference to the element where the event is being delivered; for directly bound events this is the element where the event was attached and for delegated events this is an element matching selector. (Note that this may not be equal to event.target if the event has bubbled from a descendant element.)

That "may not be equal" is annoying鈥擨'm trying to understand when it would be different.

What about using $(event.currentTarget) instead? Documentation, however, is also vague:

This property will typically be equal to the this of the function.

If you are using jQuery.proxy or another form of scope manipulation, this will be equal to whatever context you have provided, not event.currentTarget

@gakimball Thanks for the clarification.

@colin-marshall We don't use jQuery.proxy() anywhere. As far as scope manipulation, that would be this, which we also don't do:

$('#thing').click(function() {
  this; // => not event.currentTarget anymore
}.bind(this));

Arrow functions are real nice from a code readability perspective, which I think is a compelling reason to use event.currentTarget.

Another update on ES2015 stuff: after reviewing the codebase, I don't think UMD support is going to land in 6.2. Looking at the web of utility libraries we have, getting everything to work with a UMD shim is going to be something of a challenge.

So for now, the plugins are back to being defined inside of IIFEs, and the export statements have been removed from each plugin. (You can't use them inside of a function.)

A solution like the one I outlined above is possibly in the cards still, but we also have to properly handle dependencies for each module.

I was going to send a PR with this JSPM config for 6.1.2, but then I noticed that the 6.2 beta has some ES2015 support.

I initially wanted to be able to import components individually, but that ended up being too difficult/complicated with my limited understanding of all the different module systems, etc. (see here).

If I install 6.2@beta, JSPM appears to detect the following:

{
  "main": "dist/foundation.js",
  "format": "cjs",
  "meta": {
    "*": {
      "globals": {
        "process": "process"
      }
    },
    "*.json": {
      "format": "json"
    },
    "dist/foundation.js": {
      "format": "amd"
    },
    "dist/foundation.min.js": {
      "format": "amd"
    }
  },
  "map": {
    "./foundation-docs.js": "./foundation-docs/index.js"
  }
}

If I use import "foundation-sites"; which transpiles to require("foundation-sites");, I get:

Uncaught (in promise) TypeError: Multiple defines for anonymous module http://127.0.0.1:8080/jspm_packages/npm/[email protected]/dist/foundation.js

If I set the format to global, remove the AMD format detections, and shim the jQuery dependency similar to what I did for 6.1.2, this will probably work, but obviously it will still be all or nothing (no ability to import individual plug-ins).

So my question is, should I just continue to use Foundation as one large global, or will it soon be possible to import plug-ins selectively?

@glen-84 Individual modules won't be in 6.2. See my comments farther up here and here for explanations of the challenge.

Basically, UMD shims work better with individual libraries, not larger, more intricate ones like ours. That doesn't mean it isn't possible, but we need to do more research on the right way to set it up.

Might look at Angular 2 as an example, which is written in TypeScript, with code using ES2015 modules. However, their plain JavaScript version uses neatly organized window globals, ng, ng.core and so on. That's what we need鈥攚e don't want all 25 or so modules occupying their own space on the window. Everything needs to be inside window.Foundation, but we need more time to sort out our internal web of dependencies so we can set it up properly.

Going to close this as we've successfully made the shift :) Thanks to everyone who helped out!

Our long-term goals for the codebase will be to use more of these features where we can, and also rely on jQuery less where we can to make the framework faster.

And as for UMD support, read my comments farther up in the thread. We'll get there eventually!

@gakimball Thanks for your reply. =)

As I found this by searching for TypeScript in Foundation Context. I think by shifting to TypeScript Foundation for Sites and for Apps could benefit better and the usage with Angular 2 would be easier. Maybe it also would make sense to have something like Foundation Core (maybe even Core Style and Core JS/TS?) which is then extended to Foundation for Sites and Foundation for Apps. Just some crazy ideas :)

For foundation users who aren't compiling their entire project with an es6 compiler, how should we be including foundation js dependencies? I noticed that there are a few const's about the project. This, of course breaks in browsers that don't support es6 operators.

@keepitreal there are pre-transpiled JavaScript components in the dist directory.

Was this page helpful?
0 / 5 - 0 ratings