MVVM usage with kendo ui framework - javascript

I have two views having their own view models. One of them contains just a grid and the other contains a form. I'm loading two of them dynamically at the same time.
Here is the view model of my view conatining grid:
$(function () {
var ticker = $.connection.marketWatch;
var initializationData = null; // marketWatchData
function init() {
return ticker.server.getAllMarketWatchData().done(function (data) {
initializationData = data;
$("#marketWatchGrid").data("kendoGrid").dataSource.data(data);
});
}
// Add client-side hub methods that the server will call
$.extend(ticker.client, {
updateMarketWatchData: function (marketWatchData) {
// do something...
}
});
// Start the connection
$.connection.hub.start()
.pipe(init)
.done(function () {
viewModelMarketRates.data = initializationData;
viewModelOrder.updateInstruments();
});
});
var viewModelMarketRates = kendo.observable({
data: null
});
kendo.bind($("#marketWatchGrid"), viewModelMarketRates);
And the view model of my view containing form:
$(function () {
var viewModelOrder = kendo.observable({
instruments: viewModelMarketRates.data,
selectedInstrument: "EURUSD",
amount: "0.1",
slActivate: false,
sl: "0.0",
tpActivate: false,
tp: "0.0",
buy: function () {
//e.preventDefault();
//alert("buy");
},
sell: function () {
//e.preventDefault();
//alert("sell");
},
updateInstruments: function () {
this.set("instruments", viewModelMarketRates.data);
this.set("selectedInstrument", "EURUSD");
}
});
//viewModelOrder.instruments = viewModelMarketRates.data;
//alert(viewModelOrder.instruments.length);
kendo.bind($("#orderForm"), viewModelOrder);
});
As you see I'm getting market rates data in init function and storing it in data attribute of viewModelMarketRates. I call updateInstruments function of viewModelOrder but in firebug I'm getting the following error:
ReferenceError: viewModelOrder is not defined
viewModelOrder.updateInstruments();
How can I prevent this error?

The source of your problem is that your viewModelOrder is in different scope than grid's document ready.
You shouldn't reference function/objects from multiple dynamically loaded views that load at the same time. What if one loads later than the other? Or don't load at all?
Easiest solution I can think of atm:
1. Sync the views. Load form after grid.
2. Move content of grid's document ready to a
function grid_ready(){
//all that ticker and connections start stuff
}
and use that function in form's document ready
$(function(){
//create view model etc,
grid_ready();
});
edit: grammar.

Related

Backbone JS Button to open a new view, save values in form

Im new to backbone and I'm looking to a very simple 2 view configuration page usig backbone.
I have the following code;
define(
["backbone","...","..."],
function(Backbone, ... , ... ) {
var PopupView = Backbone.View.extend({
initialize: function initialize() {
Backbone.View.prototype.initialize.apply(this,arguments);
},
events: {
"click .save_conf_button": "save_conf",
},
render: function() {
this.el.innerHTML = this.get_popup_template();
return this;
},
save:conf: function save_conf() {
//get the field values from popup_template
//var items = jquery(....);
});
var ExampleView = Backbone.View.extend({
//Starting view
initialize: function initialize() {
Backbone.View.prototype.initialize.apply(this, arguments);
},
events: {
"click .setup_button": "trigger_setup", //Triggers final setup
"click .create_conf_button": "trigger_popup_setup", //This is the conf popup
},
render: function() {
this.el.innerHTML = this.get_start_html();
return this;
},
trigger_popup_setup: function trigger_popup_setup() {
console.log("Pop up");
//this.el.innerHTML = this.get_popup_template();
PopupView.render();
...
},
}); //End of exampleView
return ExampleView;
} // end of require asynch
); // end of require
E.g. The ExampleView is the starting view with a couple of fields and 2 buttons; create popup and save. Upon pressing the create_conf_button I want to render the popup view, however this does not seem to work as I expected. (Uncaught TypeError: PopupView.render is not a function)
I'm not sure how to proceed and additionally what the "best practice" is for generating these types of dialogs?
Additionally, keeping the values filled in on the previous page after returning from the popupview would be preferential.
Thanks for any help
try
new PopupView.render()
you have to create an instance to call the methods this way
#ashish is correct, you have to instantiate an instance of the PopupView before calling its render method. Currently, you have defined a blueprint for a view called PopupView, which will act as a constructor for newly created PopupView view instances. In order to use this defined view I would suggest storing it in ExampleView's render or initialize method:
// Example View's initialize method
initialize: function initialize() {
this.popUpView = new PopupView();
Backbone.View.prototype.initialize.apply(this, arguments);
},
then referencing it in your trigger_popup_setup function as follows:
trigger_popup_setup: function trigger_popup_setup() {
console.log("Pop up");
//this.el.innerHTML = this.get_popup_template();
this.popUpView.render();
...
},
As for storing state Backbone models are used for that :)
In general to nest subviews within a master view in Backbone you can do the following:
initialize : function () {
//...
},
render : function () {
this.$el.empty();
this.innerView1 = new Subview({options});
this.innerView2 = new Subview({options});
this.$('.inner-view-container')
.append(this.innerView1.el)
.append(this.innerView2.el);
}
In this example the master view is creating instances of it's subviews within its render method and attaching them to a corresponding DOM element.

How can i call a function within a js file?

I have a JavaScript file AppForm.js, which I wish to reinitialize after a successful ajax post response.
The file itself contains, among others
(function(namespace, $) {
"use strict";
var AppForm = function() {
// Create reference to this instance
var o = this;
// Initialize app when document is ready
$(document).ready(function() {
o.initialize();
});
};
var p = AppForm.prototype;
p.initialize = function() {
// Init events
this._enableEvents();
this._initRadioAndCheckbox();
this._initFloatingLabels();
this._initValidation();
};
p._enableEvents = function () {
//blah blah blah
e.preventDefault();
};
p._initRadioAndCheckbox = function () {
};
p._initFloatingLabels = function () {
};
p._initValidation = function () {
};
window.materialadmin.AppForm = new AppForm;
}(this.materialadmin, jQuery)); // pass in (namespace, jQuery):
How can I do that?
$.ajax({
url: path, type: "POST", cache: "false",
dataType: "html",
contentType: "application/json; charset=utf-8",
traditional: true,
data: JSON.stringify(postData)
}).success(function (data) {
$("#products-list").html(data);
//**PERFORM INIT OF JS FILE**
}).error(function (data) {
});
Thanks to Dan's answer the solution is pretty close but the events are not working since e.preventDefault(); is called.
And here is the full script
(function(namespace, $) {
"use strict";
var AppForm = function() {
// Create reference to this instance
var o = this;
// Initialize app when document is ready
$(document).ready(function() {
o.initialize();
});
};
var p = AppForm.prototype;
// =========================================================================
// INIT
// =========================================================================
p.initialize = function() {
// Init events
this._enableEvents();
this._initRadioAndCheckbox();
this._initFloatingLabels();
this._initValidation();
};
// =========================================================================
// EVENTS
// =========================================================================
// events
p._enableEvents = function () {
var o = this;
// Link submit function
$('[data-submit="form"]').on('click', function (e) {
e.preventDefault();
var formId = $(e.currentTarget).attr('href');
$(formId).submit();
});
// Init textarea autosize
$('textarea.autosize').on('focus', function () {
$(this).autosize({append: ''});
});
};
// =========================================================================
// RADIO AND CHECKBOX LISTENERS
// =========================================================================
p._initRadioAndCheckbox = function () {
// Add a span class the styled checkboxes and radio buttons for correct styling
$('.checkbox-styled input, .radio-styled input').each(function () {
if ($(this).next('span').length === 0) {
$(this).after('<span></span>');
}
});
};
// =========================================================================
// FLOATING LABELS
// =========================================================================
p._initFloatingLabels = function () {
var o = this;
$('.floating-label .form-control').on('keyup change', function (e) {
var input = $(e.currentTarget);
if ($.trim(input.val()) !== '') {
input.addClass('dirty').removeClass('static');
} else {
input.removeClass('dirty').removeClass('static');
}
});
$('.floating-label .form-control').each(function () {
var input = $(this);
if ($.trim(input.val()) !== '') {
input.addClass('static').addClass('dirty');
}
});
$('.form-horizontal .form-control').each(function () {
$(this).after('<div class="form-control-line"></div>');
});
};
// =========================================================================
// VALIDATION
// =========================================================================
p._initValidation = function () {
if (!$.isFunction($.fn.validate)) {
return;
}
$.validator.setDefaults({
highlight: function (element) {
$(element).closest('.form-group').addClass('has-error');
},
unhighlight: function (element) {
$(element).closest('.form-group').removeClass('has-error');
},
errorElement: 'span',
errorClass: 'help-block',
errorPlacement: function (error, element) {
if (element.parent('.input-group').length) {
error.insertAfter(element.parent());
}
else if (element.parent('label').length) {
error.insertAfter(element.parent());
}
else {
error.insertAfter(element);
}
}
});
$('.form-validate').each(function () {
var validator = $(this).validate();
$(this).data('validator', validator);
});
};
// =========================================================================
// DEFINE NAMESPACE
// =========================================================================
window.materialadmin.AppForm = new AppForm;
}(this.materialadmin, jQuery)); // pass in (namespace, jQuery):
UPDATE 1
I added window.materialadmin.AppForm.Initilize at the ajax response but the events are not working
UPDATE 2
And here is the code that does not work after the postback.
$(".ProductOnlyForDemonstation, .IncludeInMainPage, .Active")
.on('click', 'button', function(){
$('.sweet-overlay').toggle();
if (jQuery("#FORM").valid()) {
var id = $(this).attr("data-id");
$.post("/product/DemoIncludeActive", {
"Id": id,
"ProductOnlyForDemonstation": $("#ProductOnlyForDemonstation-" + id).is(':checked'),
"IncludeInMainPage": $("#IncludeInMainPage-" + id).is(':checked'),
"Active": $("#Active-" + id).is(':checked'),
},
function (data) {
}).success(function (data) {
}).error(function () {
});
}
});
You can wrap your code in a global function.
(function(namespace, $) {
"use strict";
window.main = function() {
var AppForm = function () {
// ...
};
};
window.main(); // you can initialize it here
)(this.materialadmin, jQuery);
And execute it if the response is successful.
.success(function (data) {
$("#products-list").html(data);
//**PERFORM INIT OF JS FILE**
window.main();
}).error(function (data) {
});
Edit: It looks like you're exposing the initialize method on a global object. You can just call that init method when the AJAX response completes.
.success(function (data) {
$("#products-list").html(data);
//**PERFORM INIT OF JS FILE**
window.materialadmin.AppForm.initialize();
}).error(function (data) {
});
Related to UPDATE 2
Try to register your events with delegation:
$(document).on(
'click',
'.ProductOnlyForDemonstation button, .IncludeInMainPage button, .Active button',
function() {
// Your code
}
);
I suppose you're loading something and render new page content after response, so previously registered events are not attached to new elements. With delegation you'll get your events working even after elements were added to DOM dynamically (if they match with delegating selector), because events are attached to document and bubbled from your buttons. You can attach event deeper in the DOM than document itself, but to the element containing your dynamic content (in other words: to closest element that will not be overriden after completing request).
PS. You can also add some unique class to all .ProductOnlyForDemonstation button, .IncludeInMainPage button, .Active button and delegate events to that class (shorter definition)
some checks for the events to work after postback
1)using $("#products-list").html(data) will remove all the events attached to child elements of #products-list.
So either a)attach events once on "#products-list" only with event-delegation In jQuery, how to attach events to dynamic html elements?
or b)reattach events on every child after using $("#products-list").html(data)
2) dont use .html() because it also removes all jquery data and events on children. update independent children elements instead.
I had experienced same issue like you. After reinitializing events,all events are not working properly.
I have tried lots and finally i have found issue.when i am reinitializing all control, all events are rebind.
so they are not fired properly.
so please unbind all events related to your control and then init all control agian and bind all event.
Updated answer
if you are using jQuery 1.7 or onwarads then add following code:
$(".ProductOnlyForDemonstation, .IncludeInMainPage, .Active").off();
$('[data-submit="form"]').off('click');
$('textarea.autosize').off('focus');
$('.floating-label .form-control').off('keyup change');
//-----------------
//**PERFORM INIT OF JS FILE**
before this line.
//**PERFORM INIT OF JS FILE**
and you are using jquery below 1.7 then use following code:
$(".ProductOnlyForDemonstation, .IncludeInMainPage, .Active").unbind();
$('[data-submit="form"]').unbind('click');
$('textarea.autosize').unbind('focus');
$('.floating-label .form-control').unbind('keyup change');
//-----------------
//**PERFORM INIT OF JS FILE**
before this line.
//**PERFORM INIT OF JS FILE**
for more help related to unbind click here.
for more help related to off click here.
i hope this will help.
In order to call a function you should take into account the following points below:
The function should be defined in the same file or one loaded before the attempt to call it.
The function should be in the same or greater scope then the one trying to call it.
So, the following example should work:
You declare function fnc1 in first.js, and then in second you can just have fnc1();
first.js :
function fnc1 (){
alert('test');
}
second.js :
fnc1();
index.html :
<script type="text/javascript" src="first.js"></script>
<script type="text/javascript" src="second.js"></script>
You could add the line namespace.initialize = p.initialize; at the end of your code :
(function(namespace, $) {
"use strict";
/* ....... */
// =================================================
// DEFINE NAMESPACE
// =================================================
namespace.AppForm = new AppForm;
namespace.initialize = p.initialize;
}(this.materialadmin, jQuery)); // pass in (namespace, jQuery):
Then, p.initialize becomes available globally as materialadmin.initialize, and you can call it from another file like this :
materialadmin.initialize();
Maybe two solutions
First solution
Create a file js with your functions who will reload.
<script language="text/javascript">
function load_js()
{
var head= document.getElementsByTagName('head')[0];
var script= document.createElement('script');
script.type= 'text/javascript';
script.src= 'source_file.js';
head.appendChild(script);
}
</script>
And in your success :
.success(function (data) {
$("#products-list").html(data);
load_js();
}).error(function (data) {
});
2nd Solution
Like the first solution : Create a file js with your functions who will reload.
Use use getScript instead of document.write - it will even allow for a callback once the file loads.
Description: Load a JavaScript file from the server using a GET HTTP
request, then execute it.
So you can try this :
.success(function (data) {
$.getScript('your-file.js', function() {
}).error(function (data) {
});
or simply :
jQuery.getScript('my-js.js');
You will try, and tell me if that helps.
It should be simple by printing content of this at top of your ajax url script :
<script src="your-js-to-be-initialized.js"></script>
Your jquery ajax code will remain the same. You just need to print the script on each request so that it is reinitialized and binds to your elements.
$.ajax({
url: path.php, type: "POST", cache: "false",
dataType: "html", contentType: "application/json; charset=utf-8",
traditional: true,
data: JSON.stringify(postData)
}).success(function (data) {
$("#products-list").html(data);
//**PERFORM INIT OF JS FILE**
//path.php should echo/print the <script src="your-js-to-be-initialized.js">
}).error(function (data) {
});
I looked at your edit history and saw you did
p._enableEvents = function () {
var o = this;
// Link submit function
$('[data-submit="form"]').on('click', function (e) {
e.preventDefault();
var formId = $(e.currentTarget).attr('href');
$(formId).submit();
});
// Init textarea autosize
$('textarea.autosize').on('focus', function () {
$(this).autosize({append: ''});
});
};
If this is still how you enable your events, I suspect the cause might be you have more than one subscription on form click and textarea focus after reinitializing on your ajax callback. I suggest try only do other init tasks, and exclude event bindings in your callback function.
Try make it like this:
(function($) {
"use strict";
var materialadmin = {};
var AppForm = function() {
//closure
var self = this;
(function(){
//todo: init events
};)();
//<your AppForm class's props here...>
};
materialadmin.Init = function(){
//create instance of AppForm calss for materialadmin object
materialadmin.appForm = new AppForm();
}
return materialadmin;
//*}(jQuery)); // syntax mistake, i'm sorry)).*
})(jQuery);
$(document).ready(function(){
materialadmin.Init();
});
$.ajax({
url: path,
type: "POST",
cache: "false",
dataType: "html",
contentType: "application/json; charset=utf-8",
traditional: true,
data: JSON.stringify(postData),
success: function (data) {
$("#products-list").html(data);
materialadmin.Init();
},
error: function(){
alert('error')}
});
As you're using jQuery validator, you can use Validator's resetForm method in order to reset your form.
For this purpose, you can expose a reset method like follows:
p.reset = function () {
// Reset managed form
$('.form-validate').data('validator').resetForm();
// Reset custom stuff
this._initRadioAndCheckbox();
this._initFloatingLabels();
};
Note that in order to reset your form correctly after posting your request, you need to isolate event binding from the init stuff, for instance the following event binding should move from _initFloatingLabels to _enableEvents:
// Link submit function
$('[data-submit="form"]').on('click', function (e) {
e.preventDefault();
var formId = $(e.currentTarget).attr('href');
$(formId).submit();
});
Finally, you just have to call window.materialadmin.AppForm.reset() in your POST request's callback.

Knockout: Error: You cannot apply bindings multiple times to the same element when refreshing page

I'm using Knockout to bind an MVC view. This works fine the first time but for subsequent refreshes Knockout throws the error:
Error: You cannot apply bindings multiple times to the same element.
This is my binding code (inside Document.Ready), note that I am using setTimeout to run every 25 seconds, this is where the error occurs:
function viewModel() {
Loading = ko.observable(true),
CurrentUser = ko.observable(),
Environments = ko.observableArray(),
CurrentFormattedDate = ko.observable()
}
function doPoll() {
$.get("/home/getindex")
.done(function (data) {
$(data).each(function (index, element) {
viewModel = data;
viewModel.Loading = false;
viewModel.CurrentFormattedDate = moment().format('MMMM YYYY');
});
ko.applyBindings(viewModel);
})
.always(function () {
setTimeout(doPoll, 25000);
})
.error(function (ex) {
alert("Error");
});
};
doPoll();
How do I avoid the error when DoPoll is called multiple times?
By default, bindings in Knockout may happen only once per dom element. The ko.aplyBindings would apply the binding to the document body, thus it will be already bound with data when you call it a second time from the doPoll function.
A possible solution is to make your current view model an observable property of a new view model; then only update the observable property:
var actualViewModel = {
innerViewModel: ko.observable(new viewModel());
}
function doPoll() {
$.get("/home/getindex")
.done(function (data) {
$(data).each(function (index, element) {
data.Loading = false;
data.CurrentFormattedDate = moment().format('MMMM YYYY');
actualViewModel.innerViewModel(data);
});
})
.always(function () {
setTimeout(doPoll, 25000);
})
.error(function (ex) {
alert("Error");
});
};
doPoll();
You would need to call the initial binding against the new view model:
ko.applyBindings(actualViewModel);
You will also need to update the way properties are accessed in the bindings, by putting the innerViewModel in front - for instance:
<div data-bind="text: CurrentFormattedDate">...</div>
would have to become
<div data-bind="text: innerViewModel.CurrentFormattedDate">...</div>

Kendo UI AngularJs grid directive, undefined

In a ticket entry page, I have a main ticketEntry.html page, which contains one grid for the lines and one for the payments.
When ticketEntry.html is loaded, it must first retrieve the ticket view model (via ajax calls to Web API). The line and payment grid cannot retrieve their data until the ticket view model has been received.
In my current solution, I have to use $timeout in the controller for ticketEntry.html for this to work. I am looking for a cleaner way.
Extracts from ticketEntry.html:
<div ng-controller="ticketLineController">
<div id="ticketLineGridDiv" kendo-grid="ticketLineGrid" k-options="ticketLineGridOptions"></div>
</div>
...
<div ng-controller="ticketPaymentController">
<div id="ticketPaymentGridDiv" kendo-grid="ticketPaymentGrid" k-options="ticketPaymentGridOptions"></div>
</div>
In the controller for ticketEntry.html, I have this:
$timeout(function () {
ticketService.getTicket(ticketId).then(
function(ticket) {
$scope.initPos(ticket);
},
...);
}, 500);
$scope.initPos = function(ticket) {
$scope.ticket = ticket; <-- $scope.ticket is used by the line and payment grid
$scope.$broadcast('PosReady'); <-- Tell the payment and line controllers to load their grids
}
As you can see, I am using $timeout to delay for 500ms, then I get the ticket view model and broadcast to the line and payment controller that they now can load their grids.
Here is the listener in the line controller:
$scope.$on('PosReady', function (event) {
$scope.ticketLineGrid.setDataSource(getGridDataSource());
$scope.ticketLineGrid.dataSource.read();
});
The problem is that if I do not use $timeout in the ticket entry controller, $scope.ticketLineGrid is sometimes undefined here (same thing with the payments controller).
I have tried using angular.element(document).ready(function () {...} instead of $timeout in the ticket entry controller, but that did not handle the issue.
How do I know when $scope.ticketLineGrid (for example) has been created/defined?
What is the proper way of handling this kind of scenario?
Update 9/27/2014, to provide more data on how the ticket line grid gets initialized:
In the AngularJs directive in ticketEntry.html, the k-options specifies the definition object for the grid:
<div id="ticketLineGridDiv" kendo-grid="ticketLineGrid" k-options="ticketLineGridOptions"></div>
ticketPaymentGridOptions is just an object with properties that defines the grid:
$scope.ticketPaymentGridOptions = {
autoBind: false,
height: 143,
columns: [
{
field: "payCode", title: "PayCode",
},
{
field: "amount", title: "Amount", format: "{0:n2}", attributes: { style: "text-align:right" },
},
],
pageable: false,
...
};
Update 9/29/2014: This is the solution I went with, based on suggestion by Valentin
I use two watches - one in the child scope where the ticketLineGrid lives:
$scope.$watch('ticketLineGrid', function (newVal) {
if (angular.isDefined(newVal)) {
$scope.ticketControl.lineGridReady = true;
}
});
This watch sets the parent property $scope.ticketControl.lineGridReady = true once the grid has been initialized.
The parent (ticketEntryController) has watches for lineGridReady:
$scope.$watch('ticketControl.lineGridReady', function (gridReady) {
if (gridReady) {
$scope.loadPage();
}
});
$scope.loadPage = function () {
ticketService.getTicket(ticketId).then(
function (ticket) {
$scope.initPos(ticket);
}
...
}
Not as clean as I would have liked it, but certainly better than using $timeout...
How do I know when $scope.ticketLineGrid (for example) has been created/defined?
You could use a $scope.$watch statement :
$scope.$watch('ticketLineGrid', function (newVal, oldVal) {
if(angular.isDefined(newVal)){
// do something with it
}
})
However, in my view the good way to do this is to retrieve the data not from a scope property, but from a promise. I would use only promises and no events at all for this :
var ticketPromise = ticketService.getTicket(ticketId);
ticketPromise.then(function (ticket) {
$scope.ticket = ticket;
});
// you know that part better than I do
var ticketLineGridPromise = ...;
$q.all([ticketPromise, ticketLineGridPromise])
.then(function (realizations) {
var ticket = realizations[0], ticketLineGrid = realizations[1];
$scope.ticketLineGrid.setDataSource(getGridDataSource());
$scope.ticketLineGrid.dataSource.read();
})
I can't be more precise because it's not clear from your code what initializes ticketLineGrid.
Finally, in many cases it's very handy to use resolve clauses in your route declaration.

Backbone.js with $.getScript Extending Model

I have created a new backbone.js widget model which I hope to extend:
var Widget = Backbone.Model.extend({
constructor: function() {
console.log('Widget constructor called.');
}
});
Depending on the page a user views a number of widgets will be loaded via Ajax, so for each widget I will do something like this:
$.getScript('widgets/widget1', function(xhr){
var widget1 = new pieChartWidget();
widget1.render();
widget1.update();
});
Contents of "widgets/widget1":
$(function() {
var pieChartWidget = Widget.extend({
render: function(json) {
console.log('Widget render called.');
},
update: function(json) {
console.log('Widget update called.');
}
});
});
In my Firebug console I get "pieChartWidget is not defined". I know the Javascript is being loaded successfully, I just cannot extend the "Widget" model from it.
Your widget is defined within a function. So all variable declared there are only visible withing then functions scope.
$(function() {
var pieChartWidget;
// pieChartWidget is only visible here!
});
// pieChartWidget not visible here!
To have access to the widget from outside of the function you have to assign it to a global variable (eg. your applications namespace). You could also use window (not the preferred way).
Your code should work unchanged if you assign your widget to window like so:
$(function() {
window.pieChartWidget = Widget.extend({
render: function(json) {
console.log('Widget render called.');
},
update: function(json) {
console.log('Widget update called.');
}
});
});

Categories