Update array order in Vue.js after DOM change - javascript

I have a basic Vue.js object:
var playlist = new Vue({
el : '#playlist',
data : {
entries : [
{ title : 'Oh No' },
{ title : 'Let it Out' },
{ title : 'That\'s Right' },
{ title : 'Jump on Stage' },
{ title : 'This is the Remix' }
]
}
});
HTML:
<div id="playlist">
<div v-for="entry in entries">
{{ entry.title }}
</div>
</div>
I also am using a drag and drop library (dragula) to allow users to rearrange the #playlist div.
However, after a user rearranges the playlist using dragula, this change is not reflected in Vue's playlist.entries, only in the DOM.
I have hooked into dragula events to determine the starting index and ending index of the moved element. What is the correct way to go about updating the Vue object to reflect the new order?
Fiddle: https://jsfiddle.net/cxx77kco/5/

Vue's v-for does not track modifications to the DOM elements it creates. So, you need to update the model when dragula notifies you of the change. Here's a working fiddle: https://jsfiddle.net/hsnvweov/
var playlist = new Vue({
el : '#playlist',
data : {
entries : [
{ title : 'Oh No' },
{ title : 'Let it Out' },
{ title : 'That\'s Right' },
{ title : 'Jump on Stage' },
{ title : 'This is the Remix' }
]
},
ready: function() {
var self = this;
var from = null;
var drake = dragula([document.querySelector('#playlist')]);
drake.on('drag', function(element, source) {
var index = [].indexOf.call(element.parentNode.children, element);
console.log('drag from', index, element, source);
from = index;
})
drake.on('drop', function(element, target, source, sibling) {
var index = [].indexOf.call(element.parentNode.children, element)
console.log('drop to', index, element, target, source, sibling);
self.entries.splice(index, 0, self.entries.splice(from, 1)[0]);
console.log('Vue thinks order is:', playlist.entries.map(e => e.title ).join(', ')
);
})
}
});

I created a Vue directive that does exactly this job.
It works exactly as v-for directive and add drag-and-drop capability in sync with underlying viewmodel array:
Syntaxe:
<div v-dragable-for="element in list">{{element.name}}</div>
Example: fiddle1, fiddle2
Github repository: Vue.Dragable.For

Related

Backbone rendering collection in other collection element

I have a structure that I need to render. It has linear type:
{districts : [
{
name : "D17043"
},
{
name : "D91832"
},
{
name : "D32435"
}
],
buildings : [
{
name : "B14745",
district: "D17043",
address: "1st str"
},
{
name : "B14746",
district : "D91832",
address : "1st str"
},
{
name : "B14747",
district : "D17043",
address : "1st str"
}
]
}
It is simplified to look a bit better. Districts have buildings attached to them. Possibly, the structure could be nested, but I thought that it would make additional troubles, and, as this "tree" is not structured I made simple object.
The question is - how it can be properly displayed with backbone?
Right now it looks something like this (some details are left behind the scenes):
App.Collections.Districts = Backbone.Collection.extend({
model : App.Models.District
});
var districtsCollection = new App.Collections.Districts(data.districts);
var districtsView = new App.Views.Districts({collection : districtsCollection});
then somewhere in district collection view we create a loop that make views of a single district:
var districtView = new App.Views.District({model : district});
and inside view of a single district i call another collection in render method:
App.Views.District = Backbone.View.extend({
tagName : 'div',
className : 'districtListElement',
render : function () {
this.$el.html(this.model.get('name'));
var buildingsCollection = new App.Collections.Buildings(allData.buildings);
buildingsCollection.each(function(item){
if (item.get('district') == this.model.get('name')) {
var buildingView = new App.Views.Building({model : item});
this.$el.append(buildingView.render().el);
}
}, this);
return this;
}
});
This thing works but isn't it a bit creepy? Maybe there is a proper way to do it? Especially if I have other instances to be inside buildings.
Thank you.

Toggle Tree List Branches in Backone and Marionette

I have a nested Tree list using Backone and Marionette. I would like to toggle the view of each Branch that has a leaf by clicking on the branch li.
There is a bug when I click on the second level nodes in the tree to expend them. Clicking the Car or Truck node ends up closing the branch instead of opening the next level. I am not sure how to fix this bug.
Here is a fiddle to my code: http://jsfiddle.net/aeao3Lec/
Here is my JavaScript, Data, and Templates:
JavaScript:
var TheModel = Backbone.Model.extend({});
var TheCollection = Backbone.Collection.extend({
model: TheModel,
});
var App = new Backbone.Marionette.Application();
App.addRegions({
mainRegion: '.main-region'
});
var TreeItemView = Backbone.Marionette.CompositeView.extend({
initialize: function() {
if ( this.model.get('children') ) {
this.collection = new TheCollection( this.model.get('children') );
}
},
tagName: 'ul',
className: 'tree-list',
template: _.template( $('#tree-template').html() ),
serializeData: function () {
return {
item: this.model.toJSON()
};
},
attachHtml: function(collectionView, childView) {
collectionView.$('li:first').append(childView.el);
},
events: {
'click .js-node': 'toggle'
},
toggle: function(e) {
var $e = $(e.currentTarget);
$e.find(' > .tree-list').slideToggle();
}
});
var TreeRootView = Backbone.Marionette.CollectionView.extend({
tagName: 'div',
className: 'tree-root',
childView: TreeItemView
});
var theCollection = new TheCollection(obj_data);
App.getRegion('mainRegion').show( new TreeRootView({collection: theCollection}) );
Templates:
<div class="main-region">
</div>
<script type="text/template" id="tree-template">
<li class="js-node">
<% if (item.children) { %>
Click to toggle -
<% } %>
<%- item.title %>
</li>
</script>
Data:
var obj_data = {
"title": "Ford",
"children": [
{
"title": "Car",
"children": [
{
"title": "Focus",
},
{
"title": "Taurus"
}
]
},
{
"title": "Truck",
"children": [
{
"title": "F-150"
}
]
}
]
};
The issue is that your view has several nested elements with the .js-node class. When you click the parent one, you display the children .js-node elements, but when you click one of those, the event bubbles up and re-triggers the event on the parent .js-node, which closes the children that you just clicked.
You can stop this event bubbling by calling
e.stopImmediatePropagation();
I've updated your toggle method like so and it works:
toggle: function(e) {
var $e = $(e.currentTarget);
$e.children('.tree-list').slideToggle();
e.stopImmediatePropagation();
}
http://jsfiddle.net/CoryDanielson/aeao3Lec/2/
The larger issue that I see is that your data is not really a collection... it's a tree. The CollectionView is really used to render a flat array of models, not a nested one. You should be rendering this data with multiple CollectionViews nested inside of each other... this will start to cause problems as your TreeItemView grows in complexity.
Edit: Nope, you're using a composite view which works perfectly for rendering trees.

Set default focus on first item in grid list

Once a grid is rendered how can I set focus to the first item. I am running into a problem where when the grid is updated (collection changes) then focus is lost for the entire application .
I am using the moonstone library.
{
kind: "ameba.DataGridList", name: "gridList", fit: true, spacing: 20, minWidth: 300, minHeight: 270, spotlight : 'container',
scrollerOptions:
{
kind: "moon.Scroller", vertical:"scroll", horizontal: "hidden", spotlightPagingControls: true
},
components: [
{kind : "custom.GridItemControl", spotlight: true}
]
}
hightlight() is a private method for Spotlight which only adds the spotlight class but does not do the rest of the Spotlight plumbing. spot() is the method you should be using which calls highlight() internally.
#ruben-ray-vreeken is correct that DataList defers rendering. The easiest way to spot the first control is to set initialFocusIndex provided by moon.DataListSpotlightSupport.
http://jsfiddle.net/dcw7rr7r/1/
enyo.kind({
name: 'ex.App',
classes: 'moon',
bindings: [
{from: ".collection", to: ".$.gridList.collection"}
],
components: [
{name: 'gridList', kind:"moon.DataGridList", classes: 'enyo-fit', initialFocusIndex: 0, components: [
{kind:"moon.CheckboxItem", bindings: [
{from:".model.text", to:".content"},
{from:".model.selected", to: ".checked", oneWay: false}
]}
]}
],
create: enyo.inherit(function (sup) {
return function () {
sup.apply(this, arguments);
// here, at least, the app starts in pointer mode so spotting the first control
// isn't apparent (though it would resume from that control upon 5-way action).
// Turning off pointer mode does the trick.
enyo.Spotlight.setPointerMode(false);
this.set("collection", new enyo.Collection(this.generateRecords()));
};
}),
generateRecords: function () {
var records = [],
idx = this.modelIndex || 0;
for (; records.length < 20; ++idx) {
var title = (idx % 8 === 0) ? " with long title" : "";
var subTitle = (idx % 8 === 0) ? "Lorem ipsum dolor sit amet" : "Subtitle";
records.push({
selected: false,
text: "Item " + idx + title,
subText: subTitle,
url: "http://placehold.it/300x300/" + Math.floor(Math.random()*0x1000000).toString(16) + "/ffffff&text=Image " + idx
});
}
// update our internal index so it will always generate unique values
this.modelIndex = idx;
return records;
},
});
new ex.App().renderInto(document.body);
Just guessing here as I don't use Moonstone, but the plain enyo.Datalist doesn't actually render its list content when the render method is called. Instead the actual rendering task is deferred and executed at a later point by the gridList's delegate.
You might want to dive into the code of the gridList's delegate and check out how it works. You could probably create a custom delegate (by extending the original) and highlight the first child in the reset and/or refresh methods:
enyo.Spotlight.highlight(this.$.gridList.childForIndex(0));
Ruben's answer adds on to my answer that I posted in the Enyo forums, which I'm including here for the sake of completeness:
Try using
enyo.Spotlight.highlight(this.$.gridList.childForIndex(0));
I added a rendered() function to the Moonstone DataGridList sample in the Sampler and I found that this works:
rendered: function() {
this.inherited(arguments);
this.startJob("waitforit", function() {
enyo.Spotlight.highlight(this.$.gridList.childForIndex(0));
}, 400);
},
It didn't work without the delay.

kendo ui - create a binding within another binding?

In my pursuit to get a binding for an associative array to work, I've made significant progress, but am still blocked by one particular problem.
I do not understand how to create a binding from strictly javascript
Here is a jsFiddle that shows more details than I have posted here:
jsFiddle
Basically, I want to do a new binding within the shown $.each function that would be equivalent to this...
<div data-template="display-associative-many" data-bind="repeat: Root.Items"></div>
Gets turned into this ...
<div data-template="display-associative-single" data-bind="source: Root['Items']['One']"></div>
<div data-template="display-associative-single" data-bind="source: Root['Items']['Two']"></div>
<div data-template="display-associative-single" data-bind="source: Root['Items']['Three']"></div>
And I am using the repeat binding to create that.
Since I cannot bind to an associative array, I just want to use a binding to write all of the bindings to the objects in it.
We start again with an associative array.
var input = {
"One" : { Name: "One", Id: "id/one" },
"Two" : { Name: "Two", Id: "id/two" },
"Three" : { Name: "Three", Id: "id/three" }
};
Now, we create a viewModel that will contain that associative array.
var viewModel = kendo.observable({
Name: "View Model",
Root: {
Items: input
}
});
kendo.bind('#example', viewModel);
Alarmingly, finding the items to bind was pretty easy, here is my binding so far;
$(function(){
kendo.data.binders.repeat = kendo.data.Binder.extend({
init: function(element, bindings, options) {
// detailed more in the jsFiddle
$.each(source, function (idx, elem) {
if (elem instanceof kendo.data.ObservableObject) {
// !---- THIS IS WHERE I AM HAVING TROUBLE -----! //
// we want to get a kendo template
var template = {};// ...... this would be $('#individual-item')
var result = {}; // perhaps the result of a template?
// now I need to basically "bind" "elem", which is
// basically source[key], as if it were a normal HTML binding
$(element).append(result); // "result" should be a binding, basically
}
});
// detailed more in the jsFiddle
},
refresh: function() {
// detailed more in the jsFiddle
},
change: function() {
// detailed more in the jsFiddle
}
});
});
I realize that I could just write out the HTML, but that would not perform the actual "binding" for kendo to track it.
I'm not really sure what you are attempting to do, but it seemed to me that the custom "repeat" binding was unnecessary. Here's what I came up with. Is this on track with what you are trying to do?
Here is a working jsFiddle example.
HTML
<div id="example">
<div data-template="display-associative-many" data-bind="source: Root.Items"></div>
</div>
<script type="text/x-kendo-template" id="display-associative-many">
#for (var prop in data) {#
# if (data.hasOwnProperty(prop)) {#
# if (data[prop].Id) {#
<div><span>${data[prop].Id}</span> : <span>${data[prop].Name}</span></div>
# }#
# }#
#}#
</script>
JavaScript
$(function () {
var input = {
"One" : { Name: "One", Id: "id/one" },
"Two" : { Name: "Two", Id: "id/two" },
"Three" : { Name: "Three", Id: "id/three" }
};
var viewModel = new kendo.data.ObservableObject({
Id: "test/id",
Root: {
Items: input
}
});
kendo.bind('#example', viewModel);
});

Access collection on two views in backbone.js

Hi i have a collection and two views. On my view1 i'm adding data to my collection and view2 will just render and display any changes about the collection. But i can't get it to work. The problem is originally i'm doing this
return new CartCollection();
But they say its a bad practice so i remove changed it. But when i instantiate cart collection on view1 it would add but it seems view2 doesn't sees the changes and renders nothing.
Any ideas?
here is my cart collection.
define([
'underscore',
'backbone',
'model/cart'
], function(_, Backbone, CartModel) {
var CartCollection = Backbone.Collection.extend({
model : CartModel,
initialize: function(){
}
});
return CartCollection;
});
Here is my itemView ( view1 )
AddToCart:function(ev){
ev.preventDefault();
//get data-id of the current clicked item
var id = $(ev.currentTarget).data("id");
var item = this.collection.getByCid(id);
var isDupe = false;
//Check if CartCollection is empty then add
if( CartCollection.length === 0){
CartCollection.add([{ItemCode:item.get("ItemCode"),ItemDescription:item.get("ItemDescription"),SalesPriceRate:item.get("RetailPrice"),ExtPriceRate:item.get("RetailPrice"),WarehouseCode: "Main",ItemType : "Stock",LineNum:1 }]);
}else{
//if check if the item to be added is already added, if yes then update QuantityOrdered and ExtPriceRate
_.each(CartCollection.models,function(cart){
if(item.get("ItemCode") === cart.get("ItemCode")){
isDupe = true;
var updateQty = parseInt(cart.get("QuantityOrdered"))+1;
var extPrice = parseFloat(cart.get("SalesPriceRate") * updateQty).toFixed(2);
cart.set({ QuantityOrdered: updateQty });
cart.set({ ExtPriceRate: extPrice });
}
});
//if item to be added has no duplicates add new item
if( isDupe == false){
var cartCollection = CartCollection.at(CartCollection.length - 1);
var lineNum = parseInt( cartCollection.get("LineNum") ) + 1;
CartCollection.add([{ItemCode:item.get("ItemCode"),ItemDescription:item.get("ItemDescription"),SalesPriceRate:item.get("RetailPrice"),ExtPriceRate:item.get("RetailPrice"),WarehouseCode: "Main",ItemType : "Stock",LineNum:lineNum}]);
}
}
CartListView.render();
}
My cartview (view2)
render: function(){
this.$("#cartContainer").html(CartListTemplate);
var CartWrapper = kendobackboneModel(CartModel, {
ItemCode: { type: "string" },
ItemDescription: { type: "string" },
RetailPrice: { type: "string" },
Qty: { type: "string" },
});
var CartCollectionWrapper = kendobackboneCollection(CartWrapper);
this.$("#grid").kendoGrid({
editable: true,
toolbar: [{ name: "save", text: "Complete" }],
columns: [
{field: "ItemDescription", title: "ItemDescription"},
{field: "QuantityOrdered", title: "Qty",width:80},
{field: "SalesPriceRate", title: "UnitPrice"},
{field: "ExtPriceRate", title: "ExtPrice"}
],
dataSource: {
schema: {model: CartWrapper},
data: new CartCollectionWrapper(CartCollection),
}
});
},
The problem is you've created 2 different instances of CartCollection. So when you update or fetch data into one instance the other does not change but remains the same.
Instead you need to use the same instance of CartCollection across the 2 views (or alternatively keep the 2 insync) .Assuming both views are in the same require.js module you would need to:
1) Instantiate the CartCollection instance and store it somewhere that both views have access to. You could put this in the Router, the parent view, or anywhere else really.
e.g.
var router = Backbone.Router.extend({});
router.carts = new CartCollection();
2) You need need to pass the CartCollection instance to each of your views.
e.g.
var view1 = new ItemView({ collection: router.carts });
var view2 = new CartView({ collection: router.carts });
You may also want to just pass the Cart model to the CartView instead of the entire collection.
e.g.
var cartModel = router.carts.get(1);
var view2 = new CartView({ model: cartModel });

Categories

Resources