I have seen many a times that a web page has multiple templates for a specific region of the page, one of which is loaded depending on the scenario. Typically a corresponding VM is instantiated at that point and bound to the template.
Does this design pattern not violate OOP principles in that the objects (VMs in this case) should be based on the functional entities that actually exist and not necessarily based on a specific area of the UI.
Lemme substantiate this with an example. Lets say we have a web page dealing with online sales of clothes, shoes and furniture. Once a selection in made among the three in a particular part of the page, a separate template is loaded in another part of the page which is dependent on the selection made. Should my object model be ItemVM<-ShoeVM,ClothesVM,FurntitureVM as per OOP rules, or should it be ParentVM containing ItemSelectorVM (for the UI to select the item type), shoeAdverisementVM (to bind to the shoe template loaded as a result), FurnitureAdvertisementVM and many more VMs for every part of the page?
When you say OOP principles, I'm assuming you are referring to SOLID.
View Models model the UI and do not have to be coupled to a single template. You may use a view model with a template that has been created to target desktops and then reuse that view model with a template that has been created to target mobiles.
Should my object model be ItemVM<-ShoeVM,ClothesVM,FurntitureVM as per
OOP rules, or should it be ParentVM containing ItemSelectorVM (for the
UI to select the item type), shoeAdverisementVM (to bind to the shoe
template loaded as a result), FurnitureAdvertisementVM and many more
VMs for every part of the page?
This depends on the complexity of the application and domain.
Let's assume that your UI is doing more than just iterating over an ObservableArray<T> where T could be of type Shoe or Furniture. And let's also assume that ShoeAdvertisementVM is not some decorator of ItemVM<T>.
Now, lets say you have FurnitureAdvertisementVM and ShoeAdverisementVM that look like this:
function FurnitureAdvertisementVM(furniture){
this.items = ko.observableArray(furniture);
this.doSomething = function(){
// doing something
}
}
function ShoeAdverisementVM(shoes){
this.items = ko.observableArray(shoes);
this.doSomething = function(){
// doing something
}
}
var shoeVM = new ShoeAdverisementVM(shoes);
var furnitureVM = new FurnitureAdverisementVM(furniture);
If the two view models are pretty much a copy and paste job and the only difference is the name of the view model and the type of the items that go into the collection then it's probably worth changing the implementation into something generic like this:
function ItemsVM(items){
this.items = ko.observableArray(items);
this.doSomething = function(){
// doing something
}
}
var shoeVM = new ItemsM(shoes);
var furnitureVM = new ItemsVM(furniture);
But if each view model has some very specific properties and functions, for example:
function FurnitureAdvertisementVM(furniture, someOtherDependency){
this.items = ko.observableArray(furniture);
this.doSomething = function(){
// doing something
}
this.propertyIsBoundToATextbox = ko.observable();
this.clickHandlerUsesDependency = function(ctx){
if(ctx.SomeCondition){
someOtherDependency.doSomething();
}
}
}
function ShoeAdverisementVM(shoes){
this.items = ko.observableArray(shoes);
this.doSomething = function(){
// doing something
}
this.propertyIsBoundToACheckbox = ko.observable();
}
Then it it is not as clear cut as making both view models generic.
Roy J commented:
your viewmodels are intended to model the view (the UI)
Which is so true, so if the views have different requirements, i.e. one has 3 checkboxes the other has loads of buttons etc, then you are likely going to need different view models.
If you want to factor out some of common functionality into separate classes then you can:
function ItemService(items){
this.items = ko.observableArray(items);
this.doSomething = function(){
// doing something
}
}
function FurnitureAdvertisementVM(itemService, someOtherDependency){
this.service = itemService;
this.propertyIsBoundToATextbox = ko.observable();
this.clickHandlerUsesDependency = function(ctx){
if(ctx.SomeCondition){
someOtherDependency.doSomething();
}
}
}
function ShoeAdverisementVM(itemService){
this.service = itemService;
this.propertyIsBoundToACheckbox = ko.observable();
}
Related
I have a JavaScript MVC design, implemented using prototypes, where different items may be displayed in different ways as decided by the controller. For example an 'Event' item may be displayed with the views ViewTabs or ViewSimple. Class hierarchy:
ViewBase
- ViewTabs
-- EventViewTabs, which implements EventViewTabs.Validate
- ViewSimple
-- EventViewSimple, which implements EventViewSimple.Validate
Deciding whether to use EventViewTabs or EventViewSimple is done by a EventController. My problem is: I have a Validate method for checking inputs from the Event views, but this method is identical for the EventViewTabs and the EventViewSimple views. Where should I put Validate in order to avoid duplication? I cannot put it in ViewBase, as other items (e.g. User) also inherit from this class.
Seems I need multiple inheritance for this, but is there a smarter way to do it? I have a feeling I'm overlooking something obvious.
You're missing composition. Inheritance isn't the answer to all issues about code reuse to avoid copy-paste programming.
Let's say you've a View base prototype:
function View() { }
If you want this view to support validation, you can inject the validation dependency in the constructor function:
function View(validator) {
this.validator = validator;
}
View.prototype = {}; // A lot of functions here
That is, now any view which inherits View's prototype will have an associated validator. In other words: you don't need to derive two prototypes in your concrete view (you don't need and you can't do it anyway).
In the other hand, in terms of object-oriented programming, it wouldn't make sense to derive from Validator to create a View.
When you say a view has a validator, since you're using has as verb, you're talking about an association (a form of composition). Alternatively, when you say my main screen is as view, we're talking about an inheritance, because a specific view must be also a view, so it needs base view's members to act like a view.
Basically your validator could be tailor-made with the type it has to work with. In UML, it's called composition. I figure out your code as follows:
function Validator {}
Validator.prototype.validate = function(arg) {
//arg is no longer inputs
return true|false; //the ultimate output along with additional information;
}
function EventViewTabsValidator() {}
EventViewTabsValidator.prototype = Object.extend(Validator.prototype); //inheritance
EventViewTabsValidator.prototype.constructor = EventViewTabsValidator; //enforce the constructor to point to your derived type
EventViewTabsValidator.prototype.validate = function() {
var inputs = $('inputs');
var param = 'do some stuff specific to EventViewTabsValidator based on the inputs';
return Validator.prototype.validate.call(this, param); //pass param, not inputs
}
function EventViewSimpleValidator() {}
EventViewSimpleValidator.prototype = Object.extend(Validator.prototype); //inheritance
EventViewSimpleValidator.prototype.constructor = EventViewSimpleValdiator; //enforce the constructor to point to your derived type
EventViewSimpleValidator.prototype.validate = function() {
var inputs = $('inputs');
var param = 'do some stuff specific to EventViewSimpleValidator based on the inputs';
return Validator.prototype.validate.call(this, param); //pass param, not inputs
}
function EventViewTabs() {
this.validator = null; //see init
}
EventViewTabs.prototype.init = function() {
this.validator = new EventViewTabsValidator();
}
function EventViewSimple() {
this.validator = null; //see init
}
EventViewSimple = function() {
this.validator = new EventViewSimpleValidator();
}
Your could abstract up both types to a base EventView, which could expose this.validator.
Your instance of EventController will call:
var simple = new EventViewSimple();
simple.validator.validate();
var tabs = new EventViewTabs();
tabs.validator.validate();
Whatever the EventView instance, they implement their own specific validator that can be called in a generic way.
One approach is to use mixins to add the other behavior (this is the ruby approach, and is also used by react.js and react.rb) You can google for javascript+mixins and find some excellent tutorials like this one: http://raganwald.com/2014/04/10/mixins-forwarding-delegation.html
For your specific case validate (or perhaps validator) would be the mixin.
Why not to do something like this:
ViewBase
ViewBase
-EventValidator, which implements Validate
--ViewTabs
---EventViewTabs
--ViewSimple
---EventViewSimple.
Also consider to use composition over inheritance see this video
So I'm trying to use a join table to display a list of data in my Parse app. The javascript API is similar enough to backbone.js that I'm assuming anyone who knows that could help me. I can't show my actual source code but I think I simple twitter-like "user follows user" scenario can answer my question. So assume I have a join table called "follows" that simply contains its own objectId, the id of each user in the relationship, and some meta-data about the relationship (needing metadata is why I'm using a join table, instead of Parse.Relation). I want to have a view that finds all of the users the current user follows and renders an instance of another view for each case. From what I have so far, that would looks something like this.
In the intialize of the top level view (let's call it AllFollowsView), I would have something like this.
var currentUser = Parse.User.current();
var followsQuery = new Parse.Query(Follows);
followsQuery.equalTo("userId", currentUser.id);
followsQuery.find({
success: function(followsResult){
for (var i = 0; i < followsResult.length; i++){
var view = new OneFollowView({model:followsResult[i]});
this.$("#followed-list").append(view.render().el);
}//for loop
},
error: function(error){
console.log("error finding plans query");
}
});
OneFollowsView is just a view that renders an showing data about the relationship and listens for changes on that particular relationship (mainly change or delete in my case). I understand that by passing in the corresponding model with
var view = new OneFollowView({model:followsResult[i]});
I can print out attributes of that model in the OneFollowsView template like this
<li>You are following a user with the id of <%= _.escape(followedUserId) %></li>
My problem is that this only gives me access to the information stored in the "follows" object. How would I pass in the corresponding user models (or any other models that I can query for the id of) into the template so I can access them in the html in the same way. I would like to be able to run queries in one of the views and then access those models in the html. I know I can add attributes to the object before declaring a new instance of the lower level class with that object as the model, but that doesn't help me because I don't want to save it with new attributes attached.
EDIT: My render function for the top level function is empty at the moment. It's initilize function contains this line to render the template. I guess this should probably be in the render function and then I would call render from initialize.
this.$el.html(_.template($("#all-follows-template").html()));
Here's the render for the lower (individual li) view
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
return this;
this.delegateEvents();
}
From my understanding this just renders the template to el while parsing the model to JSON and then returns to allow chained calls.
The problem here lies in you render method. When you call this.template in your render method. That method, this.template is a template function returned by calling the _.template function. When you call your this.template method, the properties of the object you pass in will be available as instance variables in your template.
In your case you're passing in the JSON of the object. So, the properties of the model become names of variables available in your template. If you want to expose additional variables to the template you have a couple options: 1) Add to the jsonified model's attributes. 2) Send in the model as a top level variable and any additional variables you may want.
// option 1
render: function() {
var templateArgs = _.extend(this.model.toJSON(), { additionalVar: 'new var' });
var content = this.template(templateArgs);
$(this.el).html(content);
this.delegateEvents();
return this;
}
// option 2
render: function() {
var templateArgs = {
followResult: this.model.toJSON(),
additionalVar: 'new var'
};
var content = this.template(templateArgs);
$(this.el).html(content);
return this;
this.delegateEvents();
}
Either option is reasonable. I would probably go with option 2. Which allows you in the template to say something like:
<li> <%= followResult.someProperty %> <%= additionalVar %> </li>
Hope that helps. :)
I'm currently working on an app whose database schema changes frequently. This rapid change creates a big problem for my front-end Angular code which consumes the backend JSON API (which I don't have much control over) via Restangular; take the following code for example:
<ul>
<li ng-repeat="item in items">
<h2>{{item.label}}</h2>
</li>
</ul>
There will be a lot of template tags like {{item.label}} scattered everywhere in the front-end code, so whenever the property name changes from, say "label" to "item_label", I'll need to remember where those tags are and change all of them. Of course, I could do a project wide search and replace, but that's not really ideal from an DRY stand point and it'll also be a maintenance nightmare.
My question is, does Angular (or Restangular) provide a way to map model property names to custom ones like this in Backbone?
That way, I can just have something like this
{
label: model.item_label
}
then next time when the "item_label" is changed to something else, I can just update it in this configuration object and not worry about all the references in the templates.
Thanks.
The idea with angular is that you can do whatever you want with the model. While this doesn't point you in any specific direction it does give you the opportunity to implement it in your own OO manner. Say you have an app that has a data object called ...Task a model for tasks might look like..
function Task(initJson){
this.name = initJson._name || 'New Task';
this.completed = initJson.is_completed || false;
this.doneDateTime = initJson.datetime || null;
}
Task.prototype = {
save: function(){
//do stuff with this and $http.put/post...
}
create: function(){
//do stuff with this and $http.put/post
}
//....etc
}
All of this might be wrapped up in a factory.
myApp.factory('TaskFactory',function($http){
var Tasks = []; //Or {};
//above constructor function...
//other helper methods...etc
return {
instance: Task,
collection: Tasks,
init: function(){} // get all tasks? run them through the constructor (Task), populate collection
};
})
You could then edit properties on your constructor (one place (for each data type), the only place). Although this isn't ideal if your using things like Restangular or $resource as they not equipped to be a large backing store but they just assume the properties that come across the wire, which for large, changing applications can sometimes be difficult to manage.
I ended up going with Restangular's setResponseExtractor config property based on this FAQ answer.
It looks like this:
Restangular.setResponseExtractor(function(response, operation, what, url) {
var newResponse = response;
angular.forEach(newResponse.items, function(item) {
item.label = item.item_label;
}
return newResponse;
}
I'm working my way through learning Knockout JS and have come to a conclusion that I need to have a base view model that is shared across other view models. This base view model needs to contain data and functionality that is common and used across pages in my jQuery Mobile application. For example, each page has a pop-out panel containing site navigation. Each page in the mobile app has the same panel, so I do not want to duplicate functions for do navigation for each page. I'm using a Knockout JS template to construct the panel for each page. The base view model also contains data used on each page. So, I did some research and found this (see this answer):
var ViewModel = function(data) {
BaseViewModel.call(this);
};
ViewModel.prototype = new BaseViewModel();
I implemented the above approach like this:
myApp.BaseViewModel = function ()
{
alert('foo');
var self = this;
self.CurrentPurchaseOrder = ko.observable(new PurchaseOrder());
self.OpenPanel = function (){..};
self.ClosePanel = function (){..};
self.LogOut = function (){..};
self.GoHome = function (){..};
self.CreateNewPurchaseOrderModal = function (){..};
self.LoadInProgressPurchaseOrders = function (){..};
self.LoadSubmittedPurchaseOrders = function (){..};
};
myApp.HomeViewModel = function ()
{
var self = this;
myApp.BaseViewModel.call(self);
self.UserModel = ko.observable(new UserModel());
};
myApp.PurchaseOrderListViewModel = function ()
{
var self = this;
myApp.BaseViewModel.call(self);
self.PurchaseOrderList = ko.observableArray([]);
};
var baseViewModel = new myApp.BaseViewModel();
myApp.HomeViewModel.prototype = baseViewModel;
myApp.PurchaseOrderListViewModel.prototype = baseViewModel;
When I open the mobile app, I'm presented with multiple alert messages each displaying 'foo' (one for each view model extending baseViewModel). I also noticed self.CurrentPurchaseOrder in base view model has different data for each page indicating that each page's view model is not sharing the same base view model.
I removed myApp.BaseViewModel.call(self) from each child view model and reran the mobile app and only receive 1 'foo' alert message. After interacting with the app, self.CurrentPurchaseOrder in base view model has the same data across all pages which is what I need.
So, I'm not sure the purpose of myApp.BaseViewModel.call(self) in each child view model. After removing it in each child view model, all pages shared the same data and each page still had access to the methods in the base class. What's the purpose of myApp.BaseViewModel.call(self)?
Will keep it short: I have the need to build different components for my app. What I'm calling a "component" here is a collection of methods and constructors that repeats itself in many places of my app but is not necessarily exactly the same.
For the sake of an example, consider a pagination component. It is made of a few methods, a view and a template:
var Pagination = function() {
this.View = Backbone.View.extend({});
this.method = function() {};
this.method2 = function() {};
};
// Create instance
var myComponent = new Pagination();
Heavily abstracted code, but gives the idea. Now, I've created a "Component" constructor to aid me in my task. It is fairly simple:
Component = function() {
this.initialize.apply(this, arguments);
};
_.extend(Component.prototype, {
initialize: function() {}
});
Component.extend = Backbone.Model.extend;
This allows me to build multiple instances of the same component with slightly different methods or contents, without having to rebuild the same thing over and over again:
var Pagination = Component.extend({
View: Backbone.View.extend({}),
method: function() {},
method2: function() {}
}};
// Create instance
var myComponent = new Pagination();
So far so good. myComponent has the list of methods as well as the constructor for the View. Here is where the problem lies: for some of my Components's instances I need to extend not only the component itself, but the Backbone constructors inside it as well. Say, for example, that I want to extend (not replace) the View inside my pagination component. That's were everything falls down.
This is the last approach I've had, but the basic idea is: I have components that repeat themselves frequently in my app but with slight differences between them. These components may include methods, backbone constructors and primitive values inside of them. I need to be able to extend these but ALSO be able to extend it's individual parts (Backbone constructors) if the need arises.Anyone has an idea on how to accomplish this?