dynamic vue-router base on webpack applications - vue.js

Good day.
Problem definition:
I would like to deploy my Vue app, that utilizes vue-router, on dynamic path, which should be controlled by the WebServer, utilize vue-router in history mode, and avoid re-packaging the application for each Deployment.
F.e.
Run same app at
http://localhost/subpath/index.html
and
http://localhost/another-subpath/index.html
As vue-router configuration is done at the packaging stage (f.e. by webpack), and thus is not designed to be controlled after the packaging stage, making this simple but so-common setup not viable.
Also, vue-router have quite a complicated lifecycle, which does not allow to easily override that base setting at the application-level.
In addition - webpack ends up hardcoding actual resources into the html body, which ensures their proper consumption by the Client at the load-time, but as chunks are inter-dependent - they are almost impossible to be injected/edited dynamically. Application integrity fails in case of any modification to those on-post-DomReady event.
Research:
My search so far have not yielded any viable options to set such configuration up.

I came up with a couple of viable solutions.
One of the problems is that changing the Router Base is not the only thing one needs, in order to dynamically change the App's root, and make it function properly on dynamic URL.
In case of vue+webpack - actual scripts are added to the index.html on-build-stage, and thus - again - end up being hardcoded.
There are few options of fixing that, which I came up with.
The Ugly solution:
Regex-replace resources URLs at the index.html, on the web-server level, and hardcoded setting for your vue-router-base.
This is very questionable approach, but viable.
Unfortunately I do not have examples of that approach left, so - I can't provide any, but this should be pretty straightforward:
On your servlet-side, Pseudocode:
let fileContent = getFileContent(requested_file_name);
if (servingFile == "index.html") {
fileContent = fileContent.regexReplace("http://url_hardcoded_at_packaging_stage", "http://url_application_is_deployed_at");
fileContent = fileContent.regexReplace("setting_token_in_your_index_html", "http://url_application_is_deployed_at");
}
return fileContent;
In addition to that you will need to add this setting_token_in_your_index_html in your index.html:
<head>
<script type="text/javascript">
window.my_app_settings = {routerBase:"setting_token_in_your_index_html"}
</script>
</head>
And consume it at the vue-router level:
export default new Router({
mode: "history",
base: window.my_app_settings.routerBase,
routes: [...]
...
});
This approach is good in a sense that there is no need to modify anything on your vue-app level, and all the changes can be kept only on your servlet-side, whatever it is.
Also it has 0 performance impact on the vue-app itself.
Still ugly, but a lots better, from code-perspective at least:
Make vue-app base-aware.
This solution is not super-simple, but works reliably on all major web-servers and can be summarized as
Dynamically add vue-resources at the app init, based on cookie provided by the Web-Server.
This approach allows to "elegantly" modify all the resources URLs, with minimum impact on the performance-side, and keep things as dynamic as they can be.
This approach consists of few small changes on the Packaging, and the app-levels, and also relies on cookie to inform the app about custom base.
Provide the custom URL cookie from Server:
At your web-server add the cookie header (all major web-servers support such functionality):
(f.e. in Scalatra)
val contextShiftCookie = "subpath_where_ui_deployed";
val cookie = new Cookie("ui_deployment_root", contextShiftCookie );
cookie.setPath("/");
response.addCookie( cookie );
App modifications (index.html):
At the <head> section:
<script type="text/javascript">
// Build script names, for later injection.
let stringsJs = [
<% for (let js in htmlWebpackPlugin.files.js) { %>
"<%= htmlWebpackPlugin.files.js[js] %>",
<% } %>
];
let stringsCss = [
<% for (let css in htmlWebpackPlugin.files.css) { %>
"<%= htmlWebpackPlugin.files.css[css] %>",
<% } %>
];
// Simple vanilla Cookie getter (replace with something else if needed).
function getCookie(cname) {
var name = cname + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
for(var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
// Simple vanilla onDomReady handler (replace with something else if needed).
(function(exports, d) {
function domReady(fn, context) {
function onReady(event) {
d.removeEventListener("DOMContentLoaded", onReady);
fn.call(context || exports, event);
}
function onReadyIe(event) {
if (d.readyState === "complete") {
d.detachEvent("onreadystatechange", onReadyIe);
fn.call(context || exports, event);
}
}
d.addEventListener && d.addEventListener("DOMContentLoaded", onReady) ||
d.attachEvent && d.attachEvent("onreadystatechange", onReadyIe);
}
exports.domReady = domReady;
})(window, document);
// Calculating vue router base.
let routerBase = "/";
if (getCookie("ui_deployment_root")) {
routerBase = getCookie("ui_deployment_root");
// console.log("Found base cookie", routerBase);
} else {
console.log("No base cookie found");
}
let scriptsBase = routerBase == "/"? "" : routerBase;
// Prefilling basic settings.
window.my_vue_app_config = {
routerBase: routerBase
};
function loadOneScript(){
if (window.my_vue_app_scripts.length > 0) {
let currentScript = window.my_vue_app_scripts.shift();
currentScript.onload = loadOneScript;
document.getElementsByTagName("body").item(0).append(currentScript);
}
}
// Injecting scripts using deployment context.
domReady(function(event) {
let head = document.getElementsByTagName("head").item(0);
stringsCss.forEach(css => {
let script = document.createElement("link");
script.setAttribute("rel","stylesheet");
script.setAttribute("href", scriptsBase + css);
head.append(script);
});
window.my_vue_app_scripts = [];
stringsJs.forEach(js => {
let script = document.createElement("script");
script.setAttribute("type","text/javascript");
script.setAttribute("src", scriptsBase + js);
window.my_vue_app_scripts.push(script);
});
loadOneScript();
});
</script>
Step-by-step explanation:
Consume base setting as-soon-as-it-is-available, BEFORE actual chunks are loaded, to ensure that vue-router can consume it nicely.
Gather all the script names from WebPack (see let stringsJs and let stringsCss)
Load all the css as soon as the DomReady (because css is consumed on-the-fly, and there is nothing specific about loading them. They just should be available). See stringsCss.forEach for that.
Load all the js chunks one-by-one, as this is the only way to ensure that webpack-ed app will be initialized properly (see stringsJs.forEach, and loadOneScript routine).
Scripts loading is done in such manner that each script (chunk) requests another chunk as soon as it is loaded and consumed by the Client (browser). This ensures app integrity, and proper initialization, regardless of how it was packed.
Chunks are processed in proper order, provided by the webpack, again - to ensure integrity.
vue-router changes:
Consume new setting at the router-level:
export default new Router({
mode: "history",
base: window.my_vue_app_config.routerBase,
routes: [...]
...
});
Webpack mechanism changes:
To support custom bundling at the webpack:
<% for (let js in htmlWebpackPlugin.files.js) { %>
"<%= htmlWebpackPlugin.files.js[js] %>",
<% } %>
, you will need to modify the /app/build/webpack.ENV.conf.js file:
...
new HtmlWebpackPlugin({
...
inject: false,
...
}),
Where .ENV. should be your desired bundle target, but I suggest changing this for all your environments, as then you will be sure that your index.html changes work properly on all Environments.
Performance considerations:
This was my biggest concern with this "sequential scripts injection", because it sounds very bad, from performance perspective.
But, to my biggest surprise, actual tests have shown only ~0.100-0.150ms raize in the JavaScript processing time, compared to the fully-static serve of the Application, according to tests on all major Web Clients (browsers).
This basically renders performance concern irrelevant in my case, but we wary that this might differ in your case.
Other impact on the application lifecycle is totally neglectable, at least in my case (less than 0.05ms in overall impact on the App Load).
P.S.
As this approach is something I've developed purely myself - I would appreciate constructive criticism, and improvement proposals :)

Related

Internationalization with Handlebars

I'm trying to internationalize my application that uses Express and Handlebars. Is it possible to get Handlebars partials (fragments) to load and render the localization resource file?
Noting that I've already read this question: express3-handlebars and 18next-node - internationalisation based on page?.
Here is my directory structure:
views/
index.html
login.html
fragments/
frag1.html
frag2.html
frag3.html
locales/
index.json
login.json
fragments/
frag1.json
frag2.json
frag3.json
If necessary, I can separate the JSON files in the locales/ directory to be something like this:
locales/
en-CA/
index.json
...other files
fr-CA/
index.json
...other files
Here is the relevant code in my server.js file:
// ...
hbs = exphbs.create({
extname: '.html',
layoutsDir: [
__dirname + '/views'
],
partialsDir: [
__dirname + '/views/fragments'
],
helpers: {
'json': function(context) {
return JSON.stringify(context);
},
't': function(k) {
// ?
}
}
});
app.engine('.html', hbs.engine);
app.set('view engine', 'html');
The t helper is what I need help with. In my templates/template fragments, I have these:
<h1>{{ t 'pageTitle' }}</h1>
<p>{{ t 'foo' }}</p>
<p>{{ t 'moreThings' }}</p>
And my JSON file could look like this:
{
"pageTitle": "Hello world",
"foo": "Paragraph contents here",
"moreThings": "There are %d things"
}
Also how do I deal with the printf parameters?
Doing internationalization in your application means doing two things:
1) Determine which locale should be used
Depending on how you determine the used locale it can be difficult to do this inside a helper. Helpers do not have access to the request object for instance. To be honest i cannot think of a good way to do this inside a helper.
Personally i use the i18n-abide middle-ware to do internationalization. They have several options to determine the locale for a given request. Once locale is determined it is added as a property to the request object. So you only need to determine the locale once for each request. An other advantage is that you have also access to the locale outside the handlebars helper.
2) Access the resource files
To access the resource files from within a helper means that you should read and parse the resource files outside the helper. Parsing resource files every time you need to translate a string really hurts performance.
Here you also should use middle-ware. You can do something like the pseudo code below.
function setup() {
// Load resource files from disk and parse them.
var resources = { /* parsed resources*/ }
return function(req, res, next) {
var locale = determineLocalFunction(req);
req.getText = function(label) {
return resources[local][label];
}
}
}
Now you can use the req.getText function every where in your code. Personally i never use language labels inside a partial. Instead i pass all the language strings needed in a partial using a data object. The reason behind this is that i think partials should be as re-usable as possible. Using hardcoded language labels inside them makes them less re-useable.
When you do want to use the getText function in your partials you can pass to getText function to your partial.
Something like this:
var objectPassedToPartial = {
getText: req.getText
}
Use it like:
{{getText 'label'}}
Read more about Mozilla's i18n-abide solution, i really love it.

Ensure phonegap and plugins load before Sencha loader

I have written an application in Sencha Touch 2.1, of which I embed a package build into Cordova/PhoneGap 2.5.0 and compile in xCode to run on iOS Simulator / iOS. I have added the PGSQLite plugin to PhoneGap, and built my own PhoneGap/SQLite Proxy for Sencha, which I used on a few of my Stores.*
Problem: When I embed a package build into PhoneGap and run in iOS Simulator, I see that Cordova does not load before Sencha initializes. I see this because my calls in my Sencha app to Cordova.exec that I make in my Proxy initialization result in an error telling me that the Cordova object cannot be found.
I do successfully use Cordova.exec later in my application to run things like the Childbrowser plugin for PhoneGap, and it works. But using Cordova.exec at an early stage in the app's execution, i.e., initialization, is too soon to guarantee that the Cordova object will have been instantiated.
Already tried: I already tried the following approaches:
I tried simply embedding the developer build of my Sencha app into PhoneGap. Although this worked, I don't want to deploy my development build as my released app because it is inefficient and takes up a lot of space. I have learned from this experiment, however, that the way the Sencha Touch microloader works on package and production builds loads PhoneGap after Sencha. This can be clearly seen when inspecting the DOM after Sencha loads in a package build.
I have already configured my app.json file to include PhoneGap and
my plugins before app.js and the Sencha Touch framework. Playing
with the order of my JS file references in my app.json did not
seem to affect the load order.
I also tried creating a script loader, as described here
(StackOverflow). I then ran the script loader for Cordova, and in
the callback for that, ran the script loader for my plugin, and
then, finally, in the callback for that, ran the Sencha Touch
microloader. This resulted in an error. Additionally, I had to
manually set that up in my index.html file after Sencha built my
package. This seems unacceptable.
What I am looking for: I am looking for answers to the following:
Is there a way to configure Sencha's microloader or my Sencha app in general so that Cordova is ensured to have loaded before Sencha's microloader runs?
Is there a way to set this up so that using Sencha Cmd still works, and I don't have to hack around in my index.html file after I build the app?
Note:
*Please don't suggest I use the existing, so-called, SQLite Proxy for Sencha. I specifically chose my approach because, though I appreciated the existing work on a SQLite proxy for Sencha Touch 2 (namely, this), it is actually a WebSQL proxy that does not store natively in SQLite on iOS. My proxy uses the PGSQLite plugin for PhoneGap to natively store data in SQLite on iOS. I plan to open-source it when I have an opportunity to clean it up and untangle it from my code.
I ended up solving this myself by building a custom loader. I am not sure if there is a more Sencha-ish way to do it, but here are the details of what I did, which does work, in case anyone else wants to ensure that PhoneGap is completely loaded in package and production builds before running anything in Sencha. (That would probably be the case in all scenarios in which PhoneGap is packaging a Sencha app).
My index.html file:
<!DOCTYPE HTML>
<html manifest="" lang="en-US">
<head>
<!-- Load Cordova first. Replace with whatever version you are using -->
<script type="text/javascript" src="cordova.js"></script>
<script type="text/javascript" charset="utf-8">
function onBodyLoad() {
// Check for whatever mobile you will run your PhoneGap app
// on. Below is a list of iOS devices. If you have a ton of
// devices, you can probably do this more elegantly.
// The goal here is to only listen to the onDeviceReady event
// to continue the load process on devices. Otherwise you will
// be waiting forever (literally) on Desktops.
if ((navigator.platform == 'iPad') ||
(navigator.platform == 'iPhone') ||
(navigator.platform == 'iPod') ||
(navigator.platform == 'iPhone Simulator') ||
(navigator.platform == 'iPadSimulator')
) {
// Listening for this event to continue the load process ensures
// that Cordova is loaded.
document.addEventListener("deviceready", onDeviceReady, false);
} else {
// If we're on Desktops, just proceed with loading Sencha.
loadScript('loader.js', function() {
console.log('Finished loading scripts.');
});
}
};
// This function is a modified version of the one found on
// StackOverflow, here: http://stackoverflow.com/questions/756382/bookmarklet-wait-until-javascript-is-loaded#answer-756526
// Using this allows you to wait to load another script by
// putting the call to load it in a callback, which is
// executed only when the script that loadScript is loading has
// been loaded.
function loadScript(url, callback)
{
var head = document.getElementsByTagName("head")[0];
var script = document.createElement("script");
script.src = url;
// Attach handlers for all browsers
var done = false;
script.onload = script.onreadystatechange = function()
{
if( !done && ( !this.readyState
|| this.readyState == "loaded"
|| this.readyState == "complete") )
{
done = true;
// Continue your code
callback();
}
};
head.appendChild(script);
}
function onDeviceReady() {
console.log("[PhoneGap] Device initialized.");
console.log("[PhoneGap] Loading plugins.");
// You can load whatever PhoneGap plugins you want by daisy-chaining
// callbacks together like I did with pgsqlite and Sencha.
loadScript('pgsqlite_plugin.js', function() {
console.log("[Sencha] Adding loader.");
// The last one to load is the custom Sencha loader.
loadScript('loader.js', function() {
console.log('Finished loading scripts.');
});
});
};
</script>
<meta charset="UTF-8">
<title>Sencha App</title>
</head>
<!-- Don't forget to call onBodyLoad() in onLoad -->
<body onLoad="onBodyLoad();">
</body>
</html>
Next, create a custom loader in loader.js in your document root, alongside your index.html. This one is heavily based on the development microloader that comes with Sencha. Much props to them:
console.log("Loader included.");
(function() {
function write(content) {
document.write(content);
}
function meta(name, content) {
write('<meta name="' + name + '" content="' + content + '">');
}
var global = this;
if (typeof Ext === 'undefined') {
var Ext = global.Ext = {};
}
var head = document.getElementsByTagName("head")[0];
var xhr = new XMLHttpRequest();
xhr.open('GET', 'app.json', false);
xhr.send(null);
var options = eval("(" + xhr.responseText + ")"),
scripts = options.js || [],
styleSheets = options.css || [],
i, ln, path;
meta('viewport', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no');
meta('apple-mobile-web-app-capable', 'yes');
meta('apple-touch-fullscreen', 'yes');
console.log("Loading stylesheets");
for (i = 0,ln = styleSheets.length; i < ln; i++) {
path = styleSheets[i];
if (typeof path != 'string') {
path = path.path;
}
var stylesheet = document.createElement("link");
stylesheet.rel = "stylesheet";
stylesheet.href = path;
head.appendChild(stylesheet);
}
for (i = 0,ln = scripts.length; i < ln; i++) {
path = scripts[i];
if (typeof path != 'string') {
path = path.path;
}
var script = document.createElement("script");
script.src = path;
head.appendChild(script);
}
})();
Notice that your index.html file does not contain a #microloader script element. That's because you should take it out and use your custom loader.
With all that in place, you will be able to sail smoothly, knowing that the whole PhoneGap environment is in place before your Sencha javascript starts doing things.

Getting Unknown Provider error when injecting a Service into an Angular unit test

I'm fairly new to Angular and have reviewed all the similarly related questions on Stack Overflow but none have helped me. I believe I have everything set up correctly but am still getting an 'Unknown Provider' error when attempting to inject a service into a unit test. I have laid out my code below - hopefully someone can spot an obvious error!
I define my modules in a seperate .js file like this:
angular.module('dashboard.services', []);
angular.module('dashboard.controllers', []);
Here is where I define a service called EventingService (with logic removed for brevity):
angular.module('dashboard.services').factory('EventingService', [function () {
//Service logic here
}]);
Here is my controller that uses the EventingService (this all works fine at runtime):
angular.module('dashboard.controllers')
.controller('Browse', ['$scope', 'EventingService', function ($scope, eventing) {
//Controller logic here
}]);
Here is my unit test - its the line where I attempt to inject the EventingService that causes an error when I run the unit test:
describe('Browse Controller Tests.', function () {
beforeEach(function () {
module('dashboard.services');
module('dashboard.controllers');
});
var controller, scope, eventingService;
beforeEach(inject(function ($controller, $rootScope, EventingService) {
scope = $rootScope.$new();
eventingService = EventingService
controller = $controller('Browse', {
$scope: scope,
eventing: eventingService
});
}));
it('Expect True to be True', function () {
expect(true).toBe(true);
});
});
When I run the test I get this error:
Error: Unknown provider: EventingServiceProvider <- EventingService
I have ensured that my jasmine specrunner.html file has all the necessary source files (this is an Asp.Net MVC project):
<!-- Include source files here... -->
#Scripts.Render("~/bundles/jquery")
<script type="text/javascript" src="#Url.Content("~/Scripts/angular.js")"></script>
<script type="text/javascript" src="#Url.Content("~/Scripts/angular-mocks.js")"></script>
<script type="text/javascript" src="#Url.Content("~/App/scripts/app.js")"></script> <!-- Angular modules defined in here -->
<script type="text/javascript" src="#Url.Content("~/App/scripts/services/eventing.js")"></script> <!-- My Eventing service defined here -->
<script type="text/javascript" src="#Url.Content("~/App/scripts/controllers/browse.js")"></script> <!-- My Browse controller defined here -->
<!-- Include spec files here... -->
<script type="text/javascript" src="#Url.Content("~/App/tests/browse.js")"></script> <!-- The actual unit test here -->
I just can not fathom why Angular is throwing this error complaining about my EventingService. My controller works fine at runtime - it's just when I try to test it that I am getting an error so I am curious as to whether I have screwed something up with the mocking/injection.
The Angular help on testing is rubbish so I am stumped at present - any help or suggestions anyone can give would be very appreciated. Thanks.
I just ran into this and solved it by switching to getting the service using the $injector explicitly:
var EventingService, $rootScope;
beforeEach(inject(function($injector) {
EventingService = $injector.get('EventingService');
$rootScope = $injector.get('$rootScope');
}));
I wish I could tell you why this works and why the simple
beforeEach(inject(function(EventingService) { .... }));
does not, but I don't have the time to investigate the internals. Always best to use one coding style and stick to it.
This style is better in that the name of the variable that you use in your tests is the correct name of the Service. But it is a bit verbose.
There is another angular magic feature that uses strange variable names like $rootScope but I don't like the hacky look of that.
Note that the most of the time people get this error because they didn't include the modules:
beforeEach(module('capsuling'));
beforeEach(module('capsuling.capsules.services'));
If your controllers (defined under dashboard.controllers module) depend on some services which are enclosed in different module (dashboard.services) than you need to reference the dependency modules in your module signature:
angular.module('dashboard.services', []);
angular.module('dashboard.controllers', ['dashboard.services']);
While this question is fairly old i lost significant time solving a problem similar to this one, i.e.:
Error: Unknown provider: SomeServiceProvider <- SomeService
Hence, i'm leaving here another possible cause to this issue. Hopefully, it would helpful to someone.
In my case, i had in my project two modules with the exactly same name but with different dependencies being created, i.e., two different .js files with:
angular.module('moduleName', [dependencies]))
From angular documentation:
Passing one argument retrieves an existing angular.Module, whereas passing more than one argument creates a new angular.Module
Conclusion: It turns out that what was being injected in the test was the module with the wrong dependencies. Removing the second argument from the module that was erroneously being created solved the problem.
Have you tried defining an additional module that depends on your other aggregated modules like so:
angular.module( 'dashboard', [ 'dashboard.services', 'dashboard.controllers' ] )
So you can in the beforeEach specify the one module that has both submodules defined in it like so:
describe('Browse Controller Tests.', function () {
beforeEach(function () {
module('dashboard');
});
var controller, scope, eventingService;
beforeEach(inject(function ($controller, $rootScope, EventingService) {
scope = $rootScope.$new();
eventingService = EventingService
controller = $controller('Browse', {
$scope: scope,
eventing: eventingService
});
}));
it('Expect True to be True', function () {
expect(true).toBe(true);
});
});

Multiple whitelists/blacklists for content script injection?

I'm building a Safari extension with two different content scripts. One script needs to be injected into all http pages (but not https pages). The other one only gets injected into google.com pages regardless of scheme.
In order to achieve this, I have set Extension Website Access to:
This should mean that at a high level, content scripts in my extension should be able to access all pages.
To get more fine-grained control, I then programatically inject the content scripts into URLs which match my patterns.
App = {
content: {
// Inject into unsecure pages.
whitelist: ['http://*/*'],
// But not into secure pages.
blackList: ['https://*/*'],
loc: safari.extension.baseURI + 'data/content.js'
},
results: {
// Inject this script into all google.com pages
whiteList: ['http://*.google.com/*', 'https://*.google.com/*'],
// Surely I don't need a blacklist if I specify a whitelist?
blacklist: undefined,
loc: safari.extension.baseURI + 'data/results.js',
}
};
// Inject the first content script.
safari.extension.addContentScriptFromURL(App.content.loc,
App.content.whitelist, App.content.blacklist, false);
// Inject the second content script.
safari.extension.addContentStyleSheetFromURL(App.results.cssLoc,
App.results.whitelist, App.results.blacklist, false);
The problem is that both scripts are being injected into all pages. It's as if my white and blacklists do nothing. What am I doing wrong?
I was using capitals in my whilelist/blacklist definitions at the top:
App = {
content: {
blackList: ['https://*/*'],
},
results: {
whiteList: ['http://*.google.com/*', 'https://*.google.com/*']
}
};
But then using non-capitalized versions of the variables when I pass the lists into the script injection function.
safari.extension.addContentScriptFromURL(App.content.loc, App.content.whitelist, App.content.blacklist, false);
This obviously means that undefined was being passed into the injection function rather than an actual whitelist/blacklist.

Rails asset pipeline & coffeescript files, how to bind actions in various files to ajax calls?

Using rails 3 asset pipeline, I've structured the javascript (by using coffeescript) to files regarding the model. For example, all comment writing related javascript is stored to /app/assets/javascripts/comments.js.coffee, and user overlay related (fetching a:href's and triggering ajax on them) are stored in /app/assets/javascripts/users.js.coffee.
However, now I'm using more and more AJAX calls, where HTML content is pulled dynamically to the site. The problem is that I need to execute the javascript in various files, but as coffeescript is scoped inside a function, I can not access them.
Let's say that I've got a general.js.coffee file with following code
$(document).ready ->
# Parse all images with action
$("img.clickableaction").click ->
# Fetch some content
$.ajax
url: "something.php"
dataType: "html"
complete: (xhr, status) ->
# We got the content, set it up to a container
$("#somecontainer").html(receiveddata)
# The tricky part:
# run code in comments.js.coffee for #somecontainer (or for the whole dom)
# run code in users.js.coffee for #somecontainer (or for the whole dom)
And comments.js.coffee contains for example the following:
$(document).ready ->
commentDiv = $('div#commentsContainer')
commentsFetch = $('a.commentsFetch')
# Set commentid for comments fetch
commentsFetch.bind 'ajax:beforeSend', (event, xhr, settings) ->
# do stuff
The comments.js.coffee code works for the initial page view, e.g. the HTML code that was received when user loaded the page. But now, I need to parse the comments.js.coffee code for the content returned from the ajax call.
I can not make a function inside comments.js because it is scoped away, and can not be accessed from the general.js. This is what coffeescript produces:
(function() {
$(document).ready(function() {
var commentDiv, commentsFetch;
commentDiv = $('div#commentsContainer');
commentsFetch = $('a.commentsFetch');
}
})
I could make a global function for each separate file, e.g. window.comments && window.users, but then I'd need to call window.comments from the many places where I need to have ajax oncomplete call. On the long term, that will produce duplicate and hard to maintain -code.
How could something like this be made:
// comments.js
window.ajaxExecuteBlocks.push(function() { // comments related stuff });
// user.js
window.ajaxExecuteBlocks.push(function() { // user related stuff });
// general.js ajax on complete:
window.runExecuteBlocks()
Then, runExecuteBlocks would somehow run through all the functions that have been initialized in various controller-specific javascript files.
Anyone implemented similar system?