marionettejs / backbone.babysitter

Manage child views for your Backbone.View (and other parents)
Other
378 stars 65 forks source link

Just Storage? #2

Closed joezimjs closed 11 years ago

joezimjs commented 11 years ago

This doesn't render any views, correct? It's just a means of storage for views?

mxriverlynn commented 11 years ago

correct!

what you do with the views is entirely up to you. it just holds on to them, makes it easy to manage adding / finding / removing them, call methods on them, iterate through them, etc.

stuff that's pretty easy to do in general - just a consolidation of boilerplate code for doing it :)

isochronous commented 11 years ago

Seems like an initializer would be quite useful on this particular component. In fact, I know it would be, because I needed one, added it, and I'm very happy with the result. I'd submit a pull request but I figure it'd be easier for you to add it yourself since you've done it about 30 zillion times already.

mxriverlynn commented 11 years ago

what's the use case that you have for an initializer, @isochronous?

i don't see a need for it, personally. my use of the container is always internal to some other object that has a constructor/initializer of it's own, never as an object that i directly extend and use in my apps.

isochronous commented 11 years ago

I'm sorry, it's driving me crazy that markdown syntax doesn't work in comments posted via email, so see my comment four posts down for the original content.

mxriverlynn commented 11 years ago

@isochronous - is your intent for this just to provide some view instances in the constructor?

new ChildViewContainer(someViews)

if that's the case, I can modify the constructor to allow for this without the need for an initialize method.

i'll add "pluck" to the list of _ methods

mxriverlynn commented 11 years ago

the dev branch has this now, and has the pluck method

var views = [someView, anotherView];
var container = new Backbone.ChildViewContainer(views);

if that's good for what you need, i'll release this as v0.0.4

isochronous commented 11 years ago

That's perfect, actually - except I actually need to parse those views, because they're not instantiated at the point they're passed to the ChildViewContainer - it's a name (used as customId, a ViewType (not instance), and a ModelType/CollectionType (not instance) - hence the need for the initialize method.

On Mon, Dec 3, 2012 at 10:12 AM, Derick Bailey notifications@github.comwrote:

the dev branch has this now, and has the pluck method

var views = [someView, anotherView];var container = new Backbone.ChildViewContainer(views);

if that's good for what you need, i'll release this as v0.0.4

— Reply to this email directly or view it on GitHubhttps://github.com/marionettejs/backbone.babysitter/issues/2#issuecomment-10956706.

isochronous commented 11 years ago

This is the original content from four posts up

Well, for example, I have a set of modules (basically a package) that're designed to provide functionality for Wizard-type views. The thing is, we have several different types of contests, including sweeps (sweepstakes), ugc (user-generated content contest), and brackets (like an NCAA playoff tree). Each of these contest types might have several different steps. They all use the same layout for displaying the "frame" of the wizard, and that layout takes a ChildViewContainer as an options parameter, with that ChildViewContainer already populated with all the view instances necessary for that contest type.

So for each contest type, we have a different instance of ChildViewContainer. The way we define steps is like this:

define(
    ['backbone', 'babysitter', 'model.wizard.step',  './views/basicsettings', /*....(other views)...,*/ './views/publish'],

    function(Backbone, ChildViewContainer, StepModel, BasicSettingsView,  ... , PublishView) {
        var steps = [
            {name: "basicsettings", ModelType: StepModel, ViewType:  BasicSettingsView},
            // ... note that you could also/or define CollectionType instead of/in addition to ModelType
            {name: "publish", ModelType: StepModel, ViewType: PublishView}
        ];
    }
);

What I'd like to do is, after that steps variable is defined, do

return new ChildViewContainer(steps);

Actually, that's what I'm doing, because like I said I already added the initializer to my own fork of BabySitter (and added Marionette.extend, and added _.pluck to the list of methods borrowed from underscore).

So when the controller is instantiating that Wizard Layout, it uses the ContestType/ContestSubType to figure out which set of steps it's going to need, uses require() to load the appropriate ChildViewContainer on demand, and passes that as an option to the Layout constructor.

Also, I modified Backbone.Picky to work on views rather than models, simply by assuming they'd be contained in a ChildViewContainer. I've published that change to a new project called Backbone.PickySitter (catchy, right?) though none of the documentation has been updated yet [Update - it has now]. What I wound up doing was (since I added extend to BabySitter) extending ChildViewContainer as StepViewContainer to add an initialize() method that first makes the container a "SingleSelectable", then parses the steps passed to the constructor and adds them to the container. Each StepModel also makes itself selectable in its own initializer.

inside my initialize method:

var singleSelect = new Backbone.PickySitter.SingleSelect(this);
_.extend(this, singleSelect);

// Loop through `steps` object passed in
for (i=0, il=steps.length; i<il; i++) {
    step = steps[i];

    // Instantiate each view from `steps`, each with its own specified model/collection type (or both)
    view = new step.ViewType({
        collection: (step.CollectionType) ? new step.CollectionType() :
            null,
        model: (step.ModelType) ? new step.ModelType() : null
    });

    // Order might be used if you need to replace a specific step on-demand
    view.order = step.order || i;

    // Save the name of each step view as a property of that view.
    view.name = step.name;

    this.add(view, step.name);
}

StepViewContainer also adds some methods for finding the "next", "previous", "first", and "last" views.

As I told you before, we have a "Master" model (that's attached to the Wizard Layout) that, on instantiation, fetches the default values for the specified contest type/subtype, and shoves the whole response into a single model attribute called "defaults" (via the model's parse() method). I should mention again that we're using Backbone.DeepModel as the base class for all of our models, so we can easily set/access nested model properties. There is a second API call that returns the exact property "paths" in that defaults object that are handled by each step (the one call gets info about all steps), and when it returns, a parsing method loops through the steps, then the step's property list, retrieves that property from the master model, and sets it into the corresponding step model.

When a step view is selected in the ChildViewContainer, the view has a handler that calls fetch() on its model, which uses wreqr.requestresponse to ask for the current master model (I can't just give each model a reference to its parents because DeepModel will create a circular reference on serialization). First it checks to see if the property exists as a regular model attribute, and if it doesn't find it, then it simply prepends "defaults." to the start of the property name and looks again. Once it finds the value wherever it exists, it sets that value on itself (stripping off the "defaults." namespace if that's where the property was located). Finally, the view's modelbinder is called, which updates the appropriate UI elements with the values from the step's model. We have "dirty" tracking built-in, so if the input bound to any given model property is changed, that property is marked as "dirty".

When a step view is deselected, the view has a handler that calls save() on its model, which again gets the current master via RequestResponse, serializes itself, filtering out all non-dirty properties, and then calls masterModel.set(serializedData). Assuming this is the first time that model has been saved, this process creates new attributes on the master model (which, by default, only has the single "defaults" attribute).

When the final step is completed, and "finish" is clicked, the master model then saves itself back to the server, but does not include the defaults attribute as part of the return object. This lets us do a half-assed version of HTTP PATCH where we only send back the attributes that differ from the default settings.

A lot of the issues I've been having to deal with really stem from our API using JSONP instead of CORS, and it's not especially RESTful, so the way Backbone handles models has to be overhauled pretty drastically.

Anyway, that was probably a lot more info than you needed to answer that question, but I figured showing you how I'm using these components together might give you some ideas.

mxriverlynn commented 11 years ago

That's perfect, actually - except I actually need to parse those views, because they're not instantiated at the point they're passed to the ChildViewContainer - it's a name (used as customId, a ViewType (not instance), and a modelType/CollectionType (not instance) - hence the need for the initialize method.

it sounds like you need to build a factory method in to your code to handle your app's specific needs, honestly

isochronous commented 11 years ago

That's kind of why I wanted an initializer method - just that simple addition to the constructor makes the component a lot more flexible. But like I said, I've got my own fork where I've already added an initializer and mixed in Marionette.extend so I can easily sub-class the ChildViewContainer, so it's not too much of an issue. Thanks for considering my needs, though =)