Skip to content

Instantly share code, notes, and snippets.

@zshannon
Last active August 29, 2015 14:06
Show Gist options
  • Save zshannon/9ae367d203bd71c3660a to your computer and use it in GitHub Desktop.
Save zshannon/9ae367d203bd71c3660a to your computer and use it in GitHub Desktop.
AngularJS Tips

Couple abstract ideas:

  • Angular apps are little separate kingdoms.
  • You can't get into them from outside, and it's really frowned upon to use jQuery within them to access the DOM.
  • Angular Controllers extend this concept. They can only control the DOM nodes at and below the node with ng-controller designated.

The loading order is somewhat important. I set it up like this: [jQuery, jQueryUI, Bootstrap, Underscore, Angular Library, any Angular Plugins, Angular Application init file, rest of angular files].

# Factory is angular for Model in MVC-speak
# @dispatched gets the application we setup in _init_app.coffee; 'Team' is the name of this model;
# the array param has the first n elements as the dependencies that become params for the last element,
# which defines and returns the model
@dispatched.factory 'Team', ['railsResourceFactory', 'railsSerializer', (railsResourceFactory, railsSerializer) ->
# initialize the object
Team = railsResourceFactory # using RailsResource for now, but plan to switch to [Angular-Data](http://angular-data.pseudobry.com/)
url: (context) -> # this gets called to generate every request URL
# if we have an instance, use its ID for Show/Update/Delete/etc.
if context[@idAttribute]?
"/teams/#{context[@idAttribute]}.json"
# otherwise we're working on the collection for Index/Create/Search/etc.
else
"/teams.json#{window.location.search}" # window.location.search is the query params like `?page=3&filter=pasta`
# team is the root node for JSON serialization from the API
# like: {'team':{name:'new orleans saints'}}
name: 'team'
extensions: ['snapshots'] # snapshots let us do undo
# I can add functions to the model here, too
# for example:
Team.prototype.helloName = (name) ->
"Hello, #{name}!"
Team
]
# Here's an example Controller. In Angular, Controllers and Controller/View hybrids in MVC-speak
# Every property of $scope is available as a variable in the View, including the functions
# The init syntax is similar to factory, except here we're depending on the Team model from team.coffee
@dispatched.controller('Teams', ['Team', '$scope', '$rootScope', (Team, $scope, $rootScope) ->
# initialize the scope
$scope.page = 1
$scope.teams = []
$scope.hasMore = true
$scope.loadingMore = false
# this gets called by our inifinite scrolling library
$scope.loadMore = ->
return true if ! $scope.hasMore || $scope.loadingMore
$scope.loadingMore = true
# NProgress is a library that shows the loading bar thing at the top of the page
# It's loaded as an external JS thing available globally (not related to angular)
NProgress.start()
# Team is from team.coffee. Query is a native function to RailsResource. Just calls index.
Team.query({page: $scope.page}).then (teams) -># it returns a promise . teams will be an array from the API
# I read somewhere this is a faster way to merge arrays in JS. Dunno.
for team in teams
$scope.teams.push team
# normal loading loop stuff
$scope.page++
$scope.hasMore = teams.length >= 25
$scope.loadingMore = false
NProgress.done()
# this gets called from the HTML view
# the HTML renderer already fetches page 1, so we load it here via JSON from the DOM
$scope.init = (teams) ->
for team in teams
$scope.teams.push new Team(team)
$scope.page++
$scope.hasMore = teams.length >= 25
# We use the Observer Pattern here to receive notes from other controllers
# There's another controller that presents a form to edit Teams that uses these
$rootScope.$on 'Teams.addedTeam', (event, team) ->
$scope.teams.push team
# these two are used by another controller not in this gist
$rootScope.$on 'Teams.addedMember', (event, team, member) ->
console.log 'addedMember', team, member
team.members.push member
$rootScope.$on 'Teams.removedMember', (event, team, member) ->
console.log 'removedMember', team, member
team.members.splice _.indexOf(member), 1
# functions on $scope are available in the View
$scope.newTeam = () ->
team = new Team()
# this is observed by that other controller
$rootScope.$emit 'Teams.presentForm', team
true
$scope.editTeam = (team) ->
$rootScope.$emit 'Teams.presentForm', team
true
$scope.deleteTeam = (team, $index) ->
return false unless team.id?
NProgress.start()
team.delete().then -># wait for railsResource to finish, then update the UI
$scope.teams.splice $index, 1
NProgress.done()
true
])
<div class="page-head">
<h2>Manage Teams</h2>
<ol class="breadcrumb">
<li class="active"><%= @Company.name %></li>
<li class="active">Teams</li>
</ol>
</div>
<div class="row-fluid">
<div class="col-sm-12">
<!--
This is the $$ part of angular. You can use the Angular router if you want to be sad, or just use
whatever routing mechanism's already in your app and sprinkle `ng-controller="XyzController"`
throughout your app views. Angular will automatically link this view to the Controller!
`ng-init` gets called automatically when the view renders. Notice inside it we specify `init`
again because we can call whatever $scope function we want, the name's incidental.
-->
<div class="content" ng-controller="Teams" ng-init="init(<%= @Teams.to_json %>)">
<div class="table-responsive">
<table class="table no-border hover">
<thead class="no-border">
<tr>
<th><strong>ID</strong></th>
<th><strong>Name</strong></th>
<th><strong>Members</strong></th>
<!--
Notice `ng-click` here will call `$scope.newTeam()`
-->
<th class="text-right"><button class="btn btn-primary btn-sm btn-flat" ng-click="newTeam()">+Team</button></th>
</tr>
</thead>
<!--
the `infinite-scroll` attrs are used by an external angular library
-->
<tbody id="teams" class="no-border-y" infinite-scroll="loadMore()" infinite-scroll-distance="3">
<!--
here's the main angular magic show. `ng-repeat` will re-render that node and its children
for each element in $scope.teams
-->
<tr ng-repeat="team in teams">
<td>{{ team.id }}</td> <!-- pretty standard JS templating style here. -->
<td>{{ team.name }}</td>
<td>
<span class="label label-primary" ng-repeat="member in team.members" style="margin-right:3px;" ng-click="editMembers(team)">{{ member.name }} &lt;{{member.email}}&gt;</span>
<!-- `ng-show` is an example of how we update the view without directly manipulating the DOM. plain JS in there -->
<span class="label label-default" ng-show="! team.members.length">No members yet.</span>
<a class="label label-primary" ng-click="editMembers(team)"><i class="fa fa-plus"></i></a>
</td>
<td class="text-right">
<a class="label label-default" ng-click="editTeam(team)"><i class="fa fa-pencil"></i></a>
<!-- $index is a special variable that will be rendered as the index of this `ng-repeat` -->
<a class="label label-danger" ng-click="deleteTeam(team, $index)"><i class="fa fa-times"></i></a>
</td>
</tr>
<tr ng-show="! teams.length">
<td colspan="4"><p class="lead">No Teams yet. Add one with the blue button above.</p></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
# Here's the other controller from this module
@dispatched.controller 'TeamForm', ['Team', '$http', '$scope', '$rootScope', (Team, $http, $scope, $rootScope) ->
$scope.team = {}
# this is another example of controlling the view without manipulating the DOM directly
$scope.modalShowing = false
$scope.init = () ->
# create the observer
$rootScope.$on 'Teams.presentForm', (event, team) ->
$scope.team = team
# create a snopshot in case we don't save our changes
$scope.team.snapshot()
# show ourselves
$scope.modalShowing = true
$scope.title = () ->
# are we editing a specific Team?
if $scope.team.id?
"Edit Team"
else
"New Team"
$scope.close = () ->
# "undo" our changes
$scope.team.rollback()
# hide ourselves
$scope.modalShowing = false
true
$scope.save = () ->
return false unless $scope.teamForm.$valid
willAdd = $scope.team.isNew()
NProgress.start()
$scope.team.save().then (team) ->
if willAdd
# we only want to do this if we want a new team added to the view
# otherwise changes will re-render automatically
$rootScope.$emit 'Teams.addedTeam', $scope.team
$scope.close()
NProgress.done()
true
# we don't call `ng-init` from this view, so we call it here
$scope.init()
]
<!-- angular will connect this up to TeamForm -->
<div ng-controller="TeamForm">
<!--
here's some more indirect DOM manipulation magic: `ng-class`. each key is a class that's added
to this element if the JS evals true
-->
<div class="md-modal colored-header custom-width md-effect-9" id="team-form-modal" ng-class="{'md-show': modalShowing }">
<div class="md-content">
<div class="modal-header">
<!-- call $scope.title() -->
<h3>{{title()}}</h3>
<button type="button" class="close md-close" ng-click="close()" aria-hidden="true">&times;</button>
</div>
<!-- catch the submit event here with `ng-submit` and call $scope.save() -->
<form name="teamForm" ng-submit="save()">
<div class="modal-body form">
<div class="form-group" ng-class="{ 'has-error' : teamForm.name.$invalid && !teamForm.name.$pristine }">
<label>Name</label>
<input type="name" class="form-control" placeholder="Equipment Repair 1" required ng-model="team.name" ng-minlength="3">
<p ng-show="teamForm.name.$invalid" class="help-block ng-hide">Team name required.</p>
</div>
<div class="form-group">
<label>Description</label>
<textarea type="description" class="form-control" placeholder="Optional description of what this team does..." ng-model="team.description"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default btn-flat md-close" ng-click="close()">Cancel</button>
<button type="submit" class="btn btn-primary btn-flat md-close">Save</button>
</div>
</form>
</div>
</div>
<div class="md-overlay" ng-click="close()"></div>
</div>
@montasaurus
Copy link

1-team.coffee mentions a _init_app.coffee . Is that where you're setting the @dispatched variable?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment