backbone drag and drop - getting dropped data - javascript

I'm relatively new to programming and this is my first time posting on here, so sorry in advance if this isn't presented correctly.
I'm building a health tracker app with Backbone JS. I am retrieving data from the Nutritionix API and populating a view on the left side of the screen with that data. I want the user to be able to drag an item from the populated view and drop it in a view to the right in which case a calorie counter will increase. I also need that data to persist so that when the user closes and reopens the app their selected data in the view will remain the same.
I've implemented the drag and drop feature just fine. I am now trying to make it so that when the user drops an item from the left view into the right view a collection is created that only contains the data in the right view. I believe that I need to do this so I can persist the data. Right now, I am having trouble transferring the API data along into the right view. Here are the relevant code snippets:
appView:
var app = app || {};
var ENTER_KEY = 13;
app.AppView = Backbone.View.extend({
el: '#health-app',
urlRoot: 'https://api.nutritionix.com/v1_1/search/',
events: {
'keypress': 'search',
'click #clearBtn': 'clearFoodList',
'drop .drop-area': 'addSelectedFood',
// 'drop #selected-food-template': 'addSelectedFood'
},
initialize: function() {
this.listenTo(app.ResultCollection, 'add', this.addFoodResult);
// this.listenTo(app.SelectedCollection, 'add', this.addSelectedFood);
app.ResultCollection.fetch();
app.SelectedCollection.fetch();
this.clearFoodList()
},
addFoodResult: function(resultFood) {
var foodResult = new SearchResultView({
model: resultFood
});
$('#list').append(foodResult.render().el);
},
// addSelectedFood: function(selectedFood) {
// // var selectedFoodCollection = app.SelectedCollection.add(selectedFood)
// console.log(app.SelectedCollection.add(selectedFood))
// },
clearFoodList: function() {
_.invoke(app.ResultCollection.toArray(), 'destroy');
$('#list').empty();
return false;
},
search: function(e) {
var food;
//When the user searches, clear the list
if($('#search').val() === '') {
_.invoke(app.ResultCollection.toArray(), 'destroy');
$('#list').empty();
}
//Get the nutrition information, and add to app.FoodModel
if (e.which === ENTER_KEY) {
this.query = $('#search').val() + '?fields=item_name%2Citem_id%2Cbrand_name%2Cnf_calories%2Cnf_total_fat&appId=be7425dc&appKey=c7abd4497e5d3c8a1358fb6da9ec1afe';
this.newUrl = this.urlRoot + this.query;
var getFood = $.get(this.newUrl, function(data) {
var hits = data.hits;
var name, brand_name, calories, id;
_.each(hits, function(hit){
name = hit.fields.item_name;
brand_name = hit.fields.brand_name;
calories = hit.fields.nf_calories;
id = hit.fields.item_id;
food = new app.FoodModel({
name: name,
brand_name: brand_name,
calories: calories,
id: id
});
//If the food isn't in the ResultCollection, add it.
if (!app.ResultCollection.contains(food)) {
app.ResultCollection.add(food);
}
});
food.save();
});
}
}
});
breakfastView:
var app = app || {};
app.BreakfastView = Backbone.View.extend({
el: '#breakfast',
attributes: {
'ondrop': 'ev.dataTransfer.getData("text")',
'ondragover': 'allowDrop(event)'
},
events: {
'dragenter': 'dragEnter',
'dragover': 'dragOver',
'drop': 'dropped'
},
initialize: function() {
this.listenTo(app.SelectedCollection, 'change', this.addSelectedFood);
},
render: function() {},
addSelectedFood: function(selectedFood) {
// var selectedFoodCollection = app.SelectedCollection.add(selectedFood)
console.log(app.SelectedCollection.add(selectedFood))
},
dragEnter: function (e) {
e.preventDefault();
},
dragOver: function(e) {
e.preventDefault();
},
dropped: function(ev) {
var data = ev.originalEvent.dataTransfer.getData("text/plain");
ev.target.appendChild(document.getElementById(data));
// console.log(app.SelectedCollection)
this.addSelectedFood(data);
},
});
new app.BreakfastView
SelectedCollection:
var app = app || {};
app.SelectedCollection = Backbone.Collection.extend({
model: app.FoodModel,
localStorage: new Backbone.LocalStorage('selected-food'),
})
app.SelectedCollection = new app.SelectedCollection();
Here's my repo also, just in case: https://github.com/jawaka72/health-tracker-app/tree/master/js
Thank you very much for any help!

Related

How can i open a form always in Edit mode , for a particular model

I have a model say 'my.attendance' , also have a form view for this which contains some attendance details.What i need is when i open this form view it should always open in Edit mode.So i can directly enter the attendance without clicking Edit button each time.
You have to extend the ViewManager to achieve this.
odoo.define('my_module.view_manager', function (require) {
"use strict";
var ViewManager = require('web.ViewManager');
ViewManager.include({
custom_events: {
execute_action: function(event) {
var data = event.data;
this.do_execute_action(data.action_data, data.env, data.on_closed)
.then(data.on_success, data.on_fail);
},
search: function(event) {
var d = event.data;
_.extend(this.env, this._process_search_data(d.domains, d.contexts, d.groupbys));
this.active_view.controller.reload(_.extend({offset: 0}, this.env));
},
switch_view: function(event) {
if ('res_id' in event.data) {
this.env.currentId = event.data.res_id;
}
var options = {};
console.log(event.data)
if (event.data.view_type === 'form' && !this.env.currentId) {
options.mode = 'edit';
} else if (event.data.mode) {
options.mode = event.data.mode;
}
// Extra added code
if (event.data.model){
if (event.data.model == 'my.model'){ // Checking the particular model.
options.mode = 'edit';
}
}
this.switch_mode(event.data.view_type, options);
},
env_updated: function(event) {
_.extend(this.env, event.data);
},
push_state: function(event) {
this.do_push_state(event.data);
},
get_controller_context: '_onGetControllerContext',
switch_to_previous_view: '_onSwitchToPreviousView',
},
});
});

Updating a view from collection

I am having a little trouble rendering my player.
Im trying to create a structure using backbonejs that will
Add a new player view when a new player joins
Updates a player score
In updatePlayerData I update the player score if the player model has already been created. If not I create a new one.
The problem is in playerScore. I am creating a span everytime render is called. This is causing many player classes to be creating under the one player view
I need to be able to loop through my current views and update the player of a certain id. I need to then re render that player view so the new content is displayed.
For reference here is my code so far:
var Player = Backbone.Model.extend({});
var PlayerView = Backbone.View.extend({
className: 'player',
initialize: function () {
_.bindAll(this, 'render');
},
render: function () {
this.playerScore();
return this;
},
playerScore: function(){
$('<span/>', {
class: 'playerScore'
}).appendTo($(this.el)).html(this.model.get('score'));
}
});
var Team = Backbone.Collection.extend({
model: Player
});
var TeamView = Backbone.View.extend({
el: '#users',
initialize: function () {
_.bindAll(this, 'updatePlayerData', 'addPlayer');
this.collection.bind('add', this.addPlayer);
this._subviews = [];
},
updatePlayerData: function (playerEntity) {
var view = _(this._subviews).find(function(v){
return playerEntity.id == v.model.get('id');
});
if(view){
view.model.set({
score: playerEntity.score
});
view.render();
return;
}
var playerModel = new Player();
playerModel.set({
score: playerEntity.score
});
this.collection.add(playerModel);
},
addPlayer: function (player) {
var playerView = new PlayerView({
model: player
});
var playerHtml = playerView.render().el;
$(this.el).html(playerHtml);
this._subviews.push(playerView);
}
});
Try modifying your playerScore method to something like:
playerScore: function () {
if (!$(this.el).find('span.playerScore').length) {
$('<span/>', {
class: 'playerScore'
}).appendTo($(this.el)).html(this.model.get('score'));
} else {
$(this.el).find('span.playerScore').html(this.model.get('score'));
}
}

Reusing a modal template

On my current project, there are starting to be a few views that are modal views that are being used to delete items on the site. They are currently generic in that it's just a text description of the item they are deleting. Maybe in the future there will be an icon or a short description as well. There are now tasks to have that functionality to delete other stuff on our site. I'm new to the web, MVC, asp.net, etc, and what I want to know is if it's better to reuse our current modal view somehow, and pass in the objects we need to show in the view. Because the view needs to send the url back to the server on which items to delete, that part of code would need to be different for the view as well. Here is some of the stuff in our view along with a .cshtml template that's pretty generic that I didn't include.
Views.DeleteGiftModal = (function () {
return Backbone.View.extend({
template: Templates["template-gift-delete-modal"],
tagName: 'div',
initialize: function (options) {
$(window).bind("disposeModal", _.bind(this.disposeModal, this));
_.bindAll(this, "showDialog", "disposeModal", "displayResults");
this.eventAggregator = options.eventAggregator;
this.itemsToDelete = options.model;
this.errors = {};
this.render();
return this;
},
events: {
"click #delete-btn": "deleteItems",
"click #ok-btn": "disposeModal",
"click #cancel-btn": "disposeModal"
},
disposeModal: function (event, refresh) {
this.$el.modal("hide");
if (event != null && event.currentTarget != null && event.currentTarget.id == 'ok-btn')
refresh = true;
this.trigger("modalClosed", refresh);
this.remove();
this.unbind();
},
showDialog: function () {
this.$el.modal("show");
},
deleteItems: function () {
var self = this;
var element = this.$el;
var numberGifts = this.getKeys(this.itemsToDelete).length;
this.results = [];
var hasError = false;
element.find("#actions").hide();
element.find("#ok-actions").show();
$.each(this.itemsToDelete, function(i, v) {
// tell model to go away
var gift = new Gift({ id: i });
gift.destroy({
success: function (model, response) {
self.results.push({ id: model.id, response: response });
numberGifts--;
if (numberGifts <= 0) {
if (!hasError) {
self.disposeModal(null, true);
} else {
self.displayResults();
}
}
}
});
});
},
displayResults: function () {
var element = this.$el;
$.each(this.results, function(i, v) {
// to do check response for error message
var list = element.find("#delete-item-" + v.id);
if (v.response.message == "Deleted") {
list.append(" - <span align='right' style='color: green'>Deleted</span>");
} else {
hasError = true;
list.append(" - <span align='right' style='color: red'>" + v.response.message + "</span>");
}
});
},
render: function () {
this.$el.append(this.template);
this.$el.find("#ok-actions").hide();
// show list of item names
var list = this.$el.find("#items-to-delete-list");
$.each(this.itemsToDelete, function (i, v) {
$("<li id='delete-item-" + i + "'>" + v.name + "</li>").appendTo(list);
});
this.$el.attr('id', 'delete-gift-dialog');
return this;
}
});
})();
As I am looking through the code, and this being my first real project, it seems like a lot of things that could be quite similar, like deleting a Gift, deleting a Toy, etc have different Controllers for each (GiftController, ToyController), and hit different URLs. So currently things are all in their own class like that. I was wondering if that's the more standard way to approach these types of problems as well with views. Thanks in advance!
The app we're developing at work had a similar issue. We're using Backbone too so I can completely relate to this. What I ended up doing is have a sort of ModalBuilder that builds a form in a modal for you and binds events on the form elements for submit. The initialization of it could look like this:
new ModalBuilder({
form: [
{
tag: 'select[name="id"]',
options: [
{ name: 'Item 1', id: 12 },
{ name: 'Item 2', id: 32 }
]
},
{
tag: 'input[type="submit"]',
value: 'Delete'
}
],
events: function(){
$('input[type="submit"]').on('click', function(){
// Delete via ajax
})
}
})
What we do is we have different templates for every form element, inputfields and textareas and so on and we reuse it all over the place. ModalBuilder takes these arguments and builds a form
Also for certain cases it might be better to render the form server-side and deliver it to your modal via ajax. You have to weigh what makes your app more performant I suppose.

Zombie views and events in Backbone marionette layout... Have I got them?

I think I might have a problem with zombie views in my Backbone Marionette app.
How can I check for unclosed views and memory leaks? I'm using the illuminations-for-developers.com add-on for Firefox and as I move around my application I see over 1000 views piling up in the 'widgets' illuminations tab - and when I inspect the HTML for them the majority are not in the DOM. Are these zombied views?
Have added the code I'm using below to get peoples opinion on if I'm attacking the problem the right way.
I'm trying to build a UI similar to the Facebook multiple friend selector dialog (see pic).
I have a layout with two collection views, one populated with a list of users, and an empty one in which the selected users are added to.
I want to use this layout in multiple areas of my app. So I have built a controller object that handles initializing it and loading the data for the collections, and then I initialize it and show it in another region whenever it is required.
Would appreciate tips on how to go about this, thanks.
Codez:
MyApp.UserFilterController
MyApp.UserFilterController = (function(){
var UserFilterController = {};
var selectedUsersCol;
var userFilterColView;
var selectedUsersColView;
var usersCol;
UserFilterController.initialize = function ( callback, excludeUsers ) {
// make a query...
// exclude the users...
var usersQ = new Parse.Query(Parse.User);
// just users with email addresses
usersQ.exists('email');
usersQ.exists('name');
usersQ.limit(1000);
usersQ.ascending('name');
usersQ.notContainedIn('objectId',excludeUsers);
usersCol = usersQ.collection();
// tell it where to render... append to the body give it an element?
userFilterColView = new MyApp.UserFilterUserCollectionView({
collection:usersCol
});
usersCol.fetch({
success:function (col) {
console.log("users collection fetched", col.length);
},
error:function () {
console.log("error fetching users collection");
}
});
$('#subpage-header').text("Users Selection");
// empty collection to hold the selected users
selectedUsersCol = new MyApp.Users();
// view to show the selected users
selectedUsersColView = new MyApp.SelectedUserCollectionView({
collection:selectedUsersCol
});
_.extend(selectedUsersCol, newBackboneAddMethod());
MyApp.userFilterLayout = new MyApp.UserFilterLayout();
MyApp.slideUp.content.show(MyApp.userFilterLayout);
MyApp.userFilterLayout.selectedusers.show(selectedUsersColView);
MyApp.userFilterLayout.allusers.show(userFilterColView);
//When user clicks on user in all users then its added to selected users
userFilterColView.on("itemview:clicked", function(childView, model){
console.log(model);
selectedUsersCol.add(model);
});
userFilterColView.on("collection:rendered", function(childView, model){
console.log('its rendered');
});
//When user clicks on selected user then it is removed from the collection
selectedUsersColView.on("itemview:clicked", function(childView, model){
console.log(model);
console.log(model.id);
selectedUsersCol.remove(model);
});
MyApp.App.vent.bind("slideUp:send",function(){
console.log("send button has been clicked. attempting call back")
callback(selectedUsersCol);
});
//unbinds the trigger above when view is being closed
userFilterColView.on('collection:before:close', function (){
MyApp.App.vent.unbind("slideUp:send");
console.log('colView before:close')
});
};
UserFilterController.removeUser = function ( user ) {
//console.log("you asked to remove", usersArray.length, 'users');
selectedUsersCol.remove(user);
usersCol.remove(user);
};
UserFilterController.generateListview = function ( user ) {
userFilterColView.$el.listview();
};
UserFilterController.resetSelected = function (user) {
selectedUsersCol.reset();
};
UserFilterController.cleanup = function () {
console.log("its closing");
//selectedUsersColView.unbindAll();
// selectedUsersColView.close();
userFilterColView.close();
// userFilterLayout.unbindAll();
// MyApp.userFilterLayout.close();
// MyApp.slideUp.content.close();
// MyApp.slideUp.close();
};
return UserFilterController;
}());
MyApp.EventDisplayLayout
MyApp.EventDisplayLayout = Backbone.Marionette.Layout.extend({
template: '#event-display-layout',
id: "EventDisplayLayout",
events: {
'click #invite': 'showUserFilter'
},
// User clicked on 'invite' button
showUserFilter: function () {
$.mobile.changePage($('#subpage'), {changeHash: false,transition: 'slideup'});
MyApp.UserFilterController.generateListview();
}
}
MyApp.showEventDisplay
MyApp.showEventDisplay = function (event) {
var eventDisplayLayout = new MyApp.EventDisplayLayout({});
MyApp.App.mainRegion.show(eventDisplayLayout);
var Invitees = event.get("invitees");
var excludeIds = [];
_.each(Invitees,function(invitee){
excludeIds.push(invitee.id);
});
MyApp.UserFilterController.initialize(function (selectUsersCol){
console.log("In call back: ",selectUsersCol);
},excludeIds);
};
MyApp.SlideUpPageLayout
// The generic layout used for modal panel sliding up from bottom of page.
MyApp.SlideUpPageLayout = Backbone.Marionette.Layout.extend({
el: '#subpage',
//template: '#homepage-temp',
regions: {
header: '.header',
content: '.content'
},
events:{
'click .send':'slideUpSend',
'click .cancel':'slideUpCancel'
},
onShow: function () {
console.log("SlideUpPage onShow");
this.$el.trigger('create');
},
initialize: function () {
// make user collection...
},
slideUpSend: function () {
console.log("send button has been pressed");
MyApp.App.vent.trigger('slideUp:send');
$.mobile.changePage($('.type-home'),{transition: 'slideup',reverse:true});
},
slideUpCancel: function () {
// MyApp.App.vent.trigger('slideUp:cancel');
$.mobile.changePage($('.type-home'),{transition: 'slideup',reverse:true});
}
});
MyApp.UserFilterLayout
// The layout used for the user filter panel sliding up from bottom of page.
MyApp.UserFilterLayout = Backbone.Marionette.Layout.extend({
template: '#userfilterlayout',
//template: '#homepage-temp',
regions: {
selectedusers: '.selectedusers',
allusers: '.allusers'
},
onShow: function () {
console.log("userfilterlayout onShow");
this.$el.trigger('create');
}
});

ComboBox that shows a tree in the dropdown

I want to write a ComboBox which lets the user type in a query and at the same time lets him select a value from a tree. I have tried writing a tree-select but if I change the code to inherit from dijit.form.ComboBox instead of a dijit.form.Select, the code breaks.
Here is what I had tree for tree select:
dojo.declare('TreeSelect',dijit.form.Select,{
constructor: function(widgetArgs){
this.tree = widgetArgs.tree || new FC_Tree();
this.initTree = widgetArgs.initTree;
if(dojo.isFunction(this.initTree))
this.initTree();
},
postCreate: function(){
this.inherited(arguments);
this.option = {label: '', value: 'NoValue'};
this.tree.option = this.option;
this.addOption(this.option);
dojo.connect(this.tree,'onClick',this,'closeDropDown');
dojo.connect(this.tree,'itemSelected',this,'selectOption');
},
selectOption: function(opt){
this.option.label = opt.label || opt;
this.option.value = opt.value || opt;
this.option.id = opt.id || opt;
this.set('value',this.option);
},
_getMenuItemForOption: function (option){
return this.tree;
},
openDropDown: function(){
this.tree.refresh();
this.inherited(arguments);
},
clear: function(){
this.tree.clear();
this.tree.option.value = '';
this.tree.option.label = '';
this.tree.option.id = '';
this.set('value',this.tree.option);
},
initializeTree: function(treeData) {
// Init the tree only if needed
dojo.forEach(treeData, function(field) {
var store = this.tree.model.store;
store.newItem(field);
}, this);
},
setOpenCallback: function(callback){
this.tree.setOpenCallback(callback);
},
resetTree: function() {
var store = this.tree.model.store;
store.fetch( { query: { id: "*" },
onItem: function(item) {
store.deleteItem(item);
}
});
}
});
I had tried replacing the code for combobox like this:
dojo.declare('TreeSelect',dijit.form.ComboBox,{
Please help me rectify it.
Thanks in advance!
Adding the code for FC_Tree:
dojo.declare('FC_Tree',dijit.Tree,{
showRoot: false,
openOnClick: true,
noIconForNode: true,
noMarginForNode: true,
persist: false,
openCallback: null,
constructor: function(){
if(dojo.isUndefined(arguments[0]) || dojo.isUndefined(arguments[0].model))
{
var forest_store = new FC_DataStore({id: 'id', label: 'label'});
this._storeloaded = false;
dojo.connect(forest_store,'loaded',this,function(){this._storeloaded = true;})
this.model = new dijit.tree.ForestStoreModel({store:forest_store});
}
},
setOpenCallback: function(callback){
this.openCallback = callback;
},
option: {},
itemSelected: function(item){
},
onClick: function(item, node, evt){
var store = this.model.store;
get = function(){
return store.getValue(item, "isDir");
};
// on folder click mark it unselectable
if(get("isDir"))
{
this.isExpanded = true;
this.isExpandable = true;
}
else
{ //In case the item has 'onClick' delegate execute it and assign the output to 'selItem'
var selItem = (item.onClick && item.onClick[0])? item.onClick[0](this.model.store,item.parentID[0]):item.id[0];
this.option.id = item.id;
this.option.value = item.value;
this.option.label = item.label;
this.itemSelected(this.option);
}
},
onOpen: function(item, node){
if(this.rootNode.item == item){
return this.inherited(arguments);
}
var data = (this.openCallback != null) ? this.openCallback(item, node) : {};
if(!data.length){
return this.inherited(arguments);
}
FC_Comm.when(data,{
onCmdSuccess: dojo.hitch(this,function(data){
var store = this.model.store;
var children = store.getValues(item, 'children');
dojo.forEach(children, function(child) {
// don't delete child if doNotDelete flag is true
if(!store.getValue(child, "doNotDelete"))
store.deleteItem(child);
});
if (data) {
var store = this.model.store;
if (store) {
dojo.forEach(data, function(child) {
store.newItem(child, {parent : item, attribute: 'children'});
});
}
}
})
});
},
refresh: function(){
if(this._storeloaded){
// Close the store (So that the store will do a new fetch()).
this.model.store.clearOnClose = true;
this.model.store.close();
// Completely delete every node from the dijit.Tree
this._itemNodesMap = {};
this.rootNode.state = "UNCHECKED";
this.model.root.children = null;
// Destroy the widget
this.rootNode.destroyRecursive();
// Recreate the model, (with the model again)
this.model.constructor(this.model)
// Rebuild the tree
this.postMixInProperties();
this._load();
this._storeloaded = false;
}
},
clear: function(){
this.model.store.load([]);
}
});
Possibly you will find inspiration / answer in this fiddle
The recursiveHunt and selectTreeNodeById functions are logic to seek out the path of an item by its id. Its quite excessive and you may find a better solution (dont know 100% what data your json is like)..
Basically, use FilteringSelect and reference the tree in this object. Also for Tree, reference the select.
Then for your tree, hook into load function (also called on refresh afaik) and in turn for the select, use onBlur to initate selecting treenode.
var combo = new dijit.form.FilteringSelect({
onBlur: function() {
// called when filter-select is 'left'
if (this.validate()) {
// only act if the value holds an actual item reference
var id = this.get("value");
var name = this.get("displayedValue");
this.tree.selectNode(id);
}
}
});
var tree = new dijit.Tree( { ....
onLoad: function() {
combostore.setData(this.model.store._arrayOfAllItems);
},
onClick: function(item) {
// uses 'this.combo', must be present
// also, we must have the same 'base store' for both combo and model
var _name = this.model.store.getValue(item, this.combo.searchAttr);
this.combo.set("item", item, false, _name);
},
selectNode: function(lookfor) {
selectTreeNodeById(this, lookfor);
},
combo: combo // <<<<<<
});
combo.tree = tree // <<<<<<
Make sure that the model has a rootId, and also that your select.searchAttr matches tree.model.labelAttr. See working sample on the fiddle

Categories