Ordering backbone views together with collection - sorting

I have a collection which contains several items that should be accessible in a list.
So every element in the collection gets it own view element which is then added to the DOM into one container.
My question is:
How do I apply the sort order I achieved in the collection with a comparator function to the DOM?
The first rendering is easy: you iterate through the collection and create all views which are then appended to the container element in the correct order.
But what if models get changed and are re-ordered by the collection? What if elements are added? I don't want to re-render ALL elements but rather update/move only the necessary DOM nodes.

model add
The path where elements are added is rather simple, as you get the index in the options when a model gets added to a collection. This index is the sorted index, based on that if you have a straightforward view, it should be easy to insert your view at a certain index.
sort attribute change
This one is a bit tricky, and I don't have an answer handy (and I've struggled with this at times as well) because the collection doesn't automatically reshuffle its order after you change an attribute the model got sorted on when you initially added it.
from the backbone docs:
Collections with comparator functions will not automatically re-sort
if you later change model attributes, so you may wish to call sort
after changing model attributes that would affect the order.
so if you call sort on a collection it will trigger a reset event which you can hook into to trigger a redraw of the whole list.
It's highly ineffective when dealing with lists that are fairly long and can seriously reduce user experience or even induce hangs
So the few things you get walking away from this is knowing you can:
always find the index of a model after sorting by calling collection.indexOf(model)
get the index of a model from an add event (3rd argument)
Edit:
After thinking about if for a bit I came up with something like this:
var Model = Backbone.Model.extend({
initialize: function () {
this.bind('change:name', this.onChangeName, this);
},
onChangeName: function ()
{
var index, newIndex;
index = this.collection.indexOf(this);
this.collection.sort({silent: true});
newIndex = this.collection.indexOf(this);
if (index !== newIndex)
{
this.trigger('reindex', newIndex);
// or
// this.collection.trigger('reindex', this, newIndex);
}
}
});
and then in your view you could listen to
var View = Backbone.View.extend({
initialize: function () {
this.model.bind('reindex', this.onReindex, this);
},
onReindex: function (newIndex)
{
// execute some code that puts the view in the right place ilke
$("ul li").eq(newIndex).after(this.$el);
}
});

Thanks Vincent for an awesome solution. There's however a problem with the moving of the element, depending on which direction the reindexed element is moving. If it's moving down, the index of the new location doesn't match the index of what's in the DOM. This fixes it:
var Model = Backbone.Model.extend({
initialize: function () {
this.bind('change:name', this.onChangeName, this);
},
onChangeName: function () {
var fromIndex, toIndex;
fromIndex = this.collection.indexOf(this);
this.collection.sort({silent: true});
toIndex = this.collection.indexOf(this);
if (fromIndex !== toIndex)
{
this.trigger('reindex', fromIndex, toIndex);
// or
// this.collection.trigger('reindex', this, fromIndex, toIndex);
}
}
});
And the example listening part:
var View = Backbone.View.extend({
initialize: function () {
this.model.bind('reindex', this.onReindex, this);
},
onReindex: function (fromIndex, toIndex) {
var $movingEl, $replacingEl;
$movingEl = this.$el;
$replacingEl = $("ul li").eq(newIndex);
if (fromIndex < toIndex) {
$replacingEl.after($movingEl);
} else {
$replacingEl.before($movingEl);
}
}
});

Related

How to create a Data Table from forge viewer

I want to create a table that represents the data from a model that i have loaded in the forge viewer.
i want the table to be in a docking panel, and show the propertys for elements in the model ( level, name, comment,Area, type name--> for each element [if it has the property])
i have tried to use the API reference, and create a DataTable, but i did not find examples of how to actually impliment it.
where and when do i need to set the datatable? ( after or before creating the docking pannel?)
what is the content of the arrays that i should pass in the constructor? ( according to the documentation : array of arrays for the rows, and array for the columns. is the row array, is simply an array that contains the columns arrays?)
this is my current code for the extension that shows the amount to instances in the model for each property that i want to adjust:
'''
class testTest extends Autodesk.Viewing.Extension {
constructor(viewer, options) {
super(viewer, options);
this._group = null;
this._button = null;
}
load() {
console.log('testTest has been loaded');
return true;
}
unload() {
// Clean our UI elements if we added any
if (this._group) {
this._group.removeControl(this._button);
if (this._group.getNumberOfControls() === 0) {
this.viewer.toolbar.removeControl(this._group);
}
}
console.log('testTest has been unloaded');
return true;
}
// The Viewer contains all elements on the model, including categories (e.g. families or part definition),
//so we need to enumerate the leaf nodes, meaning actual instances on the model.
getAllLeafComponents(callback) {
this.viewer.getObjectTree(function (tree) {
let leaves = [];// an empty 'leaf' list that we want to fill wiith the objects that has no mo children
//dbId== object id
// for each child that we enumerate from a root, call a code , and finally a true /false flag parameter that run the function recursivly for all the children of a child.
tree.enumNodeChildren(tree.getRootId(), function (dbId) {
if (tree.getChildCount(dbId) === 0) {
leaves.push(dbId);// if the object has no children--> add it to the list.
}
}, true);// the last argument we past ("true") will make sure that the function in the seccond argument ("function (dbId)(...)" ")will run recursively not only for the children of the roots,
//but for all the children and childrtn's children.
callback(leaves);//return the leaves
});
}
onToolbarCreated() {
// Create a new toolbar group if it doesn't exist
this._group = this.viewer.toolbar.getControl('allMyAwesomeExtensionsToolbar');//if there is no controller named "allMyAwesomeExtensionsToolbar" create one
if (!this._group) {
this._group = new Autodesk.Viewing.UI.ControlGroup('allMyAwesomeExtensionsToolbar');
this.viewer.toolbar.addControl(this._group);// add the control to tool bar
}
// Add a new button to the toolbar group
this._button = new Autodesk.Viewing.UI.Button('testTest');
this._button.onClick = (ev) => {
// Check if the panel is created or not
if (this._panel == null) {//check if there is an instance of our pannel. if not- create one
this._panel = new ModelSummaryPanel(this.viewer, this.viewer.container, 'modelSummaryPanel', 'Model Summary');
}
// Show/hide docking panel
this._panel.setVisible(!this._panel.isVisible());//cal a method from the parent to show/ hide the panel -->use this to toggle from visible to invisible
this._panel.set
// If panel is NOT visible, exit the function
if (!this._panel.isVisible())
return;
// First, the viewer contains all elements on the model, including
// categories (e.g. families or part definition), so we need to enumerate
// the leaf nodes, meaning actual instances of the model. The following
// getAllLeafComponents function is defined at the bottom
this.getAllLeafComponents((dbIds) => {// now we have the list of the Id's of all the leaves
// Now for leaf components, let's get some properties and count occurrences of each value
debugger;
const filteredProps = ['Level','Name','Comments','Area','Type Name'];
// Get only the properties we need for the leaf dbIds
this.viewer.model.getBulkProperties(dbIds,filteredProps , (items) => {
// Iterate through the elements we found
items.forEach((item) => {
// and iterate through each property
item.properties.forEach(function (prop) {
// Use the filteredProps to store the count as a subarray
if (filteredProps[prop.displayName] === undefined)
filteredProps[prop.displayName] = {};
// Start counting: if first time finding it, set as 1, else +1
if (filteredProps[prop.displayName][prop.displayValue] === undefined)
filteredProps[prop.displayName][prop.displayValue] = 1;
else
filteredProps[prop.displayName][prop.displayValue] += 1;
});
});
// Now ready to show!
// The PropertyPanel has the .addProperty that receives the name, value
// and category, that simple! So just iterate through the list and add them
filteredProps.forEach((prop) => {
if (filteredProps[prop] === undefined) return;
Object.keys(filteredProps[prop]).forEach((val) => {
this._panel.addProperty(val, filteredProps[prop][val], prop);
this.dt = new DataTabe(this._panel);
this.dt.setData()
});
});
});
});
};
this._button.setToolTip('Or Levis extenssion');
this._button.addClass('testTest');
this._group.addControl(this._button);
}
}
Autodesk.Viewing.theExtensionManager.registerExtension('testTest', testTest);'''
'''
We have a tutorial with step-by-step on how to do it.
Please, refer to Dashboard tutorial, specifically in Data Grid section.
In this case, the tutorial uses an external library (Tabulator) to show the data.

Angular Meteor objects not acting as expected

I am working with Angular Meteor and am having an issue with my objects/arrays. I have this code:
angular.module("learn").controller("CurriculumDetailController", ['$scope', '$stateParams', '$meteor',
function($scope, $stateParams, $meteor){
$scope.curriculum = $meteor.object(CurriculumList, $stateParams.curriculumId);
$scope.resources = _.map($scope.curriculum.resources, function(obj) {
return ResourceList.findOne({_id:obj._id})
});
console.log($scope.resources)
}]);
I am attempting to iterate over 'resources', which is a nested array in the curriculum object, look up each value in the 'ResourceList' collection, and return the new array in the scope.
Problem is, sometimes it works, sometimes it doesnt. When I load up the page and access it through a UI-router link. I get the array as expected. But if the page is refreshed, $scope.resources is an empty array.
My thought is there is something going on with asynchronous calls but have not been able for find a solution. I still have the autopublish package installed. Any help would be appreciated.
What you're going to do is return a cursor containing all the information you want, then you can work with $meteor.object on the client side if you like. Normally, publishComposite would look something like this: (I don't know what your curriculum.resources looks like)
Use this method if the curriculum.resources has only ONE id:
// this takes the place of the publish method
Meteor.publishComposite('curriculum', function(id) {
return {
find: function() {
// Here you are getting the CurriculumList based on the id, or whatever you want
return CurriculumList.find({_id: id});
},
children: [
{
find: function(curr) {
// (curr) will be each of the CurriculumList's found from the parent query
// Normally you would do something like this:
return ResourceList.find(_id: curr.resources[0]._id);
}
}
]
}
})
This method if you have multiple resources:
However, since it looks like your curriculum is going to have a resources list with one or many objects with id's then we need to build the query before returning anything. Try something like:
// well use a function so we can send in an _id
Meteor.publishComposite('curriculum', function(id){
// we'll build our query before returning it.
var query = {
find: function() {
return CurriculumList.find({_id: id});
}
};
// now we'll fetch the curriculum so we can access the resources list
var curr = CurriculumList.find({_id: id}).fetch();
// this will pluck the ids from the resources and place them into an array
var rList = _.pluck(curr.resources, '_id');
// here we'll iterate over the resource ids and place a "find" object into the query.children array.
query.children = [];
_.each(rList, function(id) {
var childObj = {
find: function() {
return ResourceList.find({_id: id});
}
};
query.children.push(childObj)
})
return query;
});
So what should happen here (I didn't test) is with one publish function you will be getting the Curriculum you want, plus all of it's resourceslist children.
Now you will have access to these on the client side.
$scope.curriculum = $meteor.object(CurriculumList, $stateParams.curriculumId);
// collection if more than one, object if only one.
$scope.resources = $meteor.collection(ResoursesList, false);
This was thrown together somewhat quickly so I apologize if it doesn't work straight off, any trouble I'll help you fix.

Looking for selectAll-like operation that will include calling object in the selection (whenever it matches the selector)

The d3.js expression
d3.select(foo).selectAll(some_selector)
will return a selection comprising all the strict descendants of foo that satisfy some_selector.
But suppose that foo itself satisfies some_selector. How can I get it included in the resulting selection when this is the case?
The following naive solution to this problem
d3.select(foo.parentNode).selectAll(some_selector)
is incorrect, because, in general, the selection resulting from it will include any siblings of foo that satisfy some_selector!
IOW, I'm looking for a solution that is clearer, more concise, and less of a dirty hack than this (for example):
// temporarily tag all candidate elements, namely, foo and all its descendants
d3.select(foo).classed('yuk', true)
.selectAll('*').classed('yuk', true);
var parent = d3.select(foo.parentNode),
wanted = parent.selectAll(some_selector)
.filter('.yuk');
// undo the tagging
parent.selectAll('.yuk').classed('yuk', false);
Your question addresses the same issue as the other one you posted yesterday, although not being an exact duplicate. My answer to that one will work for this problem as well. I have adjusted my JSFiddle to allow for some filtering on the node and its descendants:
var selector = ".foo",
x = d3.select("#x"); // your parent node
// Get all children of node x regarding selector as NodeList and convert to Array.
var xAndDescendants = Array.prototype.slice.call(
x.node().querySelectorAll(selector)
);
// Add node x to the beginning if selector is true.
if (!(x = x.filter(selector)).empty())
xAndDescendants.unshift(x.node());
// Select resulting array via d3.js
var selection = d3.selectAll(xAndDescendants);
It's possible to avoid the class but its a bit complicated.
Here is one solution (I'm sure there are simpler ways!)..
function init() {
d3.select(foo)
.selectAll('*')
.call(function () {
immediateFamily(this, function (selection) {
return selection
.style('padding', '3px')
.style('border', '1px solid green')
})
})
}
;(function () {
$(document).ready(init)
})()
function immediateFamily(selection, styling) {
styling(selection)
styling(d3.select(selection[0].parentNode))
}
The idea is to avoid repeating the styling clauses by putting them in an anonymous function in the selection chain and pass this, along with the this context, to a function that applies said styling to the the selection and the parent node of the first group in the selection.
To make it slightly more terse - and even less comprehensible...
function immediateFamily2(selection, styling) {
styling(d3.select(styling(selection)[0].parentNode))
}
To take it to it's ultimate and possibly most idiomatically correct conclusion...
;(function () {
$(document).ready(init2)
function init2() {
var foo = d3.select('tr').filter(':first-child')[0][0]
d3.select(foo)
.selectAll('*')
.call(immediateFamily, bar)
function bar(selection) {
selection
.style('margin', '10px 20px 10px 20px')
.style('outline', '1px solid green')
}
}
function immediateFamily(selection, styling) {
styling(this)
styling(d3.select(this[0].parentNode))
return this
}
})()
Of course it could be further generalised but you get the idea.
(This code runs fine, but feel free to insert your own semicolons!)

Kendo multiselect, trigger an event when an item is removed

I use the multiselect from the Kendo UI.
I want to know if there is any way to trigger a function, when the user deletes an item from the multiselect.
So far I know that the 'change' event is triggered, but it is too generic and I can't find any info on what the user removed. Or is there?
What about define change as:
change : function (e) {
var previous = this._savedOld;
var current = this.value();
var diff = [];
if (previous) {
diff = $(previous).not(current).get();
}
this._savedOld = current.slice(0);
// diff has the elements removed do whatever you want...
}
What I do is save previous values on _savedOld and then compute the difference with current using jQuery.not. It's important to note the use of slice for cloning previous list of values, if we save current then we are actually saving a reference to current list and since it is a reference next time we try to use we get again current value.
EDIT: In order to save the values set during the initialization, you can do:
dataBound : function (e) {
saveCurrent(this);
},
change : function (e) {
var previous = this._savedOld;
var current = this.value();
var diff = $(previous).not(current).get();
saveCurrent(this);
// diff has the elements removed do whatever you want...
}
where saveCurrent is a function defined as:
function saveCurrent(multi) {
multi._savedOld = multi.value().slice(0);
}

Knockout validation issues

I have the following issues with my knockout model validations and not sure how to resolve them. Following is my model first of all, with the validation rules:
var Data = function (data) {
this.Val = data;
}
function ViewModel(item) {
var parse = JSON.parse(item.d);
var self = this;
this.Name = ko.observable(parse.Name);
this.UserType = ko.observable(parse.UserType);
this.ID = ko.observable(parse.ID).extend({ required: { params: true, message: "ID is required" }, decimal: { params: 2, message: "Should be decimal"} });
this.Username = ko.observable(parsed.Username).extend({ required: {
onlyIf: function () {
return self.UserType() > 1;
}
}
});
this.WeeklyData = ko.observableArray([]);
var records = $.map(parse.WeeklyData, function (data) { return new Data(data) });
this.WeeklyData(records);
this.WeeklyData2 = ko.observableArray([]);
var records = $.map(parse.WeeklyData2, function (data) { return new Data(data) });
this.WeeklyData2(records);
}
ko.extenders.numeric = function (target, precision) {
var result = ko.dependentObservable({
read: function () {
return target().toFixed(precision);
},
write: target
});
result.raw = target;
return result;
};
Here are my problems:
1) with the ID() observable, I want to restrict it to two decimal points, so I've created the validation extender 'numeric' but it's not working. Is there anything wrong with how I'm using it and how to correct it?
2) Also, if I want to restrict an observable to whole numbers, how can I do that?
3) when I define a rule with a condition, (i.e. Username()), how do I define a custom message for that? I was able to do it for default rules, but with the conditional rules, it's not working
4) I have two observable arrays WeeklyData1 and WeeklyData2 both of which contains Data() objects. I want to have separate min/max rules for these two, for example, min/max - 1,7 for WeeklyData1 and min/max - 1,150 for WeeklyData2. How can I get it done?
4) Right now my error messages appear right next to the data field, but I want all those to appear in a single validation summary, while displaying '*' against the field. I've been told to use Validation-bindings, but I'm not sure how to use it, can someone please give an example?
It's a lot of questions, I know, but I appreciate if someone could help.
Thanks in advance
Instead of diving in your code i have created a small-small demonstrations for your questions. Ok so here we go,
1) with the ID() observable, I want to restrict it to two decimal points.... and 2) Also, if I want to restrict an observable to whole numbers....
Your 1 and 2 question are pretty similar so i covered both of this in a single fiddle. Check this fiddle.
3) when I define a rule with a condition, (i.e. Username()), how do I define a custom message ....
You can use message property to set custom messages, Check this fiddle.
4) I have two observable arrays WeeklyData1 and WeeklyData2 both of which contains Data() objects
I am not clear which this question, what type of data both of these array contains and for what you want to set min/max rule ( array length or other ). So please clear this, than i will try to help on this.
5) Right now my error messages appear right next to the data field.....
This questions answer i already given in your how to? with knockout js validations question (Check update).
Let me know if it helps!

Resources