What I've got is an ASP.NET MasterPage/ContentPage, where the ContentPage utilizes an UpdatePanel with UpdateMode set to "Conditional". This ContentPage is basically a MultiView that has 3 views: Setup, Confirm, and Complete. I've got navigation buttons that when clicked, go to the server, do what they need to do and finally update the UpdatePanel so it can come back. My problem lies in my JavaScript.
I have the following Global object literal named PAGE:
PAGE = {
panel : undefined,
currentView: undefined,
buttons : {
all : undefined,
cont : undefined
},
views : {
setup : {},
confirm : {},
complete : {}
}
};
My PAGE.init() function looks like this:
PAGE.init = function() { console.log("PAGE.init() fired");
this.panel = $('div[id$="Panel_Page"]');
this.currentView = this.panel.find('input[id$="Hidden_CurrentView"]').val();
this.buttons.all = this.panel.find('input[type="submit"]');
this.buttons.cont = this.panel.find('input[id$="Button_Continue"]');
this.buttons.all.click(function() { PAGE.panel.hide(); });
switch (this.currentView) {
case "confirm" : this.views.confirm.init(); break;
case "complete" : this.views.complete.init(); break;
default : this.views.setup.init(); break;
}
};
And last, but not least, it all gets kicked off by:
// Fire events on initial page load.
PAGE.init();
// Fire events for partial postbacks.
Sys.WebForms.PageRequestManager.getInstance().add_endRequest(PAGE.init);
My issue is that when it first fires Page.init() everything is great, however, when you click a button it immediately throws an error of: Uncaught TypeError: Cannot set property 'all' of undefined. Now I've tried to figure this out, but I'm at a loss. It seems as though this happens to ANY nested object literal off of the root of PAGE. The immediate properties like PAGE.panel work just fine, but as soon as you need access to PAGE.buttons.all or PAGE.views.setup, it throws this error. I have never seen this before.
Any ideas out there?
Use a getter for your panel property. Seeing as everything else appears to be a property of panel this should do it - you might be losing the reference to panel during the postback, so instead of getting the object once, get it every time:
get_panel: function() { return $get("Panel_Page"); }
Again, if I just have this.panel = $get(myElement) and then my partial page update destroys the node that was returned to this.panel when I created my object initially, this.panel will become undefined. Use a getter.
Hope that helps - happy coding.
EDIT:
Actually - now that I look at it again, you'll probably want to use getters for all of your properties instead of relying on get_panel().find(..) for currentView, for example, I'd do another getter there:
get_currentView: function() { return $get("Hidden_CurrentView", this.get_panel()); }
B
Instead of using this inside of the PAGE.init() function, I used PAGE and that corrected the issue. Apparently this was not referring to PAGE but rather PAGE.init. Still not sure why, as I do this elsewhere and it works.
For instance, the following use of this works and it's no different than how I use it in my OP:
PAGE.views.setup = {
buttons : {
all : undefined,
cont : undefined
},
init: function() {
var self = this,
buttons = self.buttons;
buttons.all = PAGE.panel.find('input[type="submit"]');
buttons.all.click(function() { alert(this.id + " clicked"); });
}
}
Related
Context
I have been tasked with fixing a big bug on the menu-edit page, which was caused by a stale element issue, caused by the HTML elements for it being rendered server-side. In my three-day fight against this bug, I got some inspiration from Angular and decided to try to make a menu state that will power everything on the page (adding/removing categories/items, and later, pagination of the modals for the adding)
Some Code
I came up with this IIFE (to be the "controller" of the MVC. Selector modals hit the add methods of this, and delete buttons hit the remove methods of this. Also, this gets passed to template-render function, which is literally the first thing hit when a modal gets popped):
/* all the categories, items, and modifiers that power this page */
var menuState = (function() {
let _categories = {
attached: [],
available: []
}, _items = {
attached: [],
available: []
}, _modifiers = {
attached: [],
available: []
}
function getExposedMethodsFor(obj) {
return {
all : function() { return obj.attached.concat(obj.available) },
attached : function() { return obj.attached },
available : function() { return obj.available }
// ... other methods that work on obj.attached,obj.available
}
}
let categoryExposedMethods = getExposedMethodsFor(_categories)
// other exposer objects
return {
getAllCategories : categoryExposedMethods.all,
getAttachedCategories : categoryExposedMethods.attached,
getAvailableCategories : categoryExposedMethods.available
// the rest of the exposed methods irrelevant to this question at hand
}
})()
OK, so what's the problem?
The problem is that this is false sense of security, it seems. When I try to XSS-test this structure alone, it fails.
I test it with three entities in _categories, all of which are attached, causing
menuState.getAllCategories().length
to return 3 and
menuState.getAvailableCategories().length
to return 0. Good news is that when I tried
menuState.getAllCategories().push('a')
menuState.getAllCategories().length
I still get three.
However, when I go
menuState.getAvailableCategories().push('b')
menuState.getAvailableCategories().length
I get 1, instead of 0 !!
Is there truly a way to lock down the other getters here?! If not, what are my alternatives?
I fixed it with Object.freeze, which I already used for refactoring the "enums" the dev before me wrote when he was working on this project. What it does is fully protect a state from any type of changes, including:
adding properties
deleting properties
modifying properties
re-assigning the object/array being "frozen"
How I use it
In the helper method, I did the following :
attached : function() { return Object.freeze(obj.attached) },
available : function() { return Object.freeze(obj.available) },
This prevents the arrays being changed from those methods, thus shutting down this type of XSS. Also, menuState was declared with const.
Need to set the attribute (isOpen) of an element. When I "hardcode" the value to true or false it works but when I get the value from the "test" method it sets the value and the icon changes but the accordion doesn't open and close.
_title = d.create('h1')
.setClasses(['h2', 'pull-left'])
.setAttributes([['id', ++number + "values"], ['ng-click', 'isOpen = test(isOpen)'], ['ng-init', 'isOpen = true']])
.setInnerHTML(_titleHtml)
.toElement();
This is the method it calls:
scope.test = function (isOpen) {
isOpen = !isOpen;
return isOpen;
}
The compiled code from the inspector is as follows:
I have read about using $apply and $compile but not sure how to make it work.
Appreciate the help.
UPDATE:
I am including a bit more explanation to help you help me. I need to send the isOpen value to the test method because I need to implement additional logic elsewhere depending on whether the isOpen is true or false at that time.eg: if another button is clicked, if the the accordion is already open do nothing but if closed expand etc
_title = d.create('h1')
.setClasses(['h2', 'pull-left'])
.setAttributes([['id', ++number + "values"], ['ng-click', 'isOpen=!isOpen'], ['ng-init', 'isOpen=true']])
.setInnerHTML(_titleHtml)
.toElement();
There is no need to use a method....
I have this problem with MessageBox component in ExtJS 3.4 and I'm searching desperately for a solution.
function supprimerCatalogPreEnregFunction() {
Ext.Msg.show({
msg: document.getElementById('confirmDeleteMessage').value,
buttons: Ext.Msg.YESNO,
icon: Ext.MessageBox.ERROR,
fn : function (btn) {
if (btn == 'yes') {
document.getElementById('deleteForm:deleteCatalogPreEnreg').onclick();
}
}
});
}
The problem is that my btn return value 1, not 'yes' or 'no' as i expect. And it drives me crazy because I've trying a lot of solutions and I can't understand why this happens.
This function is a handler for a new Ext.Button.
The button is part of a new Ext.Panel, buttons:[...].
I can't understand why the button has that strange value and it frustrates me a lot.
Can a missing comma produce this behaviour? Although, I didn't found a missing comma.
L.E:
I've researched more and I looked more carefully in my code and, helped by debugger, I found that my function
function (btn){}
receives as arguments on position 0: value 1, on second position receives Window (my current location) and the third argument is the one wich should've been received by my component, and it looks like ["yes", "", Object{}] etc.
And i think this is the argument I need, where 0st position is the value of my btn, but I don't know where the other arguments come from to know what I need to do to in order to make it work.
As I'm still a little confused, I'll update this with a general explanation about how I've implemented this handler.
So, at Ext.onReady I load a function init()
Then, in this function i made a var deleteButton = new Ext.Button
This button has a handler which is my initial function from question
The deleteButton is added to a new Ext.Panel with buttons:[deleteButton, etc]
This panel is added as item to a Ext.TabPanel
And, finally, TabPanel is added to a ViewPort.
The Sencha Documentation shows the implementation like:
// Prompt for user data and process the result using a callback:
Ext.Msg.prompt('Name', 'Please enter your name:', function(btn, text){
if (btn == 'ok'){
// process text value and close...
}
});
Running the following code using ExtJs 3.4:
Ext.onReady(function(){
Ext.BLANK_IMAGE_URL = '/js/ext-3.4.0/resources/images/default/s.gif';
Ext.Msg.show({
msg: "test",
buttons: Ext.Msg.YESNO,
icon: Ext.MessageBox.ERROR,
fn : function (btn, text) {
if (btn == 'yes') {
console.log(btn, text);
}
}
});
});
Output (Firebug console):
yes (an empty string)
So your code should be working. I would check to see if there are any global overrides being loaded and test it in isolation
Looking at the source it seems that the first param is the button clicked (Ext.Msg.YESNO which may give you a truthy result) but the second parameter passed is the text value you are looking for:
var handleButton = function(button){
buttons[button].blur();
if(dlg.isVisible()){
dlg.hide();
handleHide();
//here the arguments are button, activeTextEl.dom.value and opt... so look at the second argument in your callback
Ext.callback(opt.fn, opt.scope||window, [button, activeTextEl.dom.value, opt], 1);
}
};
I'd try changing your code to:
function (btn, btnTxt) {
if (btnTxt == 'yes') {
document.getElementById('deleteForm:deleteCatalogPreEnreg').onclick();
}
}
First of all, thank you very much for replies.
It seemed that the problem was from a script that I was included it in my jsf page.
And that messed up parts of my code and that's why my component had a strange bahviour.
I solved it by removing that script and let the function as I posted it initially.
I started developping a website using backbone.js and after trying during the whole morning, i'm quite stuck on the following problem.
I output here only the relevant code.
I've a View called Navigator, that contains a Collection of Records (initially empty) :
var NavigatorView = Backbone.View.extend({
template: JST['app/scripts/templates/Navigator.ejs'],
tagName: 'div',
id: '',
className: 'saiNavigator',
events: {},
initialize: function () {
this.currentRecords = new RecordsCollection();
this.currentRecords.on('reset', this.onRecordsCollectionReseted.bind(this));
},
onRecordsCollectionReseted: function(){
this.render();
},
render: function () {
var tplResult = this.template({
computeTemplate: this.computeTemplate,
records: this.currentRecords
});
this.$el.html(tplResult);
},
onDOMUpdated: function(){
var me = this;
var data = {
device : 'web',
gridId : this.model.get('gridId'),
filterId : this.model.get('filterId')
};
$.ajax({
url: App.getTokenedUrl() + '/task/getGridData.'+this.model.get('taskId')+'.action',
success: me.onRecordReceived.bind(me),
statusCode: {
500: App.handleInternalError
},
type: 'GET',
crossDomain: true,
data : data,
dataType: 'json'
});
},
onRecordReceived: function(result){
var newRecords = [];
for(var i = 0; i < result.items.length; i++){
var newRecord = new RecordModel(result.items[i]);
newRecords.push(newRecord);
}
this.currentRecords.reset(newRecords);
}
});
I've a View called dossier which html is
<div id="dossier1" class="dossier">
<div id="dossier1-navContainer" class="navigatorContainer"/>
<div class="pagesNavigatorContainer"/>
<div class="pagesContainer"/>
<div class="readOnlyFiche"/>
</div>
When i first render the dossier (and i render it only once) i create the navigator in the following render function
render: function () {
this.$el.html(this.template({
uniqBaseId: this.id,
className: this.className
}));
var nav = this.navigator = new NavigatorView({
model : this.model,
id: this.id+'navigator',
el: $('#'+this.id+'-navContainer')
});
this.navigator.render();
//We notify the navigator that it's ready. This will allow the nav to load records
nav.onDOMUpdated();
}
}
As we can see, i give the '#dossier1-navContainer' id to the navigator so that he renders there
So, here is how it works. When i render the dossier, it creates a navigator and inserts it in the DOM. When done, i notify the navigator that it can load its data from the server trough ajax request. When i receive the answer i reset the collection of data with the incoming record.
Juste before the this.$el.html(tplResult) in the navigator render function i output the resulting string.
First time it's
<div class="items"></div>
Second time when i get records, it's
<div class="items">
<div>item1</div>
<div>item2</div>
<div>item3</div>
</div>
So the template generation is correct. However, when the second rendering occurs, the this.$el.html(tplResult) does NOTHING. If i look at the DOM in the browser NOTHING CHANGED
However if i replace this line by
$('#dossier1-navigator').html(tplResult)
it works. Which means that the first time, $('#dossier1-navigator') and this.$el are the same object, the second time not.
I've NO idea why it doesn't work the second time with the standard this.$el.
Help!!
Thanks in advance
Edit : after discussing a lot with Seebiscuit, i'm adding the few lines that helped answering the question
newTask.render();
var taskHtml = newTask.$el.html();
$('#mainTaskContainer').append(taskHtml);
My hunch is that your having a binding problem. I would suggest that you replace
this.currentRecords.on('reset', this.onRecordsCollectionReseted.bind(this)); },
in your initialize, with:
this.listenTo(this.currentRecords, "reset", this.render);
No need to specially bind. Backbone's listenTo bids the callback to the Backbone object that sets the listener (the this in this.listenTo). Also has the added benefit that when you close the view (by calling this.remove()) it'll remove the listener, and help you avoid zombie views.
Try it out.
I think the problem is that you are not using what your are passing to your navigatorView;
In your navigatorView try this:
initialize:function(el) {
this.$el=el
...
}
Let me know if it helps
After countless minutes of discussion with seebiscuit, we came up with the solution. The problem is all on the definition of the $el element. The formal definition defines it as
A cached jQuery object for the view's element. A handy reference instead of re-wrapping the DOM element all the time
This is actually not very exact from a standard cache point of view. From my point of view at least the principle of a cache is to look for the value if it doesn't have it, and use it otherwise. However in this case this is NOT the case. As Seebiscuit told me,
Because when you first bound this.$el = $(someelement) this.$el will always refer to the return of $(someelement) and not to $(someelement). When does the difference matter?
When the element is not in the DOM when you do the assignment
So actually, $el holds the result of the first lookup of the selector. Thus, if the first lookup misses then it won't succeed ever! Even if the element is added later.
My mistake here is to add the main dossierView into the DOM after rendering its NavigatorView subview. I could have found the solution if the $el was a real cache as the 2nd rendering in the ajax callback would have found the element. With the current way $el works i had just nothing.
Conclusion : make sure every part of your view is properly rendered in the DOM at the moment your try to render a subview.
I have an array of hooks in jQuery that are executed before I load data into a grid. In one case, however, I want to remove the hook, then add it back for later. Whatever I'm doing is not working just right... it's probably a syntax error because I'm still somewhat new to jQuery. Any help would be appreciated, thanks!
Current code:
var preLoad = this.opts.hooks.preLoad.pop();
//stuff happens
//now I want to add the preLoad hook back
this.opts.hooks.preLoad.push(function(report) { preLoad(report); });
EDIT
It turns out the issue lies elsewhere in the code. However, I'd still like to know how best to accomplish this.
You access it the same way as any other variable stored in any other array.
this.opts.hooks.preLoad[0](myReport)
Can you not just add the function you removed like this?
var preLoad = this.opts.hooks.preLoad.pop();
//stuff happens
//now I want to add the preLoad hook back
this.opts.hooks.preLoad.push(preLoad);
And are you sure it's always the last one in the array that you want to remove?
It probably has to do with the fact that you are "canning" the argument "report" when you push the function back on the stack.
Try doing it like that:
var preLoad = this.opts.hooks.preLoad.pop();
//stuff happens
//now I want to add the preLoad hook back
this.opts.hooks.preLoad.push(preLoad);
I've tested it here http://jsfiddle.net/fWRez/
The example you gave has nothing to do with jQuery and is pure Javascript. Also, beware that what you are doing in your example is... not right. Consider this :
var ReportManager {
...
replace: function(report) {
var preLoad = this.opts.hooks.preLoad.pop();
//stuff happens
//now I want to add the preLoad hook back
this.opts.hooks.preLoad.push(function(report) { preLoad(report); });
}
}
If you execute this :
replace(null);
replace({foo:'bar'});
replace(null);
Your this.opts.hooks.preLoad array will look like this :
Array(
0: function(report) { return function(report) { return function(report) { ... } } }
)
Because you are pushing the function wrapped into itself every time you execute your code. I'm not sure why you need to pop and push it back in again, but this just look odd.
Also, Javascript is a very flexible language; which mean that you can do many weird stuff, like
"hello".concat(" world"); // -> 'hello world'
0.toString(); // -> '0'
(function(a) { return a; })("foo"); // -> 'foo'
(function() { return false; })() || (function() { return true; })(); // -> true (executes both functions)
(function(i) { return [i*2,i*3,i*4]; })(2)[1]; // -> 6
$('selector')[0]; // ...
// etc.