Skip to content

Commit

Permalink
Implemented first cut of a pre-populate behaviour #219
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisala committed Nov 21, 2023
1 parent 3cbe69a commit b841c01
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 37 deletions.
8 changes: 7 additions & 1 deletion grails-app/assets/javascripts/forms-knockout-bindings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
82 changes: 49 additions & 33 deletions grails-app/assets/javascripts/forms.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}

Expand Down
76 changes: 75 additions & 1 deletion grails-app/conf/example_models/behavioursExample.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down Expand Up @@ -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"
Expand Down
16 changes: 14 additions & 2 deletions grails-app/conf/example_models/constraintsExample.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -65,7 +77,7 @@
"items": [
{
"type": "literal",
"source": "<p>This example illustrates the use of computed constraints</p><p>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.</p><p>For each selection made in a 'Value 2' field, that value will be added as a selectable option in the 'Value 3' field.</p>"
"source": "<p>This example illustrates the use of computed constraints</p><p>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.</p><p>For each selection made in a 'Value 2' field, that value will be added as a selectable option in the 'Value 3' field.</p><p>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</p>"
}
]
},
Expand Down

0 comments on commit b841c01

Please sign in to comment.