I built a plugin that's supposed to transform any button into a modal style form, given a url where the form can be fetched.
It works fine with only one element, but when the selector returns multiple elements, all buttons use the last element's data when the get & post methods are called inside the plugin.
I've tried several answered question in SO, but haven't been able to locate and fix the bug. Looks like I'm missing something.
Here's the complete code. You'll see some {% django tags %} and {{ django context variables }} but just ignore them.
Thanks!
A.
EDIT: typo; EDIT2: added html; EDIT3: removed django tags and context vars.
<div class="modal fade" id="modal-1234" data-uuid="1234">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
<h4 class="modal-title" id="title-1234">Title</h4>
</div>
<div class="modal-body" id="body-1234">Body</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal" id="cancel-1234">Close</button>
<button type="button" class="btn btn-primary" id="confirm-1234">Save changes</button>
</div>
</div>
</div>
</div>
<script type="text/javascript">
(function($){
// define the modalform class
function ModalForm($button){
/*
You can use ModalForm to automate the ajax-form-modal process with TB3.
Usage:
var mf = new ModaForm($('#my-button')); // that's it
*/
self = this;
self.$button = $button;
self.$modal = $('#modal-1234');
// get vars
self.target = self.$button.attr('data-target');
self.uuid = self.$modal.attr('data-uuid');
self.$modal_title = $('#title-' + self.uuid);
self.$modal_body = $('#body-' + self.uuid);
self.$modal_confirm = $('#confirm-' + self.uuid);
self.modal_confirm_original_text = self.$modal_confirm.html()
self.$modal_cancel = $('#cancel-' + self.uuid);
self.$alerts = $('[data-exsutils=push-alerts]').first();
self.$spinner = $('<p class="center"><i class="ace-icon fa fa-spinner fa-spin orange bigger-300"></i></p>');
// bind button click to _get
self.$button.on('click', function(e){
e.preventDefault();
self._get();
});
}
ModalForm.prototype._get = function(){
/*
Issue a get request to fetch form and either render form or push alert when complete.
*/
var self = this;
// show modal, push spinner and change title
self.$modal.modal('show');
self.$modal_body.html(self.$spinner);
self.title = typeof(
self.$button.attr('data-title')) !== 'undefined' ?
self.$button.attr('data-title') : 'Modal form';
self.$modal_title.html(self.title);
// get content
$.ajax({
type: 'GET',
url: self.target,
statusCode: {
403: function(data){
// close modal
// forbidden => close modal & push alert
setTimeout(function(){
self.$modal.modal('hide');
self.$alerts.html(data.responseText);
}, 500);
},
200: function(data){
// success => push form
// note that we will assign self.$form
var $response = $(data);
self.$form = $response.filter('form').first();
self.$modal_body.html($response);
self.$modal_confirm.click(function(e){
e.preventDefault();
self._submit(self.$form);
});
}
},
error: function(data){
console.log(data);
}
});
}
ModalForm.prototype._submit = function(){
/*
Post this.$form data and rerender form or push alert when complete.
*/
var self = this;
// change submit button to loading state
self.$modal_confirm.addClass('disabled').html('Loading...');
// issue pot request
// cleanup
// rebind if rerender or push alerts
$.ajax({
type: 'POST',
url: self.$form.attr('action'),
data: self.$form.serialize(),
statusCode: {
200: function(data){
// this is a form error
// so we must rerender and rebind form
// else we need to rerender and rebind
self.$form.remove();
var $response = $(data);
self.$form = $response.filter('form').first();
self.$modal_body.html($response);
self.$modal_confirm.on('click', function(e){
e.preventDefault();
self._submit(self.$form);
});
},
201: function(data){
// this means object was created
// so we must push an alert and clean up
self.$form.remove();
delete self.$form;
self.$modal.modal('hide');
self.$modal_body.html('');
// we will push alerts only if there is no 201 callback
var callback_201 = self.$button.attr('data-callback-201');
if (typeof(window[callback_201]) !== 'undefined') {
window[callback_201](data);
} else {
self.$alerts.prepend(data);
};
},
403: function(data){
// this means request was forbidden => clean up and push alert
self.$form.remove();
delete self.$form;
self.$modal.modal('hide');
self.$modal_body.html('');
self.$alerts.prepend(data.responseText);
}
},
complete: function(){
// reset button
self.$modal_confirm.removeClass('disabled').html(
self.modal_confirm_original_text);
}
});
}
window.ModalForm = ModalForm;
// define plugin
$.fn.modalForm = function(){
var self = this;
return self.each(function(){
var el = this;
var _ = new window.ModalForm($(el));
$.data(el, 'modalform', _);
});
}
// run plugin
$('[data-exsutils=modal-form]').modalForm();
})(jQuery);
</script>
Edit by #Daniel Arant:
A jsFiddle with a simplified, working version of the plugin code can be found here
Note by me: Please read the selected answer. This jsfiddle + adding var self = this will give you a complete picture of the problem and a good way around it.
The source of your problem is the line self = this in the ModalForm constructor. Since you did not use the keyword var before self, the JavaScripts interpreter thinks that self is a property of the global window object and declares it as such. Therefore, each time the ModalForm constructor is invoked, self takes on a new value, and all of the references to self in the event handlers created by the constructor for previous buttons then point to the new, most recent instance of ModalForm which has been assigned to the global self property.
In order to fix this particular problem, simply add the keyword var before self = this. This makes self a local variable rather than a global one, and the click event callback functions will point to their very own instance of ModalForm instead of the last instance that was assigned to self.
I created a working jsFiddle based on your code, which can be found here
I stripped down the plugin code to eliminate the ajax calls for the sake of simplicity. I also eliminated all of the uuid references. Once you get a reference to the modal as a jQuery object, you can use jQuery's .find() method to obtain references to the various components of the modal.
If you have any questions about my quick and dirty revision of your plugin
Related
I have a function that I want to reuse throughout my program. Basically it's a bootstrap dialog box that has a confirm and a cancel button. I setup the helper function to accept two anonymous functions, one for the cancel and one for the confirm. I have everything working except I am not sure how to properly assign it to the onclick when building the html. I want to avoid using a global variable but this is the only way I was able to get this to work.
Custom function:
function confirmMessageBox(msg, cancelFunc, confirmFunc) {
var html = ' <div class="container"><div class="modal fade" id="ConfirmMsgModal" role="dialog"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h4 class="modal-title">Confirmation Needed</h4></div><div class="locationTableCanvas"><div class="modal-body"><p>' + msg + '</p></div></div><div class="modal-footer"><table><tr><td><button type="button" class="btn btn-default" data-dismiss="modal" onclick = "(' + cancelFunc + ')()">Cancel</button></td><td><button type="button" class="btn btn-default" data-dismiss="modal" onclick = "(' + confirmFunc + ')()">Confirm</button></td></tr></table></div></div></div></div></div>';
$("#confirmMsgContainer").html(html);
$('#ConfirmMsgModal').modal('show');
}
I have to do, onclick = "(' + cancelFunc + ')()"> because if I do, onclick = "' + cancelFunc() + '"> it shows up as undefined. The current way will basically just print the anonymous function out and assign it to the onclick (almost as if I just typed out the anonymous function right at the onclick)
here is where I call the function:
var transTypeHolder;
$("input[name='transType']").click(function () {
var tabLength = $('#SNToAddList tbody tr').length;
if (tabLength == 0) {
var selection = $(this).attr("id");
serialAllowableCheck(selection);
resetSerialNumberCanvasAndHide();
$("#Location").val("");
$("#SerialNumber").val("");
}
else {
transTypeHolder = $(this).val();
var confirm = function () {
var $radios = $('input:radio[name=transType]');
$radios.filter('[value='+transTypeHolder+']').prop('checked', true);
resetSerialNumberCanvasAndHide();
$('#Location').val('');
$('#SerialNumber').val('');
};
var cancel = function () {};
confirmMessageBox("This is a test", cancel, confirm);
return false;
}
});
Is there a way to some how pass a variable to the anonymous function without using the global variable I have as, "transTypeHolder" ?
Before I get the, "Why are you doing it this way??" response; Javascript isn't a strong language of mine, as I am using ASP.NET MVC4. I haven't had a chance to sit down and learn Javascript in detail and I sort of picked it up and search what I need. So if there is a better way of tackling this, I am open for constructive criticism.
Don't make event handler assignments in HTML at all. If you want people to be able to supply their own functions for canceling and confirming use on:
function confirmMessageBox(msg, cancelFunc, confirmFunc) {
var html = ' <div class="container"><div class="modal fade" id="ConfirmMsgModal" role="dialog"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h4 class="modal-title">Confirmation Needed</h4></div><div class="locationTableCanvas"><div class="modal-body"><p>' + msg + '</p></div></div><div class="modal-footer"><table><tr><td><button type="button" class="btn btn-default cancel" data-dismiss="modal">Cancel</button></td><td><button type="button" class="btn btn-default confirm" data-dismiss="modal">Confirm</button></td></tr></table></div></div></div></div></div>';
$("#confirmMsgContainer").html(html);
$("#confirmMsgContainer").off('click', '.confirm').on('click', '.confirm', confirmFunc);
$("#confirmMsgContainer").off('click', '.cancel').on('click', '.cancel', cancelFunc);
$('#ConfirmMsgModal').modal('show');
}
Note that I've edited the HTML you're using to remove the onclicks and added a class to each button. I'm also using off to be sure any previously added event handlers are removed.
As far as passing the variable to the confirm function without using a global, use a closure:
var transTypeHolder = $(this).val();
var confirm = (function (typeHolder) {
return function () {
var $radios = $('input:radio[name=transType]');
$radios.filter('[value='+typeHolder+']').prop('checked', true);
resetSerialNumberCanvasAndHide();
$('#Location').val('');
$('#SerialNumber').val('');
};
})(transTypeHolder);
That tells JavaScript to create a function, which returns a function that does what you want it to do. That "function creator" takes in the variable you want to keep around, allowing it to be used elsewhere.
Now, I haven't tested this, so you may have some debugging in your future, but hopefully it gives you a jumping-off point.
You should be able to do it by having the function being acessible from global context under a generated name (which can be multiple if you have more than one instance of the box), like so:
function confirmMessageBox(msg, cancelFunc, confirmFunc) {
window['generatedCancelFunctionName1'] = cancelFunc;
window['generatedConfirmFunctionName1'] = confirmFunc;
var html = ' <div class="container"><div class="modal fade" id="ConfirmMsgModal" role="dialog"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h4 class="modal-title">Confirmation Needed</h4></div><div class="locationTableCanvas"><div class="modal-body"><p>' + msg + '</p></div></div><div class="modal-footer"><table><tr><td><button type="button" class="btn btn-default" data-dismiss="modal" onclick = "generatedCancelFunctionName1()">Cancel</button></td><td><button type="button" class="btn btn-default" data-dismiss="modal" onclick = "generatedConfirmFunctionName1()">Confirm</button></td></tr></table></div></div></div></div></div>';
$("#confirmMsgContainer").html(html);
$('#ConfirmMsgModal').modal('show');
}
This way you are not obliged to expose the function code. You can also set an id attribute to the element and set a jquery click() function like in the second part (but you would need the html to be created before you set the click)
I have a main View Model for my screen. It consists of 2 child view models.
One handles the registration section.
One handles the login section.
One handles the menu section (If authenticated and what menu items can appear, as well as the "Welcome "Username" type stuff).
$(document).ready(function () {
// Create the main View Model
var vm = {
loginVm: new LoginViewModel(),
registerVm: new RegisterViewModel(),
layoutVm: new LayoutViewModel()
}
// Get the Reference data
var uri = '/api/Reference/GetTimezones';
$.getJSON({ url: uri, contentType: "application/json" })
.done(function (data) {
vm.registerVm.Timezones(data);
});
// Bind.
ko.applyBindings(vm);
});
Once my Login model's "Login" method completes, I want to set the "IsAthenticated" value within the Menu model, as well as some other user info.
So in my login model, I have a SignIn method.
$.post({ url: uri, contentType: "application/json" }, logindata)
.done(function (data) {
toastr[data.StatusText](data.DisplayMessage, data.Heading);
if (data.StatusText == 'success') {
alert($parent.layoutVm.IsAuthenticated());
}
else {
}
})
.fail(function () {
toastr['error']("An unexpected error has occured and has been logged. Sorry about tbis! We'll resolve it as soon as possible.", "Error");
});
The alert code is my testing. I am hoping to access (and set) the IsAuthenticated property of the layoutVm model. That's one of the child models on my main View model.
However, "$parent" is not defined.
How can I update values in the layoutVm, from my loginVm?
$parent is part of the binding context, which is only available during the evaluation of the data-bind (i.e. to the binding handler).
In your viewmodel structure, you'll have to come up with a way to communicate between models yourself. For example, by passing parent view models, or by passing along shared observables. The problem you're describing can be solved by using data-bind="visible: $root.userVM.IsAuthenticated", like I answered in your previous question.
If you'd like to go with the other approach, here's an example on how to share an observable between viewmodels.
var ChildViewModel = function(sharedObs) {
this.myObs = sharedObs;
this.setObs = function() {
this.myObs(!this.myObs());
}.bind(this);
}
var RootViewModel = function() {
this.myObs = ko.observable(false);
this.vm1 = new ChildViewModel(this.myObs);
this.vm2 = new ChildViewModel(this.myObs);
this.vm3 = new ChildViewModel(this.myObs);
}
ko.applyBindings(new RootViewModel());
div { width: 25%; display: inline-block; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="with: vm1">
<h4>vm1</h4>
<p data-bind="text: myObs"></p>
<button data-bind="click: setObs">
flip
</button>
</div>
<div data-bind="with: vm2">
<h4>vm2</h4>
<p data-bind="text: myObs"></p>
<button data-bind="click: setObs">
flip
</button>
</div>
<div data-bind="with: vm3">
<h4>vm3</h4>
<p data-bind="text: myObs"></p>
<button data-bind="click: setObs">
flip
</button>
</div>
Note that each of the child view models also have write permission, so you'll have to be careful to not accidentally update the observable
ECMAScript 6
So far there are two buttons on the page: edit and delete. There will be more (add comment etc.). So, I would like to develop some general approach and not just operate on each button individually.
Each button is of class "custom-control" and should send an AJAX request.
Into the button tag I have included the information necessary for the request (url etc.):
<button id="main-object-delete" data-url="{{ object.get_delete_url }}" data-redirect="{{ object.get_delete_success_url }}" type="button" class="custom-main custom-control custom-delete btn btn-default " aria-label="Left Align">
The code:
$( document ).ready(function() {
class GeneralManager {
// Creates managers for each type of controls.
constructor() {
this.handle_buttons();
} // constructor
handle_buttons(){
let $button_list = $('.custom-control')
$button_list.each(function(index, button){
let button_manager = new ControlManager(button);
});
}
} // GeneralManager
function show_get_form(data, button, url, redirect){
let nest = button.closest(".custom-nest")
nest.innerHTML = data;
let act_cancel_manager = new SubmitCancelManager(url, redirect);
}
class ControlManager {
// Operates on main object only.
ajax_get(){
$.ajax({method: "GET",
url: self.url,
success: function(data){ show_get_form(data,
self.button,
self.url,
self.redirect); },
error: generalFail
});
} // ajax_get
constructor(button){
self = this; // Protection against binding "this" to something else.
this.button = button;
this.url = this.button.getAttribute("data-url")
this.redirect = this.button.getAttribute("data-redirect")
this.button.onclick = this.ajax_get;
} // constructor
}
let general_manager = new GeneralManager();
}); // $( document ).ready(function()
The idea was that for each button a new ControlManager object is created.
The problem is that both buttons trigger the request to the url for deletion. Delete button was the last of the two. If I change the order of the buttons, both buttons will send the request to the edit url.
Could you help me understand why my idea of assigning a separate instance of the ControlManager class to different buttons doesn't work. And how to cope with this problem most elegantly?
constructor(button){
self = this;
This creates a (=one) variable named self. So the second instance overwrites the value set by the first one.
ajax_get(){
$.ajax({method: "GET",
url: self.url,
Here you refer to the global variable self.
Forget self and use arrow functions where necessary:
ajax_get(){
$.ajax({method: "GET",
url: this.url,
success: data => show_get_form(data,
this.button,
this.url,
this.redirect),
I have the skeleton of a chat page but am having issues tying it all together. What I'm trying to do is have messages sent to the server whenever the user clicks send, and also, for the messages shown to update every 3 seconds. Any insights, tips, or general comments would be much appreciated.
Issues right now:
When I fetch, I append the <ul class="messages"></ul> but don't want to reappend messages I've already fetched.
Make sure my chatSend is working correctly but if I run chatSend, then chatFetch, I don't retrieve the message I sent.
var input1 = document.getElementById('input1'), sendbutton = document.getElementById('sendbutton');
function IsEmpty(){
if (input1.value){
sendbutton.removeAttribute('disabled');
} else {
sendbutton.setAttribute('disabled', '');
}
}
input1.onkeyup = IsEmpty;
function chatFetch(){
$.ajax({
url: "https://api.parse.com/1/classes/chats",
dataType: "json",
method: "GET",
success: function(data){
$(".messages").clear();
for(var key in data) {
for(var i in data[key]){
console.log(data[key][i])
$(".messages").append("<li>"+data[key][i].text+"</li>");
}
}
}
})
}
function chatSend(){
$.ajax({
type: "POST",
url: "https://api.parse.com/1/classes/chats",
data: JSON.stringify({text: $('input1.draft').val()}),
success:function(message){
}
})
}
chatFetch();
$("#sendbutton").on('click',chatSend());
This seems like a pretty good project for Knockout.js, especially if you want to make sure you're not re-appending messages you've already sent. Since the library was meant in no small part for that sort of thing, I think it would make sense to leverage it to its full potential. So let's say that your API already takes care of limiting how many messages have come back, searching for the right messages, etc., and focus strictly on the UI. We can start with our Javascript view model of a chat message...
function IM(msg) {
var self = this;
self.username = ko.observable();
self.message = ko.observable();
self.timestamp = ko.observable();
}
This is taking a few liberties and assuming that you get back an IM object which has the name of the user sending the message, and the content, as well as a timestamp for the message. Probably not too far fetched to hope you have access to these data elements, right? Moving on to the large view model encapsulating your IMs...
function vm() {
var self = this;
self.messages = ko.observableArray([]);
self.message = ko.observable(new IM());
self.setup = function () {
self.chatFetch();
self.message().username([user current username] || '');
};
self.chatFetch = function () {
$.getJSON("https://api.parse.com/1/classes/chats", function(results){
for(var key in data) {
// parse your incoming data to get whatever elements you
// can matching the IM view model here then assign it as
// per these examples as closely as possible
var im = new IM();
im.username(data[key][i].username || '');
im.message(data[key][i].message || '');
im.timestamp(data[key][i].message || '');
// the ([JSON data] || '') defaults the property to an
// empty strings so it fails gracefully when no data is
// available to assign to it
self.messages.push(im);
}
});
};
}
All right, so we have out Javascript models which will update the screen via bindings (more on that in a bit) and we're getting and populating data. But how do we update and send IMs? Well, remember that self.message object? We get to use it now.
function vm() {
// ... our setup and initial get code
self.chatSend = function () {
var data = {
'user': self.message().username(),
'text': self.message().message(),
'time': new Date()
};
$.post("https://api.parse.com/1/classes/chats", data, function(result) {
// do whatever you want with the results, if anything
});
// now we update our current messages and load new ones
self.chatFetch();
};
}
All right, so how do we keep track of all of this? Through the magic of bindings. Well, it's not magic, it's pretty intense Javascript inside Knockout.js that listens for changes and the updates the elements accordingly, but you don't have to worry about that. You can just worry about your HTML which should look like this...
<div id="chat">
<ul data-bind="foreach: messages">
<li>
<span data-bind="text: username"></span> :
<span data-bind="text: message"></span> [
<span data-bind="text: timestamp"></span> ]
</li>
</ul>
</div>
<div id="chatInput">
<input data-bind="value: message" type="text" placeholder="message..." />
<button data-bind="click: $root.chatSend()">Send</button>
<div>
Now for the final step to populate your bindings and keep them updated, is to call your view model and its methods...
$(document).ready(function () {
var imVM = new vm();
// perform your initial search and setup
imVM.setup();
// apply the bindings and hook it all together
ko.applyBindings(imVM.messages, $('#chat')[0]);
ko.applyBindings(imVM.message, $('#chatInput')[0]);
// and now update the form every three seconds
setInterval(function() { imVM.chatFetch(); }, 3000);
});
So this should give you a pretty decent start on a chat system in an HTML page. I'll leave the validation, styling, and prettifying as an exercise to the programmer...
Hi All on click button I need to add object to array and then write array to cookies.
From the start this array can be not empty so I parse cookie first.
function addToBasket(){
var basket = $.parseJSON($.cookie("basket"))
if (basket.length==0||!basket){
var basket=[];
basket.push(
{ 'number' : this.getAttribute('number'),
'type' : this.getAttribute('product') }
);
}
else{
basket.push(
{ 'number' : this.getAttribute('number'),
'type' : this.getAttribute('product') }
);
}
$.cookie("basket", JSON.stringify(basket));
}
And HTML
<button type="button" class="btn btn-success btn-lg" number="12" product="accs" onclick="addToBasket()">Add</button>
Unfortunately I'm getting Uncaught ReferenceError: addToBasket is not defined onclick.
Can't understand what am I doing wrong?
Thanks!
I simplified your code a good deal, heres a fiddle: http://jsfiddle.net/yJ6gp/
I wired the click event using jQuery and simplified some of your code (see comments). Note I changed your html a little so I could select the add basket button by class - change as desired.
$(function () {//doc ready
$.cookie.json = true; //Turn on automatic storage of JSON objects passed as the cookie value. Assumes JSON.stringify and JSON.parse:
$('.add-basket').click(function() {
var basket = $.cookie("basket") || []; //if not defined use an empty array
var $this = $(this);
basket.push({
'number': $this.attr('number'),
'type': $this.attr('product')
});
console.log(basket);
$.cookie("basket", basket);
});
});