如何使用Ember.js动态添加和删除视图

时间:2013-12-03 20:12:13

标签: ember.js

我正在尝试创建一个用于遍历关系数据库中的表的接口。每个选择代表一列。如果列是外键,则向右添加新选择。这会持续发生在用户访问的每个外键上。选择的数量是动态的。

我制作了一个错误的实现,其中包含手动添加和删除选择视图的代码。我认为它可能会被更好的Ember代码替换(某些数组对象可能?),我只是不确定如何最好地使用框架来解决这个问题。

这是我的JSBin http://jsbin.com/olefUMAr/3/edit

HTML:

<!DOCTYPE html>
<html>
<head>
<meta name="description" content="Ember template" />
<meta charset=utf-8 />
<title>JS Bin</title>
  <script src="http://code.jquery.com/jquery-1.9.0.js"></script>
  <script src="http://builds.emberjs.com/handlebars-1.0.0.js"></script>
  <script src="http://builds.emberjs.com/tags/v1.1.2/ember.js"></script>
</head>
<body>
  <script type="text/x-handlebars" data-template-name="my_template">
    {{view fieldSelects}}
  </script>

  <div id="main"></div>
</body>
</html>

JavaScript的:

App = Ember.Application.create();

var TemplatedViewController = Ember.Object.extend({
    templateFunction: null,
    viewArgs: null,
    viewBaseClass: Ember.View,
    view: function () {
        var controller = this;
        var viewArgs = this.get('viewArgs') || {};
        var args = {
            template: controller.get('templateFunction'),
            controller: controller
        };
        args = $.extend(viewArgs, args);
        return this.get('viewBaseClass').extend(args);
    }.property('templateFunction', 'viewArgs'),
    appendView: function (selector) {
        this.get('view').create().appendTo(selector);
    },
    appendViewToBody: function () {
        this.get('view').create().append();
    }
});

var DATA = {};
DATA.model_data = {
  "Book": {
    "fields": [
      "id",
      "title",
      "publication_year",
      "authors"
    ],
    "meta": {
      "id": {},
      "title": {},
      "publication_year": {},
      "authors": {
        "model": "Author"
      }
    }
  },
  "Author": {
    "fields": [
      "id",
      "first_name",
      "last_name",
      "books"
    ],
    "meta": {
      "id": {},
      "first_name": {},
      "last_name": {},
      "books": {
        "model": "Book"
      }
    }
  }
};

var Controller = TemplatedViewController.extend({
    view: function () {
        var controller = this;
        return this.get('viewBaseClass').extend({
            controller: controller,
            templateName: 'my_template'
        });
    }.property(),
    selectedFields: null,
    fieldSelects: function () {
        var filter = this;
        return Ember.ContainerView.extend({
            controller: this,
            childViews: function () {
                var that = this;
                var selectedFields = filter.get('selectedFields');

                var ret = [];
                var model = 'Book';
                selectedFields.forEach(function (item, index, enumerable) {
                    var selection = item;
                    if (model) {
                        var select = that.makeSelect(model, that.getPositionIndex(), selection, true).create();
                        ret.pushObject(select);
                        model = DATA.model_data[model].meta[selection].model;
                    }
                });

                return ret;
            }.property(),
            nextPositionIndex: 0,
            incrementPositionIndex: function () {
                this.set('nextPositionIndex', this.get('nextPositionIndex') + 1);
            },
            getPositionIndex: function () {
                var index = this.get('nextPositionIndex');
                this.incrementPositionIndex();
                return index;
            },
            setNextPositionIndex: function (newValue) {
                this.set('nextPositionIndex', newValue+1);
            },
            makeSelect: function (modelName, positionIndex, selection, isInitializing) {
                var view = this;
                return Ember.Select.extend({
                    positionIndex: positionIndex,
                    controller: filter,
                    content: DATA.model_data[modelName].fields,
                    prompt: '---------',
                    selection: selection || null,
                    selectionChanged: function () {
                        var field = this.get('selection');

                        // Remove child views after this one
                        var lastIndex = view.get('length') - 1;
                        if (lastIndex > this.get('positionIndex')) {
                            view.removeAt(this.get('positionIndex')+1, lastIndex-this.get('positionIndex'));
                            view.setNextPositionIndex(this.get('positionIndex'));
                        }

                        if (! isInitializing && DATA.model_data[modelName].meta[field].model) {
                            var relatedModel = DATA.model_data[modelName].meta[field].model;
                            view.pushObject(view.makeSelect(relatedModel, view.getPositionIndex()).create());
                        }

                        // Reset ``isInitializing`` after the first run
                        if (isInitializing) {
                            isInitializing = false;
                        }

                        var selectedFields = [];
                        view.get('childViews').forEach(function (item, index, enumerable) {
                            var childView = item;
                            var selection = childView.get('selection');
                            selectedFields.pushObject(selection);
                        });
                        filter.set('selectedFields', selectedFields);
                    }.observes('selection')
                });
            }
        });
    }.property()
});
var controller = Controller.create({
    selectedFields: ['authors', 'first_name']
});

$(function () {
    controller.appendView('#main');
});

3 个答案:

答案 0 :(得分:5)

方法:

我会使用Ember Component来解决这个问题。 我使用了一个组件,因为它将是:

  • 轻松重复使用

  • 代码是自包含的,对您的任何其他代码没有外部要求。

  • 我们可以使用普通的javascript来创建视图。简单的javascript应该使代码流更容易理解(因为您不必知道Ember在幕后使用扩展对象做了什么),并且它将减少开销。

演示:

我在下面的代码中创建了此JSBin here

用法

添加到您的车把模板:

{{select-filter-box data=model selected=selected}}

创建select-filter-box代码,然后将您的模型绑定到data属性,将selected值数组绑定到selected属性。

申请表:

App = Ember.Application.create();

App.ApplicationController = Ember.ObjectController.extend({
  model: DATA.model_data,
  selected: ['Author','']
});

App.SelectFilterBoxComponent = Ember.Component.extend({

  template: Ember.Handlebars.compile(''), // Blank template
  data: null,
  lastCount: 0,
  selected: [],
  selectedChanged: function(){

    // Properties required to build view
    var p = this.getProperties("elementId", "data", "lastCount", "selected");

    // Used to gain context of controller in on selected changed event
    var controller = this;

    // Check there is at least one property. I.e. the base model.
    var length = p.selected.length;
    if(length > 1){

      var currentModelName = p.selected[0];
      var type = {};

      // This function will return an existing select box or create new
      var getOrCreate = function(idx){

        // Determine the id of the select box
        var id = p.elementId + "_" + idx;

        // Try get the select box if it exists
        var select = $("#" + id); 
        if(select.length === 0){

          // Create select box
          select = $("<select id='" + id +"'></select>");

          // Action to take if select is changed. State is made available through evt.data
          select.on("change", { controller: controller, index: idx }, function(evt){

            // Restore the state
            var controller = evt.data.controller;
            var index = evt.data.index;
            var selected = controller.get("selected");

            // The selected field
            var fieldName = $(this).val();

            // Update the selected
            selected = selected.slice(0, index);
            selected.push(fieldName);
            controller.set("selected", selected);
          });

          // Add it to the component container
          $("#" + p.elementId).append(select);
        }
        return select;
      };

      // Add the options to the select box
      var populate = function(select){
        // Only populate the select box if it doesn't have the correct model
        if(select.data("type")==currentModelName)
          return;

        // Clear any existing options
        select.html("");

        // Get the field from the model
        var fields = p.data[currentModelName].fields;

        // Add default empty option
         select.append($("<option value=''>------</option>"));

        // Add the fields to the select box
        for(var f = 0; f < fields.length; f++)
          select.append($("<option>" + fields[f] + "</option>"));

        // Set the model type on the select
        select.data("type", currentModelName);
      };

      var setModelNameFromFieldName = function(fieldName){

        // Get the field type from current model meta
        type = p.data[currentModelName].meta[fieldName];

        // Set the current model
        currentModelName = (type !== undefined && type.model !== undefined) ? type.model : null;
      };

      // Remove any unneeded select boxes. I.e. where the number of selects exceed the selected length
       if(p.lastCount > length)
        for(var i=length; i < p.lastCount; i++)
          $("#" + p.elementId + "_" + i).remove();

      this.set("lastCount", length);

      // Loop through all of the selected, to build view
      for(var s = 1; s < length; s++)
      { 
        // Get or Create select box at index s
        var select = getOrCreate(s);

        // Populate the model fields to the selectbox, if required
        populate(select);

        // Current selected
        var field = p.selected[s];

        // Ensure correct value is selected
        select.val(field);

        // Set the model for next iteration
        setModelNameFromFieldName(field);

        if(s === length - 1 && type !== undefined && type.model !== undefined)
        {
          p.selected.push('');
          this.notifyPropertyChange("selected");
        }
      }      
    }    
  }.observes("selected"),

  didInsertElement: function(){
    this.selectedChanged();
  }
});

如何运作

组件使用两个参数modelselected然后将观察者绑定到selected属性。无论是通过用户与选择框的交互,还是通过绑定到selected的属性更改选择,都将重新确定视图。

代码使用以下方法:

  • 确定选择数组(selected)是否大于1.(因为第一个值必须是基本模型)。

  • 从索引1开始循环所有选定的字段i

    1. 确定是否存在选择框i。如果没有创建选择框。
    2. 根据当前填充的模型确定选择框i是否具有正确的模型字段。如果是,则不执行任何操作,如果不填充字段。
    3. 设置选择框的当前值。
    4. 如果我们是最后一个选择框并且选择了字段链接到模型,则将空白值推到选择上,以触发下一个下拉菜单。

  • 创建选择框时,会挂起onchange处理程序,通过对当前索引右侧的selected数组进行切片并添加自己的值来更新selected值值。这将导致视图根据需要进行更改。

  • 属性count会跟踪之前selected的长度,因此如果对减少当前selected值长度的选择进行了更改,则可以删除不需要的选择框。

源代码已被评论,我希望很清楚,如果您对查询的工作方式有任何疑问,请随时提出,我会尝试更好地解释。

您的型号:

看过你的模型后,你考虑过将它简化到下面吗? 我感谢您可能无法超出问题范围的其他原因。只是一个想法。

DATA.model_data = {
  "Book": {
    "id": {},
    "title": {},
    "publication_year": {},
    "authors": { "model": "Author" }
  },
  "Author": {
    "id": {},
    "first_name": {},
    "last_name": {},
    "books": { "model": "Book" }
  }
};

因此,字段名称将从对象键读取,值将是元数据。


我希望你觉得这很有用。如果您有任何问题或疑问,请与我们联系。


控制器:

您可以使用此组件所需的任何控制器。在我的组件演示中,为了简单起见,我使用了Ember内置的ApplicationController

notifyPropertyChange()

的解释

这是因为当我们使用数组的selected功能将新字符串插入push数组时。

我使用了push方法,因为这是在现有数组中添加新条目的最有效方法。

虽然Ember确实有一个pushObject方法也应该处理通知,但是我无法接受它。所以this.notifyPropertyChange("selected");告诉Ember我们更新了数组。 但是我希望这不是一个破坏者。


Ember组件的替代方案 - 作为视图实现

如果您不希望以组件格式使用它,则可以将其实现为视图。它最终实现了相同的目标,但这对您来说可能是一种更熟悉的设计模式。

See this JSBin for implementation as a View我不会在这里包含完整的代码,因为其中一些与上面相同,你可以在JSBin中看到它

用法:

使用具有App.SelectFilterBoxViewdata属性的控制器创建selected的实例:

var myView = App.SelectFilterBoxView.create({
    controller: Ember.Object.create({
        data: DATA.model_data,
        selected: ['Author','']
    })
});

然后根据需要附加视图,例如#main

myView.appendTo("#main");

答案 1 :(得分:1)

不幸的是,即使在您的JSFiddle中添加Ember作为库之后,您的代码也无法运行,但ContainerView可能正是您所寻找的:http://emberjs.com/api/classes/Ember.ContainerView.html因为这些视图可以动态添加/除去。

答案 2 :(得分:0)

this.$().remove()this.$().append()可能是您正在寻找的内容:

Ember docs