I am very new at using Backbone. Please forgive me in advance as I am struggling to think in new ways building web apps.
I am confused about how to go about using it for items that are never really covered in any of the tutorials. All the tutorials give the basic "here is a model", "here is a collection of models", "here is a view that uses the model", etc. for entities that we all understand, such as a to-do item.
I do have those cases, and I am doing OK with those, but I am having trouble figuring out how to use Backbone for the following situation.
I have a to-do app (of course.) My UI needs to have several menus that allow the user to filter the to-dos by things like priority, due date, and other properties. So, my filter menus might look like this...
All To-Dos (100)
Inbox (15)
Important (10)
Someday (15)
Today (0)
Tomorrow (6)
This Week (7)
These are all somewhat static menus, except that when a filter is clicked, it should highlight, and possibly cause another filter to be turned off. Also, this would trigger an update in my results by performing a search and re-rendering my to-do list.
So, should I represent these items with views only? Are models needed to represent the state of the menus? Should I create a FilterMenuItem model and FilterMenu model, and also the corresponding views?
Again, I understand the samples when it comes to a model for a to-do item and a to-do collection, but I am stumped on how to tackle these seemingly simple items using Backbone.
I appreciate any suggestions or guidance.
The important thing to remember here is that collections in backbone.js inherit a bunch of cool features from underscore.js. Included in these is filter (or select), which allows you to get only those members of a collection which match your perameters. For example:
render: function(){
myCollection.filter(function(item){return item.folder === "inbox"});
}
If the menus are actually static, then you can use a case select statement to determine which page you are on & therefore which filter to use. Otherwise, you can have an array of objects representing the views, which describe how to filter, i.e.:
[
{view: "all", filter: function(item){return item;}}.
{view: "inbox", filter: function(item){return item.foler === "inbox";}},
{view: "important", filter: function(item){return item.type === "important;}}
]
As far as producing the view for your menu items goes, you have to decide if the the menu is static or not. If it is, then you can simply hard-code the items to different controller routes. If it is not, then you should probably use a collection of menuItem models:
var menuItem = Backbone.Model.extend({});
var menuList = Backbone.Collection.extend({
model: menuItem
});
var menu = new menuList([{name: "All To-Dos", url: "#!/all"}, {name: "index", url: "#!/index"}]);
which you can add or remove items to dynamically, as well as having the options built from the server specifications (i.e. the user may be able to save custom folders, which will get added here) use the refresh command to avoid unneccesary http calls. Then, you can pass this collection to your view, and render the items out however you want.
Hope this helps!
Related
Overview:
I use the angular-drag-and-drop-lists directives to handle drag and drop between lists.
My api data source returns nested lists like so:
[
{
"day_date": "Apr 4, 2017",
"day_events": [
{
"event_title": "Check in"
},
{
"event_title": "zip up"
}
]
},
{
"day_date": "Apr 5, 2017",
"day_events": [
{
"event_title": "An event on the second day",
}
]
}
];
I'm using ui-router's $stateProvider to separate these lists into nested views but using the same controller for all views. For example:
In the above image, we'll concern ourself with the first two views:
The left side nav lists the days from the api (collection of items.)
The middle nested view lists all the events in each day (the items)
The idea being that we allow the dragging and dropping of the following:
reorder of days within the days list (collection if items)
reorder of events within events list (items)
moving of events from one day to another (items moved to new collection of items)
Once the reordering is done, we will then return the newly saved order to the api. I figure we need to share the controller between views so we let the views change the data on the same scope and then post the entire change back to the same api endpoint.
As a proof-of-concept, I spun up a plunker to see if I can get the moving of items between days to work.
http://plnkr.co/edit/cgQC60FMLwTkA9WPDiRr?p=preview
It works because I nested the ng-repeat for each list.
<div ng-controller="daysCtrl">
<ul ng-repeat="day in days" dnd-list="day.day_events">
<li class="title">{{day.day_date}}</li>
<li ng-repeat="event in day.day_events" dnd-draggable="event" dnd-moved="day.day_events.splice($index, 1)" dnd-effect-allowed="move">
{{ event.event_title }}</li>
</ul>
The problem:
As mentioned, my api data is nested, so I need to reorder it and post it back to the same endpoint. Since I have separate nested views/states that deal with each part of the nested list, the approach in the previous plunker breaks down because I can't nest the ng-repeat between views.
In the example below, I don't have nested states but I did separate out the ng-repeats. Obviously, it won't work.
http://plnkr.co/edit/EfgR07hJOlLTw60n2RVQ?p=preview
Things I've tried:
I saw this but it's not the same as they are using two separate lists on the scope. I have a single nested list that's shared between views.
When a user clicks on a day in the list of days view, I could pass the array of events to the new view via the state parameters. I've done this and it works. However, it's then treated as a new list and, while I can rearrange the events within their own list, I can't move the event items between days.
What I'd like to know:
What's the best way to accomplish this without changing my data structure or my views?
Is there a better way than the way I'm currently doing it? How would you do this? (even if it means changing the api or view setup - which would suck but i'm open to it.)
Thank you
Create a new list $scope.list and copy the days_events in that list whenever the day_date is clicked. Iterate through this list in another view which is under same controller and your drag drop shall work
http://plnkr.co/edit/vsuuULgnaXvk7y9C0MOJ?p=preview
We're working on CRUD patterns for a fairly large application. One common UI pattern we're using to define a one-to-many relationship where a record is associated by checkbox. The challenge is to persist selections (checked/unchecked) through asynchronous calls (search/sort) that refresh the record list (and the associated ng-model). I'd like to hear from more advanced AngularJS users (I'm a noob) what's considered a best practice for this? Any feedback is appreciated!
EDIT
Here's a working plunk showing how I'd most likely tackle this with my current understanding of Angular. Please let me know if you have a better approach!
I think u could maintain a separate collection of selected names. So the next time you filter the list, you just need to lookup in the collection in order to keep the item selected. That you can do by binding some variable (arrSelected) in the controller or you can create a separate service also.
Have you considered using a filter instead of refetching. Of course this really depends on how many items you plan on having, but I've had success filtering a nested JSON object with around 5000 items.
Here is the full plunker : http://plnkr.co/edit/l4jYgt0LjRoP2H0YuTIT?p=preview
Highlights:
In index.html for your repeat you add | filter
<tr ng-repeat="user in users | filter:userFilter">
In script.js we add the filter function and a $scope variable to hold the filtered letter:
$scope.filteredLetter
$scope.userFilter = function (elem) {
if (elem.name.lastIndexOf($scope.filteredLetter, 0) === 0 || $scope.filteredLetter == '') {
return true;
} else {
return false
}
}
As a bonus I added in ng-class to show which letter was highlighted.
Pretty simple code but this gives you full persistence now even as people change things. You can even experiment now with adding a <input> tag with an ng-model binding to say $scope.filteredName. Then all you need to do is add the JS to the filter to do a deeper filter for part of the name.
In the following Layout, I am adding a CollectionView to display a SELECT list within onRender. Immediately after that, I am using the ui hash to enable or disable all controls within the view. This does not work for the SELECT generated by new App.View.Categories.
Should it? Or does the UI hash not work on Regions within a Layout?
App.View.UploadFile = Backbone.Marionette.Layout.extend({
template: '#upload-file-template',
regions:{
category: 'td:nth-child(4)'
},
ui:{
inputs: 'textarea, select, .save'
},
onRender: function(){
this.category.show(
new App.View.Categories({
collection: App.collection.categories
}) // generates the SELECT list
);
console.log(this.ui.inputs); // Length 2. Missing select.
console.log(this.$('textarea, select, .save')); // Length 3
this.ui.inputs.prop(
'disabled', (this.model.get('upload_status')!='staged')
);
}
});
This should be working the way you expect it to work. The code in question in the Marionette source is here: https://github.com/marionettejs/backbone.marionette/blob/master/src/marionette.itemview.js#L49-L51
The call to bindUIElements() is what converts the ui hash in to jQuery selector objects, and it is called right before the onRender method is called.
Are you seeing errors? Or is the selector simply returning nothing, and having no affect on the elements?
Update:
Ah! Of course... I wasn't paying attention to your code close enough. You're correct in that the UI element selectors happen before you're adding the the sub-view to the region. I've never run in to this situation before... but this seems like something we would want to fix / support.
For now, the best workaround I can suggest would be to call 'this.bindUIElements();' at the very end of your onRender method. This would force the ui elements to re-bind to the selectors.
I'll also add an issue to the github issues list, to look in to a better solution for this. i don't know when i'll be able to get to this, but this will at least get it on the list of things to fix.
Please consider following scenario:
<ParentView>
<FilterSubview></FilterSubview>
<ListSubview></ListSubview>
</ParentView>
To give you and example: I have a view which in turn shows view with filter (user can select to display books, magazines or both of them) and the list with items.
Both filter and list have corresponding models. Filter - what can we filter. List - list of all items.
Use case: user sees the full list and then can filter results by selecting only desired category.
Questions:
How those two views should interact? Should they know about each other or should parent view handle it?
Who should store filtered list to display? It could be list subview model directly or parent view can filter complete list and then pass it to render.
There is no one correct answer to your questions, but I'll try to explain a common, idiomatic way here.
Two sibling views should not know of each other. Instead they should interact via events through some kind of a mediator. Since in your case both FilterView and ListSubView share a common parent view which is responsible for rendering both of them, you could let the parent view mediate the events:
var ParentView = Backbone.View.extend({
initialize: function() {
this.listenTo(this.filterView, "filter", this.filterChanged);
},
filterChanged: function(filterValue) {
this.listSubView.filter(filterValue);
}
});
var FilterView = Backbone.View.extend({
events: {
"change .filter" : "filterValueChanged"
},
filterValueChanged: function() {
var filterValue = //get filter value...
this.trigger("filter", filterValue);
}
});
Alternatively (preferrably, even) you can cut out a middle man and use the Mediator pattern. For that you need a third component whose job it is to pass messages between parties who should not know of each other. If you're using Backbone 0.9.9, there's just such a mediator built in: the Backbone root object works as a global event bus for this purpose.
So:
//ListSubView
this.listenTo(Backbone, "listfilterchanged", this.filterChanged);
//FilterView
Backbone.trigger("listfilterchanged", filterValue);
Then there's the question of who should be responsible of the list data. I tend to prefer to have the most specialized component be in charge, but so that only one component is in charge. In your case that would mean that the ListSubView should manage the filtered list, but only if the ParentView doesn't need to operate on it. That's just a generalization though, so take it with a grain of salt and do what feels right for your case.
I'm working on a sample ToDo list project in Backbone and I'd like to understand how the framework would prefer me to organize its views and models in the nested list scenario.
To clarify what I mean by that, my single-page Backbone app should display lists of ToDo lists. From the backend standpoint, there's a List resource and an Item (a single entry in a todo list) resource. Something along the lines of:
Monday chores
Pick up the mail
Do the laundry
Pick up drycleaning
Grocery list
Celery
Beef
You get the idea...
Since mine is a Rails 3.2 app, I'm vaguely following the Railscasts Backbone.js tutorial, so that's where I'm getting the current design from. I would love to know if I'm wildly off the Backbone-prescribed pattern, or if I'm on the right track!
I thus far have:
ListsIndex View //index of all lists
\-- ListsCollection
\-- ListView / Model //individual list
\-- ItemsIndex View //index of items in one list
\-- ItemsCollection
\-- Item View / Model //individual todo item
The flow would be:
On router initialize, fetch() collection of lists on /lists backend route. On the 'reset' event for the collection part of ListsIndex, execute render() on each of the items in the collection, appending to the list index view template.
In the initialize method of each Item View (is this where you'd wire-up the second level fetch?) fetch() the items from the /lists/:id/items backend route into an ItemsCollection specific to that view.
In the same method, instantiate an ItemsIndex object and pass the collection into it. Once again, in ItemsIndex, have a 'reset' event handler for when the collection is populated, at which point it should render each fetched model from the item collection and append them to its own view.
I'm essentially taking the design of the List and mirroring it down one level to its items. The difference is that I no longer have a router to rely on. I therefore use the initialize method of ListView to a similar effect.
Yay / nay? Super wrong? Thanks!
TL:DR; 1) I would bootstrap your initial data instead of a fetch() reset(). 2) You can do a fetch in the initialize of a View as you need it. Or you could load the data at the start. Just remember that if you fetch in the init, the async nature won't have the data ready at render. Not a problem if you have a listener waiting for that sync/add/etc. 3) I don't know what you mean by itemIndex object but you can create objects and add to them collections as you need them. Or you can just bake the in at the start if you know all your lists are going to have a collection eventually. You can reset if you want (fetch automatically does this unless you give it option {add:true}) or just add them in one by one as they come in although reset(), remove prior views, render all views seems to be the common way people do things with a complete fetch().
I think it looks pretty good. The nice thing about Backbone is that you can do it many different ways. For example, your number 2 says to wire up a second fetch() from the view. You could do that if you want to lazy load. Or you could just grab all the data at app start before anything is done. It's really up to you. This is how I might do it.
This is how I might make an app like this (just my preference, I don't know that it's any better or worse or if its the same as you described.)
First I would create a model called ListModel. It would have an id and a name attr. This way, you can create many separate lists, each with their own id that you can fetch individually.
Each ListModel has an ItemsCollection inside of it. This collection has a url based on the ListModel it is a part of. Thus, the collection url for ListModel-1 would be something like /list/1
Finally you have ItemModel which is a resource id and text.
ListCollection
ListModel // Monday Chores
ItemCollection
ItemModel // Mail
ItemModel // Laundry
ItemModel // Drycleaning
ListModel // Grocery
ItemCollection
ItemModel // Celery
ItemModel // Beef
So in this little display you'll notice I didn't put anything to do with views in yet. I don't know if it's more of a conceptual thing but this is what the data hierarchy looks like and your views can be, should be totally independent of it. I wasn't exactly sure how you were including the views up above but I thought this might make it clearer.
As for defining these structures, I think two things.
First, I'd make sure my ListModel is defined in my collection. That way I can use the collection add(hash) to instantiate new models as I produce / add them.
Second, I would define the ListModel so that when one is created, it automatically creates an ItemCollection as a property of that ListModel object (not as an attribute).
So ideally, your ListModels would be like this:
ListModel.ItemCollection
Before the app initializes, I would bootstrap the data in and not fetch(). (This kind of addresses point 1 you make) Ideally, when your Backbone application starts it should have all the necessary data it needs from the get go. I would pass in the head some data like this:
var lists = [listModel-1-hash, listModel-2-hash];
Now when the app fires up, you can instantly create these two lists.
var myLists = new ListCollection();
_.each(lists, function(hash) {
myLists.add(hash); // Assumes you have defined your model in the ListCollection
}
Now your List Collection has all the list models it needs.
Here is where views come in. You can pass in anything to any view. But I might break views down into three things.
AppView, ListModelView, ItemModelView and that's it.
Imagine a structure like this:
<body> // AppView
<ul class="List"> // ListModelView
<li class="Item"></li> // ItemModelView
</ul>
<ul class="List"> // ListModelView
</ul>
</body>
When your start your app and create an AppView, inside AppView you'd generate each ListModelView and append it to the body. Our lists are empty. Maybe when you click on the it lazy loads the items. This is how you'd hook it up.
// In ListModelView
events: {'click':'fetchItems'}
fetchItems: function() {
this.model.itemCollection.fetch(); // Assumes you passed in the ListModel into view
}
So since I bootstrapped the data to begin with, this fetch() call would be your "second" fetch. (I'm addressing point 2 you made.) You can fetch it in your initialize. Just remember that it is an asynchronous function so if you need them at render time, it won't work. But, what you can do is add event listeners to this view that are listening for add events to your itemCollections.
this.model.itemCollection.on('add', this.addItemView, this);
addItemView() will generate new instances of the itemViews and append them.
As for point 3, you can instantiate a collection at that point you need it and throw it into your ListModel. Or you can do what I did and make sure all your models always have an ItemCollection. This depends on your preferences and goals. You probably didn't need all this but I felt like illustrating it out for some reason. I dunno, maybe it helps.