knockoutjs, observables updating each other - javascript

I have view model with 3 fields
dateStart = ko.observable();
dateEnd = ko.observable();
days = ko.observable();
assuming startDate is selected, whenever endDate is selected days field needs to be updated (days = endDate - startDate).
Also when days field is updated i need to calculate endDate (endDate = startDate + days).
how can this be done with knockoutjs ?
Thank You!
I've tried
http://jsfiddle.net/NfG4C/6/, but my js always throws too musch recursion exception.

From what I understand, you basically need 2 things.
You want to calculate the "days" field whenever someone selects the
"endDate" [assuming they have selected the "startDate" ofcourse]
You want to recalculate the "endDate" field whenever someone changes the
"days" field
One way to solve this would be to use a "writeable" computed Observable [http://knockoutjs.com/documentation/computedObservables.html]. Please go through the link for details, but in general term, a 'writeable computed observable' is something whose value is 'computed' based on some 'other' observable(s) and vice-versa.
I took the liberty to modify your fiddle and change the "days" as a computed observable. Please take a look: http://jsfiddle.net/dJQnu/5/
this.days = ko.computed({
read: function () {
//debugger;
// here we simply need to calculate the days as => (days = endDate - startDate)
if (that.dateStart() && that.dateEnd()) {
var vacDayCounter = 0;
for (var curDate = new Date(that.dateStart()); curDate <= that.dateEnd(); curDate = curDate.addDays(1)) {
if (isDateCountsAsVacation(curDate)) {
vacDayCounter++;
}
}
//that.days(vacDayCounter);
return vacDayCounter;
}
},
write: function (newDays) {
if (newDays && !isNaN(newDays) && that.dateStart()) {
var tmpEndDate = new Date(that.dateStart())
appliedDays = 0;
while (appliedDays < newDays) {
if (isDateCountsAsVacation(tmpEndDate)) {
appliedDays++;
}
tmpEndDate = tmpEndDate.addDays(1);
}
if (tmpEndDate) {
that.dateEnd(tmpEndDate);
}
}
}
});
If you notice, I simply reused your code (logic) for the read and write part. During read, we are "computing" the value for the observable itself, in this case the "days" and during write (which fires anytime the user changes the actual "days" input value) we are recalculating the "dateEnd" field.
Please let me know if you have any other question.
Hope this helps.
Thanks.

You get a recusive problem because when updating the observables from a subscription will also trigger that observables subscribe method.
You need to add a fourth member, updatedFromSubscriber
set it to false from tbe beginning, in each subscribe method add
if(this.updatedFromSubscriber)
return;
and just before updating the observable do
this.updatedFromSubscriber = true
set it to false after updating the observable

Related

Vue JS - Loop through a computed property to display a list of years

I currently have the following code that lists a list of years. I feel that all this code may be very unnecessary and perhaps a computed property for validYears, would help me make this code more optimal and get rid of the unnecessary watchers. My issue is converting this to a computed property as I'm failing to grasp the correct logic to achieve this. I'd appreciate if someone can offer an example of how I can set a computed property for valid years and still return the same result.
onBeforeMount(calculateDateRange)
watch(() => props.earliestDate, (newValue, prevValue) => {
calculateDateRange();
});
// If there is a new value passed from the parent, the dropdown should display that new value.
watch(() => props.latestDate, (newValue, prevValue) => {
calculateDateRange()
});
const validYears = ref([])
function calculateDateRange () {
for(let year = props.latestDate; year >= props.earliestDate; year--){
validYears.value.push(year)
}
}
I didn't provide the rest of the code not to clutter the question, but as one can see in this component I have a set of props that determine the values in my for loop.
You could optimize it as follows :
const validYears = computed(()=>{
let _years=[]
for(let year = props.latestDate; year >= props.earliestDate; year--){
_years.push(year)
}
return _years;
})

How to filter last day in an array?

I have an array of objects like this:
[
{
created: "2019-08-14T13:24:36Z",
email: "test1#gmail.com"
},
{
created: "2019-08-15T13:24:36Z",
email: "test2#gmail.com"
},
{
created: "2019-08-16T13:24:36Z",
email: "test1#gmail.com"
},
{
created: "2019-08-22T13:24:36Z",
email: "test4#gmail.com"
},
{
created: "2019-08-22T15:29:66Z",
email: "test1#gmail.com"
}
]
The array is sorted by created. I want to filter those records which are on the last day, irrespective of the time on that day. I added the timestamp using moment.js. Something on these lines:
router.get('/GetLastDayRecords', (req, res) => {
res.json(allRecords.filter(record => record.created.max()));
});
Split the task: first get the maximum date which you'll find at the end of the sorted array (just getting the "YYYY-MM-DD" part of it is enough) and then launch the filter:
let max = allRecords.length ? allRecords[allRecords.length-1].created.slice(0,10) : "";
res.json(allRecords.filter(({created}) => created >= max));
First you need to figure out which day is the last day. If you can assume the records are already sorted, then this is pretty simple:
// Assuming your records are stored in the variable "records"
var lastDay = records[records.length - 1].created;
Now here's where your specific answer may differ based on how you want to handle time zones. Suppose one event happened at 11 PM EST (3 AM GMT) and another event happened at 1 AM EST (5 AM GMT). Are these the same day? In Europe they are, but in America they aren't!
What you need to do is create some cipher from the date+time listed to a "day". This way you can compare two "days" to see if they're the same:
lastDay = new Date(lastDay);
// Setting hours, minutes, and seconds to 0 will give you just the "day" without the time, but by default will use the system timezone
lastDay.setHours(0);
lastDay.setMinutes(0);
lastDay.setSeconds(0);
Once you know which day was the last, it's a simple filter:
// Using a for loop
var results = []
for (var i = 0; i < records.length; i++)
{
if (records[i].created > lastDay) {
results.push(records[i]);
}
}
// Using .filter
var results = records.filter(x => x.created > lastDay);
Alternatively, since we know it's already sorted, we can do it a bit more efficiently by binary searching for the first record on the last day, then grabbing all records after that:
var test = records.length / 2;
var step = records.length / 4;
var found = false;
while (!found) {
if (records[test].created < lastDay) {
test += step;
step /= 2;
}
else if (records[test].created > lastDay) {
if (step == 1) {
// We found the exact cut-off
found = true;
}
else {
test -= step;
step /= 2;
}
}
}
var results = records.slice(test);
Because you're only interested in the "last" day, the logic is a bit simpler. If you wanted the "third" day, you would need to check if created was after the start of the third day and before the end of the third day. We can just check if it's after the start of the last day.
I would create a function to turn your created properties into data be easily compared.
I would also avoid trying to do the entire filter operation in one or two lines as it will difficult to read by other developers.
const dateToInt = date => parseInt( date.split('T').shift().replace(/-/g, '') );
The above will:
Split your created property into an array of date and time.
Select the first element, which happens to be the date.
Remove the dashes in the date.
Coerce the value into a number.
With this you can find the maximum value and filter based on that value.
const nums = foo.map( ({ created }) => dateToInt(created) )
First get a list of numbers from the dataset.
const max = Math.max( ...nums )
Get the biggest number in the list.
const lastDays = foo.filter( ({ created }) => dateToInt(created) === max )
With all that setup, getting the max date is very easy and readable.
Of course, since the list is already sorted. You could have just done this as well.
const last = foo[foo.length -1].created;
const lastDays = foo.filter( ({ created }) => created === last )
I wrote a solution using reduce and filter:
const lastDay = arr.reduce((acc, el) => {
const date = el.created.substr(0,10);
const oldDate = new Date(acc);
const nextDate = new Date(date);
if(oldDate.getTime() > nextDate.getTime()) {
return oldDate;
} else {
return nextDate;
}
}, '1900-01-01');
const lastDayArr = arr.filter(el => {
const date = el.created.substr(0,10);
const oldDate = new Date(lastDay);
const nextDate = new Date(date);
return (oldDate.getTime() === nextDate.getTime());
});
First, you find the most recent date, reducing the original array by comparing which date is the most recent, for this you drop the part of the created string that specifies the hours/minutes/seconds.
You can use a very distant in time date as initial value, or you can set it to null and add another validation in your callback function.
As a second step, you use filter, using the same technique of dropping the hours/minutes/seconds of the created string.
The end result is an array of the elements with the most recent date in your original array.
If you can assume the array is sorted, you can skip the reduce method and just do:
const lastDay = arr[arr.length - 1].created.substr(0,10);
This should work:
allRecords.filter( record => {
let last_date = allRecords[ allRecords.length - 1].created
return last_date.slice(0, 10) === record.created.slice(0, 10)
})
Basically, you are getting the last element from your array and slicing its created value down to its date. Then you are slicing your current record's created value down to its date and comparing if they are the same.
Assuming that the array is already ASC ordered:
const onLastDay = values.filter( v => {
const last = moment(values[ values.length - 1 ].created)
const differenceInDays = last.diff(moment(v.created), 'days')
return differenceInDays < 1
})
console.log(onLastDay)
NOTE: If you try with the reported array you get an error due the fact that the last date is not valid! There are 66 seconds!

Vue infinite loop ... in method

I am getting this warning [Vue warn]: You may have an infinite update loop in a component render function. and I think I know why it's happening.
My Use Case :
I have a loop where I am passing an object with index to a vue method. Since computed cannot have arguments so I cannot use it. The remaining option is method.
I am trying to increase the date by one week after the loop reaches to a odd number. Only 1 and 2 is an exception.
Here is my code I have been trying to use
getDate(date, i){
var currentDate=date.startingDate
var res=currentDate
if(i>2){
// change the date
if(i%2==0){
//use previous dates
res=date.startingDate
}else{
// increase the date by one week from the last week
var currDate = new Date(currentDate)
var d=currDate.setDate(currDate.getDate() + 7);
var d=new Date(d)
var newDate=d.toISOString()
var dateArr=newDate.split("T")
var res=dateArr[0]
console.log('ok')
date.startingDate=res // this line is the problem and causes the infinite loop problem.
}
}
return res
}
Here date.startingDate is always a fixed date.
Sample Input
date.startingDate='2018-05-11'
so when i=1, and 2 (i always starts from 1)
out put is date.startingDate='2018-05-11'
when i=3,4 date should increase by one week so expected out put is 2018-05-17
The problem is date.startingDate=res I cannot reset the date to this one. But I have to reset it to the increased new date in order to be able to add new date when i=5,6 or bla bla.
Any suggested solutions in other ways or may be this code can be made better in a different ways? Any help would be highly appreciated.
Thank you so much for the time.
Edit
<div class="row _6-2-1__Tou align-items-center team" v-for="(m,j) in data.row" :key="j">
<div class="col-auto _6-2-1__Tou-one pad-0" >
<ul class="_6-2-1__Tou-text">
<li>{{getDate(m.date,j+1)}}</li>
</ul>
</div>
Ok it seems impossible to avoid this problem. So I changed my approach. I didn't update the date while populating the doom, instead, I used vuex store and getters. When returning from getters, I changed the values there and once all date changes are done, I simply returned the array. It worked nicely. Here is my code
This code is a bit changed in terms of logic as I added dynamic conditions. But concept is same.
fixture(state){
// we can actually update here
for(var i in state.fixture){
var details=state.fixture[i].row
for(var j in details){
//console.log(details[j].date.startingDate)
if(state.counter==state.groupCount){
// increase the date
if(state.date){
var currDate = new Date(state.date)
}else{
var currDate = new Date(details[j].date.startingDate)
}
var d=currDate.setDate(currDate.getDate() + 7);
var d=new Date(d)
var newDate=d.toISOString()
var dateArr=newDate.split("T")
var res=dateArr[0]
details[j].date.startingDate=res
state.date=details[j].date.startingDate
state.counter=1
}else{
if(state.date){
details[j].date.startingDate=state.date
}
state.counter++
}
}
state.counter=0
state.date=''
}
return state.fixture
},
Hope this approach helps others.

Console.log this WIx Code function

How do i log this successfully please. When i print, it prints only the syntax and not the values. I would also want to display it as text.
$w("#datePicker1").onChange( (onChange, $w) => {
let chosenDate = new Date($w("#datePicker1").value);
let date1 =chosenDate.getDate();
return date1;
});
$w("#datePicker2").onChange( (onChange, $w) => {
let chosenDate = new Date($w("#datePicker2").value);
let date2 = chosenDate.getDate();
return date2;
});
//printing everthing instead of values
console.log($w("#datePicker1").onChange);
console.log($w("#datePicker2").onChange);
this is because you're printing the function's actual code, as described in Function.prototype.toString().
$('#some_input').onChange function is an event handler and returning the object of the triggered element (see WixCode DatePicker API Docs)
You didn't mentioned what you wish to do with the date value, so I'm guessing that you may be wanted to put it in your database to update a specific item (the current item the dataset points to). Assuming this is what you're trying to do, here is a code to help:
$w("#datePicker1").onChange((event, $w) => {
let date = event.target.value;
// maybe do some manipulation here on the saved value
$('#dataset1').setFieldValue('last_modified_date', date)
});
Hope this could help,
Cheers!

Using dijit.form.DateTextBox Not able to retain proper Time value

I am using dijit.form.DateTextBox to update my date field.
<form:input id="date_id" name="date_field" path="createDate"
dojoType="dijit.form.DateTextBox"
disabled="false" constraints="{datePattern:'dd/MM/yyyy hh:mm:ss.SS'}"
invalidMessage="invalid" promptMessage="invalid"
lang="en-us" required="true"/>
now, suppose If my 'createDate' value is '05/01/2012 21:10:17.287', but it is displaying as '05/01/2012 12:00:00.00' in date text box.
Due to which, while editing this field, I'm not able to keep it as it was.
Is there anyway I can retain that time part '21:10:17.287'.
Kindly suggest.
(This solution will work for above Dojo 1.6 versions )
The default DateTextBox overrides the old value when setting a new one. This means that the time context is lost while setting the value. If you want to make this possible, you will have to extend the default behavior of the _setValueAttr function of DateTextBox since this is the setter of the value field.
This is how you could do this:
declare("custom.DateTextBox", [DateTextBox], {
_setValueAttr: function(value, priorityChange, formattedValue) {
if(value !== undefined){
if(typeof value == "string"){
value = stamp.fromISOString(value);
}
if(this._isInvalidDate(value)){
value = null;
}
if(value instanceof Date && !(this.dateClassObj instanceof Date)){
value = new this.dateClassObj(value);
}
}
if (value != null && this.value != null) {
value.setHours(this.value.getHours());
value.setMinutes(this.value.getMinutes());
value.setSeconds(this.value.getSeconds());
value.setMilliseconds(this.value.getMilliseconds());
}
this.value = value;
this.inherited(arguments);
}
});
What happens here is pretty easy, the first thing I do is parsing the new value to a valid Date. Before replacing the original value I'm copying the time fields (hours, minutes, seconds and milliseconds).
I also made a JSFiddle to demonstrate this.

Categories

Resources