Many of the views in my application need to be "collapsible". To the user this means that you can click an arrow to collapse or expand the view's contents.
When creating a view I need to be able to easily say, "This view should be collapsible," and then run the appropriate setup code (which essentially means adding the .collapsible class to the view's wrapper and inserting a dom element that looks like this: <div class="toggle"></div>
Suggestions on ways to pull this off seamlessly? I'm currently using Backbone, Backbone.Marionette, and Underscore.
I do this with another application that doesn't use Backbone. In that application every action results in a page refresh, so I just use jQuery to look for all elements with the .collapsible class and do my setup that way.
EDIT:
I'm using Backbone.Marionette.CompositeView for these particular views, if that helps.
I've done similar thing in my project by extracting such functionality into mixins. There're different approaches to implementing mixins in Backbone. Take a look here or here
You can create parent view that extends from Marionettes compositeView and add your common functionallity there, and have your project views extend from this parent view.
var CollapsibleView = Backbone.Marionette.CompositeView.extends({
variable1: 1,
var2: true,
initialize : function() {
// your code here
},
helperfunction : function () {
// other helpful function
}
});
var MySpecificView = CollapsibleView.extends({
mySpecificFunction : function () {
// some specificView functionality
}
});
var myProjectView= new MySpecifcView();
myProjectView.helperfunction(); /// function from the parent
myProjectView.mySpecificFunction(); /// function from the specificView
/// you also have the functionality added on the initialization of the collpasibleView
Related
I'm quite new to Angular, and I'm adapting a simple CRUD app written using standard controllers and ngResource to use the components introduced in 1.5. None of the docs and resources I've found so far discuss how to:
create a new item from scratch
integrate with ngResource
so I'm wondering if anyone can give some pointers on how best to proceed.
My existing app has a simple factory declaring a resource entity, and a single controller that
instantiates a new instance of the resource: $scope.newEntity = new Entity();
populates the $scope with a list of the resources retrieved from the backend: Entity.query(function (data) { $scope.entities = data; });
provides a couple of functions for deleting, updating, and saving the resource to the backend.
In the HTML I have a form that works with $scope.newEntity and the controller saving method to save the new entity to the backend. I also have an ng-repeat that lists the entries stored in $scope.entities, with a couple of additional ng-clicks to perform some editing and deleting.
What I want to do now is implement some inline editing in the list. I know I can do this with my existing approach, but I want to cleanly reuse the form validation functionality I have in the existing entity creation form in the entity editing code, without duplicating. Components seem like a natural fit for that to my (admittedly inexperienced) eyes.
With the component-based approach, I have followed the documentation at https://docs.angularjs.org/guide/component under Example of a component tree, and created an entity-list and entity-detail component. These work okay so far, and I think I can figure out how to wire up the on-delete and on-update events. What I can't figure out is how to approach an on-create event.
Should I use a completely separate controller with my existing simple form to handle the creation event? If so, how can I get the existing list to automatically update? Will that creation event propagate across to the list controller?
Or am I missing something in the existing list controller? Or is the entity creation a special case for the detail controller?
I'm looking specifically for information about how to implement this using Angular components and ngResource, as I'd also like to be ready for Angular 2. Unless components and resources aren't meant to work together please don't post answers about how to achieve this using a completely different approach, or how to reuse HTML code without components. Thanks!
Actually the C in CRUD is realy simple. You were probably expecting an on-create method to be used from your entity-detail. entity-list should take care of the creation of the details however.
Here is the working code
I extended the example from the guide https://docs.angularjs.org/guide/component under Example of a component tree you were reading too and added the create:
(function () {
'use strict';
angular
.module('componentCrud')
.component('heroList', {
templateUrl: "component/hero-list.component.html",
controller : [
HeroListController
]
});
function HeroListController() {
var ctrl = this;
ctrl.list = createHeroes();
ctrl.updateHero = updateHero;
ctrl.deleteHero = deleteHero;
ctrl.createHero = createHero;
function createHero(){
ctrl.list.push({
name : 'Crazy Newling',
location: 'Morgues'
})
}
function updateHero(hero, prop, value) {
hero[prop] = value;
}
function deleteHero(hero) {
var idx = ctrl.list.indexOf(hero);
if (idx >= 0) {
ctrl.list.splice(idx, 1);
}
}
function createHeroes() {
return [{
name : 'Superman',
location: ''
},
{
name : 'Batman',
location: 'Wayne Manor'
}
]
}
}
})();
Then in HTML you just add a create button:
<b>Heroes</b><br>
<hero-detail ng-repeat="hero in $ctrl.list"
hero="hero"
on-delete="$ctrl.deleteHero(hero)"
on-update="$ctrl.updateHero(hero, prop, value)"></hero-detail>
<button ng-click="$ctrl.createHero()">Hire a new Hero</button>
I hope it is going to help you!
So I have a form that appears on a few pages and contains a number of groups of inputs. Say group a, group b and group c. On some pages they might have a and b and on others the form contains b and c. Each group may require its own client side custom validation maybe executed from the form controller.
What is the best way to achieve this using backbone and marionette?
Conceptually, and i'm fairly new to both, I'd assume I'd need a FormController that is instantiated from a page specific Controller which also instantiates the group controllers that I need for that page. Any advice would be great.
TL;DR
To make this work you would create two objects per input group. One for its custom functions, and one for its events. In the form controller you'd _.extend the function objects of each input group with a base form view to get the custom functions into your new form view. Next you'd pass in your custom events objects into the constructor for the new form view, where a utility function of your basic view would add the new custom events to the basic form view events. Finally, you'd have to ensure that you have a template for that form that contains the correct input groups.
I should also mention that you can accomplish this with Marionette.LayoutViews and dynamic regions, in a possibly cleaner way (you could specify an atomic template for each input group), albeit with a lot more overhead.
The expanded explanation
I've been toying with this idea a bit. I've been thinking a lot lately about using Underscore _.extend() to plug-in stock functionality to different views. Your problem would be ideal for this solution. Here's a sample of how you'd implement it.
Including functionality
Say that your input group A had a validate function that did it's thing,
validateA: function () {
// custom validation routine
},
and a submit function to handle form submissions,
submitA: function () {
// custom submit routine
}
The first thing you do is package these functions inside an object:
groupA = {
validateA: function() {...},
submitA: function() {...}
}
You'd have as many function objects as you have input groups.
You'd also build a generic form view that would house common form functionality, and which would also serve as the basic view you'd use to render your form
var GenericForm = Backbone.Marionette.ItemView.extend({...});
In here you'll put all your baseline events and functions common to all forms.
Then, as you mentioned you'd set up a FormController that would plug in the custom functionality, like this,
var formController = function () {
callFormZ: function() {
var genericForm = new GenericForm({ groupEvents: [eventsA, eventsB] });
var formZ = _.extend(genericForm, groupA, groupB);
SomeRegion.show(formZ);
}
}
By using _.extend on your basic view and your two input groups you end up with a new view, formZ that is a composite of the two input groups. If you'd look inside it would have all the functionality of GenericForm plus
{
validateA: function () { ... },
submitA: function () { ... },
validateB: function () { ... },
submitB: function () { ... },
}
Events
At this point, though there is no way to bind any events to your custom functions.
You may have noticed in the callFormZ controller that I passed in an array in a property called groupEvents. These are the custom events for your input groups. They'd normally have the form
eventsA: {
'blur .groupA': 'validateA',
'click #groupA button': 'submitA'
}
Ideally, we would just use extend to merge your events the way we did your functions. But since we want to end up with just one events property in your view, we run into a problem. _.extend will overwrite all the events properties in your view object with the events property of the last object passed into _.extend. So to get around this problem we have to pass an array of custom events, one for each form group.
First, you'd create a config array of events objects with your custom events for the particular form you're working with,
customEvents = [
eventsA: {
'blur .groupA': 'validateA',
'click #groupA button': 'submitA'
},
eventsB: {
'blur .groupB': 'validateB',
'click #groupB button': 'submitBA'
}];
You'd pass this array into your constructor, as we did in the callFormZ controller function. The array will now be loaded in your options parameter. In your initialize you can call something like this,
initialize(options) {
combineEvents(options.groupEvents);
this.delegateEvents();
}
where, combinedEvents is
combineEvents: function() {
var extendEvents = [this.eventsA, this.eventB]; // Make an array of the extended groups
_.each(extendEvents, function (extendEvent) {
for (prop in extendEvent)
this.event[prop] = extendEvent[prop];
}
}
Also, note that after combining events I called delegateEvents so that we'd rewire the new events.
Templates
To make all this work you'll have to provide a template that has the portions of each input group. I'm not aware of how we construct the template programmatically from individual input group templates. Instead, you'd have to have a template with the input groups relevant to the form.
Putting it all together
So, to make this work you would create two objects per input group. One for its custom functions, and one for its events. In the form controller you'd _.extend the function objects of each input group with a base form view to get the custom functions into your new form view. Next you'd pass in your custom events objects into the constructor for the new form view, where a utility function of your basic view would add the new custom events to the basic form view events. Finally, you'd have to ensure that you have a template for that form that contains the correct input groups.
I have skimmed through all the marionette articles on the layout view and I am not sure if there are advantages to using that versus how I have my app setup now, let me show you.
After building the components to my app I then created an app level view that handles the initialization and rending of all those views.
var Backbone = require('backbone'),
TeamsView = require('./teams'),
DataView = require('./teamData'),
LeaderView = require('./leader');
module.exports = appView = Backbone.View.extend({
el: '#wrap',
template: require('../../templates/app.hbs'),
initialize: function() {
window.App.views.teamsView = new TeamsView({ collection: window.App.data.teams });
window.App.views.dataView = new DataView({ collection: window.App.data.teams });
window.App.views.leaderView = new LeaderView({ collection: window.App.data.teams });
},
render: function() {
var teamsView = window.App.views.teamsView;
var dataView = window.App.views.dataView;
var leaderView = window.App.views.leaderView;
this.$el.find('#basketball .app').prepend(teamsView.render().el);
this.$el.find('#basketball .app').prepend(dataView.render().el);
this.$el.prepend(leaderView.render().el);
}
});
Then inside a controller I render the app view above.
This feels comfortable to me, but somewhere deep inside says it's wrong and I should be looking at layout views?
So my question more specifically is when putting the pieces together in an backbone application should I be looking into layout views or is creating a single app level view (like above) sufficient?
What you're doing is fine at the simplest level, though perhaps more manual than it needs to be. However, you may want a Layout View as things get more complex.
The value of a Layout View comes when you have a region in your App that you'd like to have contain sub-regions, but only during certain views. For example, you might have an index/list inside of your App's #mainRegion that just has a bunch of teams, in which case you could use a CollectionView (or CompositeView if you want to add some styling) along with the ItemView for each team.
However, say you click to edit one of the teams, and now you want the #mainRegion to show the edit page, which itself has some information about the team in #infoRegion and then an edit form in a #formRegion. These are regions that are specific to the edit page, and so you'd want to use a Layout View to manage them rather than delegating all the way up to the App level.
I hope this makes sense, and I'm happy to clarify if needed. You can also check out the documentation for a breakdown of when to use each view type: https://github.com/marionettejs/backbone.marionette/wiki/Use-cases-for-the-different-views
Is there a preferred way to trigger Router.navigate in Backbone when a user clicks a link?
For instance a template might have a link Log Out. Is the preferred approach really to use a custom class and attach a click handler to that class in the view? This seems to generate tons of duplicate code so I'm looking for a better way.
The way to handle this kind of thing is to extend the Backbone objects ( in this case the View). You'll notice in the Backbone docs that this is encouraged, and necessary given its minimalist code base. I'd recommend checking out Marionette (on Github) and the site Backbone patterns for good ways to extend the Backbone core.
For example, maybe you extend View with a method that wires up the navigation handlers as you like:
Backbone.View.prototype.wireupNavs = function() {
var that = this;
this.$el.find("a[role=nav]").each(function() {
var target = $(this).attr('href');
that.bind("click", router.navigate(target);
});
}
Then any a tag you want as a Backbone nav element, you'd just decorate with the role="nav" attribute; and call this.wireupNavs() in the initializer function of the appropriate views.
Trying to create a page that allows users to add edit and view a parent child combined.
UI has 3 columns
Parent : List of Parents Children : Child
I want to configure the controllers(s) so that users can come back to right where they were but see no need to have it so both Parent and child can be editable.
// Getting closer using backbone marionette but still having some small issues
MyRouter = Backbone.Marionette.AppRouter.extend({
appRoutes: {
'': 'AddClient',
'View/:clientid': 'ViewClient',
'Edit/:clientid': 'EditClient',
'View/:clientid/Add': 'PolicyAdd',
'View/:clientid/View/:policyid': 'PolicyView',
'View/:clientid/Edit/:policyid': 'PolicyEdit'
}
});
someController = {
AddClient: function () {
var someView = new ClientAdd();
MyApp.clientPane.show(someView);
},
ViewClient: function (clientid) {
var someView = new ClientView();
MyApp.clientPane.show(someView);
},
EditClient: function (clientid) {
var someView = new ClientEdit();
MyApp.clientPane.show(someView);
},
PolicyAdd: function (clientid) {
this.ViewClient(clientid);
var someView = new PolicyAdd();
MyApp.policyPane.show(someView);
},
PolicyView: function (clientid, policyid) {
this.ViewClient(clientid);
var someView = new PolicyView();
MyApp.policyPane.show(someView);
},
PolicyEdit: function (clientid, policyid) {
this.ViewClient(clientid);
var someView = new PolicyEdit();
MyApp.policyPane.show(someView);
}
};
Having the "this.ViewClient" feels hacky and also doesn't work.
Multi-part answer, here...
"this.ViewClient is not a function"
this is a bug in Marionette. the controller method is called in the context of the router instead of the controller, so the call to this.ViewClient is trying to find it on the router.
oops.
bug logged. will fix asap. https://github.com/derickbailey/backbone.marionette/issues/38
--
UPDATE: this bug is now fixed in v0.5.1 of Backbone.Marionette https://github.com/derickbailey/backbone.marionette
--
to work around this issue for now, you can do this:
PolicyEdit: {
someController.ViewClient();
// ...
}
If that doesn't work, you may need to use Underscore.js' bind or bindAll methods to ensure the correct binding on your controller functions.
These workaround won't be necessary once i get the bug fixed... hopefully later today / tonight.
is basically calling other routes the best way to manipulate multiple regions?
The direct answer to this question is no.
But, you're not calling a route in this case. You're calling a method on your controller. That's perfectly fine - and actually, I would encourage this. It's a proper use of your object, and is one of the things that I think should be done instead of calling another route / router handler.
Routers And Controllers
A router is a feature, not an architectural requirement. Your app should work without a router, and a router should only add the ability to use bookmarks and the browser's forward/backward button.
With that philosophy in mind (which I know is controversial), using a controller like you have and calling multiple methods on your controller in order to get your application to the correct state, is one of the right approaches to take.
Look at it this way: if you removed the router from your app, you would be forced to call methods on your controller directly. To prevent duplication of code, you'll want to create many small methods on your controller that can do one thing very well, and then compose larger methods out of those smaller methods.
Hope that helps. :)