I am having some issues trying to work out what is going ok with MVC SPA and Knockout.
When you create a new project some files are created for knockout.js as examples, but I am struggling to understand what is going on.
Primarily the issue is with the app.viewmodel.js and the function AddViewModel.
Here is some code which I will attempt to breakdown:
self.addViewModel = function (options) {
var viewItem = {},
navigator;
// Example options
//{
// name: "Home",
// bindingMemberName: "home",
// factory: HomeViewModel
//}
// Add view to AppViewModel.Views enum (for example, app.Views.Home).
self.Views[options.name] = viewItem; // Don't really get this, seems to add a blank object to app.Views.Home
// Add binding member to AppViewModel (for example, app.home);
self[options.bindingMemberName] = ko.computed(function () {
//if (self.view() !== viewItem) {
// console.log(self.view()); // returns {}
// console.log(viewItem); // returns {}
// return null; // should never hit this?
//}
return new options.factory(self, dataModel); // This adds our ViewModel to app.home, app.login, etc
});
// This checks to see if we have defined a navigatorFactory in our viewmodel (AddViewModel)
if (typeof (options.navigatorFactory) !== "undefined") {
navigator = options.navigatorFactory(self, dataModel);
} else {
navigator = function () {
console.log(viewItem);
self.view(viewItem);
};
}
// Add navigation member to AppViewModel (for example, app.NavigateToHome());
self["navigateTo" + options.name] = navigator;
};
ok, so let's start. First of all we declare 2 variables:
var viewItem = {},
navigator;
viewItem is set as a blank object and navigator is undefined.
The first thing we do, is set self.Views[options.name] to our viewItem, so in my understanding, this would mean:
self.Views.Home = {}
If we look at the declaration in app.viewmodel.js self.Views looks like this:
self.Views = {
Loading: {} // Other views are added dynamically by app.addViewModel(...).
};
So in here there is already a view called Loading. So I am confused as to what is actually happening here.
The next bit of code creates a function:
self[options.bindingMemberName] = ko.computed(function () {
return new options.factory(self, dataModel);
});
This is a lot easier to understand. It basically takes our ViewModel and adds it to a function under the name of self.home (or whatever the bindingMemberName of our ViewModel is.
This next piece is what confuses me:
if (typeof (options.navigatorFactory) !== "undefined") {
navigator = options.navigatorFactory(self, dataModel);
} else {
navigator = function () {
console.log(viewItem);
self.view(viewItem);
};
}
// Add navigation member to AppViewModel (for example, app.NavigateToHome());
self["navigateTo" + options.name] = navigator;
If I strip this down, it basically says if we define a navigatorFactory, then the navigator (which is currently undefined!) is equal to our navigatorFactory. That bit is easy.
It's the next bit I don't get.
It says, else, the navigator is a function that returns our self.view(viewItem) (remember that viewItem is just a blank object.
Then we set self["navigateTo" + options.name] = navigator.
So in english, this looks like it is saying, get our blank viewItem, assign it to self.view for every ViewModel we add. Then assign a function returning our self.view(viewItem) to our navigator variable (which is currently undefined) and assign this to our self.naviateToHome() (or whatever).
So to me, that looks like self.navigateToHome(), self.navigateToLogin(), self.navigateToTimbucktoo() would all return the same function with the same self.view.
So, can anyone explain to me what is actually happening?
Update 1
So, I have figured some things out. First things first, the navigator is setting the current view, so basically self.Views looks like this after all the models are added:
self.Views = {
Loading: { },
Home: { },
Login: { }
}
So even though self.view() returns an empty object, it isn't the same as the viewItem because it is stored with the name into self.Views.
So, the navigator is actually applying the viewItem to self.views.
I tested this out by changing the viewItem to this:
var viewItem = { options.name }
and sure enough, self.Views looked liked this:
self.Views = {
Loading: { },
Home: { name: "Home" },
Login: { name: "Login" }
}
so when we set self.view using our navigator, the function is called (app.home for example) and it runs the code to return our factory or null if it isn't the current view.
Related
I have a global object variable set called _storage. It looks like this:
_storage = {
test : {
}
};
When a user sends a request to add their info to the "test" object inside of storage, it's supposed to set _storage.test[1] = true; for example.
After it sets that, I did this to see what the variable looked like:
console.log(_storage.test); and it outputted the correct response:
{ '1' : true }
All seems perfect, until the user sends a request to get all the current items in the test object. This is where the problem is. The variable seems to empty itself and go back to an empty object {}.
The user sends the request to see what's inside of test after setting the data in test, so it can't be an issue caused by the user requesting to see the data before even setting it. This is done inside two separate modules, one sets the data and another one gets the data (the two modules aren't just for that only, obviously) but the _storage variable is global and shouldn't be affected by two separate modules.
Here's an example of how it goes down:
User sends request to set 1 = true inside of _storage.test, making _storage.test[1] = true;
_storage.test is now { test : { '1' : true } } according to console.log
User sends request to view all data from _storage.test
_storage.test is {} according to console.log, as if it isn't staying set
Here's the code that I use to Get, Set, and Remove the data:
exports.GetUserData = function () {
return global._storage.test;
};
exports.RemoveUserData = function (uid) {
delete global._storage.test[Number(uid)];
};
exports.AddUserData = function (uid) {
global._storage.test[Number(uid)] = true;
};
Ok, so I think you're not showing all your code, but here's what I did which worked:
// index.js
'use strict';
global._storage = global._storage || {};
global._storage.test = global._storage.test || {};
exports.GetUserData = function () {
return global._storage.test;
};
exports.RemoveUserData = function (uid) {
delete global._storage.test[Number(uid)];
};
exports.AddUserData = function (uid) {
global._storage.test[Number(uid)] = true;
};
// other.js
'use strict';
global._storage = global._storage || {};
global._storage.test = global._storage.test || {};
exports.GetUserData = function(){
return global._storage.test;
}
And then I ran this from my repl:
> var it = require('./');
undefined
> var other = require('./other');
undefined
> it.AddUserData('1234');
undefined
> it.GetUserData();
{ '1234': true }
> other.GetUserData();
{ '1234': true }
>
So that works as designed. Thing is this... I've been working with node since 2009, and I've never felt the need to use global variables. I doubt your app needs it. So even though this'll work for you, I recommend you think of another way to do it.
We are using the Page Object pattern to organize our internal AngularJS application tests.
Here is an example page object we have:
var LoginPage = function () {
this.username = element(by.id("username"));
this.password = element(by.id("password"));
this.loginButton = element(by.id("submit"));
}
module.exports = LoginPage;
In a single-browser test, it is quite clear how to use it:
var LoginPage = require("./../po/login.po.js");
describe("Login functionality", function () {
var scope = {};
beforeEach(function () {
browser.get("/#login");
scope.page = new LoginPage();
});
it("should successfully log in a user", function () {
scope.page.username.clear();
scope.page.username.sendKeys(login);
scope.page.password.sendKeys(password);
scope.page.loginButton.click();
// assert we are logged in
});
});
But, when it comes to a test when multiple browsers are instantiated and there is the need to switch between them in a single test, it is becoming unclear how to use the same page object with multiple browsers:
describe("Login functionality", function () {
var scope = {};
beforeEach(function () {
browser.get("/#login");
scope.page = new LoginPage();
});
it("should warn there is an opened session", function () {
scope.page.username.clear();
scope.page.username.sendKeys(login);
scope.page.password.sendKeys(password);
scope.page.loginButton.click();
// assert we are logged in
// fire up a different browser and log in
var browser2 = browser.forkNewDriverInstance();
// the problem is here - scope.page.username.clear() would be applied to the main "browser"
});
});
Problem:
After we forked a new browser, how can we use the same Page Object fields and functions, but applied to a newly instantiated browser (browser2 in this case)?
In other words, all element() calls here would be applied to browser, but needed to be applied to browser2. How can we switch the context?
Thoughts:
one possible approach here would be to redefine the global element = browser2.element temporarily while being in the context of browser2. The problem with this approach is that we also have browser.wait() calls inside the page object functions. This means that browser = browser2 should be also set. In this case, we would need to remember the browser global object in a temp variable and restore it once we switch back to the main browser context..
another possible approach would be to pass the browser instance into the page object, something like:
var LoginPage = function (browserInstance) {
browser = browserInstance ? browserInstance : browser;
var element = browser.element;
// ...
}
but this would probably require to change every page object we have..
Hope the question is clear - let me know if it needs clarification.
Maybe you could write few functions to make the the browser registration/start/switch smoother. (Basically it is your first option with some support.)
For example:
var browserRegistry = [];
function openNewBrowser(){
if(typeof browserRegistry[0] == 'undefined'){
browseRegistry[0] = {
browser: browser,
element: element,
$: $,
$$: $$,
... whatever else you need.
}
}
var tmp = browser.forkNewDriverInstance();
var id = browserRegistry.length;
browseRegistry[id] = {
browser: tmp,
element: tmp.element,
$: tmp.$,
$$: tmp.$$,
... whatever else you need.
}
switchToBrowserContext(id);
return id;
}
function switchToBrowserContext(id){
browser=browseRegistry[id].browser;
element=browseRegistry[id].element;
$=browseRegistry[id].$;
$$=browseRegistry[id].$$;
}
And you use it this way in your example:
describe("Login functionality", function () {
var scope = {};
beforeEach(function () {
browser.get("/#login");
scope.page1 = new LoginPage();
openNewBrowser();
browser.get("/#login");
scope.page2 = new LoginPage();
});
it("should warn there is an opened session", function () {
scope.page1.username.clear();
scope.page1.username.sendKeys(login);
scope.page1.password.sendKeys(password);
scope.page1.loginButton.click();
scope.page2.username.clear();
scope.page2.username.sendKeys(login);
scope.page2.password.sendKeys(password);
scope.page2.loginButton.click();
});
});
So you can leave your page objects as they are.
To be honest I think your second approach is cleaner...
Using global variables can bite back later.
But if you don't want to change your POs, this can also work.
(I did not test it... sorry for the likely typos/errors.)
(You can place the support functions to your protractor conf's onprepare section for example.)
Look at my solution. I simplified example, but we are using this approach in current project. My app has pages for both user permissions types, and i need to do some complex actions same time in both browsers. I hope this might show you some new, better way!
"use strict";
//In config, you should declare global browser roles. I only have 2 roles - so i make 2 global instances
//Somewhere in onPrepare() function
global.admin = browser;
admin.admin = true;
global.guest = browser.forkNewDriverInstance();
guest.guest = true;
//Notice that default browser will be 'admin' example:
// let someElement = $('someElement'); // this will be tried to be found in admin browser.
class BasePage {
//Other shared logic also can be added here.
constructor (browser = admin) {
//Simplified example
this._browser = browser
}
}
class HomePage extends BasePage {
//You will not directly create this object. Instead you should use .getPageFor(browser)
constructor(browser) {
super(browser);
this.rightToolbar = ToolbarFragment.getFragmentFor(this._browser);
this.chat = ChatFragment.getFragmentFor(this._browser);
this.someOtherNiceButton = this._browser.$('button.menu');
}
//This function relies on params that we have patched for browser instances in onPrepare();
static getPageFor(browser) {
if (browser.guest) return new GuestHomePage(browser);
else if (browser.admin) return new AdminHomePage(browser);
}
openProfileMenu() {
let menu = ProfileMenuFragment.getFragmentFor(this._browser);
this.someOtherNiceButton.click();
return menu;
}
}
class GuestHomePage extends RoomPage {
constructor(browser) {
super(browser);
}
//Some feature that is only available for guest
login() {
// will be 'guest' browser in this case.
this._browser.$('input.login').sendKeys('sdkfj'); //blabla
this._browser.$('input.pass').sendKeys('2345'); //blabla
this._browser.$('button.login').click();
}
}
class AdminHomePage extends RoomPage {
constructor(browser) {
super(browser);
}
acceptGuest() {
let acceptGuestButton = this._browser.$('.request-admission .control-btn.admit-user');
this._browser.wait(EC.elementToBeClickable(acceptGuestButton), 10000,
'Admin should be able to see and click accept guest button. ' +
'Make sure that guest is currently trying to connect to the page');
acceptGuestButton.click();
//Calling browser directly since we need to do complex action. Just example.
guest.wait(EC.visibilityOf(guest.$('.central-content')), 10000, 'Guest should be dropped to the page');
}
}
//Then in your tests
let guestHomePage = HomePage.getPageFor(guest);
guestHomePage.login();
let adminHomePage = HomePage.getPageFor(admin);
adminHomePage.acceptGuest();
adminHomePage.openProfileMenu();
guestHomePage.openProfileMenu();
I've been working on writing a custom jquery plugin for one of my web applications but I've been running into a strange error, I think it's due to my unfamiliarity with object-oriented programming.
The bug that I've been running into comes when I try to run the $(".list-group").updateList('template', 'some template') twice, the first time it works just fine, but the second time I run the same command, I get an object is not a function error. Here's the plugin code:
(function($){
defaultOptions = {
defaultId: 'selective_update_',
listSelector: 'li'
};
function UpdateList(item, options) {
this.options = $.extend(defaultOptions, options);
this.item = $(item);
this.init();
console.log(this.options);
}
UpdateList.prototype = {
init: function() {
console.log('initiation');
},
template: function(template) {
// this line is where the errors come
this.template = template;
},
update: function(newArray) {
//update code is here
// I can run this multiple times in a row without it breaking
}
}
// jQuery plugin interface
$.fn.updateList = function(opt) {
// slice arguments to leave only arguments after function name
var args = Array.prototype.slice.call(arguments, 1);
return this.each(function() {
var item = $(this), instance = item.data('UpdateList');
if(!instance) {
// create plugin instance and save it in data
item.data('UpdateList', new UpdateList(this, opt));
} else {
// if instance already created call method
if(typeof opt === 'string') {
instance[opt](args);
}
}
});
}
}(jQuery));
One thing I did notice when I went to access this.template - It was in an array so I had to call this.template[0] to get the string...I don't know why it's doing that, but I suspect it has to do with the error I'm getting. Maybe it can assign the string the first time, but not the next? Any help would be appreciated!
Thanks :)
this.template = template
Is in fact your problem, as you are overwriting the function that is set on the instance. You end up overwriting it to your args array as you pass that as your argument to the initial template function. It basically will do this:
this.template = ["some template"];
Thus the next time instance[opt](args) runs it will try to execute that array as if it were a function and hence get the not a function error.
JSFiddle
I've been creating some tests for my Dojo widget to check that boolean flags are being set correctly. However, I've found that since I have altered my constructor to pass in an object, previously ran tests seem to affect the subsequent tests.
I've tried destroying the widget in the tear down methods, but whatever I seem to do, the value persists.
Can anyone suggest what I might be doing wrong?
My widget code:
var showControls = true;
return declare([WidgetBase, TemplatedMixin, _WidgetsInTemplateMixin], {
templateString: template,
constructor: function (params) {
this.showControls = (typeof params.showControls === "undefined" || typeof params.showControls != "boolean") ? this.showControls : params.showControls;
}
});
My test class is:
var customWidget;
doh.register("Test controls", [
{
name: "Test controls are not visible when set in constructor",
runTest: function() {
var params = { showControls: false };
customWidget = new CustomWidget(params);
doh.assertFalse(customWidget.getShowControls());
}
},
{
name: "Test controls are visible when set in constructor with string instead of boolean",
runTest: function() {
var params = { showControls: "wrong" };
customWidget= new CustomWidget(params);
doh.assertTrue(customWidget.getShowControls());
}
}
]);
So, the first test passes, as showControls is set to false, however the 2nd test attempts to create a new instance, in which the constructor will check that the value is a boolean. When I debug this however, it thinks showControls starts out as 'false', not true.
Any clues?!
Thanks
dijit/_WidgetBase has a mechanism of mixing in constructor parameters and it is the reason of the behavior you described. One of the possible solutions is to define a custom setter as a method _set[PropertyName]Attr:
var defaults = {
showControls: true
}
var CustomWidget = declare([_WidgetBase, _TemplatedMixin], {
templateString: "<div></div>",
constructor: function(params) {
declare.safeMixin(this, defaults);
},
_setShowControlsAttr: function(value) {
this.showControls = (typeof value === "boolean") ? value : defaults.showControls;
}
});
See it in action: http://jsfiddle.net/phusick/wrBHp/
I would suggest you list any members of your widget, if you do not, things passed into the constructor may not be properly recognised. It seems you want to use this.showControls, so you should have a showControls member. like this :
return declare([WidgetBase, TemplatedMixin, _WidgetsInTemplateMixin], {
templateString: template,
showControls: true, // default value
constructor: function (params) {
// no further action, params are automatically mixed in already
}
});
Be careful when listing members, dojo interprets arrays and objects as class members (like static in Java, AFAIK they're attached to the prototype) so if you want each object to have e.g., a separate array of values, list it as null and initialize in your constructor.
I have a "master" view, a layout if you will, that loads other views through the loadView method. My problem is that these view classes perform some initialisation logic (in initialize) that can only be performed once (to do with templating). If, however, I try and instantiate these classes more than once, I get an error symptomatic of calling initialize on the same instance.
I have tried, in the console, instantiating them separately by loading the class and creating two new instances using var x = new MyViewClass(); but each time the first one instantiates and the second one fails because of this error caused by the templates already being initialised.
This really shouldn't be happening, but I cannot for the life of me see what is causing the problem.
The layout's loading code is below.
loadView: function(name, bootstrap_function) {
this.unloadView();
var _class = require('View/'+name), // Let's load the view file
pretty = name.replace('/', ''), // Prettify the name by removing slashes (should end up with some camelcased niceness)
bs_name = '__bootstrap'+pretty, // Generate the name of the bootstrap function
view = new _class(); // Pass the event aggregator in
// If there is a bootstrap function, bootstrap
if(typeOf(bootstrap_function) == 'function') { // Check if one has been passed in
bootstrap_function.call(this, view); // Bootstrap: function(AppView, LoadedView)
}
this._loaded = view; // Store the view in _loaded
// Now that we have a view to play with
// we should insert it into our container object
view.$el.appendTo(this.$container);
// And render!
view.render();
},
unloadView: function() {
if(this._loaded !== null) {
this._loaded.remove();
this._loaded.unbind();
this._loaded = null;
}
}
EDIT
The templating code that is having the errors is this:
processTemplates: function() {
if(this.templates === undefined) return this;
console.log(this.templates);
if(Object.getLength(this.templates) > 0) {
Object.each(this.templates, function(template, name) {
this.templates[name] = _.template(template);
}, this);
}
return this;
},
The console.log(this.templates) output shows that on the first initialisation, this.templates contains strings, as it should, but on second initialisation it shows template functions (which should only be the case after processTemplates() is called.
I wonder if it could have anything to do with the way my class is defined, for example:
define(
['backbone', 'View/Kords', 'text!Template/Pages/Landing.html', 'Collection/TenantTypes'],
function(Backbone, KordsView, landing_html, TenantTypesCollection) {
var LandingView = KordsView.extend({
tagName: 'div',
className: 'tiled-light',
templates: {
'main': landing_html
},
landing_html is defined like this in the class, but could there be a reference problem? _.template should not be affecting the value of landing_html within the scope, should it?
EDIT #2
It is not to do with the reference to landing_html. I tried just setting templates.main to a string in the class definition but I still got the errors as before.