Connecting to deeply nested attributes within an object using specified criteria

I apologize if the title of my query is not very descriptive, I couldn't come up with a better one. Please feel free to suggest improvements.

I am currently working on developing a reusable "property grid" in Angular. My goal is to create a grid where an object can be bound, but with the flexibility to customize its presentation.

Below is a snippet of the directive template (excluding the 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>

and here is the code for the directive:

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

An example usage of this directive would look like:

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

And here is the function getSitePropertyData() associated with it:

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;
};

The reason for using a "property data" function instead of directly binding to object properties is to have control over the order of properties, decide which ones should be displayed, and specify their types for presentation purposes.

Initially, I attempted passing values directly from the getSitePropertyData() function, but encountered synchronization issues between the object and the property grid. Subsequently, I resorted to using the key approach, where I could access properties like propertyObject[prop.key]. However, dealing with nested properties proved challenging as direct bindings do not support them.

I am currently stuck on how to proceed. Both proper bindings and deep property access are crucial requirements. While I know such functionality is achievable (evident in projects like UI Grid), deciphering their implementation would demand considerable time.

Am I on the right track, or is there a better way to approach this?

Answer №1

If you need to execute an arbitrary Angular expression on an object, the $parse service is designed for exactly that purpose (reference). This service can analyze an Angular expression and provide a getter and setter. To demonstrate this functionality, consider a simplified version of the formElement directive:

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);
        }
      });
    }
  };
}]);

A slight modification has been made to how the directive is implemented in order to maintain its meaning:

<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">

In this context, root-obj represents the top-level model, while path indicates the specific expression needed to access the data.

The use of $parse allows for the creation of getter and setter functions based on the provided expression and root object. By applying these accessor functions to the root object within the model.data property, the need for the entire Object.defineProperty setup is satisfied. While watches could also achieve this, they would introduce unnecessary complexity during digestion cycles.

You can test the functionality with this interactive demo: https://jsfiddle.net/zb6cfk6y/


Alternatively, a more concise and idiomatic approach to defining the getter/setter functions would be:

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

Answer №2

Incorporating the lodash library allows you to utilize the _.get function effectively for this task.

One approach is to save the _.get function in the controller of your property-grid and then employ it like so:

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

This can be utilized in your template. If the need arises for this feature across multiple sections within your application (not limited to just the property-grid), creating a custom filter would be advisable.


The downside to this method is the incapacity to bind the model in such a manner, thereby rendering the values uneditable. A solution can be found by utilizing the _.set function along with an object containing a getter and setter functions.

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

You can subsequently employ this function within the template:

<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>

For a simplified demonstration, refer to this Plunker.


If you are not utilizing lodash, a streamlined version of the _.get function is available, extracted from the lodash library.

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;
}

This function ensures avoidance of any

Cannot read property 'foo' of undefined
errors. It proves beneficial, especially when dealing with lengthy property chains where there may exist undefined values. When striving for usage of more intricate paths (e.g., foo.bar[0]), resorting to the complete _.get function from lodash is necessary.

Moreover, presented below is a simplified rendition of the _.set function, also derived from the lodash library:

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;
}

Note that these extracted functions overlook certain edge cases managed by the lodash library. Nevertheless, they should suffice for most scenarios.

Answer №3

When creating the lastSitePropertyData, consider constructing the object dynamically instead of hardcoding it

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

Later on, use functions to retrieve data as shown below

function getKey(prop){
        if(typeof prop.value === 'object'){
        return prop.value.key;  //you can run a loop or create a deep recursive method - it's up to you 
    }
    else return prop.key;
}
function getValue(prop){
        if(typeof prop === 'object'){
        return prop.value.value;   //you have to run a loop to get value from a deep recursive method - it's up to you 
    }
    else return prop.value;
}

This approach can be used in HTML using {{getKey(prop)}} and {{getValue(prop)}} For a working demo, please refer to this link - https://jsfiddle.net/718px9c2/4/

Note: This is just an idea for accessing JSON data more efficiently, Angular is not utilized in the demo.

Answer №4

One suggestion is to implement a solution like this. If you want to prevent cluttering object.proto (which is always advisable), consider moving this functionality to another module.

(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
    });


}());

Answer №5

My approach involves utilizing grids to display data, with each grid potentially showing different objects and columns. However, I do not tackle this all at once.

  1. I have a custom type service where I define types and set default configurations.
  2. There is a specific grid service that creates the grid definition options based on my specifications.
  3. In the controller, I initialize the grid by using the grid service and specifying column order and any necessary overrides for default configurations. The grid service then generates appropriate filtering and ordering configurations based on the field type definitions.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Tips for ensuring that the lightbox scrolling feature only affects the lightbox and not the entire page

Currently, I am utilizing the lightbox_me jQuery plugin to trigger a lightbox when a user clicks on a product. The issue I am facing is that the lightbox content often extends below the visible area, causing the entire page to scroll when using the right s ...

Angular Dynamic Alert Service

Is it possible to customize the text in an Angular Alert service dynamically? I'm looking to make certain words bold, add new lines, and center specific parts of the text. Specifically, I want the word "success" to be bold and centered, while the rest ...

JavaScript code returning the correct result, however, it is unable to capture all characters in the returned string

Currently, I am utilizing $.post to retrieve results from a database. The syntax I am using is as follows: $.post('addbundle_summary', {id:id}, function(resultsummary) { alert(resultsummary[0]); }) In CodeIgniter, within my model, I am retu ...

Grid layout with card tiles similar to Google Plus

I'm looking to develop a scrolling grid with card tiles similar to Google Plus that has three columns. I am currently using Material UI, but I can't seem to find the right functionality for this. I have experimented with the standard Material UI ...

Exploring the Ins and Outs of Debugging JavaScript in Visual Studio Using

I encountered a peculiar issue while testing some code. When the program is executed without any breakpoints, it runs smoothly. However, if I introduce a breakpoint, it halts at a certain point in the JSON data and does not allow me to single-step through ...

`How can I customize the appearance of individual selected <v-list-item> across various sub-groups?`

As a newcomer to Vuetify and Vue in general, I am struggling to solve a specific issue. I need to create multiple sub-groups where only one option can be selected within each "parent" list. Consider an array of cats: options:["Crookshanks", "Garfield", " ...

Storing Firestore Timestamp as a Map in the database

Snippet Below const start = new Date(this.date + 'T' + this.time); console.log(start); // Thu Sep 12 2019 04:00:00 GMT+0200 const tournament:Tournament = { start: firebase.firestore.Timestamp.fromDate(start) } When passing the tournament ...

Restricting Options in jQuery/ajax选择列表

Although there are similar questions posted, my specific issue is that I am unsure of where to place certain information. My goal is to restrict the number of items fetched from a list within the script provided below. The actual script functions correct ...

Extract table information from MuiDataTable

I am having trouble retrieving the row data from MuiDataTable. When I try to set the index from the onRowSelectionChange function to a state, it causes my checkbox animation to stop working. Below is how my options are currently configured: const option ...

How can scope binding be reversed in AngularJS when implementing transclusion?

Utilizing transclude in directives proves to be extremely beneficial when you want the scope of your controller to be linked to the HTML being passed in. But how can you achieve the opposite? (accessing the directive's scope from the controller) Her ...

"Classes can be successfully imported in a console environment, however, they encounter issues when

Running main.js in the console using node works perfectly fine for me. However, when I attempt to run it through a browser by implementing an HTML file, I do not see anything printed to the console. Interestingly, if I remove any mentions of Vector.ts fro ...

Application Layer Capability

I am currently in the process of developing an App that requires authentication. To ensure that the user is authenticated before changing routes and instantiating the route Controller, I need to implement a function that can retrieve the logged-in user fro ...

Creating a text node in a JavaScript application adds HTML content

Is there a way to append an HTML formatted block to an existing div using appendChild without the HTML code itself getting appended? Any tips on how to add HTML design instead of just code with appendChild and createTextNode? Check out this Fiddle <di ...

It appears that the home page of next.js is not appearing properly in the Storybook

Currently, I am in the process of setting up my next home page in storybooks for the first time. Following a tutorial, I successfully created my next-app and initialized storybooks. Now, I am stuck at importing my homepage into storybooks. To achieve this, ...

Having trouble with the page redirection issue? Here's how you can troubleshoot and resolve

My goal is to restrict access to both the user home and admin dashboard, allowing access only when logged in. Otherwise, I want to redirect users to the login or admin login page. import { NextResponse } from 'next/server' import { NextRequest } ...

Navigating in Angular is easy when you understand how to use special characters such as

In order to set a unique path in AngularJS routing, I am looking for the following features: An optional parameter The ability to accept values of a relative path Here are some examples of the parameter values I need to work with: /ABC /ABC/123 /ABC/12 ...

Can you explain the functionality of the const handleChange = (prop) => (event) => {} function and its significance?

While going through the documentation for MUI, I stumbled upon a new syntax for arrow functions in JavaScript that I haven't seen before. I tried to figure out how it works but couldn't find any information on it. The syntax looks like this: con ...

Is it possible for an onclick attribute to assign a function to document.getElementById without overwriting the existing value?

I want to create a numeric keypad in HTML with numbers 1-9, and I'm unsure if JavaScript can handle an onclick function for each key to show the number in the display div. What would be the most effective way to achieve this? <div id="display"> ...

Arranging information within an ExpansionPanelSummary component in React Material-UI

https://i.stack.imgur.com/J0UyZ.png It seems pretty clear from the image provided. I've got two ExpansionPanelSummary components and I want to shift the icons in the first one to the right, next to ExpandMoreIcon. I've been playing around with C ...

Showing C# File Content in JavaScript - A Step-by-Step Guide

Is there a way to showcase this File (c#) on a JavaScript site? return File(streams.First(), "application/octet-stream", Path.GetFileName(element.Path)); I am looking for something similar to this: <img id="myImg" src="_____&qu ...