|
AdvancedTechniques
Advanced TechniquesThis page is a repository for Tips & Tricks to help you use Dominoes to its fullest. Asynchronous actionsLet's take the following scenario: you need jQuery, to make an ajax call, to get data that you'll display in an HTML table. The dependency expression would go as follows: jQuery > requestData >| fillHTMLTable Problem is requestData is asynchronous, so how can you ensure its completion before trying to modify the HTML table? Dominoes provides a callback mechanism that's done just for that. All functions that are called by Dominoes get passed a callback parameter. If the function returns false, then Dominoes will not continue until the callback is called. In extendo, you get: dominoes( {
chain: "jQuery > requestData >| fillHTMLTable",
requestData: function( callback ) {
jQuery.getJSON( myURL , function( data ) {
// Some data manipulation
// ...
callback(); // Notify Dominoes the action is complete
});
return false; // Tell Dominoes to halt
},
fillHTMLTable: function() {
// DOM manipulation here
}
} );Passing data from one action to anotherDominoes call functions in context, so you can use this safely, for instance: dominoes( {
chain: "first > second",
first: function() {
// this.chain == "first > second" (we are in context)
this.data = "My Data";
},
second: function() {
// this.data == "My Data"
}
} );Cascading script dependenciesDominoes assumes script loading and execution to be asynchronous, which means the execution order of an expression like "file1.js file2.js file3.js" is non-predictable. For the very specific case of same domain files, it is possible to use XMLHTTPRequests (in place of the script tag injections Dominoes uses internally). That way, you could retrieve the source code of each file and then, later on, eval them in the correct order. There is no easy way around this, at least not without some tweaking of the javascript files code itself. Here is the idea: // In file1.js
dominoes.rule( "module1.start" , function() {
// original source code here
} );
// In file2.js
dominoes.rule( "module2.start" , function() {
// original source code here
} );
// In file3.js
dominoes.rule( "module3.start" , function() {
// original source code here
} );
// In your main file
dominoes.rule( "module1.load" , "file1.js" );
dominoes.rule( "module2.load" , "file2.js" );
dominoes.rule( "module3.load" , "file3.js" );
dominoes.rule( "module1" , "module1.load > module1.start" );
dominoes.rule( "module2" , "module1 module2.load > module2.start" );
dominoes.rule( "module3" , "module2 module3.load > module3.start" );
// Then
dominoes( "module3" , function() {
// Everything has been properly loaded here
} );The rule "module3" now translates to: ( ( ( module1.load > module1.start ) module2.load > module2.start ) module3.load > module3.start All three modules will start loading together, but the moduleX.start rules will be called sequentially. FunctorsYou can customize Dominoes by defining functors, an easy way to transform Dominoes actions. For instance, let's say you don't want to create an options object everytime you want to bypass Dominoes caching mechanism. You could do something like this: dominoes.functor( "nocache(O)" , function( options ) {
options.cache = false;
return options;
} );
// And later
dominoes( "$nocache(URL)" , callback );In that respect, $css() is nothing but a predefined functor. A functor definition goes like this: dominoes.functor("<NAME>(<TYPES>)" , function( parameterOfOneOfGivenTypes ) {
// Do something with parameterOfOneOfGivenTypes
return aDominoesAction; // a string, an options object, a chain or a function: anything Dominoes can execute
} );There are three main types you can use for the definition plus the cumulator type:
You can have functors that accept any of the main types OR the cumulator type:
If you try and call you functor and Dominoes cannot convert the parameter to the type that is part of the definition, it will fail: dominoes.functor( "onlyStrings(S)" , func );
dominoes( "$onlyStrings(Hello)" ); // Works!
dominoes( {
chain: "$onlyStrings(action)", // Fails!
action: function() {
// some code
}
} );Dominoes functors support overloading: dominoes.functor( "multiFunctor(S)" , forStrings );
dominoes.functor( "multiFunctor(F)" , forFunctions );
dominoes( "$multiFunctor(Hello)" ); // forStrings is called
dominoes( {
chain: "$multiFunctor(action)", // forFunctions is called
action: function() {
// some code
}
} );With functors, you can implement any loader you can think of. CumulatorsIt is a known fact that limiting the number of requests made by the browser to a given server accelerates page load considerably. This is usually achieved by pre-concatenating related javascript files server-side. But the technique lacks flexibility in that it requires web developpers to:
Dominoes cumulator functors can help you group file requests pretty easily: dominoes.functor( "module(+)" , function( arrayOfStrings ) {
return "http://mySite/myConcatenatingScript?modules=" + escape( arrayOfStrings.join(" ") );
} );
// Then, later on...
dominoes( "$module(css) $module(widgets) >|" , action1 );
dominoes( "$module(ajax) $module(manipulation)" , action2 );
dominoes( "$module(css) $module(ajax)" , action3 );
// Will result in a single request to
// http://mySite/myConcatenatingScript?modules=css+widgets+ajax+manipulation
// Once the request returns, action1, action2 and action3 will be executedCumulators act like urls and rules in that Dominoes will ensure you won't load the same module twice for the same cumulator: dominoes.functor( "module(+)" , function( arrayOfStrings ) {
return "http://mySite/myConcatenatingScript?modules=" + escape( arrayOfStrings.join(" ") );
} );
// Then, later on...
dominoes( "$module(css) $module(widgets) >|" , action1 );
dominoes( "$module(ajax) $module(manipulation)" , action2 );
dominoes( "$module(css) $module(ajax)" , function() {
dominoes( "$module(widgets)" , action3 );
} );
// Will result in a single request to
// http://mySite/myConcatenatingScript?modules=css+widgets+ajax+manipulation
// Once the request returns, action1, action2 and dominoes( "$module(widgets)" , action3 ) will be executed
// Since the rule module:widgets has already been executed, action3 will called right awayWith cumulators, all your dependency and loading logic can now sit within javascript. The server-side concatenating script is now limited to a mindless machine that concatenates files and eventually handles some caching & redirecting logic together with automated minification. It can also embed javascript code into a Dominoes rule definition (as seen in the Cascading script dependencies section). We plan to release such a script, written in PHP in the near future as an example. | ||||||||||||||||||
Hey Julian, thought I'd share a nice way that I've been working with dominoes.
I keep commonly used libraries, extensions and scripts defined as rules in a dominoes.properties.js file.
In a minified version of this file I also define a functor and apply this to each rule. This allows me to globally switch from development mode (where each file is loaded with a separate HTTP request) to a production mode (where all scripts are bundled and minified) by using a simple .htaccess rule to serve a .min.js version of a .js request (if one exists).
I've also developed a PHP concatenating, minifying (using Ryan Grove's PHP port of Douglas Crockford's JSMin) and caching script. Let me know if you'd find it useful.
@nevstokes: yes, I'm definitively interested, we have to set up some kind of online meeting one of these days to discuss all this. Just drop me a mail :)
I read through docs a couple of times, and am trying to understand how to provide a single callback upon completion of a recursive load tree. Imagine a tree of dependencies. Rule "A B" results in load of file A and B, A depends on C, B depends on D, C and D depend on something else... Dependencies are not localized in one place, but instead each file specifies what it depends on via another dominoes() call. I would like to react when entire tree completes loading. What would be the right way to do this with dominoes?
@rudenko.roman
I would do it like this:
dominoes.rule( "A" , "A.js" ); dominoes.rule( "B" , "B.js" );
dominoes( "A B" , yourCallback );
in A.js:
in B.js:
same in C.js and D.js.
Basically, you can add new actions to rules and if they're defined while the rule itself is being executed, then the new actions will be performed before continuing on with the current chain. What will happen is that once A.js is loaded, dominoes will add the C.js dependency to the rule, load the file then call the function with the actual code. Same with B.js. Only when the whole rules have been executed will yourCallback be executed.
So, all you have to do is to always use a rule for a js file that contains further dependencies definitions and add to the rule in the js file itself.
Hope it helps... totally untested code but if it does not work that way then it's a bug and I'd be happy to fix it ;)
Thank you, Julian. Your idea does work when implemented as described. However, I think I discovered a couple limitations of this approach.
First, doing this seems to require creating a custom rule for every single resource loaded. Using filenames themselves as dependencies (as in dominoes.rule( "B.js" , "D.js" , function() {...} ) does not appear to apply the dependency. Is this distinction intentional? To me, it seems that delaying success callback for both rules and naked filenames is a more natural thing to do.
Second, there is an interesting effect happening when trying to use functors to generate rules. dominoes.rule( "A" , "$myfunctor(A)" ); appears to do nothing, while passing anything but A as functor parameter works as expected. This looks like a bug to me. Testcase: define a functor, any functor. dominoes.rule("dog", "$myfunctor(dog)"); dominoes("dog"); //results in no HTTP request. dominoes.rule("cat", "$myfunctor(kitty)"); dominoes("cat"); //normal HTTP request issued.
@rudenko.roman:
Well, first: URLs are not rules. For instance, you don't have to "define" a URL, you can use it directly.
Second, it seems much cleaner to me to use rules rather than add dependencies on the fly on URLs. Why? Because it is easy to have conventions regarding names and sticking to them. The script can be at http://domain1/A.js or at http://domain2/Whatever.js, if you use a named rule, it will work as expected no matter the URL. Furthermore, the technique will still work in the context of concatanated scripts together with cumulators. In short, it's the most versatile and flexible approach.
As to your second point, I'm not sure you understand what's going on with the recursive definition you have there:
dominoes.rule( "A" , "$myFunctor(A)" );
dominoes( "A" );
Let's see what happens here:
- dominoes gets "A", finds its a rule, starts executing it
- dominoes gets "$myFunctor(A)", finds A is a rule so feeds myFunctor with a function that, once called will execute the rule
- here I'm guessing myFunctor only returns it's parameter ie dominoes.functor( "myFunctor" , function( action ) { return action ; } )
- So basically, myFunctor returns the function and dominoes calls it thus executing the rule... but the rule is being executed already so the chain is stalled until the first execution is finished. Since you are within the execution process for the rule, this chain will never ever finish (you can try and add a function after "$myFunctor(A)" in the rule definition, it will never be called... you pretty much dead-locked the rule).
It seems to me you're trying to use functors for something they are not meant for. Functors are meant to transform actions and have nothing to do with rule definitions:
"$myFunctor(anyRule)" will feed myFunctor with a function that will execute the rule when called, not the rule definition itself. Keep in mind rules can be as complicated as any call to dominoes() and that functors can accept strings, options objects and functions... and that's it.
I admit I'm a bit puzzled as to what you're trying to achieve here.
Thanks for explanation. I am still learning how to hammer nails with your beautiful microscope, so things I try doing are going to be strange and unexpected. Please bear with me.
I understand your point regarding keeping URLs and rules separate, and not allowing stuff to depend on them.
When it comes to use of functors, I was trying to apply them to transform rules into URLs. I wrote one that would turn Java-like rule names (foo.bar.baz) into URLs (/js/foo/bar/baz.js), and tried to use it as dominoes.rule(ruleName, "$classpath("+ ruleName + ")" ); . If that is not the intended use for a functor, I can give dominoes.rule a proper URL right away.
Overall, what I am trying to do is 1) place JS files in a directory hierarchy, and refer to each one via a Java-like dot-separated path. 2) Allow arbitrary dependencies between files 3) Run init actions when all dependencies load 4) Minimize boilerplate that is required for every file
Thanks to your explanation, I understand how I should be using rules. Initially, I was considering making a file depend on another file to avoid creating additional entities, but since that is counter to design of Dominoes, I can instead use rules as suggested (maybe with a bit of syntax sugar to avoid repeating names and URLs).
@rudenko.roman
Oh, I see now and don't worry about not using dominoes properly, I mean it doesn't even have proper documentation yet ;)
The key point is to understand that dominoes is a pretty low-level helper lib. So to go back to your problem, yes, you'll need some syntactic sugar to define your own module system using dominoes.
I'd do something like this for your specific case:
dominoes.functor( "module(S)" , function( name ) {
} );
The trick here is that every time the functor is called, it'll create a corresponding rule if it doesn't exist. Then it simply pass the rule name back to dominoes so that it executes it.
You'd use it like this:
dominoes( "$module(foo.bar.foo)" , function() {} );
and in /js/foo/bar/foo.js, since you know the rule has been defined prior to loading the file, you can use the module name directly:
dominoes.rule( "foo.bar.foo" , "$module(bar.foo.bar)" , function() {
} );
Key is to use the functor whenever you're not sure it has been called already.
To be honest, I find myself that the $functor() syntax is a bit heavy-handed and I'm contemplating using a more straight-forward "functor:string" system but I still hesitate due to http://domain URLs and the fallback code I'd need to add in.
Still, in your example, it would make all of this much more readable:
dominoes( "module:foo.bar.foo" , function() {} );
dominoes.rule( "foo.bar.foo" , "module:bar.foo.bar" , function() {
} );
Let me know what you think... and again, completely untested code but should work.