diff --git a/grails-app/assets/javascripts/forms-knockout-bindings.js b/grails-app/assets/javascripts/forms-knockout-bindings.js index 46b57600..20715f33 100644 --- a/grails-app/assets/javascripts/forms-knockout-bindings.js +++ b/grails-app/assets/javascripts/forms-knockout-bindings.js @@ -1206,10 +1206,16 @@ dataModelItem(); // register dependency on the observable. dataLoader.prepop(config).done(function (data) { data = data || {}; - var target = dataModelItem.findNearestByName(bindingContext, config.target); + 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)) { diff --git a/grails-app/assets/javascripts/forms.js b/grails-app/assets/javascripts/forms.js index 75cff735..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. @@ -300,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; } @@ -851,19 +876,10 @@ function orEmptyArray(v) { if (!context) { context = self.context; } - var result = null; - if (!_.isUndefined(context[targetName])) { - result = context[targetName]; - } else if (context['$data']) { - result = find(context['$data'], targetName) - } - else if (context['$parent'] || context['parent']) { - var parentContext = context['$parent'] || context['parent'] - // If the parent is the output model, we want to evaluate against the "data" property - parentContext = _.isObject(parentContext.data) ? parentContext.data : parentContext; - result = self.findNearestByName(targetName, parentContext); - } + ecodata.forms.navigateModel(targetName, context, function(target) { + result = target; + }) return result; } 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
" } ] },