Simple To Do App with MEAN Stack Comparison

In this example, we use a plain old Moxie.Build system and compare it to the MEAN Stack equivalent by building two functionally equivalent versions of a fleshed out To Do List Application. Both versions utilize local user authentication, a relational database system and the security measures found in typical production-level applications on the web today.

Setup Required:

A single Moxie.Build installation consists of one folder containing all of the tools that you will need to build typical web software from the ground up. Moxie.Build works similarily to a server-side rendered application where multiple web pages/views can be rendered from a single procedure.

  • Time to Learn: 6 Weeks
  • Time to Build: 6 Hours
  • Lines of Code: 381
  • Byte Count: 12237
Setup Required:

A strong understanding of a modern MVC JavaScript Framework and NodeJS is essential to building MEAN Stack web applications. In this particular example, I used AngularJS, MongoDB, NodeJS, LoopBack in conjunction with a large number of external modules for both NodeJS and AngularJS.

  • Time to Learn: 1 Year
  • Time to Build: 10 Hours
  • Lines of Code: 648
  • Byte Count: 20095

MEAN stack development is great because there is such a large JavaScript community on the web who are working together to create open source Frameworks and modules so you will always have a great number of options to choose from when you are deciding how to build/structure your application. Having options can be great, but how can you know that you are choosing the right tools to use. Additionally, problems can often arise when a particular module that you have been using forever becomes no longer supported or is no longer compatible with one of the frameworks that are you using. As time goes by, you will find yourself relearning new technologies/frameworks to accomplish the same tasks with slightly less code or with a new style of architecture.

Moxie.Build development is also great because a lot of the more complicated and tedious programming tasks are baked right into the system. There is no longer a need to build a custom CMS for each and every collection of data so that your client can easily manage it without your help. Additionally, a large part of the development and required configuration/setup of your web software is easily accessible/maintainable through the use of GUIs which makes your application simple-stupid-easy to set up and configure. Moxie allows you to spend more time on providing the business related functionality of your software and less time performing repetitive and tedious tasks that are required for typical web software these days.

In conclusion, whether you are a seasoned veteran of MEAN stack development, or you are still a Web Programming newbie, Moxie.Build offers a number of solutions to both types of developers. If you're tired of learning a new MVC Framework every two years or if you're fed up of creating custom CMS admin interfaces, try Moxie.Build and prepared to be bedazzled when you discover how simple it can be to build Web Software.



Screenshots of the To Do Application to give you a better idea of what the code is accomplishing.


A single procedure to build all of the HTML and encapsulated logic for the entire application.


Rem 'Init
    HttpNoCache
    HttpsEnsure
EndRem

Rem 'Flow Control
    [Pull]  Req.IsPost List.Edit Item.Edit Item.Del List.Del
    
    If      Req.IsPost  : And List.Edit     : And Not Item.Edit : List.Edit.Proc    List.Edit
    ElseIf                    List.Edit     : And Not Item.Edit : List.Edit.Show    List.Edit, ""
    
    ElseIf  Req.IsPost  : And List.Edit     : And Item.Edit     : Item.Edit.Proc    List.Edit, Item.Edit
    ElseIf                    List.Edit     : And Item.Edit     : Item.Edit.Show    List.Edit, Item.Edit, ""
    
    ElseIf  Req.IsPost  : And Item.Del      : Item.Del.Proc     Item.Del
    ElseIf                    Item.Del      : Item.Del.Show     Item.Del
    
    ElseIf Req.IsPost   : And List.Del      : List.Del.Proc     List.Del
    ElseIf                    List.del      : List.Del.Show     List.Del
    
    Else                                    : List.All
    End If
EndRem


Method List.All()
    Rem 'Get our data
        SetNew      "MemTab.Person.Alias", ($SessionUser)
        Children    "ToDo.List"
    EndRem
    
    Html "<div style='margin-bottom: 10px;'>"
    HtmlAButton "MainBody", "Append", " Add List", "?List.Edit=*", "&default &md"
    Html "</div>"

    'Build a Panel with each List Item
    ForEach         "Me.Output", "List.All.forList"
End Method


Method List.All.forList()
    Rem 'Get data from top query
        [New]   ListName    | ToDo.List.Name
        [New]   List        | ToDo.List.Alias
        If Not List Then Exit
        Children                "ToDo.Item"
    EndRem
    
    Rem 'Customize some fields
        WorkWith        "ToDo.Item"
            Build       "Name", ("", "Name", "", "Alias"
            NewFields   "Delete"
            Build       "Delete", ((NbSp$ 4) & "", "Alias"
        End WorkWith
        
        KeepFields      "ToDo.Item.", "Name DueDate Status Delete"
        NameField       "ToDo.Item.Delete", "Delete"
    EndRem
    
Rem 'Build Panel
        Prefix Html
            < div class="panel panel-default">
            <div class="panel-heading">
            ("<h3 class='panel-title'>" & ListName & " ")
        End Prefix
        
        Html "</div>"
        Html "<div> class='panel-body'>"
        
        Html "<div class='btn-group'>"
        HtmlAButton "MainBody", "Append", " Edit List", ("?List.Edit=" & List), "&default &sm"
        HtmlAButton "MainBody", "Append", " Delete List", ("?List.Del=" & List), "&default &sm"
        Html "<div>"
        
        HtmlAButton "MainBody", "Append", " Add Item", ("?Item.Edit=*&List.Edit=" & List), "&default &sm" 
        
        HtmlTable
        
        Html "<div>"
        Html "<div>"
    EndRem
End Method


Method List.Edit.Show(pList, pAnyErr)
    If pList = *
        WorkWith            "ToDo.List"
            Pull            "Request", "ToDo.List", "Name DueDate"
            Rem 'Customize form
                KeepFields  "ToDo.List.", "Name DueDate"
                GetFieldDefs
            EndRem
        End WorkWith
        
    Else
        List.IsMine pList                           'Security Check
        LoadRecord          "ToDo.List", pList      'Get our data
        
        Rem 'Customize form
            KeepFields      "ToDo.List.", "Name DueDate Created Modified"
            GetFieldDefs
        EndRem
    End If
    
    If pAnyErr
        WorkWith            "ToDo.List"
            Pull            "Request", "ToDo.List.", "Name DueDate"
        End WorkWith
    End If
    
    Rem 'Apply err states
        If pAnyErr
            [Pull]          "List.Edit.Proc.Input", "Name.Err DueDate.Err"
        Else
            [New]           Name.Err DueDate.Err
        End If
        
        mSetState           (Name.Err, "ToDo.List.Name")
        mSetState           (DueDate.Err, "ToDo.List.DueDate")
    EndRem
    
    HtmlForm        " Save"               'Put it on the page
End Method


Method List.Edit.Proc(pList)
    If pList <> * 'Security Check
        List.IsMine pList
    End If
    
    Rem 'Validate data
        [Pull]  "Request", "ToDo.List.", "Name DueDate"
        [New]   AnyErr Name.Err DueDate.Err
        
        If Not Name                                 : AnyErr = "y" :    Name.Err = "y" : HtmlErr "Oops, please provide a name."          : End If
        If DueDate : And Not (ValidDate$ DueDate)   : AnyErr = "y" : DueDate.Err = "y" : HtmlErr "Oops, please provide a valid Due Date" : End If
        
        If AnyErr
            List.Edit.Show pList, "y"
            Exit
        End If
    EndRem
    
    Name = MCase$ Name          'Data clean up
    
    Rem 'Save to database
        If pList = *
            SetNew              "MemTab.Person.Alias", ($SessionUser)
            WorkWith            "ToDo.List"
                SetNew          "Alias", *
                Pull            "Name DueDate"
                NewWithAttach   "Parent", "MemTab.Person"
                FailIfRecError
                [New]           nList | ToDo.List.Alias
            End WorkWith
        Else
            WorkWith            "ToDo.List"
                SetNew          "Alias", pList
                Pull            "Name DueDate"
                Backfill
                    Update
                End Backfill
                FailIfRecError
            End WorkWith
        End If
    EndRem
        
    Rem 'Show all lists again
        [Pull] Req.Path
        HttpStatus  "302"
        HttpHeader  "Location", ("/" & Req.Path)
    EndRem
End Method


Method List.IsMine(pList)
    Rem 'Search for data via relationships
        SetNew              "MemTab.Person.Alias", ($SessionUser)
        Children            "ToDo.List"
        KeepIf              "ToDo.List.Alias", =, `pList
        [New] FoundIt   |   ToDo.List.Alias
    EndRem
    
    Rem 'Handle not found
        If Not FoundIt
            HtmlErr         "Sorry,  it looks like you have an invalid link"
            List.All
            Exit Proc
        End If
    EndRem
End Method


Method Item.Edit.Show(pList, pItem, pAnyErr)
    Rem 'get our data
        If pItem = *
            WorkWith        "ToDo.Item"
                Pull        "Request", "ToDo.Item", "Name DueDate Status Desc"
                Rem 'Customize form
                    KeepFields  "ToDo.Item.", "Name DueDate Status Desc"
                    GetFieldDefs
                EndRem
            End WorkWith
        Else
            Item.IsMine pItem                       'Security Check
            LoadRecord      "ToDo.Item", pItem      'Get our data
            
            Rem 'Customize form
                KeepFields  "ToDo.Item.", "Name DueDate Status Desc Created Modified"
                GetFieldDefs
            EndRem
        End If
    EndRem
    
    Rem 'Apply error states
        If pAnyErr
            [Pull]      "Item.Edit.Proc.Input", "Name.Err DueDate.Err Status.Err Desc.Err"
        Else
            [New]       Name.Err DueDate.Err Status.Err Desc.Err
        End If
        
        mSetState       (Name.Err, "ToDo.Item.Name")
        mSetState       (DueDate.Err, "ToDo.Item.DueDate")
        mSetState       (Status.Err, "ToDo.Item.Status")
        mSetState       (Desc.Err, "ToDo.Item.Desc")
    EndRem
    
    If pAnyErr
        WorkWith    "ToDo.Item"
            Pull    "Request", "ToDo.Item", "Name DueDate Status Desc"
        End WorkWith
    End If
    
    HtmlForm        " Save"               'Put it on the page
End Method


Method Item.Edit.Proc(pList, pItem)
    Rem 'Security Check
        If pItem <> *
            Item.IsMine pItem
        End If
    EndRem
    
    Rem 'validate data
        [Pull] "Request", "ToDo.Item.", "Name DueDate Status Desc"
        [New]   AnyErr Name.Err DueDate.Err
        
        If Not Name                                 : AnyErr = "y"  : Name.Err = "y"    : HtmlErr "Oops, please provide a name."             : End If
        If DueDate  : And Not (ValidDate$ DueDate)  : AnyErr = "y"  : DueDate.Err = "y" : HtmlErr "Oops, please provide a valid Due Date."   : End If
        
        If AnyErr
            Item.Edit.Show pList, pItem, "y"
            Exit
        End If
    EndRem
    
    Name = MCase$ Name      'Clean up data

    Rem 'Save to database
        If pItem = *
            SetNew          "ToDo.List.Alias", `pList
            WorkWith        "ToDo.Item"
                SetNew      "Alias", *
                Pull        "Name DueDate Status Desc"
                NewWithAttach   "Parent", "ToDo.List"
                FailIfRecError
                [New]       nItem | ToDo.Item.Alias
            End WorkWith
        Else 
            WorkWith        "ToDo.Item"
                SetNew      "Alias", `pItem
                Pull        "Name DueDate Status Desc"
                Backfill
                    Update
                End Backfill
                FailIfRecError
            End WorkWith
        End If
    EndRem
    
    Rem 'Show all Items again
        [Pull] Req.Path
        HttpStatus  "302"
        HttpHeader  "Location", ("/" & Req.Path)
    EndRem
End Method


Method List.Del.Show(pList)
    List.IsMine     pList   'Security Check
    Warning.Show    "List"  'Display warning
End Method


Method Item.Del.Show(pItem)
    Item.IsMine     pItem   'Security Check
    Warning.Show    "Item"  'Display warning
End Method


Method List.Del.Proc(pList)
    List.IsMine pList       'Security Check
    
    Rem 'Delete it
        SetNew              "ToDo.List.Alias", `pList
        Children            "ToDo.Item"
        DetachWithDelete    "ToDo.Item", "Parent", "ToDo.List"
        FailIfRecError
        
        Reset
        SetNew              "ToDo.List.Alias", `pList
        Parents             "ToDo.List", "MemTab.Person"
        DetachWithDelete    "ToDo.List", "Parent", "MemTab.Person"
        FailIfRecError
    EndRem
    
    List.Redirect           'Return To List
End Method


Method Item.Del.Proc(pItem)
    Item.IsMine pItem       'Security Check
    
    Rem 'Delete it
        WorkWith                "ToDo.Item"
            SetNew              "Alias", `pItem
            Parents             "ToDo.Item", "ToDo.List"
            DetachWithDelete    "Parent", "ToDo.List"
            FailIfRecError
        End WorkWith
    EndRem
    
    List.Redirect           'Return To List
End Method


Method Item.IsMine(pItem)
    Rem 'Search for data via relationships
        SetNew          "MemTab.Person.Alias", ($SessionUser)
        Children        "ToDo.List"
        Children        "ToDo.Item"
        KeepIf          "ToDo.Item.Alias", =, `pItem
        [New]   FoundIt | ToDo.Item.Alias
    EndRem
    
    Rem 'Handle not found
        If Not FoundIt
            HtmlErr         "Sorry,  it looks like you have an invalid link."
            List.All
            Exit Proc
        End If
    EndRem
End Method


Method Warning.Show(pType)
    Html            "<p>"
    HtmlAButton         "<? i menu-left ?> Cancel", "?"
    Html            "</p>"
    
    HtmlAlert       "Runtime", "&danger", ("Are you sure? Deleting an " & pType & " cannot be undone.")
    
    Html            "<p>"
    HtmlButton      "<? i remove ?> Delete Item"
    Html            "</p>"
End Method


Method List.Redirect
    [Pull] Req.Path
    HttpStatus "302"
    HttpHeader "Location", ("/" & Req.Path)
End Method


Macro mSetState(pErrFld, pFullFld)
    If pErrFld      : Build (pFullFld & "#"), "Attr", "Attr", ($I), "`[State] Error", ""
    ElseIf pAnyErr  : Build (pFullFld & "#"), "Attr", "Attr", ($I), "`[State] Success", ""
    End If
End Macro

The abstract parent state and child state for viewing ToDoLists and ToDoItems.


.state('app.todos', {
  abstract: true,
  url: '/todos',
  template: '<div ui-view></div>'
})

.state('app.todos.list', {
  url: '',
  template: `
	<div class="container">

    <a ui-sref="app.todos.add" class="m-t-10 m-b-10 btn btn-sm btn-default"><i class="fa fa-plus"></i> New List</a>

    <alert-box theme="default"></alert-box>

    <div class="alert alert-info" ng-if="!todoLists.length">
        <em class="lead">
            <span ng-if="User.isAuthenticated()">You don't have any To Do Lists</span>
            <span ng-if="!User.isAuthenticated()">Login to create a To Do List</span>
        </em>
    </div>

    <div class="panel panel-default" ng-if="todoLists.length" 
        ng-repeat="list in todoLists">
        <div class="panel-heading">
            <h3 class="panel-title">{{list.name}} <span class="pull-right"><b>Due:</b> {{list.dueDate | date:MM:DD:YYYY}}</span></h3>
        </div>
        <div class="panel-body">
            <div class="btn-group" role="group" aria-label="...">
                <div class="btn-group" role="group">
                    <a ui-sref="app.todos.add({ listId: list.id })" class="btn btn-default btn-xs"><i class="fa fa-edit"></i> Edit</a>
                </div>
                <div class="btn-group" role="group">
                    <button type="button" class="btn btn-default btn-xs" ng-click="deleteList(list)"><i class="fa fa-trash"></i> Delete</button>
                </div>
            </div>

            <a ui-sref="app.todos.add-item({ listId: list.id})" class="btn btn-default btn-xs"><i class="fa fa-plus"></i> New Item</a>

            <table class="table table-striped" ng-if="list.toDoItems.length">
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Status</th>
                        <th>Due Date</th>
                        <th>Delete</th>
                    </tr>
                </thead>
                <tbody>
                    <tr ng-repeat="item in list.toDoItems">
                        <td><a ui-sref="app.todos.add-item({ listId: list.id,itemId: item.id})">{{item.name}}</a></td>
                        <td>{{item.status}}</td>
                        <td>{{item.dueDate | date: MM/DD/yyyy}}</td>
                        <td>&nbsp;&nbsp;<button type="button" class="btn btn-danger btn-sm" ng-click="deleteItem(list, item)"><i class="fa fa-close"></i></button></td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div><!-- end panel -->

</div>
  `,
  controller: ['$scope', 'AlertService', 'ToDoList', 'todoLists', function ($scope, AlertService, ToDoList, todoLists) {
    // attach resolved ToDoLists & ToDoItems data to template 
    $scope.todoLists = todoLists;

    // delete ToDoList & all ToDoItem relationships
    $scope.deleteList = function (list) {
      AlertService.reset();
      
      // using async to structure/order our chain of promises
      async.series([
        function (seriesCB) {
        // delete ToDoList item relationships first
          ToDoList.toDoItems.destroyAll({
            id: list.id
          })
          .$promise
          .then(function (response) {
            return seriesCB();
          })
          .catch(function (err) {
            return seriesCB(err);
          });
        },
        function (seriesCB) {
          // delete ToDoList second
          ToDoList.destroyById({
            id: list.id
          })
          .$promise
          .then(function (response) {
            return seriesCB();
          })
          .catch(function (err) {
            return seriesCB(err);
          });
        }
      ], function (err) {
        // set an alert message for either error or success 
        if (err) {
          return AlertService.setError({
            show: true,
            title: 'Error deleting List',
            lbErr: err
          });
        }

        AlertService.setSuccess({
          show: true,
          title: list.name + ' deleted successfully'
        });
        
        // remove the ToDoList from the dom
        var index = $scope.todoLists.indexOf(list);
        $scope.todoLists.splice(index, 1);

      });
    };

    // delete a single ToDoItem from a ToDoList
    $scope.deleteItem = function (list, item) {
      AlertService.reset();
      
      // make DELETE request to our api
      ToDoList.toDoItems.destroyById({
        id: list.id,
        fk: item.id
      })
      .$promise
      .then(function (response) {
        // success, remove ToDoItem from ToDoList and show success alert
        var index = list.toDoItems.indexOf(item);
        list.toDoItems.splice(index, 1);
        AlertService.setSuccess({
          show: true,
          title: item.name + ' deleted successfully.'
        });
      })
      .catch(function (err) {
        // fail, show error message
        AlertService.setError({
          show: true,
          title: 'Error deleting List Item',
          lbErr: err
        });
      });
    };

  }],
  resolve: {
    todoLists: ['ToDoList', 'User', function (ToDoList, User) {
      // before displaying page, we require the ToDoLists & ToDoItems data for the current user
      return ToDoList.find({
        filter: {
          where: {
            authorId: User.getCurrentId()
          },
          include: 'toDoItems'
        }
      })
      .$promise
      .then(function (response) {
        return response;
      })
      .catch(function (err) {
        return [];
      });
    }]
  }
})

The state which is responsible for creating new ToDoLists as well as updating existing ToDoLists.


.state('app.todos.add', {
  url: '/add/:listId',
  template: `
    <div class="container">

        <a ui-sref="app.todos.list" class="m-t-10 m-b-10 btn btn-sm btn-default"><i class="fa fa-chevron-left"></i> Back </a>

        <div class="panel panel-default">
            <div class="panel-heading">
                <h3 class="panel-title">New To Do List</h3>
            </div>
            <div class="panel-body">

                <alert-box theme="default"></alert-box>

                <form class="form-horizontal" ng-submit="save()">
                    <div class="form-group">
                        <label for="todoName" class="col-sm-2 control-label">Name</label>
                        <div class="col-sm-10">
                            <input type="text" class="form-control" id="todoName" placeholder="Name"
                                ng-model="todoList.name">
                        </div>
                    </div>

                    <div class="form-group">
                        <label for="dueDate" class="col-sm-2 control-label">Due Date</label>
                        <div class="col-sm-10">
                            <div class="dropdown">
                                <a class="dropdown-toggle" name="dueDate" id="dueDate" role="button" data-toggle="dropdown" href="#">
                                    <div class="input-group"><input type="text" class="form-control" data-date-time-input="MM-DD-YYYY hh:mm A" data-ng-model="todoList.dueDate"><span class="input-group-addon"><i class="glyphicon glyphicon-calendar"></i></span>
                                    </div>
                                </a>
                                <ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">
                                    <datetimepicker data-ng-model="todoList.dueDate" data-datetimepicker-config="{ dropdownSelector: '#dueDate', startView: 'year' }"/>
                                </ul>
                            </div>
                        </div>
                    </div>

                    <div class="form-group">
                        <div class="col-sm-offset-2 col-sm-10">
                            <button type="submit" class="btn btn-default"><i class="fa fa-check"></i> Save</button>
                        </div>
                    </div>
                </form>

            </div><!-- body -->
        </div><!-- end panel -->

    </div>
  `,
  controller: ['$scope', '$state', 'AlertService', 'ToDoList', 'todoList', function ($scope, $state, AlertService, ToDoList, todoList) {

    // initialization
    if (todoList) {
      // edit an existing ToDoList
      $scope.todoList = todoList;
    } else {
      // create a new ToDoList
      $scope.todoList = {
        name: '',
        dueDate: new Date(moment().format('MM-DD-YYYY hh:mm A'))
      };
    }
    
    // save a new ToDoList / edit an existing ToDoList
    $scope.save = function () {
      $scope.todoList.authorId = $scope.User.getCurrentId();
      AlertService.reset();
      
      // upsert: create if instance does not exist, otherwise edit
      ToDoList.upsert($scope.todoList)
      .$promise
      .then(function (response) {
        AlertService.setSuccess({
          show: true,
          title: $scope.todoList.name + ' saved successfully.',
          persist: true
        });
        
        // transition to previous state
        $state.transitionTo('app.todos.list');
      })
      .catch(function (err) {
        AlertService.setError({
          show: true,
          lbErr: err,
          title: 'Error Saving'
        });
      });
    };

  }],
  resolve: {
    todoList: ['$stateParams', 'ToDoList', function ($stateParams, ToDoList) {
      // if the URL contains a listId, resolve it from db, else create a new ToDoList
      if (!$stateParams.listId) return null;

      return ToDoList.findById({
        id: $stateParams.listId
      })
      .$promise
      .then(function (res) {
        return res;
      })
      .catch(function (err) {
        return null;
      });
    }]
  }
})

The state which is responsible for creating new ToDoItems as well as updating existing ToDoItems.

 UltraEdit source file - tab-js.js
.state('app.todos.add-item', {
  url: '/add-item/:listId/:itemId',
  template: `
 <div class="container"> <a ui-sref="app.todos.list" class="m-t-10 m-b-10 btn btn-sm btn-default"><i class="fa fa-chevron-left"></i> Back </a> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">New To Do List Item</h3> </div> <div class="panel-body"> <alert-box theme="default"></alert-box> <form class="form-horizontal" ng-submit="save()"> <div class="form-group"> <label for="todoName" class="col-sm-2 control-label">Name</label> <div class="col-sm-10"> <input type="text" class="form-control" id="todoName" placeholder="Name" ng-model="todoListItem.name"> </div> </div> <div class="form-group"> <label for="todoName" class="col-sm-2 control-label">Status</label> <div class="col-sm-10"> <select class="form-control" ng-model="todoListItem.status"> <option value="New">New</option> <option value="In Progress">In Progress</option> <option value="Completed">Completed</option> </select> </div> </div> <div class="form-group"> <label for="dueDate" class="col-sm-2 control-label">Due Date</label> <div class="col-sm-10"> <div class="dropdown"> <a class="dropdown-toggle" id="dueDate" role="button" data-toggle="dropdown" href="#"> <div class="input-group"><input type="text" class="form-control" data-date-time-input="MM-DD-YYYY hh:mm A" data-ng-model="todoListItem.dueDate"><span class="input-group-addon"><i class="glyphicon glyphicon-calendar"></i></span> </div> </a> <ul class="dropdown-menu" role="menu" aria-labelledby="dLabel"> <datetimepicker data-ng-model="todoListItem.dueDate" data-datetimepicker-config="{ dropdownSelector: '#dueDate', startView: 'year' }"/> </ul> </div> </div> </div> <div class="form-group"> <div class="col-sm-offset-2 col-sm-10"> <button type="submit" class="btn btn-default" ng-click="saveItem()"><i class="fa fa-check"></i> Save</button> </div> </div> </form> </div><!-- body --> </div><!-- end panel --> </div>
`, controller: '['$scope', '$state', '$stateParams', 'toDoItem', 'AlertService', 'ToDoList', function ($scope, $state, $stateParams, toDoItem, AlertService, ToDoList) { // initalization if (toDoItem) { // update existing $scope.todoListItem = toDoItem; } else { // create new $scope.todoListItem = { name: '', status: 'New', dueDate: new Date(moment().format('MM-DD-YYYY hh:mm A')) }; } // save new item or update existing $scope.saveItem = function () { // attach authorId to generate relationship $scope.todoListItem.authorId = $scope.User.getCurrentId(); AlertService.reset(); ToDoList.toDoItems.create({ id: $stateParams.listId }, $scope.todoListItem) .$promise .then(function (response) { // success alert AlertService.setSuccess({ show: true, title: $scope.todoListItem.name + ' saved successfully.', persist: true }); // transition to prev state $state.transitionTo('app.todos.list'); }) .catch(function (err) { // error alert AlertService.setError({ show: true, title: 'Error saving List Item', lbErr: err }); }); }; }], resolve: { toDoItem: ['$stateParams', 'ToDoItem', function ($stateParams, ToDoItem) { if (!$stateParams.itemId) return null; return ToDoItem.findById({ id: $stateParams.itemId }) .$promise .then(function (res) { return res; }) .catch(function (err) { return null; }); }] } });

This is a highly modular AngularJS Service which I wrote to easily handle any error/success alerts in an easy-to-use manner. This service was designed to be used with LoopBack so that it can easily parse user-friendly error messages from the server's error response.


.factory('AlertService', ['$log', function($log) {
     return {
      show: false,
      persist: false,
        /**
          * setError({ show: true, lbErr: err, title: 'string', ... }) - @params { object } 
          * - Create error with any number of properties and use alert-box
          *   directive to create custom templates
          */
      reset: function () {
        if (this.persist) return this.persist = false;

        this.show = false;
        var $this = this;
        angular.forEach(this, function(value, key) {
          // restricts users from overwriting functions
          if (!angular.isFunction(value)) {
            $this[key] = null;
          }
        });
      },
      showAlert: function () {
        this.show = true;
      },
      hasAlert: function() {
        return this.show ? true : false;
      },
      setError: function(alertObj) {
        // reference this object
        var alert = this;
        this.type = 'error';
        // first attach alert properties to current object
        angular.forEach(alertObj, function(value, key) {
          if (!angular.isFunction(value)) {
            alert[key] = value;
          }
        });
        // process a loopback err
        if (alert.lbErr) {
          $log.debug('lbErr: ', alert.lbErr);
          // check err obj for user friendly msgs
          if (alert.lbErr.data 
              && alert.lbErr.data.error 
              && alert.lbErr.data.error.details 
              && alert.lbErr.data.error.details.messages) {
            var errMsgs = alert.lbErr.data.error.details.messages;
            var errors = [];
            angular.forEach(errMsgs, function(errArr) {
              angular.forEach(errArr, function(errMsg) {
                errors.push(errMsg);
              });
            });
            // set user friendly loopback errors
            alert.errors = errors;
          }
          // check err obj for error title
          if (alert.lbErr.data 
              && alert.lbErr.data.error 
              && alert.lbErr.data.error.message) {
            var errTitle = alert.lbErr.data.error.message;
            // set user friendly error title
            alert.title = errTitle;
          }
          // overwrite title with passed in 1
          if (alertObj.title) {
            alert.title = alertObj.title;
          }
        }
      },
      setSuccess: function(alertObj) {
        // reference this object
        var alert = this;
        this.type = 'success';
        // first attach alert properties to current object
        angular.forEach(alertObj, function(value, key) {
          if (!angular.isFunction(value)) {
            alert[key] = value;
          }
        });
      }
       
    };
  }])
  
  .directive('alertBox', ['AlertService', function(AlertService) {
    return {
      restrict: 'E',
      templateUrl: function(scope, elem) {
        // Use default theme if no theme is provided
        if (elem.theme) {
          return 'js/directives/alert-box/' + elem.theme + '.html'
        } else {
          return 'js/directives/alert-box/default.html'
        }
      },
      link: function(scope, elem, attrs, ctrl) {
        // attach AlertService to alert-box's scope
        scope.alert = AlertService;
      }
    };
  }]);
  
  // 'js/directives/alert-box/default.html'
<div class="panel" ng-class="{'panel-danger': alert.type === 'error', 'panel-success': alert.type === 'success' }" 
     ng-if="alert.show" role="alert">
  <div class="panel-heading">
    <button type="button" class="close" aria-label="Close" ng-click="alert.show = false">
      <span aria-hidden="true">&times;</span>
    </button>
    <h3 class="panel-title" ng-if="alert.title">
      <span>
        <i ng-if="alert.type === 'error'" class="fa fa-exclamation"></i>
        <i ng-if="alert.type === 'success'" class="fa fa-check"></i>&nbsp;{{alert.title}}
      </span>
    </h3>
  </div>
  <div class="panel-body" ng-if="alert.errors">
    <ul>
      <li ng-repeat="error in alert.errors">{{error}}</li>
    </ul>
  </div>
</div>

Model declaration for ToDoLists. ACLs are used to enforce security restrictions.

 UltraEdit source file - tab-js.js
{
  "name": "ToDoList",
  "base": "PersistedModel",
  "strict": true,
  "idInjection": true,
  "options": {
    "validateUpsert": true
  },
  "properties": {
    "name": {
      "type": "string",
      "required": true
    },
    "dueDate": {
      "type": "date"
    }
  },
  "validations": [],
  "relations": {
    "author": {
      "type": "belongsTo",
      "model": "user",
      "foreignKey": "authorId"
    },
    "toDoItems": {
      "type": "hasMany",
      "model": "ToDoItem",
      "foreignKey": "toDoListId"
    }
  },
  "acls": [
    {
      "principalType": "ROLE",
      "principalId": "$everyone",
      "permission": "DENY"
    },
    {
      "accessType": "CREATE",
      "principalType": "ROLE",
      "principalId": "$authenticated",
      "permission": "ALLOW"
    },
    {
      "accessType": "READ",
      "principalType": "ROLE",
      "principalId": "$owner",
      "permission": "ALLOW"
    },
    {
      "accessType": "UPDATE",
      "permission": "ALLOW",
      "principleType": "ROLE",
      "principleId": "$owner"
    },
    {
      "accessType": "DELETE",
      "permission": "ALLOW",
      "principleType": "ROLE",
      "principleId": "$owner"
    }
  ],
  "methods": {}
}

Model declaration for ToDoItems. ACLs are used to enforce security restrictions.

 UltraEdit source file - tab-js.js
{
  "name": "ToDoItem",
  "base": "PersistedModel",
  "strict": true,
  "idInjection": true,
  "options": {
    "validateUpsert": true
  },
  "properties": {
    "name": {
      "type": "string",
      "required": true
    },
    "dueDate": {
      "type": "date"
    },
    "status": {
      "type": "string"
    }
  },
  "validations": [],
  "relations": {
    "toDoList": {
      "type": "belongsTo",
      "model": "ToDoList",
      "foreignKey": ""
    }
  },
  "acls": [
    {
      "principalType": "ROLE",
      "principalId": "$everyone",
      "permission": "DENY"
    },
    {
      "accessType": "CREATE",
      "principalType": "ROLE",
      "principalId": "$authenticated",
      "permission": "ALLOW"
    },
    {
      "accessType": "READ",
      "principalType": "ROLE",
      "principalId": "$owner",
      "permission": "ALLOW"
    },
    {
      "accessType": "UPDATE",
      "permission": "ALLOW",
      "principleType": "ROLE",
      "principleId": "$owner"
    },
    {
      "accessType": "DELETE",
      "permission": "ALLOW",
      "principleType": "ROLE",
      "principleId": "$owner"
    }
  ],
  "methods": {}
}