Skip to content

Commit 295efae

Browse files
committed
Implementation of #177
1 parent e3648eb commit 295efae

File tree

5 files changed

+349
-9
lines changed

5 files changed

+349
-9
lines changed

grails-app/assets/javascripts/forms.js

+92-8
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,6 @@ function orEmptyArray(v) {
243243
};
244244
}();
245245

246-
247-
248246
function arrayFunction(array, expression, reducer, memo) {
249247
var parsedExpression = exprEval.Parser.parse(expression);
250248
var variables = parsedExpression.variables();
@@ -751,12 +749,54 @@ function orEmptyArray(v) {
751749
}
752750
else if (_.isObject(metadata.constraints)) {
753751
if (metadata.constraints.type == 'computed') {
754-
self.constraints = ko.computed(function () {
755-
var rule = _.find(metadata.constraints.options, function (option) {
756-
return ecodata.forms.expressionEvaluator.evaluateBoolean(option.condition, context);
752+
if (_.isArray(metadata.constraints.options)) {
753+
self.constraints = ko.computed(function () {
754+
var rule = _.find(metadata.constraints.options, function (option) {
755+
return ecodata.forms.expressionEvaluator.evaluateBoolean(option.condition, context);
756+
});
757+
return rule ? rule.value : metadata.constraints.default;
757758
});
758-
return rule ? rule.value : metadata.constraints.default;
759-
});
759+
}
760+
// This configuration takes a set of default constraints then either
761+
// adds values based on selections from other model items in the form or
762+
// removes selections based on other model items in the form. The main use
763+
// case this was developed for was to only allow each contraint to be selected once
764+
// in a form.
765+
else if (metadata.constraints.excludePath || metadata.constraints.includePath) {
766+
var defaultConstraints = metadata.constraints.default || [];
767+
var path = metadata.constraints.excludePath || metadata.constraints.includePath;
768+
self.constraints = ko.computed(function() {
769+
var currentValue = self();
770+
var selectedSoFar = [];
771+
context.outputModel.eachValueForPath(path, function(val) {
772+
if (_.isArray(val)) {
773+
selectedSoFar = selectedSoFar.concat(val);
774+
}
775+
else {
776+
selectedSoFar.push(val);
777+
}
778+
});
779+
780+
if (metadata.constraints.excludePath) {
781+
return _.filter(defaultConstraints, function (value) {
782+
var isCurrentSelection = _.isArray(currentValue) ? currentValue.indexOf(value) >= 0 : value == currentValue;
783+
return (isCurrentSelection || selectedSoFar.indexOf(value) < 0);
784+
});
785+
}
786+
else {
787+
var constraints = defaultConstraints.concat(selectedSoFar);
788+
var currentSelection = _.isArray(currentValue) ? currentValue : [currentValue];
789+
for (var i=0; i<currentSelection.length; i++) {
790+
791+
if (currentSelection[i] != null && constraints.indexOf(currentSelection[i]) < 0) {
792+
constraints.push(currentSelection[i]);
793+
}
794+
}
795+
796+
return constraints;
797+
}
798+
});
799+
}
760800
}
761801
else if (metadata.constraints.type == 'pre-populated') {
762802
var defaultConstraints = metadata.constraints.defaults || [];
@@ -769,7 +809,7 @@ function orEmptyArray(v) {
769809
constraintsInititaliser.resolve();
770810
});
771811
}
772-
else if (metadata.constraints.type == 'literal' || metadata.contraints.literal) {
812+
else if (metadata.constraints.type == 'literal' || metadata.constraints.literal) {
773813
self.constraints = [].concat(metadata.constraints.literal);
774814
}
775815
}
@@ -1197,6 +1237,50 @@ function orEmptyArray(v) {
11971237
return deferred;
11981238
};
11991239

1240+
/**
1241+
* Invokes the callback function for each value of the specified path in this model.
1242+
* The path must treat array/list valued items as a simple property name (without
1243+
* an index) - each array in the path will be iterated over so the callback may be
1244+
* invoked more than once.
1245+
* @param path a string specifying the path to the item of interest e.g. 'list1.list2.property'
1246+
* @param callback a function taking a single argument which will be the value of the property
1247+
* specified by path.
1248+
*/
1249+
self.eachValueForPath = function(path, callback) {
1250+
var pathAsArray = path.split('.');
1251+
self.iterateOverPath(pathAsArray, callback, self.data);
1252+
}
1253+
1254+
/**
1255+
* Recursively iterates over each element in the supplied pathAsArray, taking into
1256+
* account when values are lists.
1257+
* @param pathAsArray
1258+
* @param callback
1259+
* @param dataModel
1260+
*/
1261+
self.iterateOverPath = function(pathAsArray, callback, dataModel) {
1262+
var modelItem = ko.utils.unwrapObservable(dataModel[pathAsArray[0]]);
1263+
1264+
function nextPath(model) {
1265+
var nextPath = pathAsArray.slice(1);
1266+
self.iterateOverPath(nextPath, callback, model);
1267+
}
1268+
// If the current model is an array, iterate over all elements in the array.
1269+
if (_.isArray(modelItem)) {
1270+
for (var i=0; i<modelItem.length; i++) {
1271+
nextPath(modelItem[i]);
1272+
}
1273+
}
1274+
// Otherwise, just move deeper into the object hierarchy
1275+
else if (pathAsArray.length > 1) {
1276+
nextPath(modelItem);
1277+
}
1278+
// If we are at the final element of the path, we have reached the value we are after, so
1279+
// invoke the callback with this value.
1280+
else if (pathAsArray.length == 1) {
1281+
callback(modelItem);
1282+
}
1283+
}
12001284

12011285
self.attachDocument = function (target) {
12021286
var url = config.documentUpdateUrl || fcConfig.documentUpdateUrl;

karma.conf.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ module.exports = function (config) {
9797

9898
// Continuous Integration mode
9999
// if true, Karma captures browsers, runs the tests and exits
100-
singleRun: true
100+
singleRun: true,
101+
102+
listenAddress: '127.0.0.1'
101103
});
102104
};

src/test/js/spec/DataModelItemSpec.js

+66
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,70 @@ describe("DataModelItem Spec", function () {
134134
});
135135
});
136136

137+
it("Can allow each constraint to only be used once in the form", function() {
138+
var constraints = ['1', '2', '3', '4'];
139+
var metadata = {
140+
name:'item',
141+
dataType:'text',
142+
constraints: {
143+
type:'computed',
144+
excludePath:'list.item',
145+
default:constraints
146+
}
147+
};
148+
149+
var invokedWithPath;
150+
var vals = ['1', '3'];
151+
var customContext = _.extend({}, context, {outputModel: { eachValueForPath: function(path, callback) {
152+
invokedWithPath = path;
153+
for (var i=0; i<vals.length; i++) {
154+
callback(vals[i]);
155+
}
156+
}}});
157+
158+
var dataItem = ko.observable().extend({metadata:{metadata:metadata, context:customContext, config:config}});
159+
160+
expect(dataItem.constraints()).toEqual(['2', '4']);
161+
dataItem('1');
162+
expect(dataItem.constraints()).toEqual(['1', '2', '4']);
163+
164+
// Tt should also support array valued fields (e.g. multi-select)
165+
vals = [['1', '2'], ['3']];
166+
dataItem = ko.observableArray().extend({metadata:{metadata:metadata, context:customContext, config:config}});
167+
expect(dataItem.constraints()).toEqual(['4']);
168+
});
169+
170+
171+
it("Can support constraints being determined by other form selections", function() {
172+
173+
var metadata = {
174+
name:'item',
175+
dataType:'text',
176+
constraints: {
177+
type:'computed',
178+
includePath:'list.item',
179+
default:[]
180+
}
181+
};
182+
183+
var invokedWithPath;
184+
var vals = ['1', '3'];
185+
var customContext = _.extend({}, context, {outputModel: { eachValueForPath: function(path, callback) {
186+
invokedWithPath = path;
187+
for (var i=0; i<vals.length; i++) {
188+
callback(vals[i]);
189+
}
190+
}}});
191+
192+
var dataItem = ko.observable().extend({metadata:{metadata:metadata, context:customContext, config:config}});
193+
194+
expect(dataItem.constraints()).toEqual(['1', '3']);
195+
dataItem('4');
196+
expect(dataItem.constraints()).toEqual(['1', '3', '4']);
197+
198+
// Tt should also support array valued fields (e.g. multi-select)
199+
vals = [['1', '2'], ['3']];
200+
dataItem = ko.observableArray().extend({metadata:{metadata:metadata, context:customContext, config:config}});
201+
expect(dataItem.constraints()).toEqual(['1', '2', '3']);
202+
});
137203
});

src/test/js/spec/OutputModelSpec.js

+61
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,65 @@ describe("OutputModel Spec", function () {
7474

7575
});
7676

77+
78+
var nestedViewModelData = {
79+
"name": "Nested lists",
80+
"data": {
81+
"number1": "3",
82+
"notes": "notes",
83+
"list": [
84+
{
85+
"value1": "0.value1",
86+
"nestedList": [
87+
{
88+
"value2": "0.0.value2"
89+
},
90+
{
91+
"value2": "0.1.value2"
92+
}
93+
]
94+
},
95+
{
96+
"value1": "1.value1",
97+
"nestedList": [
98+
{
99+
"value2": "1.0.value2"
100+
},
101+
{
102+
"value2": "1.1.value2"
103+
},
104+
{
105+
"value2": "1.2.value2"
106+
}
107+
]
108+
}
109+
]
110+
}
111+
};
112+
113+
it("The eachValueForPath method can correctly handle properties inside nested lists", function() {
114+
var model = new ecodata.forms.TwiceNestedViewModel(
115+
{name:"TwiceNestedViewModel"}, ecodata.forms.twiceNestedViewModel, {}, {});
116+
model.loadData(nestedViewModelData.data);
117+
118+
var values = [];
119+
model.eachValueForPath("list.nestedList.value2", function(val) {values.push(val) });
120+
121+
expect(values).toEqual(['0.0.value2', '0.1.value2', '1.0.value2', '1.1.value2', '1.2.value2']);
122+
123+
values = [];
124+
model.iterateOverPath(['list', 'nestedList', 'value2'], function(val) { values.push(val) }, nestedViewModelData.data);
125+
126+
expect(values).toEqual(['0.0.value2', '0.1.value2', '1.0.value2', '1.1.value2', '1.2.value2']);
127+
128+
values = [];
129+
model.eachValueForPath('list.value1', function(val) { values.push(val) });
130+
131+
expect(values).toEqual(['0.value1', '1.value1']);
132+
133+
values = [];
134+
model.eachValueForPath('notes', function(val) { values.push(val) });
135+
expect(values).toEqual(['notes']);
136+
});
137+
77138
});

0 commit comments

Comments
 (0)