Backbone with slider - backbone.js

This scenario could apply to a whole bunch of UI widgets, but for a simple example I'll use a slider (E.g. jQuery UI slider).
I have a jQuery slider that notifies the Backbone.Model when it 'slides' as well as when it stops. The Views update in both cases.
I want to add Undo/Redo functionality that will listen for changes in the Model, and create Undo objects for each change, using the previous() values. However, I only want to create Undo objects when the slider STOPs, not on every change during sliding.
So, I need the slider to notify the Model of changes to the slider value in two different ways that can be distinguished by the Undo code.
At the moment, I'm doing Model.trigger('slideValue', [newValue]) while sliding and the Views listen and update on this trigger.
Then when the slider stops, I do Model.set('slideValue', newValue) and the Undo functionality listens for these change events to create a new Undo object and add to the queue.
The reason I'm doing Model.trigger('slideValue', [newValue]) is that this allows me to notify all the Views that the Model is changing (so they can render this change), but when I come to do Model.set('slideValue', newValue) when the slider stops, the previous() value of the Model is available to my Undo functionality (hasn't been changed during the sliding).
But this still feels horribly hacked. Is there a nicer alternative to the Model.trigger()?

Please consider this working example http://jsfiddle.net/B4Ar6/1/
I used Backbone.Collection to add new undo values on stop event and Backbone.Model to hold/update current slider value on slide event.
// Get reference to the slider div
var sliderDiv = $( "#slider" );
// Get reference to the undo button
var undoButton = $( "#undo" );
// Create new model to save slider value state
var sliderValueStateModel = new (Backbone.Model.extend());
// Create new collection to save slider undo values
var sliderValueUndoCollection = new (Backbone.Collection.extend());
// Initialize silider
sliderDiv.slider();
// Add initial slider undo value to the collection
sliderValueUndoCollection.add({ value: sliderDiv.slider("value") });
// Listen to the undo button click
undoButton.on("click", function() {
var model, value;
// Do nothing when there is no undo history
if (sliderValueUndoCollection.length < 2) return false;
// Remove the last slider undo model with current value
sliderValueUndoCollection.pop();
// Get previous slider undo model
if (sliderValueUndoCollection.length === 1) {
// Keep initial value in collection
model = sliderValueUndoCollection.first();
} else {
// Get and remove the value from collection
model = sliderValueUndoCollection.pop();
}
// Get slider undo value from the model
value = model.get("value");
// Save new undo value to the collection
sliderValueUndoCollection.add({ value: value });
// Set the new value as previous slider undo value
sliderDiv.slider("option", "value", value);
});
// Listen to slide event from slider and set value to the model
sliderDiv.on("slide", function( event, ui ) {
// Save new slider value to the model
sliderValueStateModel.set({ value: ui.value });
});
// Listen to stop event from slider and add undo value to the collection
sliderDiv.on("slidestop", function( event, ui ) {
// Add new slider undo value to the collection
sliderValueUndoCollection.add({ value: ui.value });
});

Related

Knockout binding for gijgo treeview with checkbox

I like the gijgo treeview with checkbox as its clean and neat and it solves the purpose of showing the hierarchy information. Check below link for documentation.
https://gijgo.com/tree/demos/bootstrap-treeview-checkbox
Since knockout.js is preferred for the front end development hence its needed to develop a knockout binding for this particular requirement.
The idea is to populate the hierarchy data from the backend and bind it to the custom knockout binding.
The user selects/un-selects some checkboxes and then hits the save button. the selected/unselected data is again sent back to the server for the save.
The below code is the usage of the control in jquery.
The function tree.getCheckedNodes() returns the array of selected checkboxes.
How would one call the above function from an knockout binding.
ko.bindingHandlers.tree = {
init: function (element, valueAccessor, allBindingsAccessor) {
},
update: function (element, valueAccessor, allBindingsAccessor) {
var options = valueAccessor() || {};
var value = ko.utils.unwrapObservable(valueAccessor());
var tree = $(element).tree(value);
}
}
In the init method:
Unwrap the widget's initial data passed by your viewmodel from the valueAccessor
Convert the initial data to the format the tree widget understands
Initialize the widget with the correct settings $(element).tree({ /* ... */ })
Attach an event listener (.on("change", function() { }) to track user-input
In the event listener function, write back the data from the UI to the viewmodel (e.g. valueAccessor() (tree.getCheckedNodes()))
Optional: add custom disposal logic to clean up the widget if knockout removes it from the DOM
In the update method, which is called if your view model's value changes
Implement the logic that updates the widget based on your new settings. Probably something like tree.check(ko.unwrap(valueAccessor())). Make sure the update is "silent", if it would trigger a change event, you'd end up in an infinite loop.

Angular - Prevent event looping?

I need your help as I am still having trouble with events handling.
Here is the concept :
To contextualize, let's say that I have visuals items, that I call "components".
A component is defined by 2 javascript objects. One is containing data (like position x, y, width, height, color, etc...). The other one is the view exploiting a 3rd party library named draw2d.
Draw2d uses it's own x y width height color etc... properties. Which mean that I need to "bind" values to always keep them equals.
For this purpose, I just use events. There are 'on' events in draw2d letting me watch theses modifications. For the angular side, I just use the ng-model directive directly on my data bindings. When a data is updated, I update draw2d view, and when the view is edited, I update my data.
This part is working correctly as this is not hard to handle it.
I then tried to implement multiple selection editing. For that, I store in an array every selected component. When a value from the view or from the data is edited, I trig an event which let every selected component to set the same data to the same value. Again, it's working great.
Where i'm starting to struggle hard, it's when I want to edit the position. I don't want every components to be on top of each other. For that, when I edit multiple x or y attributes, instead of positioning elements, I calculate the offset so that every components move with this offset only. Here is where it's not okay. Of course, when an offset is set, others components updates, also trigging their new value, so I'm stuck in a hell loop.
I would like to know if you know any pattern to handle events so that, when you have 2 view and a model, you can stop an event from propagate.
Here is my propagation snippet :
scope.canvas.on('figureEdited', function (canvas, event) {
if (scope.selectedComponents.length > 1) {
var key = event.key;
var value = event.value;
var originComponent = event.origin;
for (var i = 0; i < scope.selectedComponents.length; i++) {
var selectedComponent = scope.selectedComponents[i];
if (selectedComponent !== originComponent) {
if (key !== 'x' && key !== 'y') {
selectedComponent.setData(key, value);
} else {
var offset = value - event.oldValue;
selectedComponent.setData(key, selectedComponent.getData(key) + offset, true);
}
}
}
}
});
the issue is caused by
selectedComponent.setData(key, selectedComponent.getData(key) + offset, true);
It retriggers a figureEdited event, making every component move again by this offset, infinitely (stopped by 10 digest iterations).
I hope this is clear, sorry for the long text and thanks a lot for your help :)
Alann.
If the setData call triggers new events synchronously you can ignore events triggered while in your loop by setting an ignore flag and checking it at the start of the function.
var ignoreEvent = false;
scope.canvas.on('figureEdited', function (canvas, event) {
if (ignoreEvent) return;
ignoreEvent = true;
//...
ignoreEvent = false;
});

Backbone.Collection.reset() => child view is out of sync with parent

I have a list of items. They are stored in backbone pageable collection.
They are displayed like this
|---item1---------------------------|
|---item2---------------------------|
|---item3---------------------------|
|---item4---------------------------|
|---item5---------------------------|
|---item6---------------------------|
|---item7---------------------------|
<< 1,2,3...end >>
User can click on individual item to open detail view in a separate page. Detail view has listeners initialized
when it's created. Those listeners are bound to the item model.
Since the detail view is huge, I cache it in the DOM by toggling the visibility.
The subsequent click on the item will toggle the cached view.
------ here is the problem -----
When item list is switched to another page, the collection is reset (by paginator). And all the models previously stored in the collection is dereferenced and
a new set of models is created. So after the page is switched back and forth, the previously opened item has a different copy of itself stored
in the collection. So when I change the name of the item in the detail view (in the view cache), the name in the item list is not changed.
The views are out of sync! because they are referencing to different models.
Not sure if anyone else encounter this before. If you do, please share with me how you solve it.
Thanks very much.
The most straight-forward way to maintain a fresh reference between your list view items and the corresponding detail view, on page change, is to re-render the detail view. But I'm assuming this options is not acceptable within the scope of your project.
What I often do, when I have the task of forming relationships within logically separate views is use listeners. As long as the views share a unique identifier (for example, they both share a model, or at least identical model ids), I can always send a message that will reach the view I'm interested in.
For this you'll need a centralized event hub, which with Backbone is trivially easy to generate. In some appropiately global variable (like, for example, MyApp) we simply do:
MyApp.EventBus = _.extend({}, Backbone.Events);
Set up the detail view
On the detail view initialize function I would drop this listener,
initialize: function () {
// Listen to a toggle visibility on this view
this.listenTo(MyApp.EventBus, 'detail-view:toggle-view', toggleView);
},
toggleView: function (id) {
if (this.model.id == id) {
// Show this view if I have the passed id
this.$el.show()
// Notify the parent list item view that its detail view exists
MyApp.EventBus.trigger('detail:view:exists', true);
} else {
// Hide all other views
this.$el.hide();
}
},
changeName: function () {
// logic that parses DOM user input to
// local variable name
// We now trigger an event 'detail-view:change:name', and we send as
// parameters our model's id and the new name
MyApp.EventBus.trigger('detail-view:change:name', this.model.id, name);
}
Setting up the list item view
The list item view will want to listen to a name change (or any other model property in the detail view that you want the list item to be aware of). So we'll set up a handler for the 'detail-view:change:name' event.
We'll also want to wire our click handler to toggle the visibility of the list item's detail view. The tricky part is to handle the event that a view has not been rendered yet (I'm assuming you're lazy loading the detail view). So we set up a second listener for the detail:view:exists event the detail view triggers when it catches a detail-view:toggle-view event. If we don't hear the detail:view:exists event from the targeted detail view in a timely manner (I'm using 100 ms, but you can play around with that to suit your needs), then we render the view.
initialize: function () {
// Listen to when the detail associated with this list item changes
// the the list item name
this.listenTo(MyApp.EventBus, 'detail-view:change:name', onNameChange);
// Set a property in this view if its detail view exists
this.listenTo(MyApp.EventBus, 'detail:view:exists',
_.bind(function () { this.detailViewExists = true; }, this));
// Create a debounced function that tests whether this view's
// detail view exists
_.debounce(_.bind(this.handleViewState, this), 100);
},
events {
click: 'toggleDetailView'
},
toggleDetailView: function (id) {
MyApp.EventBus.trigger('detail-view:toggle-view', this.model.id);
this.handleViewState();
},
// Debounced function that will wait 100ms asynchronously for the
// detail view to respond. If the detailViewExists bit is not set to true
// then we assume the view does not exist and we render it
handleViewState: function () {
if (!this.detailViewExists)
// The view does not exist, render and attach the view
// Set the bit to false to allow testing in the event that the detail view
// is destroyed in the future
this.detailViewExists = false;
},
changeName: function (id, newname) {
if (this.model.id == id) {
// Change the name of this list item view
this.$('.item-name').text(newname);
}
The take-away
Now, the reference between these two disparate views is the shared unique identifier. Since, by design, these two identifiers are unique in their scope, and should not change, and assuming the detail view has been rendered and attached to the DOM, then regardless of the rendering its state the list item view will always be able to communicate with its detail view.

Collection create function firing add event too quickly

(Using Backbone 0.9.10)
I have a list view of sorts where the user can click a button to show a modal view. The list view has a counter that shows the amount of items contained in the list. Clicking a button in the modal view executes create on a collection that is passed into both views. This fires the add event in the list view, which in turn runs this function:
renderCount: function () {
// Container for model count
var $num = this.$el.find('.num');
if ($num.length > 0) {
// 'count' is a value returned from a service that
// always contains the total amount of models in the
// collection. This is necessary as I use a form of
// pagination. It's essentially the same as
// this.collection.length had it returned all models
// at once.
$num.html(this.collection.count);
}
}
However, add seems to be fired immediately (as it should be, according to the docs), before the collection has a chance to update the model count. I looked into sync but it didn't seem to do much of anything.
How can I make sure the collection is updated before calling renderCount?
Here's the list view initialize function, for reference:
initialize: function (options) {
this.collection = options.collection;
this.listenTo(this.collection, 'add remove reset', this.renderCount);
this.render();
}
EDIT:
Turns out I was forgetting to refetch the collection on success in the modal view.
$num.html(this.collection.count);
shoule be:
$num.html(this.collection.size());
Backbone.Collection uses methods imported from underscore, here is list: http://backbonejs.org/#Collection-Underscore-Methods
Turns out I was forgetting to refetch the collection on success in the modal view.

Registering events works only first time

I have a window, and inside it a panel. The panel contains text (basic html). After the window is ready I call the following function, which finds elements with specific class, and registers click events on them. This works at first.
After closing the window, and recreating 1:1 similar window the events will not fire. The same happens if I .update() the panel and re-run my function - the events fail to fire. Why is this?
I can still see elements being found, and apparently some events must be registered, but my clicks can't be captured by the debug code, or the receiving function anymore.
addEvents: function(win) {
// The Window
var ow = win;
// Using this debug trick I can see that on the second time the events wont fire
// -- nothing gets printed to console
Ext.util.Observable.capture(ow, function(){
console.log(arguments);
});
// Will search for elements, finds elements that have class myclass
// In my case the elements are just ordinary html tags in the visible content
// area of the panel.
var elems = ow.down('panel').getEl().select(".myclass").elements;
Ext.Array.forEach(elems, function (item, index, allItems) {
// We need Ext DOM element to be able to attach stuff to it
var elem = new Ext.dom.Element(item);
elem.on ('click', function (evt, el, o) {
ow.fireEvent('myevent', ow, elem);
});
});
}
I suspected at first that I have to actually unregister the previous events and destroy the window, so I tried adding this to the close of the window:
window.down('panel').destroy();
window.destroy();
However it seems I have some other problem I really am unable to understand.
This is a very JQuery-ish way to add events. You are dealing with components and should add event listeners on components. If you need to delegate events down to html elements then you need to set a single event listener on the Component encapsulating the elements and add delegate config to the actual html elements.
Here are some resources:
Explain ExtJS 4 event handling
Event delegation explained:
http://www.sencha.com/blog/event-delegation-in-sencha-touch (applies to extjs just as well)
More on Listeners with extjs: https://stackoverflow.com/a/8733338/834424

Resources