30. August 2016

synTechTalk: AngularJS CRUD Beispielapplikation

Screenshot: synaix, AngularJS Logo by AngularJS.org website, MIT, https://commons.wikimedia.org/w/index.php?curid=19180291

Mit Hilfe von AngularJS lassen sich „leichtgewichtige“ Web-Anwendungen entwickeln. Im heutigen synTechTalk Beitrag stelle ich ein simples Beispiel für eine CRUD (Create, Read, Update, Delete) Webanwendung vor, die sich später einfach erweitern lässt.

Dabei gehe ich davon aus, dass Du erste Grundkenntnisse von AngularJS in der Version 1.x besitzt und daher auch schon einmal mit JavaScript und JSON gearbeitet hast. Außerdem sollten Kenntnisse in HTML(5) vorhanden sein.

Zunächst zeige ich den Inhalt der jeweiligen Datei und erläutere dann ein paar Worte zu den wichtigen Stellen.

Als Beispiel nehme ich eine Task-Entität, mit der wir uns kleine Aufgaben wie auf einer ToDo Liste merken können.

Die Dateistruktur

Fangen wir zunächst mit der Datei-Struktur des Projektes an:

  • Wir benötigen eine index.html, die als unser Frame fungiert, sowie einen js und partials Ordner.
  • Im js Ordner erstellen wir eine app.js. Diese ist das Herz unserer Applikation und versorgt unsere Views mit dynamischen Daten.
  • Im partials Ordner legen wir dann unsere Views in Unterordnern an sowie eine index.html, die den Inhalt der Hauptseite anzeigt.
  • Für die Tasks erstellen wir noch einen Unterordner in partials mit den Views für das Erstellen sowie Anzeigen der Tasks.

Nachdem wir all das erstellt haben sollte unsere Struktur so aussehen:

/
|- js/
|  |- app.js
|- partials/
|  |- tasks/
|  |  |- create.html
|  |  |- index.html
|  |  |- show.html
|  |- index.html
|- index.html

Noch eine Anmerkung bzw. Voraussetzung: Damit wir im nächsten Schritt im partials Ordner die Dateien aus lokalen Quellen dynamisch laden können, benötigen wir einen WebServer wie nginx oder node.js um die Dateien zu hosten.

Das Frame

Zunächst schreiben wir die index.html im root-Verzeichnis.

<!DOCTYPE html>
<html ng-app="angularjscrudexample">
	<head>
		<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
		<title>AngularJS CRUD example</title>
		<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
		<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css" integrity="sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J5ezn3uZzapT0u7EYsXMjQV+0En5r" crossorigin="anonymous">
	</head>
	<body ng-controller="AppCtrl">
		<div class="container">
			<h1>AngularJS CRUD Example</h1>
			<div ng-view></div>
		</div>
		<script type="text/javascript" src="https://code.jquery.com/jquery-2.2.3.min.js" integrity="sha256-a23g1Nt4dtEYOj7bR+vTu7+T8VP13humZFBJNIYoEJo=" crossorigin="anonymous"></script>
		<script type="text/javascript" src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
		<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
		<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-route.min.js"></script>
		<script type="text/javascript" src="js/app.js"></script>
	</body>
</html>

In Zeile 2 ist direkt schon unsere erste sogenannte Direktive ng-app aufgeführt. Dieser übergeben wir den Projektnamen unserer Applikation, damit Angular später in der app.js weiß, um welchen Bereich es sich kümmern soll. Bei dem Projektnamen ist darauf zu achten, dass der Name mit einem Kleinbuchstaben beginnt.

Im Head legen wir nun ein meta-Tag ab sowie 2 Stylesheets von Bootstrap. Der meta sorgt dafür, dass man auf Mobilgeräten nicht mehr nach Belieben zoomen kann, sondern der Benutzer das sieht, was wir ihm vorgeben. Die beiden Stylesheets benutzen wir zur Verschönerung der Webseite sowie zur Unterstützung, um uns eine Responsive-Webseite zu bauen.

Im Body legen wir in Zeile 9 mit der Direktive ng-controller einen App-Controller fest, der später dafür sorgt, dass wir einige Hilfsmethoden benutzen können, unabhängig davon, auf welcher Sub-Seite wir uns befinden. (Für Fortgeschrittene: Dies könnte man auch über einen Rootscope machen.)

Nun kommt wohl einer der wichtigsten Teile: die Direktive ng-view in Zeile 12. Diese unscheinbare Zeile sorgt dafür, dass später der Inhalt aus einer jeweiligen Datei unseres partials Ordern dynamisch angezeigt/eingeladen wird.

An das Ende des Bodys legen wir unsere ganzen benötigten JavaScripte und Abhängigkeiten sowie als letztes unsere eigene js/app.js ab.

Das Herzstück

Nun widmen wir uns dem Herzstück, der js/app.js. Dies ist zugleich unsere größte Datei, die mit fortgeschrittenen Kenntnissen auch in Module unterteilt werden kann.

var app = angular.module('angularjscrudexample', ['ngRoute']);

app.config(function ($routeProvider) {
	$routeProvider
		.when('/', {templateUrl: 'partials/index.html'})
		.when('/tasks', {templateUrl: 'partials/tasks/index.html', controller: 'TaskCtrl'})
		.when('/tasks/create', {templateUrl: 'partials/tasks/create.html', controller: 'TaskCtrl'})
		.when('/tasks/:id', {templateUrl: 'partials/tasks/show.html', controller: 'TaskCtrl'})
		.otherwise({redirectTo: '/'});
});

app.controller('AppCtrl', function ($scope, $location) {
	$scope.navigateTo = function (to) {
		$location.path(to);
	};
});

app.factory('TaskService', function () {
	// TaskMock for Database
	var tasks = [{
		id: 1,
		title: "Homework",
		description: "Learning english vocabulary",
		done: false,
		createdDate: new Date(2016, 4, 3),
		completionDate: new Date(2016, 4, 10)
	}, {
		id: 2,
		title: "Another Task",
		description: "Learning english vocabulary",
		done: true,
		createdDate: new Date(2016, 4, 3),
		completionDate: null
	}];
	var nextId = tasks.length + 1;
	var _taskServiceInstance = {};

	// Help method
	_taskServiceInstance.validate = function (task) {
		if (task == undefined) {
			return false;
		} else if (typeof task.title != 'string' || task.title == '') {
			return false;
		} else if (typeof task.done != 'boolean') {
			return false;
		} else if (task.createdDate == undefined) {
			return false;
		}
		return true;
	};

	// Help method
	_taskServiceInstance.createEmpty = function () {
		return {
			title:'',
			description: '',
			done: false,
			createdDate: new Date(),
			completionDate: null
		};
	};

	// List
	_taskServiceInstance.findAll = function () {
		return tasks;
	};

	// Save
	_taskServiceInstance.save = function (task) {
		if (!_taskServiceInstance.validate(task)) {
			console.warn('Task was not valid');
		} else if (task.id != undefined) {
			console.warn('Task was already saved');
		} else {
			task.id = nextId;
			tasks.push(task);
			nextId++;
		}
	};

	// Read
	_taskServiceInstance.get = function (id) {
		return tasks.find(function (task) {return task.id == id;});
	};

	// Update
	_taskServiceInstance.update = function (id, task) {
		if (!_taskServiceInstance.validate(task)) {
			console.warn('Task was not valid');
		} else {
			var oldTask = _taskServiceInstance.get(id);
			oldTask.title = task.title;
			oldTask.description = task.description;
			oldTask.done = task.done;
			oldTask.createdDate = task.createdDate;
			oldTask.completionDate = task.completionDate;
		}
	};

	// Delete
	_taskServiceInstance.delete = function (id) {
		var index = tasks.map(function (task) {return task.id;}).indexOf('id');
		tasks.splice(index, 1);
	};

	return _taskServiceInstance;
});

app.controller('TaskCtrl', function ($scope, $routeParams, $location, TaskService) {
	var id = $routeParams.id;
	switch ($location.path()) {
		case '/tasks':
			$scope.order = 'id';
			$scope.changeOrder = function (columnname) {
				if ($scope.order == columnname) {
					$scope.order = '-' + columnname;
				} else {
					$scope.order = columnname;
				}
			};
			$scope.tasks = TaskService.findAll();
			$scope.delete = TaskService.delete;
			break;
		case '/tasks/create':
			$scope.task = TaskService.createEmpty();
			$scope.submit = function () {
				TaskService.save($scope.task);
				$scope.navigateTo('/tasks');
			};
			break;
		case '/tasks/' + id:
			$scope.task = TaskService.get(id);
			$scope.submit = function () {
				TaskService.update($scope.task.id, $scope.task);
				$scope.navigateTo('/tasks');
			};
			break;
		default:
			console.error('Something went wrong');
			break;
	}
});

In der ersten Zeile erstellen wir eine variable app, die wir mit angular.module(<Abhängigkeiten>); initialisieren.

Mit Hilfe der app erstellen wir nun eine Konfiguration für die Pfade zur Hauptseite sowie den Taskseiten und zum Abfangen einen Redirect auf die Hauptseite.

Für die Weiterleitungen auf die Taskseiten benutzen wir einen TaskCtrl.

Danach bauen wir den AppCtrl mit einer Hilfsmethode navigateTo, die im Scope sichtbar ist. Diese ermöglicht es uns später, von jeder partial eine andere partial aufzurufen.

Falls uns später noch weitere Hilfsmethoden einfallen, die für die gesamte Web-Applikation benötigt werden, ist hier ein guter Ort dafür.

Nun bauen wir einen TaskService, der – da wir keine Datenbank bzw. API-Backend haben – Mock-Daten bereitstellt sowie die wichtigen CRUD-Methoden besitzt.

Innerhalb des Service legen wir ein Array aus Taskobjekten als DatenbankMock sowie eine Variable nextId für die Simulierung der Datenbank ID-Sequenz an.

Dann erstellen wir eine weitere lokale Variable mit dem Namen taskServiceInstance, die zunächst einmal als leeres Objekt initialisiert wird. Dieser fügen wir nun Methoden hinzu und geben die Variable am Ende als Returnwert des Services zurück.

Erstellen der Methoden

Wir erstellen die folgenden Methoden: createEmpty, validate, findAll, save, get, update, delete. Die letzten vier Methoden sind unsere CRUD-Methoden, wobei findAll zusätzlich als zweite Read-Methode gezählt werden kann.

Die Methode createEmpty ist eine Hilfsmethode und muss nicht unbedingt einen Aufruf an das – hier nicht vorhandene – Backend machen.

Die Methode validate gehört eigentlich ins Backend- bzw. Datenbank-Umfeld und wird hier zur Simulation benötigt.

Der Vorteil an diesem Aufbau ist, dass man bei Verwendung eines Backends bzw. einer Datenbank nur noch die Methoden im Service umbauen muss und der Rest der Angular-Applikation nach wie vor funktioniert.

Zuletzt benötigen wir noch den TaskCtrl. Dieser verwendet unseren eben erstellten TaskService.

Wir legen eine lokale Variable id an, die wir den Routenparametern entnehmen. Diese ist oft nicht definiert, wird aber für die Route show benötigt.

Danach folgt ein switch-case das auf den aktuellen Pfad horcht. Wir horchen auf die drei Fälle ‚/tasks‘, ‚/tasks/create‘ und ‚/tasks/’+id. Außerdem legen wir sicherheitshalber ein default mit einer console.error an, der uns darauf hinweist, wenn etwas schief gelaufen ist.

Im Fall ‚/tasks‘ möchten wir eine Liste aller Tasks anzeigen lassen, daher rufen wir TaskService.findAll() auf und übergeben diese an eine tasks Variable im Scope.

Außerdem fügen wir dem scope noch ein paar weitere Variablen und Funktionen für das Sortieren der Tabellen-/Listen-Ansicht hinzu. Für das Löschen bauen wir dem Scope eine Weiterleitung auf die delete Methode des TaskService.

Im Fall ‚/tasks/create‘ rufen wir unsere Hilfsmethode createEmpty auf und weisen diese mit der Variable task dem Scope zu. Dann bauen wir eine Funktion für das Abschicken der Form mit einem save-Aufruf und einer Weiterleitung auf die Listenansicht.

Im Fall ‚/tasks/’+id sieht es ähnlich aus wie beim create-Befehl; der Unterschied ist, dass wir get statt createEmpty mit dem Argument id aufrufen und in der Methode zum Abschicken der Form einen update-Aufruf mit den Argumenten der ID des aktuellen Tasks sowie dem Task selber machen.

Die Partials

<button type="button" ng-click="navigateTo('/tasks/create')" class="btn btn-default">New Task</button>
<table class="table table-striped">
    <thead>
        <tr>
            <th class="col-sm-1" ng-click="changeOrder('id')">ID</th>
            <th class="col-sm-5" ng-click="changeOrder('title')">Title</th>
            <th class="col-sm-1" ng-click="changeOrder('done')">Done</th>
            <th class="col-sm-3" ng-click="changeOrder('createdDate')">Created</th>
            <th class="col-sm-3" ng-click="changeOrder('completionDate')">Completion Date</th>
            <th class="col-sm-1"></th>
            <th class="col-sm-1"></th>
        </tr>
    </thead>
    <tbody>
        <tr ng-repeat="task in tasks | orderBy:order">
            <td>{{task.id}}</td>
            <td>{{task.title}}</td>
            <td><span class="glyphicon glyphicon-{{task.done ? 'ok' : 'remove'}}" aria-hidden="true"></td>
            <td>{{task.createdDate | date}}</td>
            <td><span ng-if="!task.completionDate">-</span>{{task.completionDate | date}}</td>
            <td><button type="button" ng-click="navigateTo('/tasks/' + task.id)" class="btn btn-primary btn-block" aria-label="Edit"><span class="glyphicon glyphicon-edit" aria-hidden="true"></span></button></td>
            <td><button type="button" ng-click="delete(task.id)" class="btn btn-danger btn-block" aria-label="Remove"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span></button></td>
        </tr>
    </tbody>
</table></pre>
<pre>

In der partials/tasks/index.html legen wir einen Button an, der uns auf die Erstellungsseite von einem neuen Task führt, daher navigateTo(‚/tasks/create‘).

Außerdem legen wir (mit Hilfe von Bootstrap) eine Tabelle für die Auflistung der Tasks an. Den Tabellenköpfen geben wir unsere definierte Funktion zum Sortieren der jeweiligen Spalte an. Die weiteren Tabellenzeilen werden dynamisch durch ein ng-repeat über die im Scope liegende Variable tasks erzeugt. Hier verwenden wir außerdem unsere order Variable.

Wir bauen noch Buttons ein zum Editieren und Löschen eines jeweiligen Tasks und schon haben wir eine fertige Listenansicht für unsere Tasks.

<button type="button" ng-click="navigateTo('/tasks')" class="btn btn-default">Tasklist</button>
<h2>Create Task</h2>
<form ng-submit="submit()" class="form-horizontal">
	<div class="form-group">
		<label for="title" class="col-sm-2 control-label">Title:</label>
		<div class="col-sm-10">
			<input type="text" name="title" ng-model="task.title" class="form-control">
		</div>
	</div>
	<div class="form-group">
		<label for="description" class="col-sm-2 control-label">Description:</label>
		<div class="col-sm-10">
			<textarea name="description" ng-model="task.description" class="form-control"></textarea>
		</div>
	</div>
	<div class="form-group">
		<label for="done" class="col-sm-2 control-label">Done:</label>
		<div class="checkbox col-sm-10">
			<label><input type="checkbox" name="done" ng-model="task.done"></label>
		</div>
	</div>
	<div class="form-group">
		<label for="createdDate" class="col-sm-2 control-label">Created:</label>
		<div class="col-sm-10">
			<input type="date" name="createdDate" ng-model="task.createdDate" class="form-control">
		</div>
	</div>
	<div class="form-group">
		<label for="completionDate" class="col-sm-2 control-label">Completion:</label>
		<div class="col-sm-10">
			<input type="date" name="completionDate" ng-model="task.completionDate" class="form-control">
		</div>
	</div>
	<div class="form-group">
		<button type="submit" class="btn btn-primary btn-block">Create</button>
	</div>
</form>

Erstellen/Editieren

Dann bauen wir die partials/tasks/create.html. Wir erstellen wieder einen Button, der uns zur Listenansicht zurück schickt, sowie eine Überschrift für unsere folgende Form.

Der Form geben wir ein ng-submit zur unserer im Scope liegenden Funktion submit() im TaskCtrl mit. Diese wird ausgeführt, sobald wir den Submit-Button der View betätigen.

Danach bauen wir mit viel Unterstützung von Bootstrap die benötigten Input-Felder für unsere Task-Attribute und verknüpfen diese über 2-WayBinding mit unserer im Scope liegenden task Variable. Diese werden im submit()an den TaskService übergeben und der leitet sie dann entweder an unsere Mock implementation oder an das jeweilige Backend.

<button type="button" ng-click="navigateTo('/tasks')" class="btn btn-default">Tasklist</button>
<h2>Edit Task</h2>
<form ng-submit="submit()" class="form-horizontal">
	<div class="form-group">
		<label for="id" class="col-sm-2 control-label">ID:</label>
		<div class="col-sm-10">
			<input type="text" name="id" ng-model="task.id" class="form-control" disabled>
		</div>
	</div>
	<div class="form-group">
		<label for="title" class="col-sm-2 control-label">Title:</label>
		<div class="col-sm-10">
			<input type="text" name="title" ng-model="task.title" class="form-control">
		</div>
	</div>
	<div class="form-group">
		<label for="description" class="col-sm-2 control-label">Description:</label>
		<div class="col-sm-10">
			<textarea name="description" ng-model="task.description" class="form-control"></textarea>
		</div>
	</div>
	<div class="form-group">
		<label for="done" class="col-sm-2 control-label">Done:</label>
		<div class="checkbox col-sm-10">
			<label><input type="checkbox" name="done" ng-model="task.done"></label>
		</div>
	</div>
	<div class="form-group">
		<label for="createdDate" class="col-sm-2 control-label">Created:</label>
		<div class="col-sm-10">
			<input type="date" name="createdDate" ng-model="task.createdDate" class="form-control">
		</div>
	</div>
	<div class="form-group">
		<label for="completionDate" class="col-sm-2 control-label">Completion:</label>
		<div class="col-sm-10">
			<input type="date" name="completionDate" ng-model="task.completionDate" class="form-control">
		</div>
	</div>
	<div class="form-group">
		<button type="submit" class="btn btn-primary btn-block">Edit</button>
	</div>
</form>

Anzeigen

Und nun zur letzten Ansicht, der partials/tasks/show.html. Diese sieht bis auf ein paar Abweichungen fast aus wie die create.html.

Natürlich müssen wir dafür eine andere Überschrift vergeben und wir können die ID des Tasks anzeigen lassen. Dazu benutzen wir ein Input-Feld, das deaktiviert angezeigt wird.

Außerdem beschriften wir den Submit-Button nicht mit „Create“ sondern mit „Edit“.

Das war´s, damit ist unsere Anwendung fertig.

Nun starten wir das Ganze mit nginx/node.js und testen alles ´mal.

Da unsere Applikation nicht persistent arbeitet, sind natürlich alle Daten weg, sobald man die Seite einmal neu lädt.

Anhand dieses Beispiels kannst Du nun eigene Entitäten mit Services anlegen. – Bei Fragen und Anmerkungen freue ich mich über Deine Rückmeldung im Kommentarfeld!

Wenn Du tiefer in AngularJS einsteigen möchtest, findest Du hier die AngularJS Dokumentation.

(Christopher Quadflieg)

 

Anhang: Source-Code angularjs_crud_example

In unserer Reihe synTechTalk geben unsere DEV und OPS Teams Einblicke in ihr Technologie- und Architektur- KnowHow zur Umsetzung erfolgreicher digitaler Geschäftsmodelle.

Keine Kommentare