How would you achieve multiple drag and drop with ember - javascript

I've seen different examples of single object drag and drop like referenced in this question Ember.js + HTML5 drag and drop shopping cart demo
But since the drag event is on the view object, I don't se how I would achieve multiple view selection drag and drop (aka like in an email client or in evernote for instance).
Any jsbin is more than welcome.

This is an example of using drag and drop using ember along with jquery-ui . Although it is not necessary to split the draggable components into separate views, they have been split just to demonstrate the multiple view selection mentioned by the op.
So combine the following code with the example found in this thread
How do I drag multiple elements at once with JavaScript or jQuery?
(look at the comments http://jsfiddle.net/zVZFq/358/)
http://emberjs.jsbin.com/sasasuka/1/edit
hbs
<script type="text/x-handlebars">
<h2> Welcome to Ember.js</h2>
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="index">
<ul>
{{#each post in model}}
{{#with post}}
<div class="placeholder">
{{render "post" post}}
</div>
{{/with}}
{{/each}}
</ul>
</script>
<script type="text/x-handlebars" data-template-name="post">
<div class="post" {{bind-attr id="id"}}>
{{name}}
</div>
</script>
js
App = Ember.Application.create();
App.Router.map(function() {
// put your routes here
});
App.IndexRoute = Ember.Route.extend({
model: function() {
return allPosts;
}
});
App.IndexView = Ember.View.extend({
classNames:["post-container"]
});
App.PostController = Ember.ObjectController.extend({
});
App.PostView = Ember.View.extend({
templateName:"post",
classNameBindings: ['selected'],
selected:Ember.computed.alias("context.selected"),
didInsertElement:function(){
this.$(".post").draggable({ revert: "invalid", snap: ".post-container",snapMode:"inner" });
var self = this;
/*jquery ui create the draggable component*/
this.$(".post").draggable({ revert: "invalid", snap: ".post-container",snapMode:"inner" });
/*create the droppable component*/
this.$().droppable({
drop:function(event,ui){
var draggedPostId = parseInt(ui.draggable.attr("id"),10);
var draggedPost = self.get("parentView").get("controller").findBy("id",draggedPostId);
var draggedOrder = draggedPost.get("order");
var droppedPost = self.get("controller").get("model");
var droppedOrder = droppedPost.get("order");
draggedPost.set("order",droppedOrder);
droppedPost.set("order",draggedOrder);
allPosts = allPosts.sortBy("order");
self.get("parentView").get("controller").set("model",allPosts);
}
});
},
click:function(){
this.toggleProperty("controller.selected");
}
});
App.Post = Ember.Object.extend({
id:null,
name:null,
order:null
});
/*this would come from a server or web storage*/
var allPosts = [];
allPosts.pushObject(App.Post.create({id:1,name:"post1",order:1}));
allPosts.pushObject(App.Post.create({id:2,name:"post2",order:2}));
allPosts.pushObject(App.Post.create({id:3,name:"post3",order:3}));
allPosts.pushObject(App.Post.create({id:4,name:"post4",order:4}));
allPosts.pushObject(App.Post.create({id:5,name:"post5",order:5}));

I'm using jqueryUI for this. Add the file below into your app and then you can extend the custom jquery views!
https://gist.github.com/jamesmgg/9191149

Related

Ember.js: View stops updating after rending the first two items

I have an ArrayController (List) with an itemController (ListItem). The problem I am seeing is that the template that renders the list stops updating after the first two list items are rendered.
In the code below, there are two setTimeout calls when the ArrayController's view is inserted into the DOM. Each of these setTimeout calls will connect to the ArrayController and add two more list items to it. The first setTimeout works fine: two list items are added to the ArrayController, and the template shows both list items. However, after the second setTimeout, the template still only shows the first two list items instead of all four. At this point, there are only two list items shown on the page, but if I console.log the length of the content array on the ArrayController, it shows the correct length of 4.
Templates:
<!-- List View -->
<script type="text/x-handlebars" data-template-name="list" id="list">
{{each controller itemViewClass="App.ListItemView"}}
</script>
<!-- Item View -->
<script type="text/x-handlebars" data-template-name="listitem" id="listitem">
<li>Testing: {{content.caption}}</li>
</script>
Controllers:
App.ListController = Ember.ArrayController.extend({
itemController: 'list-item',
addItem: function (caption) {
this.pushObject(
App.ListItem.create({
caption: caption
})
);
}
});
App.ListItemController = Ember.Controller.extend({
});
Views:
App.ListView = Ember.View.extend({
templateName: 'list',
didInsertElement: function () {
setTimeout(function () {
this.get('controller').addItem('test1');
this.get('controller').addItem('test2');
}.bind(this), 1000);
setTimeout(function () {
this.get('controller').addItem('test3');
this.get('controller').addItem('test4');
}.bind(this), 2000);
}
});
App.ListItemView = Ember.View.extend({
tagName: '',
templateName: 'listitem',
});
Model:
App.ListItem = Ember.Object.extend({
caption: ''
});
Now, if I change the blockless each helper into a regular each helper like the following, then this all works fine. But I want to be able to define a class for the view of each item.
<!-- List View -->
<script type="text/x-handlebars" data-template-name="list" id="list">
{{#each controller}}
<li>Testing: {{content.caption}}</li>
{{/each}}
</script>
As seen above, I had originally not assigned a tag to the list item template. After I assigned it a tag, it works.
I changed the item template to:
<!-- Item View -->
<script type="text/x-handlebars" data-template-name="listitem" id="listitem">
Testing: {{content.caption}}
</script>
And the item view to:
App.ListItemView = Ember.View.extend({
tagName: 'li',
templateName: 'listitem',
});

After using jQuery UI to sort an Ember.js item, using Ember Data's model.deleteRecord() doesn't work

I'm using jQuery UI Sortable with Ember.js to sort a list of items, and it seems to work great, until I go to delete one of the Ember Data records. The model is deleted properly, but the UI doesn't update to reflect that. If you delete the last record, an Index Out of Range error is thrown. If you delete a middle record, the one after it is removed from the DOM. If you delete the first record, it removes the first and second one from the DOM. What gives?
Given the following Handlebars:
<script type="text/x-handlebars" data-template-name="application">
<h1>Ember Data + jQueryUI Sortable Problems</h1>
{{outlet}}
Try sorting and then deleting something. The model is deleted properly, but the DOM does not reflect that. Sometimes it stays in the DOM, sometimes the wrong one is deleted, and sometimes both the right and a wrong one is deleted.
</script>
<script type="text/x-handlebars" data-template-name="index">
{{#each model}}
{{render 'formField' this}}
{{/each}}
</script>
<script type="text/x-handlebars" data-template-name="formField">
<div class="form-field" {{bind-attr data-id=id}}>
<span class="delete" {{action 'delete'}}>X</span>
{{name}} ({{displayOrder}})
<span class="handle">#</span>
</div>
</script>
And the following JavaScript:
App.IndexController = Ember.ArrayController.extend({
sortProperties: ['displayOrder'], // Force sort by displayOrder, not ID
updatePositions : function(positionData){
this.propertyWillChange('content'); // Manually notify Ember
this.get('content').forEach(function(formField) {
var key = formField.get('id');
formField.set('displayOrder', positionData[key] + 1);
});
this.propertyDidChange('content'); // Manually notify Ember
}
});
App.IndexView = Ember.View.extend({
handleSort: function(event,ui){
var positionData = {};
this.$(".form-field").each(function(index, element){
// Get model ID from bind-attr in template
var key = $(element).data('id');
positionData[key] = index;
});
this.get('controller').updatePositions(positionData);
// Delay recreating the sortable
Ember.run.next(function(){ this.makeSortable(); }.bind(this));
},
makeSortable: Ember.on('didInsertElement', function(){
try {
this.$().sortable("destroy");
} catch(err){
window.console.warn('No sortable to destroy', err);
}
finally {
this.$().sortable({
handle: '.handle',
axis: 'y',
update: this.handleSort.bind(this)
});
}
})
});
App.FormFieldController = Ember.ObjectController.extend({
actions: {
'delete': function() {
window.console.log('deleting record', this.get('name'));
this.get('model').deleteRecord();
}
}
});
Here's a fiddle.
The trick is in your use of sortProperties and {{#each model}} or {{#each content}}. The sortProperties method does not actually arrange content or model, it arranges arrangedContent. By changing it to {{#each arrangedContent}}, your problems disappear because the DOM arrangement will stay in sync with your model arrangement.

Nested layouts in backbone.marionette.js

Let's say that I have this JavaScript all nicely written out for Backbone.js, with Marionette.backbone.js):
(function () {
var Application;
$(function () {
Application = new Backbone.Marionette.Application();
Application.addRegions({
top: "#top",
middle: "#middle",
bottom: "#bottom"
});
var topLayout = Backbone.Marionette.ItemView.extend({
template: "#tpl_topLayout",
tagName: "article"
});
var middleLayout = Backbone.Marionette.Layout.extend({
template: "#tpl_middleLayout",
regions: {
left: "#left",
right: "#right"
}
});
var middleLayoutOne = Backbone.Marionette.ItemView.extend({
template: "#tpl_middleLayoutOne",
tagName: "article"
});
var middleLayoutTwo = Backbone.Marionette.ItemView.extend({
template: "#tpl_middleLayoutTwo",
tagName: "article"
});
var bottomLayout = Backbone.Marionette.ItemView.extend({
template: "#tpl_bottomLayout",
tagName: "article"
});
var a = new middleLayout;
a.left.show(new middleLayoutOne);
a.right.show(new middleLayoutTwo);
Application.top.show(new topLayout);
Application.middle.show(a);
Application.bottom.show(new bottomLayout);
Application.start();
});
}());
and this HTML ...
<article id="layouts">
<section id="top"></section>
<section id="middle"></section>
<section id="bottom"></section>
</article>
<script type="text/template" id="tpl_topLayout">
Top layout
</script>
<script type="text/template" id="tpl_middleLayout">
Middle layout
<div id="left"></div>
<div id="right"></div>
</script>
<script type="text/template" id="tpl_middleLayoutOne">
Middle layout 1
</script>
<script type="text/template" id="tpl_middleLayoutTwo">
Middle layout 2
</script>
<script type="text/template" id="tpl_bottomLayout">
Bottom layout
</script>
The 'middle' layout doesn't render properly (it renders #tpl_middleLayout, but not #tpl_middleLayoutOne or #tpl_middleLayoutTwo).
Any ideas on what I'm "forgetting" to do? I've got my guesses as to /why/ it's not working, but no idea on how to fix that problem .. and Google doesn't seem to want me to know the answer yet. :)
Any help would be very, very much appreciated.
when a parent is shown, all of it's existing children are closed so just change the order your code to show the parent view before showing children inside of it
Application.middle.show(a);
a.left.show(new middleLayoutOne);
a.right.show(new middleLayoutTwo);
Here follows the working JSFiddle. What happens is that your nested views are closed if you show your middle region after showing them. It's a "cascade". :)
So:
var a = new middleLayout;
Application.middle.show(a);
a.left.show(new middleLayoutOne);
a.right.show(new middleLayoutTwo);

Backbone: remove ellipsis onclick

A bit of background
I'm trying to create a twitter like feed where the tweet/row will expand onclick, revealing more information.
The data is pulled from a JSON file (sent from the backend to the frontend).
I use backbone to render the data on the frontend.
Let's say my feed displays 10 rows, each row displays a few information then onclick the row/div expands to reveal more information.
The description field contains quite a lot of text therefore I'm applying a JavaScript ellipsis on it. I use Javascript ellipsis since the short description needs to be more than one line (don't think CSS ellipsis works for more than one line).
I created a plugin that will truncate the description text and onclick I want to remove the ellipsis and replace it by the full description (since the row will expand).
I created a plugin that will save the full description (before being truncated) into an array.
Issue
My idea was to compare the index of the row clicked (currentTarget) to the index of the rows saved (in the array) then replace the ellipsis text with the full description then expand the div with jQuery animate.
I'm not sure if there is a way to get an index from the backbone "click event" (in order to compare it to the index saved in the array)?
Feel free to let me know if there is a better way to approach this.
Thanks in advance
Here is my code:
Truncate & save original text functions
/**
* Plugins
*/
var arr = [];
$.fn.truncate = function(){
return this.each(function(index,element){
var elementText = $(element).text();
if(elementText.length > 165){
var truncated = elementText.trim().substring(0, 165).split(" ").slice(0, -1).join(" ") + "…";
}
$(element).text(truncated);
});
};
$.fn.getText = function(){
return this.each(function(index,element){
arr.push({
i: index,
v: $(element).text()
});
});
};
Backbone Model & Collections
/**
* Model
*/
var Task = Backbone.Model.extend();
/**
* Collections
*/
var RecentTasksList = Backbone.Collection.extend({
model: Task,
url: 'json/recentTasks.json'
});
Backbone Views
/**
* Views
*/
var RecentTasksView = Backbone.View.extend({
el: '.taskList',
template: _.template($('#recentTasksTemplate').html()),
render: function(){
_.each(this.model.models, function(data){
this.$el.append(this.template(data.toJSON()));
}, this);
$('.description').getText();
$('.description').truncate();
return this;
}
});
var FullTaskView = Backbone.View.extend({
el: '.taskContainer',
events: {
'click .task': 'showFullDetails'
},
showFullDetails: function(e){
var eTarget = $(e.currentTarget);
var $desc = $('.description');
if(eTarget.hasClass('expanded')){
eTarget.animate({
'height': '80px'
},
function(){
eTarget.removeClass('expanded');
});
}
else{
console.log($(eTarget).find($desc).html());
eTarget.animate({
//doesn't work lesser IE 8
'height': eTarget[0].scrollHeight
},
function(){
eTarget.addClass('expanded');
});
}
}
});
var AppView = Backbone.View.extend({
el: 'body',
initialize: function(){
//Recent Tasks
var recentTasksList = new RecentTasksList();
var recentTasksView = new RecentTasksView({
model: recentTasksList
});
recentTasksList.bind('reset', function(){
recentTasksView.render();
});
recentTasksList.fetch();
//Full Task Details
var fullTaskView = new FullTaskView();
}
});
var appView = new AppView();
Underscore template
<script id="recentTasksTemplate" type="text/template">
<div class="task clearfix">
<div class="image">
<img src="<%= image %>" />
</div>
<div class="details">
<h3 class="title"><%= title %></h3>
<div class="description">
<%= description %>
</div>
</div>
<div>
</script>
HTML
<div class="taskContainer">
<div class="taskList"></div>
</div>
EDIT
One last question. I added a tab to my page (similar call to action). Same type of information will be display onclick (I'm using the same template). For instance I now have RecentTask and PopularTask.
I created a view for the tabs containing click events. Do I need to instanciate the model & view & fetch the data each time or can I reuse the ones already initialized?
I created a new view for a second tab. Grabbing JSON file from the server:
var PopularTasksList = Backbone.Collection.extend({
model: Task,
url: 'json/popularTasks.json'
});
var PopularTasksView = Backbone.View.extend({
el: '.taskList',
render: function(){
$('.taskList').empty();
_.each(this.model.models, function(model){
var taskView = new TaskView({model: model});
this.$el.append(taskView.render().el);
}, this);
return this;
}
});
Then I created a tab view that will show the correct Tasks onclick.
var TabsView = Backbone.View.extend({
el: 'body',
events:{
'click .tabRecent': 'fetchDataRecentTasks',
'click .tabPopular': 'fetchDataPopularTasks'
},
fetchDataRecentTasks: function(){
var recentTasksList = new RecentTasksList();
var recentTasksView = new RecentTasksView({
model: recentTasksList
});
recentTasksList.bind('reset', function(){
recentTasksView.render();
});
recentTasksList.fetch();
},
fetchDataPopularTasks: function(){
var popularTasksList = new PopularTasksList();
var popularTasksView = new PopularTasksView({
model: popularTasksList
});
popularTasksList.bind('reset', function(){
popularTasksView.render();
});
popularTasksList.fetch();
}
});
I think you should create a new view for an individual task. Then in that view, you can handle the click, so you have access to the task model, and also access to the DOM of that view very easily.
Then you can get rid of your FullTaskView, and the jQuery plugins.
/**
* Model
*/
var Task = Backbone.Model.extend({
getShortDescription: function(){
var desc = this.get('description');
if(desc.length > 165){
return desc.trim().substring(0, 165).split(" ").slice(0, -1).join(" ") + "…";
}
return desc;
}
});
Add new TaskView, and change RecentTasksView to create/render them.
/**
* Views
*/
var TaskView = Backbone.View.extend({
template: _.template($('#recentTasksTemplate').html()),
events: {
'click': 'showFullDetails'
},
render: function(){
// pass the model json, plus the short description to the template
this.$el.html(this.template({
data: this.model.toJSON(),
shortDesc: this.model.getShortDescription()
}));
return this;
},
showFullDetails: function(){
// change text, show/hide, animate here
// In the view, this.$() will only match elements within this view.
// if expand...
this.$('.description').html(this.model.get('description'));
// if hide...
this.$('.description').html(this.model.getShortDescription());
}
});
var RecentTasksView = Backbone.View.extend({
el: '.taskList',
render: function(){
_.each(this.model.models, function(model){
// create a view for each task, render and append it
var taskView = new TaskView({model: model});
this.$el.append(taskView.render().el);
}, this);
return this;
}
});
Change template to use new data passed to it.
// access the model stuff with data.title, etc.
<script id="recentTasksTemplate" type="text/template">
<div class="task clearfix">
<div class="image">
<img src="<%= data.image %>" />
</div>
<div class="details">
<h3 class="title"><%= data.title %></h3>
<div class="description">
<%= shortDesc %>
</div>
</div>
<div>
</script>
EDIT:
A Backbone view is meant to manage a DOM element, so it is just a good idea to have each task be its own view instance. This makes it easier to do the expanding and changing the text based on the click. Also it is a best practice to not have code outside the view changing things inside its DOM element, so it is good to do that manipulation inside each task view.
This is similar to a TodoView in the Todo sample:
http://backbonejs.org/docs/todos.html
http://backbonejs.org/examples/todos/index.html
You can pass the template function any javascript object (even an object with functions, not just properties). Since you want to display some data that is not technically part of the model, passing that data object is just a way to get the stuff you need into the template.

Super simple page navigation with Backbone

I have searched all over the place to no avail. Everyone seems to have their own way of building some form of a todo list with backbone. I need to do something a little different albeit more crude. I need to build a four page site on top of backbone–I know I could easily do this with jQuery or equivalent, but this might be something that is increased in scope down the road. So really Im not using Models or Collections, just routes, views and templates. My number of templates is so small I don't need to get them from an external folder I can just swap out divs and have the templates live inline.
In its most simple form I have a single page web app that I need to swap out 4 static views with with one button, one direction, with a start and finish. Thats it. No tutorial or documentation I have found performs something this basic with backbone. Any savvy folks out there care to point me in the right direction?
Here's a simple little one-page app that swaps out 4 different templates and steps from page 1 to 4 as you press the button. Let me know if you have questions.
<html>
<head>
<title>Steps</title>
<script src="http://code.jquery.com/jquery-1.8.0.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.3.3/underscore-min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/backbone.js/0.9.2/backbone-min.js"></script>
<script type="text/x-template" id="page-1">
<p>Page one content</p>
</script>
<script type="text/x-template" id="page-2">
<p>Page two content</p>
</script>
<script type="text/x-template" id="page-3">
<p>Page three content</p>
</script>
<script type="text/x-template" id="page-4">
<p>Page four content</p>
</script>
<script>
(function($){
// Helper to get template text.
function getTemplate(index){
return $('#page-' + index).text();
}
// Simple view to render a template, and add a button that
// will navigate to the next page when clicked.
var PageView = Backbone.View.extend({
index_: null,
events: {
'click button': 'nextPage_'
},
initialize: function(options){
this.index_ = options.index;
},
render: function(){
var html = getTemplate(this.index_);
// If there is a next page, add a button to proceed.
if (html && getTemplate(this.index_ + 1)){
html += '<button>Next</button>';
}
this.$el.html(html);
},
nextPage_: function(){
router.navigate('page/' + (this.index_ + 1), {trigger: true});
}
});
// Router handling a default page, and the page urls.
var Router = Backbone.Router.extend({
routes: {
'page/:index': 'loadPage',
'*notFound': 'defaultPage'
},
defaultPage: function(){
this.loadPage();
},
loadPage: function(index){
// Default to page 1 when no page is given.
index = parseInt(index, 10) || 1;
if (this.pageView_) this.pageView_.remove();
this.pageView_ = new PageView({index: index});
this.pageView_.render();
this.pageView_.$el.appendTo('#content');
}
});
var router;
$(function(){
router = new Router();
Backbone.history.start({pushState: true});
});
})(jQuery);
</script>
</head>
<body>
<!-- Some page header -->
<section id="content"></section>
</body>
</html>

Categories

Resources