Skip to content

Instantly share code, notes, and snippets.

@tbranyen
Forked from davearel/jquery_amd.js
Last active August 29, 2015 13:56
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tbranyen/9255362 to your computer and use it in GitHub Desktop.
Save tbranyen/9255362 to your computer and use it in GitHub Desktop.
// Use an AMD package here to gain access to nested internal modules.
require.config({
packages: [{
name: "jquery",
location: "vendor/jquery/src",
main: "index.js"
}]
});
// If we are using AMD, we don't care about core.
define(function(require, exports, module) {
"use strict"
var ajax = require("jquery/ajax");
var data = require("jquery/data");
var req = ajax.get("/modular");
req.then(function() {
// Modules, sucka.
});
exports.ajax = ajax;
exports.data = data;
});
// jquery/html would return a module representing the function.
require(["jquery/html"], function(html) {
html.call(elems, "value");
});
// Want chaining instead, hold onto your seats!!!!
require(["jquery/core", "jquery/html"], function($, html) {
// Attach to core.
$.fn.html = html;
// Now that's what I'm talking about.
$(".some-elems").html("value");
});
@dmethvin
Copy link

How do you know what parts you need?

@tbranyen
Copy link
Author

What do you mean? if you're using this style of imports, you'll only require what you're using. I'm not saying this fits every single use case, but neither does having a monolithic library.

@tbranyen
Copy link
Author

There's room for improvement, I'm just speculating ideas. Would love to chat during a dev meeting if possible.

@dmethvin
Copy link

To elaborate, it's really clear when I need a module in node:

var path = require("path");
// and later in the page
var dir = path.dirname(file);

If I forget to declare path, JavaScript throws an error and/or my linter complains. If I try to require() it and haven't declared it in my dependencies, node complains. Pretty bulletproof.

How bulletproof are these approaches in comparison? Do they make your dependency tracking easier, or do they make it harder? How will they work with third-party code that isn't written in this style, such as most plugins?

@tbranyen
Copy link
Author

Answering plugins first:

Existing plugins would work identical, for example:

// Existing plugin.
$.fn.myCoolPlugin = function() { };

Now in your configuration you'll need to shim (but you already have to do this anyways with AMD):

require.config({
  shim: {
    "mycoolplugin": ["jquery/core"]
  }
});

As for tracking dependencies, absolutely easier. Look at how much better it is already with the work @timmywil has done.

If you want to use html and you forget to require it, it will error identical to node:

//var html = require("jquery/html");

html.call(elems, "hello") // Errors, dangit forgot to require html again!

@dmethvin
Copy link

I wasn't saying I wanted jQuery code to look like node code, which that does, but that I wanted the dependency and error process to be as transparent. So given code like this:

$("#name").css("color", "green").html("<b>Dave</b>");

What wrapper would I need to support that? If I changed it to this:

$("#name").css("color", "green").text("Dave");

Would I need to make a bunch of edits elsewhere?

@mikesherov
Copy link

And to @tbranyen's point, if "jquery/core" (or maybe "jQuery/construct"), upon each require, returned a constructor function without an external API (which isn't to say that instances can't have some shared state) , you'd even get errors with the case where you forget to construct a new jQuery with html attached.

@mikesherov
Copy link

@dmethvin, yes. You'd need to now require "jQuery/text" and attach it to the jQuery object. I mean, this is what dependency management is about. Knowing your dependencies and declaring them.

@mikesherov
Copy link

@dmethvin, and you'd only have to know to require the external API components you need. If you want to call .stop and don't know that it requires animate, you stil only have to require "jQuery/stop" so you can access it externally. The rest of the dependency management chain is handled by jQuery and AMD.

@dmethvin
Copy link

Right, I guess I wanted to make sure I understood that's what people are proposing. So now instead of just s/html/text/ I have to go back and edit a couple of other places. Since this case isn't quite the same as node (or tbranyen's) and I'm using a method off the jQuery prototype, it won't be found until runtime because I'm using a chained method.

For the utility-ish methods like $.ajax() it's a lot easier to see peeling them off into separate modules. For the chained methods it seems like being specific is a lot of hassle and will lead to errors. It's like being bothered because I want some of path but not all of it.

What problem are we trying to solve? I understand that people want to be more specific about the parts of jQuery they actually call, so potentially the subset they actually load can be smaller. But remember we are working in a huge ecosystem where jQuery plugins lack any equivalent to package.json.Can we solve this in core alone, or will the entire ecosystem need to come along before it pays off? And is the size of jQuery so big that this amount of work is worth the effort?

@mikesherov
Copy link

I can't speak for "it's worth the effort" nor "will plugins do the right thing". I guess giving plugins the option is good though. I guess making it possible to allow people to be explicit is good though. I also guess that the only at runtime argument only exists because, as of today, tools can't detect this at static time without some other ceremony (like Closure compiler annotations).

@tbranyen
Copy link
Author

@dmethvin I agree 100%, maybe we could focus on getting utility methods exposed first and baby step into what feels right.

@timmywil
Copy link

I understand the utility of the jQuery "factory" approach, but I just don't like the API proposed. I like the idea of starting with utility methods, but we can go a step further and expose methods that work on a single element. We'd still only be exposing functions and we wouldn't be extending a namespace. For those that want to work with jQuery instances, they can do a custom build, which is the same as saying a custom-built jQuery (admittedly only one) AMD module.

define(['jquery/sizzle', 'jquery/attr'], function( Sizzle, attr ) {
  var elem = Sizzle('#boosh')[0];
  attr( elem, 'contenteditable', 'true' );
});

@timmywil
Copy link

Or even:

define([
  'jquery/sizzle',
  'jquery/attr',
  'lodash.foreach'
], function( Sizzle, attr, each ) {
  each(Sizzle('.allofthem'), function(elem) {
    attr(elem, 'contenteditable', 'true');
  });
});

@davearel
Copy link

jQuery becomes a much different library if we're only passing methods around. I'm not entirely sold on the factory approach either, but it preserves the single-namespace/chainable syntax.

Maybe that paradigm doesn't belong in AMD...? Or rather, doesn't need to.

@davearel
Copy link

This:

$('.class').find('.a');

Is so much cleaner than

var el = Sizzle('.class');
find(el, '.class');

@davearel
Copy link

The tradeoff of a factory method may just be worth it.

@timmywil
Copy link

var el = Sizzle('.class');
find(el, '.class');

We would not be limited to this API.

Maybe that paradigm doesn't belong in AMD

I think I'm gravitating towards this. I'm not a fan of sharing a namespace between modules. The most flexible (and performant) pattern we see over and over champions the principle that a module should just do one thing and do it well. Chaining these modules together can be offloaded either to another module, several modules, or to the user. I really want to avoid mixing AMD with the namespace because only then will we have the most versatile environment to construct various design patterns. Then we can decide on the best one.

@davearel
Copy link

@timmywil I can get behind this. It's a paradigm shift, but its a very decoupled architecture; which I like.

Though I'm not sure why we can't have the same level of granularity in each module, while still allowing a way to extend several modules into one chainable namespace. Only within the module that is requiring it.

Per my original gist:

var $ = jq.require(find, addClass);

@tbranyen
Copy link
Author

@timmywil I still don't understand why we can't move slowly and keep the API intact as-is. All the chainable methods are attached to $.fn anyways so why not keep it that way?

An idea I was thinking is that if jquery/core is loaded, each method once required could test for, and then attach to, that module.

Concept:

define(["require"], function(require) {
  "use strict";

  function html(elems, value) {
    /*.. */
  }

  // If the module is defined, simply attach it.
  if (require.defined("jquery/core")) {
    require("jquery/core").fn.html = html;
  }

  return html;
});

The entire internal API could be designed this way and that's also how it could be trivially assembled without having that massive $.fn.extend call.

@davearel
Copy link

@tbranyen the problem with that is you are extending the core object for all future modules.

What happens when you extend the prototype for html in two of your app modules, but forget to require it in one? If you remove the dependency from that original module, it will be removed from production code, but your code is still trying to access it.

My example creates a clone for use within that one module only. That way, it is truly decoupled among your modules.

@xMartin
Copy link

xMartin commented Mar 2, 2014

What you want is what for example the Dojo Toolkit is doing since years and it's great. The complete code base is structured in tiny AMD modules with explicit dependency management and scoping of these. This approach is amazingly powerful. The toolkit also covers all features and jQuery and way beyond - just require what you need.

jQuery has a completely different approach. Wrapping everything in jQuery objects makes a beautiful DSL for the DOM and is very valuable as such if that's all you need. But you cannot have both. The limitation of jQuery is not that it is not modular. It is that it is by design monolithic as in namespace and wrapping things.

If you need something that is more powerful (and thus less simple) move on. jQuery cannot be the answer to all needs.

@davearel
Copy link

davearel commented Mar 2, 2014

I disagree, only in that I think it's possible for jQuery to have both. However, As I stated before:

Maybe that paradigm doesn't belong in AMD

It may be a little too aggressive for jQuery to migrate away from the current syntax, and I'm not 100% sure if it's in its best interest; chaining is a valuable thing. Regardless, that doesn't mean we can't have chaining and AMD.

My original example was exactly that:

define([

  // jquery core is always required
  'jquery/core',

  // jquery utilities
  'jquery/ajax',
  'jquery/data'

], function(jq, ajax, data) {

  // Using the core module, create a jQuery instance
  // with the required extensions
  var $ = jq.require(ajax, data);

  // The local instance of $ contains the necessary jQuery
  // extensions, but once this module is done executing,
  // "$" no longer exists to other modules.

});

https://gist.github.com/davearel/9254418

Of course, that doesn't describe the actual implementation, but rather the outcome. The point is simply that we extend the core jQuery namespace, with the necessary dependencies, to a LOCAL variable. That way it is only accessible to that particular module.

The require method would return a new instance of jQuery that only contains the core methods and the dependency methods.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment