I made a reusable table view which takes a table definition (JSON array) and data (JSON) and renders the table using javascript. I made the table using an mvc pattern like here updated for es6.
I have hard coded events in the view which work great within instances of my model, view and controller. These events are things like "addRowClicked" and "modelChanged" which then calls a callback function to operate.
For example On the view I created the event:
this.addRowClicked = new CIEvent(this);
On the Controller I attach the callback:
this.view.addRowClicked.attach(
function() {
self.addRow()
}
);
Then when creating the view I can pass a button array with an event name which creates the button and attaches the notify method of the event when clicked
element.onclick = function () {
self[button.event_name].notify();
}
So that all works perfect and is pretty awesome!!
Now I am trying to add custom events without luck. A Custom event would be something like "updateQuantity" which is unique to an instance of the table. updateQuantity would get fired from an input element when changing and would sum up specified cells and put that sum in a different cell.
I thought I would be able to simply create the event on the view, attach the callback on the controller, then attach the event notification to onchange or onkeyup... etc
But that is not working.
Here is some extracted code of the entire process....
To build the table I do this:
let model = new TableModel(table_definition, data);
let view = new DataTableView(model, div, buttons);
let controller = new DataTableController(model, view);
Now I add the event 'updateQuantity' on the view and the callback on the controller:
view.updateQuantity = new CIEvent(view);
controller.view.updateQuantity.attach(
function() {
console.log('update the quantity row')
}
);
If I then call the event from the within same file it works fine:
view.updateQuantity.notify(); // works fine
However, if I try to attach this to an an element onclick, onkeyup, etc, the event will not fire... the commented code attaches dynamically however I just hardwired it to try to get it working. I tried using events like onkeyup directly and using addEventListener
let self = this;
// element[index] = this[event[index]].notify();
// element.onkeyup = this.updateQuantity.notify();
element.addEventListener("onkeyup", function(){
// self[event[index]].notify();
self.updateQuantity.notify();
});
When modifying the element I get no response.
I hope I am missing something elementary. thanks all!
element.addEventListener("onkeyup", function(){
// self[event[index]].notify();
self.updateQuantity.notify();
});
Should be
element.addEventListener("keyup", function(){
// self[event[index]].notify();
self.updateQuantity.notify();
});
onkeyup should have been keyup
Related
I'm attempting to write some Javascript objects to manage dynamic forms on a page.
The forms object stores an array for forms and renders them into a container.
I'd like to have click events for certain fields on each form so decided to make a seperate object and tried to bind an event inside the objects init method.
The init method is clearly fired for every new form that I add. However on change event only ever fires for the last form object in my array.
JS Fiddle Demonstrating Issue
can be found: here
function Form(node) {
this.node = node;
this.init = function() {
$(this.node).find("input:checkbox").change(event => {
console.log('Event fired');
});
};
this.init();
}
// Object to manage addition / removal
var forms = {
init: function() {
this.formsArray = [];
this.cacheDom();
this.bindEvents();
this.render();
}
// Only selector elems from the DOM once on init
cacheDom: function() { ... },
// Set up add / remove buttons to fire events
bindEvents: function() { ... },
render: function() {
for (let form of forms)
this.$formSetContainer.append(form.node)
}
addForm: function() {
// Logic to create newRow var
this.formsArray.push(new Form(newRow));
},
removeForm: function() {
// Logic to check if a form can be removed
this.formsArray.pop();
}
},
What I've Tried Already
I'm actually able to bind events inside render by removing this.init() inside the Form constructor and altering render like so:
for (let form of this.formsArray) {
this.$formSetContainer.append(form.node)
form.init();
}
Then the events will successfully fire for every form
But I'd rather not have this code run every time I call render() which is called every time I add / remove forms.
I have a feeling that this is a scoping issue or that the event is somehow being clobbered. Either that or I'm misunderstanding how events are bound. Any pointers would be appreciated
Looking at the code in the JSFiddle, the problem comes from using this.$formSetContainer.empty() in the render function. .empty() removes all the event handlers from your DOM nodes.
To avoid memory leaks, jQuery removes other constructs such as data and event handlers from the child elements before removing the elements themselves.
If you want to remove elements without destroying their data or event handlers (so they can be re-added later), use .detach() instead.
https://api.jquery.com/empty/
You can replace this with this.$formsetContainer.children().detach() and it will do what you want.
I have a html richText editor. My code structure is like this:
function richTextEditor(div)
{
var self=this;
self.instanceIdentifier=Math.floor(Date.now());
//Richtext editor creation logic
$(document).on('click.'+self.instanceIdentifier,function()
{
//some logic
})
self.destroy=function()
{
//delete all properties of self
// detach all listeners
$(document).off('click.'+self.instanceIdentifier) ;
}
}
Our app is single page application, and there are multiple richtexteditor instances opened in different panes. I need to destroy the instance when the node corresponding to this has been removed. Destroy should remove all the event handlers attached by that instance.
So far Date.now() for uniquely identifying the handler is working but I think there must be some elegant way to do that.
var div1=$('#notes')[0];
var editorInstance1=new richTextEditor(div1);
//remove is not a valid jquery event, its just for illustration
// I am getting remove event from another library
$(div1).on('remove',function(){
editorInstance1.destroy();
})
Please suggest if this is the correct way to go.
What you want is a GUID or UUID. There is a great answer to this question here.
I am a bit confused on how to correctly remove an event listener in Hammer.js 2.0
Following the advice in this question it seems I just need to use
mc.off(eventString, functionEvent);
However I can't seem to get this to work, when I have 2 functions which I need to call with the same recognizer. My example is I need someone to panleft, then I need to remove this listener and listen for another panleft.
As you can see in my simplified codepen example I try to call mc.off in the first function, then set up the next event, but it runs both simultaneously.
So what I want in my example is for the first panleft to trigger the first function, then the second panleft to trigger a new function
var myElement = document.getElementById('myElement');
var mc = new Hammer(myElement);
mc.on("panleft", function(ev) {
selectFirst('first')
});
function selectFirst (text) {
myElement.textContent = text;
mc.off('panleft', selectFirst);
mc.on("panleft", function(ev) {
selectSecond('second')
});
}
function selectSecond (text) {
myElement.textContent = text;
}
As indicated in the question I linked to above and its jsfiddle
As was mentioned in that answer, you can fix the issue by not using an anonymous function.
hammertime.on("touch", callback);
However it didn't seem to fix my issue of trying to remove the initial function and bind to a new one with the same recognizer.
I ended up just adding a new element and binding this to the second function. I suggest the same as you probably should only have 1 panleft event for the element and if it needs to do something different just add another hammer element with the panleft on it.
I am using the following code to load two underscore.js templates. Once the first link is clicked, the skeleton template is loaded. The first trigger executes the find bind, which executes the loadBookmarks function correctly, but the 'loaded' trigger never fires and the loadFriendBookmarks never executes. Why is this? Is there another way to make this happen?
$('#bookmarks-link').click(function() {
$('#bookmarks-count').text("0");
var skeleton = modalTemplate();
$('#bookmarks').append(skeleton);
$('#bookmarks').trigger('skeleton');
});
$('#bookmarks').bind('skeleton', function() {
$('#bookmarks .thumbnails').loadBookmarks( getBookmarksUrl(1) );
// If I add an alert('hi') here, it works perfectly.
$('#bookmarks').trigger('loaded');
});
$('#bookmarks').bind('loaded', function() {
$('#bookmarks .thumbnails a').each(function() {
$(this).bind('click', function() {
$('#bookmarks .bookmarks-table tbody').empty();
$('#bookmarks .bookmarks-table tbody').loadFriendBookmarks(
getFriendBookmarksUrl($(this).attr('data-item'))
);
});
});
});
So interesting enough, the triggers do work correctly: If I stick an alert in between loadBookmarks and trigger, everything works fine. If I take it out, then it doesn't. Any idea why?
Based on your description and common sense, it sounds like loadBookmarks() loads data from a remote source, such as an ajax call. This means that trigger('loaded') can fire before loadBookmarks() has received the data. You can add a callback argument to loadBookmarks() and trigger the event there:
$('#bookmarks .thumbnails').loadBookmarks( getBookmarksUrl(1) , function() {
$('#bookmarks').trigger('loaded');
});
But this requires your loadBookmarks to know to call this function after it receives the data and creates the needed HTML - I can't demonstrate this without seeing the actual code you have in loadBookmarks.
Additional suggestion: don't bind handlers this way, use event delegation instead:
$('#bookmarks').on('click', '.thumbnails a', function(e) {
e.preventDefault(); // don't want the link to actually be followed, do we
var url = getFriendBookmarksUrl($(this).attr('data-item'));
if(url) { // in case it's clicked before the data attribute is set
var $tbody = $('#bookmarks .bookmarks-table tbody');
$tbody.empty();
$tbody.loadFriendBookmarks(url);
}
});
This means that all elements matching the selector '#bookmarks .thumbnails a' will call this click handler, even if they were added to the document after you called on. Meaning you can delegate these events even before calling loadBookmarks, removing the need for the loaded event at all. Plus, this way you only have one copy of the handler function in memory, as opposed to your bind which created a separate copy of the function for each a node.
the problem is else where in your code. probably some js error in loadBookmarks* functions.
see:
http://jsfiddle.net/BBESV/
triggers work perfectly
If I'm following a rough MVC pattern in JavaScript, what is the best way for the view (such as a button element) to notify the controller?
Should the button fire an event that the controller has to listen to? Or, should the button call a controller function directly? Or maybe the controller should assign the event to the view?
Thanks for any input!
I would say that the View should catch the event fired by the button and fire its own event that will be handled by the controller.
Let me explain:
#raynos wrote:
Controllers listen on input. This means controllers listen on events
from DOM nodes
personally even though I agree with the first statement I don't like the interpretation.
To follow this statement means that the controller has to know of every button/text field/element in the UI and its ID/Selector.
I prefer to have the View fire semantic events such as "languageSelected" or "searchRequested" and add the relevant data to the event.
so a typical flow would be that the View renders some UI (lets say it has a search box and a button), when the user clicks the button - the View handles the event and fires its own "searchRequested" event. This event is handled by the Controller that would call the Model asking it to perform the search. when done, the Model will fire a "searchResultsUpdated" evnet which will be handled by the View causing it to show the results.
if you now choose to change the design of your app to show search term links instead of a search box and a button (or even if you have then side by side - or on different screens) the only thing you need to change is the View.
A technical side-note:
If using JQuery and assuming your view is a javascript object you can use
$(view).triggerHandler(
$.Event('eventName',{'object:'with','more':'event','related':'data'})
);
to fire the event
And
$(view).on('eventName',handler);
to listen for and handle the event.
Interesting question. I think it would depend quite a lot on your situation, the complexity of your example, and the particular JavaScript patterns that you're using.
If the button you're talking about is simply an HTML element, this might be a simple way:
var MyController = function() {
this.particularMethod = function() {
// update model
}
// Using jquery
var button = $("#myButton");
button.click( function() { myController.particularMethod() } )
}
Or, if your button is an object or module that you've created, you could set a callback:
var Button = function(selector, clickFunction) {
// Using jquery
$(selector).click(clickFunction)
...
}
var MyController = function() {
this.particularMethod = function() {
// update model
}
var button = new Button("#myButton", this.particularMethod);
...
}
Unfortunately, trivial examples don't really illustrate the benefits of different approaches!
There are many ways to do it. Here is one way.
var app = new App();
function App() {
var model = new Model();
var view = new View();
var controller = new Controller(view, model);
}
function Controller(view, model) {
view.addActionListener(function() {
alert("on activate");
});
}
function View() {
var self = this;
$("button").onclick = function() {
self.listener();
}
}
View.prototype.addActionListener = function(listener) {
this.listener = listener;
}