Application view
The AngularJS module is named
wikiApp
.wiki.js
holds the code for our AngularJS module and controller.
As you can see beyond AngularJS we are using the following dependencies from external CDNs:
Boostrap to style our interface,
to provide icons,
Lodash to help with some functional idioms in our JavaScript code.
Bootstrap requires some further scripts that can be loaded at the end of the document for performance reasons:
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"
integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"
crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"
integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
crossorigin="anonymous"></script>
</body>
</html>
Our AngularJS controller is called WikiController
and it is bound to a div
which is also a Bootstrap container:
<div class="container" ng-controller="WikiController">
<!-- (...) -->
The buttons on top of the interface consist of the following elements:
The refresh button is bound to the
reload
controller action. All other buttons work the same way.The
ng-show
directive allows us to show or hide the element depending on the controllerpageExists
method value.This
div
is used to display notifications of success or failures.
The Markdown preview and editor elements are the following:
<div class="row">
<div class="col-md-6" id="rendering"></div>
<div class="col-md-6">
<form>
<div class="form-group">
<label for="markdown">Markdown</label>
<textarea id="markdown" class="form-control" rows="25" ng-model="pageMarkdown"></textarea> (1)
</div>
<div class="form-group">
<label for="pageName">Name</label>
<input class="form-control" type="text" value="" id="pageName" ng-model="pageName" ng-disabled="pageExists()">
</div>
<button type="button" class="btn btn-secondary" ng-click="save()"><i class="fa fa-pencil"
aria-hidden="true"></i> Save
</button>
</form>
</div>
</div>
ng-model
binds thetextarea
content to thepageMarkdown
property of the controller.
Application controller
The wiki.js
JavaScript starts with an AngularJS module declaration:
'use strict';
angular.module("wikiApp", [])
.controller("WikiController", ["$scope", "$http", "$timeout", function ($scope, $http, $timeout) {
var DEFAULT_PAGENAME = "Example page";
var DEFAULT_MARKDOWN = "# Example page\n\nSome text _here_.\n";
// (...)
The wikiApp
module has no plugin dependency, and declares a single WikiController
controller. The controller requires dependency injection of the following objects:
$scope
to provide DOM scoping to the controller, and$http
to perform asynchronous HTTP requests to the backend, and$timeout
to trigger actions after a given delay while staying tied to the AngularJS life-cycle (e.g., to ensure that any state modification triggers view changes, which is not the case when using the classic setTimeout function).
Creating a new page consists in initializing controller properties that are attached to the $scope
object. Reloading the pages objects from the backend is a matter of performing a HTTP GET request (note that the $http
request methods return promises). The pageExists
method is being used to show / hide elements in the interface.
Loading the content of the page is also a matter of performing a HTTP GET request, and updating the preview a DOM manipulation:
$scope.load = function (id) {
$http.get("/api/pages/" + id).then(function(response) {
var page = response.data.page;
$scope.pageId = page.id;
$scope.pageName = page.name;
$scope.pageMarkdown = page.markdown;
$scope.updateRendering(page.html);
});
};
$scope.updateRendering = function(html) {
document.getElementById("rendering").innerHTML = html;
};
The next methods support saving / updating and deleting pages. For these operations we used the full then
promise method with the first argument being called on success, and the second one being called on error. We also introduce the success
and error
helper methods to display notifications (3 seconds on success, 5 seconds on error):
$scope.save = function() {
var payload;
if ($scope.pageId === undefined) {
payload = {
"name": $scope.pageName,
"markdown": $scope.pageMarkdown
};
$http.post("/api/pages", payload).then(function(ok) {
$scope.reload();
$scope.success("Page created");
var guessMaxId = _.maxBy($scope.pages, function(page) { return page.id; });
$scope.load(guessMaxId.id || 0);
}, function(err) {
$scope.error(err.data.error);
});
} else {
var payload = {
"markdown": $scope.pageMarkdown
};
$scope.success("Page saved");
}, function(err) {
$scope.error(err.data.error);
});
};
$scope.delete = function() {
$http.delete("/api/pages/" + $scope.pageId).then(function(ok) {
$scope.reload();
$scope.newPage();
$scope.success("Page deleted");
}, function(err) {
$scope.error(err.data.error);
});
};
$scope.success = function(message) {
$scope.alertMessage = message;
var alert = document.getElementById("alertMessage");
alert.classList.add("alert-success");
alert.classList.remove("invisible");
$timeout(function() {
alert.classList.add("invisible");
alert.classList.remove("alert-success");
}, 3000);
};
$scope.error = function(message) {
$scope.alertMessage = message;
var alert = document.getElementById("alertMessage");
alert.classList.add("alert-danger");
alert.classList.remove("invisible");
$timeout(function() {
alert.classList.add("invisible");
alert.classList.remove("alert-danger");
}, 5000);
};
initializing the application state and views is done by fetching the pages list, and starting with a blank new page editor:
Finally here is how we perform live rendering of Markdown text:
var markdownRenderingPromise = null;
$scope.$watch("pageMarkdown", function(text) { (1)
if (markdownRenderingPromise !== null) {
$timeout.cancel(markdownRenderingPromise); (3)
}
markdownRenderingPromise = $timeout(function() {
markdownRenderingPromise = null;
$http.post("/app/markdown", text).then(function(response) { (4)
$scope.updateRendering(response.data);
});
}, 300); (2)
});
$scope.$watch
allows being notified of state changes. Here we monitor changes on the property that is bound to the editortextarea
.300 milliseconds is a fine delay to trigger rendering if nothing has changed in the editor.
We ask the backend to render the editor text into some HTML, then refresh the preview.