diff --git a/extensions/mistral/static/mistral/js/mistral.workbook.models.js b/extensions/mistral/static/mistral/js/mistral.workbook.models.js index 53715dd..1d0a103 100644 --- a/extensions/mistral/static/mistral/js/mistral.workbook.models.js +++ b/extensions/mistral/static/mistral/js/mistral.workbook.models.js @@ -559,7 +559,12 @@ 'index': 0, 'panelIndex': 0, 'row': 0 - } + }, + '@constraints': [ + function(value) { + return value !== 'workbook1' ? true : 'The sample validation failure.'; + } + ] }) }, 'description': { diff --git a/merlin/static/merlin/js/merlin.directives.js b/merlin/static/merlin/js/merlin.directives.js index 002e655..8d53ca8 100644 --- a/merlin/static/merlin/js/merlin.directives.js +++ b/merlin/static/merlin/js/merlin.directives.js @@ -112,6 +112,36 @@ } } }) + .directive('validatableWith', function($parse) { + return { + restrict: 'A', + require: 'ngModel', + link: function(scope, element, attrs, ctrl) { + var model; + if ( attrs.validatableWith ) { + model = $parse(attrs.validatableWith)(scope); + scope.error = ''; + model.setValidatable && model.setValidatable(true); + model.on && model.on('validation', function(result) { + var isValid = (result == 'succeeded'), + baseMessage = ''; + // (FIXME): hack until Barricade supports validation of empty required entries + if ( !model.get() && model.isRequired() ) { + isValid = false; + baseMessage = 'This field is required.' + } + ctrl.$setValidity('barricade', isValid); + scope.error = model.hasError() ? model.getError() : baseMessage; + }); + ctrl.$formatters.push(function(modelValue) { + return modelValue === undefined ? + ( ctrl.$isEmpty(ctrl.$viewValue) ? undefined : ctrl.$viewValue ) : + modelValue; + }); + } + } + } + }) .directive('typedField', ['$compile', 'merlin.templates', function($compile, templates) { function updateAutoCompletionDirective(template) { diff --git a/merlin/static/merlin/js/merlin.field.models.js b/merlin/static/merlin/js/merlin.field.models.js index 47ae295..dbc393b 100644 --- a/merlin/static/merlin/js/merlin.field.models.js +++ b/merlin/static/merlin/js/merlin.field.models.js @@ -32,11 +32,18 @@ }); var modelMixin = Barricade.Blueprint.create(function(type) { + var isValid = true, + isValidatable = false; this.value = function() { if ( !arguments.length ) { - return this.get(); + if ( isValidatable ) { + return isValid ? this.get() : undefined; + } else { + return this.get(); + } } else { this.set(arguments[0]); + isValid = !this.hasError(); } }; this.id = utils.getNewId(); @@ -45,6 +52,10 @@ return type; }; + this.setValidatable = function(validatable) { + isValidatable = validatable; + }; + this.setType = function(_type) { type = _type; }; diff --git a/merlin/static/merlin/scss/merlin.scss b/merlin/static/merlin/scss/merlin.scss index b1b842a..b33a08c 100644 --- a/merlin/static/merlin/scss/merlin.scss +++ b/merlin/static/merlin/scss/merlin.scss @@ -54,8 +54,13 @@ } } -editable .fa-pencil { - color: black; +editable { + .fa-pencil { + color: black; + } + button:disabled { + color: gray; + } } .section { diff --git a/merlin/static/merlin/templates/editable.html b/merlin/static/merlin/templates/editable.html index 06bdd31..7d8dd91 100644 --- a/merlin/static/merlin/templates/editable.html +++ b/merlin/static/merlin/templates/editable.html @@ -5,6 +5,6 @@ - + diff --git a/merlin/static/merlin/templates/fields/number.html b/merlin/static/merlin/templates/fields/number.html index 2b53f39..fbcc2b2 100644 --- a/merlin/static/merlin/templates/fields/number.html +++ b/merlin/static/merlin/templates/fields/number.html @@ -3,5 +3,6 @@ + autocompletable="true" validatable-with="value"> +
{$ error $}
diff --git a/merlin/static/merlin/templates/fields/string.html b/merlin/static/merlin/templates/fields/string.html index e738076..b526869 100644 --- a/merlin/static/merlin/templates/fields/string.html +++ b/merlin/static/merlin/templates/fields/string.html @@ -2,5 +2,6 @@ + autocompletable="true" validatable-with="value"> +
{$ error $}
diff --git a/merlin/static/merlin/templates/fields/text.html b/merlin/static/merlin/templates/fields/text.html index b961209..24173c3 100644 --- a/merlin/static/merlin/templates/fields/text.html +++ b/merlin/static/merlin/templates/fields/text.html @@ -2,5 +2,6 @@ + autocompletable="true" validatable-with="value"> +
{$ error $}
diff --git a/merlin/test/js/directivesSpec.js b/merlin/test/js/directivesSpec.js index 3d8ca23..fd8fcc4 100644 --- a/merlin/test/js/directivesSpec.js +++ b/merlin/test/js/directivesSpec.js @@ -347,4 +347,83 @@ describe('merlin directives', function() { }); }); + + describe("'validatable'", function() { + var fields; + + beforeEach(inject(function($injector) { + fields = $injector.get('merlin.field.models'); + })); + + describe('working with the @constraints property:', function() { + var model, elt, + goodValue = 'allowedValue', + badValue = 'restrictedValue', + errorMessage = 'Wrong value provided'; + beforeEach(function() { + var modelClass = fields.string.extend({}, { + '@constraints': [ + function(value) { + return value !== badValue ? true : errorMessage; + } + ] + }); + $scope.model = modelClass.create(); + elt = $compile('
')($scope); + }); + + describe('any valid value', function() { + beforeEach(function() { + $scope.form.model.$setViewValue(goodValue); + $scope.$digest(); + }); + + it('is allowed to be entered', function() { + expect($scope.form.model.$viewValue).toEqual(goodValue); + }); + + it('is propagated into the model', function() { + expect($scope.model.value()).toEqual(goodValue); + }); + + it('does not cause the input to be marked as erroneous', function() { + expect(elt.find('input').hasClass('ng-valid')).toBe(true); + }); + + it('sets error message on scope to an empty string', function() { + expect($scope.error).toEqual(''); + }); + }); + + describe('any invalid value', function() { + beforeEach(function() { + $scope.form.model.$setViewValue(badValue); + $scope.$digest(); + }); + + it('is allowed to be entered', function() { + expect($scope.form.model.$viewValue).toEqual(badValue); + }); + + it('is not propagated into the model', function() { + expect($scope.model.value()).toBe(undefined); + }); + + it('causes the input to be marked as erroneous', function() { + expect(elt.find('input').hasClass('ng-invalid')).toBe(true); + }); + + it('exposes error message in the parent scope', function() { + expect($scope.error).toEqual(errorMessage); + }) + }); + }); + + describe('working with the @required property', function() { + // TODO: fill in once validation of @required fields changes in Barricade + }); + }); + });