5

I'm looking for a library that allows me to easily chain together methods but defer their execution until arguments are provided further along in the chain:

chain
    .scanDirectory  ( '/path/to/scan' )
        .recursively()
        .for        ( /\.js$/i        )
    .cache()
    .provideTo      ( '0.locals'      )
        .as         ( 'scripts'       )
    .defer();

The important thing is that the code behind the scanDirectory function isn't actually called until it's defined that it should be recursive and looking for .js files.

I'm not quite sure how to logically set this up so that I can do something like:

chain
    .scanDirectory( '/path/to/scan' )
    .scanDirectory( '/another/path' )
        .for      ( /\.js$/i        ) // provided to both paths above?
    .doSomethingElse()

which is why I'm looking for a library that may have more mature ideas that accomplish this :)

  • Try that one. It's a old one but it's good : ) https://github.com/chriso/chain.js/ – thinklinux Jun 05 '13 at 20:29
  • 1
    @thinklinux: Good one indeed, but I'm not sure it's exactly the paradigm he had in mind. Interesting lib though. (you should have kept that as an answer, I think) – haylem Jun 05 '13 at 20:34
  • @haylem: It was basically just a link so I converted it to a comment. See http://meta.stackexchange.com/questions/8231/are-answers-that-just-contain-links-elsewhere-really-good-answers/8259#8259 – ThiefMaster Jun 05 '13 at 20:36
  • 1
    @ThiefMaster: right, good point, but maybe notifying thinklinux first with a comment would have been better, as he could have extended his answer on the fly. I was right in the middle of typing my comment when you switched it. Many people sketch out their answers as they go and start with something simple. I know I do, and I'd be pretty pissed if in the middle of typing my follow-up I'd realize my answer has been deleted and that I'd now need to create a new one just because I didn't get a 10 min editing "grace period". – haylem Jun 05 '13 at 20:39
  • @thinklinux -- Chain looks cool, but it's very linear. Looking to break that up a bit :) –  Jun 05 '13 at 20:40
  • @cwolves I'm following this question with big interest : ) – thinklinux Jun 05 '13 at 20:43
  • @haylem very good points indeed! – thinklinux Jun 05 '13 at 20:44
  • A little late, but I just found the possible duplicate [Designing a fluent Javascript interface to abstract away the asynchronous nature of AJAX](http://stackoverflow.com/questions/2796375/designing-a-fluent-javascript-interface-to-abstract-away-the-asynchronous-nature) :-) – Bergi Jun 27 '13 at 18:23

5 Answers5

2

This post talks about types of execution in JS, there are links to relevant libraries in the end of it

Execution in JavaScript

You have two types of execution in JS:

  • Synchronous - stuff that happens right when it's called
  • Asynchronous - stuff that happens when after the current code is done running, also what you refer to as deferred.

Synchronous

Synchronously, you can push actions and parameters to a queue structure, and run them with a .run command.

You can do something like:

var chain = function(){
   var queue = []; // hold all the functions

   function a(param){
       //do stuff, knowing a is set, may also access other params functions set
   }
   return {
       a:function(someParam){
          queue.push({action:a,param:someParam});
          return this;
       },
       ... // more methods
       run:function(){
           queue.forEach(function(elem){ // on each item
               elem.action.apply(null,param);//call the function on that item
           });
       }
   };
}

This will execute all the functions in the queue when you call run, syntax would be something like

chain().a(15).a(17).run();

Asynchronous

You can simply set a timeout, you don't need to use something like .run for this.

var chainAsync = function(){ // no need for queue

   function a(param){
       //do stuff, knowing a is set, may also access other params functions set
   }

   return {
       a:function(someParam){
          setTimeout(a,0,someParam);
          return this;
       },
       ... // more methods
   };
}

Usage would be something like

chain().a(16).a(17);

Some issues:

  • If you want to share parameters between functions, you can store them somewhere in the object itself (have a var state in addition to the queue).

  • It's either sync, or async. You can't detect one or the other by context. Workarounds are being built for ES6.

More resources

Community
  • 1
  • 1
Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • "It's either sync, or async. You can't detect one or the other by context. Workarounds are being built for ES6." -- Q gets around this rather nicely. If a function returns an un-resolved promise, everything else gets deferred transparently. If it returns a resolved promise, or anything that isn't a promise, it's immediate. But it doesn't solve my syntax issue :) Thanks for the resources, looking through them now to find anything particularly valuable :) –  Jun 05 '13 at 21:37
  • Q is a really nice library and it does include resolutions to the issue (though they don't work yet in most browsers, if you check `Q.async` in the source code you can use [yield](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators) which makes for really nice syntax. See [this blog post](http://wingolog.org/archives/2013/05/08/generators-in-v8) that discusses the subject. – Benjamin Gruenbaum Jun 05 '13 at 21:41
  • @cwolves Q works fine on browsers don't worry :) Generators, (that is using `yield`) only work in Firefox and Chrome if you turn on the correct flags. Generators are an awesome thing but are not in the scope of this question really. The whole concept of this syntax for co-routines is very nice, it does _wonders_ like async/await in C# and will be extremely interesting in years to come. I agree that Q's workaround is very nice even without using `yield`. I also think it's by far the most mature library for promises in JavaScript at this time, and the only one I'd use in production code. – Benjamin Gruenbaum Jun 05 '13 at 21:44
  • I've used `async` as well, but it's a very different flow. And the generator spec is nice, I haven't fully examined all of the ES6 yet. I think I saw them briefly but hadn't fully looked into it. –  Jun 05 '13 at 21:55
  • @cwolves Nice, I meant `async/await` as the C# construct though, not as the JS library `async` (which is interesting in its own). – Benjamin Gruenbaum Jun 05 '13 at 23:45
  • yeah, I know, I wasn't referring to your response about C#, I was making a separate comment in response to you only using Q in production :) –  Jun 05 '13 at 23:46
  • I'd use async in production (and have), it serves a different purpose though. This is getting chatty, if you'd like to discuss this further, you're welcome at the StackOverflow JS chat room :-) – Benjamin Gruenbaum Jun 06 '13 at 01:37
0

Note sure you'll find an all-around working solution for this.

Looks like you're looking for a generic solution to something that would require to have been already baked into the library. I mean, I'm sure there are libraries that have this functionality, but they wouldn't hook auto-magically on other libraries (expect if they have specifically implemented overrides for the right version of the libraries you want to target, maybe).

However, in some scenarios, you may want to look at the Stream.js library, which probably covers enough data-related cases to make it interesting for you:

haylem
  • 22,460
  • 3
  • 67
  • 96
  • yeah, I'm looking for something along the lines of a promise library with more functionality. Stream looks cool, but seems to be a very different idea that what I'm trying to accomplish :) –  Jun 05 '13 at 20:39
0

I don't know whether there's a library to build such methods, but you can easily build that feature yourself. Basically, it will be a settings object with setter methods and one execute function (in your case, defer).

function Scanner() {
    this.dirs = [];
    this.recurse = false;
    this.search = "";
    this.cache = false;
    this.to = "";
    this.name = "";
}
Scanner.prototype = {
    scanDirectory: function(dir) {
        this.dirs.push(dir);
        return this,
    },
    recursively: function() {
        this.recurse = true;
        return this;
    },
    for: function(name) {
        this.search = name;
        return thsi;
    },
    cache: function() {
        this.cache = true;
        return this;
    },
    provideTo: function(service) {
        this.to = service;
        return this;
    },
    as: function(name) {
        this.name = name;
        return this;
    },
    defer: function() {
        // now, do something with all the given settings here
    },
    doSomethingElse: function() {
        // now, do something else with all the given settings here
    }
};

That's the standard way to build a fluent interface. Of course, you could also create a helper function to which you pass a methodname-to-setting map which writes the methods for you if it gets too lengthy :-)

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Yeah, I know I can build one, but I think I need built-in promises, etc. But your model doesn't quite work, e.g. in the case where I want to `methodA().as('foo').methodB().as('bar')`. `methodB` needs to get the result of `methodA`, with different `as` params –  Jun 05 '13 at 21:34
  • Uh, and without final calls… Actually I've only seen this pattern for initialisation of instances, not for providing arguments to methods. Some directions I'd think of: 1) make `as` the invocation and let `methodA/B` set a flag only about what should be triggered 2) make them instances of different classes (or at least a new instance) so that `as` will be two different functions 3) reverse the invocation to `as('foo').methodA().as('bar').methodB()`, then my model would work. The promise integration shouldn't be a huge problem for you, either put it on a data property or use it as a mixin. – Bergi Jun 05 '13 at 21:59
  • I want the methods to go first so I can do this: `var dir = chain.scan( '/path' )` ... `dir.find( '.js' ).recursively().run()` ... `dir.find( '.css' ).run()`. At the moment I'm thinking that I'll have methods and providers. Whenever a method in the chain is hit, the previous method will run with all the providers. `run` would be a method that does nothing :) And definitely looking at making methods provide new class instances, with a copy of the chain up to that point –  Jun 05 '13 at 22:02
  • Yeah, if you have that `run()` suffix everywhere (or, implicitly, in a new method call) then it's no problem to have the methods coming first. Only if you wanted to avoid that it gets really complicated :-) – Bergi Jun 05 '13 at 22:07
  • well I want to avoid it in the middle of a chain (e.g. the example in the original question). But I also need to tell it "hey, do something now" at the end –  Jun 05 '13 at 22:08
  • though there's an interesting idea... have each method return a function that runs the chain :) so `dir.find( '.css' )()`, while still having it chainable. That way I can pass the chain off to an event listener: `obj.on( 'foo', dir )` –  Jun 05 '13 at 22:10
  • Returning functions is quite handy sometimes, but doesn't work well with prototypical inheritance unfortunately - you'd have to copy all your methods on each new instance. – Bergi Jun 05 '13 at 22:11
  • sure it does, just not quite as transparently. `fn = new chain(); fn.chain = this.chain.clone(); return fn` –  Jun 05 '13 at 22:12
0

You need a queue to maintain the async and sync-ness of your method chain.

Here is an implementation using jQuery.queue I did for a project:

function createChainable(options) {
    var queue = [];

    var chainable = {

        method1 : function () {
            queue.push(function(done){
                // code here
                done();
            });
            return chainable;
        },

        exec1 : function () {
            queue.push(function(done){
                // code here
                done();
            });
            $.queue(ELEMENT, QUEUE_NAME, queue).dequeue(QUEUE_NAME);
            return chainable;
        }
    };

    return chainable;
}
voidvector
  • 1,976
  • 2
  • 18
  • 19
-3

As @Jordan Doyle said in his comment:

Just return this

So every method in your objects should return the object in the return statement so that you can chain on another method.

For example:

var obj = new (function(){

   this.methOne = function(){
      //...
      return this;
   }

   this.methTwo = function(){
      //...
      return this;
   }

   this.methThree = function(){
      //...
      return this;
   }

})();

//So you can do:
obj.methOne().methTwo().methThree();
Community
  • 1
  • 1
Naftali
  • 144,921
  • 39
  • 244
  • 303