I am using Rails and jQuery, making an ajax call initiated by clicking a link. I setup my application.js file to look like the one proposed here and it works great. The problem I'm having is how can I use $(this) in my say.. update.js.erb file to represent the link I clicked? I don't want to have to assign an ID to every one, then recompile that id in the callback script..
EDIT
To give a simple example of something similar to what I'm trying to do (and much easier to explain): If a user clicks on a link, that deletes that element from a list, the controller would handle the callback, and the callback (which is in question here) would delete the element I clicked on, so in the callback delete.js.erb would just say $(this).fadeOut(); This is why I want to use $(this) so that I dont have to assign an ID to every element (which would be the end of the world, just more verbose markup)
application.js
jQuery.ajaxSetup({ 'beforeSend': function(xhr) {xhr.setRequestHeader("Accept", "text/javascript,application/javascript,text/html")} })
function _ajax_request(url, data, callback, type, method) {
if (jQuery.isFunction(data)) {
callback = data;
data = {};
}
return jQuery.ajax({
type: method,
url: url,
data: data,
success: callback,
dataType: type
});
}
jQuery.extend({
put: function(url, data, callback, type) {
return _ajax_request(url, data, callback, type, 'PUT');
},
delete_: function(url, data, callback, type) {
return _ajax_request(url, data, callback, type, 'DELETE');
}
});
jQuery.fn.submitWithAjax = function() {
this.unbind('submit', false);
this.submit(function() {
$.post(this.action, $(this).serialize(), null, "script");
return false;
})
return this;
};
// Send data via get if <acronym title="JavaScript">JS</acronym> enabled
jQuery.fn.getWithAjax = function() {
this.unbind('click', false);
this.click(function() {
$.get($(this).attr("href"), $(this).serialize(), null, "script");
return false;
})
return this;
};
// Send data via Post if <acronym title="JavaScript">JS</acronym> enabled
jQuery.fn.postWithAjax = function() {
this.unbind('click', false);
this.click(function() {
$.post($(this).attr("href"), $(this).serialize(), null, "script");
return false;
})
return this;
};
jQuery.fn.putWithAjax = function() {
this.unbind('click', false);
this.click(function() {
$.put($(this).attr("href"), $(this).serialize(), null, "script");
return false;
})
return this;
};
jQuery.fn.deleteWithAjax = function() {
this.removeAttr('onclick');
this.unbind('click', false);
this.click(function() {
$.delete_($(this).attr("href"), $(this).serialize(), null, "script");
return false;
})
return this;
};
// This will "ajaxify" the links
function ajaxLinks(){
$('.ajaxForm').submitWithAjax();
$('a.get').getWithAjax();
$('a.post').postWithAjax();
$('a.put').putWithAjax();
$('a.delete').deleteWithAjax();
}
show.html.erb
<%= link_to 'Link Title', article_path(a, :sentiment => Article::Sentiment['Neutral']), :class => 'put' %>
The combination of the two things will call update.js.erb in rails, the code in that file is used as the callback of the ajax ($.put in this case)
update.js.erb
// user feedback
$("#notice").html('<%= flash[:notice] %>');
// update the background color
$(this OR e.target).attr("color", "red");
jQuery already handles the this issue for you with the event properties:
$("a").click(function(e){
e.preventDefault();
$("#foo").fadeIn(3000, function(){
$(e.target).text("Foo loaded");
});
});
Note how I can refer back to the main link via its event. This is the case with any events that are handled within as well. Just give them unique names, such as e2, e3, etc. No more need to constantly write yet another var item = $(this) line to keep track of this three events back.
Online Demo: http://jsbin.com/egelu3/edit
If your JS is coming from the server, there is really no way that $(this) can operate in the same context. The closest you could get would be to load some script from the server and eval it in the context of your client-side function.
I basically have an id for each of the DOM elements I need to manipulate, and refer to them within my scripts. It is occasionally ugly, but the alternatives are worse.
If your JS is coming from the server,
there is really no way that $(this)
can operate in the same context. The
closest you could get would be to load
some script from the server and eval
it in the context of your client-side
function.
Not true
I basically have an id for each of the
DOM elements I need to manipulate, and
refer to them within my scripts. It is
occasionally ugly, but the
alternatives are worse.
I don't think this is ugly.
The key to this problem is functional scoping. Let me show you what I mean. You need to create a function that is called before you send your XHR call. In your case, you're doing it with a click event, so let me show you an example tailored for you:
$( '#somelink' ).click( function( )
{
// this stores the location of the current "this" value
// into this function, and will available even after we
// end this function (and will still live while the XHR's
// callback is being executed
var theLink = this;
// fire the AJAX call
$.post
(
'some/url',
{ 'some':'data' }, // optional
function( )
{
// use theLink however you want
// it'll still be there
// also, if you're sending in callbacks
// as variables, you can safely say
hideAndStore.call( theLink, data );
// which executes callback( ), overriding
// "this" with theLink (your DOM node)
// and sending in the responseText as the
// first argument
}
);
} );
and then you could make your callback something like:
function hideAndStore( response )
{
// you can safely use "this" as the DOM node
$( this ).css( { 'display':'none' } );
// and you can do whatever you wish with the response
window.globalData = response;
}
where you'd make it do whatever you actually want it to do, haha.
For more info about functions in JavaScript that change the "this" value, check out .apply and .call at MDC
https://developer.mozilla.org/En/Core_JavaScript_1.5_Reference/Objects/Function/Apply https://developer.mozilla.org/En/Core_JavaScript_1.5_Reference/Objects/Function/Call
What are you doing in the javascript you are sending back? Maybe you can send back some html or json and operate on it in a callback.
$('a:clickable').bind('click', function() {
var elem = $(this);
$.ajax({
url: ...,
dataType: 'json',
success: function(data) {
// elem is still in scope
elem.addClass(data.class_to_add_to_link);
}
});
});
This cannot be accomplished, The method in which i am trying to do this makes it impossible, i cannot pass references to javascript objects through views.
Solution was to assign IDs to each item, and refer to them by that.
Related
I am having trouble in updating span tag in my html file. I am getting JSON object from server and its showing me in console.log too. But When I try to update it on span tag in AJAX:Success it doesn't work. If I call same line outside the success tag, it does work there.
AJAX.JS
$('a.up_vote').click(function(e) {
e.preventDefault();
$(this).siblings("span").css( "background-color", "green" );
$.ajax({
url: $(this).attr('href'),
type :'get' ,
success : function (data){
$(this).find("span").css( "background-color", "red" );
$(this).siblings('span').html(data.count);
$(this).siblings("span").css( "background-color", "red" );
},
failure : function (data){
alert('failure') ;
}
}) ; // ajax call
}); // upvote link call
HTML
<div class ='up' style="float:left">
<a href='{% url 'upvote-detail' post.id %}' class='up_vote'>Up vote</a>
<span> {{ post.upvote }} </span>
</div>
The issue is your use of $(this)
Using the context of the 'this' keyword with jQuery
https://remysharp.com/2007/04/12/jquerys-this-demystified
A simple way is to store referance to $(this) then use it later.
For example:
$('a.up_vote').click(function(e) {
e.preventDefault();
window.alert("hello");
console.log("hello there");
var $that = $(this);
$that.siblings("span").css( "background-color", "green" );
$.ajax({
url: $that.attr('href'),
type :'get' ,
success : function (data){
// alert('success');
console.log('hello from success ajax')
console.log(data.count);
$that.find("span").css( "background-color", "red" );
$that.siblings('span').html(data.count);
$that.siblings("span").css( "background-color", "red" );
// $('#upvote_count').html(data.count);
console.log('siblings passed')
},
failure : function (data){
alert('failure') ;
}
}) ; // ajax call
}); // upvote link call
The $that is just a var name starting with a $, not specific to jquery as such but a useful habit for variable that store jquery objects (including $() wrapped DOM elements, as those are jquery objects too)
The $that = $(this) is a useful pattern to apply, using whatever variable names you want.
Also, a simple console call to check key variables is always recomended when something doesnt work
console.log('debug',$(this));
You just hit F12 and go to console tab (or google your browser name + dev console if nothing happens) and see what is printed there (or even use breakpoints or other debugging method, see link)
Debugging Links
Chrome DevTools: https://developer.chrome.com/devtools
Debugging JS in Chrome: https://developer.chrome.com/devtools/docs/javascript-debugging
$(this) inside the success callback is not referencing the clicked item anymore.
You have to reference it directly or use a temporary variable like:
var clickedItem = $(this); // before ajax call
and then inside success callback try
$(clickedItem) instead of $(this)
I hope this works for you; please let me know.
Here you have a nice explanation of 'this' keyword use inside callbacks.
$('a.up_vote').click(function (e) {
e.preventDefault();
$(this).siblings("span").css("background-color", "green");
$.ajax({
url: $(this).attr('href'),
type: 'get',
success: function (data) {
$('a.up_vote').siblings("span").css("background-color", "red");
$('a.up_vote').siblings('span').html(data.count);
},
failure: function (data) {
alert('failure');
}
}); // ajax call
}); // upvote link call
try this one.
I want to prevent from adding a category to the Select2 element if it fails creating the row first in my db. The action is not prevented when i call ev.preventDefault(); Nothing happens.. what is wrong?
$('#sel2').select2({
placeholder: 'Enter categories',
minimumInputLength: 3,
multiple: true,
ajax: {
url: 'async/get_categories.php',
dataType: 'json',
quietMillis: 250,
data: function (term, page) {
return {
q: term,
};
},
results: function (data, page) {
return {
results: data.items
};
},
cache: true
},
formatResult: format,
formatSelection: format
}).on('select2-selecting', function(e) {
console.log(e);
if (e.val == 4) {
// if category id equals 4
// do not add this category to select 2
// e.preventDefault();
// the above works just fine and its just for testing
}
// Is something wrong here?
var ev = e;
$.ajax({
type: 'POST',
url: 'async/create_profile_category.php',
data: {
profile_id: '1',
category_id: ev.val
},
success: function(response) {
console.log(response);
if (response.error === false) {
// category assigned successfully
} else {
// failed to assign category
// so i want now to prevent from adding to select2
console.log('should not add this category');
ev.preventDefault();
// the above is not working
}
},
error: function() {
alert('Failed to assign category!');
}
});
});
The AJAX request is made asynchronusly, so by the time it has finished the element has already been added. Even though you are calling ev.preventDefault(), it is too late for it to make a difference. So this leaves you with two options:
Make the request synchronusly, which will allow preventDefault to make the difference.
Make the request asynchronusly, and manually remove the element if it fails.
Both options have their pros and cons, and it's up to you to decide which option you go with.
Making the request synchronusly
Pros
The value will never be added if the request fails.
Works well in cases where the element cannot be added quite often.
Cons
Blocks the UI - So the user is potentially left with an unresponsive page while the request is made.
Making the request asynchronusly
Pros
Does not block the UI.
Works well in cases where elements typically can be added.
Cons
The value will always show up for the user, even if it fails later.
You must manually unset the new option.
What's important to consider here is the user experience of both options. When making synchronus requests, it's not uncommon for the browser to stop relaying events - which gives the illusion that the UI has locked up and the page has gone unresponsive. This has the benefit of ensuring that the value never shows up if it isn't allowed. But if users typically can add the elements, it also has the downside of complicating the most common use case.
If users can usually add elements, then it is a better experience to add the element while the request is being made, and then notifying the user later (while removing the element) if there was an issue. This is very common is web applications, and you can see it being used in many places, such as the Twitter and Facebook like buttons (where requests usually work), as well as places on Stack Overflow.
There is a way to get around this with version4 of the select2 library.
on select2:selecting we cancel the preTrigger event. Which will stop the select2:select event. We do our ajax call. On success we then get out Select2 instance then call the trigger of the Observer that way it by passes overwritten trigger method on your select2 instance.
The call method needs your select2 instance as the context so that the existing listeners are available to call.
var sel = $('#sel');
sel.select2(config);
sel.on('select2:selecting', onSelecting);
function onSelecting(event)
{
$.ajax({
type: 'POST',
url: 'async/create_profile_category.php',
data: {
profile_id: '1',
category_id: event.params.args.data.id
},
success: function(event, response) {
console.log(response);
if (response.error === false) {
// category assigned successfully
// get select2 instance
var Select2 = $users.data('select2');
// remove prevented flag
delete event.params.args.prevented;
// Call trigger on the observer with select2 instance as context
Select2.constructor.__super__.trigger.call(Select2, 'select', event.params.args);
} else {
// failed to assign category
// so i want now to prevent from adding to select2
console.log('should not add this category');
}
}.bind(null, event),
error: function() {
alert('Failed to assign category!');
}
});
event.preventDefault();
return false;
}
here how I did it for yii2 Select2 integrated into Gridview:
'pluginEvents' => [
'select2:selecting' => "
function(event)
{
var select2 = $('#types-" . $model->id . "');
select2.select2('close');
$.post('update',{id: " . $model->id . ", type_id: event.params.args.data.id})
.done (function(response)
{
select2.val(event.params.args.data.id);
select2.trigger('change');
})
.fail(function(response)
{
krajeeDialog.alert('Error on update:'+response.responseText);
});
event.preventDefault();
return false;
}",
],
it allows to asynchoronous update data in the grid using select2 and ajax and return it to previous value if there was an error on updating.
I'm trying to select a row from a json array using jquery. This is what i have:
$(document).ready(function() {
$.getJSON( "js/collectie.json", function(data) {
jsoncollectie = data;
})
$( "#collectie li" ).click(function(){
var thumb_id = $(this).data("id");
for(var i = 0; i < jsoncollectie.stoelen.length; i++){
if(jsoncollectie.stoelen[i].ref == thumb_id){
$("#detailimage").attr('src', jsoncollectie.stoelen[i].image);
$("#detailimage").attr('title', jsoncollectie.stoelen[i].title);
$("#title").html('<h4> '+jsoncollectie.stoelen[i].naam+' </h4>');
$("#secondaryimage").attr('src', jsoncollectie.stoelen[i].secondaryimage);
$("#secondaryimage").attr('title', jsoncollectie.stoelen[i].secondarytitle);
$("#description").html('<p> '+jsoncollectie.stoelen[i].description+' </p>');
}
}
});
});
Now when i click on a list item (#collectie li) the console outputs "ReferenceError: jsoncollectie is not defined". I don't know why it's doing that and i'm pretty sure it worked two weeks ago. Don't know much about javascript/jquery yet, but i'm slowly learning.
$(document).ready(function()
{
// Provide access to data outside of the getJSON call
var m_oJsonCollectie = null;
// Get the data
$.getJSON( "js/collectie.json", function(data)
{
// Set the data
m_oJsonCollectie = data;
// Apply the click handler
$( "#collectie li" ).click(function()
{
var thumb_id = $(this).data("id");
for(var i = 0; i < m_oJsonCollectie.stoelen.length; i += 1)
{
if(m_oJsonCollectie.stoelen[i].ref == thumb_id)
{
$("#detailimage") .attr('src', m_oJsonCollectie.stoelen[i].image);
$("#detailimage") .attr('title', m_oJsonCollectie.stoelen[i].title);
$("#title") .html('<h4> '+ m_oJsonCollectie.stoelen[i].naam+' </h4>');
$("#secondaryimage").attr('src', m_oJsonCollectie.stoelen[i].secondaryimage);
$("#secondaryimage").attr('title', m_oJsonCollectie.stoelen[i].secondarytitle);
$("#description") .html('<p> '+ m_oJsonCollectie.stoelen[i].description+' </p>');
}
}
});
});
});
JS have block level scope, so you wont get the values outside of the function unless you provide access to them or they are declared in global scope (which is considered bad practice).
This pattern should help you keep your data accessible, and only applies the click handler if the getJSON call is successful.
Check that your getJSON request is being received and returned by using deferred methods
// Syntax that will shed light to your issue :
$.getJSON
(
"js/collectie.json",
function (oJSON) { /*success*/ }
)
.done(function() { /* succeeded */ })
.fail(function() { /* failed */ })
.always(function() { /* ended */ });
I came to this conclusion due to comments and the fact that a variable only declared in the success handler for getJSON was undefined. Since the JSON containing variable was undefined, the success handler must never have been called. Chances are that the path to the JSON you are trying to get is incorrect.
Documentation for the methods to accomplish :
getJSON
done
fail
always
UPDATE
Knowing that the response is 304, and the results are undefined are the important details here. This issue has been addressed by jQuery already here
This is actually correct, given the ifModified header has not been set to false.
To fix this issue, use ajaxSetup() to modify the header.
NOTE : the use of this method is not recommended by jQuery, but in this case it works.
// place this is document ready handler before making any calls.
$.ajaxSetup({ ifModified : false });
When I make an AJAX call to replace a div, the response contains a script tag pointing to an external .js file. However, I can't get the returned JS to execute. I've tried to eval() the response but that didn't work. I also tried to call a function inside the external .js file from within the onComplete callback, but this also does not work. Not sure what else to do. I'm using mootools core 1.4.5
Main page's JS
window.addEvent('domready', function(){
function ajaxfunc(i)
{
return function(e){
e.stop();
var requestData = new Request({
url: 'blah.php?cat=' + i,
evalScripts: true,
evalResponse: true,
onComplete: function(response){
$('rt-main').set('html', response);
}
});
requestData.send();
};
}
var total = $('cat_table').getChildren('div').length;
for(var i=1; i<=total; i++)
{
$('catClick'+i).addEvent('click', ajaxfunc(i));
}
});
The returned HTML
<script src="listings.js" type="text/javascript"></script>
...(other markup, etc)
And inside that listings.js file
window.addEvent('domready', function(){
function gotoItem(i)
{
return function(e){
e.stop();
var id= i;
var requestData = new Request ({
url: 'blah.php?id='+id,
onComplete: function(response){
$('rt-main').set('html', response);
}
});
requestData.send();
};
}
$$('.itemBox').each(function(el){
el.getElement('a.itemClick').addEvent('click', gotoItem(el.id));
});
});
The environment I'm working in is Joomla 3.1 in case that affects anything.
No domready will fire a second time for you as per listings.js, your DOM is already ready.
You could manually do window.removeEvents('domready') beforehand, then load via XHR and do window.fireEvent('domready') to run it.
If you use event delegation you can avoid having to run any js after initial ajax requests, all you'd need is something like this.
window.addEvent('domready', function () {
var ct = document.id('cat_table'),
divs = ct.getChildren('div'), //need a more qualified selector
rtMain = document.id('rt-main');
divs.each(function(el, i){
// store the index, if you don't have it as an attribute like id, rel or data-id
el.store('index', i);
});
ct.addEvent('click:relay(div)', function(e){ // needs more qualified also.
e && e.stop();
new Request({
method: 'get', // not post, check - faster
url: 'blah.php?cat=' + this.retrieve('index'),
evalResponse: true,
onComplete: function(){
rtMain.set('html', this.response.text);
}
}).send();
});
// delegation, assumes .itemBox are children of rtMain - just delegate to other parent otherwise.
rtMain.addEvent('click:relay(.itemBox)', function(e){
// reliance on index in collection is bad, try to change response above to contain data-id.
e.stop();
new Request({
method: 'get',
url: 'blah.php?id=' + this.get('data-id'),
onComplete: function(){
rtMain.set('html', this.response.text);
}
}).send();
});
});
keep in mind you had a reliance on the index of the item in the Elements collection, which is less than safe. use a hard db id, provided by the backend. this is insecure, ppl can mod their DOM, delete elements and get to ids they should not see. Not that they can't do so otherwise but...
Bringing in scripts via XHR and evaling responses is an anti-pattern and gives a vector of attack on your page for XSS, don't forget that.
It looks like the script in the linked .js is wrapped in a 'domready' event, but (I'm assuming a little here) the dom would already have fired the ready event. Try removing the window.addEvent('domready', function(){ ...} wrapper.
EDIT since you say that is not the case could it work for you if instead of returning the script tag, you simply returned the script and appended it to the page like this:
onComplete: function (response) {
//
var script = document.createElement('script');
script.innerHTML = response;
document.getElementById('rt-main').appendChild(script);
}
EDIT I've just played with this a little, and it seems I have an inelegant solution fiddle.
I have a simple jQuery function that resizes text areas, and I want it to apply to all text areas.
For the most part, this works great:
$(document.ready(function(){$("text_area").resizer('250px')});
However, because it is only called once when the document is ready, it fails to catch text areas that are later added onto the page using Ajax. I looked at the .live() function, which seems very close to what I'm looking. However, .live() must be bound to a specific event, whereas I just need this to fire once when they're done loading (the onLoad event doesn't work for individual elements).
The only thing I can get working is a really obtrusive inclusion of the JavaScript call directly into the Ajax. Is that the recommended way to be doing this?
Edit: Here is the rails source code for what it does for Ajax requests:
$('a[data-confirm], a[data-method], a[data-remote]').live('click.rails', function(e) {
var link = $(this);
if (!allowAction(link)) return false;
if (link.attr('data-remote') != undefined) {
handleRemote(link);
return false;
} else if (link.attr('data-method')) {
handleMethod(link);
return false;
}
});
// Submits "remote" forms and links with ajax
function handleRemote(element) {
var method, url, data,
dataType = element.attr('data-type') || ($.ajaxSettings && $.ajaxSettings.dataType);
if (element.is('form')) {
method = element.attr('method');
url = element.attr('action');
data = element.serializeArray();
// memoized value from clicked submit button
var button = element.data('ujs:submit-button');
if (button) {
data.push(button);
element.data('ujs:submit-button', null);
}
} else {
method = element.attr('data-method');
url = element.attr('href');
data = null;
}
$.ajax({
url: url, type: method || 'GET', data: data, dataType: dataType,
// stopping the "ajax:beforeSend" event will cancel the ajax request
beforeSend: function(xhr, settings) {
if (settings.dataType === undefined) {
xhr.setRequestHeader('accept', '*/*;q=0.5, ' + settings.accepts.script);
}
return fire(element, 'ajax:beforeSend', [xhr, settings]);
},
success: function(data, status, xhr) {
element.trigger('ajax:success', [data, status, xhr]);
},
complete: function(xhr, status) {
element.trigger('ajax:complete', [xhr, status]);
},
error: function(xhr, status, error) {
element.trigger('ajax:error', [xhr, status, error]);
}
});
}
So in my particular case, I've got a link, that has data-remote set to true, which points to a location that will return JavaScript instructing a form containing a text area to be appended to my document.
A simple way to do this would be to use ajaxComplete, which is fired after every AJAX request:
$(document).ajaxComplete(function() {
$('textarea:not(.processed)').resizer('250px');
});
That says "every time an AJAX request completes, find all textarea elements that don't have the processed class (which seems to be added by the resizer plugin -- terrible name for its purpose!) and call the resizer plugin on them.
You may be able to optimise this further if we could see your AJAX call.
Generally speaking, I would do it this way..
$.ajax({
type : "GET",
url : "/loadstuff",
success: function(responseHtml) {
var div = $("#containerDiv").append(responseHtml);
$("textarea", div).resizer("250px");
}
});
Wondering if you could use .load for this. For example:
$('text_area').load(function() {
$("text_area").resizer('250px');
});