Application view

    1. The AngularJS module is named wikiApp.

    2. 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:

    1. <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
    2. integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
    3. crossorigin="anonymous"></script>
    4. <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"
    5. integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"
    6. crossorigin="anonymous"></script>
    7. <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"
    8. integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
    9. crossorigin="anonymous"></script>
    10. </body>
    11. </html>

    Our AngularJS controller is called WikiController and it is bound to a div which is also a Bootstrap container:

    1. <div class="container" ng-controller="WikiController">
    2. <!-- (...) -->

    The buttons on top of the interface consist of the following elements:

    1. The refresh button is bound to the reload controller action. All other buttons work the same way.

    2. The ng-show directive allows us to show or hide the element depending on the controller pageExists method value.

    3. This div is used to display notifications of success or failures.

    The Markdown preview and editor elements are the following:

    1. <div class="row">
    2. <div class="col-md-6" id="rendering"></div>
    3. <div class="col-md-6">
    4. <form>
    5. <div class="form-group">
    6. <label for="markdown">Markdown</label>
    7. <textarea id="markdown" class="form-control" rows="25" ng-model="pageMarkdown"></textarea> (1)
    8. </div>
    9. <div class="form-group">
    10. <label for="pageName">Name</label>
    11. <input class="form-control" type="text" value="" id="pageName" ng-model="pageName" ng-disabled="pageExists()">
    12. </div>
    13. <button type="button" class="btn btn-secondary" ng-click="save()"><i class="fa fa-pencil"
    14. aria-hidden="true"></i> Save
    15. </button>
    16. </form>
    17. </div>
    18. </div>
    1. ng-model binds the textarea content to the pageMarkdown property of the controller.

    Application controller

    The wiki.js JavaScript starts with an AngularJS module declaration:

    1. 'use strict';
    2. angular.module("wikiApp", [])
    3. .controller("WikiController", ["$scope", "$http", "$timeout", function ($scope, $http, $timeout) {
    4. var DEFAULT_PAGENAME = "Example page";
    5. var DEFAULT_MARKDOWN = "# Example page\n\nSome text _here_.\n";
    6. // (...)

    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:

    1. $scope.load = function (id) {
    2. $http.get("/api/pages/" + id).then(function(response) {
    3. var page = response.data.page;
    4. $scope.pageId = page.id;
    5. $scope.pageName = page.name;
    6. $scope.pageMarkdown = page.markdown;
    7. $scope.updateRendering(page.html);
    8. });
    9. };
    10. $scope.updateRendering = function(html) {
    11. document.getElementById("rendering").innerHTML = html;
    12. };

    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):

    1. $scope.save = function() {
    2. var payload;
    3. if ($scope.pageId === undefined) {
    4. payload = {
    5. "name": $scope.pageName,
    6. "markdown": $scope.pageMarkdown
    7. };
    8. $http.post("/api/pages", payload).then(function(ok) {
    9. $scope.reload();
    10. $scope.success("Page created");
    11. var guessMaxId = _.maxBy($scope.pages, function(page) { return page.id; });
    12. $scope.load(guessMaxId.id || 0);
    13. }, function(err) {
    14. $scope.error(err.data.error);
    15. });
    16. } else {
    17. var payload = {
    18. "markdown": $scope.pageMarkdown
    19. };
    20. $scope.success("Page saved");
    21. }, function(err) {
    22. $scope.error(err.data.error);
    23. });
    24. };
    25. $scope.delete = function() {
    26. $http.delete("/api/pages/" + $scope.pageId).then(function(ok) {
    27. $scope.reload();
    28. $scope.newPage();
    29. $scope.success("Page deleted");
    30. }, function(err) {
    31. $scope.error(err.data.error);
    32. });
    33. };
    34. $scope.success = function(message) {
    35. $scope.alertMessage = message;
    36. var alert = document.getElementById("alertMessage");
    37. alert.classList.add("alert-success");
    38. alert.classList.remove("invisible");
    39. $timeout(function() {
    40. alert.classList.add("invisible");
    41. alert.classList.remove("alert-success");
    42. }, 3000);
    43. };
    44. $scope.error = function(message) {
    45. $scope.alertMessage = message;
    46. var alert = document.getElementById("alertMessage");
    47. alert.classList.add("alert-danger");
    48. alert.classList.remove("invisible");
    49. $timeout(function() {
    50. alert.classList.add("invisible");
    51. alert.classList.remove("alert-danger");
    52. }, 5000);
    53. };

    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:

    1. var markdownRenderingPromise = null;
    2. $scope.$watch("pageMarkdown", function(text) { (1)
    3. if (markdownRenderingPromise !== null) {
    4. $timeout.cancel(markdownRenderingPromise); (3)
    5. }
    6. markdownRenderingPromise = $timeout(function() {
    7. markdownRenderingPromise = null;
    8. $http.post("/app/markdown", text).then(function(response) { (4)
    9. $scope.updateRendering(response.data);
    10. });
    11. }, 300); (2)
    12. });
    1. $scope.$watch allows being notified of state changes. Here we monitor changes on the property that is bound to the editor textarea.

    2. 300 milliseconds is a fine delay to trigger rendering if nothing has changed in the editor.

    3. We ask the backend to render the editor text into some HTML, then refresh the preview.