【问题标题】:Why do new instances of a Backbone.js model contain additional values to the predefined defaults?为什么 Backbone.js 模型的新实例包含预定义默认值的附加值?
【发布时间】:2012-04-03 13:51:54
【问题描述】:

我正在使用 Backbone.js 创建复合表单字段,其中包含电话号码以及电话号码的类别。有两个视图:一个呈现复合字段(请参阅 PhoneFieldView),而另一个呈现包含字段的字段集(请参阅 PhoneFieldSetView)。字段集视图还包含一个用于动态添加新电话字段的按钮。

当 PhoneFieldsetView 初始化时,它会从 DOM 中的对象(var window.Namecards.phone)中读取值。这是一个对象数组,其中包含用于填充初始化时设置的字段的值(例如,在重新加载表单时,如果电话字段验证失败,它将重新加载以前的值并添加一个错误 css 类)。一个示例 DOM 对象是:

window.Namecards.phone = [ 
  {  
    value: '1111111',
    type: 'work',
    cssClass: ['form-field-error']
  }, 
  {
    value: '222222',
    type: 'home',
    cssClass: ['form-field-error']
  }
]

问题是当每次调用调用PhoneFieldsetView时

var phoneField = new PhoneField(); 

导致实例看起来像这样:

phoneField = {
  cssClass: ["phone-number", "form-field-error", "form-field-error"],
  number: "111",
  selectTypeElementName: "phone[type]",
  textInputElementName: "phone[number]",
  type: "home"
}

问题在于 cssClass 属性。如您所见,它包含两个“form-field-error”字符串。事实证明,字符串“form-field-error”出现在数组 cssClass 中的次数等于 window.Namecards.phone 中的对象数。例如,如果 window.Namecards.phone 包含 5 个项目,则“form-field-error”将在每个 PhoneField 实例中出现 5 次。

根据我的理解,这让我感到困惑,当我打电话时

var phoneField = new PhoneField(); 

我应该只有 PhoneField 模型的默认值。那么为什么还要为 cssClass 添加这些附加值。

当我向表单添加一个新的空白字段时,也会发生同样的事情(请参阅 PhoneFieldsetView.addNewField())。当我创建 PhoneField 模型的新实例时,它还包含额外的“form-field-error”cssClass 值。

我猜这是范围的问题,但我找不到源。以下是完整代码。

(function($){

  $(document).ready(function() { 

    var PhoneField = Backbone.Model.extend({
      defaults: {
        textInputElementName: 'phone[number]',
        selectTypeElementName: 'phone[type]',
        number: '',
        type: 'work',
        cssClass: ['phone-number']
      }
    });

    var PhoneFields = Backbone.Collection.extend({
      model: PhoneField
    });

    var PhoneFieldView = Backbone.View.extend({
      tagName: 'div',
      events: {
        'click a.delete-phone-number': 'remove'
      },
      initialize: function() {
        _.bindAll(this, 'render', 'remove');
      },
      render: function(counter) {
        var inputCssClass = this.model.get('cssClass').join(' ');
        this.$el.html('<input id="phone-number-' + counter + '" type="text" name="' + this.model.get('textInputElementName') + '" value="' + this.model.get('number') + '" class="' + inputCssClass + '" autocomplete="off" />' +
            '<select  id="phone-type-' + counter + '" name="' + this.model.get('selectTypeElementName') + '" phone="phone-type">' +
            '  <option value="work">Work</option>' +
            '  <option value="home">Home</option>' +
            '  <option value="other">Other</option>' +
            '</select>' +
            '&nbsp;<a href="#" class="delete-phone-number">Delete</a>');
        // Select default option.
        this.$('select option[value="' + this.model.get('type') + '"]').attr('selected', 'selected');
        return this;
      },
      remove: function() {
        // Destroy the model associated with this view.
        this.model.destroy();
        // Remove this model's view from DOM.
        this.$el.remove();
      }
    });

    var PhoneFieldsetView = Backbone.View.extend({
      el: $('fieldset.phone'),
      events: {
        'click button#add-phone-button': 'addNewField'
      },
      initialize: function() {
        var self = this;
        _.bindAll(this, 'render', 'addField', 'addNewField', 'appendField', 'removeField');
        this.counter = 0;
        this.collection = new PhoneFields();
        // Create initial fields. The variable window.Namecards.phone is set 
        // by the server-side controller.  Note that this is added before binding 
        // the add event to the collection. This prevents the field being appended 
        // twice; once during initialization and once during rendering. 
        if (typeof window.Namecards.phone !== 'undefined') {
          _.each(window.Namecards.phone, function(item, index, list) {
            self.addField(item.value, item.type, item.cssClass);
          });
        }
        // Bind collection events to view.
        this.collection.bind('add', this.appendField);
        this.collection.bind('remove', this.removeField);
        // Render view.
        this.render();
      },
      render: function() {
        var self = this;
        this.$el.append('<legend>Phone</legend>');
        this.$el.append('<div id="phone-field"></div>');
        this.$el.append('<button type="button" id="add-phone-button">New</button>');
        _(this.collection.models).each(function(item){ // in case collection is not empty
          self.appendField(item);
        }, this);
      },
      // Add field containing predetermined number and type.
      addField: function(number, type, cssClass) {
        var phoneField = new PhoneField();
        console.log(phoneField.attributes);
        if (typeof number !== 'undefined') { 
          phoneField.set({
            number: number
          });
        };
        if (typeof type !== 'undefined') {
          phoneField.set({
            type: type
          });
        }
        if (typeof cssClass !== 'undefined' && cssClass.trim() !== '') {
          phoneField.get('cssClass').push(cssClass);
        }
        this.collection.add(phoneField);
      },
      // Add new empty field.
      addNewField: function() {
        var phoneField = new PhoneField();
        console.log(phoneField.attributes);
        this.collection.add(phoneField);
      },
      appendField: function(item) {
        // This appears to be what is binding the model to the view.  
        // In this case it would be binding PhoneField to PhoneFieldView.
        phoneFieldView = new PhoneFieldView({
          model: item
        });
        this.$('#phone-field').append(phoneFieldView.render(this.counter).el);
        this.counter++;
      },
      removeField: function(item) {
        // Create a new field if the last remaining field has been remove.  
        // This ensures that there will always be at least one field present.
        if (this.collection.length === 0) {
          this.addField();
        }
      }
    });

    // Create instance of PhoneFieldView.
    var phoneFieldsetView = new PhoneFieldsetView();

  });

})(jQuery);

【问题讨论】:

    标签: javascript backbone.js


    【解决方案1】:

    Backbone.js 模型适用于值类型,但我不了解引用类型,因为我非常怀疑它是为此而设计的。在行中

    phoneField.get('cssClass').push(cssClass);
    

    我有点想每个模型实例都从默认值中引用相同的数组,并且 cssClass 中的任何值都会被推送到该数组。然后,当您声明new PhoneField(); 时,您当然会得到一些奇怪的东西。作为一个快速的解决方案,我想我会将模型的 cssClass 属性更改为字符串(您将其视为序列化的数组)。应该不难做到,因为“。”不能用作 css 类,因此无论何时分配它,获取值并用“.”连接,当您需要使用 css 类时,用“。”分隔然后用 " " 加入。

    【讨论】:

    • 至于更通用的解决方案,我必须承认我不确定它可能是什么。我从来没有尝试过在字符串数组之类的东西上使用 Backbone 集合。
    • 感谢 JayC。我终于解决了。原来我错过了理解 new 运算符在 Javascript 中的工作原理。我认为在创建包含引用类型的对象的新实例时会创建引用类型的新实例(例如数组)。现在我意识到事实并非如此。
    【解决方案2】:

    了解问题后,我可以提出以下解决方案。诀窍是为“defaults”属性分配一个函数,该属性返回模型的默认值。这样做会为每个 PhoneField 实例创建一个单独的 Array ccsClass 实例。

    var PhoneField = Backbone.Model.extend({
      defaults: function() { 
        return {
          textInputElementName: 'phone[number]',
          selectTypeElementName: 'phone[type]',
          number: '',
          type: 'work',
          cssClass: ['phone-number']
        };
      }
    });
    

    【讨论】:

      【解决方案3】:

      基本上你是正确的,他们引用了同一个数组,但你应该能够重写它,以便每个人都有自己的数组副本,尝试在初始化(构造函数)函数中分配数组。

      你可能想看看这个问题

      Arrays in a Backbone.js Model are essentially static?

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-10-13
        • 1970-01-01
        • 2012-11-11
        • 2021-09-29
        相关资源
        最近更新 更多