diff --git a/grails-app/assets/javascripts/forms-knockout-bindings.js b/grails-app/assets/javascripts/forms-knockout-bindings.js index 951b11b6..5707d602 100644 --- a/grails-app/assets/javascripts/forms-knockout-bindings.js +++ b/grails-app/assets/javascripts/forms-knockout-bindings.js @@ -679,7 +679,10 @@ var options = _.defaults(valueAccessor() || {}, defaults); $(element).select2(options).change(function(e) { - model($(element).val()); + if (ko.isWritableObservable(model)) { // Don't try and write the value to a computed. + model($(element).val()); + } + }); if (options.preserveColumnWidth) { @@ -1173,7 +1176,54 @@ }); }); // This is a computed rather than a pureComputed as it has a side effect. return target; - } + }; + + ko.bindingHandlers['triggerPrePopulate'] = { + 'update': function (element, valueAccessor, allBindings, viewModel, bindingContext) { + + + var dataModelItem = valueAccessor(); + var behaviours = dataModelItem.get('behaviour'); + for (var i = 0; i < behaviours.length; i++) { + var behaviour = behaviours[i]; + + if (behaviour.type == 'pre_populate') { + var config = behaviour.config; + var dataLoaderContext = dataModelItem.context; + + var dataLoader = new ecodata.forms.dataLoader(dataLoaderContext, dataModelItem.config); + + var dependencyTracker = ko.computed(function () { + dataModelItem(); // register dependency on the observable. + dataLoader.prepop(config).done(function (data) { + data = data || {}; + var target = config.target; + if (!target) { + target = viewModel; + } + else { + target = dataModelItem.findNearestByName(target, bindingContext); + } + if (!target) { + throw "Unable to locate target for pre-population: "+target; + } + if (_.isFunction(target.loadData)) { + target.loadData(data); + } else if (_.isFunction(target.load)) { + target.load(data); + } else if (ko.isObservable(target)) { + target(data); + } else { + console.log("Warning: target for pre-populate is invalid"); + } + + }); // This is a computed rather than a pureComputed as it has a side effect. + }); + } + } + + } + }; })(); diff --git a/grails-app/assets/javascripts/forms.js b/grails-app/assets/javascripts/forms.js index 6621d316..2c395a3a 100644 --- a/grails-app/assets/javascripts/forms.js +++ b/grails-app/assets/javascripts/forms.js @@ -179,6 +179,49 @@ function orEmptyArray(v) { } }; + /** + * Traverses the model or binding context starting from a nested context and + * working backwards towards the root until a property with the supplied name + * is matched. That property is then passed to the supplied callback. + * Traversing backwards is simpler than forwards as we don't need to take into + * account repeating model values (e.g. for repeating sections and table rows) + * @param targetName the name of the model variable / property to find. + * @param context the starting context + * @param callback a function to invoke when the target variable is found. + */ + ecodata.forms.navigateModel = function(targetName, context, callback) { + if (!context) { + return; + } + if (!_.isUndefined(context[targetName])) { + callback(context[targetName]); + } + // If the context is a knockout binding context, $data will be the current object + // being bound to the view. + else if (context['$data']) { + ecodata.forms.navigateModel(targetName, context['$data'], callback); + } + // The root data model is constructed with fields inside a nested "data" object. + else if (_.isObject(context['data'])) { + ecodata.forms.navigateModel(targetName, context['data'], callback); + } + // Try to evaluate against the parent - the bindingContext uses $parent and the + // ecodata.forms.DataModelItem uses parent + else if (context['$parent']) { + ecodata.forms.navigateModel(targetName, context['$parent'], callback); + } + else if (context['parent']) { + ecodata.forms.navigateModel(targetName, context['parent'], callback); + } + // Try to evaluate against the context - this is setup as a model / binding context + // variable and refers to data external to the form - e.g. the project or activity the + // form is related to. + else if (context['$context']) { + ecodata.forms.navigateModel(targetName, context['$context'], callback); + } + + } + /** * Helper function for evaluating expressions defined in the metadata. These may be used to compute values * or make decisions on which constraints to apply to individual data model items. @@ -253,6 +296,10 @@ function orEmptyArray(v) { return _.findWhere(list, obj); }; + parser.functions.deepEquals = function(value1, value2) { + return _.isEqual(value1, value2); + }; + var specialBindings = function() { return { @@ -296,27 +343,9 @@ function orEmptyArray(v) { result = specialBindings[contextVariable]; } else { - if (!_.isUndefined(context[contextVariable])) { - result = ko.utils.unwrapObservable(context[contextVariable]); - } - else { - // The root view model is constructed with fields inside a nested "data" object. - if (_.isObject(context['data'])) { - result = bindVariable(variable, context['data']); - } - // Try to evaluate against the parent - else if (context['$parent']) { - // If the parent is the output model, we want to evaluate against the "data" property - var parentContext = _.isObject(context['$parent'].data) ? context['$parent'].data : context['$parent']; - result = bindVariable(variable, parentContext); - } - // Try to evaluate against the context - used when we are evaluating pre-pop data with a filter - // expression that references a variable in the form context - else if (context['$context']) { - result = bindVariable(variable, context['$context']); - } - } - + ecodata.forms.navigateModel(contextVariable, context, function(target) { + result = ko.utils.unwrapObservable(target); + }); } return _.isUndefined(result) ? null : result; } @@ -334,14 +363,14 @@ function orEmptyArray(v) { var expressionCache = {}; function evaluateInternal(expression, context) { - var parsedExpression = expressionCache[expression]; - if (!parsedExpression) { - parsedExpression = parser.parse(expression); - expressionCache[expression] = parsedExpression; - } + var parsedExpression = expressionCache[expression]; + if (!parsedExpression) { + parsedExpression = parser.parse(expression); + expressionCache[expression] = parsedExpression; + } - var variables = parsedExpression.variables(); - var boundVariables = bindVariables(variables, context); + var variables = parsedExpression.variables(); + var boundVariables = bindVariables(variables, context); var result; try { @@ -619,16 +648,32 @@ function orEmptyArray(v) { if (prepopData) { var result = prepopData; var mapping = conf.mapping; - if (conf.filter && conf.filter.expression) { - if (!_.isArray(prepopData)) { - throw "Filtering is only supported for array typed prepop data." + + function postProcessPrepopData(processingFunction, processingConfig, data) { + if (!_.isArray(data)) { + throw "Filter/find is only supported for array typed prepop data." } - result = _.filter(result, function(item) { - var expression = conf.filter.expression; - var itemContext = _.extend({}, item, {$context:context, $config:config}); - return ecodata.forms.expressionEvaluator.evaluateBoolean(expression, itemContext); + if (!processingConfig.expression) { + throw "Missing expression attribute in configuration" + } + return processingFunction(data, function(item) { + var expression = processingConfig.expression; + var namespace = processingConfig['namespace'] || 'item'; + var evalContext = {}; + evalContext[namespace] = item; + + var evalContext = _.extend(evalContext, {$context:context, $config:config}); + return ecodata.forms.expressionEvaluator.evaluateBoolean(expression, evalContext); }); } + + if (conf.filter) { + result = postProcessPrepopData(_.filter, conf.filter, result); + } + else if (conf.find) { + result = postProcessPrepopData(_.find, conf.find, result); + } + if (mapping) { result = self.map(mapping, result); } @@ -796,16 +841,46 @@ function orEmptyArray(v) { function buildPrepopConstraints(constraintsConfig, constraintsDeferred) { var defaultConstraints = constraintsConfig.defaults || []; var constraintsObservable = ko.observableArray(defaultConstraints); - - return ko.computed(function() { - var dataLoaderContext = _.extend({}, context, {$parent:context.parent}); - var dataLoader = ecodata.forms.dataLoader(dataLoaderContext, config); - dataLoader.prepop(constraintsConfig.config).done(function (data) { + var dataLoaderContext = _.extend({}, context, {$parent:context.parent}); + var dataLoader = ecodata.forms.dataLoader(dataLoaderContext, config); + + ko.computed(function() { + var prepopConf = constraintsConfig.config; + // If the prepop needs to post process the data, we need to execute the post processing + // in the context of the computed so as to register any dependencies on values used in + // the post-processing expressions. + // The dataloader won't execute post processing synchronously as retrieving the data can be done + // via a remote call. + if (prepopConf.filter) { + ecodata.forms.expressionEvaluator.evaluate(prepopConf.filter.expression, dataLoaderContext); + } + if (prepopConf.find) { + ecodata.forms.expressionEvaluator.evaluate(prepopConf.filter.expression, dataLoaderContext); + } + dataLoader.prepop(prepopConf).done(function (data) { constraintsObservable(data); constraintsDeferred.resolve(); }); - return constraintsObservable(); }); + + return constraintsObservable; + } + + /** + * Finds the model attribute with the specified name searching from the context of this + * DataModelItem if no context is supplied. + * This is so that for nested items we can find the nearest neighbour with the specified name. (e.g. + * when the model represents a repeating section or table row) + */ + self.findNearestByName = function(targetName, context) { + if (!context) { + context = self.context; + } + var result = null; + ecodata.forms.navigateModel(targetName, context, function(target) { + result = target; + }) + return result; } function attachIncludeExclude(constraints) { diff --git a/grails-app/conf/example_models/behavioursExample.json b/grails-app/conf/example_models/behavioursExample.json index 6d697c36..d588fa52 100644 --- a/grails-app/conf/example_models/behavioursExample.json +++ b/grails-app/conf/example_models/behavioursExample.json @@ -31,6 +31,47 @@ } } ] + }, + { + "dataType": "text", + "name": "item5", + "behaviour": [ + { + "config": { + "source": { + "url": "/preview/prepopulate", + "params": [ + { + "name": "param", + "type": "computed", + "expression": "item5" + }, + { + "name": "item5", + "type": "computed", + "expression": "item5" + } + ] + }, + "mapping": [ + { + "source-path": "param", + "target": "item6" + }, + { + "source-path": "item5", + "target": "item5" + } + ], + "target": "$data" + }, + "type": "pre_populate" + } + ] + }, + { + "dataType": "text", + "name": "item6" } ], "viewModel": [ @@ -69,7 +110,40 @@ "title": "Item 4" } ] - + }, + { + "type": "row", + "items": [ + { + "type": "col", + "items": [ + { + "type": "literal", + "source": "Note for this example, data entered into item5 will trigger a pre-pop call and be mapped back to item5 and item6. Note that the target of the pre-pop is $data which is the current binding context (or the root object in this case). A current limitation is the load method is used, which means if the pre-pop result does not contain keys for all data in the target object, the data for missing fields will be set to undefined. A planned enhancement is to only replace data where keys in the pre-pop data exist." + } + ] + } + ] + }, + { + "type": "row", + "items": [ + { + "type": "col", + "items": [ + { + "preLabel": "Item 5", + "source": "item5", + "type": "text" + }, + { + "preLabel": "Item 6", + "source": "item6", + "type": "text" + } + ] + } + ] } ], "title": "Behaviours example" diff --git a/grails-app/conf/example_models/constraintsExample.json b/grails-app/conf/example_models/constraintsExample.json index 299837e9..8832be70 100644 --- a/grails-app/conf/example_models/constraintsExample.json +++ b/grails-app/conf/example_models/constraintsExample.json @@ -20,7 +20,19 @@ "type": "pre-populated", "config": { "source": { - "url": "/preview/prepopulateConstraints" + "url": "/preview/prepopulateConstraints", + "params" : [ + { + "name":"p1", + "value":"1" + }, + { + "name": "p2", + "type": "computed", + "expression": "number1" + } + + ] } }, "excludePath": "list.value1" @@ -65,7 +77,7 @@ "items": [ { "type": "literal", - "source": "

This example illustrates the use of computed constraints

The 'Value 2' field will only allow each item in the dropdown to be selected once, no matter how many times 'Value 2' appears on the page.

For each selection made in a 'Value 2' field, that value will be added as a selectable option in the 'Value 3' field.

" + "source": "

This example illustrates the use of computed constraints

The 'Value 2' field will only allow each item in the dropdown to be selected once, no matter how many times 'Value 2' appears on the page.

For each selection made in a 'Value 2' field, that value will be added as a selectable option in the 'Value 3' field.

Note also that the constraints for 'value1' include a parameter that references a form variable. When that variable changes, the constraint pre-population is re-executed

" } ] }, diff --git a/package-lock.json b/package-lock.json index 4bc48ade..a958c794 100644 --- a/package-lock.json +++ b/package-lock.json @@ -771,13 +771,14 @@ } }, "node_modules/asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" } }, "node_modules/assert": { @@ -1046,28 +1047,80 @@ } }, "node_modules/browserify-rsa": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", + "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", "dependencies": { - "bn.js": "^4.1.0", + "bn.js": "^5.0.0", "randombytes": "^2.0.1" } }, + "node_modules/browserify-rsa/node_modules/bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + }, "node_modules/browserify-sign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", - "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.2.tgz", + "integrity": "sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==", + "dependencies": { + "bn.js": "^5.2.1", + "browserify-rsa": "^4.1.0", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.4", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.6", + "readable-stream": "^3.6.2", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/browserify-sign/node_modules/bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + }, + "node_modules/browserify-sign/node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/browserify-sign/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dependencies": { - "bn.js": "^4.1.1", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.2", - "elliptic": "^6.0.0", - "inherits": "^2.0.1", - "parse-asn1": "^5.0.0" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" } }, + "node_modules/browserify-sign/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/browserify-zlib": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", @@ -1718,9 +1771,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.592", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.592.tgz", - "integrity": "sha512-D3NOkROIlF+d5ixnz7pAf3Lu/AuWpd6AYgI9O67GQXMXTcCP1gJQRotOq35eQy5Sb4hez33XH1YdTtILA7Udww==", + "version": "1.4.594", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.594.tgz", + "integrity": "sha512-xT1HVAu5xFn7bDfkjGQi9dNpMqGchUkebwf1GL7cZN32NSwwlHRPMSDJ1KN6HkS0bWUtndbSQZqvpQftKG2uFQ==", "dev": true }, "node_modules/elliptic": { @@ -3256,15 +3309,15 @@ } }, "node_modules/parse-asn1": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", - "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", + "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", "dependencies": { - "asn1.js": "^4.0.0", + "asn1.js": "^5.2.0", "browserify-aes": "^1.0.0", - "create-hash": "^1.1.0", "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3" + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" } }, "node_modules/parseurl": { @@ -3611,8 +3664,7 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/semver": { "version": "6.3.1", @@ -4858,13 +4910,14 @@ } }, "asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", "requires": { "bn.js": "^4.0.0", "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" } }, "assert": { @@ -5139,26 +5192,62 @@ } }, "browserify-rsa": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", + "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", "requires": { - "bn.js": "^4.1.0", + "bn.js": "^5.0.0", "randombytes": "^2.0.1" + }, + "dependencies": { + "bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + } } }, "browserify-sign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", - "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", - "requires": { - "bn.js": "^4.1.1", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.2", - "elliptic": "^6.0.0", - "inherits": "^2.0.1", - "parse-asn1": "^5.0.0" + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.2.tgz", + "integrity": "sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==", + "requires": { + "bn.js": "^5.2.1", + "browserify-rsa": "^4.1.0", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.4", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.6", + "readable-stream": "^3.6.2", + "safe-buffer": "^5.2.1" + }, + "dependencies": { + "bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } } }, "browserify-zlib": { @@ -5644,9 +5733,9 @@ "dev": true }, "electron-to-chromium": { - "version": "1.4.592", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.592.tgz", - "integrity": "sha512-D3NOkROIlF+d5ixnz7pAf3Lu/AuWpd6AYgI9O67GQXMXTcCP1gJQRotOq35eQy5Sb4hez33XH1YdTtILA7Udww==", + "version": "1.4.594", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.594.tgz", + "integrity": "sha512-xT1HVAu5xFn7bDfkjGQi9dNpMqGchUkebwf1GL7cZN32NSwwlHRPMSDJ1KN6HkS0bWUtndbSQZqvpQftKG2uFQ==", "dev": true }, "elliptic": { @@ -6882,15 +6971,15 @@ } }, "parse-asn1": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", - "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", + "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", "requires": { - "asn1.js": "^4.0.0", + "asn1.js": "^5.2.0", "browserify-aes": "^1.0.0", - "create-hash": "^1.1.0", "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3" + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" } }, "parseurl": { @@ -7184,8 +7273,7 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "semver": { "version": "6.3.1", diff --git a/src/main/groovy/au/org/ala/ecodata/forms/ConstraintType.groovy b/src/main/groovy/au/org/ala/ecodata/forms/ConstraintType.groovy index 17067e43..7681e362 100644 --- a/src/main/groovy/au/org/ala/ecodata/forms/ConstraintType.groovy +++ b/src/main/groovy/au/org/ala/ecodata/forms/ConstraintType.groovy @@ -12,7 +12,8 @@ enum ConstraintType { ENABLE("enable", true, false), ENABLE_AND_CLEAR("enableAndClear", true, false), DISABLE("disable", true, false), - CONDITIONAL_VALIDATION("conditionalValidation", false, false) + CONDITIONAL_VALIDATION("conditionalValidation", false, false), + PRE_POPULATE("triggerPrePopulate", false, false) /** The knockout data binding that implements this constraint */ String binding diff --git a/src/main/groovy/au/org/ala/ecodata/forms/EditModelWidgetRenderer.groovy b/src/main/groovy/au/org/ala/ecodata/forms/EditModelWidgetRenderer.groovy index a3b44ba3..39cd4a81 100644 --- a/src/main/groovy/au/org/ala/ecodata/forms/EditModelWidgetRenderer.groovy +++ b/src/main/groovy/au/org/ala/ecodata/forms/EditModelWidgetRenderer.groovy @@ -134,10 +134,13 @@ public class EditModelWidgetRenderer implements ModelWidgetRenderer { context.writer << "" } + private static boolean isReadOnly(WidgetRenderContext context) { + context.model.readonly || context.dataModel.computed + } @Override void renderSelectMany(WidgetRenderContext context) { - if (context.model.readonly) { + if (isReadOnly(context)) { renderSelectManyAsString(context) } else { @@ -147,16 +150,23 @@ public class EditModelWidgetRenderer implements ModelWidgetRenderer { @Override void renderSelect2Many(WidgetRenderContext context) { - context.databindAttrs.add 'options', context.source + '.constraints' - context.databindAttrs.add 'optionsValue', context.source + '.constraints.value' - context.databindAttrs.add 'optionsText', context.source + '.constraints.text' - String options = "{value: ${context.source}, tags:true, allowClear:false}" - if (context.model.displayOptions) { - options = "_.extend({value:${context.source}}, ${context.source}.displayOptions)" + if (isReadOnly(context)) { + renderSelectManyAsString(context) + } + else { + context.databindAttrs.add 'options', context.source + '.constraints' + context.databindAttrs.add 'optionsValue', context.source + '.constraints.value' + context.databindAttrs.add 'optionsText', context.source + '.constraints.text' + + String options = "{value: ${context.source}, tags:true, allowClear:false}" + if (context.model.displayOptions) { + options = "_.extend({value:${context.source}}, ${context.source}.displayOptions)" + } + context.databindAttrs.add 'multiSelect2', options + context.writer << "" } - context.databindAttrs.add 'multiSelect2', options - context.writer << "" + } @Override diff --git a/src/test/js/spec/TriggerPrePopulateBindingSpec.js b/src/test/js/spec/TriggerPrePopulateBindingSpec.js new file mode 100644 index 00000000..b328d50e --- /dev/null +++ b/src/test/js/spec/TriggerPrePopulateBindingSpec.js @@ -0,0 +1,61 @@ +describe("triggerPrePopulate binding handler Spec", function () { + + var mockElement; + beforeEach(function () { + jasmine.clock().install(); + + mockElement = document.createElement('input'); + document.body.appendChild(mockElement); + + }); + + afterEach(function () { + jasmine.clock().uninstall(); + document.body.removeChild(mockElement); + }); + + it("should add the score class and a tooltip to the element", function () { + var metadata = { + name:'item', + dataType:'number', + behaviour: [ + { + type:"pre_populate", + config: { + source: { + "context-path":"test" + }, + target: "item2" + + } + } + ] + }; + var context = { + test: { + val1:"1", + item3: "3" + } + }; + var config = {}; + var dataItem = ko.observable().extend({metadata:{metadata:metadata, context:context, config:config}}); + + + var model = { + item:dataItem, + item2: { + load:function(data) { + this.item3(data.item3); + }, + item3:ko.observable() + } + } + $(mockElement).attr('data-bind', 'triggerPrePopulate:item'); + ko.applyBindings(model, mockElement); + + jasmine.clock().tick(10); + + expect(model.item2.item3()).toEqual("3"); + + }); +}); \ No newline at end of file