Estenda $ resource com AngularJs

Quase todos os aplicativos AngularJs precisam em um ponto para se conectar a uma API para interagir e recuperar dados. O AngularJs out-of-the-box fornecem 2 maneiras comuns de fazer isso, usando $ http ou $ resource object.

O último envolve o primeiro nos bastidores para uso em cenários de API da web RESTful. Hoje, vou mostrar um pequeno truque para estender $ resource e dar a ele alguns recursos extras convenientes.

$ resource

Para estendê-lo, primeiro precisamos saber como geralmente usamos $ resource . Para os fins deste artigo, vamos presumir que nosso aplicativo precisa jogar com os usuários usando os seguintes urls de recursos da API:

GET    /users
GET
/users/:id
POST
/users
PATCH
/users/:id
DELETE
/users/:id

Uma implementação padrão de $ resource para este caso específico seria:

(function () {
'use strict';
angular

.module('app.core.resources')
.factory('User', function ($resource) {
return $resource('/users/:id', { id: '@id' }, {
update
: { method: 'PATCH' },
destroy
: { method: 'DELETE' }
});
});
})();

Estendendo $ resource

O exemplo acima funciona muito bem no que faz, e não há nada de errado em fazer dessa forma.

No entanto, no meu caso, e para permanecer consistente entre meu back-end Rails e meu front-end, eu queria ser capaz de ter um objeto que eu pudesse usar dessa maneira:

// Create a new user
var user = new User({ lastname: 'Kent', firstname: 'Clark' });
user
.save();

// Get all users
var users = User.all();

// Find a user
var user = User.find(22);

// Update a user
User.find(22, function (user) {
user
.firstname = 'Martha';
user
.save();
});

// Delete a user
User.find(22, function (user) {
user
.delete();
});

O objeto ActiveResource

Vamos chamar nossa extensão ActiveResource . Métodos como save () e delete () precisam ser criados no nível do protótipo para serem usados ​​pela instância do objeto.

Além disso, save () precisa verificar se um id já está ou não definido na instância para executar um POST ou uma solicitação PATCH. Este serviço conta com https://github.com/iobaixas/angular-inflector para funcionar.

(function () {
'use strict';

angular

.module('wam.core')
.provider('ARSettings', function () {
this.apiUrl = undefined;

this.$get = function () {
return {
apiUrl
: this.apiUrl,
};
};

this.configure = function (config) {
for (var key in config) {
this[key] = config[key];
}
};
})
.factory('ActiveResource', function ($resource, ARSettings, inflector, $injector) {

/**
* Check whether an object is a number.

*

* @param {Object} object - Object to check numericallity on.

* @return {Boolean} True if number, false otherwise.

*/

var isNumeric = function (object) {
return !isNaN(parseFloat(object)) && isFinite(object);
};

/**
* Generate options based on arguments passed.

* If the object passed is :

* - An object : Use it directly.

* - A string : Inflect it and use the default config.

* - Something else : Throw an error.

*

* @param {Object} args - Javascript object or string as the name of the resource (singular).

* @return {Object} Options to pass to $resource.

*/

var sanitizeOptions = function (args) {
if (args !== null && typeof args === 'string') {
var _resName = inflector.pluralize(args);
return {
url
: '/' + _resName + '/:id/:action',
params
: { id: '@id' },
namespace
: args
};
} else if (args !== null && typeof args === 'object') {
return args;
} else {
throw new Error(args + ' is not a valid options');
}
};

/**
* ActiveResource core definition.

*/

var Resource = function (options) {
options
= sanitizeOptions(options);
options
.params = options.params || {};
options
.methods = options.methods || {};

/**
* Transform data before querying the server.

* In the case of Rails, will wrap the data with a resource namespace.

*

* @param {Object} data - Data to send.

* @return {String} Stringify data.

*/

var transformRequest = function (data) {
if (!options.namespace) {
return JSON.stringify(data);
}

var datas = {};
datas
[options.namespace] = data;
return JSON.stringify(datas);
};

/**
* Transform data after querying the server.

* If the response contains an object (instead of a query) with the resource namespace in plural :

*

* new ActiveResource('user') => Check for the key users

*

* then attach to each object the Resource object. This is a particular case

* mostly used in pagination scenario.

*

* @param {Object} data - Data to send.

* @return {String} Stringify data.

*/

var transformResponse = function (data) {
data
= JSON.parse(data);

var namespace = inflector.pluralize(options.namespace);
if (data[namespace]) {
var ClassObject = $injector.get(
inflector
.camelize(inflector.singularize(namespace), true)
);

angular
.forEach(data[namespace], function (object, index) {
var instance = new ClassObject();
data
[namespace][index] = angular.extend(instance, object);
});
}

return data;
};

var defaults = {
browse
: { method: 'GET', transformResponse: transformResponse },
query
: { method: 'GET', transformResponse: transformResponse, isArray: true },
get: { method: 'GET', transformResponse: transformResponse },
create
: { method: 'POST', transformRequest: transformRequest },
update
: { method: 'PATCH', transformRequest: transformRequest },
destroy
: { method: 'DELETE' }
};

angular
.extend(defaults, options.methods);
var resource = $resource(ARSettings.apiUrl + options.url, options.params, defaults);

/**
* Get an entire collection of objects.

*

* @param {Object} args - $resource.query arguments.

* @return {Promise} Promise

*/

resource
.all = function (args) {
var options = args || {};
return this.query(options);
};

/**
* Get an entire collection of objects.

* Since a search is often returning pagination type of data,

* the collection of object will be wrapped under a key within that response.

* See transformResponse for more information about that case.

*

* @param {Object} args - $resource.query arguments.

* @return {Promise} Promise

*/

resource
.search = function (args) {
var options = args || {};
return this.browse(options);
};

/**
* Find a specific object.

*

* @param {Object|Integer} args - $resource.get arguments, or { id: args } if numeric.

* @param {Function} callback - $resource.get callback function if any.

* @return {Promise} Promise

*/

resource
.find = function (args, callback) {
var options = isNumeric(args) ? { id: args } : args;
return this.get(options, callback);
};

/**
* Mixin custom methods to instance.

*

* @param {Object} args - Set of properties to mixin the $resource object.

* @return {this} this. Chainable.

*/

resource
.instanceMethods = function (args) {
angular
.extend(this.prototype, args);
return this;
};

/**
* $resource's $save method override.

* Allow to use $save in order to create or update a resource based on it's id.

*

* @return {Promise} Promise

*/

resource
.prototype.save = function () {
var action = this.id ? '$update' : '$create';
return this[action]();
};

/**
* Delete instance object.

*

* @return {Promise} Promise

*/

resource
.prototype.delete = function () {
if (!this.id) {
throw new Error('Object must have an id to be deleted.');
}

var options = { id: this.id };
return this.$destroy(options);
};

return resource;
};
return Resource;
});
})();

Explicação

Nosso objeto ActiveResource assume como parâmetro um argumento que pode ser um objeto ou uma string. No caso de uma string, deve ser o nome do recurso no singular.

new ActiveResource('user');

As opções serão então transformadas como:

{
url
: '/users/:id/:action',
params
: { id: '@id', action: '@action' },
namespace
: 'user'
}

NOTA: A opção de namespace aqui é usada para transformar dados antes de uma solicitação POST ou PATCH.

Para adicionar save () e delete () à nossa instância ActiveResource, precisamos adicioná-los no nível do protótipo. O que isso significa é que a referência disso dentro desses métodos é, na verdade, a própria instância. Isso é importante porque, no caso de um save (), precisamos chamar o método $ update () apenas se um id estiver presente:

resource.prototype.save = function () {
var action = this.id ? '$update' : '$create';
return this[action]();
};

Se você não percebeu, também adiciono instanceMethods (). É um método conveniente que pode ser usado no nível de instância para adicionar mixins personalizados a uma instância de objeto. Por exemplo, no caso de um usuário:

(function () {
'use strict';
angular

.module('app.core.resources')
.config(function (ARSettingsProvider) {
ARSettingsProvider.configure({
apiUrl
: 'https://api.endpoint.com'
});
})
.service('User', function (ActiveResource) {
return new ActiveResource('user').instanceMethods({
isAdmin
: function () {
return this.role == 'admin';
},
isUser
: function () {
return this.role == 'user';
},
fullname
: function () {
return this.lastname + ' ' + this.firstname;
}
});
});
})();

Esses métodos podem ser usados ​​em qualquer objeto injetado:

(function () {
'use strict';
angular

.module('app.module.user')
.controller('UsersController', function (User) {
var vm = this;
User.find({ action: 'me' }, function (me) {
if (me.isAdmin()) {
vm
.users = User.all();
}
});
});
})();

bem como nas visualizações:

<ul>
<li ng-repeat="user in users">
{{ user.fullname() }}

<div ng-if="user.isAdmin()">
<button class="btn btn-primary">Delete</button>
</div>
</li>
</ul>

Conclusão

Este artigo apenas arranha a superfície do que é possível fazer. Seu objetivo era demonstrar como é fácil usar objetos nativos Angular e adicionar funcionalidades a eles.