Trigger function on click if View has a certain class (backbone.js) - javascript

I have a div generated by a backbone.js view. When the user clicks on this div, a class active is added to the div and the function addToSet is executed.
Problem: I want another function to be triggered when the View's div has the class active. However, my attempt shown below always cause addToSet function to run when its clicked.
Now, I remove 'click': 'addToSet' from the events function, leaving only 'click .active': 'removeFromSet'. Clicking on the div does not cause anything to happen! Is this because the event handler cannot select the div of the view itself, just the elements inside it?
Any idea how I can solve this problem? Thanks!
JS Code
SetView = Backbone.View.extend({
tagName: 'div',
className: 'modal_addit_set',
template: _.template( $('#tpl_modal_addit_set').html() ),
events: {
'click': 'addToSet',
'click .active': 'removeFromSet'
},
initialize: function(opts) {
this.post_id = opts.post_id;
},
render: function() {
$(this.el).html( this.template( this.model.toJSON() ) );
if(this.model.get('already_added'))
$(this.el).addClass('active');
return this;
},
addToSet: function() {
$.post('api/add_to_set', {
post_id: this.post_id,
set_id: this.model.get('id'),
user_id: $('#user_id').val()
});
},
removeFromSet: function() {
$.post('api/remove_from_set', {
post_id: this.post_id,
set_id: this.model.get('id')
});
}
});

Have you tried to use a :not(.active) selector for one of your event delegates? This may help differentiate between the two scenarios.
Something like this:
events: {
'click :not(.active)' : callback1
'click .active' : callback2
}

These events:
events: {
'click': 'addToSet',
'click .active': 'removeFromSet'
}
don't work and you sort of know why. From the fine manual:
Events are written in the format {"event selector": "callback"}. The callback may be either the name of a method on the view, or a direct function body. Omitting the selector causes the event to be bound to the view's root element (this.el).
So your 'click': 'addToSet' binds addToSet to a click on the view's el itself but 'click .active': 'removeFromSet' binds removeFromSet to a .active element inside the view's el.
I think the easiest solution is to have a single event:
events: {
'click': 'toggleInSet'
}
and then:
toggleInSet: function() {
if(this.$el.hasClass('active')) {
$.post('api/remove_from_set', {
post_id: this.post_id,
set_id: this.model.get('id')
});
}
else {
$.post('api/add_to_set', {
post_id: this.post_id,
set_id: this.model.get('id'),
user_id: $('#user_id').val()
});
}
}
You could use an instance variable instead of a CSS class to control the branching in toggleInSet if that makes more sense.

Related

Javascript identifier for widget in Odoo

If you have two widget in a view. And you do something with the first widget and you want to update (call display_field) the second widget. How to have the identifier for the second widget?
For example in the extend definition of a widget:
local.FieldNewWidget = instance.web.form.AbstractField.extend({
init: function(parent, options) {
},
events: {
'click .oe_new_input_button': 'open_new_specific_form',
},
start: function() {
},
display_field: function() {
},
render_value: function() {
},
open_new_specific_form: function(event) {
var self = this;
var new_action = {
type: 'ir.actions.act_window',
name: $(event.target).data('name'),
res_model: $(event.target).data('data-model'),
res_id: $(event.target).data('res-id'),
view_mode: 'form',
view_type: 'form',
views: [[false, 'form']],
target: 'new',
context: {
},
flags: {'form': {'action_buttons': true}},
}
self.do_action(new_action, {
on_close: function() {
// I want to refresh (call display_field) the second widget here.
// What is the identifier for the second widget?
},
});
},
});
i think this will work but i don't know if it's the best solution. I think every widget knows witch view it's by using (this.view). why don't you use a special event to trigger it from one widget and listen for it in the other one.
For example Register an event listener on the widget to listen for property changing on the view:
//in first widget register the event listener:
this.view.on('property_name', this, this.your_method);
// in second widget trigger the event by setting the value
this.view.set('property_name', a_value);
i'm new to odoo javascript let me know if this works for you i think there is a better solution by using events triggering without changing properties at all.

(Re)rendering Backbone view in change event handler does not work

I'm having two form elements, both 2-way-databinded via backbone.stickit.
The second form element (#input) is just cosmetics - there for showing it's actually working.
The idea is that my View gets (re)rendered,every time the option inside the dropdown (#select) menu gets changed.
I'm trying to achieve that by catching the the 'changed' event of #select and call this.render() to (re)render the view.
Apparently that doesn't work. The selected option doesn't get saved back into the model and I fail to understand why.
I'm not looking for a solution, rather than an explanation, why the following code doesn't work. The solution (as in: works for me) is part of the fiddle - commented out.
HTML:
<script type="text/template" id="tpl">
<h1>Hello <%= select %></h1>
<select id="select">
</select>
<p>Select:
<%= select %>
</p>
<hr>
<input type="text" id="input">
<p>Input:
<%= input %>
</p>
</script>
<div id="ctr"></div>
JavaScript:
Foo = Backbone.Model.extend({
defaults: {
select: "",
input: "",
}
});
FooView = Backbone.View.extend({
el: '#ctr',
template: _.template($('#tpl').html()),
initialize() {
this.model.bind('change', function() {
console.log("model change:");
console.log(this.model.get('select'));
console.log(this.model.get('input'));
}, this);
//this.model.bind('change:select', function() { this.render(); }, this); // <--------------------- WORKS
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
this.stickit();
return this;
},
events: {
'change #select': function(ev) {
console.log('change event triggered:');
console.log(this.model.get('select'));
console.log(this.model.get('input'));
this.render(); // <--------------------- DOES NOT WORK - WHY?
},
/* 'click #render': function(ev) {
console.log('render event triggered:');
console.log(this.model.get('select'));
console.log(this.model.get('input'));
this.render();
} */
},
bindings: {
'#input': 'input',
'#select': {
observe: 'select',
selectOptions: {
collection: function() {
return [{
value: '1',
label: 'Foo'
}, {
value: '2',
label: 'Bar'
}, {
value: '3',
label: 'Blub'
}]
}
}
},
},
});
new FooView({
model: new Foo()
}).render();
https://jsfiddle.net/r7vL9u07/9/
The reason it does not work to call this.render() from within your change #select event handler is because you are disrupting the two-way data binding that Backbone.stickit is providing you. The flow goes something like the following:
User changes the value of '#select'.
Your change #select handler fires and calls this.render().
render repopulates #ctr with a new select menu with no selected option.
Backbone.stickit responds to the change to #select.
Backbone.stickit tries to obtain the value of #select, but since it contains no selected option the value is undefined.
Backbone.sticket sets the model's select attribute to undefined.
The reason it works if you move the this.render() call to within the model's change:select handler is because Backbone.stickit is able to correctly update the model without the DOM changing before it gets the chance.

Marionette CompositeView undelegating childview events on setElement

I have a marionette compositeview which I am using to create a item list for a profile page on an app. For the child view, I extend from an already existing ItemView.
When I use this.setElement(this.el.innerHTML) in the compositeview onRender function, all the events set in the child view no longer are triggered and even more so, triggering them in the console on the inspector tool in the browser, does nothing.
However when I do not use setElement, the container div is added to my markup, but now all the events in the child view work.
Can someone help me understand this please.
The Collection I am using has a custom clone method.
I am using a global collection which is updated and stored in cache on each fetch.
When I actually instantiate my view, the collection has already been used and a region in the main layout view has been populated with a item list similar to the one I want to render.
This is how I instantiate my view:
var currentUser = Profile.get('username');
// Perform changes to global collection
Items.url = API + '/items/search?q=' + currentUser + '&size=20';
Items.parse = function (response) {
if (!response.results) {
return response;
} else {
return response.results;
}
};
Items.fetch(
{success: function (collection, response, options) {
this.listOfItems = new View.itemListProfilePage({
template: TemplIds.profilePagePostedItems,
parentClass: 'profile-cols',
collection: Items, // global collection
filterAttr: {user: currentUser},
isFiltered: true,
lazyLoad: true,
childViewContainer: '#profile-items',
childView: View.itemProfilePage.extend({
template: TemplIds.item
})
});
Backbone.trigger('main:show', this.listOfItems); //'main:show' is an event in layoutview which calls region.show
},
remove: false
});
My compositeview:
View.itemListProfilePage = Marionette.CompositeView.extend({
collection: null, //original collection cloned later for filtering
fetch: null, //promise for fetched items
lazyView: null,
options: {
parentClass: '',
filterAttr: {},
isFiltered: false,
lazyLoad: false
},
initialize: function () {
this.stopListening(this.collection);
//Change collection property and re-apply events
this.collection = this.collection.clone(this.options.filterAttr, this.options.isFiltered);
this._initialEvents();
this.collection.reset(this.collection.where(this.options.filterAttr), {reset: true});
this.listenTo(Backbone, 'edit:profileItems', this.addEditClassToSection);
},
onRender: function () {
this.setElement(this.el.innerHTML, true);
},
onShow: function () {
if (this.options.parentClass) {
this.el.parentElement.className = this.options.parentClass;
}
},
addEditClassToSection: function (options) {
if ( options.innerHTML !== 'edit' ) {
this.el.classList.add('edit-mode');
} else {
this.el.classList.remove('edit-mode');
}
},
}
The parent ItemView:
View.Item = Marionette.ItemView.extend({
model: null,
numLikes: null, //live set of DOM elements containing like counter
modalItem: null, //view class with further details about the item to be used within a modal
events: {
'click img.highlight': 'showModal'
},
initialize: function (options) {
var itemWithHeader; //extended item view class with header at the top and no footer
var addToCart;
//Set up all like-related events
this.listenTo(this.model, "change:numLikes", this.updateNumLikes);
this.listenTo(this.model, "change:liked", this.updateLiked);
//Set up the view classes to be used within the modal on click
itemWithHeader = View.ItemWithHeader.extend({
template: this.template,
model: this.model //TODO: move to inside itemDetails
});
itemAddToCart = View.ItemAddToCart.extend({
template: TemplIds.itemAddCart,
model: this.model //TODO: move to inside itemDetails
});
this.modalItem = View.ItemDetails.extend({
template: TemplIds.itemDetails,
model: this.model,
withHeader: itemWithHeader,
addToCart: itemAddToCart
});
},
onRender: function () {
var imgContainerEl;
var likeButtonEl;
//Get rid of the opinionated div
this.setElement(this.el.innerHTML);
this.numLikes = this.el.getElementsByClassName('num');
//Add the like button to the image
likeButtonEl = new View.LikeButton({
template: TemplIds.likeButton,
model: this.model
}).render().el;
this.el.firstElementChild.appendChild(likeButtonEl); //insert button inside img element
},
showModal: function (evt) {
var modalView = new View.Modal({
views: {
'first': {view: this.modalItem}
}
});
Backbone.trigger('modal:show', modalView);
},
});
The itemView for each individual item in my list:
View.itemProfilePage = View.Item.extend({
events: _.extend({},View.Item.prototype.events, {
'click .delete-me': 'destroyView'
}
),
onRender: function () {
View.Item.prototype.onRender.call(this);
this.deleteButtonEl = new View.itemDeleteButton({
template: TemplIds.deleteButton
}).render().el;
this.el.firstElementChild.appendChild(this.deleteButtonEl);
},
destroyView: function (evt) {
this.model.destroy();
}
});
The short answer is that you should not be using setElement.
Backbone specifically uses the extra container div to scope/bind the view's events. When you use setElement you are changing what the parent element is. Since you are doing this in the onRender function, which is called after the template has been rendered and the events have already been bound, you are losing your event bindings.
The correct thing to do if you are going to use Marionette and Backbone is to expect and utilize the "extra" div wrapper that is generated when you render a view. You can take control of the markup for that "wrapper" div by using className, id, and tagName view properties on your view classes.

Marionette: can you use the same event configuration for a trigger and an event?

If I set up a Marionette view using the same event configuration for both a trigger and an event, only the trigger seems to get fired. Here's a simplified example: clicking .button should run show and trigger the select:something event. However, it only triggers the event.
var MyView = Marionette.CompositeView.extend({
ui: {
'button': '.button'
},
triggers: {
'click .button': 'select:something',
},
events: {
'click .button': 'show'
},
show: function() {
// won't fire
}
});
var view = new MyView({});
view.on('select:something', handleSelect); // will fire
The Marionette trigger docs suggested that setting stopPropogation to false would help, but that didn't change the behavior for me:
triggers: {
'click .button': {
event: "select:something",
preventDefault: true,
stopPropagation: false
}
}, ...
Marionette.View delegateEvents codes is:
// behavior events will be overriden by view events and or triggers
_.extend(combinedEvents, behaviorEvents, events, triggers, behaviorTriggers);
Backbone.View.prototype.delegateEvents.call(this, combinedEvents);
Events will be overriden by triggers because that events hash is before triggers hash in _.extend.
You could use different key in events and triggers hash. The code like this:
var viewtemplate = _.template('<button class="button show-btn">show</button>');
var MyView = Marionette.CompositeView.extend({
template: viewtemplate,
triggers: {
'click .button': 'select:something',
},
events: {
'click .show-btn': 'show'
},
show: function() {
console.log('click .show-btn');
}
});

Backbone nested view has events which are not triggering

Okay so I have a parent view which has a click event which renders a child view. Within this child view is a form which I'm trying to validate and then submit. So my parent view looks something like this:
var MapView = Backbone.View.extend({
el: '.body',
template: _.template(MapTemplate),
render: function() {
...
},
events: {
'click #log-pane-title': 'loadLogView'
},
loadLogView: function() {
var eventLogView = new EventLogView({
id: properties._id
});
eventLogView.render();
}
});
And my child view looks something like this:
var EventLogView = Backbone.View.extend({
el: '#eventlog',
logform: new NewLogForm({
template: _.template(AddLogTemplate),
model: new LogModel()
}).render(),
render: function() {
// Render the form
$("#addtolog").html(this.logform.el);
},
events: {
'submit #addlogentry': 'test'
},
test: function() {
alert('inside eventlogview');
return false;
}
});
The problem I'm facing is that test() never fires. For debugging purposes I made sure the submit event was even firing by putting:
$('#addlogentry').on('submit', function() {
alert( "submit firing" );
return false;
});
In render() of the EventLogView. That does actually trigger, so I'm not sure what's going on and why test() isn't triggering.
To avoid scoping issues all the events delegation are scoped to the views el in Backbone.
So your #addlogentry button should live inside your EventLogView el.
And your sanity check in the render should look something like this to mimic how Backbone works internally :
this.$el.on('submit', '#addlogentry', function() {
alert( "submit firing" );
return false;
});

Categories