KnockoutJS throttle input - input

I'm trying to implement something like a typesafe ViewModel using KnockoutJS. It works pretty well until I start to update observables via HTML input tags.
I have implemented type extender which returns computed observable:
return ko.computed({
read: target,
write: fixer
})
Where fixer is something like:
function (newValue) {
var current = target(),
valueToWrite = (newValue == null ? null : fixNumber(newValue, 0));
if (valueToWrite !== current) target(valueToWrite);
else if (newValue !== current) target.notifySubscribers(valueToWrite);
}
And fixNumber is
function fixNumber(value, precision) {
if (value == null || value === '') return null;
var newValue = (value || '').toString().replace(/^[^\,\.\d\-]*([\.\,\-]?\d*)([\,\.]?\d*).*$/, '$1$2').replace(/\,/, '.'),
valueToWrite = Number(newValue);
return !!(valueToWrite % 1) ? round(valueToWrite, precision) : valueToWrite;
}
It looks not so straightforward, but I have to consider possible use of comma as a decimal separator.
Often I need to update my observables as soon as user presses key to reflect this change immediately:
<input type="text" data-bind="value: nonThrottled, valueUpdate: 'afterkeyup'"></input>
And here comes a lot of problems, because, for example, I can't input decimal values less than 1 (0.1, 0.2, etc) there.
When I try to throttle an observable it mostly works. But sometimes user input and type fixer go out of sync, so it looks like some input gets lost occasionally.
Full example is there http://jsfiddle.net/mailgpa/JHztW/. I would really appreciate any hints, since I have spent days trying to fix these problems.
UPDATE 11/04/2013
I solved my problem providing custom value binding, so now throttled observables doesn't eat my input occasionally.
I've added additional valueThrottle option-binding to throttle updating of element's value:
var valueThrottle = allBindingsAccessor()["valueThrottle"];
var valueThrottleTimeoutInstance = null;
/* ... */
if (valueThrottle) {
clearTimeout(valueThrottleTimeoutInstance);
valueThrottleTimeoutInstance = setTimeout(function () {
ko.selectExtensions.writeValue( element, ko.utils.unwrapObservable(valueAccessor()) );
}, valueThrottle);
} else applyValueAction();
Also I've noticed that inability to enter values like 0.2 in my case comes from that statement in original value binding:
if ((newValue === 0) && (elementValue !== 0) && (elementValue !== "0"))
valueHasChanged = true;
I've rewritten it as
if ((newValue === 0) && (elementValue != 0))
valueHasChanged = true;
It works at least at Chrome, but I haven't tested it properly and even not sure that it's correct.
Example is to be added, for some reason jsFiddle does not accept my custom binding.
Any comments are really appreciated.

Related

How to format money on Shopify using JavaScript

I found this Gist for money formatting on Shopify using JavaScript https://gist.github.com/stewartknapman/8d8733ea58d2314c373e94114472d44c
I placed it in my cart page and when I try:
Shopify.formatMoney(2000, '$')
I get this:
cart:2166 Uncaught TypeError: Cannot read property '1' of null
at Object.Shopify.formatMoney (cart:2166)
at <anonymous>:1:9
this is at switch(formatString.match(placeholderRegex)[1]) {
I expect to get $20.00
Do you know where the problem is?
Similar question: Shopify Buy Button Error: "cannot read property '1' of null"
The Gist content
var Shopify = Shopify || {};
// ---------------------------------------------------------------------------
// Money format handler
// ---------------------------------------------------------------------------
Shopify.money_format = "${{amount}}";
Shopify.formatMoney = function(cents, format) {
if (typeof cents == 'string') { cents = cents.replace('.',''); }
var value = '';
var placeholderRegex = /\{\{\s*(\w+)\s*\}\}/;
var formatString = (format || this.money_format);
function defaultOption(opt, def) {
return (typeof opt == 'undefined' ? def : opt);
}
function formatWithDelimiters(number, precision, thousands, decimal) {
precision = defaultOption(precision, 2);
thousands = defaultOption(thousands, ',');
decimal = defaultOption(decimal, '.');
if (isNaN(number) || number == null) { return 0; }
number = (number/100.0).toFixed(precision);
var parts = number.split('.'),
dollars = parts[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1' + thousands),
cents = parts[1] ? (decimal + parts[1]) : '';
return dollars + cents;
}
switch(formatString.match(placeholderRegex)[1]) {
case 'amount':
value = formatWithDelimiters(cents, 2);
break;
case 'amount_no_decimals':
value = formatWithDelimiters(cents, 0);
break;
case 'amount_with_comma_separator':
value = formatWithDelimiters(cents, 2, '.', ',');
break;
case 'amount_no_decimals_with_comma_separator':
value = formatWithDelimiters(cents, 0, '.', ',');
break;
}
return formatString.replace(placeholderRegex, value);
};
I found formatMoney function in my theme and I was able to call it and it worked.
pipelineVendor.themeCurrency.formatMoney(2000);
result: $20.00
It's probably too late for the original asker, but for anyone else looking for an answer, I believe the correct call for this function is:
Shopify.formatMoney(2000, '${{amount}}')
instead of
Shopify.formatMoney(2000, '$')
based on the comment of the gist author on Github:
It doesn’t actually care what the currency is. The first argument is
the unformatted amount. The second argument is the format, which could
be whatever you need for the currently displayed currency i.e. “£{{
amount }}”. Then pass it a new amount and new format when you change
the currency.

Correct search statement when applying filters

I have created a search function for my React-native app, however, I am not quite sure how to create the correct search statement. What I have now is as follows. You can open a filter screen where you can type a few search criteria (for vehicles) so make, model, color, license plate.
After saving the filters you are re-directed to a result page. On this page, I populate a const with Redux data (the vehicle database) and then filter this data before showing it in flatlist.
const vehicles = useSelector(state => state.uploadedVehicles.vehicles)
const filters = props.navigation.getParam('savedFilters')
const filteredVehicles = vehicles.filter(vehicle =>
vehicle.make === filters.makeFilter ||
vehicle.model === filters.modelFilter ||
vehicle.color === filters.licenseplateFilter ||
vehicle
.licenseplate === filters.licenseplateFilter
)
...return Flatlist with filteredVehicles here...
If I set a filter for a particular Make, only vehicles from this make are found. If I set a filter for a model, only vehicles from this model are found. However, if I set a filter for two statements it now shows vehicles with one matching search criteria. If I would search for a blue Dodge I would find vehicles matching the make Dodge, but also every blue vehicle that is uploaded.
How can I expand my search function so It will show vehicles matching 1 filter, but if 2 or more filters are added it will combine these filters to a more specific search function?
I like to take another approach to this, also using Redux. Here I show you an example code:
case SAVE_FILTERS: {
const appliedFilters = state.filters; // Here you have your filters that have to be
initialized in your state. They also have to be turned to true or false but you can
do it in another function (See next one)
// Here we check every condition
const updatedData = state.data.filter(data => {
if (appliedFilters.filter1 && !data.filter1) {
return false;
}
if (appliedFilters.filter2 && !data.filter2) {
return false;
}
if (appliedFilters.filter3 && !data.filter3) {
return false;
} else {
return true;
}
});
// and now you will return an updated array of data only for the applied filters
return {
...state,
displayData: updatedLocations,
};
The trick is to check every single condition inside of the action. In this particular case, we are checking by filters that are true or not, but you can expand to other conditionals.
The flow for the code above is:
If we have a filter applied AND the data HAS that filter, then we pass the conditional. WE do this for all conditionals. If we pass all of them, return true which means it will be added to the display data and the user will see it.
I hope it helps.
Maybe this isn't the most beautiful way of getting the filter to work. But I didn't quite get my filter working with MIPB his response bit it did push me in the right direction.
I am passing the filters in appliedFilters. Then I constantly checking every filter with the part of the vehicle that is filtered for.
Starting with the make of the vehicle. If the make filter is nog set (so "") I just return the vehicles array which contains every vehicle, else I return every vehicle that is matched with the appliedFilters.makeFilters.
This new makeFilterArray is checked with the modelFilter. If this is not set just set it to the makeFilter array to continue checking other filters, if it is set check the makeFilterArray for the matching model.
Maybe not the best/most elegant solution, but with my limited knowledge I got it working! :-)
case FILTER_VEHICLES:
const appliedFilters = action.setFilters;
console.log(appliedFilters)
console.log(appliedFilters.makeFilter)
console.log(appliedFilters.modelFilter)
console.log(appliedFilters.licenseplateFilter)
console.log(appliedFilters.colorFilter)
const makeFilterArray = appliedFilters.makeFilter === "" ? vehicles : state.vehicles.filter(vehicle => vehicle.make === appliedFilters.makeFilter)
const modelFilterArray = appliedFilters.modelFilter === "" ? makeFilterArray : makeFilterArray.filter(vehicle => vehicle.model === appliedFilters.modelFilter)
const licenseplateFilterArray = appliedFilters.licenseplateFilter === "" ? modelFilterArray : modelFilterArray.filter(vehicle => vehicle.licenplate === appliedFilters.licenseplate)
const filteredArray = appliedFilters.colorFilter === "" ? licenseplateFilterArray : licenseplateFilterArray.filter(vehicle => vehicle.color === appliedFilters.color)
// and now you will return an updated array of data only for the applied filters
return {
...state,
filteredVehicles: filteredArray
};

Vue.js: error setting a computed property

in the following code (jsbin available here) I have two input elements, a range and a text, bound together via a computed property.
var vm = new Vue({
el: '#main-container',
data: {
sliderValue: 100,
},
computed: {
actualValue: {
get: function() {
if (this.sliderValue <= 100) {
return this.sliderValue;
} else {
return Math.round(this.sliderValue * 12.5 - 1150);
}
},
/* set won't work for val > 100*/
set: function(val) {
if (val <= 100) {
this.sliderValue = val;
} else {
this.sliderValue = Math.round((val + 1150)/12.5);
}
}
}
},
methods: {
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
<div id="main-container">
<input type="range" v-model="sliderValue" min=1 max=132>
<input type="text" v-model="actualValue">
<p>Slider: {{sliderValue}}</p>
<p>Actual: {{actualValue}}</p>
</div>
The range goes from 1 to 132, and its range is mapped [1..500] in the text input, with a simple transformation (basically it's a linear mapping with two different slopes for [1..100] and [101..132]) using the actualValue computed property.
Getting actualValue works as expected: dragging the slider correctly updates the input text with appropriate values in the range [1..500].
I'm not able to find a way to set actualValue, though. I'd like to be able to type a value in the text input, and make the slider's thumb update accordingly to the inverse transformation (val + 1150)/12.5.
It works as long as the typed number is in the range [1..100], but it "explodes" for numbers >100, e.g. 101 makes the sliderValue jump at 80892 and actualValue is then re-calculated as 1010000. As far as I understand, it's a looping-feedback scenario.
I've tried also alternative approaches (watch; v-on:change in the text input; using a third variable) to no avail.
Thanks in advance for any suggestion!
It's an amazing puzzle, and challenged me for a long time!
Look at the screenshot below. Your sliderValue and actualValue are strings, not integers. When you set actualValue as 101, you are actually setting it as a string value of "101"
Now, your sliderValue = ((actualValue + 1150)/12.5)
"101" + 1150 = "1011150" (another string!, try it in the developer console)
That messes up your entire calculation. Now you know how to fix it :-)
And you need to get that Vue devtools from here: https://github.com/vuejs/vue-devtools
EDIT: Response to comment #3
Here is the modified jsBin: http://jsbin.com/mahejozeye/1/edit?html,js,output
The only difference is introduction of two console.log statements in your map2 function. This helps you identify if your non-linear mapping function is working correctly or not. If you keep your developer console open, you will see what is happening in this function.
Case 1: When you set the value radius = 25 using the text box, your sliderRadius gets set to 111.55518394648828 and your radius again gets re-calculated as 25. So it comes around in a full circle and everything is stable here.
Case 2: When you set the value radius = 55, your sliderRadius gets set to 173.03607214428857 through your non-linear map2() function, which resets radius to 51.29869180420927
Clearly there is a circular dependency issue. Your sliderRadius and radius are very much dependent on each other and therefore radius is unable to take the value between 51 and 58.
You need to evaluate why it happens, as it has a lot to do with the non-linear mapping function that you have defined. The moment radius can take stable values at 55 (through the map2 function), then your current problem will be resolved.
The simplest fix is to set your input type to number:
<input type="number" v-model="actualValue">
or you can convert your value to an integer with something like:
set: function(val) {
var intVal = parseInt(val, 10);
if (!isNaN(intVal)) {
if (intVal <= 100) {
this.sliderValue = Math.max(1, intVal);
} else {
this.sliderValue = Math.min(132, Math.round((intVal + 1150) / 12.5));
}
}
}

Validation .passes if null/empty/length == 0 doesn't fire

Validation doesn't seem to be called when the property has no value.
This is the code I'm using to try and make it work:
.ensure('baseContent.SetNamePrint').passes((name) =>
{
var webNameLength = this.baseContent.SetNameWeb.length;
var printNameLength = name.length;
console.log(webNameLength);
console.log(printNameLength);
if ((webNameLength > 1 && webNameLength < 51) || (printNameLength > 1 && printNameLength < 51)) {
return true;
}
return false;
}).withMessage('Web Name or Print Name is Required')
The passes only fires when the value of the property changes to something with a length, when it's empty (a blank string) nothing happens.
What I need is for the .passes() to be called every time there is a change to the value not just when there is a change and it has a value.
You need to additionally constraint the target property with isNotEmpty.
.ensure('baseContent.SetNamePrint').isNotEmpty().passes(...
From the documentation:
The isNotEmpty rule is always checked first before any other
validation rule. This means that without the isNotEmpty rule, the
.hasMinLength(5) rule would still consider a value of '' as valid
because the field is allowed to be empty.
PS: I heard that aurelia-validation is under rewriting. Perhaps that's why I can't find the documentation from the master branch anymore, but in another branch here
To get what I wanted I ended up with the following code.
this.validator = this.validation.on(this)
.ensure('SetNameWeb', (config) => {config.computedFrom(['SetNamePrint', 'SetNameWeb'])})
.if(() => { return this.HasImageEitherPrintNameOrWebName === false })
.isNotEmpty().withMessage('or "Print Name" is required')
.hasLengthBetween(0, 50)
.endIf()
.ensure('SetNamePrint', (config) => {config.computedFrom(['SetNameWeb', 'SetNamePrint'])})
.if(() => { return this.HasImageEitherPrintNameOrWebName === false })
.isNotEmpty().withMessage('or "Web Name" is required')
.hasLengthBetween(0, 50)
.endIf()
This gave me the functionality I needed with both of the fields being updated with each other.
However there was a bug in the aurelia code with the way it handles computedFrom that needed fixing to get this to work.
This problem was with aurelia-validation#0.6.8 though and there is now a new version which works in a completely different way so my recommendation is to update if you're having this problem.

How to do an "or" in chai should

How do I do an or test with chai.should?
e.g. something like
total.should.equal(4).or.equal(5)
or
total.should.equal.any(4,5)
What's the right syntax? I couldn't find anything in the documentation.
Asserts that the target is a member of the given array list. However, it’s often best to assert that the target is equal to its expected value.
expect(1).to.be.oneOf([1, 2, 3]);
expect(1).to.not.be.oneOf([2, 3, 4]);
https://www.chaijs.com/api/bdd/#method_oneof
Viewing the Chai expect / should documentation, there are several ways to do this test.
Note that you can chain using "and" but apparently not "or" - wish they had this functionality.
Check whether an object passes a truth test:
.satisfy(method)
#param{ Function }matcher
#param{ String }message_optional_
Asserts that the target passes a given truth test.
Example:
expect(1).to.satisfy(function(num) { return num > 0; });
In your case, to test an "or" condition:
yourVariable.should.satisfy(function (num) {
if ((num === 4) || (num === 5)) {
return true;
} else {
return false;
}
});
Check whether a number is within a range:
.within(start, finish)
#param{ Number }startlowerbound inclusive
#param{ Number }finishupperbound inclusive
#param{ String }message_optional_
Asserts that the target is within a range.
Example:
expect(7).to.be.within(5,10);
I have a similar problem to write tests to postman. I solved using the following script:
// delete all products, need token with admin role to complete this operation
pm.test("response is ok and should delete all products", function() {
pm.expect(pm.response.code).to.satisfy((status) => status === 204 || status === 404);
});
Here, I shared exactly what you need to check.
expect(true).to.satisfy(() => {
if (total == 4 || total == 5) return true;
else return false;
});
Because chai assertions throw error you could use try/catch construction:
try {
total.should.equal(4)
} catch (e) {
total.should.equal(5)
}
example of more difficult case:
try {
expect(result).to.have.nested.property('data.' + options.path, null)
} catch (e) {
expect(result).to.have.property('data', null)
}