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);
Related
Here is, what I have achieve so far. I am having a div inside my first template. When I am showing another layoutView inside that div. it is showing the following error.
Uncaught Error: An "el" #nestedDiv must exist in DOM
HTML -
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>MarionetteJS</title>
</head>
<body>
<div id="container"><div>
<script type='text/template' id='myTemplate'>
<h2><%=heading%></h2>
<div id='nestedDiv'></div>
</script>
<script type='text/template' id='innerTemplate'>
<h2><%=nestedHeading%></h2>
</script>
<script src='_assets/js/_lib/jquery-1.7.2.min.js'></script>
<script src='_assets/js/_lib/underscore.js'></script>
<script src='_assets/js/_lib/backbone.js'></script>
<script src='_assets/js/_lib/backbone.marionette.js'></script>
<script src='_assets/js/layoutView.js'></script>
</body>
</html>
JS -
//Application Object
var myApp = new Marionette.Application({
regions: {
main: '#container'
}
});
//First Model
var TaskModel = Backbone.Model.extend({
defaults: {
'heading' : 'Welcome to Backbone'
}
});
//Second Model
var Person = Backbone.Model.extend({
defaults: {
'nestedHeading' : 'This is a subheading.'
}
});
//View for Div #nestedDiv
var PersonView = Marionette.ItemView.extend({
template: '#innerTemplate'
});
//View for main Region
var TaskView = Marionette.LayoutView.extend({
template : '#myTemplate',
onShow: function() {
var person = new Person();
var personView = new PersonView({model: person});
var PersonLayoutView = Marionette.LayoutView.extend({
regions: {
'foo' : '#nestedDiv'
}
});
var obj = new PersonLayoutView();
obj.foo.show(personView);
}
});
var taskModel = new TaskModel();
var taskView = new TaskView({model:taskModel});
myApp.main.show(taskView);
and here is JSBin Link - http://jsbin.com/dusica/1/edit?html,js,console,output
onShow is a callback on the Marionette Region object and cannot be called on the layout directly.
You probably want to:
1 Call render() on the instantiated LayoutView, in order to render the template in the DOM;
2 Instantiate a new view to show within a Region that was defined on the LayoutView;
3 Show the view within the region. If you need it, you can use obj.foo.onShow() as a callback after the view was rendered within the region;
According to the docs:
https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.layoutview.md
interactions with Marionette.Region will provide features such as
onShow callbacks, etc. Please see the Region documentation for more
information.
https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.region.md
"show" / onShow - Called on the view instance when the view has been
rendered and displayed.
"show" / onShow - Called on the region
instance when the view has been rendered and displayed.
Sidenote: in case you would be tempted to use onRender() (LayoutView extends from ItemView), don't. onRender does not mean that the view is present in the DOM, but rather that it is prepared for insertion.
That error basically means that your #nestedDiv is not yet in DOM when you are trying to show the view. And that is understandable as you have not showed obj (a PersonLayoutView) in any region.
In fact, you do not need another nested layout view. Consider modifying your view for main region to:
//View for main Region
var TaskView = Marionette.LayoutView.extend({
template : '#myTemplate',
regions: {
'personLayoutRegion': '#nestedDiv'
},
onShow: function() {
var person = new Person();
var personView = new PersonView({model: person});
this.personLayoutRegion.show(personView);
}
});
It works at: http://jsbin.com/pecoxujose/2/
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
I currently have a composite view, and I would like for each ItemView to render based on its index.
For example, I want to add a class to an every third ItemView.
The solution I'm leaning towards is altering appendHtml() to add a class to the view every third time. I've put the code for this below.
Is there any advantage to using getItemView()? A disadvantage I see is that it doesn't have direct access to the index.
Templates
<script id="list-item" type="text/html">
<%= name %>
</script>
<script id="list-layout" type="text/html">
<div class='collection'>
<h3><%= name %></h3>
<ul></ul>
</div>
</script>
JS
var ListItemView = Backbone.Marionette.ItemView.extend({
template: '#list-item',
tagName: 'li'
});
var ListComposite = Backbone.Marionette.CompositeView.extend({
itemView: ListItemView,
itemViewContainer: "ul",
template: '#list-layout',
appendHtml: function(cv, iv, index){
if ((index + 1) % 3 == 0) iv.$el.addClass('rowend');
var $container = this.getItemViewContainer(cv);
$container.append(iv.el);
}
});
One option would be to use the buildItemView
https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.collectionview.md#collectionviews-builditemview
Now you won't have direct access to the index, but you can use underscores methods to fix that (these should all be augmented onto backbone collections, in particular this one http://underscorejs.org/#indexOf).
The main benefit you get is that you can directly influence the classes of the itemviews
var ListComposite = Backbone.Marionette.CompositeView.extend({
itemView: ListItemView,
itemViewContainer: "ul",
template: '#list-layout',
buildItemView: function(item, ItemViewType, itemViewOptions){
var index = this.collection.indexOf(item);
var options = _.extend({model: item}, itemViewOptions, {className:"someClassName" + index});
var view = new ItemViewType(options);
return view;
},
});
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.
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>