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!