Our SPA will also be using the responsive Twitter Bootstrap CSS3 styles.
The whole source code for this SPA can be found in the following GitHub repository:
https://github.com/CarmelSoftware/OrchidsSPA
We'll design a unique and single main HTML file (the SPA itself, loading 4 different HTML templates , each one of them corresponding to each one of the CRUD operations)
In this tutorial , we see how to create an AngularJS SPA and, if you want to insert it to an MVC application, just drag and drop the files inside some directory in your MVC.
This tutorial is a standalone, and allows you to design a SPA in 30 minutes, copying-pasting code blocks to your own app as you move forward learning. But if you wish to learn AngularJS from 0, you can take a look at the Tutorials for Absolute Beginners at The AngularJS Club starting at Lesson #1 .
This is a snapshot of the SPA AngularJS that we'll develop from scratch here, in 30 minutes :
How to Design an AngularJS SPA with CRUD operations for OData RESTful Web API in an MVC App
As you move forward through this Tutorial, you will find the source code to copy-paste to your project, beside every snapshot. That will allow you to go ahead faster. However, you can download the complete AngularJS SPA from this GitHub repository, packed all together in a single ZIP file:
https://github.com/CarmelSoftware/OrchidsSPA/archive/master.zip
If you already have an MVC application working, create a folder named "AngularJS", and keep using it as your root directory while creating all the files in this tutorial. We'll focus here in developing the SPA, therefore just copy it inside that folder in your MVC.
We'll use CDN(content delivery network), to add the references for javascripts and styles, instead of downloading the files to the project:
As you see, we add 2 AngularJS scripts, and 2 Bootstrap CSS3 files. Also, we create directories for "Content" and "Controllers".
(copy-paste the source code):
<!doctype html>
<html data-ng-app="OrchidsApp">
<head>
<title>AngularJS SPA App
</title>
<link href="Contents/Style.css" rel="stylesheet" />
<link rel="stylesheet" href="http://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" />
<link rel="stylesheet" href="http://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap-theme.min.css" />
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.7/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.7/angular-route.js"></script>
<script src="/App/Controllers/SPAControllers.js" type="text/javascript"></script>
</head>
Next, copy-paste the CSS3 style to your style.css (placed at the "Contents" folder inside your root directory) file, which we'll be using besides the Bootstrap:
body {background:rgba(255, 238, 238, 0.5);
}
img {width:99%;height:99%;
}
.select
{
width:100px;
padding:5px 5px 5px 25px;
margin:10px 15px 15px 25px;
font:900 12px Comic Sans MS;
opacity:0.9;
background:#f0f0f0;
border:5px solid #ddd;
border-radius: 10px;
box-shadow:10px 10px 2px #c0c0c0;
}
.centered
{
text-align:center;
}
.div-table{
display:table;
width:auto;
background-color:#eee;
border-spacing:5px;
}
.div-table-row{
display:table-row;
width:auto;
clear:both;
}
.div-cell-left{
float:left;
display:table-cell;
width:33%;
height:200px;
padding:5px 5px 5px 5px;
}
.div-cell-center{
float:left;
display:table-cell;
width:56%;
height:200px;
padding:5px 5px 5px 5px;
}
.div-cell-right{
float:left;
display:table-cell;
width:10%;
height:200px;
padding:5px 5px 5px 5px;
}
.msg {
font:900 Comic Sans MS;
color:#1b42ae;
}
We'll also add a NavBar.html file (again at the Contents folder) containing the Bootstrap NavsBar (for more about adding the Twitter Bootstrap see this Bootstrap Tutorial):
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="/">Orchids</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li class="active"><a href="/">Home <span class="sr-only">(current)</span></a></li>
<li><a href="#/">Orchids SPA</a></li>
<li><a href="/PDF">Create PDF</a></li>
<li><a href="/Home">Help</a></li>
<li><a href="/Home/About">About</a></li>
</ul>
<form class="navbar-form navbar-left" role="search">
<div class="form-group">
<input type="text" class="form-control" placeholder="Search">
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
<ul class="nav navbar-nav navbar-right">
<li><a href="/Home">Technologies</a></li>
</ul>
</div>
<!-- /.navbar-collapse -->
</div>
<!-- /.container-fluid -->
</nav>
As we said, we'll have a unique main HTML file (the SPA itself, and 4 different templates , for each one of the CRUD operations).At it, add a <div> element bound to an data-ng-view, and a data-ng-include to insert the NavBar HTML5 inside the web page:
<body class="container">This <div> element will each time be replaced by AngularJS with four Template Views, according to the User's selection ("#/" , "#/add" ,"#/edit" and "#/delete" ), that we're designing next: a List of All Items, a template for Creating a new Item, another one for Editing, and one for Deleting it .
<div data-ng-include="" src="'Contents/Navbar.html'" ></div>
<div class="jumbotron">
<h1>Orchids SPA - AngularJS App</h1>
</div>
<div id="container">
<div data-ng-view=""></div>
</div>
</body>
</html>
We'll first design the List of flowers, so create an "/App/Views/OrchidsList.html" file, and paste the following code inside it:
<div class="jumbotron" >
<h2>List of my Favorite Orchids</h2>
<h4 class="msg">{{Message}}</h4>
</div>
<div class="jumbotron">
<ul class="list-group">
<li data-ng-repeat="Orchid in OrchidsList" class="list-group-item">
<div class="div-table" >
<div class="div-table-row">
<div class="div-cell-left">
<img src="http://carmelwebapi.somee.com/AngularJS/Contents/Images/{{Orchid.MainPicture}}" alt="{{Orchid.Title}}" title="{{Orchid.Title}}" >
</div>
<div class="div-cell-center">
<span >{{Orchid.BlogID}} . {{Orchid.Title | uppercase}} <br /><br />
{{Orchid.Text}} {{Orchid.DatePosted | date }}
</span>
</div>
<div class="div-cell-right">
<a href="#/edit/{{Orchid.BlogID}}">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
</a>
</div>
</div>
</div>
</li>
</ul>
<div class="panel panel-default">
<div class="panel-body | centered">
<button data-ng-click="fnShowOrchids(-1)" class="btn btn-default btn-lg" ><span class="glyphicon glyphicon-hand-left" aria-hidden="true"></span></button>
<input type="number" data-ng-model="pageSize" max="4" min="1" value="2" class="select"/>
<button data-ng-click="fnShowOrchids(1)" class="btn btn-default btn-lg" ><span class="glyphicon glyphicon-hand-right" aria-hidden="true"></span></button>
</div>
</div>
<a href="#/add">Add your Favorite Flowers</a>
</div>
I've remarked the more relevant code in red. The two buttons at the bottom are for Pagination of the items forward and backwards, by sending to a Controller's method a parameter "+1" or "-1" to add to the current page index. The Input element "number" is for setting the size of the page while paging, so you could state 1 - 2 - 3 etc items per page.
The AngularJS data-ng-repeat Directive is for making a loop through all items in the Flowers collection.
For a deeper understanding of using AngularJS collections and the data-ng-repeat in this Article .
We also add two link buttons to load the "Edit" and the "Delete" HTML View Templates, using the Bootstrap's Glyphicons.
These links send the ID of the selected item to the Edit or the Delete Controllers' Methods at the AngularJS Module.
Next, we're going to code the AngularJS Module, the Routing which glues the HTML Views to its Controllers, and the Controllers themselves.
Therefore, add a javascript SPAControllers.js file inside a "/App/Controllers/" folder, with an AngularJS Module, as follows:
var oOrchidsApp = angular.module('OrchidsApp', ['ngRoute','ngResource']);
oOrchidsApp.config(['$routeProvider', function ($routeProvider) {
$routeProvider
.when('/',
{
templateUrl: "App/Views/OrchidsList.html",
controller: "OrchidsAllCtl"
})
.when('/add',
{
templateUrl: "App/Views/OrchidsAdd.html",
controller: "OrchidsAddCtl"
})
.when('/edit/:id',
{
templateUrl: "App/Views/OrchidsEdit.html",
controller: "OrchidsEditCtl"
})
.when('/delete/:id',
{
templateUrl: "/App/Views/OrchidsDelete.html",
controller: "OrchidsDeleteCtl"
})
.otherwise({ redirectTo: "/" });
}]);
Using an AngularJS $routeProvider , we bind each View with the correspondent HTML Template and Controller. Then, when the default "/" is required, the user will be faced with the "All Flowers" HTML template. IBut if the "/add" page is required, the "OrchidsAdd" template will be displayed.
The same for the "Edit" template.
This way, this is always the same HTML web page which is browsed here. This is a SPA application: everything is done in THE SAME url, with no reloads of the web page!!!
That's why we needed to reference the angular-route.js AngularJS javascript at the <head> of the main HTML file: to import the $routeProvider at the Module, enabling an SPA application.
We'll also need some Global variables, that is, some data that will be reused by the different Controllers all along our application, and we do not want to repeat several times all over the App:
oOrchidsApp.value('msg', { value: '' });
oOrchidsApp.factory('GlobalSvc', [function () {
var oFlowersPictures = ["haeckel_orchidae.jpg", "Bulbophyllum.jpg", "Cattleya.jpg", "Orchid Calypso.jpg", "Paphiopedilum_concolor.jpg", "Peristeria.jpg", "Phalaenopsis_amboinensis.jpg", "Sobralia.jpg"];
var sURLDev = 'http://localhost:21435/WebAPI/OrchidsWebAPI/';
var sURLProd = 'http://CARMELWEBAPI.SOMEE.COM/WebAPI/OrchidsWebAPI/';
var bIsDevelopmentTest = false;
var sURL = bIsDevelopmentTest ? sURLDev : sURLProd;
return {
getFlowers: function () { return oFlowersPictures; },
getURL: function () { return sURL; }
};
}]);
This "Value" variable will hold a global message which communicates between the different Templates.
That "GlobalSvc" object is an AngularJS Service created in an Factory, and holding the URLs used all over the SPA, and also the list of pictures to be displayed by the Select list that we'll insert in every View Template.
We set two URLs: one for testing purposes, and one for Deployment.
Next, we code an AngularJS Resource, that will allow us sending HTTP GET requests for some specific ID :
oOrchidsApp.factory('OrchidsResource', ['GlobalSvc', '$resource',function (GlobalSvc, $resource) {
return $resource(GlobalSvc.getURL() + ":id", { id: "@id" });
}]);
1 - Fetching All Items with HTTP GET:
Now we need the code for loading the View ALL Flowers Template, so add to this Controller to the Module :
oOrchidsApp.controller('OrchidsAllCtl', ['GlobalSvc', '$scope', '$http', '$log', 'msg', function (GlobalSvc, $scope, $http, $log, msg) {
$scope.angularClass = "angular";
$scope.OrchidsList = [];
$scope.pageSize = 2;
var iCurrentPage = -1;
$scope.fnShowOrchids = function (direction) {
iCurrentPage = iCurrentPage + direction;
iCurrentPage = iCurrentPage >= 0 ? iCurrentPage : 0;
var sURL = GlobalSvc.getURL() +
"?$skip=" +
iCurrentPage * $scope.pageSize
+ "&$top=" +
$scope.pageSize;
$http.get(sURL).success(function (response) {
$scope.OrchidsList = response;
$log.info("OK");
},function (err) { $log.error(err) })
$scope.Message = "";
}
$scope.fnShowOrchids(1);
$scope.Message = msg.value;
}
]);
At this Controller we use the Angular $http Service to send an HTTP GET request, using its get() and success() methods. We modify the URL from the "GlobalSvc" Service, and concatenate it according to the OData protocol, using $skip and $top for Paging, and finally send an HTTP GET request using the $http Service. The success() method is the callback function to be called when the response is 200 (OK). In this method , we populate the list collection with the data from the response.
For a deeper insight about sending HTTP GET Ajax requests , take a look at this article about the $http HTTP GET Service.
That "direction" argument, is for paging the results backwards ("-1") and forward ("1").
Using Angular Dependency Injection, we got three services : $scope, in order to get the scope variables; $http is for sending requests to the RESTful server; and finally, $log, for logging purposes.
What OData RESTful web service will you use to test your SPA?
If you don't have a web service of your own, you are invited to use the OData RESTful service that I developed and deployed for you at this URL:
http://carmelwebapi.somee.com/WebAPI/OrchidsWebAPI
You are invited to freely use it. An example of using this OData REST Web API, skipping 2 items and fetching the next 3 ones:
http://carmelwebapi.somee.com/WebAPI/OrchidsWebAPI/?$skip=2&$top=3
As we said before, the $http AngularJS service provide all kinds of HTTP functionality, including sending HTTP GET, POST, PATCH, PUT or DELETE requests. We will use both the HTTP GET, the HTTP POST , PATCH and DELETE verbs.
Why we use HTTP PATCH and not HTTP PUT for updating?
Because PUT requires ALL fields to be send to the service, while PATCH accepts partial updates.
Official documentation for the $http service can be learned from the Angular official web site:
2 - Adding a New Item with HTTP POST:
Now we're designing the AngularJS Template View for creating a new item into the Orchids collection.
Add an "Add" Template to the Views folder, that will contain the following markup:
<div class="container">
<div class="jumbotron">
<div class="" >
<h2>Add your Favorite Orchid</h2>
</div>
<form name="addOrchid" class=""
data-ng-submit="fnAdd()">
<input type="text" class="form-control"
placeholder="Title"
data-ng-model="Orchid.Title"
required>
<input type="text" class="form-control"
placeholder="Text"
data-ng-model="Orchid.Text"
required>
<select data-ng-model="Orchid.MainPicture" title="Select a Picture" data-ng-options="Img for Img in Flowers" class="form-control"></select>
<input type="submit" class="btn btn-default btn-lg"
value="Add"
data-ng-disabled="addOrchid.$invalid">
<span>{{fnShowMsg()}}</span>
</form>
<a href="#/">See All Flowers</a>
</div>
</div>
As you can see, we bind the Form markup to the properties of an "Orchid" object. The submit button will trigger the fnAdd() Controller method, which we'll code inside the .js file into a new Controller , aimed to add a new item :
oOrchidsApp.controller('OrchidsAddCtl',
['GlobalSvc', '$http', '$scope', '$location', '$log', 'msg',
function (GlobalSvc, $http, $scope, $location, $log, msg) {
msg.value = "";
$scope.Flowers = GlobalSvc.getFlowers();
$scope.fnAdd = function () {
var oFlower = { "Title": $scope.Orchid.Title, "Text": $scope.Orchid.Text,
"MainPicture": $scope.Orchid.MainPicture };
$http({
url: GlobalSvc.getURL(),
method: "POST",
data: oFlower,
headers: { 'Content-Type': 'application/json' }
}).success(function (data, status, headers, config) {
msg.value = "New Orchid saved";
$scope.IsSaved = true;
}).error(function (err) {
$log.error(err);
});
}
$scope.fnShowMsg = function () { return msg.value; }
}
]);
As before, we use the $http service to send a request, that in this case will be an HTTP POST, which will be interpreted by the REST web service as an adding request.
3 - Editing an Item with HTTP PATCH:
Next, we'll create the markup for the "Edit" View Template at our SPA. Add to the "Views" folder a new HTML file called "OrchidsEdit.html" and type the following markup:
<div class="container">Again as before, we bind all HTML5 elements to the "Orchid" object. Also, we have here a "select" element that allows the user to choose a picture for the post. This select will be loaded at a new Controller that you will append to the javascript Module as follows:
<div class="jumbotron">
<div class="" >
<h2>Edit your Favorite Orchid</h2>
</div>
<form name="editOrchid" class=""
data-ng-submit="fnEdit()">
<input type="text" class="form-control"
placeholder="Title"
data-ng-model="Orchid.Title"
required>
<input type="text" class="form-control"
placeholder="Text"
data-ng-model="Orchid.Text"
required>
<select data-ng-model="Orchid.MainPicture" title="Select a Picture" data-ng-options="Img for Img in Flowers" class="form-control"></select>
<input type="submit" class="btn btn-default btn-lg"
value="Update"
data-ng-disabled="editOrchid.$invalid">
<span>{{fnShowMsg()}}</span>
</form>
<a href="#/">See All Flowers</a>
</div>
</div>
oOrchidsApp.controller('OrchidsEditCtl',
['OrchidsResource', 'GlobalSvc', '$http', '$routeParams', '$scope', '$location', '$log', 'msg',
function (OrchidsResource, GlobalSvc, $http, $routeParams, $scope, $location, $log, msg) {
msg.value = "";
$scope.Flowers = GlobalSvc.getFlowers();
$scope.Orchid = OrchidsResource.get({ id: $routeParams.id });
$scope.fnEdit = function () {
var oFlower = { "BlogId": $routeParams.id , "Title": $scope.Orchid.Title,
"Text": $scope.Orchid.Text, "MainPicture": $scope.Orchid.MainPicture };
$http({
url: GlobalSvc.getURL() + $routeParams.id,
method: "PATCH",
data: oFlower,
headers: { 'Content-Type': 'application/json' }
}).success(function (data) { msg.value = "Orchid successfully updated"; }).error(function (err) { });
}
$scope.fnShowMsg = function () { return msg.value; }
}
]);
Also , the OrchidsResource's get() method is used to send an HTTP GET request and to get the specific data for the item that we're going to edit.
Then as before, we use the $http to send this time an HTTP PATCH request. If the response is successful ("success"), we output in the "msg" value some feedback to the user. Because this "msg" variable is a Global one, it will also allows us to show a message at the List View Template (remember: this "List" View has another different $scope at all, so a variable from this "Edit" Controller will not be seen at the "List" Controller).
Save and run your SPA:
Click the "Add" link to be shown the "Add" template, which will look this way:
Here you will create a new item. The list of pictures is shown at this drop down list:
Save a new flower, to test how it works:
If the web service is working properly, after a sec you should see the "New orchid saved" message .
Now, go back to the "See all flowers" template View , and remember, since this is a SPA , we're actually located at the SAME html web page.
At the List, we should see the new created item added to the collection. By clicking the "Edit" icon, we'll edit it:
Take a looka at the URL : it contains the ID of the item being edited. Proceed to make some editing, and click the "Update" button, thus sending an HTTP PATCH request to the OData REST service:
By now, we have added and edited product items. In case that you get no response from the web service, check at the Developer's Tools (pressing the F12) , at the "Network" tab, for the response status. If there is some error , take a look at this HTTP Error Tutorial.
If everything is OK, check for the edited item at the items List, and notice the "Msg" displayed there.
4 - Deleting an Item with HTTP DELETE:
Finally, we build the support for deleting an item:
We'll add the "Delete" functionality to our SPA application, by coding a new View template called OrchidsDelete.html as follows:
<div class="container">
<div class="jumbotron">
<div class="" >
<h2>Delete this Orchid</h2>
</div>
<form name="deleteOrchid" class=""
data-ng-submit="fnDelete()">
<input type="text" class="form-control"
placeholder="Title"
data-ng-model="Orchid.Title"
disabled>
<input type="text" class="form-control"
placeholder="Text"
data-ng-model="Orchid.Text"
disabled>
<input data-ng-model="Orchid.MainPicture"
class="form-control"
disabled/>
<input type="submit" class="btn btn-default btn-lg"
value="Delete"
data-ng-disabled="fnDisable()" ><span> {{fnShowMsg()}}</span>
</form>
<a href="#/">See All Flowers</a>
</div>
</div>
Notice that all fields are read-only on this View. We do not allow here to edit the item.
At the javascript file, we append a fourth Controller, for the delete functionality:
oOrchidsApp.controller('OrchidsDeleteCtl',['OrchidsResource', 'GlobalSvc', '$http', '$routeParams', '$scope', '$location', '$log', 'msg',function (OrchidsResource, GlobalSvc, $http, $routeParams, $scope, $location, $log, msg) {
msg.value = "";$scope.isDisabled = false;$scope.Orchid = OrchidsResource.get({ id: $routeParams.id });
$scope.fnDelete = function () {
$http({url: GlobalSvc.getURL() + $routeParams.id,method:"DELETE"
}).success(function (response) {msg.value = "Orchid successfully deleted";$scope.isDisabled = true;}).error(function (err) { $log.error(err); });}
$scope.fnDisable = function () { return $scope.isDisabled;}
$scope.fnShowMsg = function () { return msg.value; }
}]);
As you can see here, we call the AngularJS $resource to get the data for displaying the selected item. We do this by sending an HTTP GET request to the service, containing the ID of the item.
When the submit button is clicked, we send an HTTP DELETE request by using the Angular $http service inside the Controller .
The $routeParams service instantiated by Dependency Injection is for getting the ID of the item for deleting.
In addition, we add this methods: the fnDisable() one is to disable the submit button only in the case that the response has been successfully received. Therefore, the user cannot try to erase the same item twice.
And the fnShowMsg() method is to display a message to the user.
To check this functionality, go to the Main web page, and click over the Bootstrap's "delete" icon:
The Delete View will be displayed as follows:
Click on the "Delete" button, and wait some secs for the success message:
The item has been erased.
Try to remember using the $log functionality in your SPA, to send to yourself messages from your AngularJS app.
Enjoy AngularJS, and Happy Programming ....
by Carmel Schvartzman
כתב: כרמל שוורצמן
No comments:
Post a Comment