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;
}
Related
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
I have isolated the issue, see and try the full source here.
Steps to reproduce:
Press Ctrl+Enter to run the snippet
Click on 'Say Hello' custom command button, and check if the event
handler runs
Click on top left 'Save State' button
Click on 'Load State' button, and restore the previous state.
Now click again on 'Say Hello' button and demonstrate the event handle will not run, instead something weird is happening.
Notes: Please do not search for the solution around the localStorage. The issue can be reproduced by using different server side state persisting solution. (as my original app does)
Any idea where to patch? ... or workaround?
Hopefully this will help you out.
http://dojo.telerik.com/EDUCO/4
I have added the following piece of code for you:
dataBound: function (e) {
$(".k-grid-SayHello").on('click', function (a) {
console.log(e);
a.preventDefault();
alert('Hello');
});
},
When the rebind occurs I suspect that it is losing the connection to the event handler so all I have done if looked for the button based on it's class name and reattached it.
Obviously you can adapt the solution to meet your needs but this is something I do for my projects when I need to "invoke" custom actions on buttons/ dynamically create things on the fly.
Any issues let me know.
To keep function references after calling grid.setOptions()
I added the function references back to the deserialized configuration object before passing it to the setOptions method.
( http://docs.telerik.com/kendo-ui/api/javascript/ui/grid#methods-setOptions )
$(document).ready(function () {
var grid = $("#myGrid").data("kendoGrid");
var originalOptions = grid.getOptions();
var savedOptions = JSON.parse(localStorage["myGrid-options"]);
if (savedOptions) {
var detaylarFunc = originalOptions.columns[3].command[0].click;
savedOptions.columns[3].command[0].click = detaylarFunc;
grid.setOptions(savedOptions);
} else {
grid.dataSource.read();
}
});
//Custom command
function Detaylar(e) {
e.preventDefault();
var grid = $("#myGrid").data("kendoGrid");
options = grid.getOptions();
localStorage["myGrid-options"] = kendo.stringify(grid.getOptions());
}
I'm looking into deferred and custom events. I'm not sure what method would suit my application.
I have a class that calls another class. Inside this class a user can drag files on to the window.
Once the file has been dragged on, I wish to send details about the files to my main class.
I've thought about using deferred, but the user needs to drag files over and over again, and as of my understanding this can only be used once.
So in my main class I:
this.dropBox = new DropBox();
Then in the DropBox class I have:
$(window).on('drop', this.drop);
But what should I put in my 'drop' method. Every time something is dropped I wish to 'alert' my main class about it and act upon it. How can I 'listen' for the event. Should I use deferred, custom event or something else?
There are typically two options for this:
Delegate
A delegate should implement a certain "interface", a set of functions that handle certain events.
function DropBox(delegate)
{
this.delegate = delegate;
$(window).on('drop', $.proxy(this, 'drop'));
}
DropBox.prototype.drop = function(e) {
// do stuff with event
this.delegate.drop(e);
}
// inside main instance
this.dropBox = new DropBox(this);
// delegate interface
this.drop = function(e) {
// handle file drop
};
Callback
If the delegate only needs one function, you can use a callback as well:
function DropBox(dropEventHandler)
{
this.dropEventHandler = dropEventHandler;
$(window).on('drop', this.drop);
}
DropBox.prototype.drop = function(e) {
this.dropEventHandler(e);
};
// inside main instance
var self = this;
this.dropBox = new DropBox(function(e) {
// handle file drop
// use 'self' to reference this instance
});
Why not just give a callback over to the DropBox?
Well, like this code in the main class:
this.dropBox = new DropBox(function(fileInfo) {
// this code can be executed by the DropBox Object multiple times!
});
And the DropBox:
window.DropBox = function(callback) {
this.userHasDroppedFiles = function(fileinfo) {
// do stuff
callback(fileinfo); // give the fileinfo back with the callback!
}
}
Also, there are no classes in JavaScript! You have only Objects, and you can use constructor-functions combined with prototypes to generate a class like behaviour, but you will never actually have classes like in Java, C# or similar languages. Keep this always in mind.
Some JS frameworks build their own class layer on top of the main JS possibilities, then you may have Framework classes, but also never native JavaScript classes, because native JavaScript classes dont exist!
I am creating an Ajax Client Control in ASP.Net. By inheriting from IScriptControl and then adding the relavant javascript class (which would inherit from a javascript control). I have found a memory leak in the following code:
Type.registerNamespace("mynamespace");
myClass = function (element) {
myClass.initializeBase(this, [element]);
}
myClass.prototype = {
initialize: function () {
myClass.callBaseMethod(this, 'initialize');
var me = this;
$(document).ready(function () {
me._initializeControl();
me._hookupEvents();
});
},
dispose: function () {
//Add custom dispose actions here
myClass.callBaseMethod(this, 'dispose');
},
//...other code ...
_hookupEvents: function () {
var me = this;
var e = this.get_element();
$("#viewRates", e).click(function () {
me.openDialog();
});
},
//...other code...
myClass.registerClass('myClass', Sys.UI.Control);
if (typeof (Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();
_hoookupEvents is a function in my javascript file. The leak is related ot the line me.openDialog. If I remove this line, there is no leak. However, I need this line to be able to call a function from the class (I cannot just use 'this' in the function because it would refer to the button). Is there a better way to do this? Or maybe I just need to call some methods in the dispose function to clean such things?
The memory leak at this code can happen on this line, as you also note
$("#viewRates", e).click(function () {
me.openDialog();
});
when you call it with UpdatePanel, or in general call it for the same component and with out first clear the previous events for the click, the previous handler stay on, and here we have two cases.
To register the same click event more than ones.
To update the dom with ajax, and not previous clear that handlers, as results the previous code stay for ever (for ever == until you leave the page).
In general the solution is to clear any previous handler for the click,
before add a new one.
when initialize a new ajax call with UpdatePanel and before get the new response.
Use a function like that to remove the click and clear the resource for the handler.
this.get_events().removeHandler('click');
I'm extremely hesitant to call it a memory leak if there are only 2 instance of myclass. If there are 2,000 instances of myclass there's DEFINITELY a leak.
I'd search real hard for any dynamic instantiation statements that you have, that create myClass on certain conditions. That is what i see a lot (creating classes in loops at application init, perhaps a form submit can trigger instantiation and it wasn't fully QA'd to see if you can get a submission to create multiple objects, etc).
I'm building a Backbone app and I came across this weird issue. In the state A (route: ""), I've got a view like that:
var view = Backbone.View.extend({
events : {
"click a.continue" : "next"
},
next : function(e) {
//Some stuff
Backbone.history.navigate("/page2");
}
});
and once I click on the anchor with "continue" class, I am redirected to a state B (route: "/page2"). If I click on the back button of my browser, and then I click on the anchor, debugging I've noticed that the next function is triggered twice. Actually if I keep going back and forth the number of times the event is triggered keeps increasing.
Any clue?
You've got a zombie view hanging around.
The gist of it is that when you are instantiating and displaying the second view ("state B"), you are not disposing of the first view. If you have any events bound to the view's HTML or the view's model, you need to clean those up when you close the form.
I wrote a detailed blog post about this, here: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
Be sure to read the comments as "Johnny O" provides an alternative implementation which I think is quite brilliant.
I Have the same problem, the solution is...
App.Router = Backbone.Router.extend({
routes: {
"fn1": "fn1",
"fn2": "fn2"
},
stopZombies: function(objView){
if(typeof objView === "object"){
objView.undelegateEvents();
$(objView.el).empty();
}
},
fn1: function(){
this.stopZombies(this.lastView);
var view1 = new App.v1();
this.lastView = view1;
},
fn2: function(){
this.stopZombies(this.lastView);
var view2 = new App.v2();
this.lastView = view2;
}
});
Store the last execute view in this.lastView, then stopZoombies() remove the events from this view.