Angular JS Array with N-level hierarchy

I am developing an application for questionnaires, where questions have responses that can lead to child questions with even more responses. This creates a hierarchical structure of multiple levels. I am looking for the best strategy to display this data in an HTML list. With the normal ng-repeat method, there is a limit to the number of levels that can be displayed. In this example, I have showcased a chain of 4 levels, but it could potentially go beyond that. Any comments or suggestions on how to efficiently handle and display this complex nested structure would be greatly appreciated.

var myApp = angular.module('myApp',[]);
myApp.controller('myCtrl',function ($scope){
$scope.questionnaire = [
    {
        QuestionID: 1,
        Description: "Question 1",
        Responses: [{
            RespDescription: "Response 1"
        },
        {
            RespDescription: "Response 2",
            ChildQuestions: [{
                QuestionID: 2,
                Description: "Child Question 2.1",
                Responses: [{
                    RespDescription: "Child Response 2.1.1"
                },
                {
                    RespDescription: "Child Response 2.1.2",
                    ChildQuestions: [{
                        QuestionID: 3,
                        Description: "Child Question 2.1.2.1",
                        Responses:[{
                            RespDescription: "Child Response..."
                            ChildQuestions:[{
                                QuestionID:4,
                                Description: "Other Child Question",
                                Responses:[{
                                    RespDescription: "Response..."
                                }]
                            }]
                        }]
                    }]
                }]
            }]
        }]
    }
];

})

Answer №1

Having worked on a similar questionnaire application with a structured approach, I utilized a back-end API that showcased a tree-like relationship structure.

It is essential to integrate this system into a back end rather than just writing it out manually, as it could lead to complications resembling a callback nightmare.

You can find the initial project setup on GitHub here. It leverages Loopback for data modeling and connects to an Angular front-end, but feel free to customize the back-end according to your preferences.

The core concept involves querying a primary question, which then branches out to multiple child answers, each linked to another question, forming a cascading effect. The emphasis lies on defining the relationships between different models.

This method allows you to develop a controller where selecting, say, answerC to questionA triggers a database query for the associated questionC object along with all its linked answers.

Subsequently, you can append the newly retrieved questionC and its answers to the main question array and navigate through them accordingly.

For illustration purposes, here's a simplified pseudo-code snippet:

//controller.js
app.controller('questionair', function(Answer, Question){
  //Load the first question along with its 3 related answers
  Question.findById({id: 1}, {include: 'answers'}).$promise
    .then(function(question){
      $scope.questions = [question];
    });

  //Function to fetch new question based on selected answer
  $scope.answerMe = function(questionId){
    Question.findById({id: questionId}, {include: 'answers'}).$promise
      .then(function(newQuestion){
        $scope.questions.push(newQuestion);
      },function(error){
        console.log('You\'ve answered the last question!');
      });
  };
});

//index.html
<div ng-repeat="question in questions">
   <h2>{{ question.text }}</h2>
   <ul>
     <li ng-repeat="answer in question.answers" 
         ng-click="answerMe(answer.questionId)">
       {{ answer.text }}
     </li>
   </ul>
</div>

Answer №2

As I was going through Mark Lagendijk's code in plunker, I discovered his solution for a task that involved recursion. By using a directive that calls itself, it is possible to represent a structure with multiple levels. The key component here is the service called RecursionHelper, which helps compile and avoid infinite loops in the directive. After adapting the code to suit my requirements, this was the result:

RecursionHelper

/* 
 * An Angular service which aids in creating recursive directives.
* @author Mark Lagendijk
* @license MIT
*/
angular.module('RecursionHelper', []).factory('RecursionHelper', ['$compile', function($compile){
return {
    /**
     * Manually compiles the element, fixing the recursion loop.
     * @param element
     * @param [link] A post-link function, or an object with function(s) registered via pre and post properties.
     * @returns An object containing the linking functions.
     */
    compile: function(element, link){
        // Normalize the link parameter
        if(angular.isFunction(link)){
            link = { post: link };
        }

        // Break the recursion loop by removing the contents
        var contents = element.contents().remove();
        var compiledContents;
        return {
            pre: (link && link.pre) ? link.pre : null,
            /**
             * Compiles and re-adds the contents
             */
            post: function(scope, element){
                // Compile the contents
                if(!compiledContents){
                    compiledContents = $compile(contents);
                }
                // Re-add the compiled contents to the element
                compiledContents(scope, function(clone){
                    element.append(clone);
                });

                // Call the post-linking function, if any
                if(link && link.post){
                    link.post.apply(null, arguments);
                }
            }
        };
    }
};
}]);

questionTree Directive :

directives.directive('questionTree', function (RecursionHelper) {
return {
    restrict: "AE",
    scope: {
        items: "=",
    },
    priority: 500,
    replace: true,
    //I use templateURL but for simplicity I used inline template in this code
    template: function (el, attr) {
        var itemType = attr["itemType"];

        if (itemType == "question") {
            return '<ul>'+
                     '<li ng-repeat="item in items">'+
                         '<div ng-click="loadChildResponses(item);$event.stopPropagation();">{{item.Description}}</div>'+
    '<question-tree items="item.Responses" item-type="reponse"></question-tree>'+
                     '</li>'+
                   '</ul>';

        }
        else {
            return '<ul>'+
                       '<li ng-repeat="item in items">'+
                           '<div ng-click="loadChildQuestions(item);$event.stopPropagation();">{{item.Description}}</div>'+
                           '<question-tree items="item.ModelWizardQuestions" item-type="question"></question-tree>'+
                        '</li>'+
                      '</ul>';

        }

    },
    controller: function ($scope, $http) {
        $scope.loadChildResponses = function (item) {



            $http.get(siteUrls.GetReponses + "?QuestionID=" + item.QuestionID)
           .success(function (data) {
               if (data && data.length > 0) {
                   item.Responses = data;
               }
           });
        };

        $scope.loadChildQuestions = function (item) {



            $http.get(siteUrls.getChildQuestions + "?ResponseID=" + item.ResponseID)
           .success(function (data) {
               if (data && data.length > 0) {
                   item.Questions = data;
               }
           });
        };
    },
    compile: function (element) {
        // Use the compile function from the RecursionHelper,
        // And return the linking function(s) which it returns
        return RecursionHelper.compile(element);
    }
}
});

By starting with loading the first level of questions and attaching the questionTree directive, the application can handle N levels of recursion.

The HTML:

 <ul>
   <li ng-repeat="question in Questions">{{question.Description}}
      <ul>
         <li ng-repeat="response in question.Responses"><span>{{response.Description}}</span>
           <question-tree items="response.Questions" item-type="question"></question-tree>
          </li>
       </ul>
    </li>
 </ul>

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

Invoking a plugin method in jQuery within a callback function

Utilizing a boilerplate plugin design, my code structure resembles this: ;(function ( $, window, document, undefined ) { var pluginName = "test", defaults = {}; function test( element, options ) { this.init(); } test.pro ...

Handle changes to input values on ng-click even if they were made outside of Angular's control

Looking to pass input values on ng-click even when they are changed outside of Angular. Everything works fine when manually typing, but once the inputs have dynamic values, ng-click passes empty form data. Below is the HTML code snippet: <form id="form ...

Tips for effectively managing the timeout of server-side AJAX calls1. Maxim

I'm currently developing server code in ASP.NET with the MVC framework. The client-side code is written in javascript. When sending an AJAX request from the browser to the server, whether using JQuery or not, timeouts can be set. Additionally, the br ...

Enhance the appearance of Ionic popups

Can someone help me with resizing a pop up? I've been struggling to get it right. This is the popup template in question: <ion-view> <ion-content scroll="false" class=""> test </ion-content> < ...

Is there a way to update the content of both spans within a button component that is styled using styled-components in React?

I am interested in creating a unique button component using HTML code like the following: <button class="button_57" role="button"> <span class="text">Button</span> <span>Alternate text</span> ...

Using Angular's ng-repeat directive to iterate over an array from JavaScript

I am attempting to display an array in the HTML HTML: <div class="personWrapper" ng-repeat="message in messages"> <p>{{message}}</p> </div> JS: var app = angular.module('matcherApp', [ "ngRoute", "ngStorage" ] ...

Error: The navigation property is not defined - React Native

Utilizing Firebase in combination with react-native for user authentication. The main file is App.js which directs users to the Login component and utilizes two components for managing routes: Appnavigator.js to create a switchNavigator and DrawerNavigator ...

Monitoring and refining console.error and console.log output in CloudWatch

I've encountered a situation where my lambda function includes both console.error and console.log statements, which Node.js interprets as stderr and stdout outputs respectively. However, upon viewing the logs in CloudWatch, I noticed that they appear ...

Utilizing adapter headers in contexts other than ActiveModelAdapter

I have successfully implemented my authorization system with Ember Data. All my ember-data calls are secure and signed correctly using adapter.ajax() instead of $.ajax. However, I am facing a situation where I need to utilize a third-party upload library t ...

Looking for a feature where users can easily update their profile using an interactive edit button

I'm currently working on a website project and one of the features I'm tackling is the user's profile page. My objective is to create an edit button within the page that allows the user to modify their name, username, email, and update their ...

Swapping out nodes for images using d3.js

Below is the code snippet I am currently executing http://jsfiddle.net/a7as6/14/ I have implemented the following code to convert a node to an image: node.append("svg:image") .attr("class", "circle") .attr("xlink:href", "https://github.com/favico ...

Vue Material - Effortlessly integrate native transitions within router views

In the project I'm currently working on, I have chosen to use Vue Material for the development of a single-page application. The approach I am taking follows a common trend in which a central "container" component is utilized to manage the shifting vi ...

Tips for selecting specific types from a list using generic types in TypeScript

Can anyone assist me in creating a function that retrieves all instances of a specified type from a list of candidates, each of which is derived from a shared parent class? For example, I attempted the following code: class A { p ...

Angular2 - Utilizing Promises within a conditional block

I'm currently facing an issue where I need to await a response from my server in order to determine if an email is already taken or not. However, I am struggling to achieve this synchronously. TypeScript is indicating that the function isCorrectEmail( ...

Creating a script to open multiple URLs in HTML and JavaScript

Currently, I am working on creating a multiple URL opener using HTML and JavaScript. However, I have encountered an issue where HTTP links are opening fine but HTTPS links are not. Can someone provide assistance with this problem? Below is the code snippet ...

Tips for incorporating asp.net MVC code into an AngularJS file

Can anyone explain how to transfer dynamic JavaScript values using Url.action()? I came across this thread on Stack Overflow and am curious if the same concept can be implemented in a separate JavaScript file, specifically within an AngularJS controller. ...

Using the JSON parameter in C# with MVC 3

I'm facing an issue with sending JSON data from a JavaScript function to a C# method using Ajax. When I receive the data in C#, it's not being recognized as JSON. How can I resolve this issue? If I try to output the received data using Response.W ...

Updating the value property of an object within a loop dynamically in React

At the moment, I am utilizing an array of objects called "mainArr" as shown below, I have implemented a loop inside a function to filter object properties, but I want to dynamically replace obj.name with obj."param" based on the parameter passed. Both nam ...

Can the .scroll function be turned off when a user clicks on an anchor link that causes the page to scroll?

I am currently developing a timeline page and I want to implement a feature similar to the chronological list of years displayed on the right side of this webpage: As part of this feature, I have set up a click event which adds a circle border around the ...

Can you please explain the significance of (session?._id)?

export default NextAuth({ ... callbacks: { session: async ({ session, token }) => { if (session?.user) { session.user.id = token.uid; } return session; }, jwt: async ({ user, token }) => { if (user) { ...