【发布时间】: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>' +
' <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);
【问题讨论】: