Skip to content

Commit

Permalink
Merge pull request #126 from legalthings/repeater_issue_with_expressions
Browse files Browse the repository at this point in the history
Repeater issue with expressions
  • Loading branch information
svenstm authored Dec 6, 2018
2 parents 7642e3c + a0acab6 commit 1c94664
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 5 deletions.
11 changes: 10 additions & 1 deletion js/legalform-calc.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ function LegalFormCalc($) {
data[name + '-validation'] = expandCondition(field.validation, step.group || '', true);
}

if (type === 'expression') {
if (type === 'expression' && !step.repeater) {
setComputedForExpression(name, step, field, data);
} else if (type === 'external_data' || field.external_source) {
setComputedForExternalUrls(name, step, field, data);
Expand Down Expand Up @@ -157,6 +157,15 @@ function LegalFormCalc($) {
meta.yearly = !!(typeof field.yearly !== 'undefined' && field.yearly);
}

if (type === 'expression' && step.repeater) {
var expression = {};
var name = (step.group ? step.group + '.' : '') + field.name;
var key = name + '-expression';

setComputedForExpression(name, step, field, expression);
meta.expressionTmpl = expression[key];
}

addGroupedData(data, step.group, field.name, meta);
});

Expand Down
71 changes: 71 additions & 0 deletions js/lib/ractive-dynamic-computed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Dynamic addition and removing of computed properties in current used version of Ractive (0.9.13) is not supported out of the box.
* So we use this object for that purpose. It uses code, extracted from ractive, and simplified to only cover our needs
* (that is only support computed properties, given as strings).
*/

if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = RactiveDynamicComputed;
}

function RactiveDynamicComputed() {
var self = this;

this.dotRegExp = /\./g;
this.computedVarRegExp = /\$\{([^\}]+)\}/g;

/**
* Remove computed property from existing rative instance
* @param {object} ractive
* @param {string} key
*/
this.remove = function(ractive, key) {
var escapedKey = key.replace(dotRegExp, '\\.');

delete ractive.computed[key];
delete ractive.viewmodel.computations[escapedKey];
}

/**
* Add computed expression to existing ractive instance
* @param {object} ractive
* @param {string} key
* @param {string} value
*/
this.add = function(ractive, key, value) {
var signature = getComputationSignature(ractive, key, value);

ractive.computed[key] = value;
ractive.viewmodel.compute(key, signature);
}

function getComputationSignature(ractive, key, signature) {
if (typeof signature !== 'string') {
throw 'Unable to dynamically add computed property with value of type ' + (typeof signature);
}

var getter = createFunctionFromString(signature, ractive);
var getterString = signature;

return {
getter: getter,
setter: undefined,
getterString: getterString,
setterString: undefined,
getterUseStack: undefined
};
}

function createFunctionFromString(str, bindTo) {
var hasThis;

var functionBody = 'return (' + str.replace(self.computedVarRegExp, function (match, keypath) {
hasThis = true;
return ("__ractive.get(\"" + keypath + "\")");
}) + ');';

if (hasThis) { functionBody = "var __ractive = this; " + functionBody; }
var fn = new Function( functionBody );
return hasThis ? fn.bind( bindTo ) : fn;
}
}
22 changes: 22 additions & 0 deletions js/lib/repeated-step-expression.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = tmplToExpression;
}

var dotRegExp = /\./g;

/**
* Insert repeated step index into expression
* @param {object} step
* @param {string} expression
* @return {string}
*/
function tmplToExpression(expressionTmpl, group, idx) {
var prefix = group + '.';
prefix = prefix.replace(dotRegExp, '\\.');

var prefixRegExp = new RegExp('\\$\\{' + prefix, 'g');
var replacement = '${' + group + '[' + idx + '].';

return expressionTmpl.replace(prefixRegExp, replacement);
}
84 changes: 80 additions & 4 deletions js/ractive-legalform.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
*/
validation: null,

/**
* Expressions used in repeated steps
*/
repeatedStepExpressions: {},

/**
* Suffixes in keypath names that determine their special behaviour
* @type {Object}
Expand Down Expand Up @@ -204,7 +209,7 @@
//Use timeout because of some ractive bug: expressions, that depend on setting key, may be not updated, or can even cause an error
setTimeout(function() {
ractive.set(setName, newValue);

if (newValue) {
$(input).parent().removeClass('is-empty');
}
Expand Down Expand Up @@ -284,15 +289,21 @@
var repeater = newValue;
var stepCount = value.length;

if (!repeater) value.length = 0;
else if (repeater < stepCount) value = value.slice(0, repeater);
else if (repeater > stepCount) {
if (!repeater && stepCount) {
this.removeRepeatedStepExpression(name, 0, stepCount);
value.length = 0;
} else if (repeater < stepCount) {
this.removeRepeatedStepExpression(name, repeater, stepCount);
value = value.slice(0, repeater);
} else if (repeater > stepCount) {
var addLength = repeater - stepCount;
for (var i = 0; i < addLength; i++) {
value.push($.extend(true, {}, tmpl));
}
}

this.addRepeatedStepExpression(name, 0, value.length);

ractive.set(name, value);

var meta = ractive.get('meta');
Expand All @@ -303,6 +314,69 @@
ractive.set('meta', meta);
},

/**
* Save repeated step expression tmpl to cache on ractive init
* @param {string} keypath
* @param {string} expressionTmpl
*/
cacheExpressionTmpl: function(keypath, expressionTmpl) {
var parts = keypath.split('.0.');
if (parts.length !== 2) return; // Step is not repeatable (shouldn't happen) or has nested arrays (can't be, just in case)

var group = parts[0];
var fieldName = parts[1];
var cache = this.repeatedStepExpressions;

if (typeof cache[group] === 'undefined') cache[group] = {};
cache[group][fieldName] = expressionTmpl;
},

/**
* Create computed expression dynamically for repeated step
* @param {string} group
* @param {int} fromStepIdx
* @param {int} stepCount
*/
addRepeatedStepExpression: function(group, fromStepIdx, stepCount) {
var expressionTmpls = this.repeatedStepExpressions[group];
if (typeof expressionTmpls === 'undefined' || !expressionTmpls) return;

for (var idx = fromStepIdx; idx < stepCount; idx++) {
var prefix = group + '[' + idx + ']';

for (var key in expressionTmpls) {
var keypath = prefix + '.' + key + this.suffix.expression;
var value = this.get(keypath);

if (typeof value !== 'undefined') continue;

var tmpl = expressionTmpls[key];
var expression = tmplToExpression(tmpl, group, idx);
this.ractiveDynamicComputed.add(this, keypath, expression);
}
}
},

/**
* Remove computed expressions for repeated steps
* @param {string} group
* @param {int} fromStepIdx
* @param {int} stepCount
*/
removeRepeatedStepExpression: function(group, fromStepIdx, stepCount) {
var expressionTmpls = this.repeatedStepExpressions[group];
if (typeof expressionTmpls === 'undefined' || !expressionTmpls) return;

for (var idx = fromStepIdx; idx < stepCount; idx++) {
var prefix = group + '[' + idx + ']';

for (var key in expressionTmpls) {
var keypath = prefix + '.' + key + this.suffix.expression;
this.ractiveDynamicComputed.remove(this, keypath);
}
}
},

/**
* Show / hide likert questions
*/
Expand Down Expand Up @@ -877,6 +951,8 @@
setByKeyPath(ractive.defaults, key, today);
} else if (meta.type === "date") {
setByKeyPath(ractive.defaults, key, "");
} else if (meta.type === 'expression' && typeof meta.expressionTmpl !== 'undefined') {
ractive.cacheExpressionTmpl(key, meta.expressionTmpl);
}
});

Expand Down
42 changes: 42 additions & 0 deletions spec/lib/repeated-step-expression.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use strict';

describe("test inserting repeated step index into expression", function() {
var tmplToExpression = require('../../js/lib/repeated-step-expression');

function usePlaceholderProvider() {
return [
{
note: 'insert indexes for "foo" step',
group: 'foo',
expression: '${foo.bar} + ${baz.bar} + "test" + "foo." + ${foo.zoo} + ${test.foo.zoo}',
expected: '${foo[2].bar} + ${baz.bar} + "test" + "foo." + ${foo[2].zoo} + ${test.foo.zoo}'
},
{
note: 'insert indexes for "foo.bar.baz" step',
group: 'foo.bar.baz',
expression: '${foo.bar.baz.prop1} + ${baz.bar} + "test" + "foo.bar.baz." + ${foo.bar.baz.prop2} + ${test.foo.bar.baz.zoo}',
expected: '${foo.bar.baz[2].prop1} + ${baz.bar} + "test" + "foo.bar.baz." + ${foo.bar.baz[2].prop2} + ${test.foo.bar.baz.zoo}'
},
{
note: 'return expression as it is if step group is not set',
group: '',
expression: '${foo.bar} + ${baz.bar} + "test" + "foo." + ${foo.zoo} + ${test.foo.zoo}',
expected: '${foo.bar} + ${baz.bar} + "test" + "foo." + ${foo.zoo} + ${test.foo.zoo}'
},
{
note: 'return expression as it is if step group is not found in expression',
group: 'not_used',
expression: '${foo.bar} + ${baz.bar} + "test" + "foo." + ${foo.zoo} + ${test.foo.zoo}',
expected: '${foo.bar} + ${baz.bar} + "test" + "foo." + ${foo.zoo} + ${test.foo.zoo}'
},
];
}

usePlaceholderProvider().forEach(function(spec) {
it(spec.note, function() {
var result = tmplToExpression(spec.expression, spec.group, 2);

expect(result).toBe(spec.expected);
});
});
});

0 comments on commit 1c94664

Please sign in to comment.