I have a ModalPopupExtender tied to a RadListBox so that when an item is selected from the list box, I need a "Please Wait" message while the page behind loads the data into RadCharts. The Modal does Hide when the loading is completed. The problem I'm having is if the same list item is selected again, the Modal popup shows again, but never goes away. I've tried just about everything, but the click/selection of a list item in the RadListBox immediately shows the Modal and I can't seem to find a way to do item checking to see if its the same item, then to do nothing.
Here is my Panel and Modal code (ASPX)
<asp:Panel ID="pnlProgress" runat="server" Height="50px" Width="50px" >
<div>
<div class="popupbody">
<table width="50%">
<tr>
<td align="center">
<asp:Image ID="imgProgress" runat="server" ImageUrl="~/_images/ajax-loader.gif" />
<br />
<br />
<asp:Label ID="lblLoading" runat="server" Text='Please wait...'
Font-Bold="true"></asp:Label>
</td>
</tr>
</table>
</div>
</div>
</asp:Panel>
<ajaxToolKit:ModalPopupExtender ID="mpeProgress" runat="server" TargetControlID="lboxTestedMachines" PopupDragHandleControlID="pnlProgress" `enter code here`
X="1000" Y="500" PopupControlID="pnlProgress" BackgroundCssClass="modalBackground" RepositionMode="RepositionOnWindowResize" BehaviorID="lboxTestedMachines">
</ajaxToolKit:ModalPopupExtender>
And here is my ASPX.CS code
protected void lboxTestedMachines_SelectedIndexChanged(object sender, EventArgs e)
{
int iResultID = Convert.ToInt32(lboxTestedMachines.SelectedValue);
if (tbl_charts.Style.Value != "display:normal")
tbl_charts.Style.Value = "display:normal";
GetMachineName(iResultID);
RdListView_Chart.DataSource = LoadCassetteForFoodChart(iResultID);
GetApprovalRejectionStatus(iResultID);
}
The RadListBox has an internal logic on item click to determine if the item was already selected. If it was, it would not trigger the OnClientSelectedIndexChanging event, hence no postback on clicking a selected item.
The ModalPopupBehavior on the other hand reacts to any click event inside TargetControlID control's element. Here are some code snippets obtained using the browser's DevTools (search for Sys.Extended.UI.ModalPopupBehavior.prototype.initialize by following steps from Get IntelliSense for the client-side object)
initialize: function() {
Sys.Extended.UI.ModalPopupBehavior.callBaseMethod(this, "initialize"),
this._isIE6 = Sys.Browser.agent == Sys.Browser.InternetExplorer && Sys.Browser.version < 7,
this._popupDragHandleControlID && (this._dragHandleElement = $get(this._popupDragHandleControlID)),
this._popupElement = $get(this._popupControlID),
this._createDomElements(),
this._showHandler = Function.createDelegate(this, this._onShow),
$addHandler(this.get_element(), "click", this._showHandler),
this._okControlID && (this._okHandler = Function.createDelegate(this, this._onOk),
$addHandler($get(this._okControlID), "click", this._okHandler)),
this._cancelControlID && (this._cancelHandler = Function.createDelegate(this, this._onCancel),
$addHandler($get(this._cancelControlID), "click", this._cancelHandler)),
this._scrollHandler = Function.createDelegate(this, this._onLayout),
this._resizeHandler = Function.createDelegate(this, this._onLayout),
this.registerPartialUpdateEvents(),
this._resetAnimationsTarget(),
this._onHiding.get_animation() && (this._hidingAnimationEndedHandler = Function.createDelegate(this, function () {
this._isAnimationJustEnded = !0,
this.hide()
}),
this._onHiding.get_animation().add_ended(this._hidingAnimationEndedHandler)),
this._onShowing.get_animation() && (this._showingAnimationEndedHandler = Function.createDelegate(this, function () {
this._isAnimationJustEnded = !0,
this.show()
}),
this._onShowing.get_animation().add_ended(this._showingAnimationEndedHandler))
},
_onShow: function(e) {
if (!this.get_element().disabled)
return this.show(),
e.preventDefault(),
!1
},
Solution 1: Subscribe to the Showing event of the ModalPopupBehavior and allow it to show only when you set a flag from the OnClientSelectedIndexChanging event. The example below stores the flag in the RadListBox client-side object as an expando property:
<telerik:RadCodeBlock runat="server" ID="rdbScripts">
<script type='text/javascript'>
function pageLoadHandler() {
var modalPopupExtenderClientObject = $find("<%= mpeProgress.ClientID %>")
modalPopupExtenderClientObject.add_showing(function (sender, args) {
// "sender" argument represents the Modal popup control client-side object
// sender.get_element() returns the DOM element of the RadListBox
// sender.get_element().control return the client-side object of the RadListBox where we stored the expando property __allowModalPopupShow
if (sender.get_element().control.__allowModalPopupShow !== true) {
args.set_cancel(true);
}
})
}
function OnClientSelectedIndexChanging(sender, args) {
// sender in this context is the RadListBox client-side object
sender.__allowModalPopupShow = true;
}
Sys.Application.add_load(pageLoadHandler);
</script>
</telerik:RadCodeBlock>
Solution 2: Use RadAjaxLoading panel and show it programmatically in OnClientSelectedIndexChanging:
https://docs.telerik.com/devtools/aspnet-ajax/controls/ajaxloadingpanel/how-to/show-and-hide-ajaxloadingpanel-explicitly
https://demos.telerik.com/aspnet-ajax/ajaxloadingpanel/functionality/transparency/defaultcs.aspx
Bottom line, I want to know exactly which leads (form submissions) came through Adwords.
It's a manually built form (no JotForm or anything) and submits through manually built php that, at the moment) sends me an Email. The page and form itself is HTML.
So we can add UTM info to the Adwords URLs, and I'm looking for a way to grab that UTM information and pass it on to the Email (like through a hidden field in the form or something.)
I think this would be the easiest way to make sure we know which leads we paid for, but if not, please let me know of a better way.
TIA
The best way to track these utm is to store them into a js cookie and then retrieve them to store values into hidden fields in form.
using cookies will allow you to navigate through different pages without having to pass the data again and again in the url.
You can modify this solution to use for your form.
http://www.decorumsol.com/tracking-utm-parameters-in-contact-form-7/
Edit:
Here is the code for better understanding.
function getQueryVariable(variable)
{
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i=0;i<vars.length;i++) {
var pair = vars[i].split("=");
if(pair[0] == variable){return pair[1];}
}
return(false);
}
jQuery(document).ready(function() {
jQuery('form').find('input.utm_source').each(function() {
var a = getQueryVariable('utm_source');
if(a){
jQuery(this).val(a);
}
});
jQuery('form').find('input.utm_medium').each(function() {
var a = getQueryVariable('utm_medium');
if(a){
jQuery(this).val(a);
}
});
jQuery('form').find('input.utm_campaign').each(function() {
var a = getQueryVariable('utm_campaign');
if(a){
jQuery(this).val(a);
}
});
jQuery('form').find('input.utm_term').each(function() {
var a = getQueryVariable('utm_term');
if(a){
jQuery(this).val(a);
}
});
jQuery('form').find('input.utm_content').each(function() {
var a = getQueryVariable('utm_content');
if(a){
jQuery(this).val(a);
}
});
});
function createCookie(name,value,days) {
var expires = "";
if (days) {
var date = new Date();
date.setTime(date.getTime()+(days*24*60*60*1000));
var expires = "; expires="+date.toGMTString();
}
document.cookie = name+"="+value+expires+"; path=/";
}
function readCookie(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for(var i=0;i < ca.length;i++) {
var c = ca[i];
while (c.charAt(0)==' ') c = c.substring(1,c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
}
return null;
}
function eraseCookie(name) {
createCookie(name,"",-1);
}
var c_name = "_aaa_utmz";
if(getQueryVariable("utm_source") != "") {
createCookie("_aaa_utmz", getQueryVariable("utm_source") + "|" + getQueryVariable("utm_medium")+ "|" + getQueryVariable("utm_term")+ "|" + getQueryVariable("utm_campaign")+ "|" + getQueryVariable("utm_content"), 60);
}
else if (readCookie(c_name)){
c_start=readCookie(c_name);
var _pipe = c_start.split("|");
jQuery("input[name=utm_source], .utm_source").val(_pipe[0]);
jQuery("input[name=utm_medium], .utm_medium").val(_pipe[1]);
jQuery("input[name=utm_term], .utm_term").val(_pipe[2]);
jQuery("input[name=utm_campaign], .utm_campaign").val(_pipe[3]);
jQuery("input[name=utm_content], .utm_content").val(_pipe[4]);
}
in your html form, create
<div style="display:none;">
<input type="text" value="" class="utm_source" name="utm_source" />
<input type="text" value="" class="utm_medium" name="utm_medium" />
<input type="text" value="" class="utm_term" name="utm_term" />
<input type="text" value="" class="utm_campaign" name="utm_campaign" />
<input type="text" value="" class="utm_content" name="utm_content" />
</div>
When submitting a form in AngularJS and use the browser remember password functionality, and in a subsequent login attempt you let the browser fill in the login form with the username and password, the $scope model won't be changed based on the autofill.
The only dirty hack I found is to use the following directive:
app.directive("xsInputSync", ["$timeout" , function($timeout) {
return {
restrict : "A",
require: "?ngModel",
link : function(scope, element, attrs, ngModel) {
$timeout(function() {
if (ngModel.$viewValue && ngModel.$viewValue !== element.val()) {
scope.apply(function() {
ngModel.$setViewValue(element.val());
});
}
console.log(scope);
console.log(ngModel.$name);
console.log(scope[ngModel.$name]);
}, 3000);
}
};
}]);
The problem is that the ngModel.$setViewValue(element.val()); doesn't change the model nor the view based on the element.val() returned value. How can I accomplish that?
Apparently this is a known issue with Angular and is currently open
I'm not sure what you could do here besides some sort of work around like you're trying. It seems you're on the right track. I couldn't get my browser to try to remember a password for your plunk, so I'm not sure if this will work but have a look:
app.directive('autoFillSync', function($timeout) {
return {
require: 'ngModel',
link: function(scope, elem, attrs, ngModel) {
var origVal = elem.val();
$timeout(function () {
var newVal = elem.val();
if(ngModel.$pristine && origVal !== newVal) {
ngModel.$setViewValue(newVal);
}
}, 500);
}
}
});
<form name="myForm" ng-submit="login()">
<label for="username">Username</label>
<input type="text" id="username" name="username" ng-model="username" auto-fill-sync/><br/>
<label for="password">Password</label>
<input type="password" id="password" name="password" ng-model="password" auto-fill-sync/><br/>
<button type="submit">Login</button>
</form>
I think you just need to simplify your approach a bit. The one thing I definitely recommend is to check ngModel.$pristine and make sure you're not overwriting some poor user's input. Also, 3 seconds is probably too long. You shouldn't have to call $apply() in a $timeout, BTW, it should queue a $digest for you automatically.
The real catch: Will your browser beat Angular to execution? What about my browser?
This is probably an unwinnable war, which is why Angular (or Knockout) hasn't been able to solve it readily. There's no guarantee of the state of the data in your input at the time of the directive's initial execution. Not even at the time of Angular's initialization.... So it's a tricky problem to solve.
Here is a solution that is far less hacky than other solutions presented and is semantically sound AngularJS: http://victorblog.com/2014/01/12/fixing-autocomplete-autofill-on-angularjs-form-submit/
myApp.directive('formAutofillFix', function() {
return function(scope, elem, attrs) {
// Fixes Chrome bug: https://groups.google.com/forum/#!topic/angular/6NlucSskQjY
elem.prop('method', 'POST');
// Fix autofill issues where Angular doesn't know about autofilled inputs
if(attrs.ngSubmit) {
setTimeout(function() {
elem.unbind('submit').submit(function(e) {
e.preventDefault();
elem.find('input, textarea, select').trigger('input').trigger('change').trigger('keydown');
scope.$apply(attrs.ngSubmit);
});
}, 0);
}
};
});
Then you simply attach the directive to your form:
<form ng-submit="submitLoginForm()" form-autofill-fix>
<div>
<input type="email" ng-model="email" ng-required />
<input type="password" ng-model="password" ng-required />
<button type="submit">Log In</button>
</div>
</form>
You don't have to use a $timeout or anything like this. You can use an event system.
I think it's more Angularish and does not depend on jQuery or custom event catching.
For example on your submit handler:
$scope.doLogin = function() {
$scope.$broadcast("autofill:update");
// Continue with the login.....
};
And then you can have an autofill directive like this:
.directive("autofill", function () {
return {
require: "ngModel",
link: function (scope, element, attrs, ngModel) {
scope.$on("autofill:update", function() {
ngModel.$setViewValue(element.val());
});
}
}
});
Finally, your HTML will be like:
<input type="text" name="username" ng-model="user.id" autofill="autofill"/>
No need to hack anymore! Angular dev tbosch made a polyfill that triggers a change event when the browser changes form fields without triggering a change event:
https://github.com/tbosch/autofill-event
For now they won't build this into the Angular code, as this is a bugfix for the browser, and also works without Angular (e.g. for plain jQuery apps).
"The polyfill will check for changes on document load and also when an input is left (only in the same form). However, you can trigger the check manually if you want to.
The project has unit tests as well as semi automatic tests, so we finally have a place to collect all the different use case together with the required browser settings.
Please note: This polyfill works with plain AngularJS apps, with AngularJS/jQuery apps but also with plain jQuery apps that do not use Angular."
It can be installed with:
bower install autofill-event --save
Add the script autofill-event.js after jQuery or Angular in your page.
This will do the following:
after DOMContentLoaded: check all input fields
a field is left: check all other fields in the same form
API (to manually trigger the check):
$el.checkAndTriggerAutoFillEvent(): Execute the check for all DOM elements in the given jQuery / jQLite element.
How it works
Remember all changes to input elements by the user (listening for change events) and also by JavaScript (by intercepting $el.val() for jQuery / jQLite elements). That changed value is stored on the element in a private property.
Checking an element for auto fill: Compare the current value of the element with the remembered value. If it's different, trigger a change event.
Dependencies
AngularJS or jQuery (works with either one or both)
More info and source on the github page.
Original Angular Issue #1460 on Github can be read here.
Dirty code, check if issue https://github.com/angular/angular.js/issues/1460#issuecomment-18572604 is fixed before using this code.
This directive triggers events when field is filled, not only before submit (it's necessary if you have to handle input before submit)
.directive('autoFillableField', function() {
return {
restrict: "A",
require: "?ngModel",
link: function(scope, element, attrs, ngModel) {
setInterval(function() {
var prev_val = '';
if (!angular.isUndefined(attrs.xAutoFillPrevVal)) {
prev_val = attrs.xAutoFillPrevVal;
}
if (element.val()!=prev_val) {
if (!angular.isUndefined(ngModel)) {
if (!(element.val()=='' && ngModel.$pristine)) {
attrs.xAutoFillPrevVal = element.val();
scope.$apply(function() {
ngModel.$setViewValue(element.val());
});
}
}
else {
element.trigger('input');
element.trigger('change');
element.trigger('keyup');
attrs.xAutoFillPrevVal = element.val();
}
}
}, 300);
}
};
});
Seems like clear straight ahead solution. No jQuery needed.
UPDATE:
Model is updated only when model value isn't equal to actual input
value.
Checking doesn't stop on first autofill. In case if you wish to use
another account for example.
app.directive('autofillable', ['$timeout', function ($timeout) {
return {
scope: true,
require: 'ngModel',
link: function (scope, elem, attrs, ctrl) {
scope.check = function(){
var val = elem[0].value;
if(ctrl.$viewValue !== val){
ctrl.$setViewValue(val)
}
$timeout(scope.check, 300);
};
scope.check();
}
}
}]);
Solution 1 [Using $timeout]:
Directive:
app.directive('autoFillSync', function($timeout) {
return {
require: 'ngModel',
link: function(scope, elem, attrs, model) {
var origVal = elem.val();
$timeout(function () {
var newVal = elem.val();
if(model.$pristine && origVal !== newVal) {
model.$setViewValue(newVal);
}
}, 500);
}
};
});
HTML:
<form name="myForm" ng-submit="login()">
<label for="username">Username</label>
<input type="text" id="username" name="username" ng-model="username" auto-fill-sync/><br/>
<label for="password">Password</label>
<input type="password" id="password" name="password" ng-model="password" auto-fill-sync/><br/>
<button type="submit">Login</button>
</form>
Solution 2 [Using angular events]:
Ref: Becko's answer
Directive:
app.directive("autofill", function () {
return {
require: "ngModel",
link: function (scope, element, attrs, ngModel) {
scope.$on("autofill:update", function() {
ngModel.$setViewValue(element.val());
});
}
};
});
HTML:
<form name="myForm" ng-submit="login()">
<label for="username">Username</label>
<input type="text" id="username" name="username" ng-model="username" autofill/><br/>
<label for="password">Password</label>
<input type="password" id="password" name="password" ng-model="password" autofill/><br/>
<button type="submit">Login</button>
</form>
Solution 3 [Using relay method calls]:
Directive:
app.directive('autoFill', function() {
return {
restrict: 'A',
link: function(scope,element) {
scope.submit = function(){
scope.username = element.find("#username").val();
scope.password = element.find("#password").val();
scope.login();//call a login method in your controller or write the code here itself
}
}
};
});
HTML:
<form name="myForm" auto-fill ng-submit="submit()">
<label for="username">Username</label>
<input type="text" id="username" name="username" ng-model="username" />
<label for="password">Password</label>
<input type="password" id="password" name="password" ng-model="password" />
<button type="submit">Login</button>
</form>
Well, the easiest way it's to emulate the browser's behavior, so if there is a problem with the change event, just fire it yourself. Much simpler.
Directive:
yourModule.directive('triggerChange', function($sniffer) {
return {
link : function(scope, elem, attrs) {
elem.on('click', function(){
$(attrs.triggerChange).trigger(
$sniffer.hasEvent('input') ? 'input' : 'change'
);
});
},
priority : 1
}
});
HTML:
<form >
<input data-ng-model="user.nome" type="text" id="username">
<input data-ng-model="user.senha" type="password" id="password" >
<input type="submit" data-ng-click="login.connect()" id="btnlogin"
data-trigger-change="#password,#username"/>
</form>
You can do some variations, like putting the directive on the form and firing the event on all inputs with the .dirty class on form submit.
This is jQuery way :
$(window).load(function() {
// updates autofilled fields
window.setTimeout(function() {
$('input[ng-model]').trigger('input');
}, 100);
});
This is Angular way :
app.directive('autofill', ['$timeout', function ($timeout) {
return {
scope: true,
require: 'ngModel',
link: function (scope, elem, attrs, ctrl) {
$timeout(function(){
$(elem[0]).trigger('input');
// elem.trigger('input'); try this if above don't work
}, 200)
}
}
}]);
HTML
<input type="number" autofill />
Here's another workaround that's less hacky, but requires some extra code in the controller.
HTML:
<form ng-submit="submitForm()" ng-controller="FormController">
<input type="text" ng-model="username" autocomplete-username>
<input type="submit">
</form>
Directive (CoffeeScript):
directives.directive 'autocompleteUsername', ->
return (scope, element) ->
scope.getUsername = ->
element.val()
Controller:
controllers.controller 'FormController', [->
$scope.submitForm = ->
username = $scope.getUsername?() ? $scope.username
# HTTP stuff...
]
This is the only solution I've found that allowed all of my Angular' validations to work as designed including disable/enable of submit button. Installs with bower and 1 script tag. Bazinga!
https://github.com/tbosch/autofill-event
Changing the model value, instead of using a timeout function worked for me.
Here is my code:
module.directive('autoFill', [ function() {
return {
require: 'ngModel',
link:function(scope, element, attr, ngModel) {
var origVal = element.val();
if(origVal){
ngModel.$modelValue = ngModel.$modelValue || origVal;
}
}
};
}]);
One-liner workaround in the submit handler (requires jQuery):
if (!$scope.model) $scope.model = $('#input_field').val();
I force a $setValue(val()) on submit: (this works without jQuery)
var ValidSubmit = ['$parse', function ($parse) {
return {
compile: function compile(tElement, tAttrs, transclude) {
return {
post: function postLink(scope, element, iAttrs, controller) {
var form = element.controller('form');
form.$submitted = false;
var fn = $parse(iAttrs.validSubmit);
element.on('submit', function(event) {
scope.$apply(function() {
var inputs = element.find('input');
for(var i=0; i < inputs.length; i++) {
var ele = inputs.eq(i);
var field = form[inputs[i].name];
field.$setViewValue(ele.val());
}
element.addClass('ng-submitted');
form.$submitted = true;
if(form.$valid) {
fn(scope, {$event:event});
}
});
});
scope.$watch(function() { return form.$valid}, function(isValid) {
if(form.$submitted == false) return;
if(isValid) {
element.removeClass('has-error').addClass('has-success');
} else {
element.removeClass('has-success');
element.addClass('has-error');
}
});
}
}
}
}
}]
app.directive('validSubmit', ValidSubmit);
I am very new to Angularjs, but I found a simple solution to that problem=>
Force Angular to reevaluate expression... by changing it!
(of course you need to remember the initial value to revert to initial state)
Here is the way it goes in your controller function for submitting the form:
$scope.submit = function () {
var oldpassword = $scope.password;
$scope.password = '';
$scope.password = oldpassword;
//rest of your code of the submit function goes here...
where of course, the value entered in your password input has been set by windows and not by user.
You can try this code :
yourapp.directive('autofill',function () {
return {
scope: true,
require: 'ngModel',
link: function (scope, elem, attrs, ctrl) {
var origVal = elem.val();
if (origVal != '') {
elem.trigger('input');
}
}
}
});
A minor modification to this answer (https://stackoverflow.com/a/14966711/3443828): use an $interval instead of a $timeout so you don't have to race the browser.
mod.directive('autoFillSync', function($interval) {
function link(scope, element, attrs, ngModel) {
var origVal = element.val();
var refresh = $interval(function() {
if (!ngModel.$pristine) {
$interval.cancel(refresh);
}else{
var newVal = element.val();
if (origVal !== newVal) {
ngModel.$setViewValue(newVal);
$interval.cancel(refresh);
}
}
}, 100);
}
return {
require: 'ngModel',
link: link
}
});
This is the solution I ended up using in my forms.
.directive('autofillSync', [ function(){
var link = function(scope, element, attrs, ngFormCtrl){
element.on('submit', function(event){
if(ngFormCtrl.$dirty){
console.log('returning as form is dirty');
return;
}
element.find('input').each(function(index, input){
angular.element(input).trigger('input');
});
});
};
return {
/* negative priority to make this post link function run first */
priority:-1,
link: link,
require: 'form'
};
}]);
And the form's template will be
<form autofill-sync name="user.loginForm" class="login-form" novalidate ng-submit="signIn()">
<!-- Input fields here -->
</form>
This way I was able to run any parsers/formatters I have on my ng-model and have the submit functionality transparent.
Solution without directives:
.run(["$window", "$rootElement", "$timeout", function($window, $rootElement, $timeout){
var event =$window.document.createEvent("HTMLEvents");
event.initEvent("change", true, true);
$timeout(function(){
Array.apply(null, $rootElement.find("input")).forEach(function(item){
if (item.value.length) {
item.$$currentValue = item.value;
item.dispatchEvent(event);
}
});
}, 500);
}])
This is a simple fix that works for all the cases I've tested in both Firefox and Chrome. Note that with the top answer (directive w/ timeout) I had issues with -
Browser back / forward buttons, don't re-fire page load events (so the fix doesn't apply)
Loading of credentials some time after page load. e.g. in Firefox, double click on the login box and select from stored credentials.
Need a solution that updates before form submission since I disable the Login button until valid input provided
This fix is obviously very dumb and hacky, but it works 100% of the time -
function myScope($scope, $timeout) {
// ...
(function autoFillFix() {
$timeout(function() {
$('#username').trigger('change');
$('#password').trigger('change');
autoFillFix(); }, 500);
})();
}
None of these solutions worked for my use case. I have some form fields that use ng-change to watch for change. Using $watch is no help as it is not triggered by autofill. Since I have no submit button there is no easy way to run some of the solutions and I was not successful using intervals.
I ended up disabling autofill - not ideal but a lot less confusing to the user.
<input readonly onfocus="this.removeAttribute('readonly');">
Found the answer here
If you are using jQuery you could do this on form submit:
HTML:
<form ng-submit="submit()">
<input id="email" ng-model="password" required
type="text" placeholder="Your email">
<input id="password" ng-model="password" required
type="password" placeholder="Password">
</form>
JS:
$scope.submit = function() {
$scope.password = $('#password').val();
}
If you want to keep it simple just get the value using javascript
In your angular js controller :
var username = document.getElementById('username').value;
I am learning to use MVC 4/MVVM/Knockout for a web-managed data project. I have been running into a problem updating the View when using the remove function on an observable array. The updates happen when using push or unshift, but not remove. Using the debugger in chrome I can see that the data is being removed from the array, the update event just isn't working.
Snippet from the html is the table below, there is a form I did not include for adding or editing data.
<div id="MessageDiv" data-bind="message: Message"></div>
<div class="tableContainer hiddenHead">
<div class="headerBackground"></div>
<div class="tableContainerInner">
<table id="adapter-table" class="grid" data-bind="sortTable: true">
<thead>
<tr>
<th class="first">
<span class="th-inner">Name</span>
</th>
<th>
<span class="th-inner">DeviceID</span>
</th>
<th>
<span class="th-inner"></span>
</th>
<th>
<span class="th-inner"></span>
</th>
</tr>
</thead>
<tbody data-bind="template: { name: 'AdaptersTemplate', foreach: Adapters }">
</tbody>
</table>
<script id="AdaptersTemplate" type="text/html">
<tr>
<td data-bind="text: Name"></td>
<td data-bind="text: DeviceID"></td>
<td>Edit
<td>Delete
</tr>
</script>
</div>
<input type="button" data-bind='click: addAdapter' value="Add New Adapter" />
<input type="button" data-bind='click: saveAll' value="Save Changes" id="SaveChangesButton" />
</div>
My javascript has been set up to manage the VM as restful and caches the changes. Add, Edit, and Saving/Deleting data all seems to work without throwing errors that I am seeing in the debugger in Chrome. Confirming changes seems to work fine and makes the changes to the database as expected.
$(function () {
var viewModel = new AdaptersModel();
getData(viewModel);
});
function getData(viewModel) {
$.getJSON("/api/AdapterList",
function (data) {
if (data && data.length > 0) {
viewModel.SetAdaptersFromJSON(data);
}
ko.applyBindings(viewModel);
});
}
//#region AdapterVM
function Adapter(name, siFamily, deviceIDs) {
var self = this;
self.Name = ko.observable(name);
self.DeviceID = ko.observable(deviceIDs);
self.ID = 0;
}
function AdaptersModel() {
var self = this;
self.Adapters = ko.observableArray([]);
self.DeleteAdapters = ko.observableArray([]);
self.NewAdapter = ko.observable(new Adapter("", "", "", ""));
self.Message = ko.observable("");
self.SetAdaptersFromJSON = function (jsData) {
self.Adapters = ko.mapping.fromJS(jsData);
};
//#region Edit List Options: confirmChanges
self.confirmChanges = function () {
if (self.NewAdapter().ID == 0) {
self.Adapters.push(self.NewAdapter());
}
};
//#endregion
//#region Adapter List Options: addAdapter, selectItem, deleteItem, saveAll
self.addAdapter = function () {
self.NewAdapter(new Adapter("", "", "", ""));
};
self.selectItem = function (item) {
self.NewAdapter(item);
};
self.deleteItem = function(item) {
self.DeleteAdapters.push(item.ID());
self.Adapters.remove(item);
};
self.saveAll = function () {
if (self.Adapters && self.Adapters().length > 0) {
var filtered = ko.utils.arrayFilter(self.Adapters(),
function(adapter) {
return ((!isEmpty(adapter.Manufacturer())) &&
(!isEmpty(adapter.Name())) &&
(!isEmpty(adapter.DeviceIDs()))
);
}
);
var updateSuccess = true;
if (self.DeleteAdapters().length > 0) {
jsonData = ko.toJSON(self.DeleteAdapters());
$.ajax({
url: "/api/AdapterList",
cache: false,
type: "DELETE",
data: jsonData,
dataType: "json",
contentType: "application/json; charset=utf-8",
success: function () { updateSuccess = true; },
error: function () { updateSuccess = false; }
});
}
var jsonData = ko.toJSON(filtered);
$.ajax({
url: "/api/AdapterList",
type: "POST",
data: jsonData,
dataType: "json",
contentType: "application/json; charset=utf-8",
success: function(data) {
self.SetAdaptersFromJSON(data);
updateSuccess = true && updateSuccess;
},
error: function () { updateSuccess = false; }
});
if (updateSuccess == true) { self.Message("Update Successfull"); }
else { self.Message("Update Failed"); }
}
};
//#endregion
}
//#endregion
ko.bindingHandlers.message = {
update: function(element, valueAccessor) {
$(element).hide();
ko.bindingHandlers.text.update(element, valueAccessor);
$(element).fadeIn();
$(element).fadeOut(4000);
}
};
ko.bindingHandlers.sortTable = {
init: function (element, valueAccessor) {
setTimeout(function () {
$(element).addClass('tablesorter');
$(element).tablesorter({ widgets: ['zebra'] });
}, 0);
}
};
function isEmpty(obj) {
if (typeof obj == 'undefined' || obj === null || obj === '') return true;
if (typeof obj == 'number' && isNaN(obj)) return true;
if (obj instanceof Date && isNaN(Number(obj))) return true;
return false;
}
The specific script portion that is failing to update my html table is:
self.deleteItem = function(item) {
self.DeleteAdapters.push(item.ID());
self.Adapters.remove(item);
};
Everything seems to work except for the remove, so I seem to be at a loss for what to look at next, and I am too new to javascript or knockout to know if this is a clue: If I run ko.applyBindings() command in the self.deleteItem function, I get the update to happen but it does give me an unhandled error:
Uncaught Error: Unable to parse bindings.
Message: ReferenceError: Message is not defined;
Bindings value: message: Message
Message was defined in the VM before binding... was there something I missed in all this?
In the beginning of your Js file you are defining var viewModel = new AdaptersModel(); but lower you are stating that function Adapter() is the view model in your region declaration. It is making your code difficult to read. I am going to take another stab at what you can do to troubleshoot, but I would suggest that your viewmodel contains the adapters and your model contains a class-like instance of what each adapter should be.
The specific error you are getting is because you are binding Message() to something and then deleting Message(). One thing you could do to trouble shoot this is to change your div to something like :
<div id="MessageDiv" data-bind="with: Message">
<h5 data-bind="message: $data"><h5>
</div>
If you could create a fiddle I could give a more definite example of why, but basically if Message() is blank the with binding should not show the header which is undefined after deletion.
What you probably need to do though is look at what is being sent as 'item' and make sure it is not your viewmodel.
self.deleteItem = function(item) {
console.log(item); // << Check console and see what is being returned
self.DeleteAdapters.push(item.ID());
self.Adapters.remove(item);
};
You are probably deleting more than just a single adapter.
This will lead you the right direction, but I would seriously consider either renaming your code.
There was a lot of help solving surrounding issues but nothing actually solved the "why" of the problem. The updates worked perfectly sometimes but not other times. When I was troubleshooting it and started to get it dumbed down and working in JSFiddle I didn't include the data-bind="sortTable: true" in all my working versions. Apparently, if you sort a table or using the code as I did it will not work. The example code I have seen floating around is here at http://jsfiddle.net/gregmason/UChLF/16/, pertinent code:
ko.bindingHandlers.tableSorter = {
init: function (element) {
setTimeout(function () { $(element).tablesorter(); }, 0);
},
update: function (element, valueAccessor) {
ko.utils.unwrapObservable(valueAccessor()); //just to get a dependency
$(element).trigger("update");
}
};
The errant behavior can be obvious by clicking the delete link on the row.
If you click on the row without sorting, you will see the row disappear correctly.
If you first click on a column to re-sort in a different order, THEN delete the row, it remains in the table and appears to have cached.
This can be handled by binding each of the table headers instead of the table itself and replacing the tableSorter code with a custom sort behavior as discussed in this thread:
knockout js - Table sorting using column headers. The sort replacement is here:
ko.bindingHandlers.sort = {
init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var asc = false;
element.style.cursor = 'pointer';
element.onclick = function(){
var value = valueAccessor();
var prop = value.prop;
var data = value.arr;
asc = !asc;
if(asc){
data.sort(function(left, right){
return left[prop]() == right[prop]() ? 0 : left[prop]() < right[prop]() ? -1 : 1;
});
} else {
data.sort(function(left, right){
return left[prop]() == right[prop]() ? 0 : left[prop]() > right[prop]() ? -1 : 1;
});
}
}
}
};
This has fixed my sorting/editing/deleting issues and a working jsFiddle is here: http://jsfiddle.net/gregmason/UChLF/18/