I am working in Marionette and have an accordion which we have set up so that the individual panels are templates that are called in and created by
var AccorionView = require(“../folder/AccordionView”);
var expandButtons = require(“../folder/expandButtons”);
var MainPage = Marionette.View.extend({
regions: {
region: “.region”,
button: “.buttons”
},
this.newAccordion = new AccordionView({
header: “header goes here”,
childView: new panelView(),
});
this.showChildView(‘region’, this.newAccordion);”
I am going to pull in another view with the actual Expand/Collapse All button in it, which will expand and collapse all of the accordion panels on this page. The JavaScript that would be used on this page would be
expandAll: function() {
this.newAccordion.expand();
},
However, this function will be put into the new JavaScript view of the buttons. I am going to send the names of the accordion panels to the button view when calling it into this page, but how do I get the function on that view to influence the accordion panels on this main page?
I would use Backbone.Radio in this case:
const Radio = require('backbone.radio');
const accorionChannel = Radio.channel('accorion');
const MainPage = Marionette.View.extend({
// ...
initialize() {
accorionChannel.on('expand', function() {
this.newAccordion.expand();
});
accorionChannel.on('unexpand', function() {
this.newAccordion.unexpand();
});
}
// ...
});
const WhateverView = Marionette.View.extend({
someEventHandler() {
accorionChannel.trigger('expand');
// OR
accorionChannel.trigger('unexpand');
}
});
Radio channel is singleton, you can create a new one every time but it will refer to the same channel. This saves you from passing the channel variable around or having a global variable.
You can do this one of two ways
1) With triggers/childViewEvents
// in expandButtons
expandButtons = Marionette.View.extend(
triggers: {
'click #ui.expandAll': 'expandAll'
}
);
// in MainPage
MainPage = Marionette.View.extend({
childViewEvents: {
'expandAll': 'expandAll'
},
expandAll: function(child) {
this.newAccordion.expand();
// OR
this.getChildView('region').expand();
}
})
OR
2) With Backbone.Radio
// in expandButtons
var Radio = require('Backbone.Radio');
var expandChannel = Radio.channel('expand');
var expandButtons = Marionette.View.extend({
events: {
'click #ui.expandAll': 'expandAll'
},
expandAll: function(e) {
expandChannel.trigger('expand:all');
}
});
// in AccordionView
var AccordionView = Marionette.View.extend({
channelName: 'expand',
radioEvents: {
'expand:all': 'expand' // triggers this.expand();
}
});
In this case, it might be even easier to do #2 but instead of adding the radio listener to the AccordionView, attach the listeners to the PanelView (AccordionView's childView). This is because AccordionView's expand function will likely have to iterate each of its children like:
this.children.each(function(childView) {
childView.expand();
});
Related
Anyone know why that, when clicked, the buttons do not add or remove overlays from the map? Full PLNKR here
The HTML
<div id="toggleButtons" style="display: none">
<button id="add">Add Overlays</button>
<button id="remove">Remove Overlays</button>
</div>
The Javascript
L.Control.GroupedLayers.include({
addOverlays: function () {
for (var i in this._layers) {
if (this._layers[i].overlay) {
if (!this._map.hasLayer(this._layers[i].layer)) {
this._map.addLayer(this._layers[i].layer);
}
}
}
},
removeOverlays: function () {
for (var i in this._layers) {
if (this._layers[i].overlay) {
if (this._map.hasLayer(this._layers[i].layer)) {
this._map.removeLayer(this._layers[i].layer);
}
}
}
}
});
var control = new L.Control.GroupedLayers(ExampleData.Basemaps, {
'Landmarks': {
'Cities': ExampleData.LayerGroups.cities,
'Restaurants': ExampleData.LayerGroups.restaurants
},
'Random': {
'Dogs': ExampleData.LayerGroups.dogs,
'Cats': ExampleData.LayerGroups.cats
}
}).addTo(map);
L.DomEvent.addListener(L.DomUtil.get('add'), 'click', function () {
control.addOverlays();
});
L.DomEvent.addListener(L.DomUtil.get('remove'), 'click', function () {
control.removeOverlays();
});
And then I added the mapbox legendControl.addLegend method (from the mapbox API documentation)
map.legendControl.addLegend(document.getElementById('toggleButtons').innerHTML);
Although the buttons are shown in the map, their click properties are not working. Any clues? Thanks!
You're not 'adding' the buttons with javascript, you're making a copy of them and placing the copy into the legendControl. The actual buttons with the eventhandlers are still present in the DOM but hidden because you've added display: none as inline style. What you want to do is select the buttons and remove them from the body:
var buttons = document.getElementById('toggleButtons');
document.body.removeChild(buttons);
Then you can add them to the legend and attach the eventhandlers:
var legendControl = L.mapbox.legendControl().addTo(map);
legendControl.addLegend(buttons.innerHTML);
L.DomEvent.addListener(L.DomUtil.get('add'), 'click', function () {
control.addOverlays();
});
L.DomEvent.addListener(L.DomUtil.get('remove'), 'click', function () {
control.removeOverlays();
});
Working example on Plunker: http://plnkr.co/edit/7pDkrZbS7Re1YshKZSLs?p=preview
PS. I'm quite baffled as to why you would abuse mapbox's legend control class to add two buttons. If you need a custom control you can just create one using leaflet's L.Control class. It spares you from loading the legend control class which you're not using, thus bloat.
EDIT: As promised in the comments below an example of rolling this solution into your own custom control. I'll explain to more throughout the comments in the code but the general idea is take the basic L.Control interface and adding the functionality and DOM generation to it:
// Create a new custom control class extended from L.Control
L.Control.Toggle = L.Control.extend({
// Have some default options, you can also change/set
// these when intializing the control
options: {
position: 'topright',
addText: 'Add',
removeText: 'Remove'
},
initialize: function (control, options) {
// Add the options to the instance
L.setOptions(this, options);
// Add a reference to the layers in the layer control
// which is added to the constructor upon intialization
this._layers = control._layers;
},
onAdd: function (map) {
// Create the container
var container = L.DomUtil.create('div', 'control-overlaystoggle'),
// Create add button with classname, append to container
addButton = L.DomUtil.create('button', 'control-overlaystoggle-add', container),
// Create remove button with classname, append to container
removeButton = L.DomUtil.create('button', 'control-overlays-toggleremove', container);
// Add texts from options to the buttons
addButton.textContent = this.options.addText;
removeButton.textContent = this.options.removeText;
// Listen for click events on button, delegate to methods below
L.DomEvent.addListener(addButton, 'click', this.addOverlays, this);
L.DomEvent.addListener(removeButton, 'click', this.removeOverlays, this);
// Make sure clicks don't bubble up to the map
L.DomEvent.disableClickPropagation(container);
// Return the container
return container;
},
// Methods to add/remove extracted from the groupedLayerControl
addOverlays: function () {
for (var i in this._layers) {
if (this._layers[i].overlay) {
if (!this._map.hasLayer(this._layers[i].layer)) {
this._map.addLayer(this._layers[i].layer);
}
}
}
},
removeOverlays: function () {
for (var i in this._layers) {
if (this._layers[i].overlay) {
if (this._map.hasLayer(this._layers[i].layer)) {
this._map.removeLayer(this._layers[i].layer);
}
}
}
}
});
Now you can use your new control as follows:
// Create a new instance of your layer control and add it to the map
var layerControl = new L.Control.GroupedLayers(baselayers, overlays).addTo(map);
// Create a new instance of your toggle control
// set the layercontrol and options as parameters
// and add it to the map
var toggleControl = new L.Control.Toggle(layerControl, {
position: 'bottomleft',
addText: 'Add overlays',
removeText: 'Remove overlays'
}).addTo(map);
I know, this is quick and dirty but it should give you a decent idea of what you can do with the L.Control class in general.
Here's a working example on Plunker: http://plnkr.co/edit/7pDkrZbS7Re1YshKZSLs?p=preview
And here's the reference for L.Control: http://leafletjs.com/reference.html#control
You need to follow delegation strategy here..
document.querySelector('body').addEventListener('click', function(event) {
if (event.target.id.toLowerCase() === 'add') {
control.addOverlays();
}
if (event.target.id.toLowerCase() === 'remove') {
control.removeOverlays();
}
});
Given a panel
var panel = new Backbone.CollectionView({...})
How do I get the current model being sorted?
panel.on('sortStart', function(e) {
var index = something;
});
I suppose you use some kind of UI manipulation tool for example jQuery UI. As Lesha said in her comment it can be done through triggering of event on the model view.
//creting children view
var PanelItem = Backbone.View.extend({
events: {
"sortStart": "sortEventPropagation"
},
initialize : function (options) {
this.parentView = options.parentView;
},
sortEventPropagation: function(){
this.parentView.trigger('sort:start:propagated', this.model);
},
})
Everytime you are creating panelItem view you need to pass it panel in options as parentView.
var childView = new PanelItem({
parentView: panel
})
And on panel you could easily listenTo sort:start:propagated event
var Panel = Backbone.CollectionView.extend({
initialize: function(){
this.listenTo(this, 'sort:start:propagated', function(model){
//Do magic with model
})
},
})
I have one form for saving and editing records. On clicking on a record, the form should be filled with the data. After filling, I want to do some UI actions (call jQuery Plugin etc.).
The pre-filling works, but when I'm trying to access the values, it works only at the second click. On the first click, the values are empty or the ones from the record clicked before.
This action is stored in the controller:
edit: function(id) {
var _this = this;
// prefill form for editing
var customer = this.store.find('customer', id).then(function(data) {
_this.set('name',data.get('name'));
_this.set('number',data.get('number'));
_this.set('initial',data.get('initial'));
_this.set('description',data.get('description'));
_this.set('archived',data.get('archived'));
// store user for save action
_this.set('editedRecordID',id);
_this.set('isEditing',true);
$('input[type="text"]').each(function() {
console.log(this.value)
});
});
},
I need a generic way to check if the input field is empty, because I want to include this nice UI effect: http://codepen.io/aaronbarker/pen/tIprm
Update
I tried to implement this in a View, but now I get always the values from the record clicked before and not from the current clicked element:
View
Docket.OrganizationCustomersView = Ember.View.extend({
didInsertElement: function() {
$('input[type="text"]').each(function() {
console.log(this.value)
});
}.observes('controller.editedRecordID')
});
Controller
Docket.OrganizationCustomersController = Ember.ArrayController.extend({
/* ... */
isEditing: false,
editedRecordID: null,
actions: {
/* ... */
edit: function(id) {
var _this = this;
// prefill form for editing
var customer = this.store.find('customer', id).then(function(data) {
_this.set('name',data.get('name'));
_this.set('number',data.get('number'));
_this.set('initial',data.get('initial'));
_this.set('description',data.get('description'));
_this.set('archived',data.get('archived'));
// store user for save action
_this.set('editedRecordID',id);
_this.set('isEditing',true);
});
},
/* ... */
});
Update 2
OK, I think I misunderstood some things.
At first, my expected console output should be:
1.
2.
3.
but is:
1.
3.
2.
Secondly: I can use any name, even foobar, for the observed method in my view. Why?
Controller
edit: function(id) {
var _this = this;
// prefill form for editing
var customer = this.store.find('customer', id).then(function(data) {
_this.set('name',data.get('name'));
_this.set('number',data.get('number'));
_this.set('initial',data.get('initial'));
_this.set('description',data.get('description'));
_this.set('archived',data.get('archived'));
console.log('1.')
// store user for save action
_this.set('editedRecordID',id);
_this.set('isEditing',true);
console.log('2.')
});
},
View
Docket.OrganizationCustomersView = Ember.View.extend({
foobar: function() {
console.log('3.')
$('input[type="text"]').each(function() {
console.log(this.value)
});
}.observes('controller.editedRecordID')
});
Update 3
I think I "figured it out" (but I don't know why):
Docket.OrganizationCustomersView = Ember.View.extend({
movePlaceholder: function() {
$('input[type="text"], textarea').bind("checkval",function() {
var $obj = $(this);
setTimeout(function(){
console.log($obj.val());
},0);
}.observes('controller.editedRecordID')
});
setTimeout(function(){ ... }, 0); does the trick. But why?!
You can convert use that jquery code in a component, this is the best way to create a reusable view, without putting ui logic in controllers, routers etc.
Template
<script type="text/x-handlebars" data-template-name="components/float-label">
<div class="field--wrapper">
<label >{{title}}</label>
{{input type="text" placeholder=placeholder value=value}}
</div>
</script>
FloatLabelComponent
App.FloatLabelComponent = Ember.Component.extend({
onClass: 'on',
showClass: 'show',
checkval: function() {
var label = this.label();
if(this.value !== ""){
label.addClass(this.showClass);
} else {
label.removeClass(this.showClass);
}
},
label: function() {
return this.$('input').prev("label");
},
keyUp: function() {
this.checkval();
},
focusIn: function() {
this.label().addClass(this.onClass);
},
focusOut: function() {
this.label().removeClass(this.onClass);
}
});
Give a look in that jsbin http://emberjs.jsbin.com/ILuveKIv/3/edit
I'm trying to remove my old carView and add the next one once the NEXT button is clicked.
Everything is coming out of a JSON file and is incrementing correctly but I want to view to also change.
Here's the code for my view:
window.CarContainerView = Backbone.View.extend({
el: $('#car-container'),
template:_.template($('#tpl-car-container').html()),
initialize:function () {
_.bindAll(this, 'clickNext');
this.car_number = 0;
this.car_model = this.model.get('carCollection').models[this.question_number];
this.question_view = null;
},
render:function () {
$(this.el).html(this.template());
this.car_view = new CarView({el: $(this.el).find('#car'), model: this.car_model});
this.question_view.render();
$('#next').bind('click', this.clickNext);
return this;
},
createNewCar: function () {
//build
console.log('createNewCar');
if(condition) {
//if the next button is pressed, clear screen and add new screen
}
},
clickNext: function () {
this.car_number++;
console.log(this.car_number);
createNewCar();
},
clickPrevious: function () {
}
});
Comments explain the changes. Basically, create a new CarView each time. And don't pass in the el to the view, else when you call remove that element will be gone. Instead, render the new view into #car each time.
window.CarContainerView = Backbone.View.extend({
el: $('#car-container'),
template:_.template($('#tpl-car-container').html()),
// use events hash instead of directly using jquery.
events: {
'click #next': 'clickNext'
},
initialize:function () {
// no need to use bindAll since using events hash
// binds the context of clickNext to this view already.
this.car_number = 0;
},
render:function () {
// use this.$el instead.
this.$el.html(this.template());
this.createNewCar();
return this;
},
createNewCar: function () {
if(this.car_view){
// cleanup old view.
this.car_view.remove();
}
// do some bounds checking here, or in clickNext... or both!
var car_model = this.model.get('carCollection').models[this.car_number];
// create a new view for the new car.
this.car_view = new CarView({model: car_model});
// render into #car instead of passing `el` into the view.
// let Backbone generate a div for the view, you dont need to
// set an `el` in the CarView either.
this.$('#car').html(this.car_view.render().el);
},
clickNext: function () {
this.car_number++;
this.createNewCar();
},
clickPrevious: function () {
}
});
I'm developing a web app using Backbonejs.
I have a use case where I have to pass the new position of div1 to a double click event handler of a Backbone view.
My code looks like
var MyView = Backbone.Views.extend({
events: {
'dblclick #div1' : 'div1ClickHandler' //here I want to pass new offset for #div1
}
});
div1ClickHandler: function()
{
......
}
var myView = new MyView({model: myModel,el : #div1});
You can do that: inside div you need to add a new field with name data-yourfieldName and from js call that:
yourFunctionName: function(e) {
e.preventDefault();
var email = $(e.currentTarget).data("yourfieldName");
}
Assuming that your view element is a child element of the jquery widget, the best thing is probably to grab the values you need in the click handler:
var MyView = Backbone.Views.extend({
events: {
'dblclick #div1' : 'div1ClickHandler'
}
});
div1ClickHandler: function()
{
var $this = $(this);
var $widget = $this.parents('.widget-selector:first');
$this.offset($widget.offset());
$this.height($widget.height());
$this.width($widget.width());
}
var myView = new MyView({model: myModel,el : #div1});
If the jquery widget is always the direct parent of your view element, you can replace parents('.widget-selector:first') with parent(); otherwise, you'll need to replace .widget-selector with a selector that will work for the jquery widget.
You can pass widget in view itself, then you will have full control over widget.
var MyView = Backbone.Views.extend({
initialize: function(options) {
this.widget = options.widget; // You will get widget here which you passed at the time of view creation
}
events: {
'dblclick #div1' : 'div1ClickHandler' //here I want to pass new offset for #div1
}
});
div1ClickHandler: function() {
// Query to fetch new position and dimensions using widget
// update the respective element
}
var myView = new MyView({model: myModel, el: $('#div1'), widget: widgetInstance});