few weeks ago I've started to work with typescript and knockoutJS, I have a specific problem and yet I have solution for it, it's so ugly I can't stand that, but can't get anything better from it, there's too much code to be pasted, but i'll try to describe my problem the best I can:
I have two view models that communicate with the same data model. Let's say that model is an array of Simple Objects called Numbers. Every Number has following properties: Value, isMinValueEnabled, minValue, isMaxValueEnabled, maxValue, isStepEnabled, stepValue, valueFormat. valueFormat might be numeric or percentage (so that value, min, max and step are multiplied by 100). I can activate minimum, maximum and step values and deactivate them. Then save data to the model and do exactly the same (with some restrictions) in another viewModel.
The problem is with those optional parameters and percentage values, because when I'm reading data I firstly check if Number is percentage or not and if every property is Enabled. Then I eventually multiply value by 100 if it is set. I have to do the same operation when I'm saving data, that is check every number for format and is*Enabled and eventually divide by 100. With 3-4 properties there is no problem, but now I have to write few more optional properties that depend's on the format and enabled/disabled state and I'm getting into troubles with ton's of if's statements, I myself can't even read that. Is there some better patter that can be used in this situation?
EDIT
Ok, so things look like this: I have a series of numbers, they can look like:
100, 2 000, 34 000.21, 2.1k, 2.11M, 22% but those are only display values whereas real values should stand like this for the example given: 100, 2000, 34000.21, 2100, 2110000, 0.22. The user can edit the value to anything else, like, let's say has 22% in input and then edit this into 1k. I shall convert 1k to original value which is 1000 and check if minimumValue and maximumValue for that number are set. If they are, I will check, and let's say maxValue is 800, then user input can no longer be 1k, but 0.8k instead because he can not get out of maximumValue. MinimumValue, MaximumValue, StepValue and so on are properties of every single Number. I was playing with ko.pureComputed, but I need to abstract it somehow:
var f = ko.computed(<KnockoutComputedDefine<number>>{
read: ...
write: ...
});
What I have now is totally ugly and looks like this:
export class Variable {
[...]
public inputType: KnockoutObservable<VariableInputType>;
public typeAndFormat: KnockoutObservable<DataTypeFormat>;
public isMinEnabled: KnockoutObservable<boolean>;
public minValue: KnockoutObservable<number>;
public isMaxEnabled: KnockoutObservable<boolean>;
public maxValue: KnockoutObservable<number>;
public isStepEnabled: KnockoutObservable<boolean>;
public stepValue: KnockoutObservable<number>;
public value: KnockoutObservable<number>;
[...]
constructor(...) {
[...]
this.inputType = ko.observable(VariableInputType.Input);
this.typeAndFormat = ko.observable(variable.typeAndFormat || DataTypeFormat.Number);
if (variable.minValue !== null) {
this.isMinEnabled = ko.observable(true);
this.minValue = ko.observable(variable.minValue);
} else {
this.isMinEnabled = ko.observable(false);
this.minValue = ko.observable(null);
}
if (variable.maxValue !== null) {
this.isMaxEnabled = ko.observable(true);
this.maxValue = ko.observable(variable.maxValue);
} else {
this.isMaxEnabled = ko.observable(false);
this.maxValue = ko.observable(null);
}
if (variable.step !== null) {
this.isStepEnabled = ko.observable(true);
this.stepValue = ko.observable(variable.step);
} else {
this.isStepEnabled = ko.observable(false);
this.stepValue = ko.observable(null);
}
if (variable.defaultValue !== null) {
this.value = ko.observable(variable.defaultValue);
} else {
this.value = ko.observable(0);
}
if (this.typeAndFormat() === DataTypeFormat.NumberPercentage) {
this.value(this.value() * 100);
if (this.isMinEnabled()) this.minValue(this.minValue() * 100);
if (this.isMaxEnabled()) this.maxValue(this.maxValue() * 100);
if (this.isStepEnabled()) this.stepValue(this.stepValue() * 100);
}
[...]
this.isMinEnabled.subscribe((v) => { if (v !== true) this.minValue(null) }, this);
this.isMaxEnabled.subscribe((v) => { if (v !== true) this.maxValue(null) }, this);
this.isStepEnabled.subscribe((v) => { if (v !== true) this.stepValue(null)}, this);
[...]
}
public getModifiedVariable() {
[...]
this.originalData.typeAndFormat = this.typeAndFormat();
this.originalData.minValue = this.minValue();
this.originalData.maxValue = this.maxValue();
this.originalData.step = this.stepValue();
this.originalData.defaultValue = this.value();
[...]
if (this.typeAndFormat() === DataTypeFormat.NumberPercentage) {
this.originalData.defaultValue = this.originalData.defaultValue / 100;
if (this.isMinEnabled()) this.originalData.minValue = this.originalData.minValue / 100;
if (this.isMaxEnabled()) this.originalData.maxValue = this.originalData.maxValue / 100;
if (this.isStepEnabled()) this.originalData.step = this.originalData.step / 100;
}
[...]
return this.originalData;
};
[...]
}
The second viewmodel that has even more validation and restrictions looks even worse... I don't really know how I could abstract that so that it would be readable for me and for others.
There are two different problems
you need a custom visualization, that needs formatting, and a custom data input, that needs parsing
you need to add some business logic (validation of values)
The first question can be solved by using an extender. With this technique your observable must store the actual value, not the formatted value. You can use it to add a child observable, which could be called formattedValue. This must be a writable computed observable, which two functions:
read: format the underlying actual value, and return it formatted, so that the user has a beautiful view of the value
write: parse the value received from the user input, and store the result in the underlying actual value
You can find examples of extenders like theses ones: Three Useful Knockout Extenders. The extenders can recevie parameters, so that they can be configured individually (in your case you can set percentage, steps, and so on). Another big example of this technique is the ko.valdiation library.
If you use this technique, in the HTML you need to bind the child observable, instead of the underlying observable with the real value, i.e.:
<input type="text" data-bind="value: vm.someValue.formattedValue"/>
As explained, the formattedValue is a new child observable which formats/parses the value.
The second question can also be solved with writable computed observables. You can add the validation logic in the write method, so that any time the value is modified, it's validated, and rejected or corrected, depending on what you want to do. The computed observable can access other values from the view model, so its implementation should be easy. Of course, the validation logic must access the observables with the actual values. I.e it can completely ignore if the observable is extended or not.
The great advantage of this implementation is that you can implement an test each required functionality independently:
implement and test the parsing/formatting extenders, the format/parse in the extenders
implementa and test the the business logic in the writable computed observables
Once implemented an tested, start using them together.
Related
Requirement: I want to update the value of a custom attribute (name: badges) (type: enum-of-strings) for a Product via code. I want to set the value "bestSeller" as selected. How should I do that update because the code below is not working?
Screenshot of the Custom Attribute in Business Manager
Code snippet:
function updateBestSeller() {
var ProductMgr = require('dw/catalog/ProductMgr');
var Site = require('dw/system/Site');
var UUIDUtils = require('dw/util/UUIDUtils');
var CustomObjectMgr = require('dw/object/CustomObjectMgr');
var currentSite = Site.getCurrent();
var bestSellerOrderUnits = Object.hasOwnProperty.call(currentSite.preferences.custom, 'bestSellerOrderUnits') ? currentSite.getCustomPreferenceValue('bestSellerOrderUnits') : 0;
try {
Transaction.wrap(function () {
var count = 1;
var products = ProductMgr.queryAllSiteProducts();sni
var HashSet = require('dw/util/HashSet');
var badges = new HashSet();
if (products.count > 0) {
while (products.hasNext() && count < 5) {
var product = products.next();
var badges = [];
badges.push('bestSeller');
if (Object.hasOwnProperty.call(product.custom, 'badges')) {
product.custom.badges = badges
}
count++;
Logger.debug('{0}',product.ID);
}
}
products.close();
});
} catch (ex) {
Logger.error(ex.toString());
return new Status(Status.ERROR, 'ERROR', 'UPDATE failed');
}
return new Status(Status.OK, 'OK', 'UPDATE successful');
}
I think what is probably happening here is that your attempt to validate that a product has a badges key in the product.custom attribute collection is failing. This prevents the update from occurring.
I suggest removing the condition around the following line:
product.custom.badges = badges;
If that line were to not execute, then the update to the database would never occur.
The way custom attributes work is that they will never exist until a value is set for that attribute for a given persistent object. (eg: Product) So checking to see if it exists via something like: 'badges' in product.custom (which is the recommended way) will often be false even when the custom attribute definition exists because many products have never had a badge value set. Once an object has had a value set to a particular custom attribute even if it is now set to null then it will exist.
Additionally, there are some other issues with the code that may be causing problems. One example is defining the badges variable twice within the same scope. Another example is sni that is placed at the end of the line where you define the products variable. This is likely causing an error in your code. Finally, it is a good practice to avoid the use of the queryAllSiteProducts method if you can. An alternative may be to use the ProductSearchModel instead; This may not always meet your need, but it is a good idea to rule it out before resorting to queryAllSiteProducts for performance reasons.
Something else to consider is that if badges currently has any selected values, you'll be overwriting those values with the code you have today. Consider setting badges initially to [] then check to see if there is a value for that product by doing:
if ('badges' in product.custom && !empty(product.custom.badges) {
badges = product.custom.badges;
}
In AgGrid, I am tyring to use aggregation functions and valuegetter on the same column. It seems that because of valuegetter, my aggreation functions are not working, I just get 0 or null value on aggregation. Could you please check my code for any possible solutions?
Thanks.
{ headerName: "Price", filter: "agNumberColumnFilter", valueGetter: priceValueGetter, allowedAggFuncs: ['avg', 'sum', 'min', 'max']};
function priceValueGetter(params) {
var value = '';
if (params.data) {
var EPrice = params.data.a;
var p1 = params.data.b;
if (EPrice && p1) {
value = (EPrice - p1).toFixed(2);
}
}
return value;
}
Your valueGetter is returning a string.
So the built-in aggregation functions, which expect numbers, are failing.
Try this valueGetter instead:
function priceValueGetter(params): number {
if (params.data && params.data.a && params.data.b) {
return params.data.a - params.data.b
}
return null;
}
This won't format the way that you want, but you can fix that by adding a valueFormatter.
One other piece of advice - given that you have a variable named 'EPrice', I'm assuming that you're dealing with money. You shouldn't depend on Javascript's native 'number' type for money, as it is essentially a 'float', and rounding errors will ensue. There are lots of articles about how to properly handle monetary values in Javascript - I personally use a Javascript port of Java's BigDecimal class, called 'big.js', but there are several other solutions.
Edit - also be careful of using if on a number - it evaluates to false if the number is zero. If that's what you want, fine, but if it isn't, adjust your logic accordingly.
I'm a backend dev moved recently onto js side. I was going through a tutorial and came across the below piece of code.
clickCreate: function(component, event, helper) {
var validExpense = component.find('expenseform').reduce(function (validSoFar, inputCmp) {
// Displays error messages for invalid fields
inputCmp.showHelpMessageIfInvalid();
return validSoFar && inputCmp.get('v.validity').valid;
}, true);
// If we pass error checking, do some real work
if(validExpense){
// Create the new expense
var newExpense = component.get("v.newExpense");
console.log("Create expense: " + JSON.stringify(newExpense));
helper.createExpense(component, newExpense);
}
}
Here I tried to understand a lot on what's happening, there is something called reduce and another thing named validSoFar. I'm unable to understand what's happening under the hood. :-(
I do get the regular loops stuff as done in Java.
Can someone please shower some light on what's happening here. I should be using this a lot in my regular work.
Thanks
The reduce function here is iterating through each input component of the expense form and incrementally mapping to a boolean. If you have say three inputs each with a true validity, the reduce function would return:
true && true where the first true is the initial value passed into reduce.
true && true and where the first true here is the result of the previous result.
true && true
At the end of the reduction, you're left with a single boolean representing the validity of the entire, where by that if just a single input component's validity is false, the entire reduction will amount to false. This is because validSoFar keeps track of the overall validity and is mutated by returning the compound of the whether the form is valid so far and the validity of the current input in iteration.
This is a reasonable equivalent:
var validExpense = true;
var inputCmps = component.find('expenseform')
for (var i = 0; i < inputCmps.length; i++) {
// Displays error messages for invalid fields
inputCmp.showHelpMessageIfInvalid();
if (!inputCmp.get('v.validity').valid) {
validExpense = false;
}
}
// Now we can use validExpense
This is a somewhat strange use of reduce, to be honest, because it does more than simply reducing a list to a single value. It also produces side effects (presumably) in the call to showHelpMessageIfInvalid().
The idea of reduce is simple. Given a list of values that you want to fold down one at a time into a single value (of the same or any other type), you supply a function that takes the current folded value and the next list value and returns a new folded value, and you supply an initial folded value, and reduce combines them by calling the function with each successive list value and the current folded value.
So, for instance,
var items = [
{name: 'foo', price: 7, quantity: 3},
{name: 'bar', price: 5, quantity: 5},
{name: 'baz', price: 19, quantity: 1}
]
const totalPrice = items.reduce(
(total, item) => total + item.price * item.quantity, // folding function
0 // initial value
); //=> 65
It does not make sense to use reduce there and have side effects in the reduce. Better use Array.prototype.filter to get all invalid expense items.
Then use Array.prototype.forEach to produce side effect(s) for each invalid item. You can then check the length of invalid expense items array to see it your input was valid:
function(component, event, helper) {
var invalidExpenses = component.find('expenseform').filter(
function(ex){
//return not valid (!valid)
return !ex.get('v.validity').valid
}
);
invalidExpenses.forEach(
//use forEach if you need a side effect for each thing
function(ex){
ex.showHelpMessageIfInvalid();
}
);
// If we pass error checking, do some real work
if(invalidExpenses.length===0){//no invalid expense items
// Create the new expense
var newExpense = component.get("v.newExpense");
console.log("Create expense: " + JSON.stringify(newExpense));
helper.createExpense(component, newExpense);
}
}
The mdn documentation for Array.prototype.reduce has a good description and examples on how to use it.
It should take an array of things and return one other thing (can be different type of thing). But you won't find any examples there where side effects are initiated in the reducer function.
I have an observable item and I am trying to apply user driven formatting on that observable. I have an integer input box where a user can select how many decimal places, 0-6. I am trying to update the observable to apply decimal places based on the selection in that input box.
I first tried a computed value, which did not work. Returned an error that 'toFixed' was not a function.
this.formattedResult = ko.computed(function () {
var newValue = self.decimalValue();
var precision = self.decimalPlaces();
return newValue.toFixed(precision);
});
I then tried adding a binding handler, which results in 'uncaught object' on the 'toFixed'
ko.bindingHandlers.numericText = {
update: function (element, valueAccessor, allBindingsAccessor) {
var value = new String(ko.utils.unwrapObservable(valueAccessor()).toString());
var precision = ko.utils.unwrapObservable(allBindingsAccessor().precision);
var formattedValue = value.toFixed(precision);
ko.bindingHandlers.text.update(element, function () { return formattedValue; });
},
defaultPrecision: 1
};
Then I tried extending numeric with a numericText, which also results in a 'uncaught object' error on the 'toFixed'.
ko.extenders.numeric = function (target, precision) {
if (precision() != null) {
var precisionValue = target();
console.log(precisionValue);
var precisionDecimal = precisionValue.toFixed(2);
console.log(precisionDecimal);
var result = ko.dependentObservable({
read: function () {
return target().toFixed(precision());
},
write: target
});
result.raw = target;
return result;
}
else
return target;
};
The binding on the HTML elements were all updated to take these changes into account but they did not work. I also tried just simply adding 'toFixed' to the data binding in the HTML and that also did not work.
I found these similar questions but the solutions are not working for me.
Format knockout observable with commas and/or decimal places within html data binding, NOT in viewmodel
Formatting rules for numbers in KnockoutJS
Adding decimal formatting to Knockout number data bindings
This is a very frustrating problem as I thought it would be really simple to add decimal places in a computed function. What am I doing wrong?
I'm not sure if it matters but I am using knockout 3.1
Edit:
I also tried just simply adding 'toFixed' to the data-binding but that also results in an uncaught object.
<p data-bind="text: decimalValue().toFixed(2)"></p>
I didn't end up solving my problem in the way that I had anticipated but I modified the computed column to force the decimal to a float and then added the decimals to that float value.
The final function looks like this:
this.formattedResult = ko.computed(function () {
var newValue = parseFloat(self.decimalValue());
var precision = self.decimalPlaces();
return newValue.toFixed(precision);
});
I don't know why this works but the other does not? Maybe when I assign the value to newValue I'm just assigning an observable to it and the observable doesn't know how to handle toFixed()? Doesn't explain all of the other problems but at least this is fixed for me.
I recommend using the following JavaScript Number constructor which will return NaN if the number is invalid.
this.formattedResult = ko.computed(function () {
// call the number constructor. newValue is now a number or NaN
//we choose a default value as well.
var newValue = isNaN(self.decimalValue()) ? 0 :
Number(self.decimalValue());
// call the number constructor again
var precision = isNaN(self.decimalPlaces()) ? 0 : Number(self.decimalPlaces());
return newValue.toFixed(precision);
});
I want to have a Backbone model with float attributes in it but without worrying too much about variable types.
I would like to encapsulate the value parsing right there in the model so I am thinking of overriding the set function:
var Place = Backbone.Model.extend({
set: function(attributes, options) {
if (!_.isEmpty(attributes.latitude)){
attributes.latitude == parseFloat(attributes.latitude);
}
if (!_.isEmpty(attributes.longitude)){
attributes.longitude == parseFloat(attributes.longitude);
}
Backbone.Model.prototype.set.call(this, attributes, options);
}
});
However this seems cumbersome, since I would have a similar logic in the validate method and potentially repeated across multiple models. I don't think the View should take care of these conversions.
So what is the best way of doing it?
Use a validation plugin for your model so that you can validate the input in a generic fashion.
There are several out there including one that I have written:
Backbone.Validator
Backbone.Validation
Then you don't worry about performing data validation anywhere else - your model does it and sends out and error message you can listen for and provide appropriate feedback.
Also, a lat/lng pair can, in rare circumstances, be an integer, such as Greenwich England: 0,0 or the north pole: 90,180. And since JavaScript only has "number" any valid input for parseFloat is also valid for parseInt.
But parseFloat will always return a float.
My solution was to replace Backbone.Model.prototype.set with a preprocessor proxy:
/**
* Intercept calls to Backbone.Model.set and preprocess attribute values.
*
* If the model has a <code>preprocess</code> property, that property will be
* used for mapping attribute names to preprocessor functions. This is useful
* for automatically converting strings to numbers, for instance.
*
* #param Backbone
* the global Backbone object.
*/
(function(Backbone) {
var originalSet = Backbone.Model.prototype.set;
_.extend(Backbone.Model.prototype, {
set: function(key, val, options) {
if(!this.preprocess) {
return originalSet.apply(this, arguments);
}
// If-else copied from Backbone source
if (typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
for(attr in this.preprocess) {
if(_.has(attrs, attr)) {
attrs[attr] = this.preprocess[attr](attrs[attr]);
}
}
return originalSet.call(this, attrs, options);
},
});
})(Backbone);
After this, models with a preprocess property will use it to map attribute names to preprocessor functions. For instance, preprocess: { age: parseInt } means that whenever the age attribute is set, the value will be passed through parseInt before actually setting it. Attributes with no corresponding preprocess entry will not be affected.
Example usage:
var Thing = Backbone.Model.extend({
preprocess: {
mass: parseInt,
created: function(s) { return new Date(s); },
},
});
var t = new Thing({
label: '42',
mass: '42',
created: '1971-02-03T12:13:14+02:00',
});
console.log(t.get('label')+3); // 423
console.log(t.get('mass')+3); // 45
console.log(t.get('created').toLocaleString('ja-JP', { weekday: 'short' })); // 水
Pros
The functionality is available in all models without needing to duplicate code
No need to send { validate: true } in every call to set
No need to duplicate preprocessing in validate, since this happens before validate is called (this might also be a con, se below)
Cons
Some duplication of Backbone code
Might break validation since preprocessing happens before validate is called. JavaScript parsing methods usually return invalid values instead of throwing exceptions, though (i.e. parseInt('foo') returns NaN), so you should be able to detect that instead.