1

When programming in ASP.NET MVC I became accustomed to using partial views for CRUD operations. And now that I'm working with Spring + AngularJS I was hoping I could work in a similar way. So far, with the use of angular-ui-router I've managed to accomplish some basic partialing for create/update.

Example:

layout.html

<!doctype html>
<html lang="en" ng-app="Example">
<head>
    <meta charset="utf-8">
    <title>Test</title> 
</head>
<body>
<div class="container">
    <nav class="row">
        <ul class="nav nav-pills">
            <li ui-sref-active="active"><a ui-sref="clients">Clients</a></li>
        </ul>           
    </nav>    
</div>
<div class="container">
    <div class="row">
        <div class="col-md-12" ui-view></div>
    </div>
</div>
<script src="libs/jquery/dist/jquery.min.js"></script>
<script src="libs/angular/angular.min.js"></script>
<script src="libs/angular-ui-router/release/angular-ui-router.min.js"></script>
<script src="appjs/app.js"></script>
<script src="appjs/services.js"></script>
<script src="appjs/controllers/ClientController.js"></script>
<script src="appjs/filters.js"></script>
<script src="appjs/directives.js"></script>
</body>
</html>

/client/create.html

<h1>Create new client</h1>
<form name="clientForm">    
    <div ui-view></div>
</form>

/client/edit.html

<h1>Edit client</h1>
<form name="clientForm" ng-show="!loading">
    <input type="hidden" name="id" ng-model="client.id" />  
    <div ui-view></div>
</form>

/client/_createOrEdit.html

<div class="form-group" ng-class="{'has-error': clientForm.name.$invalid}">

    <label for="name">Name:</label>
    <input id="name" 
    name="name" 
    type="text" 
    ng-model="client.name" 
    class="form-control" 
    required />

    <span class="help-block" ng-show="clientForm.name.$error.required">Name field is required</span>

</div>

<div class="form-group" ng-class="{'has-error': clientForm.address.$invalid}">

    <label for="address">Address:</label>
    <input id="address" 
    name="address" 
    type="text" 
    ng-model="client.address" 
    class="form-control" 
    required />

    <span class="help-block" ng-show="clientForm.address.$error.required">Address field is required</span>

</div>


<button class="btn btn-primary btn-lg btn-block" 
ng-click="addOrUpdate(client)" 
ng-disabled="clientForm.$invalid || clientForm.$pristine">Save</button>

app.js

App.config(function($stateProvider, $urlRouterProvider) {
    $urlRouterProvider.otherwise("/");

    $stateProvider
    /* Client */
    .state("clients", {
        url : "/clients",
        templateUrl : "clients/index",
        controller : ClientListController
    })
    .state("clientCreate", {
        abstract : true,
        url : "/client",
        templateUrl : "clients/create",
        controller : ClientCreateController
    })
    .state("clientCreate.createOrEdit", {
        url : "/create",
        templateUrl : "clients/createOrEdit",
    })
    .state("clientEdit", {
        abstract: true,
        url : "/client/{id:[0-9]{1,9}}",
        templateUrl : "clients/edit",
        controller : ClientEditController
    })
    .state("clientEdit.createOrEdit", {
        url : "/edit",
        templateUrl : "clients/createOrEdit",
    })

});

ClientController.js

var ClientListController = function($scope, $http) {
    $scope.error = false;
    $scope.loading = false;

    $scope.getClientList = function() {
        $scope.loading = true;
        $http.get('clients/clientlist.json')
        .success(function(clientList) {
            $scope.clients = clientList;
            $scope.loading = false;
        })
        .error(function() {
            $scope.error = true;
            $scope.loading = false;
        });
    }

    $scope.getClientList();
};

var ClientCreateController = function($scope, $http, $location) {
    $scope.client = {};
    $scope.loading = false;

    $scope.addOrUpdate = function(client) {
        client.id = null;
        $scope.loading = true;
        $http.post('clients/createOrEdit', client)
        .success(function() {
            $scope.loading = false;
            $location.path('/clients').replace(); //redirect to index 
        });
    };
};

var ClientEditController = function($scope, $stateParams, $http, $location) {
    $scope.client = {};
    $scope.error = false;
    $scope.loading = false;

    $scope.getClientById = function(id) {
        $scope.loading = true;
        $http.get('clients/' + id + '/client.json')
        .success(function(client) {
            $scope.client = client;
            $scope.loading = false;
        })
        .error(function() {
            $scope.error = true;
            $scope.loading = false;
        });
    }

    $scope.addOrUpdate = function(client) {
        $scope.loading = true;
        $http.post('clients/createOrEdit', client)
        .success(function() {
            $scope.loading = false;
            $location.path('/clients').replace(); //redirect to index
        });
    };

    var selectedId = $stateParams.id;
    $scope.getClientById(selectedId);
}

This is my current setup and in itself I'm quite happy with it. However when trying to expand this by adding a bit more complexity, I've gotten stuck.

Let's say that our clients can have multiple locations, so now each client has a list of locations.

What I'd like to do is make partial views for creating/editing locations and when editing or creating clients show them for each existing location the client has, the ability to remove them, and the ability to add new locations, all on the client create/edit page.

In ASP.NET MVC I'd be able to do this with partial views, BeginCollectionItem, and a bit of jQuery. How should I approach this with angular?

I tried using ng-repeat with ng-include inside, however when trying to add/remove locations nothing was reflected onto ng-repeat, and also I didn't like the approach.

I assume that the reason why nothing happened upon clicking the button has something to do with me receiving Async XHR deprecated warning in console upon clicking the button with insertLocation method.

insertLocation() looks like this

$scope.insertLocation = function() {
    $scope.client.locations.push({});
}

and placed inside create and edit controllers, and also the client var is edited to look like this

$scope.client = {locations: []};

Please note. I'm not exactly looking for a fix to this situation(although it would help), but more of an advice on my approach. Is it wrong? If it is what would be the proper mindset, proper approach?

MrPlow
  • 1,295
  • 3
  • 26
  • 45
  • I am a bit confused about the layout, why is there a CreateOREdit view? I get the point of having a create view for creating client, edit for editing, what does createoredit do? – Rohit Jul 24 '14 at 17:50
  • @Rohit Both create and edit forms have identical inputs with the only difference between the two being that edit also has a hidden input to store the id of the edited item. So createOrEdit contains form inputs which are common to both create and edit actions. It's purpose is to keep to the DRY principle as much as possible. – MrPlow Jul 24 '14 at 18:10
  • Do you mean repeating the second `form-group` 1 or more times for locations? (n locations + 1 empty for adding new location or something like that?) – craigb Jul 25 '14 at 06:29
  • @craigb A location is an object with 3 properties. I'd like to have a partial view with form inputs for each of these properties and 1. repeat it n times for each existing location the client has and fill it with respective data(along with a hidden id input), 2. On the press of a + button add the same partial without the hidden id input and with empty inputs. – MrPlow Jul 25 '14 at 08:35

2 Answers2

4

The n locations can be rendered using an ng-repeat with an extra form-group for adding a new location:

<div class="form-group" ng-repeat="location in client.locations">
    <label for="address">Address:</label>
    <input type="text" model="location.address"  />
    <a ng-click="remove(location)">remove</a>
</div>

<div class="form-group">
    <label for="address">Address:</label>
    <input type="text" model="newlocation.address"  />
    <a ng-click="addLocation()">add</a>
</div>

Where the controller would look something like this:

.controller('MyCtrl', function($scope, client) {

    $scope.client = client;
    $scope.newlocaiton = {
        address: null
    }

    $scope.remove = function(location) {
        // using underscore.js/lodash.js for remove
        _.remove($scope.client.locations, location);
    };

    $scope.addLocation = function() {
        // ignoring whatever you might need to do calling 
        // server to persist etc. Just adding to client
        $scope.client.locations.push($scope.newlocation);
    };
})

As to "partials" I'm not entirely clear what you want, but the Angular way of markup reuse is through directives like this:

.directive('LocationForm', function() {
    return {
        restrict: 'EA',
        scope: {
            location: '=',
            onRemove: '='
        },
        templateUrl: 'location.edit.html',
        link: function(scope, el, attr) {
            // omitted
        }
    }
})

location.edit.html

<label for="address">Address:</label>
<input type="text" model="location.address"  />
<a ng-click="remove(location)">remove</a>

and usage:

<div class="form-group" ng-repeat="location in client.locations">
    <location-form location="location" />
</div>

(Caveat, I didn't try this so the directive example is probably incomplete)

see:

Community
  • 1
  • 1
craigb
  • 16,827
  • 7
  • 51
  • 62
  • 1
    What I meant by partials is exactly what you figured. Reusable markup. In asp.net mvc they are called "partial views" and can be injected into html using @Html.Partial("viewlocation", bound_model), or returned by controllers for ajax calls. In my OP I consider _createOrEdit.html to be a partial view since it's injected both in create and edit htmls. Anyway I'll try to cook something up using your answer as a reference and see what I come up with and will hence update the OP. Oh and by the way, thanks for introducing me to lodash, looks like a really useful library. – MrPlow Jul 26 '14 at 17:51
  • I've managed to get what I've wanted using your example as a reference. Will update the OP soon with a simple example. Upvotes and bounty go to you. Thanks! – MrPlow Jul 27 '14 at 11:25
2

For completeness sake here's my solution: Plunker link.

To hopefully make things simpler I've replaced many-to-one relationship between client-location into a many-to-one relationship between an item and it's subitems.

So in order of completion:

  1. I've created a "partial" html (a "reusable" template so to speak) of the subitem inputs. It looks like this: _SubItem.html(I decided to reuse the ASP.NET MVC way of prefixing partials with an underscore)

    <div class="form-group">
      <label for="subinput1">subinput1</label>
      <input type="text" id="subinput1" ng-model="subitem.subinput1" class="form-control">
    </div>
    <div class="form-group">
      <label for="subinput2">subinput2</label>
      <input type="text" id="subinput2" ng-model="subitem.subinput2" class="form-control">
    </div>
    <div class="form-group">
      <label for="subinput3">subinput3</label>
      <input type="text" id="subinput3" ng-model="subitem.subinput3" class="form-control">
    </div>
    <button class="btn btn-danger btn-block" ng-click="removeSubItemAt($index)">Remove SubItem</button>
    
  2. Then I've created a directive out of it. Currently I'm quite unhappy with the directive since it doesn't use an isolated scope like in craigb's answer above. The reason it doesn't use an isolated scope is because I needed a reference to the $index of the ng-repeat and most importantly to the controller scope of the owning item controller so that I can use the removeSubItemAt(index) method. That in itself is a bad design on my part. Ideally I'd like to create a special controller linked to the directive itself, and have it take a reference to the subitem owner so that the directive controller has the necessary methods for manipulating it's own collection in the owner. OR just pass the index and remove function. However I'm currently not knowledgeable enough to pull something like that off. Here's how the directive currently looks:

    .directive('subItemPartial', function(){
        return {
          restrict: "E",
          templateUrl: "_SubItem.html"
        };
    });
    

    Looks simple. And here's how it's invoked in index.html

    <div class="col-xs-4" ng-repeat="subitem in item.subitems track by $index">
      <sub-item-partial></sub-item-partial>
    </div>
    

And that's basically it with the partial directive. Item controller contains methods for creating and removing subitems. Also you might notice that ng-repeat is tracked by $index. Reason for this is that I'd like to be able to create an n amount of subitems at once, fill them in and pass them to the server. Since ng-repeat uses objects themselves and not their index by default, it did not allow duplicate entries.

MrPlow
  • 1,295
  • 3
  • 26
  • 45