Created
August 8, 2013 18:13
-
-
Save hallister/6187144 to your computer and use it in GitHub Desktop.
An Angular directive that has no awareness of the DOM
This file contains 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('ui.bootstrap.editable', []) | |
.constant('editableConfig', { | |
validators: { | |
required: 'This field is required.', | |
min: 'A minimum value of {{ value }} is required.', | |
max: 'A maximum value of {{ value }} is allowed.', | |
ngMinlength: 'A minimum length of {{ value }} is required.', | |
ngMaxlength: 'A maximum length of {{ value }} is allowed.', | |
email: 'A valid email address is required.', | |
url: 'A valid URL is required.', | |
number: 'A valid number is required.', | |
defaultError: 'This is not a valid value.' | |
}, | |
templateUrl: 'template/editable/editable.html', | |
inputTemplateUrl: 'template/editable/text.html', | |
inputClass: 'input-medium', | |
type: 'text', | |
bindOn: 'click', | |
optionLabel: 'label', | |
optionKey: 'key', | |
groupBy: '' | |
}) | |
.directive('editable', ['$compile', '$timeout', '$http', '$interpolate', '$templateCache', '$parse', 'editableConfig', function ($compile, $timeout, $http, $interpolate, $templateCache, $parse, editableConfig) { | |
return { | |
require: 'ngModel', | |
scope: { | |
'source': '&', | |
'ngModel': '&model', | |
'type': '@' | |
}, | |
link: function postLink(scope, element, attrs, ctrl) { | |
var display = element.css('display'), | |
form, | |
error, | |
submit, | |
template, | |
errors, | |
originalValue; | |
scope.opts = angular.extend({}, scope.$eval(attrs.uiOptions || attrs.editableOptions || attrs.options), editableConfig); | |
// attribute options override non-attribute options.. probalby a better way to do this | |
angular.forEach(attrs, function(value, id) { | |
if (angular.isDefined(editableConfig[id]) && id != 'validators') { | |
scope.opts[id] = value; | |
} | |
}); | |
// we need the attributes for editable-input as well | |
scope.attrs = attrs; | |
// setup the errors | |
scope.errors = ''; | |
// if the binding isn't on click, we need to prevent the default click behavior | |
if (scope.opts.bindOn != 'click') { | |
element.bind('click', function(e) { | |
e.preventDefault(); | |
}); | |
} | |
// changes from inside isolate scope -> outside isolate scope | |
scope.$watch('model', function(val) { | |
// when the element is intially created, it's undefined | |
if (angular.isDefined(val)) { | |
ctrl.$setViewValue(val); | |
} else { | |
// get the original value | |
originalValue = ctrl.$modelValue; | |
} | |
}); | |
// changes from outside isolate scope -> isolate scope | |
scope.$watch(attrs.ngModel, function(val) { | |
scope.model = ctrl.$viewValue; | |
}); | |
// called by editableInput directive when the input has successfully replaced <editable-input> | |
scope.inputReady = function() { | |
compileForm(); | |
}; | |
// displays the errors need to refactor some of this | |
scope.showError = function() { | |
// reset the errors | |
scope.errors = ''; | |
errors = []; | |
// make sure the form field is defined | |
if (angular.isDefined(scope.editable_form.editable_field)) { | |
// loop through all editable_field errors | |
angular.forEach(scope.editable_form.editable_field.$error, function(key, value) { | |
// if error is true | |
if (key) { | |
// most validation fields don't have ng prefixes. But since ngMinlength and ngMaxlength do | |
// we have to check. | |
var camelValue = camelCase('ng-' + value); | |
var validator = scope.opts.validators[value] || scope.opts.validators[camelValue] || false; | |
// if validator is not falsy | |
if(validator) { | |
// interpolate the value... this only works for min/max/minlength/maxlength or custom | |
// it will return a null for any validator that doesn't have a {{ value }} | |
var errorFunction = $interpolate(validator, true); | |
var errorString; | |
// if a function is returned | |
if (angular.isFunction(errorFunction)) { | |
// get the value for {{ value }} and replace it | |
var replacementVal = attrs[value] || attrs[camelValue]; | |
errorString = errorFunction({ value: replacementVal }); | |
} else { | |
// no interpolation required, just set string | |
errorString = validator; | |
} | |
// push the error | |
errors.push(errorString); | |
} else { | |
// we don't have a validator for this (how?) push the default error | |
errors.push(scope.opts.validators.defaultError); | |
} | |
} | |
}); | |
} | |
// push each error into the error array | |
angular.forEach(errors, function(value, key) { | |
if (angular.isDefined(value)) { | |
scope.errors = scope.errors + value + '<br>'; | |
} | |
}); | |
}; | |
// submits the form if it's valid, shows error otherwise | |
scope.submitForm = function() { | |
if ((angular.isDefined(scope.editable_form) && scope.editable_form.$invalid)) { | |
// if we have an error, show it | |
scope.showError(); | |
} else { | |
form.remove(); | |
showElement(); | |
// the new original value | |
originalValue = ctrl.$modelValue; | |
} | |
}; | |
// cancels the form and sets the viewValue back to the originalValue | |
scope.cancelForm = function() { | |
scope.model = originalValue; | |
form.remove(); | |
showElement(); | |
scope.$apply(); | |
}; | |
var getTemplate = function() { | |
// bind the element | |
$http.get(scope.opts.templateUrl, {cache: $templateCache}) | |
.success(function(result) { | |
template = result; | |
element.bind(scope.opts.bindOn, function(e) { | |
e.preventDefault(); | |
hideElement(); | |
buildForm(); | |
}); | |
}); | |
}; | |
// hides the element | |
var hideElement = function() { | |
element.css('display', 'none'); | |
}; | |
// shows the element | |
var showElement = function() { | |
element.css('display', 'inline'); | |
}; | |
// form must have an editable-input somewhere | |
var buildForm = function() { | |
form = angular.element(template); | |
element.after(form); | |
// we have to compile the editable input first, | |
// or the form invalidation won't work | |
$compile(form.find('editable-input'))(scope); | |
scope.$apply(); | |
}; | |
// compiles complete form against current scope | |
var compileForm = function() { | |
$compile(form)(scope); | |
}; | |
// taken directly from Angular.js | |
// converts snake-case to camelCase | |
var camelCase = function(name) { | |
return name. | |
replace(/([\:\-\_]+(.))/g, function(_, separator, letter, offset) { | |
return offset ? letter.toUpperCase() : letter; | |
}). | |
replace(/^moz([A-Z])/, 'Moz$1'); | |
}; | |
// start it off | |
getTemplate(); | |
} | |
}; | |
}]) | |
.directive('editableInput', ['$compile', '$timeout', '$http', '$templateCache', '$interpolate', 'editableConfig', function ($compile, $timeout, $http, $templateCache, $interpolate, editableConfig) { | |
return { | |
restrict: 'E', | |
link: function postLink(scope, element, attrs) { | |
// scope.source | |
// scope.ngModel | |
var input, | |
optionsAttr, | |
template, | |
requiresValidation = [], | |
groupBy = '', | |
templates = { | |
text: '<input name="editable_field" type="{{ opts.type }}" class="{{ opts.inputClass }}">', | |
select: '<select name="editable_field" class="{{ opts.inputClass }}"></select>' | |
}; | |
scope.validationAttributes = ''; | |
scope.onElementChange = function() { | |
if (angular.isDefined(scope.editable_form) && scope.editable_form.$invalid) { | |
scope.showError(); | |
} | |
}; | |
var getTemplate = function() { | |
// get the template, check template cache first | |
$http.get(scope.opts.inputTemplateUrl, {cache: $templateCache}) | |
.success(function(result) { | |
// set the template and build the form | |
template = angular.element(result); | |
buildInput(); | |
}) | |
.error(function(result) { | |
// we could use Response interceptors here, but it seems unncessary at this point since global | |
// interceptors aren't needed | |
// we couldn't find the template, so let's fall back to editableConfig's default | |
if (scope.opts.inputTemplateUrl !== editableConfig.inputTemplateUrl) { | |
scope.opts.inputTemplateUrl = editableConfig.inputTemplateUrl; | |
getTemplate(); | |
// final fall back, just grab the default text-type template | |
} else if (scope.opts.inputTemplateUrl !== 'template/editable/text.html') { | |
scope.opts.inputTemplateUrl = 'template/editable/text.html'; | |
getTemplate(); | |
} | |
}); | |
}; | |
var buildInput = function() { | |
findValidationElement(template); | |
element.replaceWith(template); | |
// we call this so the entire editable can be compiled | |
// otherwise the form validation (the field isn't attached to scope.form_name). | |
scope.inputReady(); | |
}; | |
// find all children in the template that require the validators | |
var findValidationElement = function(valElement) { | |
// if it has wg-editable-validators, push it | |
if (valElement.hasClass('wg-editable-validators')) { | |
addInputAttributes(valElement); | |
} | |
// if element has children, each child should be tested | |
if (valElement.children().length > 0) { | |
angular.forEach(valElement.children(), function(value, key) { | |
// recurse | |
findValidationElement(angular.element(value)); | |
}); | |
} | |
}; | |
// adds the input attributes per scope.opts.validators | |
var addInputAttributes = function(valElement) { | |
angular.forEach(scope.attrs, function(value, key) { | |
if (angular.isDefined(scope.opts.validators[key])) { | |
valElement.attr(snakeCase(key), scope.attrs[key]); | |
// range and number input types accept "step" as a attribute | |
if ((scope.opts.type == 'number' || scope.opts.type == 'range') && angular.isDefined(scope.attrs.step)) { | |
valElement.attr('step', attrs.step); | |
} | |
} | |
}); | |
}; | |
// taken directly from Angular. | |
// converts camelCase to snake-case | |
var snakeCase = function(name, separator) { | |
separator = separator || '-'; | |
return name.replace(/[A-Z]/g, function(letter, pos) { | |
return (pos ? separator : '') + letter.toLowerCase(); | |
}); | |
}; | |
// start it! | |
getTemplate(); | |
} | |
}; | |
}]); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment