0

I couldn't think of a great way to title this question, but essentially I'm wondering what the best solution is for evaluating expressions within Ng-Repeat. For example, if I have some code that looks like this that lists out my projects (assuming I have a form with ng-click associated with a function to add the contents to an array of projects, with an associated array of milestones for each project).

<h2>List of Projects!</h2>
<ul>
    <li ng-repeat="project in projects">
        <p>{{project.name}}</p>
        <li ng-repeat="milestone in project.milestones">
            <p>{{milestone.name}}</p>
            <p>{{milestone.dateAccomplished}}</p>
        </li>
    </li>
<ul>

Now here is where my problem lies. Since each milestone has a dateAccomplished associated with it, I want to create another list that chronologically prints off ALL of the milestones for all of the projects, with their associated project and date. My thought was to loop through all of the projects, get all their milestones, and use an angular filter by dateAccomplished (which I think would work), but where can I leverage ng-repeat in this scenario?

Essentially, I know how to solve this problem on paper, but I do not know how to solve it the Angular way. I can't figure out a way to loop through my projects (using ng-repeat) without creating extra HTML that I don't want to deal with. Am i over-thinking it? Should i call a function within the ng-repeat parameters that does some work for me, such as

<li ng-repeat="milestone in getAllMilestones()">

Thanks for your help.

Edit: JSON object for project looks like this:

project = {
    name: "some string",
    milestones: [{
        title: "some string",
        date: Date.now()
    ]}
}

Edit 2: JS-Fiddle: http://jsfiddle.net/HB7LU/1672/

NOTE: The fiddle is working how I want it to, thanks to imcg's comment; however, Nahn's answer seems to indicate that I should use a service and inject that service, so perhaps I'm still not quite there yet.

7
  • 2
    Yes I would create a new array '$scope.allMilestones' in the controller and use that in a simple ngRepeat Commented Jan 15, 2014 at 14:57
  • So you're saying you know how to do it, but would like to find an alternative method so as to avoid "excess" HTML? Commented Jan 15, 2014 at 14:57
  • Yeah essentially I could just create a method in the controller to handle all my problems, but I felt like that wasn't how an experienced Angular developer would do it. What imcg suggested was more what I was looking for. Commented Jan 15, 2014 at 15:02
  • I wasn't sure if there was a simpler way to nest ng-repeat statements without having to create extra variables in the controller was essentially my problem Commented Jan 15, 2014 at 15:02
  • 1
    @Chris - just so you know, every time Angular performs its digest to keep objects in sync, it's going to call that getAllMilestones() method. So whenever you type a letter in your "project" input, it's going to trigger a digest which re-evaluates getAllMilestones(). Since that function has nested loops, you could find yourself dealing with slow performance if your projects & milestones list grows. You should have your controller bind to an injected service's Projects and Milestones arrays, and the service will handle updating the array w/o re-creating from scratch each time. Commented Jan 15, 2014 at 16:41

3 Answers 3

2

The other solutions with binding ng-repeat to allMilestones() do work, but you have to keep in mind that the expression within ng-repeat is going to be evaluated every time there's a $digest cycle, which can happen anytime a model changes. If you evaluate the current version of your JSFiddle, you'll notice that the allMilestones() function is called several times for every letter you type in your Project Name input field. That in itself isn't so bad, but the function has nested loops - which could eventually lead to performance problems if you start having a large number of projects / milestones.

Even if all of that is acceptable, a cleaner solution is to encapsulate your logic into a Service, and have the Service do the work of maintaining a list of all milestones for you. This keeps your service and controllers very testable and maintainable.

See fiddle at http://jsfiddle.net/HB7LU/1676/

And the code is

myApp.service('ProjSvc', function () {
    var projects = [];
    var milestones = [];
    return {
        createProject: function (projectName) {
            var proj = {
                name: projectName,
                milestones: [{
                    title: "started project",
                    dateCreated: Date.now()
                }]
            };
            projects.push(proj);
            milestones.push(proj.milestones[0]);

        },
        projects: projects,
        allMilestones: milestones
    };
});

function MyCtrl($scope, ProjSvc) {

    $scope.currentProjects = ProjSvc.projects;
    $scope.allMilestones = ProjSvc.allMilestones;

    $scope.createProject = function () {
        ProjSvc.createProject($scope.newProjectName);
        $scope.newProjectName = "";
    };
}

You'd want to setup unit tests against both your ProjSvc and your MyCtrl to capture your requirements of maintaining a list of all milestones in addition to all projects.

In a more robust incarnation of this, you'd probably want to add some fields to your milestone so you can keep track of relationships (ie, milestone.projId).

Sign up to request clarification or add additional context in comments.

2 Comments

Hey Mike, thanks for taking the time to explain a cleaner solution. As a newcomer to Angular, these kind of answers go a long way. One last question: As you mentioned, in a more robust application, i'll probably want to add an id to each milestone.. do you recommend handling this relationship like a database? Each project will have an id and each milestone will have the same id as the project its associated with?
@Chris - well it really depends on what you're going to be doing with the data. Are you going to be saving the data to some back-end data store or is it always just going to live in local storage on the user's browser (keep in mind local storage can get wiped out)? Can users collaborate on projects/milestones? Do you need to provide reporting and analytics?
1

After re-reading the question a few times, I modified my answer. (hopefully I undestood correctly)

Here's the logic:

So, if you want to construct ANOTHER array that contains all the milestones, loop through all the projects and add each milestone to the new array.

Before adding it, and add extra information to each milestone object: the parent project name.

This will result in a "All milestones with project names" array. You can then do the ng-repeat on it and sort it by whatever you want.

This can be done straight-up in the controller, but the recomended way is to have the logic of "getting" the milestones in a service and exposing the function so that the controller can execute it.

$scope.allMilestonesWithProjectNames = InjectedService.getAllMilestones();

EDIT:

After providing the JFiddle, here's a rough solution that works:

1. Change $scope.allMilestones with the following:

    $scope.allMilestones = function(){

            var milestoneList = [];
            $scope.currentProjects.forEach(function(project) {
                project.milestones.forEach(function(milestone) {
                    milestone.parentproject= project;
                    console.log(milestone);
                    milestoneList.push(milestone);
                });
            });
            return milestoneList;
        };

I added "milestone.parentproject = project" above "milestoneList.push(milestone)"

2.Changed the ng-repeat to the following:

<li ng-repeat="milestone in allMilestones() | orderBy: '-dateCreated'">
                        <p>Milestone: {{milestone.title}}</p>
                        <p>Date Accomplished: {{milestone.dateCreated}}</p>
                        <p>{{milestone.parentproject.name}}</p>
                    </li>

Here I added the new field to display: milestone.parentproject,name

EDIT 2:

Mike Pugh nailed it when it comes to the structure you should pursue in your app.

Now, add the following to his code and you have a complete solution:

Inside the createProject function:

projects.push(proj);
proj.milestones[0].projectName = proj.name
milestones.push(proj.milestones[0]);

Notice that, in order to avoid circular references (if you prefer), just add the name of the parent object instead of the actual object to each milestone object.

Do this "object retrofitting" operation every time you add a project with milestones in it, or a milestone at a time, and it won't be a performance problem. You won't iterate through all projects anymore.

Of course, you still need to add | orderBy: '-dateCreated' and <p>Project Name {{milestone.projectName}}</p> to your ng-repeat.

A comment about adding a project id to a milestone: it may or may not be a good idea. Depends what you want. It's a performance overhead. When you display the name of the project for each milestone, you have to do a "getProjectById" query for each milestone. Its easy to just have the string on each object.

1 Comment

This is an excellent answer - thank you so much. It should be noted, however, that Mike Pugh's comment about binding the controller to an injected service is entirely correct from a performance standpoint.
0

Something in this fashion?

<ul>
    <li ng-repeat="project in projects | orderBy: 'dateAccomplished'">
        <li ng-repeat="milestone in project.milestones">
            <p>{{milestone.name}} - {{project.name}}</p>
            <p>{{milestone.dateAccomplished}}</p>
        </li>
    </li>
<ul>

2 Comments

This was my original thinking, but I believe this would order the milestone's by date accomplished only within their respective project, whereas I was trying to get the milestones ordered by date regardless of what project they were associated with.
can you put together a plunker or jsFiddle?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.