How to load environment specific configuration with requirejs? - backbone.js

I am making a single page app and using backbone with requirejs. Now I want to include different configurations depending environment variable. I don't know how can I do this and what is best practices.
I have three different config file:
config.js : This keep common configurations.
config.dev.js : This keeps development environment specific configurations
config.production.js : This keeps production specific configurations
I am extending Config model(in config.js) from environment specific model and I requiring the Config model from other modules.
This working fine but downloading dev and production config files both. I want only load config.dev.js or config.production.js not both.
// config.js
define([
'backbone',
'modules/config/config.dev',
'modules/config/config.production'
], function(Backbone, DevConfig, ProdConfig) {
// Select environment model
EnvConfig = (ENV == 'dev') ? DevConfig : ProdConfig;
var ConfigModel = EnvConfig.extend({
// Extending attributes.
defaults: _.extend({
foo : 'bar'
}, EnvConfig.prototype.defaults)
});
return new ConfigModel();
});
In other modules I am using Config as below style:
define([
'backbone',
'modules/config/config'
], function(Backbone, Config) {
var ProfileModel = Backbone.Model.extend({
urlRoot: Config.get('apiRoot') + '/profiles'
...
});
return ProfileModel;
});
How to load one of development or production specific config file, not both?

I'm not sure where the ENV variable comes from, but assuming it's just a global variable you could do something like this:
index.html
<script type="text/javascript">
// adding it here so the example works on its own
window.ENV = 'production'; // or 'dev'
// this will be added to RequireJS configuration
var require = {
paths: {
// 'env-config' will be an alias for actual file name
'env-config': 'config.' + window.ENV
}
};
</script>
<script data-main="scripts/main" src="scripts/requirejs.js"></script>
config.dev.js:
define({
'env': 'development!'
});
config.production.js:
define({
'env': 'production!'
});
main.js:
require(['config', 'env-config'], function(conf, envConfig) {
console.log('The environment is: ', envConfig.env);
});

Related

ngHtml2JsPreprocessor does not load the module containing the templates, tested with karma

Testing my angular app with karma ngHtml2JsPreprocessor, I have read dozens of pages on how ton configure karma to generate js templates instead of html ones that are loaded by Angular Directives, I am stuck with the message :
Module 'js_templates' is not available! You either misspelled the module name or forgot to load it. If registering a module ensure that you specify the dependencies as the second argument.
http://errors.angularjs.org/1.4.6/$injector/nomod?p0=js_templates
My folder structure is like this :
/System/Root/Path/
web/
js/
app/
test/
specs/
UserModule/
UserDirectives.spec.js
modules/
UserModule/
UserDirectives.js <=== Where the directive is defined
Templates
login.tpl.html
My directive is defined like this :
directives.directive("loginForm", [function(){
return{
restriction: "E",
templateUrl: assetsRootPath+'/Templates/login.tpl.html' //assetsRootPath is defined to '' for karma
}]);
Karma is configured as follow :
module.exports = function(config) {
config.set({
basePath: './web/js/',
frameworks: ['jasmine', 'requirejs'],
preprocessors: {
'../Templates/**/*.html': ['ng-html2js']
},
ngHtml2JsPreprocessor: {
stripPrefix: '.*web/',
prependPrefix: '/',
moduleName: 'js_templates',
enableRequireJs: false
},
plugins : [
'karma-chrome-launcher',
'karma-jasmine',
'karma-requirejs',
'karma-ng-html2js-preprocessor'
],
// list of files / patterns to load in the browser
files: [
{ pattern: '../Templates/**/*.html', included: false },
//....Other needed files
]
The spec for testing the directive :
define(['angular', 'ngmocks', 'app/modules/UserModule/UserDirectives'], function() {
describe("Test User Directive > ", function () {
var $compiler, compiledElement;
beforeEach(module('User.Directives'));
beforeEach(module('js_templates'));
beforeEach(inject(function (_$rootScope_, _$compile_) {
$rootScope = _$rootScope_;
$compile = _$compile_;
scope = $rootScope.$new();
compiledElement = $compile("<login-form></login-form>")(scope);
scope.$digest();
}));
it('Tries to test', function(){
expect(1).toEqual(1);
});
});
});
When I launch the tests the following error appears :
Module 'js_templates' is not available! You either misspelled the module name or forgot to load it. If registering a module ensure that you specify the dependencies as the second argument.
http://errors.angularjs.org/1.4.6/$injector/nomod?p0=js_templates
I have tried a lot of configurations and nothing works, I have also tried to debug ngHtml2JsPreprocessor by adding some log.debug & I see that the module 'js_templates' is rightly created, the problem is why can't I load it for the specs. Information that could be useful (not sure if it changes something) : I am working with requirejs.
Thanks for your help
Well, as no config worked, I have chosen to use a different approach, below is what I ended up to do to make it work :
1 - Remove all pre-processors
2 - Install the simple html2js node plugin : npm install karma-html2js-preprocessor --save-dev
3 - Inside my karma.conf file :
pattern: { files: '../Templates/**/*.html', included : true } //Note the included property to true
preprocessors: {
'../Templates/**/*.html': ['html2js']
},
plugins : [
'karma-chrome-launcher',
'karma-jasmine',
'karma-requirejs',
'karma-html2js-preprocessor'
],
Inside my test-main.js file which is the entry point of require-js I have done this :
require([
'angular',
...
], function(angular, app) {
var $html = angular.element(document.getElementsByTagName('html')[0]);
angular.element().ready(function() {
// bootstrap the app manually
angular.bootstrap(document, ['app']);
//Making all templates to be availabe through js_template module
angular.module('js_templates', []).run(function($templateCache){
angular.forEach(window.__html__, function(content, filename){
var modifiedFilename = filename;
/*****************Modify this to fit your app**********************/
var breakPos = modifiedFilename.indexOf('/Templates');
modifiedFilename = filename.substr(breakPos, modifiedFilename.length - breakPos);
/*****************/Modify this to fit your app*********************/
$templateCache.put(modifiedFilename, content);
});
});
});
}
);
Not very sharp but not so bad as well, anyway it does work and it's exactly what I wanted. When a solution will come to work naturally with ngHtml2JsPreprocessor, I will use it.

r.js cannot resolve dependencies mentioned in shim

I've recently joined a project which is built using Backbonejs, (uses Marionette for view rendering) + nodejs. They also use requirejs to load the backbonejs files. Would like to add at this stage, that I've never worked with backbonejs or requirejs before and hence I'm struggling with the issue I describe later.
Some code that will help explain the issue that I run into (All this code was already written by previous dev's)
Folder Structure:
/public
/js
/collection (consists all Backbone.js collections files)
/lib
/bower_components
/backone
/marionette
/etc
/models (consists all Backbone.js models files)
/views (consists all Backbone.js view files)
/main.js
/main.build.js
/app.js
/controller.js
/router.js
Code from files that I think relate to issue:
main.js
requirejs.config({
paths: {
'async': 'lib/bower_components/requirejs-plugins/src/async',
'jquery': 'lib/bower_components/jquery/dist/jquery.min',
'underscore': 'lib/bower_components/underscore/underscore-min',
'lodash': 'lib/bower_components/lodash/dist/lodash.min',
'backbone': 'lib/bower_components/backbone/backbone',
'marionette': 'lib/bower_components/marionette/lib/backbone.marionette.min',
'markercluster':'lib/markercluster',
'jquerymobile': 'lib/jquery.mobile-1.4.0.min',
'hogan': 'lib/template-2.0.0.min',
'templates': '/templates',
'real': 'lib/mainjs',
'touch': 'lib/jquery.touchSwipe.min',
'mouse': 'lib/jquery.mousewheel',
'moment': 'lib/moment-2.5.1.min',
'humanize': 'lib/bower_components/humanize-plus/public/dist/humanize.min',
'validator': 'lib/bower_components/validator-js/validator.min',
'real': 'lib/mainfile'
},
shim: {
backbone: {
deps: ["underscore"]
},
marionette: {
deps: ["backbone"]
},
templates: {
deps: ["hogan", "jquery"]
},
real: {
deps: ["jquery", "jquerymobile", "touch", "mouse"]
},
markercluster: {
exports: "MarkerClusterer"
},
humanize: {
exports: "humanize"
}
},
waitSeconds: 0
});
define('gmaps', ['async!http://maps.google.com/maps/api/js?v=3&key=AIzaSyBiV8f88yLWJ_IMSdP1fVNO1-gt3eLVSgg&sensor=true&callback=gMapsCallback'], function(){
// define('gmaps', ['http://maps.google.com/maps/api/js?v=3&sensor=false'], function(){
return window.google.maps;
});
require(['app', 'templates', 'real'], function(app) {
app.start({
version: "0.9.9"
});
});
main.build.js
({
baseUrl: ".",
name: "main",
wrapShim: true,
out: "main-built.js"
})
app.js
define(['underscore', 'controller', 'router', 'models/Cache', 'views/RootView'], function(_, Controller, Router, Cache, RootView) {
var Application = Marionette.Application.extend({
propertyListPageSize: 3,
initialize: function() {
_.templateSettings = { interpolate : /\{\{(.+?)\}\}/g };
},
onStart: function(options){
new RootView();
this.controller = new Controller();
this.router = new Router({controller: this.controller});
this.cache = new Cache();
this.context = {};
//this.evHistory = [];//#todo remove once BB/marionette navigation is in place
if(Backbone.history) Backbone.history.start({ pushState: false });
if(Backbone.history.fragment === "") this.navigate('home');
},
navigate: function(fragment, trigger, replace){
this.router.navigate(fragment, {trigger:trigger, replace:replace});
},
back: function() {
window.history.back();
}
});
app = new Application();
return app;
});
rootView.js
define(['marionette', 'views/HomeView', 'views/HeaderView', 'views/FooterView', 'views/MenuView', 'views/VideoView', 'views/LocationSearchView', 'views/LoginView', 'views/FindView', 'views/ServicesView', 'views/ValueView', 'views/PropertyListView', 'views/SideBySideView', 'views/ConfirmRegistrationView', 'views/ForgotPasswordView', 'views/CreateAccountView', 'views/UserHomeView', 'views/MyBrokerView', 'views/GiveFeedbackView', 'views/SeeFeedbackView', 'views/ViewingScheduleView', 'views/MyViewingsSummaryView', 'views/MyAccountView', 'views/ViewingConfirmView', 'views/ValueAddressPropertyListView'],
function(Marionette, HomeView, HeaderView, FooterView, MenuView, VideoView, LocationView, LoginView, FindView, ServicesView, ValueView, PropertyListView, SideBySideView, ConfirmRegistrationView, ForgotPasswordView, CreateAccountView, UserHomeView, MyBrokerView, GiveFeedbackView, SeeFeedbackView, ViewingScheduleView, MyViewingsSummaryView, MyAccountView, ViewingConfirmView, ValueAddressPropertyListView) {
var RootView = Marionette.LayoutView.extend({
...some view code
});
Use case I'm trying to solve:
So when I access the site in the browser, I notice in the debugger that it loads all the js files right at the beginning. During the load process my site is blank and user has to wait a while before he can use the site.
So what I've been able to understand is that when app is 'started' in main.js, app.js creates an instance of rootView.js , which in turn has all the views listed as dependencies. This triggers a download request for all the other views which in turn would solve their own dependencies and download all the relevant models and collections. Hence all files being downloaded when the user accessed the site.
Solution I've been trying:
Since requirejs is being used, I'm trying to use r.js to optimize and combine all the js files to reduce the number of downloads.
Issue I'm running into:
When i run r.js. i get the following error
Tracing dependencies for: main
Error: ENOENT: no such file or directory, open '/var/node_projects/rm/rm.src.server/src/public/js/underscore.js'
In module tree:
main
app
Error: Error: ENOENT: no such file or directory, open '/var/node_projects/rm/rm.src.server/src/public/js/underscore.js'
In module tree:
main
app
at Error (native)
If I add the underscore.js files directly to the specified path in the error, then I get the same error for marionette.js. What I think is happening is that app.js is not recognizing the shim'ed dependencies and hence its trying to find the files directly at specified path in the error.
Things I've tried:
- I've added wrapShim: true in the main.build.js file but that did not help
Honestly, I've been sitting on this for a couple of days and I'm not sure what I can do next and hence this post.
Any help/direction would be appreciated.
You need to include the same shim configuration in your build file, as wrapShim is not sufficient.
If shim config is used in the app during runtime, duplicate the config here. Necessary if shim config is used, so that the shim's dependencies are included in the build. Using "mainConfigFile" is a better way to pass this information though, so that it is only listed in one place. However, if mainConfigFile is not an option, the shim config can be inlined in the build config.
https://github.com/jrburke/r.js/blob/master/build/example.build.js

Chrome extension using RequireJS, Backbone (Chaplin) conflicts

I am creating a Google Chrome Extension that have to add content on the visited websites (like a toolbox).
I have to use RequireJS and BackboneJS (Chaplin) and everything is ok except when i'm visiting a website using RequireJS (and Backbone, but the problem seems to come from RequireJS conflicts).
(This is when I use content scripts to include a -script- tag that includes RequireJS.)
I suppose it's normal to have conflicts if I add content directly in the page so I tried the solution here : Loading multiple instances of requireJS and Backbone
It seems to work (for now), but the website is trying to reload his own RequireJS file (with his path, but in my extension) before loading mine and I'm afraid it could lead to unexpected behaviour.
Plus, I have to precise my file paths in requirejs.config or it's looking for them in Bitbucket sources (cloudfront). (Maybe it's normal though)
Example with bitbucket :
Denying load of chrome-extension://mgncmiffelpdhlbkkmmaedbodabdchea/https://d3oaxc4q5k2d6q.cloudfront.net/m/7aaf1677069c/amd/build/main.js?8uxr. Resources must be listed in the web_accessible_resources manifest key in order to be loaded by pages outside the extension.
<--------- This file is Bitbucket's RequireJS, Bitbucket is still working fine though
Is there another solution I didn't find yet ? Or am I doing it wrong ? I'm a beginner with RequireJS (and Chrome ext.. and Backbone...) so I might have missed something.
Here is the Content script part in manifest.json
"content_scripts": [
{
"matches": ["https://*/*", "http://*/*"],
"js": ["bower_components/requirejs/require.js",
"extension/init-app.js",
"extension/main.js"]
}],
init-app.js is Rob's script
require.load = function(context, moduleName, url) {
url = chrome.extension.getURL(url);
var x = new XMLHttpRequest();
// Append Math.random()... to bust the cache
x.open('GET', url + '?' + Math.random().toString(36).slice(-4));
x.onload = function() {
var code = x.responseText;
x += '\n//# sourceURL=' + url; // Optional, for debugging.
window.eval(code);
context.completeLoad(moduleName);
};
x.onerror = function() {
// Log error if you wish. This is usually not needed, because
// Chrome's developer tools does already log "404 Not found"
// errors for scripts to the console.
};
x.send();
};
and main.js contain requirejs.config + app
// Configure the AMD module loader
requirejs.config({
skipDataMain: true,
// The path where your JavaScripts are located
baseUrl: 'extension',
// Specify the paths of vendor libraries
paths: {
jquery: '../bower_components/jquery/jquery',
underscore: '../bower_components/lodash/dist/lodash',
backbone: '../bower_components/backbone/backbone',
handlebars: '../bower_components/handlebars/handlebars',
text: '../bower_components/requirejs-text/text',
chaplin: '../bower_components/chaplin/chaplin',
application: '/extension/application',
routes: '/extension/routes',
},
// Underscore and Backbone are not AMD-capable per default,
// so we need to use the AMD wrapping of RequireJS
shim: {
underscore: {
exports: '_'
},
backbone: {
deps: ['underscore', 'jquery'],
exports: 'Backbone'
},
handlebars: {
exports: 'Handlebars'
}
}
// For easier development, disable browser caching
// Of course, this should be removed in a production environment
//, urlArgs: 'bust=' + (new Date()).getTime()
});
// Bootstrap the application
require(['application', 'routes'], function(Application, routes) {
new Application({routes: routes, controllerPath: 'scripts/controllers/', controllerSuffix: '-controller'});
});
It works on gooogle.com for instance, but I get
GET chrome-extension://ccgfmmmnebacpnbdpdnphmnmicaooddg/extension/Home.js?9zfr net::ERR_FILE_NOT_FOUND
on https://www.cloud9trader.com (website using RequireJS) because it has
<script data-main="/0.2.59/scripts/Home.js" src="//cdnjs.cloudflare.com/ajax/libs/require.js/2.1.14/require.min.js"></script>
in its source. To summarize I just need the script to ignore the "current" website Require file.
The skipDataMain option is synchronously checked when require.js is loaded. Setting this variable after loading require.js has no effect on the loader any more, because the data-main scan has already run at that point.
The correct way to skip data-main is to declare the configuration before loading require.js, as follows:
// extension/config.js
var require = {
skipDataMain: true
};
manifest.json:
{
...
"content_scripts": [{
"matches": ["https://*/*", "http://*/*"],
"js": [
"extension/config.js",
"bower_components/requirejs/require.js",
"extension/init-app.js",
"extension/main.js"
]
}],
...
}

Configure value using a service constant

In my latest project I'm making some API requests.
As far as I need to setup different endpoints (local development, remote production), I set the value of the used endpoint when defining the app. Everything works just fine.
var app = angular.module('app', ['lelylan.dashboards.device', 'ngMockE2E'])
app.value('client.config', { endpoint: 'http://localhost:8000' });
The only problem is that I want to set the endpoint http://localhost:8000 from a service that defines some constants [1]. I tried using the run and config block, but the value is not set. I was doing something like this, without any result.
var app = angular.module('app', ['lelylan.dashboards.device', 'ngMockE2E'])
.config(function(ENV) {
app.value('lelylan.client.config', { endpoint: ENV.endpoint });
})
This looks quite terrible to me, but I've no idea on how to solve this issue.
Thanks a lot.
[1] grunt-ng-constant
Please see here : http://jsbin.com/foqar/1/
var app = angular.module('app', []);
var client = {
endpoint : 'http://localhost:8000'
}
app.value('config', client);
app.controller('firstCtrl', function($scope, config){
$scope.endpoint = config.endpoint;
});
or: http://jsbin.com/foqar/2/edit
var app = angular.module('app', []);
app.value('config',{client :{endpoint : 'http://localhost:8000'}});
app.controller('firstCtrl', function($scope, config){
$scope.endpoint = config.client.endpoint;
});
It looks like the grunt-ng-constant creates a new module file to define the constants. You do not have to bother about it on your app JS file, other than declare the module containing those constants as a dependency.
The below is taken as is on the example config of the documentation of grunt-ng-constant
grunt.initConfig({
ngconstant: {
options: {
name: 'config',
dest: 'config.js',
constants: {
title: 'grunt-ng-constant',
debug: true
}
},
dev: {
constants: {
title: 'grunt-ng-constant-beta'
}
},
prod: {
constants: {
debug: false
}
},
}
});
In the options section, you specify the name of the module, file for the module to be written to and a general set of constants. It works like this,
options: {
name: 'config',
dest: 'config.js',
constants: {
title: 'grunt-ng-constant',
debug: true
}
}
The above code will become,
/*File: config.js*/
angular.module('config', [])
.constant('title', 'grunt-ng-constant')
.constant('debug', true);
To change the constants based on your scenario (development / production) you would be using different task sets. Here is where the dev and prod section comes into play
Considering you are using ng-boilerplate, in the gruntfile.js you have tasks build and compile. The build task is used during development, and the compile gets your app ready to be pushed to production.
In the build task you would add ngconstant:dev and in the compile task you would add ngconstant:prod.
grunt.registerTask('build', ['clean', 'html2js', 'otherTasksComeHere', 'ngconstant:dev']);
grunt.registerTask('compile', ['clean', 'html2js', 'otherTasksComeHere', 'ngconstant:prod']);
For your scenario the code would be as below:
/*gruntfile.js*/
grunt.initConfig({
ngconstant: {
options: {
name: 'lelylan.client.config',
dest: 'config.js',
values: {
endpoint : 'http://localhost:8000'
}
},
dev: {
debug: true
},
prod: {
endpoint: 'http://www.production-server.com/',
debug: false
}
},
});
grunt.registerTask('build', ["ngconstant:dev"]);
grunt.registerTask('compile', ["ngconstant:prod"]);
grunt.registerTask.('default', ["build", "compile"]);
/*app.js*/
var app = angular.module('app', ['lelylan.dashboards.device', 'leylan.client.config', 'ngMockE2E']);
app.controller("appCtrl", ["$scope", "$http", "endpoint",
function($scope, $http, endpoint) {
$scope.submit = function(formData) {
$http.post(endpoint+'/processform.php', formData);
}
}]);
Now it all depends on whether you run grunt build or grunt compile. The default task is run when you use the grunt command.
The solution was to use providers. As the docs states:
during application bootstrap, before Angular goes off creating all
services, it configures and instantiates all providers. We call this
the configuration phase of the application life-cycle. During this
phase services aren't accessible because they haven't been created
yet.
For this reason I created a provider to set the needed configurations.
<script>
angular.module('app', ['lelylan.dashboards.device'])
angular.module('app').config(function(lelylanClientConfigProvider, ENV) {
lelylanClientConfigProvider.configure({ endpoint: ENV.endpoint });
});
</script>
In this way all services will then be able to use the new endpoint. Using .value this was not possible.
Thanks everyone for your help.

Requirejs2: How to handle own files?

I have configured requirejs to load the core libs (jquery, underscore, backbone).
Now I would like to add my backbone models, controllers, views, etc to be loaded asyncronly
I found a lots of tutorials to this topic and lots of "ready" boilerplates unfortunatly I mentioned that most approaches are depreceated or rather complicated (even there are better approaches).
One example is how I configured requirejs for the main libs:
https://stackoverflow.com/a/10914666/1309847
So how do I load Backbone Views, Models, Collections, Routers, Controllers and Templates with a simple and valid Requirejs configuration?
I followed youre advice but get some strange error
main.js
require.config({
paths: {
jquery: 'vendors/jquery/jquery',
underscore: 'vendors/underscore/underscore',
backbone: 'vendors/backbone/backbone'
},
shim: {
underscore: {
exports: '_'
},
backbone: {
deps: ['underscore', 'jquery'],
exports: 'Backbone'
}
}
});
require(['app'], function(app){
});
app.js
define(['jquery', 'underscore', 'backbone'], function($, _, Backbone){
var Message = new Backbone.Model.extend({
//idAttribute: '_id',
//defaults: { body: '' }
//url: function(){ return this.id ? '/messages/' + this.id : '/messages'; }
});
var newMessage = new Message({ body: 'hi' });
newMessage.save();
});
The error occours in app.js:
Uncaught TypeError: Object [object Object] has no method 'apply'
When I comment the new Backbone.Model.extend part I don't get any error anymore.
in my experience, the best way to bootstrap your application is by creating a Backbone.Router. So you can associate urls with your application functionality.
If you are using RequireJS+Backbone, you probably have a main.js where RequireJS is configured (paths, shims, etc). The first call to "require" is used to load a initial script in order to bootstrap the whole app.
For example:
/**
* main.js - RequireJS bootstrap
*/
require.config({
paths: {
//your paths
},
shim: {
//your shims
}
});
require(
[
'app' //app.js is at the same directory as main.js
],
function(app) {
app.init();
}
);
then in app.js you can create a new Router instance, or you can just start creating Views and Models.
For further reference: http://addyosmani.github.com/backbone-fundamentals/
So as I have now understood right: You have to wrap a requirejs function around youre own custom js file.
The function is called define. The first parameter is an array of the dependencies which you have defined in the main.js file or a relative path to another custom js from you.
The second parameter is the callback which holds the original file. Important is that you return the object, function, array or variable which you want to share.
The whole thing looks like this:
define(
['underscore', 'backbone'], // the dependencies (either relative paths or shortcuts defined in main.js
function(_, Backbone){ // the return statement of the deps mapped to a var
var MessageModel = Backbone.Model.extend({ // the original code, file
defaults: { body: '' },
initialize: function(){}
});
return MessageModel; // the return statement, sharing the "final result", sometimes you return the initialize parameter
});
The same for a collection wrapping the models:
define(
['jquery', 'underscore', 'backbone', 'models/message_model'], // deps and the last one is the relative path
function($, _, Backbone,MessageModel){ // same as above explained
var MessageCollection = Backbone.Collection.extend({
model: MessageModel,
initialize: function(){}
});
return MessageCollection;
});
I now only have to figure out how I can bootstrap to whole application. But I think I need more knowledge of backbone to do this :)

Resources