Large scale KnockoutJS application architecture - javascript

I love KnockoutJS but have been struggling to figure out the best way to build large scale Javascript applications with it.
Right now the way I'm handling the code is by building with a root view model which usually starts at the master page level and then expanding on that. I only ko.applyBindings() on the main view. Here is the example code I have:
var companyNamespace = {};
// Master page. (a.k.a _Layout.cshtml)
(function(masterModule, $, ko, window, document, undefined) {
var private = "test";
masterModule.somePublicMethod = function() {};
masterModule.viewModel = function() {
this.stuff = ko.observable();
};
}(companyNamespace.masterModule = companyNamespace.masterModule || {}, jQuery, ko, window, document));
// Index.cshtml.
(function(subModule, $, ko, window, document, undefined) {
var private = "test";
subModule.somePublicMethod = function() {};
subModule.viewModel = function() {
this.stuff = ko.observable();
};
$(document).ready(function() {
ko.applyBindings(companyNamespace.masterModule);
});
}(companyNamespace.masterModule.subModule = companyNamespace.masterModule.subModule || {}, jQuery, ko, window, document));
I'm just worried since this is a tree structure that if I needed to insert a double master page or something like that, that this would be very cumbersome to re-factor.
Thoughts?
EDIT
I'm aware that you can apply bindings to separate elements to change the scope of the bindings however what if I have nested view models?

I have a rather large knockout.js single page application. (20K+ lines of code currently) that is very easy for anyone to maintain and add additional sections to. I have hundreds of observables and the performance is still great, even on mobile devices like an old iPod touch. It is basically an application that hosts a suite of tools. Here are some insights into the application I use:
1. Only one view model. It keeps things simple IMHO.
The view model handles the basics of any single page application, such as visibility of each page (app), navigation, errors, load and toast dialogs, etc. Example Snippet of View Model: (I separate it out even further js files, but this is to give you an overview of what it looks like)
var vm = {
error:
{
handle: function (error, status)
{
//Handle error for user here
}
},
visibility:
{
set: function (page)
{
//sets visibility for given page
}
},
permissions:
{
permission1: ko.observable(false),
permission2: ko.observable(false)
//if you had page specific permissions, you may consider this global permissions and have a separate permissions section under each app
},
loadDialog:
{
message: ko.observable(''),
show: function (message)
{
//shows a loading dialog to user (set when page starts loading)
},
hide: function()
{
//hides the loading dialog from user (set when page finished loading)
}
},
app1:
{
visible: ko.observable(false),
load: function ()
{
//load html content, set visibility, app specific stuff here
}
},
app2:
{
visible: ko.observable(false),
load: function ()
{
//load html content, set visibility, app specific stuff here
}
}
}
2. All models go into a separate .js files.
I treat models as classes, so all they really do is store variables and have a few basic formatting functions (I try to keep them simple). Example Model:
//Message Class
function Message {
var self = this;
self.id = ko.observable(data.id);
self.subject = ko.observable(data.subject);
self.body = ko.observable(data.body);
self.from = ko.observable(data.from);
}
3. Keep AJAX database calls in their own js files.
Preferably separated by section or "app". For example, your folder tree may be js/database/ with app1.js and app2.js as js files containing your basic create retrieve, update, and delete functions. Example database call:
vm.getMessagesByUserId = function ()
{
$.ajax({
type: "POST",
url: vm.serviceUrl + "GetMessagesByUserId", //Just a default WCF url
data: {}, //userId is stored on server side, no need to pass in one as that could open up a security vulnerability
contentType: "application/json; charset=utf-8",
dataType: "json",
cache: false,
success: function (data, success, xhr)
{
vm.messaging.sent.messagesLoaded(true);
for (var i = 0; i < data.messages.length; i++)
{
var message = new Message({
id: data.messages[i].id,
subject: data.messages[i].subject,
from: data.messages[i].from,
body: data.messages[i].body
});
vm.messaging.sent.messages.push(message);
}
},
error: function (jqXHR)
{
vm.error.handle(jqXHR.getResponseHeader("error"), jqXHR.status);
}
});
return true;
};
4. Merge and Minify all your model, view model, and database js files into one.
I use the Visual Studio "Web Essentials" extension that allows you to create "bundled" js files. (Select js files, right click on them and go to Web Essentials --> Create Javascript Bundle File) My Bundle file is setup like so:
<?xml version="1.0" encoding="utf-8"?>
<bundle minify="true" runOnBuild="true">
<!--The order of the <file> elements determines the order of them when bundled.-->
<!-- Begin JS Bundling-->
<file>js/header.js</file>
<!-- Models -->
<!-- App1 -->
<file>js/models/app1/class1.js</file>
<file>js/models/app1/class2.js</file>
<!-- App2 -->
<file>js/models/app2/class1.js</file>
<file>js/models/app2/class2.js</file>
<!-- View Models -->
<file>js/viewModel.js</file>
<!-- Database -->
<file>js/database/app1.js</file>
<file>js/database/app2.js</file>
<!-- End JS Bundling -->
<file>js/footer.js</file>
</bundle>
The header.js and footer.js are just a wrapper for the document ready function:
header.js:
//put all views and view models in this
$(document).ready(function()
{
footer.js:
//ends the jquery on document ready function
});
5. Separate your HTML content.
Don't keep one big monstrous html file that is hard to navigate through. You can easily fall into this trap with knockout because of the binding of knockout and the statelessness of the HTTP protocol. However, I use two options for separation depending on whether i view the piece as being accessed by a lot by users or not:
Server-side includes: (just a pointer to another html file. I use this if I feel this piece of the app is used a lot by users, yet I want to keep it separate)
<!-- Begin Messaging -->
<!--#include virtual="Content/messaging.html" -->
<!-- End Messaging -->
You don't want to use server-side includes too much, otherwise the amount of HTML the user will have to load each time they visit the page will become rather large. With that said, this is by far the easiest solution to separate your html, yet keep your knockout binding in place.
Load HTML content async: (I use this if the given piece of the app is used less frequent by users)
I use the jQuery load function to accomplish this:
// #messaging is a div that wraps all the html of the messaging section of the app
$('#messaging').load('Content/messaging.html', function ()
{
ko.applyBindings(vm, $(this)[0]); //grabs any ko bindings from that html page and applies it to our current view model
});
6. Keep the visibility of your pages/apps manageable
Showing and hiding different sections of your knockout.js application can easily go crazy with tons of lines of code that is hard to manage and remember because you are having to set so many different on and off switches. First, I keep each page or app in its own "div" (and in its own html file for separation). Example HTML:
<!-- Begin App 1 -->
<div data-bind="visible: app1.visible()">
<!-- Main app functionality here (perhaps splash screen, load, or whatever -->
</div>
<div data-bind="visible: app1.section1.visible()">
<!-- A branch off of app1 -->
</div>
<div data-bind="visible: app1.section2.visible()">
<!-- Another branch off of app1 -->
</div>
<!-- End App 1 -->
<!-- Begin App 2 -->
<div data-bind="visible: app2.visible()">
<!-- Main app functionality here (perhaps splash screen, load, or whatever -->
</div>
<!-- End App 2 -->
Second, I would have a visibility function similar to this that sets the visibility for all content on your site: (it also handles my navigation as well in a sub function)
vm.visibility:
{
set: function (page)
{
vm.app1.visible(page === "app1");
vm.app1.section1.visible(page === "app1section1");
vm.app1.section2.visible(page === "app1section2");
vm.app2.visible(page === "app2");
}
};
Then just call the app or page's load function:
<button data-bind="click: app1.load">Load App 1</button>
Which would have this function in it:
vm.visibility.set("app1");
That should cover the basics of a large single page application. There are probably better solutions out there than what I presented, but this isn't a bad way of doing it. Multiple developers can easily work on different sections of the application without conflict with version control and what not.

I like to set up my view models using prototypal inheritance. Like you I have a "master" view model. That view model contains instances of other view models or observable arrays of view models from there you can use the "foreach" and "with" bindings to in your markup. Inside your "foreach" and "with" bindings you can use the $data, $parent, $parents and $root binding contexts to reference your parent view models.
Here are the relevant articles in the KO documentation.
foreach binding
with binding
binding context
If you want I can throw together a fiddle. Let me know.

Related

Using Ezoic (or Adsense) with VueJS v2

I have a new version of my site which uses VueJS v2 (the previous one didn't). The main code is placed inside <div id="app"></div> and Vue is initiated. The issue is that I partner with an advertising company called Ezoic that injects ads through using AI onto the page, but these ads aren't displaying properly. I believe it is related to these errors:
https://pagead2.googlesyndication.com/pagead/show_ads.js
show_ads.js:53 Failed to execute 'write' on 'Document': It isn't
possible to write into a document from an asynchronously-loaded
external script unless it is explicitly opened.
Ezoic works with Google Ad Exchange, so I believe it is the above line that's related to the issue.
I'm wondering, is there any way in which I could make my application compatible with Ezoic/Adsense? I thought about having Vue on the page only where needed, rather than the entire page (<div id="app"></div> goes from the start of body to the end of body), but this would mean I need multiple Vue instances running as I have components at the top (search box) and also throughout the pages.
I have no access to the code that Ezoic inject onto the page as this is done on their end (my site uses their DNS and they modify the response before sending to the visitor, to include the ad code). Ezoic team is also having a look into this issue presently but any information I could pass along could be helpful!
At the request of Dynamic Remo I am submitting an answer for Ezoic's standalone implementation that is compatible with Vue.
I will however preface this with they absolutely hate it when you install it this way and essentially refuse to support it. - with that said you have way more control over placement
The Solution:
First add the following script tag somewhere outside or your custom defined root element.
<script type="text/javascript" src="//www.ezojs.com/ezoic/sa.min.js" async=""></script>
In your vue component you will need to create all your placeholder elements with a id of "ezoic-pub-ad-placeholder-xx" where xx is replaced by the actual id found in your enzoic dashboard. Dynamic creation does work as long as there is a matching id in ezoic so you have two options:
Dynamic:
<div v-for="placeholderId in ezoicArray" :id="'ezoic-pub-ad-placeholder-' + placeholderId" class="ezoic"></div>
Standard:
<div id="ezoic-pub-ad-placeholder-102" class="ezoic"></div>
To actually display ads in your placeholders you can use this function I wrote. Just call it when the component is mounted.
ezoic(placeholderList) {
if (window.ezstandalone !== undefined) {
window.ezoicPlaceholderArray = window.ezoicPlaceholderArray || [];
// Add Placeholders to Array
placeholderList.forEach((placeholder) => {
this.addPlaceholderOnce(window.ezoicPlaceholderArray, placeholder);
});
// Enable Once - Refresh on Change
window.ezoicRefreshed = false;
window.ezoicEnabled = window.ezoicEnabled || false;
// Next Tick Ensures All Enzoic Blocks Are Loaded
this.$nextTick(() => {
// On First Load We Must Enable
if (!window.ezoicEnabled) {
window.ezstandalone.define(window.ezoicPlaceholderArray);
window.ezoicPlaceholderArray = null;
console.log('ezoic defined and array reset');
window.ezstandalone.enable();
console.log('ezoic enabled');
window.ezstandalone.display();
console.log('ezoic displayed');
window.ezoicEnabled = true;
window.ezoicRefreshed = true;
}
// On Refresh We Have To Destroy & Refresh
if (!window.ezoicRefreshed) {
window.ezstandalone.destroy();
console.log('ezoic destroyed');
window.ezstandalone.define(window.ezoicPlaceholderArray);
window.ezoicPlaceholderArray = null;
console.log('ezoic refresh defined and array reset');
window.ezstandalone.refresh();
console.log('ezoic refreshed');
window.ezoicRefreshed = true;
}
});
} else {
console.log('Error: Missing Ezoic Standalone');
}
},
addPlaceholderOnce(array, placeholder) {
if (!array.includes(placeholder)) {
array.push(parseInt(placeholder));
console.log('ezoic-pub-ad-placeholder-' + placeholder + ' - Created');
}
},
If you need to change your placeholder setup for different reasons as I do simply add window.ezoicRefreshed = false; to the appropriate lifecycle hook. In my case I have it in beforeUnmount because each route gets a custom placeholder list.
Hopefully this helps!

Deleting a view and onclick opening it again, as if it is new - SAPUI5

I have a multiple View setup, a main View and then modules who are opened on button click after certain actions.
Right now the view is reset by me whenever its opened, but what I want to accomplish is, when the view is left, that its gone from the core and its initiated again as if it is opened for the first time.
I am not certain how, and I tried multiple things, none worked as I intended (.destroy(), .removeAllContent(), ... ).
Did I miss a function or is there a way to accomplish that?
Some code:
index.js (how the View is instantiated the first time, its not called a second time)
module.exports.Partner = function(place) {
View = require('./app/js/suche.view');
Bearbeiten = require('./app/js/bearbeiten.view');
var ctrl = View.getController();
ctrl.setPlace(ctrl, place);
ctrl.setEditPlace(ctrl, place);
ctrl.setCreatePlace(ctrl, place);
sap.ui.getCore().byId('suche').placeAt(place);
sap.ui.getCore().byId('suche').byId('searchBtn').attachPress(ctrl.nummerSearch, ctrl);
};
controller (how the View and the index.js is called)
handlePartnerSuche : function(){
this.hideView(); //this is the main view
var p = require('bit-js-business-partner');
var partner_view = p.Partner('content');
p.setCallbackForSchliessen(this, this.callbackForSchliessen);
},
controller (callback function)
callbackForSchliessen: function(){
this.showView();
},
The second View (how its closed)
handleSchliessen : function () {
var p = this.getView('suche');
p.setVisible(false);
this.callbackMethodSchliessen.call(this.callbackCtrlSchliessen);
},
You mentioned you cannot use the Router mechanism due to company restrictions -- am really curious to know what these restriction are then ;-) -- and toggle the visibility properties of the respective views instead.
In that case, I would trigger the OData service in the method where you set the view's visibility to visible, and (re)bind the ODataModel to that view.
From a performance perspective, I would not advise to destroy views from the core

Umbraco 7: How can I refresh the tree view in my custom section?

I have created a custom section in umbraco to manage some data in an SQL database.
I can edit items OK but when adding I need to refresh the page to see my new row in the custom tree on the left.
How can I cause a refresh of my custom tree using AngularJS? My tree is called "clients".
I have tried debugging the code and looking at the source to find the event but I can't seem to work out how to do it.
Is there a method I can call on the umbTreeDirective somehow? Or an event to subscribe to?
I am fairly new to AngularJS and am struggling a little.
You're looking for the navigationService.
This line is example of a syncTree call:
navigationService.syncTree({ tree: 'clients', path: content.path, forceReload: false, activate: true });
Here's a contrived, spaghetti promised but full example:
angular.module("umbraco")
.directive('nowplaying', ['navigationService', 'contentResource', 'contentEditingHelper', function (navigationService, contentResource, contentEditingHelper) {
//spaghetti example to create new document
contentResource.getScaffold(parentId, alias)
.then(function (scaffold) {
var myDoc = scaffold;
myDoc.name = name;
//we have minimum to publish
contentResource.publish(myDoc, true, [''])
.then(function (content) {
$scope.newlyCreatedNode = content;
//Sync ('refresh') the tree!
navigationService.syncTree({ tree: 'clients', path: content.path, forceReload: false, activate: true });
});
});
}]);
All of the Belle documentation lives here. -I'm not sure it's actively maintained, i can say for certain that one or two signatures have changed since it was first posted. That aside, it's the best resource i know of to interact with all the umbraco exposed modules and services.

Combining jQuery UI and HotTowel

I'm getting back into web development a bit after having been kind of out of it for the past 10 years or so, and I'm overwhelmed by all the new technologies that I'm having to catch up with, ASP.NET, MVC, jQuery, SPA, Knockout, etc. I don't know the second thing about jQuery and my experience with ASP.NET is very limited. I have a little familiarity with ASP.NET WebForms, but MVC (and the rest) is totally new to me.
After seeing how many technologies there were, and not knowing which route to explore in my new project, I saw that Hot Towel seems to be a template that combines all the latest stuff into one nice package, so I decided to get the Hot Towel template and start an ASP.NET MVC4 SPA project with it.
Now I'm trying to integrate with our in-house UI framework (which has been developing without me over the past few years). I decided to try to update the Details page in the Hot Towel template to have some content. I added a simple <span>, and all's well and good. But if I try to add what I understand to be a jQuery-widget-based component (?), I get nothing. Even for the simplest test of adding content via jQuery, I get nothing:
<section>
<h2 class="page-title" data-bind="text: title"></h2>
<span>Test this</span>
<div id="testDiv"></div>
<script type="text/javascript">
$("#testDiv").append("Testing");
</script>
</section>
I see the span, but not the modified div. And I can't see any of this content in the source ("View source") or the IE9 console (not surprising given the nature of SPA, but what should I do about it?). And the Visual Studio Page Inspector seems to be totally useless (can't get past the splash screen).
What is the proper method of adding elements to the UI under the HotTowel/jQuery/MVC/SPA/KockoutJS/Breeze/Durandal model? All these new frameworks are driving my crazy.
Edit some more details: The jQuery stuff works fine when I move it to the main page of the SPA, but when I have it on the Details "page" it doesn't work. I suspect it has something to do with the SPA nature of this application and how the content of alternate views are delivered not as an entire page, but as updated content for the main page.
Edit after further investigation, I have discovered the existence of a view model named "detail" which is probably related to this detail view code I have posted. This is the code from the view model:
define(['services/logger'], function (logger) {
var title = 'Details';
var vm = {
activate: activate,
title: title
};
return vm;
//#region Internal Methods
function activate() {
logger.log(title + ' View Activated', null, title, true);
return true;
}
//#endregion
});
The script is probably executing but cannot find the div. To correct manipulate div put your jquery code to in a function and trigger that function using attached/compositionComplete callback for duranadal 2.0 or viewAttached callback for durandal 1.x
1.x link - https://github.com/BlueSpire/Durandal/blob/master/docs/1.2/Composition.html.md#view-attached
2.0 link - http://durandaljs.com/documentation/Hooking-Lifecycle-Callbacks/
// in your detail view model, if using durandal 1.x
define(['services/logger'], function (logger) {
var title = 'Details';
var vm = {
activate: activate,
title: title,
viewAttached : function(view){
// view is the root element of your detail view and is passed in
// by durandal
$(view).append("Testing");
}
};
return vm;
//#region Internal Methods
function activate() {
logger.log(title + ' View Activated', null, title, true);
return true;
}
//#endregion
});
// in your detail view model, if using durandal 2.0, you have two options
define(['services/logger'], function (logger) {
var title = 'Details';
var vm = {
activate: activate,
title: title,
attached : function(view, parent){
// view is the root element of your detail view
// and is passed in by durandal
$(view).append("Testing first method");
},
compositionComplete: function(view, parent){
// view is the root element of your detail view
// and is passed in by durandal
$(view).append("Testing second method");
}
};
return vm;
//#region Internal Methods
function activate() {
logger.log(title + ' View Activated', null, title, true);
return true;
}
//#endregion
});

Code from multiple view instances causing conflicts

I'm porting a web service into a single-page webapp with Backbone. There is a basic layout consisting on a header, an empty div#content where I'm attaching the views and a footer.
Every route creates the corresponding view and attachtes it to div#content replacing the view that was rendered before with the new one.
I'm using require.js to load the backbone app and it's dependencies.
All Backbone code is pretty small, only one file as I'm only using a router and a view.
This AMD module depends on a util.js file exporting functions that are used in the views.
After a view is created and rendered, It executes the utilities (jquery stuff, ajax, etc) it needs from util.js.
The problem is that when I render a view, it's utilities get called, and when I navigate to another route, and a new view is created, the new view's utilities are called now, but the older view's utilities are still running.
At some point, I have utilities from like five views running altogether, causing conflicts sometimes.
It's clear than my approach is not good enough, as I should have a way to stop/start utilities functions as some kind of services.
I'll paste relevant code that shows my current approach:
require(["utilities"], function(util) {
...
Application.view.template = Backbone.View.extend({
el: "div#content",
initialize: function(){
this.render();
},
render: function(){
var that = this;
// ajax request to html
getTemplate(this.options.template, {
success: function(template) {
var parsedTemplate = _.template( template, that.options.templateOptions || {});
that.$el.html(parsedTemplate);
// execute corresponding utilities
if(that.options.onReady) {
that.options.onReady();
}
},
error: function(template) {
that.$el.html(template);
}
})
}
});
...
Application.router.on('route:requestPayment', function(actions) {
var params = { template: 'request-payment', onReady: util.requestPayment };
var view = new Application.view.template(params);
});
...
});
util.requestPayment consist of a function having all stuff needed to make template work.
I'm confused about how should I handle this issue. I hope I was clear, and any suggestions or help will be appreciated.
EDIT: utilities.js snippet:
...
var textareaCounter = function() {
$('#requestMessage').bind('input propertychange', function() {
var textarea_length = 40 - $(this).val().length;
if(textarea_length === 40 || textarea_length < 0) {
$('#message-counter').addClass('error').removeClass('valid');
$("#submitForm").attr('disabled', 'disabled');
}
else if(textarea_length < 40 && textarea_length > 0) {
$('#message-counter').removeClass('error');
$("#submitForm").removeAttr('disabled');
}
$('#message-counter').text(textarea_length);
});
}
...
var utilities = utilities || {};
...
utilities.requestPayment = function() {
textareaCounter();
initForm();
preventCatching();
requestPaymentCalcFallback();
};
...
return utilities;
...
I would suggest that you should store reference to the currently active view somewhere in your app.
You create a new view here :
var view = new Application.view.template(params);
but you have no access to this variable afterwards. So it exists but you can't stop/delete/get rid of it.
What we normally do is to have a Parent App class which initializes the whole app and manages everything. Your every module in requirejs would be depenedent on it. When a new route is navigating, you ask the Parent App class to change the view. It will delete the old view, create a new one, populate div#content and then store the reference of it.
I think when you delete the old view, all the utilities will stop responding to it.
If you still have the issue with events being called, then you might need to use stopListening event binders before deleting the view reference.

Categories