Dependency Injection in a distributable Javascript library? - javascript

We are using Backbone to create reusable components. We create controllers in order to setup bindings between models and views. We wish to offer people the ability to replace the models and views with their own implementations.
Since the majority of people will use the the components we provide, I don't want to force developers to configure or create anything that isn't different from the defaults.
This means that someone should be able to pass an instance of an object they want to use as a model or view to the controller, all configured and setup and ready to go, or they can pass in some configuration to what the controller will use by default.
I am not sure what the best approach is.
// Idea #1
var controller = new Controller({
dependencyA: {
conf: { // config for depedencyA }
},
dependencyB: {
conf: { // config for dependencyB }
class: custom.implement.Class
}
});
In this approach, the user doesn't have control over how to instantiate the object. What's bad about this is, for example, Backbone models take two arguments in the constructor while views only take one.
// Idea #2
var controller = new Controller({
dependencyA: {
args: ['arg1',{
opt1: 'opt-value',
}]
},
dependencyB: {
args: ['a','b','c']
class: custom.implement.Class
}
});
Args would be the arguments passed to a constructor. This means the controller calls the constructor with the args array, and again this only really benefits you if you're passing in custom configuration for default dependencies. If you want to pass your own implementation it's more awkward.
// Idea #3
var controller = new Controller({
dependencyA: new outOfBoxModel({ // configuration }),
dependencyB: new custom.imeplement.Class('a','b','c')
});
In this approach, the user is forced to instantiate the out of box model. If the model's default settings are all appropriate though, then the user is doing unnecessary work. The only bit they HAVE to do is create an instance of their own custom implementation.
I am not sure what the best approach would be here?

Of the three approaches, I most prefer approach number 3. Here is why:
It is more consistent than the other approaches. In the 3rd approach, the user only has to learn to pass in constructed instances of dependencies into the controller. In the other approaches, the user has to pass in either args, or args and a class name.
It does not violate the Single Responsibility Principle. In the first two approaches, your controller is made responsible for constructing and configuring its dependencies. This doesn't feel like dependency injection at all! I think it's better, and simpler, to leave the construction of dependencies to the user or another part of your application. In my opinion, its not a terrible thing to force the user to construct their own implementations - it gives them the freedom to define their constructors however they want, rather than forcing you to define and maintain constructor APIs for the Controllers dependencies, and forcing the user to conform to them.
A different idea:
If you have this freedom in your application, I would consider putting your Controller construction logic in a factory class or method:
var createController = function(config) {
// Parse, validate, extract relevant config items
// var a = create dependency a
// var b = create dependency b
return new Controller(a, b);
}
This approach allows you to be as fancy as you want with your definition of config - you could support all three of the config definitions you provided in your original post - although I wouldn't recommend that :-). At a minimum, I would have the factory method support a zero args invocation (in which case it would return the default construction of Controller) and one of your preferred config definitions.

Related

How to create an instance of the Model Class in MVC Pattern - SapUI5

I am currently trying to implement the Model-View-Controller in my SAPUI5 / Fiori Project.
I managed to create an Instance of the Controller with: new sap.ui.core.mvc.Controller('controller.js')
This does not work for the Model (sap.ui.core.mvc does not contain a Model attribute).
Now I am searching a way to call functions of the Model from the Controller, to get my data.
I already tried this code: oObjModel = new sap.ui.mode.Model(), using this I cannot call functions from my Model.
I recommend you look at the walkthrough on the SAPUI5 documentation site. It shows how to initialize all the aspects of MVC in the correct way.
Models in SAPUI5 come in different classes to support different forms of data. For example, there is JSONModel, XMLModel, ODataModel, etc.
So to create a model, you need to first determine the specific type of model you need and use its specific constructor. For example, if you have JSON data (or simply a JavaScript object), you use the JSONModel:
var yourData = { "hello": "world" };
var oModel = new JSONModel(yourData);
Note that the above code assumes you are following the recommended way to use modules and that this code is wrapped with a sap.ui.define or sap.ui.require, where the module sap/ui/model/json/JSONModel is assigned to the variable JSONModel. The walkthrough shows this correct usage pattern. Accessing the constructor directly like the below is not recommended:
// Also probably works, but not the recommended way
var oModel = new sap.ui.model.json.JSONModel(yourData);
Your way of creating a controller is also not correct. You should preferably let the view instantiate the controller for you by providing it a controllerName, as shown in the walkthrough for Controllers.
<mvc:View xmlns="sap.m" xmlns:mvc="sap.ui.core.mvc" controllerName="name.of.your.controller">
<!-- ... -->
</mvc:View>
If you need to manually instantiate a controller from code, use this:
Controller.create({ name: "name.of.your.controller" }).then(function(oController) {
// Do something with oController
});
This again assumes you have the module sap/ui/core/mvc/Controller linked to the variable Controller.
Before version 1.56, you can use the now-deprecated sap.ui.controller function to create controllers instead:
sap.ui.controller("name.of.your.controller", null, /*async=*/true).then(function(oController) {
// Do something with oController
});
Be aware that both of these examples load the controller asynchonously, as synchronous XHR is being globally deprecated outside of Workers, and thus the framework recommends you to use async only. In fact, the new way of loading does not even provide an option for sync loading.

How to share a global variable/object with RequireJS

I'd like to set a variable or object in main.js which I can then reference from any backbone view.
I considered using localStorage, but the data is dynamic and a little sensitive so I wouldn't like to have it stored in localStorage as it could be manipulated by the user very easily.
Since you said "main.js" I think you're confused between RequireJS and Backbone.js. RequireJS is not part of Backbone. It is an AMD module loader which happens to be used a lot with backbone projects.
Looks like you need a RequireJS module like:
define(function (require) {
var someData;
var singleton = function () {
return {
getMyData = function(){},
setMyData = function(data){},
};
};
return singleton();
});
P.S: Above code can be made object literal, an instance of proper constructor function, es6 class of whatever. I just posted something as an example.
#TJ already gave what's needed to achieve what I call a Service within my app, borrowed from AngularJS services. I have a couple services, like i18n which is just a i18next instance.
Like you, I wanted to manage certain data relative to the app that could be shared everywhere, without putting it in the window object.
I came up with a AppState service which is just a Backbone model instance.
define(['underscore', 'backbone', 'color'], function(_, Backbone, Color) {
var State = Backbone.Model.extend({
setBrandColor: function(hexColor, options) {
return this.set({ color: new Color(hexColor) }, options);
},
getBrandColor: function() {
return this.get('color');
},
});
return new State(); // return a new instance instead of the constructor.
});
What's cool with a Backbone model is that anything within the app can listen to its events. This is inspired from React.
this.listenTo(AppState, 'change:color', this.onBrandColorChange);
Note that I prefer to use PascalCase for services even though they're instances, they're closely related to a static type in other languages.
This way, when the app state changes, other parts of the app may or may not react accordingly. Without the events, the app would need to be more coupled which is undesirable.

Merge two json objects on the server side

I have inherited an express application. In the application we pass to res.render a massive list of local variables. For every page we pass about 30 variables that do not change. If one does change it needs to be changed in a dozen places. This is obviously bad.
So what I want to do store the values locals that do not change separate from the truly local. So I placed these non-changing values in their own file and load the file at the start of each route, like so:
'use strict';
var locals =
{
indexName: 'Home',
...lots more of these...
};
module.exports = { locals : locals };
// in each route files
var local = require('../locals.js');
And I can use it via res.render('sensor', local.locals);
However I also need to add page specific values. So I tried local.locals + { ... }; which doesn't while it doesn't give an error doesn't even show the original values.
I then tried local.locals.concat({...}) but that does give an error (TypeError: Object #<Object> has no method 'concat'), likewise for extend and push.
Is there no methods to extend or merge two objects? Do I need to roll my own?
Also, is this the best way of making the an array global? I would prefer just to call it locals, and not local.locals as that is just cumbersome.
In addition to Object.assign from #mostruash, there are two JS libraries that are very similar, which both provide an object merge function (along with a lot of nice helper utilities). Pick one:
underscore
lodash
Edit: and as to your second question about local.locals...of course, just export the object itself, rather than an object that wraps it:
module.exports = locals;
...and in your routes:
var locals = require('../locals');
console.log(locals.indexName);

What's the secret to data-binding?

For most JS frameworks and libraries, the value they bring are often in the form of a new structure as to how to build an application (Backbone, React), or new ideas that effectively power-up the language (Angular), or simply the methods they offer are well tested, fast, and really convenient (jQuery).
Usually the ideas and methods they provide are pretty straightforward usage of JavaScript, but with a very clever team behind it that find interesting ways to do things which you can think through and get a solid guess as to how the guts work.
However, I've been unable to think through the ability to two-way bind JS models to view components. What is the secret sauce at the heart of this feature that makes this work? Changing an internal variable from a user input is simple, but what about the reverse? How would you be able to "know" when a JS variable has changed in order to update the display instantly? Surely it can't be polling, so what then?
Whenever a block of your JS runs that angular triggered it will run a digest cycle when the block finishes executing. This basically checks all the values that might of changed and would require updates to the view.
If angular didn't trigger the code then it won't know that something might of changed so your bindings can get out of sync. For example if you run something like this
setTimeout(function() {$scope.myValue = '123'});
Angular won't know that myValue changed and it actually won't update the view. That's why Angular has it's own services for doing everything. e.g. $timeout or $http.
If you have some callback function that Angular doesn't know about then you can manually tell it to check for changes by calling $scope.$apply()
there are several ways to do it. Object.observe is great, but lacks good support. You can poll for values as well, keeping a 2nd copy of the object around to compare. You can also write your own explicit set/get methods to update the model like backbone does.
One neat method i use a lot is using getters/setters to keep the model synced to the dom:
//a demo "model" of data:
model = {
name: "Fred"
};
function change(k,v){alert([k,v]);} // a stand-in change monitor for demo
// iterate model and replace values with getter/setter combos:
Object.keys(model).forEach(function(key) {
var val = model[key];
delete model[key];
Object.defineProperty(model, key, {
get: function() {
return val;
},
set: function(v) {
val = v;
change(key, val);
} //call change upon setting
});
change(key, val); //update view "onload"
}); // alerts "Fred";
//update model (fires change() with "name" and "sally" arguments:
model.name="sally"; // alerts "sally";
the change function is quite simple and for your case should just find elements bound to keys.
the advantage here is that you don't need special custom CRUD methods, you can just modify the object properties via assignment like it's 1999. It also doesn't poll, and works correctly all the way back to IE9 and any other ES5 environments. It's the simplest way to bind JS>DOM (afaik) without custom methods.
It does have some limits: nested objects are tricky to get/set upon, you can't do the whole object at once, you can only "watch" primitives. Arrays are a problem too: you can't really replace expando properties with getters/setters without side-effects. But, upon a relatively flat collection of JSON-safe data, get/set works a charm and needs no complex libs to get operational.
checkout a complete example using this method: http://pagedemos.com/xg3szbguqnwu/4
I can speak to how it's done in Backbone, which has a relatively low-level perspective on data-binding.
It's a combination of 1. the library having control over attribute setter methods 2. invoking callback functions when attributes change (e.g. by dispatching events) in order to update the UI.
The essential pseudocode is this:
class Model:
method set(name, value):
if value != this.attributes[name]
this.triggerEvent('change', name, value)
this.attributes[name] = value
m = new Model()
someInputWidget.onEvent('userChangedInput', function(value) {
m.set(someInputWidget.name, value)
})
m.onEvent('change', function(name, value) {
getInputWidgetByName(name).setValue(value)
})
Backbone does not do any data binding to the UI, but you can refer to Backbone's annotated source for the actual event-dispatching implementation.

How to clear view model in Durandal

Is there a way to selectively tell Durandal to reinitialize a view model. I am aware of the singleton vs new instance approaches to initialize view models.
//singleton since a declared object is returned
define(function() {
return { prop1: 1, prop2: 2 }
});
//new instance since a constructor is returned
define(function() {
var ctor = function(){};
return ctor;
});
I generally don't like to declare view models as singletons, but I have to do it in a special case due to sub routing which requires me to pass data from a parent router to my child router. However, the singleton has other side effects, so I was wondering: Is there a way to selectively request a new instance of the view model even if it was initially declared as a singleton?
Not that I know of - this is more of a limitation of requirejs versus Durandal, though. Once require has loaded the module, so far as I know it will always return you the same version of that module. Unless there's a way to tell require to reload the module?
The only thing I could think of would be to "reset" the view model during the activate method. If you're changing routes and finding that the activate method isn't being called, it may be because Durandal thinks that your module is already active (in which case it won't reactivate). You can change this behavior by customising the areSameItem function for the router (see this question for an explanation).
Hope that helps.
I don't know if it helps in your case, but you could use the activate() method in your view model. For more information see http://durandaljs.com/documentation/Hooking-Lifecycle-Callbacks.

Categories

Resources