<!DOCTYPE html>
<html ng-app="crudApp">

  <head>
    <script data-require="angular.js@1.3.15" data-semver="1.3.15" src="https://code.angularjs.org/1.3.15/angular.js"></script>
    <script data-require="angular-animate.js@1.3.15" data-semver="1.3.15" src="https://code.angularjs.org/1.3.15/angular-animate.js"></script>
    <script data-require="angular-mocks@1.3.15" data-semver="1.3.15" src="https://code.angularjs.org/1.3.15/angular-mocks.js"></script>
    <script data-require="angular-messages@1.3.15" data-semver="1.3.15" src="https://code.angularjs.org/1.3.15/angular-messages.js"></script>
    <script data-require="ui-router@*" data-semver="0.2.13" src="//rawgit.com/angular-ui/ui-router/0.2.13/release/angular-ui-router.js"></script>
    <script data-require="lodash.js@*" data-semver="3.5.0" src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.5.0/lodash.js"></script>
    <script data-require="ui-bootstrap@*" data-semver="0.12.1" src="https://angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.12.1.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/nprogress/0.1.6/nprogress.min.js"></script>
    <script src="app.js"></script>
    <script src="controllers.js"></script>
    <script src="directives.js"></script>
    <script src="services.js"></script>
    <script src="httpbackend.js"></script>
    <link rel="stylesheet" href="style.css" />
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/nprogress/0.1.6/nprogress.min.css" />
    <link data-require="bootstrap-css@*" data-semver="3.3.1" rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css" />
    <link data-require="animate.css@*" data-semver="3.2.0" rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/animate.css/3.2.0/animate.min.css" />
  </head>

  <body>
    <h1>CRUP App:</h1>
    <ul class="nav nav-tabs">
      <li ui-sref-active="active">
        <a ui-sref="app.list">Liste</a>
      </li>
      <li ui-sref-active="active">
        <a ui-sref="app.add">Ajout</a>
      </li>
    </ul>
    <div ui-view class="main"></div>
  </body>

</html>
var appServer = angular.module('myFakeServer', ['ngMockE2E']);

appServer.factory('MesRessourcesServeur', function() {
  var ressources = [
    {
      id: 0,
      title: 'Harry Potter',
      desc: 'Harry Potter, un jeune orphelin, est élevé par son oncle Vernon et sa tante Pétunia qui le détestent. Alors qu\'il était haut comme trois pommes, ces derniers lui ont raconté que ses parents étaient morts dans un accident de voiture.',
      link: 'http://www.allocine.fr/film/fichefilm_gen_cfilm=29276.html',
      isFirstClass: false
    },
    {
      id: 1,
      title: 'Le Seigneur des anneaux',
      desc: 'Les armées de Sauron ont attaqué Minas Tirith, la capitale de Gondor. Jamais ce royaume autrefois puissant n\'a eu autant besoin de son roi. Mais Aragorn trouvera-t-il en lui la volonté d\'accomplir sa destinée ?',
      link: 'http://www.allocine.fr/film/fichefilm_gen_cfilm=39187.html',
      isFirstClass: true
    },
    {
      id: 2,
      title: 'Le Loup de Wall Street',
      desc: 'L’argent. Le pouvoir. Les femmes. La drogue. Les tentations étaient là, à portée de main, et les autorités n’avaient aucune prise. Aux yeux de Jordan et de sa meute, la modestie était devenue complètement inutile. Trop n’était jamais assez…',
      link: 'http://www.allocine.fr/film/fichefilm_gen_cfilm=127524.html',
      isFirstClass: true
    }
  ];
  
  return {
    getList: function() {
      return ressources;
    },
    get: function(id) {
      return angular.copy( _.find(ressources, {id: Number(id)}) );
    },
    update: function(id, data) {
      var res = _.find(ressources, {id: Number(id)});
      res.title = data.title;
      res.desc = data.desc;
      res.link = data.link;
      res.isFirstClass = data.isFirstClass;
    },
    delete: function(id) {
      _.remove(ressources, {id: Number(id)});
    },
    add: function(data) {
      data.id = Math.floor(Math.random() * 10000);
      ressources.push(data);
      return data;
    }
  };
});

appServer.run(function($httpBackend, MesRessourcesServeur) {
    $httpBackend.whenGET('/ressources').respond(function(method, url, data) {
        var ressources = MesRessourcesServeur.getList();
        return [200, ressources, {}];
    });
    
    $httpBackend.whenGET(/\/ressources\/\d+/).respond(function(method, url, data) {
        var ressourceid = url.split('/')[2];
        var ressource = MesRessourcesServeur.get(ressourceid);
        return [200, ressource, {}];
    });

    $httpBackend.whenPOST('/ressources').respond(function(method, url, data) {
        var params = angular.fromJson(data);
        var ressource = MesRessourcesServeur.add(params);
        var ressourceid = ressource.id;
        return [201, ressource, { Location: '/ressources/' + ressourceid }];
    });

    $httpBackend.whenPUT(/\/ressources\/\d+/).respond(function(method, url, data) {
        var params = angular.fromJson(data);
        
        if(params.title.indexOf('bob') > -1) {
          return [400, ressource, { message: 'Interdit de mettre bob dans le titre!' }];
        }
        else {
          var ressourceid = url.split('/')[2];
          var ressource = MesRessourcesServeur.update(ressourceid, params);
          return [201, ressource, { Location: '/ressources/' + ressourceid }];
        }
    });
    
    $httpBackend.whenDELETE(/\/ressources\/\d+/).respond(function(method, url, data) {
        var ressourceid = url.split('/')[2];
        MesRessourcesServeur.delete(ressourceid);
        return [204, {}, {}];
    });    
    
    $httpBackend.whenGET(/templates\//).passThrough();
});

appServer.config(function($provide) {
    $provide.decorator('$httpBackend', function($delegate) {
        var proxy = function(method, url, data, callback, headers) {
            
            var duration = (url.indexOf('templates') > -1) ? 0 : 700;
          
            var interceptor = function() {
                var _this = this,
                    _arguments = arguments;
                setTimeout(function() {
                    callback.apply(_this, _arguments);
                }, duration);
            };
            return $delegate.call(this, method, url, data, interceptor, headers);
        };
        for(var key in $delegate) {
            proxy[key] = $delegate[key];
        }
        return proxy;
    });
})
<div>
  <h2>Ajouter une ressource</h2>
  
  <ressource-form btn-label="Ajouter" ressource="newRessource" validate="add(res)"></ressource-form>
</div>
<div>
  <h2>Modifier une ressource</h2>
  
  <ressource-form btn-label="Modifier" ressource="detailRessource" validate="update(res)"></ressource-form>
</div>
<div>
  <h2>Liste des ressources:</h2>
  
  <table class="table table-bordered">
    <thead>
      <tr>
        <th>titre</th>
        <th>description</th>
        <th>lien</th>
        <th>indisp.</th>
        <th></th>
      </tr>
    </thead>
    
    <tbody>
      <tr ng-repeat="res in ressources track by res.id" class="ressource-item">
        <td><a ui-sref="app.detail({id: res.id})">{{ res.title }}</a></td>
        <td>{{ res.desc }}</td>
        <td><a ng-href="{{ res.link }}">{{ res.link }}</a></td>
        <td>
          <span ng-class="{true: 'label-success', false: 'label-primary'}[res.isFirstClass]"
                class="label">{{ getLabelText(res.isFirstClass) }}</span>
        </td>
        <td>
          <button ng-click="remove(res)" class="btn btn-xs btn-danger">X</button>
        </td>
      </tr>
    </tbody>
  </table>
</div>
<form name="resForm" ng-submit="validate({res: ressource})" novalidate>
  <div class="form-group">
    <label>Titre</label>
    <input name="titre" class="form-control" ng-model="ressource.title" type="text" ng-minlength="4" required />
    
    <ng-messages for="resForm.titre.$error" ng-show="resForm.titre.$dirty">
      <p class="alert alert-danger" ng-message="required">Vous devez renseigner un titre!</p>
      <p class="alert alert-danger" ng-message="minlength">Le titre doit comporter au minimum 4 caractéres!</p>
    </ng-messages>
  </div>
  
  <div class="form-group">
    <label>Description</label>
    <textarea name="description" class="form-control" ng-model="ressource.desc" ng-minlength="20" required></textarea>
    
    <ng-messages for="resForm.description.$error" ng-show="resForm.description.$dirty">
      <p class="alert alert-danger" ng-message="required">Vous devez renseigner un description!</p>
      <p class="alert alert-danger" ng-message="minlength">Le description doit comporter au minimum 20 caractéres!</p>
    </ng-messages>
  </div>
  
  <div class="form-group">
    <label>Lien</label>
    <input name="lien" class="form-control" ng-model="ressource.link" type="url" required allocine-link />
    
    <ng-messages for="resForm.lien.$error" ng-show="resForm.lien.$dirty">
      <p class="alert alert-danger" ng-message="required">Vous devez renseigner un lien!</p>
      <p class="alert alert-danger" ng-message="url">Le lien doit comporter "http://"!</p>
      <p class="alert alert-danger" ng-message="allocine">Cela doit être un lien allociné!</p>
    </ng-messages>
  </div>
  
  <div class="checkbox">
    <label>
      <input name="important" ng-model="ressource.isFirstClass" type="checkbox"> Ressource de premier choix
    </label>
  </div>
  
  <button class="btn btn-primary" type="submit" ng-disabled="resForm.$invalid">{{ btnLabel }}</button>
</form>
var app = angular.module('crudApp');

app.factory('MesRessources', function($http, Loading) {
  return {
    getList: function() {
      Loading.show();
      return $http({
        method: 'GET',
        url: '/ressources'
      }).then(function(response) {
        return response.data;
      }).finally(function() {
        Loading.hide();
      });
    },
    
    get: function(id) {
      Loading.show();
      return $http({
        method: 'GET',
        url: '/ressources/' + id
      }).then(function(response) {
        return response.data;
      }).finally(function() {
        Loading.hide();
      });
    },
    
    update: function(id, data) {
      Loading.show();
      return $http({
        method: 'PUT',
        url: '/ressources/' + id,
        data: data
      }).finally(function() {
        Loading.hide();
      });
    },
    
    delete: function(id) {
      Loading.show();
      return $http({
        method: 'DELETE',
        url: '/ressources/' + id
      }).finally(function() {
        Loading.hide();
      });
    },
    
    add: function(data) {
      Loading.show();
      return $http({
        method: 'POST',
        url: '/ressources',
        data: data
      }).finally(function() {
        Loading.hide();
      });
    }
  };
});


app.factory('Loading', function($http) {
  return {
    nbLoadProgress: 0,
    
    show: function() {
        this.nbLoadProgress++;
        
        if(this.nbLoadProgress == 1) {
            NProgress.start();
        }
    },
    hide: function() {
        this.nbLoadProgress--;
        
        if(this.nbLoadProgress == 0) {
            NProgress.done();
        }
        else {
            NProgress.inc();
        }
    }
  };
});






var app = angular.module('crudApp');

app.directive('ressourceForm', function() {
  return {
    restrict: 'E',
    scope: {
      btnLabel: '@',
      ressource: '=',
      validate: '&'
    },
    templateUrl: 'templates/ressourceForm.tpl.html'
  };
});

app.directive("allocineLink", function() {
    return {
        restrict: "A",
        require: "ngModel",
         
        link: function(scope, element, attributes, ngModel) {
            ngModel.$validators.allocine = function(modelValue) {  
                return modelValue.indexOf('www.allocine.fr') > -1;
            }
        }
    };
});
var app = angular.module('crudApp', ['myFakeServer', 'ngAnimate', 'ngMessages', 'ui.bootstrap', 'ui.router']);

app.config(function($stateProvider, $urlRouterProvider) {
  $stateProvider
  .state('app', {
    url: '/',
    template: '<div ui-view class="mainContent"></div>'
  })
  .state('app.list', {
    url: 'list',
    controller: 'ListCtrl',
    templateUrl: 'templates/list.tpl.html',
    resolve: {
      ressources: function(MesRessources) {
        return MesRessources.getList();
      }
    }
  })
  .state('app.detail', {
    url: 'detail/:id',
    controller: 'DetailCtrl',
    templateUrl: 'templates/detail.tpl.html',
    resolve: {
      ressource: function($stateParams, MesRessources) {
        return MesRessources.get($stateParams.id);
      }
    }
  })
  .state('app.add', {
    url: 'add',
    controller: 'AddCtrl',
    templateUrl: 'templates/add.tpl.html'
  });
  
  $urlRouterProvider.otherwise('/list');
});
var app = angular.module('crudApp');

app.controller('ListCtrl', function($scope, $modal, MesRessources, ressources) {
  $scope.ressources = ressources;
  
  $scope.getLabelText = function(b) {
    return b ? 'OUI' : 'NON';
  };
    
  $scope.remove = function(item) {
    var modal = $modal.open({
      templateUrl: 'templates/modalConfirmation.tpl.html',
      controller: 'ModalConfirmationCtrl',
      size: 'lg',
      resolve: {
        ressource: function () {
          return item;
        }
      }
    });

    modal.result.then(function () {
      MesRessources.delete(item.id).then(function(response) {
        _.remove($scope.ressources, { 'id': item.id });
      });
    });
  };
});



app.controller('ModalConfirmationCtrl', function($scope, $modalInstance, ressource) {
  $scope.ressource = ressource;
  $scope.$modalInstance = $modalInstance;
});



app.controller('DetailCtrl', function($scope, $state, $stateParams, MesRessources, ressource) {
  $scope.detailRessource = ressource;
  
  $scope.update = function() {
    MesRessources.update($scope.detailRessource.id, $scope.detailRessource).then(function(response) {
      $state.go('app.list');
    });
  };
});



app.controller('AddCtrl', function($scope, $state, MesRessources) {
  $scope.newRessource = {
    title: '',
    desc: '',
    link: '',
    isFirstClass: false
  };
  
  $scope.add = function() {
    MesRessources.add($scope.newRessource).then(function(response) {
      $state.go('app.list');
    });
  };
});


# dfvbdfv

<div>
  <div class="modal-header">
    <h3 class="modal-title">Confirmation de suppression</h3>
  </div>
  <div class="modal-body">
    <p>Etes-vous sur de vouloir supprimer "{{ ressource.title }}" ?</p>
  </div>
  <div class="modal-footer">
    <button class="btn btn-warning" ng-click="$modalInstance.close()">Oui</button>
    <button class="btn btn-primary" ng-click="$modalInstance.dismiss()">Non</button>
  </div>
</div>

@mixin animation ($delay, $duration, $animation) {
    -webkit-animation-delay: $delay;
    -webkit-animation-duration: $duration;
    -webkit-animation-name: $animation;
    -webkit-animation-fill-mode: forwards; /* this prevents the animation from restarting! */
 
    -moz-animation-delay: $delay;
    -moz-animation-duration: $duration;
    -moz-animation-name: $animation;
    -moz-animation-fill-mode: forwards; /* this prevents the animation from restarting! */
 
    -o-animation-delay: $delay;
    -o-animation-duration: $duration;
    -o-animation-name: $animation;
    -o-animation-fill-mode: forwards; /* this prevents the animation from restarting! */
 
    animation-delay: $delay;
    animation-duration: $duration;
    animation-name: $animation;
    animation-fill-mode: forwards; /* this prevents the animation from restarting! */
}

body {
  padding: 10px;
}


.main {
  position: relative;
  
  .mainContent {
    position: absolute;
    width: 100%;
    padding: 20px;
    
    &.ng-enter {
      @include animation(0, .6s, fadeInLeft); // zoomInDown bounceInLeft
    }
    &.ng-leave {
      @include animation(0, .5s, fadeOutDown); // zoomOutDown bounceOutRight
    }
  }
}

.ressource-item {
  &.ng-leave {
    @include animation(0, .5s, zoomOut); // zoomOutDown bounceOutRight
  }
}


input,
textarea {
  
  &.ng-dirty {
    
    &.ng-valid {
      border-color: green;
    }
    &.ng-invalid {
      border-color: red;
    }
  }
}