app = angular.module "plunker", [
"gg.resource"
]
app.directive "spinner", ["$q", ($q) ->
restrict: "E"
replace: true
scope:
promise: "="
template: """
<div ng-show="loading">Loading...</div>
"""
link: ($scope, $el, attrs) ->
$scope.$watch "promise", (promise) ->
$scope.loading = !promise
]
app.controller "demo", ["$scope", "plunks", ($scope, plunks) ->
$scope.trending = plunks.trending
$scope.plunk = plunks.findOrCreate(id: "e5iLyQ")
]
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8">
<title>AngularJS Plunker</title>
<script>
document.write('<base href="' + document.location + '" />');
</script>
<link rel="stylesheet" href="style.css">
<script src="http://code.angularjs.org/1.1.3/angular.js"></script>
<script src="app.js"></script>
<script src="resource.js"></script>
</head>
<body ng-controller="demo">
<h3>Single plunk:</h3>
<spinner promise="plunk"></spinner>
<strong>{{plunk.id}}</strong>
<button ng-click="plunk.refresh()">Refresh</button>
<p>{{plunk.description}}</p>
<h3>Trending Plunks:</h3>
<button ng-click="trending.refresh()">Refresh</button>
<spinner promise="trending"></spinner>
<ul>
<li ng-repeat="plunk in trending">
<spinner promise="plunk"></spinner>
<strong>{{plunk.id}}</strong>
<button ng-click="plunk.refresh()">Refresh</button>
<p>{{plunk.description}}</p>
</li>
</ul>
</body>
</html>
/* Put your css in here */
module = angular.module "gg.resource", []
module.factory "plunks", ["$http", ($http) ->
apiUrl = "http://api.plnkr.co"
identityMap = {}
class Plunk
constructor: (data = {}, options = {}) ->
# Constructor functions can return an object
# If this plunk is already in our identityMap then return that instance
if data.id and instance = identityMap[data.id]
instance.update(data)
return instance
plunk = @
# Copy plunk data to the plunk object (this)
angular.copy(data, plunk)
# options.serverState will be truthy if this plunk is being created based
# on server data
unless options.serverState
# We have no idea what the server state is so we mark this plunk as a
# duck-typed promise by giving it a .then() function that will refresh
# the plunk and return refresh()'s promise's then
plunk.then = -> plunk.refresh().then
update: (data = {}) ->
plunk = @
angular.extend plunk, data
plunk
refresh: ->
plunk = @
request = $http.get("#{apiUrl}/plunks/#{plunk.id or ''}").then (response) ->
plunk.update(response.data)
# Since the plunk now is resolved from server data, we remove the
# duck-typed psuedo-promise
delete plunk.then
# Let coffee-script implicitly return the updated plunk
plunk
# Until the XHR is finished, we give the plunk a .then() method that
# is really an alias of the request's .then() method
plunk.then = request.then.bind(request)
# Let coffee-script implicitlyr return the plunk (that is now a duck-typed
# promise)
plunk
# Coffee-Script will implicitly return the object below
# Return an array of trending plunks
findOrCreate: (data = {}) -> new Plunk(data)
trending: do ->
trending = []
# Refresh will request a new list of trending plunks from the server
trending.refresh = ->
request = $http.get("#{apiUrl}/plunks/trending").then (response) ->
# Reset the trending array to empty without changing the reference
trending.length = 0
trending.push(new Plunk(json, serverState: true)) for json in response.data
delete trending.then
trending
trending.then = request.then.bind(request)
trending
# Make our trending list a duck-typed, unresolved promise (yay chaining!)
# Notice that no request is issued until needed. This lazy loading can be
# easily avoided by pre-empting a call to .refresh()
trending.then = -> trending.refresh().then
# Return the empty array (that is a duck-typed, unresolved promise)
trending
]
# Lazy-loaded, duck-typed resources
**IDEA:** By assigning objects and arrays a `then()` method while that
object/array represents a client-side representation of the resource, we can
get some pretty neat behaviour based on AngularJS deep promise integration.
```javascript
// Pseudo-code example for the array
var lazy = [];
lazy.then = function() {
// 1. Issue an async operation
// 2. Delete lazy.then and update lazy itself when that operation completes
return promise.then.bind(promise)
};
$scope.lazy = lazy;
// $scope.lazy is now an empty array
// However, if we try and use it (for example, ng-repeat) it will be fetched
// automagically
```
### How is it done?
1. Queries to resources return an empty object or array
2. A `then()` method is assigned on that object / array that will trigger an
asynchronous request to get the server-side values for the object / array.
3. This `then()` method will delegate to the `then` of the promise that
is responsible for executing the server-side refresh.
4. When the server-side state is retrieved, we remove the `then()` property of
the object / array, thereby transforming it into a resolved object.
### Why does it work?
In promise-land (no idea which specific spec this is based on), anything with a
`then()` method is considered a promise.
We can use this behaviour to make our lazy objects / arrays into unresolved
promises whenever we need to by simply adding a `then()` method and removing it
once resolved.
### Why is this cool?
Deep integration of promises in AngularJS means that this can be used virtually
everywhere transparently.
See the attached code for an object-based and list-based example.
Pay attention to the fact that `plunks.trending` is just an array and not a
method and that it is empty until something uses it.
### Feedback
Ping me on twitter @filearts with hashtag #angular to discuss!