Last active
September 8, 2015 15:42
-
-
Save dehypnosis/79fe286edf8d2971643f to your computer and use it in GitHub Desktop.
Extended Restangular (it will be made as a independent module, soon )
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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