Originally posted on: http://geekswithblogs.net/alexhildyard/archive/2016/02/23/simple-generic-crud-with-angularjs-parse-and-bootstrap.aspx
Envisage almost any data-driven user interface, and sooner or later you will probably come across a requirement like:
- present the user with the list (of users, addresses, products, etc.)
- allow the user to edit an existing item in the list
- allow the user to create a new item and add it to the list
I decided to have a look at several web-based technologies to see how easy it might be to fulfil this requirement in as generic and extensible a way as possible, with the absolute minimal in the way of coding or configuration. I ended up opting for:
- angularJS for a data-bound UI
- Bootstrap for icons and element styling
- Parse for remote object definition, serialisation and persistence
My implementation involves:
- creating an angularJS Parse-based persistence service, which serialises the objects within a named Parse table
- creating a generic angularJS controller to present data supplied by the service and respond to user events
In order to use it, you just need to:
1. Subclass the service, specifying the name of the table you want bound to the UI
The following code creates an angular service bound to a Parse table called "Users":
angular.module('crudApp').service('UserPersistenceService', function () {
return new PersistenceService("Users");
});
At the same time, you should create a table named "Users" within Parse, with columns for any information you want to bind. In this example, I created the fields "name", "bio" and "score." But I am electing just to bind the "name" and "bio" elements.
2. Subclass the controller, and add hooks to define application behaviour when items are added or edited
The controller exploits "backing fields" to store partially edited information, as well as supply default values when a new
record is created. These operate as follows:
- initBackingFields(): called when a new record is created
- copyBackingFields(): called when you choose to edit a record
- commitBackingFields(): called when you have edited a record and elect to commit your changes
So in the example above, we have just two editable fields, "name" and "bio", which we will back with two further fields,
"newName" and "newBio", reflecting information that has been edited but not yet committed. The full implementation of the subclassed controller looks like this:
angular.module('crudApp').controller('UserListCtrl', ['$scope', '$controller', 'UserPersistenceService', function ($scope,
$controller, persistenceService) {
$controller('PersistenceController', {
$scope: $scope,
persistenceService: persistenceService,
initBackingFields: function (item) {
item.name = "";
item.newName = item.name;
item.bio = "";
},
copyBackingFields: function (item) {
item.newName = item.attributes.name;
item.newBio = item.attributes.bio;
},
commitBackingFields: function (item) {
item.set("name", item.newName);
item.set("bio", item.newBio);
}
});
} ]);
3. Bind the fields that interest you to the UI with appropriate directives
Create an HTML page to display the items in the table in a list. The Persistence controller exposes three helper functions to facilitate record editing:
- toggleItemEdit(): switch a record in the UI between "editable" and "read only"
- deleteItem(): delete the currently selected item, and persist changes to the back end
- commitItemEdit(): update an existing or add a new record, and persist the changes
The resultant HTML page now looks like this:
<html>
<head>
<link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
<link rel="Stylesheet" href="./styles.css" />
<script src="http://www.parsecdn.com/js/parse-1.6.7.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular.min.js"></script>
<script src="./crudController.js"></script>
<title>CRUD Test</title>
</head>
<body ng-app="crudApp">
<!-- item controller -->
<div class="userContainer" ng-controller="UserListCtrl">
<h3>Users</h3>
<table class="table table-striped">
<thead><tr>
<th>Name</th>
<th>Bio</th>
</tr></thead>
<tbody>
<tr ng-repeat="item in items">
<td>
<span ng-show="!item.isEditing">{{item.attributes.name}}</span>
<input ng-model="item.newName" ng-show="item.isEditing" type="text"/>
</td>
<td>
<span ng-show="!item.isEditing">{{item.attributes.bio}}</span>
<input ng-model="item.newBio" ng-show="item.isEditing" type="text"/>
</td>
<td>
<button ng-hide="item.isEditing" class="btn" ng-click="toggleItemEdit(item)">
<span class="glyphicon glyphicon-pencil"></span> Edit
</button>
<button ng-hide="item.isEditing" class="btn" ng-click="deleteItem(item)">
<span class="glyphicon glyphicon-remove"></span>Delete
</button>
<span ng-show="item.isEditing">
<button ng-disabled="item.newName==''" class="btn" ng-click="commitItemEdit(item)">
<span class="glyphicon glyphicon-pencil"></span>Update
</button>
<button class="btn" ng-click="toggleItemEdit(item)">
<span class="glyphicon glyphicon-pencil"></span>Cancel
</button>
</span>
</td>
</tr>
</tbody>
</table>
<table>
<tr>
<td>
<button class="btn btn-success" ng-click="addNewItem()">
<span class="glyphicon glyphicon-item"></span>New User
</button>
</td>
</tr>
</table>
<hr>
</div>
</body>
</html>
Finally, here is the Javascript file that does all the work, "crudController.js" and the accompanying "styles.css", which adds some spacing to the table elements.
-- styles.css --
input
{
width: 200px;
}
td
{
width: 200px;
}
-- crudController.js --
var app = angular.module('crudApp', []);
function PersistenceService(parseTable) {
var objParse = Parse.initialize("YOUR PARSE ADMIN KEY", "YOUR PARSE APP KEY");
var items = Parse.Object.extend(parseTable);
var queryItems = new Parse.Query(items);
// Store the type of the persistence service created
this.persistenceTable = parseTable;
// Load a list of items from the back end
this.load = function (itemsLoadedCallback) {
queryItems.find(
{
success: function (results) {
itemsLoadedCallback(results);
},
error: function (results) {
alert("Failed to load items: " + results.message)
}
});
}
// Add a new or update an existing item
this.update = function (item, itemUpdateCallback) {
item.save(
{
success: function (results) {
itemUpdateCallback(results);
},
error: function (results) {
alert("Failed to update item: " + results.message)
}
});
}
// Delete an item
this.remove = function (item, itemDeletedCallback) {
item.destroy(
{
success: function (results) {
itemDeletedCallback(results);
},
error: function (results) {
alert("Failed to delete item: " + results.message)
}
});
}
}
angular.module('crudApp').controller('PersistenceController', ['$scope', 'persistenceService', 'initBackingFields', 'copyBackingFields', 'commitBackingFields', function ($scope, persistenceService, initBackingFields, copyBackingFields, commitBackingFields) {
// Callback to receive list of users, and bind them to the scope
$scope.itemsLoaded = function (items) {
// Set the isEditing and newName attributes on each user; these aren't persisted
// but the controller uses them to facilitate editing
for (i = 0; i < items.length; i++) {
// We add these non-persisted fields to facilitate editing; "parse" won't serialise them
copyBackingFields(items[i]);
items[i].isEditing = false;
}
$scope.items = items;
// Force scope update, to rebind updated item list to the view
$scope.$apply();
}
// Called by the service when an item has been deleted, added or updated
$scope.OnDatabaseChanged = function (items) {
// Refresh the view
persistenceService.load($scope.itemsLoaded);
}
$scope.toggleItemEdit = function (item) {
item.isEditing = !item.isEditing;
}
$scope.commitItemEdit = function (item) {
commitBackingFields(item)
item.isEditing = false;
// Commit the changes to the database
persistenceService.update(item, $scope.OnDatabaseChanged);
}
$scope.addNewItem = function () {
// This method doesn't commit the new user to the database; it just adds a row to the view,
// and makes it editable
var Item = Parse.Object.extend(persistenceService.persistenceTable);
var item = new Item();
item.isEditing = true;
initBackingFields(item);
$scope.items[$scope.items.length] = item;
}
$scope.deleteItem = function (item) {
persistenceService.remove(item, $scope.OnDatabaseChanged);
}
// Start with an empty collection of items
$scope.items = [];
// Load the items from the service
persistenceService.load($scope.itemsLoaded);
} ]);
![]()