16

I'm using $resource to manipulate my data. I would like to convert dates straigt away into date objects when I fetch them. It just makes it easier to work with datepicker etc.

My factory:

AppFactories.factory("Books", ['$resource' ,function($resource){

  return $resource(
        "/books/:id",
        {id: "@id" },
        {
            "update": {method: "PUT", isArray: true },
            "save": {method: "POST", isArray: true },

        }
    );

}]);

Can I create a function within the factory to convert dates from the database into a date object and vice versa when I post/update?

It would be really nice to integrate it right into the factory so that I can reuse it.

Tino
  • 3,340
  • 5
  • 44
  • 74

4 Answers4

12

Instead of doing this on every resource you could apply it to the underlying $http as described in more detail here.

app.config(["$httpProvider", function ($httpProvider) {
     $httpProvider.defaults.transformResponse.push(function(responseData){
        convertDateStringsToDates(responseData);
        return responseData;
    });
}]);
djskinner
  • 8,035
  • 4
  • 49
  • 72
  • I used the regex suggested [here](http://stackoverflow.com/questions/12756159/regex-and-iso8601-formated-datetime) by Édouard Lopez over the regex in the referenced blog post: /^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/ – martinoss May 21 '15 at 12:30
  • this should be an accepted answer... its simple, elegant and generic! – Lightning3 Jul 13 '17 at 07:26
10

The $resource action object can have a property called interceptor, which allows you to define an interceptor object for this particular action (like with $http interceptors). You can define response and responseError properties for this interceptor. This way you can define some date parsing functionality for the actions the resource offers:

function parseResponseDates(response) {
  var data = response.data, key, value;
  for (key in data) {
    if (!data.hasOwnProperty(key) && // don't parse prototype or non-string props
        toString.call(data[key]) !== '[object String]') continue;
    value = Date.parse(data[key]); // try to parse to date
    if (value !== NaN) data[key] = value;
  }
  return response;
}

return $resource('/books/:id', {id: '@id'},
  {
    'update': {
      method: 'PUT', 
      isArray: true, 
      interceptor: {response: parseResponseDates}
    },
    ....
  }
);

Hope this helps!

jakee
  • 18,486
  • 3
  • 37
  • 42
4

I wanted to use Angular.js's ngResource ($resource), but I also wanted my date parsing not to interfere with anything else in $http, and the action's transformResponse option seemed more appropriate than the interceptor option. @jakee's parseResponseDates is nice because it's general purpose, but it's more brute-force than I wanted; I know exactly what fields can be dates in my models, and I only want to parse those fields as dates.

The documentation says the default transformResponse is just angular.fromJson but that's not quite true; it also strips the JSON vulnerability protection prefix, )]}',\n, before running angular.fromJson (http.js source reference). Since $resource doesn't readily expose each action's transformResponse array, you have to handle extending the default transformResponse manually, by injecting $http and then calling $http.defaults.transformResponse.concat(your_transformer). The solution I arrived at, in the context of your example, would look something like this:

AppFactories.factory("Books", function($resource, $http) {
  var raiseBook = function(book) {
    // "created_at" is declared as NOT NULL in the database
    book.created_at = new Date(book.created_at);
    // "completed_at" is a NULL-able field
    if (book.completed_at) {
      book.completed_at = new Date(book.completed_at);
    }
    return book;
  };

  var raiseBooks = function(books) {
    return books.map(raiseBook);
  };

  return $resource("/books/:id", {
    id: "@id"
  }, {
    update: {
      method: "PUT",
      isArray: true,
      // some_array.concat(...) creates and returns a new Array,
      //   without modifying the original some_array Array
      transformResponse: $http.defaults.transformResponse.concat(raiseBooks),
    },
    save: {
      method: "POST",
      isArray: true,
      transformResponse: $http.defaults.transformResponse.concat(raiseBooks),
    },
  });
});

Note that Date.parse(...) returns a Number (milliseconds since the UNIX epoch), so if that's what you want, you'll have to change the new Date(...) calls. But since the original question calls for date objects, new Date(...) is probably what you want.

chbrown
  • 11,865
  • 2
  • 52
  • 60
  • 2
    Thanks for the excellent answer! Worth noting that for `transformRequest` the order would probably be the other way around to benefit from angular json conversion, so: `transformRequest: [raiseBooks].concat($http.defaults.transformRequest)` – Amr Mostafa Dec 20 '14 at 22:47
3

I adopted @chbrown's answer and make angular service for using in any situation where we need transparent transform of dates in ng-resource request/response on the fly.

Here is the gist.

Example of usage:

var Session = $resource('/api/sessions/:id', {id: '@id'}, {
  query: {
    method: 'GET',
    isArray: true,
    transformResponse: dateTransformer.transformResponse.toDate(['start', 'end'])
  },
  update: {
    method: 'PUT',
    transformRequest: dateTransformer.transformRequest.toLocalIsoString(['start', 'end'])
  }
});

After Session.query() fields 'start' and 'end' in returned resource objects will be JS Date objects. After session.$update() server will receive JSON where JS Date objects in fields 'start' and 'end' will be just strings in ISO format without timezone.

Supported transformers: toDate, toLocalIsoString, toZonedIsoString.

This service uses moment.js for date parsing and format because native JS Date doesn't support dates without timezone (they always parsed as UTC+0).

Also Lodash is used for object manipulations.

Ruslan Stelmachenko
  • 4,987
  • 2
  • 36
  • 51
  • Ended up using this solution. It's the best fit for my case. Not overly broad (I know exactly the fields that are to be parsed as dates) and should have low side-effects. Thanks. Oh, I had to use the fix of line #77 suggested by DSpeichert on the linked gist: https://gist.github.com/xak2000/62ce767d871cf6d5e0f9#gistcomment-1407627 – molecular May 19 '16 at 10:04
  • I've [updated](https://gist.github.com/xak2000/62ce767d871cf6d5e0f9/revisions) the [gist](https://gist.github.com/xak2000/62ce767d871cf6d5e0f9) to the latest version. It works with Lodash 4.x now and has more features. Previous version was designed to work with Lodash 3.x (this is the cause of the problem with line #77). – Ruslan Stelmachenko May 19 '16 at 22:07