根据规则绑定到对象上的任意深度属性

时间:2016-06-09 09:08:37

标签: javascript angularjs

(对不起,如果我的问题标题不是很好,我就不会想到更好的问题。请随意提出更好的选择。)

我试图创建一个可重复使用的属性网格"在Angular中,可以将对象绑定到网格,但是这样可以在某种程度上定制对象的呈现。

这是指令模板的样子(form-element对我的问题并不重要,所以我将其排除在外:

<div ng-repeat="prop in propertyData({object: propertyObject})">
    <div ng-switch on="prop.type">
        <div ng-switch-when="text">
            <form-element type="text"
                          label-translation-key="{{prop.key}}"
                          label="{{prop.key}}"
                          name="{{prop.key}}"
                          model="propertyObject[prop.key]"
                          focus-events-enabled="false">
            </form-element>
        </div>
    </div>
</div>

和指令代码:

angular.module("app.shared").directive('propertyGrid', ['$log', function($log) {
    return {
        restrict: 'E',
        scope: {
            propertyObject: '=',
            propertyData: '&'
        }
        templateUrl: 'views/propertyGrid.html'
    };
}]);

以下是一个示例用法:

<property-grid edit-mode="true"
               property-object="selectedSite"
               property-data="getSitePropertyData(object)">
</property-grid>

与之相关的getSitePropertyData()函数:

var lastSite;
var lastSitePropertyData;
$scope.getSitePropertyData = function (site) {
    if (site == undefined) return null;

    if (site == lastSite)
        return lastSitePropertyData;

    lastSite = site;
    lastSitePropertyData = [
        {key:"SiteName", value:site.SiteName, editable: true, type:"text"},
        //{key:"Company.CompanyName", value:site.Company.CompanyName, editable: false, type:"text"},
        {key:"Address1", value:site.Address1, editable: true, type:"text"},
        {key:"Address2", value:site.Address2, editable: true, type:"text"},
        {key:"PostalCode", value:site.PostalCode, editable: true, type:"text"},
        {key:"City", value:site.City, editable: true, type:"text"},
        {key:"Country", value:site.Country, editable: true, type:"text"},
        {key:"ContactName", value:site.ContactName, editable: true, type:"text"},
        {key: "ContactEmail", value: site.ContactEmail, editable: true, type:"email"},
        {key: "ContactPhone", value: site.ContactPhone, editable: true, type:"text"},
        {key: "Info", value: site.Info, editable: true, type:"text"}
    ];
    return lastSitePropertyData;
};

我正在浏览这样一个&#34;属性数据&#34;函数,而不仅仅是直接绑定到对象的属性是我需要控制属性的顺序,以及它们是否应该显示给用户,以及它是什么类型的属性(文本,电子邮件) ,数量,日期等)为了介绍。

首先,正如您可以从value函数中的getSitePropertyData()属性残差所知,我首先尝试直接从此函数提供值,但这不会绑定到对象,因此在对象中进行更改或形成属性网格不会来回同步。接下来,我正在使用key这个想法,我可以这样做:propertyObject[prop.key] - 这对于直接属性非常有用,但正如您所看到的,我必须注释掉&#34;公司&#34;字段,因为它是属性的属性,而propertyObject["a.b"]不起作用。

我正努力想弄清楚在这做什么。我需要绑定才能工作,我需要能够在绑定中使用任意深度属性。我知道这种事情在理论上是可行的;我已经看到它在例如UI Grid中完成了,但是这样的项目有很多代码,我可能花了几天时间来了解它们是如何做到的。

我是否已接近,或者我是否认为这一切都错了?

5 个答案:

答案 0 :(得分:2)

您想在对象上运行任意Angular表达式。这正是$parseref)的目的。这个服务很好...解析一个Angular表达式并返回一个getter和setter。以下示例是formElement指令的过度简化实现,演示了$parse的使用:

app.directive('formElement', ['$parse', function($parse) {
  return {
    restrict: 'E',
    scope: {
      label: '@',
      name: '@',
      rootObj: '=',
      path: '@'
    },
    template:
      '<label>{{ label }}</label>' +
      '<input type="text" ng-model="data.model" />',
    link: function(scope) {
      var getModel = $parse(scope.path);
      var setModel = getModel.assign;
      scope.data = {};
      Object.defineProperty(scope.data, 'model', {
        get: function() {
          return getModel(scope.rootObj);
        },
        set: function(value) {
          setModel(scope.rootObj, value);
        }
      });
    }
  };
}]);

我稍微改变了指令的使用方式,希望不改变语义:

<form-element type="text"
    label-translation-key="{{prop.key}}"
    label="{{prop.key}}"
    name="{{prop.key}}"
    root-obj="propertyObject"
    path="{{prop.key}}"
    focus-events-enabled="false">

其中root-obj是模型的顶部,path是达到实际数据的表达式。

如您所见,$parse为任何根对象创建给定表达式的getter和setter函数。在model.data属性中,将$parse创建的访问器函数应用于根对象。整个Object.defineProperty构造可以被watch替换,但这只会增加摘要周期的开销。

这是一个工作小提琴:https://jsfiddle.net/zb6cfk6y/

顺便说一句,写入get / set的另一种(更简洁和惯用)方式是:

  Object.defineProperty(scope.data, 'model', {
    get: getModel.bind(null, scope.rootObj),
    set: setModel.bind(null, scope.rootObj)
  });

答案 1 :(得分:1)

如果您使用的是lodash,则可以使用_.get功能来实现此目的。

您可以将_.get存储在property-grid的控制器中,然后使用

model="get(propertyObject,prop.key)"

在你的模板中。如果您在应用程序的多个位置(而不仅仅是property-grid)中需要此功能,则可以为此编写filter

问题在于您无法以这种方式绑定模型,因此您无法编辑这些值。您可以使用_.set函数和带有getter和setter的对象来实现此功能。

vm.modelize = function(obj, path) {
    return {
      get value(){return _.get(obj, path)},
      set value(v){_.set(obj, path,v)}
    };
}

然后您可以在模板中使用该功能:

<div ng-repeat="prop in propertyData({object: propertyObject})">
  <input type="text"
          ng-model="ctrl.modelize(propertyObject,prop.key).value"
          ng-model-options="{ getterSetter: true }"></input>
</div>

有关简化示例,请参阅此Plunker

如果您不使用lodash,则可以使用我从_.get中提取的lodash函数的简化版本。

function getPath(object, path) {
  path = path.split('.')

  var index = 0
  var length = path.length;

  while (object != null && index < length) {
    object = object[path[index++]];
  }
  return (index && index == length) ? object : undefined;
}

此功能可确保您不会收到任何Cannot read property 'foo' of undefined错误。如果您有可能存在未定义值的长链属性,这将非常有用。如果您希望能够使用更高级的路径(例如foo.bar[0]),则必须使用_.get中的完整lodash功能。

以下是_.set的简化版本,也是lodash提取的形式:

function setPath(object, path, value) {
    path = path.split(".")

    var index = -1,
        length = path.length,
        lastIndex = length - 1,
        nested = object;

    while (nested != null && ++index < length) {
        var key = path[index]
        if (typeof nested === 'object') {
            var newValue = value;
            if (index != lastIndex) {
                var objValue = nested[key];
                newValue = objValue == null ?
                    ((typeof path[index + 1] === 'number') ? [] : {}) :
                    objValue;
            }


            if (!(hasOwnProperty.call(nested, key) && (nested[key] === value)) ||
                (value === undefined && !(key in nested))) {
                nested[key] = newValue;
            }


        }
        nested = nested[key];
    }
    return object;
}

请记住,这些提取的函数会忽略lodash处理的一些边缘情况。但它们应该在大多数情况下起作用。

答案 2 :(得分:0)

创建lastSitePropertyData时,您可以通过这种方式创建对象而不对其进行硬编码

 function createObject (){
     for(var key in site){
        lastSitePropertyData.push({key:key, value:site[key], editable: true, type:"text"});  
} }

后来使用函数来获取类似这样的数据

function getKey(prop){
        if(typeof prop.value === 'object'){
        return prop.value.key;  //can run loop create a  go deep reccursive method - thats upto u 
    }
    else return prop.key;
}
function getValue(prop){
        if(typeof prop === 'object'){
        return prop.value.value;   //have tp run loop get value from deep reccursive method - thats upto u 
    }
    else return prop.value;
}

可以在html {{getKey(prop)}}{{getValue(prop}}中使用 有关工作演示,请查看此链接 - https://jsfiddle.net/718px9c2/4/

注意:它只是想以更好的方式访问json数据,我没有在演示中使用angular。

答案 3 :(得分:0)

另一个想法就是像这样做。 如果你不想避免使object.proto脏(这总是好主意)只需将此功能移动到另一个模块。

(function () {

    'use strict';
    if (Object.hasOwnProperty('getDeep')) {
        console.error('object prototype already has prop function');
        return false;
    }

    function getDeep(propPath) {
        if (!propPath || typeof propPath === 'function') {
            return this;
        }

        var props = propPath.split('.');

        var result = this;
        props.forEach(function queryProp(propName) {
            result = result[propName];
        });

        return result;
    }

    Object.defineProperty(Object.prototype, 'getDeep', {
        value: getDeep,
        writable: true,
        configurable: true,
        enumerable: false
    });


}());

答案 4 :(得分:0)

我有类似的东西用于在网格中显示数据,这些网格可能显示相同的对象但没有相同的列。但是我不能一气呵成。

  1. 我有一个类型服务,我声明我的类型和一些默认配置
  2. 我有一个网格服务,它根据我指定的内容生成网格定义选项。
  3. 在控制器中,我使用网格服务实例化网格,指定列的排序和一些特定配置,这些配置覆盖默认配置。网格服务本身生成适当的过滤配置,使用字段的类型定义进行排序。