Reputation: 6419
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?
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.
Upvotes: 0
Views: 94
Reputation: 39004
There are two different problems
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:
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:
Once implemented an tested, start using them together.
Upvotes: 1