Form ready / init event on Ninja Forms - Backbone / Marionette - javascript

I am currently working with the "NinjaForms" plugin for WordPress, which provides custom form management for the backend. It's based on Marionette / Backbone JS. After some research, I had no problem triggering JavaScript functions on input change and on form submit. However, I have not found a way to fish for an event once the form is initialised / rendered / shown (any of these, really).
What I am doing right now is initialising a Marionette object, adding listeners to a radio and then adding functions to execute it on event:
if(typeof Marionette !== 'undefined') {
var mySubmitController = Marionette.Object.extend( {
initialize: function() {
// init listener
this.listenTo( Backbone.Radio.channel( 'forms' ), 'view:show', this.initAction);
// field change listener
this.listenTo( Backbone.Radio.channel( 'fields' ), 'change:modelValue', this.valueChanged);
// submit listener
this.listenTo( Backbone.Radio.channel( 'forms' ), 'submit:response', this.actionSubmit );
},
// init action
initAction: function() {
console.log("init");
},
// input update action
valueChanged: function(model) {
console.log("update");
},
// submit action
actionSubmit: function( response ) {
// handled via php
console.log("submit");
},
});
// initialise listening controller for ninja form
new mySubmitController();
}
However, the line this.listenTo( Backbone.Radio.channel( 'forms' ), 'view:show', this.initAction); is not working. I have tried the events view:render, view:show, show:view, render:view without success.
I searched in the Backbone / Marionette documentations, but could not find a fitting event. This question might be a duplicate, but I could not really find any topic regarding form initialisation events with Backbone.

after some long search, googling and frustration I've found a solution. Using the following JavaScript-Code, you can execute code on form render:
// if there is a ninja form on this page
if(typeof Marionette !== 'undefined') {
var mySubmitController = Marionette.Object.extend( {
initialize: function() {
// init listener
this.listenTo( nfRadio.channel( 'form' ), 'render:view', this.initAction );
},
// init action
initAction: function() {
// code to execute on form render
},
});
// initialise listening controller for ninja form
new mySubmitController();
}

Related

backbone click event not firing on first click

I'm having an issue when I am trying to switch the view after a model save on a click event.
The flow I am trying to create is a reorder process, the user will have a confirmation page to reorder. On clicking submit an api call will execute and the invoice page will load on success.
Currently when I click the submit button the first time nothing happens and when i click again I can get an invoice page. no such issue for the cancel button.
var confirmView = Backbone.View.extend({
initialize: function(){
this.render();
},
render: function(){
var template = _.template( $("#confirmReorder_template").html());
this.$el.html(template);
},
events: {
"click #submitButton": "submitReorder",
"click #cancelButton": "cancelReorder"
},
submitReorder: function(event){
var URI='<config property="api.url.itemReorder"/>';
var ItemReorderModel = new itemReorderModel({url:URI});
$("#submitButton").click(function(event) {
event.preventDefault();
ItemReorderModel.set('id','1');
ItemReorderModel.save( {}, {
success : function() {
var response = ItemReorderModel.toJSON();
var InvoiceView = new invoiceView({el: $("#itemData")});
},
error : function(model, xhr, options) {
}
});
});
},
cancelReorder: function(event){
document.location.href = "items_list.ctl";
}
});
second view
var invoiceView = Backbone.View.extend({
initialize: function(){
this.render();
},
render: function(){
var template = _.template( $("#reorderInvoice_template").html());
this.$el.html(template);
},
events: {
"click #returnButton": "itemlist",
"click #printButton": "print"
},
itemlist: function(event){
document.location.href = "items_list.ctl";
},
print: function(event){
}
});
loading of first view
$(document).ready(function() {
var ConfirmView = new confirmView({el:$('#itemData')});
});
I'm new to backbone so not sure if I should be using a route, I also have read something about binding, but still trying to get my head around how it all works.
any advice is much appreciated.
You are binding a new event handler in submitReorder method, and your actual functionality is inside that event handler.
So the fist time you click the button, the event handler delegated toview via backbone event hash will trigger submitReorder, which binds a new event handler with actual functionality directly to the button element.
Next time when you click it, this new direct handler will also trigger and fire the functionality you expect.
Each time you click the button you're adding a new event handler.
Your code should be simply:
submitReorder: function(event){
event.preventDefault();
var URI='<config property="api.url.itemReorder"/>';
//-----^------ if this is hardcoded, why not specify this in the model itself..?
var ItemReorderModel = new itemReorderModel({url:URI});
//-------------^----------- why not do this just once while initializing view..?
ItemReorderModel.set('id','1');
//-------------^----------- if this is hardcoded, why not set specify it in model..?
ItemReorderModel.save( {}, {
success : function() {
var response = ItemReorderModel.toJSON();
var InvoiceView = new invoiceView({el: $("#itemData")});
},
error : function(model, xhr, options) {
}
});
},
I also suggest initializing the model in the view's initialize method and caching it as it's property rather than initializing a new model on every click.

eventlistener and dispatchevent in the same widget

I have a widget which includes an event listener:
var MyWidget = function(parseTreeNode,options) {
this.initialise(parseTreeNode,options);
this.addEventListeners([
{type: "tw-my-message", handler: "handleMyMessage"}
]);
};
and, in the handler of the same widget a dispatchevent:
MyWidget.prototype.handleMyMessage = function(event) {
...
this.dispatchEvent({type: "tw-my-message",param: "myparam"});
...
The widget listens for the message, then passes on the same message (if required) to another widget.
Currently I have to use two similar widgets (with the same function) which listen for different messages to avoid the widget from catching its own message.
How do I prevent the widget from catching its own message?
You will need to pass along the publisher of the event, and then test to make sure that the event publisher is not this. Pseudo code to follow:
MyWidget.prototype.handleMyMessage = function(event) {
if (event.publisher === this)
return; // bail out, I published this event
// ...
};
Pseudo code for dispatchEvent:
MyWidget.prototype.dispatchEvent = function(event) {
event.publisher = this;
// ... loop through subscribers and notify them
};

Backbone and jQuery trigger / bind custom event not heard

I'm writing a backbone app within an ASP.NET MVC page, and I'm having a truly baffling (to me) problem with using two custom events.
So, the MVC View (Planning.cshtml) has a select control which triggers the Backbone router to fetch a model on change. This model takes a good 10 seconds or so to load, so I want to disable the select while it loads to prevent multiple requests. To do this, I'm having my router trigger two custom events: 'subview-loading' and 'subview-loading-complete', and having my MVC view listen for these events.
The problem I'm having is that the router can hear both events, but Planning.cshtml will only hear the 'subview-loading-complete' event, not the first event.
Planning.cshtml code:
$(document).ready(function () {
var router = new Router({
el: '#TermPlanningReport'
});
var $routerEl = $(router.el);
$routerEl.bind('subview-loading', function() {
console.log('Planning.cshtml heard subview-loading');
//disable control
});
$routerEl.bind('subview-loading-complete', function() {
console.log('Planning.cshtml heard subview-loading-complete');
//enable control
});
});
Router.viewTerm() - the route method loading the model:
viewTerm: function (term) {
var that = this,
termModel = new PlanningTerm({ term: term }),
$thisEl = $(that.el);
//for testing purposes, see if router can hear itself...
$thisEl.on('subview-loading', function() {
console.log('router heard subview-loading');
});
$thisEl.on('subview-loading-complete', function () {
console.log('router heard subview-loading-complete');
});
//trigger loading
$thisEl.trigger('subview-loading');
// fetch data for model
termModel.fetch({
success: function (model, response, options) {
$thisEl.trigger('subview-loading-complete');
//render a vew...
},
error: function (model, response, options) {
$thisEl.trigger('subview-loading-complete');
//render a view...
}
});
},
Finally, my console output:
router heard subview-loading
router heard subview-loading-complete
Planning.cshtml heard subview-loading-complete
The variables $routerEl and $thisEl are both returning the same element properly.

$(this.el).find() works in event handler, not in initialize function (backbone.js)

I am using backbone.js to create a View which contains a Like button. The model of this View contains the attribute is_liked, and if its value is 1, then the function setStateLike called will change the style of the Like button.
Problem: I am not able to select the button using this.setStateLike() in the initialize function. Doing so just returns a []. However, when I define this.setStateLike as a click event handler, selecting the button works! The stranger thing is that this.setStateLike() called within initialize is able to select $(this.el), but not $(this.el).find()!
Any idea what has happened here and how can it be fixed? Thanks!
PhotoListItemView = Backbone.View.extend({
tagName: 'div',
className: 'photo_box',
events: {
'click': 'setStateLike'
},
initialize: function() {
this.setStateLike();
},
render: function() {
$(this.el).html( this.template( this.model.toJSON() ) );
return this;
},
setStateLike: function() {
console.log( $(this.el).find('#like') ); // returns []
if(this.model.get('is_liked')) {
console.log( $(this.el) ); // returns correctly
console.log( $(this.el).find('#like') ); // returns []
// Change icon to Active state
$(this.el).find('#like.photo_btn').addClass('photo_btn_active').attr('id', 'unlike');
}
}
});
If your script comes before the bulk of the body HTML and if the initialize function is being called immediately, that's your issue: the DOM isn't actually built yet, so no elements can be selected. Either run the script at the end of the </body>, or use jQuery's DOM-ready handler.

Backbone.js: How to handle selection of a single view only?

I am stuck on the following issue:
I have a model with a property that defines if it is visibly selected or not, which I will call SelectModel for the purpose of this question.
SelectModel = Backbone.Model.extend({
defaults:{
isSelected: false
}
})
Now the first part that I do not really get is how I should handle the selection in general.
If I want to use the observer pattern, my View should listen to the change of the isSelected property. But my view also triggers this in the first place, so I would have.
SelectView = Backbone.View.extend({
initialize: function(){
this.model.bind("change:isSelected", this.toggleSelectionVisually)
},
events: {
"click" : toggleSelection
},
toggleSelection: function(){
this.model.set({"isSelected": !this.model.get("isSelected");
},
toggleSelectionVisually:(){
//some code that shows that the view is now selected
},
})
So this in itself already feels a bit absurd but I guess I just understand something wrong.
But the part which I really fail to implement without making my code horrible is handling the selection for multiple models that only one model is selected at a time.
SelectListView = Backbone.View.extend({
initialize: function(){
this.collection = new SelectList();
},
toggleSelection: function(){
????
}
})
So who should notify whom of the selection change? Which part should trigger it and which part should listen? I am really stuck on this one. For a single View it is doable, for a collection I am sadly lost.
I would have suggested the following simplification for your SelectView until I saw the second part of your question:
SelectView = Backbone.View.extend({
events: {
"click" : toggleSelection
},
toggleSelection: function(){
this.model.set({"isSelected": !this.model.get("isSelected");
//some code that shows whether the view is selected or not
}
});
However, since the isSelected attribute is apparently mutually exclusive, can be toggled off implicitly when another one is toggled on, I think the way you have it is best for your case.
So, using your existing SelectView and, you could have a SelectListView as follows. WARNING: it iterates over your entire collection of models each time one is selected. If you will have a large number of models this will not scale well, and you'll want to cache the previously-selected model rather than iterating over the entire collection.
SelectListView = Backbone.View.extend({
initialize: function(){
this.collection = new SelectList();
this.collection.bind('change:isSelected', this.toggleSelection, this);
},
toggleSelection: function(toggledModel){
//A model was toggled (on or off)
if(toggledModel.get('isSelected') {
//A model was toggled ON, so check if a different model is already selected
var otherSelectedModel = this.collection.find(function(model) {
return toggledModel !== model && model.get('isSelected');
});
if(otherSelectedModel != null) {
//Another model was selected, so toggle it to off
otherSelectedModel.set({'isSelected': false});
}
}
}
});
I would recommend that your model not keep track of this, but rather the view.
In my mind the model has nothing to do with its display, but rather the data that you're tracking. The view should encapsulate all the info about where and how the data is displayed to the user
So I would put isSelected as an attribute on the view. Then it's trivial to write a method to toggle visibility. If you then need to explain the other views that a specific view is selected you can attach a listener $(this.el).on('other_visible', toggle_show) which you can trigger on your toggle_visibility method with $(this.el).trigger('other_visible')
Very close to the solution suggested by #rrr but moving the logic from the View to the Collection where I think it bellows to:
SelectsCollection = Backbone.Collection.extend({
initialize: function() {
this.on( "change:selected", this.changeSelected );
},
changeSelected: function( model, val, opts ){
if( val ){
this.each( function( e ){
if( e != model && e.get( "selected" ) ) e.set( "selected", false );
});
};
},
});
There are different ways you could do it. You could trigger an event on the collection itself and have all the SelectModel instances listen for it and update themselves accordingly. That seems a bit wasteful if you have a lot of SelectModel instances in the collection because most of them won't end up doing any work. What I would probably do is keep track of the last SelectModel in your View:
SelectListView = Backbone.View.extend({
initialize: function(){
this.collection = new SelectList();
this.lastSelectedModel = null;
},
toggleSelection: function(){
// populate newSelectedModel with the SelectedModel that you're toggling
var newSelectedModel = getNewSelectedModel();
if (!newSelectedModel.get('isSelected')) {
// if the SelectModel isn't already selected, we're about to toggle it On
// so we need to notify the previously selected SelectModel
if (this.lastSelectedModel) {
this.lastSelectedModel.set({isSelected: false});
}
this.lastSelectedModel = newSelectedModel;
} else {
// if the newSelectedModel we're about to toggle WAS already selected that means
// nothing is selected now so clear out the lastSelectedModel
this.lastSelectedModel = null;
}
newSelectedModel.set({isSelected: !newSelectedModel.get('isSelected')});
}
})

Categories

Resources