{"id":20956824,"url":"https://github.com/markbrown4/gmail-angular","last_synced_at":"2025-05-14T05:32:30.215Z","repository":{"id":20181013,"uuid":"23451983","full_name":"markbrown4/gmail-angular","owner":"markbrown4","description":"An Angular tutorial - building a Gmail clone.","archived":false,"fork":false,"pushed_at":"2016-04-06T23:56:21.000Z","size":132,"stargazers_count":23,"open_issues_count":0,"forks_count":11,"subscribers_count":4,"default_branch":"master","last_synced_at":"2023-03-26T17:13:21.155Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"CSS","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/markbrown4.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2014-08-29T03:59:06.000Z","updated_at":"2023-02-08T11:59:30.000Z","dependencies_parsed_at":"2022-09-02T20:11:02.654Z","dependency_job_id":null,"html_url":"https://github.com/markbrown4/gmail-angular","commit_stats":null,"previous_names":[],"tags_count":null,"template":null,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markbrown4%2Fgmail-angular","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markbrown4%2Fgmail-angular/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markbrown4%2Fgmail-angular/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/markbrown4%2Fgmail-angular/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/markbrown4","download_url":"https://codeload.github.com/markbrown4/gmail-angular/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":225277518,"owners_count":17448697,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-19T01:28:03.978Z","updated_at":"2024-11-19T01:28:04.736Z","avatar_url":"https://github.com/markbrown4.png","language":"CSS","funding_links":[],"categories":[],"sub_categories":[],"readme":"# nGmail\n\nWhat better example of a rich client-side application than Gmail, the iconic web app that started it all.\n\nIn this tutorial we'll explain all of the important components of Angular - Modules, Controllers, Scopes, Directives, Services and Filters whilst building out a Gmail clone.  No prior knowledge of Angular is necessary.\n\n[Screencast](http://youtu.be/1P2lPfJejek) - Bump up the quality to 720p for better viewing.\n\n### Prerequisites\n\nYou'll need npm installed and an intermediate knowledge of JavaScript and a tolerance or love of CoffeeScript.\n\n## Install\n\n```bash\ngit clone https://github.com/markbrown4/gmail-angular\ncd gmail-angular\ngit checkout origin/start\nnpm start\n```\n\nIn a separate process watch our assets for changes\n\n```bash\nnpm run assets\n```\n\nHit [http://localhost:8000/](http://localhost:8000/) in your favourite browser and you should see a bunch of familiar Gmail elements on the screen - you'll be bringing that static page to life and responding to events, just like Pinocchio.\n\nIf you're not familiar with Bower, it simply downloads the dependencies listed in bower.json into the bower_components/ directory.  These are already included in scripts at the bottom of index.html\n\n```html\n...\n\u003cscript src=\"bower_components/angular/angular.js\"\u003e\u003c/script\u003e\n\u003cscript src=\"bower_components/angular-route/angular-route.js\"\u003e\u003c/script\u003e\n\u003cscript src=\"bower_components/angular-resource/angular-resource.js\"\u003e\u003c/script\u003e\n\u003cscript src=\"bower_components/angular-sanitize/angular-sanitize.js\"\u003e\u003c/script\u003e\n\u003cscript src=\"app/js/app.js\"\u003e\u003c/script\u003e\n\u003cscript src=\"app/js/controllers.js\"\u003e\u003c/script\u003e\n\u003cscript src=\"app/js/directives.js\"\u003e\u003c/script\u003e\n\u003cscript src=\"app/js/filters.js\"\u003e\u003c/script\u003e\n\u003cscript src=\"app/js/services.js\"\u003e\u003c/script\u003e\n\u003c/body\u003e\n```\n\nAll you need to do to start using Angular is to include angular.js and whack an ng-app attribute on the part of the page you want to use it - Let's add it to the `\u003chtml\u003e` tag so we can use it everywhere.\n\n```html\n\u003chtml lang=\"en\" ng-app\u003e\n```\n\nThis special attribute is an angular directive, we'll cover directives in detail later, for now all you need to know is that they are attributes or elements for expanding the capabilities of HTML.\n\n*Congratulations*, you're running Angular!\n\nWe can now start adding expressions and directives anywhere within the document, add an expression to the title tag to confirm everything is working as it should.\n\n```html\n\u003ctitle\u003enGmail {{ (2 + 2) + \"!\" }}\u003c/title\u003e\n```\n\nAfter loading the title in the browser should read nGmail 4!\n\nThese expressions in double curly braces are auto updated bindings that are evaluated whenever the underlying data changes, you'll be using these whenever you want to dynamically produce a value in the HTML.\n\n### Modules\n\nModules are a way to group related controllers, directives and services together.  Let's start by creating a module for our application and adding the name to the ng-app attribute we added earlier.  angular.module takes 2 arguments, a name and an array of dependencies, which we don't require just yet.\n\n\n*app.coffee*\n\n```coffee\nwindow.app = angular.module('nGmail', [])\n```\n\n*index.html*\n\n```html\n\u003chtml lang=\"en\" ng-app=\"nGmail\"\u003e\n```\n\n## The Inbox\n\nLet's start by making our list of threads in the inbox dynamic.\n\n\n### Controllers\n\nControllers are responsible for exposing variables and functions to the view through a $scope object, you link an HTML element to a controller through the ng-controller attribute\n\n```html\n\u003cul id=\"threads\" ng-controller=\"ThreadsController\"\u003e\n...\n\u003c/ul\u003e\n```\n\nWhen creating a controller you give it a name a name and a list of dependencies, in this case we want to pass $scope so we can send data back to the view and $http so we can fetch JSON.\n\n*controllers.coffee*\n\n```coffee\napp.controller 'ThreadsController', ($scope, $http)-\u003e\n  $scope.threads = []\n  $http.get('/api/threads/index.json').success (data)-\u003e\n    $scope.threads = data\n```\n\nThis code is pretty straight forward, we're making a request to `/api/threads/index.json` and saving the array in `$scope.threads`\n\nOur view has access to all properties on this scope so we can start looping through the threads and making it dynamic.\n\n*index.html*\n\n```html\n\u003cul id=\"threads\" ng-controller=\"ThreadsController\"\u003e\n  \u003cli ng-repeat=\"thread in threads\"\u003e\n    \u003ca href\u003e\n      \u003ctime\u003e{{ thread.last_message.created_at }}\u003c/time\u003e\n      \u003cspan class=\"check\"\u003e\u003c/span\u003e\n      \u003cspan class=\"people\"\u003e\n        \u003cspan class=\"name\" ng-repeat=\"person in thread.participants\"\u003e{{ person.first_name }} {{ person.last_name }} \u003c/span\u003e\n        \u003cspan\u003e({{ thread.message_count }})\u003c/span\u003e\n      \u003c/span\u003e\n      \u003cspan class=\"subject\"\u003e{{ thread.last_message.subject }}\u003c/span\u003e\n      \u003cspan class=\"body\"\u003e- {{ thread.last_message.snippet }}\u003c/span\u003e\n    \u003c/a\u003e\n  \u003c/li\u003e\n\u003c/ul\u003e\n```\n\nThe only new piece in this code above is the ng-repeat directive, which we're using to loop through the threads and the participants.\n\n### Filters\n\nWe can format the date using the date filter - filters are tacked onto the end of an expression with a pipe followed by any arguments separated by colons.\n\n```html\n\u003ctime\u003e{{ thread.last_message.created_at  | date : \"MMM dd\" }}\u003c/time\u003e\n```\n\nGmail does even better by returning a time if it's less than 1 day old, let's achieve this by making a new filter to format our smartDate\n\n*filters.coffee*\n\n```coffee\napp.filter 'smartDate', ($filter)-\u003e\n  $dateFilter = $filter('date')\n\n  (date)-\u003e\n    oneDayAgo = Date.now() - 86400000\n    if date \u003c oneDayAgo\n      $dateFilter(date, \"MMM dd\")\n    else\n      $dateFilter(date, \"h:mm a\")\n```\n\nWe're passing in the $filter dependency because we want to make use of Angular's date filter\n\n```html\n\u003ctime\u003e{{ thread.last_message.created_at  | smartDate }}\u003c/time\u003e\n```\n\nTo confirm this is working update one of the last messages in our JSON with a fresh timestamp generated with `Date.now()` in the console, refresh the page and you should see a mix of dates and times in your inbox.\n\n```json\n\"last_message\": {\n  ...\n  \"created_at\": 1409366556530\n}\n```\n\nBoom.\n\nGmail applies different styles to the thread and names if there's unread messages, let's apply these via the `ng-class` directive\n\n```html\n\u003cli ng-repeat=\"thread in threads\" ng-class=\"{ unread: thread.unread }\"\u003e\n  ...\n  \u003cspan class=\"name\" ng-repeat=\"person in thread.participants\" ng-class=\"{ unread: person.unread }\"\u003e\n```\n\nNow we'll see some nicely highlighted threads and names in our list if there's unread messages.  Angular is smart enough to mix `class` and `ng-class` and not clobber anything.  Look back at `threads/index.json` to see where these unread booleans are coming from.\n\nThe list of participants still needs work, Gmail also does these things:\n\n- Replaces your name with \"me\"\n- Comma separates names\n- Only shows first names if the number of participants is greater than 1\n- Only shows the message count if it's greater than 1\n\nLet's put a global object in the page called `currentUser` so we can test against the current signed in user.\n\n*index.html*\n\n```html\n...\n\u003cscript\u003e\nwindow.currentUser = {\n  email: 'markbrown4@gmail.com',\n  first_name: 'Mark',\n  last_name: 'Brown',\n  avatar: 'me.jpg',\n  accounts: [{\n    id: 1,\n    email: \"markbrown4@gmail.com\",\n    first_name: \"Mark\",\n    last_name: \"Brown\",\n    avatar: \"me.jpg\"\n  },{\n    id: 2,\n    email: \"mark@inspire9.com\",\n    first_name: \"Mark\",\n    last_name: \"Brown\",\n    avatar: \"me.jpg\"\n  },{\n    id: 3,\n    email: \"mark@adioso.com\",\n    first_name: \"Mark\",\n    last_name: \"Brown\",\n    avatar: \"me.jpg\"\n  }]\n}\n\u003c/script\u003e\n\u003c/body\u003e\n```\n\nWe could use `$http` to fetch the current signed in user but it's best to bootstrap core data like this on page load as the page is useless without it, why wait for a second response before we can make the page do something? Global variables are rightfully frowned upon but I make an exception with things like this as I do want to be to access them globally.\n\nLet's make another filter called `smartName` to apply our logic.\n\n```coffee\napp.filter 'smartName', -\u003e\n  (person, fullName=false)-\u003e\n    if currentUser.email == person.email\n      'me'\n    else if fullName\n      \"#{person.first_name} #{person.last_name}\".trim()\n    else\n      person.first_name\n```\n\nIn the view we'll use our `smartName` filter, passing through true if the threads `message_count` is 1\n\n```html\n\u003cspan class=\"people\"\u003e\n  \u003cspan ng-repeat=\"person in thread.participants\"\u003e\n    \u003cspan class=\"name\" ng-class=\"{ unread: person.unread }\"\u003e{{ person | smartName : thread.message_count == 1 }}\u003c/span\u003e{{ $last ? '': ', ' }}\n  \u003c/span\u003e\n  \u003cspan ng-show=\"thread.message_count \u003e 1\"\u003e({{ thread.message_count }})\u003c/span\u003e\n\u003c/span\u003e\n```\n\nWithin loops we can access a few magic variables like `$index`, `$first` and `$last`, we're using `$last` to conditionally omit the last comma.\n\nThe `ng-show` directive conditionally applies an 'ng-hide' class to set \"display: none\" on elements.  We could also use the inverse directive `ng-hide` to achieve the same thing.\n\n```html\n\u003cspan ng-hide=\"thread.message_count == 1\"\u003e({{ thread.message_count }})\u003c/span\u003e\n```\n\nThe last thing we'll do on the inbox for now is wiring up the selected states when you toggle the checkbox, with an ng-click directive we access anything in the current scope so we can simply toggle a property on the thread and display a class on the list item.\n\n```html\n\u003cli ng-repeat=\"thread in threads\" ng-class=\"{ unread: thread.unread, selected: thread.selected }\"\u003e\n  ...\n  \u003cspan class=\"check\" ng-click=\"thread.selected = !thread.selected\"\u003e\u003c/span\u003e\n```\n\nThis is the first example of 2 way binding that we've seen so far, we didn't need to do anything special to apply the selected class Angular automatically updates these bound expressions whenever the underlying data changes.\n\nNow, we do a celebratory backfilp.  The inbox is looking sharp, and we've written surprising little code to do it.\n\n## A second view\n\nLet's move the `#threads` ul from `index.html` into `partials/threads.html` and shift the `#thread` div into `partials/thread.html` and load the correct template based on the route.  Add an `ng-view` directive to the now empty `#content` div to say where these views should be rendered inside.\n\n```html\n\u003cdiv id=\"content\" ng-view\u003e\u003c/div\u003e\n```\n\nWe'll inject the `ngRoute` dependencies into our `app` module, configure our routes to load the correct controller and template, and while we're here also expose our `currentUser` on `$rootScope`, making it available in all of the views.\n\n*app.coffee*\n\n```coffee\nwindow.app = angular.module('nGmail', ['ngRoute'])\n\napp.run ($rootScope)-\u003e\n  $rootScope.current_user = window.currentUser\n\napp.config ($routeProvider)-\u003e\n  $routeProvider\n    .when '/inbox',\n      templateUrl: 'partials/threads.html'\n    .when '/threads/:id',\n      templateUrl: 'partials/thread.html'\n    .otherwise\n      redirectTo: '/inbox'\n```\n\nBy default the routing will use the hash, we can easily make the router use pushState for updating the urls through `$locationProvider` but let's leave this out for now.  We link the views together using good old fashioned anchor tags.\n\n*partials/threads.html*\n\n```html\n\u003ca href=\"#/threads/{{ thread.id }}\"\u003e\n```\n\n*index.html*\n\n```html\n\u003ca href=\"#/inbox\" class=\"btn\"\u003e\u003cimg src=\"/public/images/icons/back.png\"\u003e\u003c/a\u003e\n...\n\u003cli class=\"active\"\u003e\u003ca href=\"#/inbox\"\u003eInbox\u003c/a\u003e\u003c/li\u003e\n```\n\nThe thread detail view controller will need the `$routeParams` service so we can fetch the dynamic :id from our route.\n\n*controllers.coffee*\n\n```coffee\napp.controller 'ThreadController', ($scope, $http, $routeParams)-\u003e\n  $scope.thread = {}\n  $http.get(\"/api/threads/#{ $routeParams.id }.json\").success (data)-\u003e\n    $scope.thread = data\n```\n\nNow for the view.\n\n*partials/thread.html*\n\n```html\n\u003cdiv id=\"thread\" ng-controller=\"ThreadController\"\u003e\n  \u003ch1\u003e{{ thread.messages[0].subject }}\u003c/h1\u003e\n  \u003cul class=\"messages\"\u003e\n    \u003cli ng-repeat=\"message in thread.messages\" ng-class=\"{ active : message.active }\"\u003e\n      \u003cdiv class=\"thread-tools\"\u003e\n        \u003ctime\u003e{{ message.created_at | smartDate }} (timeAgo)\u003c/time\u003e\n        \u003cdiv class=\"split-btn\" ng-show=\"message.active\"\u003e\n          \u003ca href class=\"btn\"\u003e\u003cimg src=\"/images/icons/reply.png\"\u003e\u003c/a\u003e\n          \u003cdiv class=\"drop-down btn btn-mini\"\u003e\n            \u003cimg src=\"/images/icons/down.png\"\u003e\n            \u003cul class=\"align-right\"\u003e\n              \u003cli\u003e\u003ca href\u003eReply\u003c/a\u003e\u003c/li\u003e\n              \u003cli\u003e\u003ca href\u003eReply all\u003c/a\u003e\u003c/li\u003e\n              \u003cli\u003e\u003ca href\u003eForward\u003c/a\u003e\u003c/li\u003e\n            \u003c/ul\u003e\n          \u003c/div\u003e\n        \u003c/div\u003e\n      \u003c/div\u003e\n      \u003cimg class=\"avatar\" src=\"/images/avatars/{{ message.from.avatar }}\"\u003e\n      \u003cdiv class=\"from\"\u003e\n        \u003cspan class=\"name\"\u003e{{ message.from | smartName }}\u003c/span\u003e\n        \u003cspan class=\"email\"\u003e\u0026lt;{{ message.from.email }}\u0026gt;\u003c/span\u003e\n      \u003c/div\u003e\n      \u003cdiv class=\"to\" ng-show=\"message.active\"\u003eto\n        \u003cspan ng-repeat=\"person in message.to\"\u003e{{ person | smartName : false }}{{ $last ? '': ', ' }}\u003c/span\u003e\n      \u003c/div\u003e\n      \u003cdiv class=\"body\" ng-bind-html=\"message.active ? message.body : message.snippet\"\u003e\u003c/div\u003e\n    \u003c/li\u003e\n  \u003c/ul\u003e\n  \u003cdiv class=\"reply\"\u003e\n    \u003cimg class=\"avatar\" src=\"/images/avatars/{{ current_user.avatar }}\"\u003e\n    \u003cdiv class=\"reply-box\"\u003e\n      \u003cp\u003eClick here to \u003ca href\u003eReply\u003c/a\u003e, \u003ca href\u003eReply to all\u003c/a\u003e or \u003ca href\u003eForward\u003c/a\u003e\u003c/p\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n```\n\nThe only new piece in the above template is the `ng-bind-html` directive which will santize the HTML before inserting it into the document - We'll need to pass it in as a dependency for this to work.\n\n```coffee\nwindow.app = angular.module('nGmail', ['ngRoute', 'ngSanitize']\n```\n\nGreat, now the data in our thread list is dynamic too.\n\nThere's something funky going on with the images though, they are displaying ok but in console there's 404's that have started popping up.\n\nThe reason for this is that the browser is requesting the image at \"images/avatars/{{ message.from.avatar }}\" before Angular has had a chance go in and rewrite those attributes.\n\n```html\n\u003cimg src=\"images/avatars/{{ message.from.avatar }}\"\u003e\n```\n\nWe need to change these `src` attributes to `ng-src` which is specifically there to solve this problem.\n\n```html\n\u003cimg ng-src=\"images/avatars/{{ message.from.avatar }}\" class=\"avatar\"\u003e\n...\n\u003cimg ng-src=\"images/avatars/{{ current_user.avatar }}\" class=\"avatar\"\u003e\n```\n\nNo more 404's.\n\nLet's add one more filter for handling the `timeAgo`\n\n```coffee\napp.filter 'timeAgo', ($filter)-\u003e\n  units = [\n    { name: \"second\", limit: 60, in_seconds: 1 },\n    { name: \"minute\", limit: 3600, in_seconds: 60 },\n    { name: \"hour\", limit: 86400, in_seconds: 3600  },\n    { name: \"day\", limit: 604800, in_seconds: 86400 },\n    { name: \"week\", limit: 2629743, in_seconds: 604800  },\n    { name: \"month\", limit: 31556926, in_seconds: 2629743 },\n    { name: \"year\", limit: null, in_seconds: 31556926 }\n  ]\n\n  (date)-\u003e\n    diff = (Date.now() - date)/1000\n    return \"just now\" if diff \u003c 5\n\n    for unit in units\n      if diff \u003c unit.limit || !unit.limit\n        diff =  Math.floor(diff / unit.in_seconds)\n        return \"#{diff} #{unit.name}#{ if diff \u003e 1 then 's' else '' } ago\"\n```\n\nAnd update the view accordingly\n\n```html\n\u003ctime\u003e{{ message.created_at | smartDate }} ({{ message.created_at | timeAgo }})\u003c/time\u003e\n```\n\nDifferent parts of this view are visible when the message is active, let's toggle this value using a new method we'll place on the `$scope`.\n\n```html\n\u003cli ng-repeat=\"message in thread.messages\" ng-class=\"{ active : message.active }\" ng-click=\"toggleActive(message)\"\u003e\n```\n\nGmail automatically activates the last message and doesn't let us toggle it, let's do this in the controller.\n\n```coffee\napp.controller 'ThreadController', ($scope, $routeParams, $http)-\u003e\n\n  $http.get(\"/api/threads/#{ $routeParams.id }.json\").success (data)-\u003e\n    $scope.thread = data\n    $scope.lastMessage = thread.messages[thread.messages.length-1]\n    $scope.lastMessage.active = true\n\n  $scope.toggleActive = (message)-\u003e\n    unless message == $scope.lastMessage\n      message.active = !message.active\n```\n\nNice!  We can now click between our inbox and specific thread, fetch data from our API, toggle some application state, format things nicely with custom filters - things are shaping up nicely.\n\n### Directives\n\nWe did manage to break something by adding links to our threads though, the checkboxes 🙁\n\nWhen you click on a checkbox it toggles and then follows the parent link, let's make a custom directive `stopEvent` which will prevent the event moving up to the parent link.\n\n*directives.coffee*\n\n```coffee\napp.directive 'stopEvent', -\u003e\n  restrict: 'A'\n  link: (scope, element, attr)-\u003e\n    element.bind 'click', (event)-\u003e\n      event.preventDefault()\n      event.stopPropagation()\n```\n\nThe whacky \"restrict: 'A'\" limits this directive to \u003cb\u003eA\u003c/b\u003ettributes, we'll look at \u003cb\u003eE\u003c/b\u003elements next.\n\nThe link function fires just after the element is added to the DOM so it's safe to mess with.\n\nWe can now start sprinkling our new `stop-event` attribute anywhere in the document we want this behavior to occur, note the normalised casing for both the directive name and attribute.\n\n```html\n\u003cspan class=\"check\" ng-click=\"thread.selected = !thread.selected\" stop-event\u003e\u003c/span\u003e\n```\n\nYay, It works!\n\nOne place we can make use of an element directive is building our drop down menus - we've got them sprinkled through the HTML already:\n\n```html\n\u003cdiv class=\"drop-down active\"\u003e\n  \u003cimg src=\"/public/images/icons/down.png\"\u003e\n  \u003cul class=\"align-right\"\u003e\n    \u003cli\u003e\u003ca href\u003eAn option\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href\u003eYet another\u003c/a\u003e\u003c/li\u003e\n  \u003c/ul\u003e\n\u003c/div\u003e\n```\n\nAngular allows us to create new HTML elements and attach our desired behavior, within `#sub-header` replace the drop down's `\u003cdiv\u003e` tag with a `\u003cdrop-down\u003e` element.\n\n```html\n\u003cdrop-down class=\"drop-down btn\"\u003e\n  ...\n\u003c/drop-down\u003e\n```\n\nThen add our new directive that will toggle a class on the root element when clicked\n\n```coffee\napp.directive 'dropDown', -\u003e\n  restrict: 'E'\n  link: (scope, el, attr)-\u003e\n    el.bind 'click', (event)-\u003e\n      event.preventDefault()\n      angular.element(this).toggleClass 'active'\n```\n\n`angular.element` delegates to jQuery if present, or in our case uses their own mini jQuery alternative *jqLite*, jqLite is a really nice option if you don't need the whole hog - it only includes a very small subset of the features so it's not a drop-in replacement (https://code.google.com/p/jqlite/wiki/UsingJQLite)\n\nYou could use jQuery to do something similar though, and if this was all the drop down element was doing there's not much difference between them.\n\n```coffee\n$(document.body).on 'click', '.drop-down', (event)-\u003e\n  event.preventDefault()\n  $(event.target).closest('.drop-down').toggleClass 'active'\n```\n\nDirectives give you a lot more power than what we're demonstrating here though, they allow access to `$scope`, can evaluate expressions on attributes and watch for changes to make updates i.e they make the element place nicely with the rest of Angular.  Any time you'd consider using jQuery for updating the DOM and responding to user events you should first consider using a directive.\n\nHere's a full featured drop down directive using the angular patterns if you're interested - https://github.com/angular-ui/bootstrap/blob/master/src/dropdown/dropdown.js\n\n## Services\n\nServices are objects that can be included anywhere else in our application like controllers, directives and filters.  There's a few different methods for creating them:\n\n- factory: for creating a singleton\n- service: for a constructor function\n- provider: if you want to be able to configure a passed in object.\n\nWe'll use the `ngResource` service to work with our Models, let's first add the dependency to our app module.\n\n```coffee\nwindow.app = angular.module('nGmail', ['ngRoute', 'ngSanitize', 'ngResource'])\n```\n\nModels in our application will be singletons so we'll use a factory.  In `services.coffee` make a `Thread` factory responsible for querying our API through `ngResource`. The second parameter to `$resource` is a list of default params that can be overridden in the query and get methods.\n\n```coffee\napp.factory 'Thread', ($resource)-\u003e\n  $resource '/api/threads/:id.json', { id: 'index' }\n```\n\nThen working with our models becomes considerably nicer in our controllers.  We just pass in `Thread` as a dependency and can then call methods like query and get to fetch our threads. Sweet.\n\n```coffee\napp.controller 'ThreadsController', ($scope, Thread)-\u003e\n  $scope.threads = []\n  Thread.query (threads)-\u003e\n    $scope.threads = threads\n\napp.controller 'ThreadController', ($scope, $routeParams, Thread)-\u003e\n  $scope.thread = {}\n  Thread.get { id: $routeParams.id }, (thread)-\u003e\n    $scope.thread = thread\n    $scope.lastMessage = thread.messages[thread.messages.length-1]\n    $scope.lastMessage.active = true\n  ...\n```\n\n### Menu States\n\nLet's work through the `#sub-header` element next and make it respond to state changes.\n\nThe first thing we'll do is promote `ThreadsController` to manage more of the page so it's scope data can effect the sub-header too - Move it from `ul#threads` to `div#wrapper`\n\n```html\n\u003cdiv id=\"wrapper\" ng-controller=\"ThreadsController\"\u003e\n```\n\nAfter querying our data we can save the counts for some simple paging.\n\n```coffee\napp.controller 'ThreadsController', ($scope, Thread)-\u003e\n  $scope.threads = []\n  Thread.query (threads)-\u003e\n    $scope.threads = threads\n    $scope.page =\n      from: 1\n      to: threads.length\n      count: threads.length\n```\n\nWe won't have any real paging data without a back-end but here's how it could be implemented. Display 1 of 1 if there's 1 record, 1-3 of 3 for 3 records and showing inactive states on the next and previous buttons.\n\n```html\n\u003cdiv class=\"paging\"\u003e\n  \u003cstrong\u003e{{ page.from == page.to ? page.from : page.from + '-'+ page.to }}\u003c/strong\u003e\n  of\n  \u003cstrong\u003e{{ page.count }}\u003c/strong\u003e\n  \u003cdiv class=\"split-btn\"\u003e\n    \u003ca href class=\"btn btn-mini\" title=\"Previous\" ng-class=\"{ inactive: page.from == 1 }\"\u003e\u003cimg src=\"images/icons/prev.png\"\u003e\u003c/a\u003e\n    \u003ca href class=\"btn btn-mini\" title=\"Next\" ng-class=\"{ inactive: page.to == page.count }\"\u003e\u003cimg src=\"images/icons/next.png\"\u003e\u003c/a\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n```\n\nThe back button we only want to show if we're not already on the index page\n\n```html\n\u003ca href=\"#/inbox\" class=\"btn\" ng-hide=\"isRouteActive('/inbox')\"\u003e\u003cimg src=\"images/icons/back.png\"\u003e\u003c/a\u003e\n```\n\nBecause our `isRouteActive` helper will be helpful in many pages of the app let's add it to `$rootScope`, the `$location` service gives us access to the active route.\n\n*app.coffee*\n\n```coffee\napp.run ($rootScope, $location)-\u003e\n  ...\n  $rootScope.isRouteActive = (route)-\u003e\n    route == $location.path()\n```\n\nNext, let's add more functions to our controller to handle the bulk select dropdown.\n\n```coffee\napp.controller 'ThreadsController', ($scope, Thread)-\u003e\n  ...\n  $scope.selectAll = -\u003e\n    for thread in $scope.threads\n      thread.selected = true\n\n  $scope.selectNone = -\u003e\n    for thread in $scope.threads\n      thread.selected = false\n\n  $scope.selectUnread = -\u003e\n    for thread in $scope.threads\n      thread.selected = thread.unread\n\n  $scope.selectRead = -\u003e\n    for thread in $scope.threads\n      thread.selected = !thread.unread\n\n  $scope.someSelected = -\u003e\n    selected = false\n    for thread in $scope.threads\n      selected = true if thread.selected\n\n    selected\n\n  $scope.noneSelected = -\u003e\n    !$scope.someSelected()\n\n  $scope.allSelected = -\u003e\n    return false if $scope.threads.length == 0\n    selected = true\n    for thread in $scope.threads\n      selected = false if !thread.selected\n\n    selected\n\n  $scope.selectToggle = -\u003e\n    if $scope.someSelected()\n      $scope.selectNone()\n    else\n      $scope.selectAll()\n```\n\nLet's use all of these in the view, only showing the dropdown if it's the `/inbox` route\n\n```html\n\u003cdrop-down class=\"drop-down btn\" ng-show=\"isRouteActive('/inbox')\"\u003e\n  \u003ca href class=\"check\" ng-click=\"selectToggle()\" ng-class=\"{ 'all-selected': allSelected(), 'some-selected': someSelected() }\" stop-event\u003e\u003c/a\u003e\n  \u003cimg src=\"images/icons/down.png\"\u003e\n  \u003cul\u003e\n    \u003cli\u003e\u003ca href ng-click=\"selectAll()\"\u003eAll\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href ng-click=\"selectNone()\"\u003eNone\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href ng-click=\"selectRead()\"\u003eRead\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href ng-click=\"selectUnread()\"\u003eUnread\u003c/a\u003e\u003c/li\u003e\n  \u003c/ul\u003e\n\u003c/drop-down\u003e\n```\n\nThe next three buttons we only want to show if any threads are selected, the refresh button we only show if it's the `/index` route and no threads are selected.\n\n```html\n\u003cdiv class=\"split-btn\" ng-show=\"someSelected()\"\u003e\n  \u003ca href class=\"btn\" title=\"Archive\"\u003e\u003cimg src=\"images/icons/archive.png\"\u003e\u003c/a\u003e\n  \u003ca href class=\"btn\" title=\"Report Spam\"\u003e\u003cimg src=\"images/icons/spam.png\"\u003e\u003c/a\u003e\n  \u003ca href class=\"btn\" title=\"Delete\"\u003e\u003cimg src=\"images/icons/delete.png\"\u003e\u003c/a\u003e\n\u003c/div\u003e\n\u003ca href class=\"btn\" ng-show=\"isRouteActive('/inbox') \u0026\u0026 noneSelected()\"\u003e\u003cimg src=\"images/icons/refresh.png\"\u003e\u003c/a\u003e\n```\n\nNice.\n\n### Search\n\nIt's a little bit silly to implement a search without a backend but implementing a client-side search gives us a chance to look at two new features so we'll do it.\n\n```html\n\u003cinput name=\"query\" ng-model=\"query\"\u003e\n```\n\nThe `ng-model` directive saves the value of a form element on the current `$scope`, so we can pass it into a built in filter named.. `filter` which will do a fuzzy search on the data.\n\n```html\n\u003cli ng-repeat=\"thread in threads | filter : query\"\u003e\n```\n\nAnd there you go, a client-side fuzzy search in 30 characters or less.\n\n## Compose\n\nThe final component we'll make is the New Message popover, let's add a controller and add a visible state.\n\n*index.html*\n\n```html\n\u003cdiv id=\"compose\" ng-controller=\"ComposeController\" ng-show=\"visible\"\u003e\n```\n\n*controllers.coffee*\n\n```coffee\napp.controller 'ComposeController', ($scope)-\u003e\n  $scope.visible = false\n```\n\nNow, things get interesting here because the compose button that launches this sits outside of this controllers scope, we need a way to communicate across controllers.  There's a few ways to do this, one way is through events on `$rootScope`, or passing in a shared object(a service) as dependencies to both controllers.\n\n*index.html*\n\n```html\n\u003ca href class=\"compose\" ng-click=\"composeMessage()\"\u003eCOMPOSE\u003c/a\u003e\n```\n\nWe'll look at using events first, include `$rootScope` as a dependency and `$broadcast` our event when the button is clicked.\n\n*controllers.coffee*\n\n```coffee\napp.controller 'ThreadsController', ($rootScope, $scope, Thread)-\u003e\n  ...\n  $scope.composeMessage = -\u003e\n    $rootScope.$broadcast 'composeMessage'\n```\n\nThen in our `ComposeController`, listen for the composeMessage event on `$rootScope`\n\n```coffee\napp.controller 'ComposeController', ($rootScope, $scope)-\u003e\n  $scope.visible = false\n\n  $rootScope.$on 'composeMessage', -\u003e\n    $scope.visible = true\n```\n\nLet's add close and send click handlers to call functions in our controller.\n\n```html\n\u003ca class=\"close\" ng-click=\"close()\"\u003e\u0026times;\u003c/a\u003e\n...\n\u003cinput type=\"submit\" value=\"Send\" class=\"btn primary-btn\" ng-click=\"send()\"\u003e\n```\n\nBoth of these actions close the New Message popover so let's do that.\n\n```coffee\napp.controller 'ComposeController', ($rootScope, $scope)-\u003e\n  ...\n  $scope.close = -\u003e\n    $scope.visible = false\n\n  $scope.send = -\u003e\n    $scope.visible = false\n```\n\nAs a second example of cross controller messaging let's wire up a Flash that we can push messages like \"Sending...\" to and display at the top of the page.  We'll be using a simple singleton object `Flash` which we can pass around and different parts of the app can set it's message.\n\n*services.coffee*\n\n```coffee\napp.factory 'Flash', -\u003e\n  message: ''\n```\n\nOur view will reference a new controller and show itself depending on it's message.\n\n*index.html*\n\n```html\n\u003cdiv class=\"flash\" ng-controller=\"FlashController\" ng-show=\"flash.message.length \u003e 0\"\u003e\n  \u003cdiv class=\"inner\"\u003e{{ flash.message }}\u003c/div\u003e\n\u003c/div\u003e\n```\n\nWe'll need to expose our `Flash` object to the view through `$scope`\n\n```coffee\napp.controller 'FlashController',  ($scope, Flash)-\u003e\n  $scope.flash = Flash\n```\n\nNow we can inject `Flash` and display a message anywhere in the app that needs it, let's display \"Sending...\" from the send function and clear it after 1 second.\n\n```coffee\napp.controller 'ComposeController', ($rootScope, $scope, Flash)-\u003e\n  ...\n  $scope.send = -\u003e\n    $scope.visible = false\n    Flash.message = 'Sending...'\n    setTimeout -\u003e\n      Flash.message = ''\n    , 1000\n```\n\nThis displayed our message perfectly but the message never goes away.. WTF\n\nCode within `setTimeout` won't cause changes to scopes unless you explicitly call `$scope.apply()` - it's safer just to replace setTimeout with Angular's `$timeout` which behaves as you'd expect.\n\n```coffee\napp.controller 'ComposeController', ($rootScope, $scope, Flash, $timeout)-\u003e\n  ...\n  $timeout -\u003e\n    Flash.message = ''\n  , 1000\n```\n\nNow it displays the message and clears itself after a second, Nice.\n\nThe completed compose view looks this, we've added `ng-model` to our inputs, conditionally displayed sections depending on the active one, you can see how that's wired up below.\n\n```html\n\u003cdiv id=\"compose\" ng-controller=\"ComposeController\" ng-show=\"visible\"\u003e\n  \u003cdiv class=\"header\"\u003e\n    \u003ca class=\"close\" ng-click=\"close()\"\u003e\u0026times;\u003c/a\u003e\n    \u003ch2\u003eNew Message\u003c/h2\u003e\n  \u003c/div\u003e\n  \u003cdiv\u003e\n    \u003cdiv ng-hide=\"active_section == 'to'\"\u003e\n      \u003cinput placeholder=\"Recipients\" name=\"recipients\" class=\"full\" ng-focus=\"active_section = 'to'\" ng-model=\"message.to\"\u003e\n    \u003c/div\u003e\n    \u003cdiv ng-show=\"active_section == 'to'\"\u003e\n      \u003cdiv class=\"input\" ng-show=\"active_section == 'to'\"\u003e\n        \u003clabel for=\"message_to\"\u003eTo\u003c/label\u003e\n        \u003cdiv class=\"fit\"\u003e\n          \u003cinput id=\"message_to\" class=\"full\" ng-model=\"message.to\"\u003e\n        \u003c/div\u003e\n      \u003c/div\u003e\n      \u003cdiv class=\"input\" ng-show=\"cc_active\"\u003e\n        \u003clabel for=\"message_cc\"\u003eCc\u003c/label\u003e\n        \u003cdiv class=\"fit\"\u003e\n          \u003cinput id=\"message_cc\" class=\"full\" ng-model=\"message.cc\"\u003e\n        \u003c/div\u003e\n      \u003c/div\u003e\n      \u003cdiv class=\"input\" ng-show=\"bcc_active\"\u003e\n        \u003clabel for=\"message_bcc\"\u003eBcc\u003c/label\u003e\n        \u003cdiv class=\"fit\"\u003e\n          \u003cinput for=\"message_bcc\" class=\"full\" ng-model=\"message.bcc\"\u003e\n        \u003c/div\u003e\n      \u003c/div\u003e\n      \u003cdiv\u003e\n        \u003clabel\u003eFrom\u003c/label\u003e\n        \u003ca href class=\"bcc\" ng-click=\"bcc_active = true\" ng-hide=\"bcc_active\"\u003eBcc\u003c/a\u003e\n        \u003ca href class=\"cc\" ng-click=\"cc_active = true\" ng-hide=\"cc_active\"\u003eCc\u003c/a\u003e\n        \u003cdrop-down class=\"drop-down from-address\"\u003e\n          \u003cspan\u003e{{ message.from | nameAndEmail }}\u003c/span\u003e\n          \u003cimg src=\"images/icons/down.png\"\u003e\n          \u003cul class=\"align-right\"\u003e\n            \u003cli ng-repeat=\"account in current_user.accounts\"\u003e\u003ca href ng-click=\"message.from = account\"\u003e{{ account | nameAndEmail }}\u003c/a\u003e\u003c/li\u003e\n          \u003c/ul\u003e\n        \u003c/drop-down\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n  \u003cdiv\u003e\n    \u003cinput id=\"message_subject\" placeholder=\"Subject\" class=\"full\" ng-model=\"message.subject\" ng-focus=\"active_section = 'subject'\"\u003e\n  \u003c/div\u003e\n  \u003cdiv\u003e\n    \u003ctextarea id=\"message_body\" placeholder=\"Body\" ng-model=\"message.body\" ng-focus=\"active_section = 'body'\"\u003e\u003c/textarea\u003e\n  \u003c/div\u003e\n  \u003cdiv class=\"footer\"\u003e\n    \u003cinput type=\"submit\" value=\"Send\" class=\"btn primary-btn\" ng-click=\"send()\"\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n```\n\nThis view uses a new filter called `nameAndEmail` for formatting this common string.\n\n*filters.coffee*\n\n```coffee\napp.filter 'nameAndEmail', -\u003e\n  (person)-\u003e\n    \"#{ person.first_name } #{ person.last_name } \u003c#{ person.email }\u003e\"\n```\n\nWe also need to reset the message whenever the controller is closed\n\n*controllers.coffee*\n\n```coffee\napp.controller 'ComposeController', ($rootScope, $scope, Flash, $timeout)-\u003e\n  reset = -\u003e\n    $scope.visible = false\n    $scope.cc_active = false\n    $scope.bcc_active = false\n    $scope.active_section = null\n    $scope.message =\n      from: currentUser.accounts[0]\n\n  reset()\n\n  $rootScope.$on 'composeMessage', -\u003e\n    $scope.visible = true\n    $scope.active_section = 'to'\n\n  $scope.close = -\u003e\n    reset()\n\n  $scope.send = -\u003e\n    reset()\n\n    Flash.message = 'Sending...'\n    $timeout -\u003e\n      Flash.message = ''\n    , 1000\n```\n\nThe final touch will be controlling input focus, we'll set focus on the to field when it launches and focus on the cc and bcc fields when they're enabled with a a new directive focusWhen.  It watches if the passed in expressions value changes, when it produces a truthy value we'll focus our element.\n\n*directives.coffee*\n\n```coffee\napp.directive 'focusWhen', ($timeout)-\u003e\n  link: (scope, element, attrs)-\u003e\n    scope.$watch attrs.focusWhen, (value)-\u003e\n      return unless value\n      $timeout -\u003e\n        element[0].focus()\n```\n\nThen we can pass in an expression that states when our inputs should gain focus, easy!\n\n```html\n\u003cinput id=\"message_to\" class=\"full\" ng-model=\"message.to\" focus-when=\"active_section == 'to'\"\u003e\n\u003cinput id=\"message_cc\" class=\"full\" ng-model=\"message.cc\" focus-when=\"cc_active\"\u003e\n\u003cinput for=\"message_bcc\" class=\"full\" ng-model=\"message.bcc\" focus-when=\"bcc_active\"\u003e\n```\n\nThis concludes our exploration of some of Angular's most important concepts and features.\n\n**A note on compression**\n\nAngular's Dependency Injection API is a bit whack.. code like we've been writing below will explode when uglified because Angular uses the name of these arguments to find out which dependency to inject. Ouch.\n\n```js\napp.controller('ThreadController', function($scope, $routeParams, Thread) {\n\n});\n```\n\nThe solution is to add a sweaty armpit of doubled up names and arguments.  This is shit but it's the recommended way to do dependency injection.\n\n```js\napp.controller('ThreadController', ['$scope', '$routeParams', 'Thread', function($scope, $routeParams, Thread) {\n\n}]);\n```\n\n## Closing\n\nI've enjoyed learning Angular far more than I thought I would, it will be interesting to see how it feels as the complexity grows.\n\nAs we build out an app like this further we'd need to organise the code into related modules - everything so far we've whacked on the app module, a fully fledged Gmail application would have separate modules for any distinct components like Notifications, Settings, Chat etc..\n\nI'm finding the views automatic bindings to the underlying data particularly nice, the core components of modules, controllers, services, directives and filters are great.  I have certain gripes with the API and wish things could have been named better - nobody mention the `transclude` function! and I'm sure I'll drop a few wtf's when learning the differences between services, factories and providers but there's always things to learn.\n\nEnjoy.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarkbrown4%2Fgmail-angular","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmarkbrown4%2Fgmail-angular","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarkbrown4%2Fgmail-angular/lists"}