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

The comparison between StrictNullChecks and Union Types in terms of syntax usage

Understanding StrictNullChecks in TypeScript Traditionally, null and undefined have been valid first class type citizens in JavaScript. TypeScript formerly did not enforce this, meaning you couldn't specify a variable to potentially be null or unde ...

"By selecting the image, you can initiate the submission of the form

I am trying to figure out why clicking on the image in my form is triggering the form submission by default. Can someone please provide guidance on this issue? <form name="test1" action="er" method="post" onsubmit="return validateForm()" <input ty ...

Keep track of the user's email address as they complete the form

I currently use a Google Form to gather information from employees who work in remote locations Emp No * Punch * Customer details / mode or travel All the data collected is stored in a Google spreadsheet structured as follows: Timestamp Emp No Punch ...

Leveraging $http and $q in an Angular configuration with a service/provider

My goal is to load specific configurations for each controller in the app.config section. Each controller requires a distinct set of data, but these sets are not mutually exclusive. I am struggling to find a solution to this issue. .config(['$routePr ...

I am trying to understand why my React component's render function is being called twice - first without any data and then again with data, resulting in a

I am facing an issue with my TreeNav component, where the data is fetched from an API call. I have set up the reducer, action, and promise correctly, but when I try to map over the data in the component render function, I get an error saying "Uncaught Type ...

Issue: Unable to use the b.replace function as it is not recognized (first time encountering this error)

I can't seem to figure out what I'm doing wrong. It feels like such a silly mistake, and I was hoping someone could lend a hand in solving it. Here is my controller - I'm utilizing ng-file-upload, where Upload is sourced from: .controller( ...

Importing Vue and Vuex modules from separate files

Recently, I encountered an uncommon issue. I decided to keep my main.js and store.js files separate in a Vue project. In the store.js file, I imported Vuex, set up a new vuex store, and exported it. However, when importing this file into main.js and settin ...

Alert: A notification appears when executing Karma on grunt stating that 'The API interface has been updated'

While executing karma from a grunt task, I encountered the following warning: Running "karma:unit" (karma) task Warning: The api interface has changed. Please use server = new Server(config, [done]) server.start() instead. Use --force to continue. A ...

When utilizing webpack in Angular 5, the :host selector does not get converted into a component entity

Initially, I set up Angular with webpack following the configuration in this guide, including configuring webpack sass-loader. Everything was working smoothly until I encountered an issue today: app.component.ts @Component({ selector: 'ng-app&ap ...

The CSS navigation bar is not properly aligned in the center

This menu was constructed by me: JSBIN EDIT ; JSBIN DEMO Upon closer inspection, it appears that the menu is not centered in the middle of the bar; rather, it is centered higher up. My goal is to have it positioned lower, right in the middle. I ...

Error: an empty value cannot be treated as an object in this context when evaluating the "businesses" property

An error is occurring stating: "TypeError: null is not an object (evaluating 'son['businesses']')". The issue arose when I added ['businesses'][1]['name'] to 'son' variable. Initially, there was no error wi ...

"Attempt to create angular fork results in an unsuccessful build

I have recently created a fork of angularJs and I am facing an issue when trying to build it. The build process fails when running grunt package npm -v --> 3.5.2 bower --version --> 1.7.2 I followed the steps provided in the official documentation to bu ...

Error: Unable to encode data into JSON format encountered while using Firebase serverless functions

I am currently working on deploying an API for my application. However, when using the following code snippet, I encountered an unhandled error stating "Error: Data cannot be encoded in JSON." const functions = require("firebase-functions"); const axios = ...

How do I send a 404 error in Node JS Express when a third party API receives a bad request?

I've set up a Node JS server with a route handler that sends a request to a third-party API to retrieve a username: app.get('/players/:player', apiLimiter, function(request, response) { const player = request.params.player; const api_url = ...

Is it possible for ng-model to substitute for Ids?

Is it possible to create elements without using IDs, for example by utilizing ng-model for easier form field validation and other tasks? <div ng-repeat="x in TestData"> <input type="text" id="nameField-{{$index}}"/> </div> The code ...

Is Firebase the Answer to Shopify's Authentication Validation?

I've been following this tutorial on building a Shopify app using Node, Nextjs, and React. Progress has been smooth so far, but I've now reached a point where I need to store some of my app data in Firestore. My current approach involves utiliz ...

Using JQUERY to create a smooth sliding transition as images change

Currently, I have set up three images (1.jpg, 2.jpg, 3.jpg) along with an image using the code: <img id="add" src="1.jpg"></img>. The width, height, and position of this additional image are adjusted as needed and are functioning correctly. Fur ...

It appears that protractor-flake is programmed to re-run all tests instead of just the ones that have failed

Running tests using the latest version of "[email protected]", encountering failures during testing but the tests are rerunning again. No custom reporter used except Allure reporting. Below is the command used for running: protractor-flake --max-at ...

Retrieve the user_id without triggering any route

Is there a way to access the logged in user data without needing to make a request to any route or endpoint? //for example, how can I retrieve the id of the logged in user here? router.get('/',function(req,res,next){ //typically we would acce ...

Interacting between various components in separate files using React.js

Creating a page using React involves two Components with different functions. The first component, ProfileFill, is responsible for capturing form data. On the other hand, the second component, ProfileFillPercent, which resides in a separate file, calculate ...