Skip to content

Instantly share code, notes, and snippets.

@dehypnosis
Last active September 8, 2015 15:42
Show Gist options
  • Save dehypnosis/79fe286edf8d2971643f to your computer and use it in GitHub Desktop.
Save dehypnosis/79fe286edf8d2971643f to your computer and use it in GitHub Desktop.
Extended Restangular (it will be made as a independent module, soon )
angular
.module('app', ['restangular'])
/** ServerSide validation presenter **/
.factory('Validator', function(){
return {
clean: function($form) {
if (typeof $form == 'string') {
$form = angular.element('form[name="'+$form+'"]').controller('form');
}
angular.forEach($form, function($input) {
if(!$input) return;
angular.forEach($input.$error, function(invalid, rule) {
if(invalid) {
$input.$setValidity(rule, true);
}
});
});
},
show: function($form, errors) {
if (typeof $form == 'string') {
$form = angular.element('form[name="'+$form+'"]').controller('form');
}
angular.forEach(errors, function(errors,inputName){
angular.forEach(errors, function(message, rule){
if ($form[inputName]) {
$form[inputName].$setValidity(rule, false);
$form[inputName].$setPristine();
$form[inputName].$error[rule] = message;
}
});
});
},
handle: function($form, errors) {
this.clean($form);
this.show($form, errors);
}
}
})
/** Restangular factory **/
.factory('RestangularFactory', function($log, $window, $timeout, $q, Validator, Restangular){
var paceOptions; // dump Pace.js original options
if (Pace) paceOptions = Pace.options.ajax;
/**
USAGE:
var options = {
confirmWhatOperation: 'string',
beforeWhatOperation: function(obj){}, // return false for cancel operation
afterWhatOperation: function(obj, returnObj){},
errorWhatOperation: function(response){}
relations: {
parentModelName: {
field1: 'childModelName1', // 1-1 relation
field2: ['childModelName2'] // 1-M relation
},
...
}
};
var Rest = RestangularFactory(options);
**/
return function(options){
var Rest,
options = angular.extend({relations:{}}, options || {}),
lastRequest = null,
lastElem = null,
lastValidatedForm = null;
var forRelated = function(elem, callback, normalOrder){
var rel = options.relations[elem.route];
for(var field in rel) {
var childModelName = rel[field];
if (typeof childModelName == 'string') {
var childElem = elem[field];
if (!childElem) continue;
callback.call(null, childElem, field);
} else {
var childCol = elem[field];
if (!childCol) continue;
if (normalOrder) {
var i = 0, len = childCol.length;
while(i < len) {
callback.call(null, childCol[i], field);
i++;
}
} else {
// loop by reverse order for [].splice, etc.
var i = childCol.length;
while(i--) {
callback.call(null, childCol[i], field);
}
}
}
};
};
var forRelation = function(elem, callbackElem, callbackCol){
var rel = options.relations[elem.route];
for(var field in rel) {
var childModelName = rel[field];
if (typeof childModelName == 'string') {
if (callbackElem && callbackElem.call(null, childModelName, field) === false) break;
} else if (callbackCol && callbackCol.call(null, childModelName[0], field) === false) break;
};
};
var elementExtends = {
$setEdit: function(elem, childModelNames){ // granfa.$setEdit('father,son')
// recursively setEdit
if (typeof childModelNames == 'string') childModelNames = childModelNames.split(',');
if (childModelNames && childModelNames.length>0) {
forRelated(elem, function(childElem, field){
var index = childModelNames.indexOf(childElem.route);
if (index > -1) {
var pass = angular.copy(childModelNames);
pass.splice(index,1);
childElem.$setEdit(pass);
}
});
}
elem.$edit = true;
elem.$old = elem.$plain();
return elem;
},
$setRemove: function(elem) {
elem.$remove = true;
return elem;
},
$delete: function(elem){
elem.$remove = true;
return elem.$publish();
},
$restore: function(elem, childModelNames, remainEdit){
// if new one, detach it from collection or parent and just ignore about oldData and children
if (elem.$new) {
elem.$detach();
if (!remainEdit) delete elem.$edit;
delete elem.$new;
return elem;
}
// recursively restore
if (typeof childModelNames == 'string') childModelNames = childModelNames.split(',');
if (childModelNames && childModelNames.length>0) {
forRelated(elem, function(childElem, field){
var index = childModelNames.indexOf(childElem.route);
if (index > -1) {
if (childElem.$new) {
childElem.$detach();
} else {
var pass = angular.copy(childModelNames);
pass.splice(index,1);
childElem.$restore(pass, remainEdit);
}
}
});
}
if (elem.$old) {
angular.extend(elem, elem.$old);
delete elem.$old;
}
if (!remainEdit) delete elem.$edit;
delete elem.$remove;
return elem;
},
$restoreAndEdit: function(elem, childModelNames){
return elem.$restore(childModelNames, true);
},
$publish: function(elem, childModelNames, remainEdit){
var chain = $q.when(),
elemNeedPublish = true;
// if element is $remove state, remove and detach it from collection or parent, and all done.
if (elem.$remove) {
if (elem.$new) {
elem.$detach();
} else {
chain = chain.then(function(){
return elem.remove().then(function(){
elem.$detach();
});
});
}
return chain;
}
// if element is $new state, firstly post it
if (elem.$new) {
// if parent $new one not posted yet, firstly post parent
if (elem.parentResource && !elem.parentResource.id) {
chain = chain.then(function(){
return elem.$parent.$publishAndEdit();
});
}
chain = chain.then(function(){
return elem.post().then(function(res){
elem.$merge(res); // posted and merged
delete elem.$new;
// if elem is not pushed to collection, or not attached to parent yet
if (elem.$yet) {
if (elem.$collection) {
elem.$collection.push(elem);
} else if (elem.$parent) {
elem.$parent[elem.$yet] = elem;
} else {
throw "Need to set collection, or parent to attach model";
}
delete elem.$yet;
}
if (elem.$postCallback) {
elem.$postCallback();
delete elem.$postCallback;
}
});
});
elemNeedPublish = false;
}
// recursively publish children and self
if (typeof childModelNames == 'string') childModelNames = childModelNames.split(',');
var publishRec = function(curElem) {
if (childModelNames && childModelNames.length>0) {
forRelated(elem, function(childElem, field){
if (!childElem.$edit) return;
var index = childModelNames.indexOf(childElem.route);
if (index > -1) {
var pass = angular.copy(childModelNames);
pass.splice(index,1);
chain = chain.then(function(){
return childElem.$publish(pass, remainEdit);
});
}
}, true);
}
// and put current elem at last
chain = chain.then(function(){
if (curElem != elem || elemNeedPublish) {
return curElem.put().then(function(res){
curElem.$merge(res); // posted and merged
if (!remainEdit) delete curElem.$edit;
delete curElem.$old;
});
} else {
if (!remainEdit) delete curElem.$edit;
delete curElem.$old;
}
});
}
publishRec(elem);
return chain;
},
$publishAndEdit: function(elem, childModelNames){
return elem.$publish(childModelNames, true);
},
$merge: function(elem, newElem){
var data = newElem.$plain();
// elem is newerly posted one
if (!elem.id) {
angular.extend(elem, data);
// recursively patch elem's id to children
var setElemIdRec = function(curElem, elemId){
forRelated(curElem, function(childElem, field){
childElem.parentResource.id = elemId;
setElemIdRec(childElem, elemId);
});
}
setElemIdRec(elem, elem.id);
} else {
angular.extend(elem, data);
}
return elem;
},
$plain: function(elem, what) {
var route = elem.route || what,
rel = options.relations[route] || {};
// call Restangular's plain()
if (elem.plain) elem = elem.plain();
// clean $prefixed and relations
for(var k in elem) {
if (k.charAt(0) == '$' || k in rel) delete elem[k];
}
return elem;
},
$add: function(elem, childModelNames, callback){ // set 1-1 children
var childModelNames = childModelNames.split(',');
forRelation(elem, function(childModelName, field){
if (childModelNames.indexOf(childModelName)>-1) {
var childElem = elem.one(childModelName).$setEdit(); // set references and states
childElem.$parent = elem;
childElem.$new = true;
elem[field] = childElem;
if (callback) elem.$postCallback = callback;
}
});
return elem;
},
$make: function(elem, childModelName, callback){
var childElem;
forRelation(elem, function(childModelNameR, field){
if (childModelNameR == childModelName) {
childElem = elem.one(childModelName).$setEdit(); // set references and states
childElem.$parent = elem;
childElem.$new = true;
childElem.$yet = field;
if (callback) childElem.$postCallback = callback;
return false;
}
});
return childElem;
},
$detach: function(elem){
if (elem.$collection) {
var index = elem.$collection.indexOf(elem);
if (index > -1) {
elem.$collection.splice(index,1);
delete elem.$collection;
}
} else if(elem.$parent) {
forRelation(elem.$parent, function(childModelName, field){
if (elem.route == childModelName) {
if (elem.$parent[field] == elem) {
delete elem.$parent[field];
delete elem.$parent;
}
return false;
}
});
} else {
throw "Need to set collection to detach root model";
}
return elem;
}
};
var collectionExtends = {
$add: function(col, callback){
var childElem = col.$parent.one(col.route).$setEdit(); // set references and states
childElem.$collection = col;
childElem.$parent = col.$parent;
childElem.$new = true;
if (callback) childElem.$postCallback = callback;
col.push(childElem);
return col;
},
$make: function(col, callback){
var childElem = col.$parent.one(col.route).$setEdit(); // set references and states
childElem.$collection = col;
childElem.$parent = col.$parent;
childElem.$new = true;
childElem.$yet = true;
if (callback) childElem.$postCallback = callback;
return childElem;
},
$each: function(col, callback, normalOrder){
if (normalOrder) {
var i = 0, len = col.length;
while(i < len) {
callback.call(null, col[i]);
i++;
}
} else {
var i = col.length;
while(i--) {
callback.call(null, col[i]);
}
}
return col;
},
$find: function(col, id){
var i = col.length;
while(i--) {
if (col[i].id == id) return col[i];
}
return;
},
$restore: function(col){
var i = col.length;
while(i--) {
col[i].$restore();
}
return col;
}
};
Rest = Restangular.withConfig(function(configuer){
// confirm and before filter
configuer.addFullRequestInterceptor(function(element, operation, what, url, headers, params, httpConfig, callerElement){
lastElem = callerElement;
lastRequest = _.capitalize(_.camelCase(what+'_'+operation));
// clean validator
if (lastValidatedForm) {
Validator.clean(lastValidatedForm);
lastValidatedForm = null;
}
// assign abort promise on httpConfig to cancel request
var abort = false, abortDefer = $q.defer();
httpConfig.timeout = abortDefer.promise;
// confirm ..
var cfirm = options['confirm'+lastRequest];
if (cfirm && !$window.confirm(cfirm)) {
if (operation == 'remove') delete lastElem.$remove;
abort = true;
}
// before ...
if (!abort) {
var filter = options['before'+lastRequest];
if (filter && filter(lastElem, element, params) === false) abort = true;
}
// abort request
if (abort) {
abortDefer.resolve();
if (Pace) {
// prevent Pace.js bar progess temporarily
Pace.options.ajax = false;
}
}
// clean (nested) relation from POST/PUT methods payload
if (element) { // here already Restanuglar's plain()ed
element = element.$plain(what);
}
return {
headers: headers,
params: params,
element: element,
httpConfig: httpConfig
};
});
// after callback
configuer.addResponseInterceptor(function(data, operation, what, url, response, deferred) {
// restore Pace.js
if (Pace) {
Pace.options.ajax = paceOptions;
}
// after callbacks
var after = options['after'+lastRequest];
deferred.promise.then(function(madeElem){
if (after) after(lastElem, madeElem);
});
return data;
});
// error callback
configuer.setErrorInterceptor(function(response, deferred, responseHandler) {
// restore Pace.js
if (Pace) {
Pace.options.ajax = paceOptions;
}
// keep error to be handled
var propagation = true;
// present serverside validation errors
var method = (response.config.method).toLowerCase();
if (response.status == 422) {
var foundForm;
angular.element('form').each(function(i,form){
var item = angular.element(form).scope().item;
if (item == lastElem) {
foundForm = form;
}
});
if (foundForm) {
propagation = false;
lastValidatedForm = angular.element(foundForm).controller('form');
Validator.handle(lastValidatedForm, response.data);
}
} else if (response.status == 0) { // request canceled
propagation = false;
} else {
propagation = false;
$window.alert('서버와의 연결에 문제가 발생했습니다.\r\n('+response.status+' '+response.statusText+')');
}
// custom error handlers
if (propagation) {
var errorHandler = options['error'+lastRequest];
if (errorHandler) {
errorHandler(lastElem, response);
propagation = false;
}
}
return propagation;
});
// extend model methods
configuer.setOnElemRestangularized(function(model, isCollection, what){
// make references to root collection/element
if (!model.$parent) {
model.$parent = Rest;
if (isCollection) {
angular.forEach(model, function(rootElem){
rootElem.$collection = model;
rootElem.$parent = Rest;
});
}
}
// bind methods
var obj = (isCollection) ? collectionExtends : elementExtends;
for(var k in obj) {
var f = obj[k];
(function(f, k){
model[k] = function(){
var args = [].slice.call(arguments);
args.unshift(model);
return f.apply(null, args);
};
})(f, k);
};
return model;
});
});
// add hooks dynamically
Rest.addHooks = function(obj){
delete obj.relations;
angular.extend(options, obj);
return Rest;
};
// extend nested relations
angular.forEach(options.relations, function(rel, modelName){
// on new element
Rest.extendModel(modelName, function(parentElem){
if (!parentElem.$parent) parentElem.$parent = Rest;
angular.forEach(rel, function(childModelName, field){
// child element
if (typeof childModelName == 'string') {
var childElem = parentElem[field];
if (childElem) {
childElem.$parent = parentElem;
Rest.restangularizeElement(parentElem, childElem, childModelName);
}
// child collection
} else {
var childCol = parentElem[field],
childModelName = childModelName[0];
if (childCol) {
childCol.$parent = parentElem;
angular.forEach(childCol, function(childElem){
childElem.$collection = childCol;
childElem.$parent = parentElem;
});
Rest.restangularizeCollection(parentElem, childCol, childModelName);
} else {
childCol = parentElem.all(childModelName);
childCol.$parent = parentElem;
parentElem[field] = childCol;
}
}
});
return parentElem;
});
});
return Rest;
};
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment