1
\$\begingroup\$

Using Backbone/Marionette, I'm building a simple widget that looks like:

enter image description here

I'm hoping to get some feedback on my approach because I think I may be doing it wrong.

jsfiddle

// widget/invites/person.js
var Person = Backbone.Model.extend({
    defaults: {
        name: ''
    }
});

// widget/invites/people.js
var People = Backbone.Collection.extend({
    model: Person
});

// widget/invites/personField.js
var PersonView = Marionette.ItemView.extend({
    template: _.template(...),
    hasSiblings: false,
    initialize: function(opts) {
        this.hasSiblings = opts.hasSiblings || false;
    },
    templateHelpers: function() {
        return {
            id: this.model.cid,
            showDel: this.hasSiblings
        }
    },
});

// widget/invites/form.js
var FormView = Marionette.CompositeView.extend({
    template: _.template(...),
    childView: PersonView,
    childViewOptions: function() {
        return {
            hasSiblings: (this.collection.length > 1)
        };
    },
    childViewContainer: '.values',
    ui: {
        'addBtn': '.add-name',
        'removeBtn': '.del-name',
        'wrap': '.values'
    },
    events: {
        'click @ui.addBtn': 'addFormRow',
        'click @ui.removeBtn': 'removeFormRow'
    },
    addFormRow: function(ev) {
        var name;
        ev.preventDefault();

        if (this.collection.length === 1) {
            // also rerender first input to include remove button
            name = this.ui.wrap.find('[name="person[]"]').get(0).value;
            this.collection.reset([{name: name}, {name: ''}]);
        }
        else {
            this.collection.add({name:''});
        }
    },
    removeFormRow: function(ev) {
        var id, model;
        ev.preventDefault();

        id = $(ev.currentTarget).data('id');
        model = this.collection.get(id);
        this.collection.remove(model);

        if (this.collection.length === 1) {
            // rerender first input so it doesn't have remove button
            name = this.ui.wrap.find('[name="person[]"]').get(0).value;
            this.collection.reset([{name: name}]);
        }
    }
});

Note, the list is saved by clicking a Submit button (not shown in mockup), as there will be other fields in the form as well.


The code works, but consider the following workflow:

  1. User enters "John Doee" in first input
    • Collection: ['']
  2. User clicks "add", and enters "Sally Something" in the second input
    • Collection: ['', '']
  3. User realized they spelled "John Doe" (two e's) and fixes it
    • Collection: ['', '']

or consider an existing list being edited:

  1. Collection: ['John Doe', 'Sally Something', 'Austin Powers']
  2. User removes "Austin Powers"
    • Collection: ['John Doe', 'Sally Something']
  3. User updates "John Doe" to "Dr. John Doe"
    • Collection: ['John Doe', 'Sally Something']

I'm using collection.add() and collection.remove() to trigger the view to update itself, but the collection doesn't always reflect the data on the screen. Is that okay?

Additionally, I did try adding a hook for blur which grabbed every value and reset the collection, but it seemed too brute force. Hence, my doubt about this approach.

Any thoughts or other approaches?

\$\endgroup\$
  • \$\begingroup\$ Are there missing values in your first example ? And I will your questions Is that okay by another question : If your collection to an external service and there is missing values would it be a problem ? I'm guessing it would be since you're not sending what the user see/think is in the collection. \$\endgroup\$ – Marc-Andre Dec 12 '14 at 18:27
  • \$\begingroup\$ "Are there missing values in your first example?": Yes and no. When the "add" button is clicked, an empty model is added to the collection to trigger the view to be updated. When a user changes a value, I'm not touching the model because the view doesn't need to be updated. \$\endgroup\$ – jbarreiros Dec 12 '14 at 18:45
  • \$\begingroup\$ "... is missing values would it be a problem?": Not in this context. My thinking is that when the form's submit button is clicked, I tell the view to grab all of the values, update the collection and then call save() to actually save the values to the DB. \$\endgroup\$ – jbarreiros Dec 12 '14 at 18:46
  • \$\begingroup\$ Ohh you didn't included the submit button in your jsfiddle or the ui preview here. If you're only saving on the submit button it's I think if you do not alway update your model. The thing is you will need to be sure that being out of sync would not cause some weird bug. \$\endgroup\$ – Marc-Andre Dec 12 '14 at 18:51
  • \$\begingroup\$ Thanks Marc-Andre. At the moment, the model isn't being used by anything else, so it shouldn't be a problem. I couldn't find similar examples online so I was just wondering if there were other, possibly better, approaches. \$\endgroup\$ – jbarreiros Dec 12 '14 at 19:56
0
\$\begingroup\$

Answering my own question. A View's Model represents "state", not data. This is a subtle distinction that wasn't apparent to me at first. The Collection should match the View, blank values and all.

When it comes time to save the list, the Collection's toJSON method can be overridden to omit any blank models.

I updated my original jsfiddle to show values being saved on blur.

var Person = Backbone.Model.extend();

var People = Backbone.Collection.extend({
  model: Person,
  toJSON: function() { /*filter out empty models*/}
});

var PersonView = Marionette.ItemView.extend({
  ...
  events: {
    'blur input': 'onChange', //calls model.set('name', input value)
  },
  triggers: {
    'click .del-name': 'remove:form:row' //calls collection.remove(...)
  },
  ...
});

var FormView = Marionette.CompositeView.extend({
  events: {
    'click @ui.addBtn': 'addFormRow' //calls collection.add({name: ''})
  },
  collectionEvents: {
    add: 'onRowAdded'  //rerender first row to show delete button
  },
  ...
});
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.