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

What's with the lack of acknowledgment when I triumph in rock, paper, scissors, lizard, spock?

My game is running smoothly, except for when lizard or spock are involved and I win. For some reason, the outcome does not display correctly, even though it works fine when I lose. I've double-checked for typos but couldn't find any. If someone c ...

The values are not being properly initialized in the componentDidMount() function

I've been refactoring my code lately and experimenting with using all the lifecycle methods available to me. In an attempt to initialize the state of a component using componentDidMount(), I've encountered some issues. Previously, I used this.pro ...

Footer div is being covered by the page

I am currently working on a website built with "WordPress", and I have implemented a mobile footer plugin that is designed to only appear on mobile devices. However, I am encountering an issue where the page content overlaps the footer on phones. I attemp ...

Rails MultiSelect Array not converting to String Correctly

I am having trouble converting my array to a string properly. I am using rails multiselect: Views: <%= f.select :foo, [ ['a'], ['b'], ['c'] ], {:prompt => "Select an alpha"}, {:multiple => true} %> Controller: ...

NodeJS/WebdriverJS: Error - 'Element is not connected to the webpage'

Hello there! I am new to WebDriver (chromedriver) and my knowledge of JavaScript syntax is quite basic, so please excuse me if my code appears messy. Currently, I have a clickAllElements(); function that identifies all elements within a class and performs ...

Adding date restrictions to the main method is necessary for controlling the input and output

I have a function in my react-native package that requires "from" and "to" dates as parameters. I need to implement a rule that ensures the "to" date is always after the "from" date. Where should I insert this requirement in the following code snippe ...

Converting a UUID from a string to an integer in JavaScript

Need help passing a UUID to a function that calls an endpoint to delete a blog. The UUID is in string format like "9ba354d1-2d4c-4265-aee1-54877f22312e" and I'm encountering a TypeError: Cannot create property 'message' on string '9ba35 ...

ReactJS Chatkit has not been initialized

I made some progress on a tutorial for creating an Instant Messenger application using React and Chatkit. The tutorial can be found in the link below: https://www.youtube.com/watch?v=6vcIW0CO07k However, I hit a roadblock around the 19-minute mark. In t ...

Assigning a value to a variable from a method in Vue: a step-by-step guide

I'm having trouble assigning values from a method to variables in HTML. Here's what I have in my code: <b-card-text>X position {{xpos}}</b-card-text> <b-card-text>Y position {{ypos}}</b-card-text> I would like to assign v ...

Can we import a CSS file selectively in Vue.js?

Can specific css-files be applied only when the current route has a "forAdmin" meta in my Vue.js project that includes both an admin-panel and client pages? ...

Learn the process of moving the focus to the next input type using AngularJS

There are four input text boxes for entering credit card numbers. I am looking to automate shifting focus to the next input field. I know how to do this using normal jQuery, but I want to achieve it using Angular. Can anyone offer some guidance? Thank you ...

Is it possible to obtain the output of a JavaScript file directly? (kind of like AJAX)

My previous experience with AJAX involved a server-side language like PHP generating xHTML with attached JS. The JS would then query another file using parameters set in either GET or POST. The output of the queried file would be returned to the JS, which ...

An error message 'module.js:557 throw err' appeared while executing npm command in the terminal

Every time I try to run npm in the terminal, I encounter this error message and it prevents me from using any npm commands. This issue is also affecting my ability to install programs that rely on nodejs. $ npm module.js:557 throw err; ^ Error: Cannot ...

Issues with AngularJS routing when accessing a website on a local port

I am currently developing a web app using AngularJS. I utilize ngRoute for routing and templating, as well as gulp-serve to run the website locally. However, every few days, the website suddenly stops working. Oddly enough, changing the local port resolves ...

Using a function to send multiple child data in Firebase

I am trying to figure out how to save data to a Firebase multi-child node structure: --Events ----Races -------Participants Below is some dummy data example that represents the type of data I need to store in Firebase: var dummyData = [ { ...

Achieving nested views functionality with various modules in AngularJS and ui-router

I am currently working on a complex AngularJS application with a large module that I am looking to break down into smaller, more manageable modules. Our main page consists of several ui-views for menu, footer, content, sidebar, etc. Each $stateProvider.st ...

Tap on the child to reveal their parent

I am working with a family tree that includes dropdown menus containing the names of parents and children. Each child has a link, and when I click on a child's link, I want their father to be displayed in the dropdown menu as the selected option. Can ...

Angular.js Error: SyntaxError - Unexpected token ":"

When using Angular.js to call my Web API, I encountered an error: P01:1 Uncaught SyntaxError: Unexpected token :. In the resources tab, it is indicated that these braces should not be there {"ProductID1":"P01","ProductName1":"Pen","Quantity1":10,"Price1": ...

Changing a String into an XML Document using JavaScript

Found this interesting code snippet while browsing the jQuery examples page for Ajax: var xmlDocument = [create xml document]; $.ajax({ url: "page.php", processData: false, data: xmlDocument, success: someFunction }); ...

Utilizing AMAZON_COGNITO_USER_POOLS in conjunction with apollo-client: A comprehensive guide

Struggling to populate my jwtToken with the latest @aws-amplify packages, facing some challenges. Encountering an error when attempting to run a Query: Uncaught (in promise) No current user It seems that when using auth type AMAZON_COGNITO_USER_POOLS, I ...