1

could you advice why javascript / vuejs is crashing on my JSFiddle when I focus on a start date datepicker (it hangs the browser). Try to you uncomment endDate computed property and comment current one it works ok however the functionality is gone.

endDate: {
      get() {
        return moment(this.startDate).add(this.interval * this.periods, 'days')
      },
      set(value) {
        this.interval = (moment(value).diff(this.startDate, 'days')+1) / this.periods
      }
    },

original post re functionality I tried to implement

dascorp
  • 183
  • 7
  • 15
  • Because there is a circular dependency. Setting `this.endDate` triggers a recalculation of `this.interval`, which triggers a recalculation of `this.endDate`, which triggers a recalculation of `this.interval`, which triggers a recalculation of `this.endDate`, which triggers a recalculation of `this.interval`, which... – acdcjunior Mar 04 '18 at 14:37
  • I don't think that's true, if you set this.interval = 8, or any number, the application still works. I really really really think its the way moment is being used. Moment is most likely swallowing the value that's being passed, and it's not leaving, and it's triggering vue from some internal mechanism because it's wrapped in a computed property. – sksallaj Mar 04 '18 at 16:59
  • @ acdcjunior app is fully functional until I amend startDate the endDate works no problem – dascorp Mar 04 '18 at 17:23
  • @sksallaj Circular dependency: https://jsfiddle.net/ghLmnm2c/204/ open the console and then focus the input. – acdcjunior Mar 04 '18 at 18:20
  • @acdcjunior, I been testing this, and there are very weird results in the code on what works and what doesn't work. The plugins he is not handling things as expected. It's not a circular reference. Updating this.interval does not trigger a recalc of this.endDate. I updated this.interval in some of my tests, and there was no trigger. – sksallaj Mar 04 '18 at 18:23
  • if you are not convinced, open the original source, and remove the divisor where the formula is dividing by this.periods, remove that and run it. – sksallaj Mar 04 '18 at 18:23
  • @sksallaj I'm sorry, you may be right, but I can't reproduce. Like this: https://jsfiddle.net/acdcjunior/ghLmnm2c/208/ ? I commented the update of `this.interval` and the cycle stops. -- From these tests, updating interval triggers recalc of `this.endDate`: https://jsfiddle.net/ghLmnm2c/210/ (click the button I created) which makes sense, since `this.interval` is part of `this.endDate()` code. – acdcjunior Mar 04 '18 at 18:35
  • https://jsfiddle.net/ghLmnm2c/212/ try that, I removed the divisor and it worked, if you add it back in, it fails. Check out my analysis in the posted answer down below, and you'll see how the craziness unfolded for me. – sksallaj Mar 04 '18 at 18:52
  • i have added `else` statement to setter suggested by @sksallaj https://jsfiddle.net/ghLmnm2c/217/ it seems to work now, have a look – dascorp Mar 04 '18 at 21:56
  • false alarm - it breaks again, try to use jsfiddle.net/ghLmnm2c/217 then input 60 periods and then change back to say 12 it hangs but after couple of seconds comes back so there is deffinately some heavy lifting there but i am not skilled enough or dont know the tools / ways to debug it – dascorp Mar 04 '18 at 22:04
  • Yeh I would definitely have to rework the way you save the endDate and calling a computed property, I really think the problem sits on the plugin that requires moment js, but I can't be too sure. Your v-model is reactive, and your computed property gets updated when there's a value change, the rule of thumb is to bind your v-model to reactive data. Here's an example of using a reactive variable as a proxy: https://www.codeday.top/2017/10/11/47383.html – sksallaj Mar 05 '18 at 02:41
  • Look at my updated answer to see how that's done. I probably could have save a lot of time with my analysis if I did it the way I said it should have been done in the very beginning! But it the reactive proxy variable in data() seemed to do the trick. – sksallaj Mar 05 '18 at 02:52
  • I think in 227 we lost objective functionality to have start, end dates and interval being reactive to changes. 227 change in enddate does not change interval, 217 has it but crashes – dascorp Mar 05 '18 at 22:22
  • @sksallaj i still struggle with this task, i created new [jsfiddle](https://jsfiddle.net/dascorp/863tpnxo/) without datepicker and moment you can see there the circular dependency and ticks when you edit endDate rounding forces it to recalculate endDate again and again - this may trigger the crash – dascorp Mar 09 '18 at 16:21
  • @dascorp I posted a new answer below, apparently you were missing v-once to prevent any rerenders the datepicker was making. I remember running into this before, and when I had v-once tagged on, it wouldn't render anymore after. But in your case, it's fine to add. I explained why in the post. – sksallaj Mar 09 '18 at 18:59
  • great stuff @sksallaj. ill test it now. – dascorp Mar 09 '18 at 22:19
  • @dascorp thanks! Lemme know if there is any more tweaks that is needed.. If it goes beyond the scope you can open a new issue.. Otherwise you can mark the answer below :) – sksallaj Mar 10 '18 at 03:28

3 Answers3

1

Edit: This solution loses the reactivity of the endDate, but this is proof that the plugin renders needlessly more than it needs to, and because there is an infinite amount of dates, it will render for all of the dates for the datepicker. For the correct answer, see the new post regarding removal of the computed properties.


I looked again at the original code you posted, and I added a fix. I don't know why I didn't think of this earlier.

Please look at this jsfiddle: https://jsfiddle.net/ghLmnm2c/301/

In order to prevent any new re-renders on your code, you can add v-once on top of the plugin directives you are using.

  <date-picker v-model="startDate" :config="config" v-once></date-picker>
  <date-picker v-model="endDate" :config="config" v-once></date-picker>

Notice how I place the v-once directive. So there really wasn't anything wrong with your code, it was just vue's rerendering of the datepicker, which you don't want to keep on rendering, as the datepicker has its own rendering functionality.

https://v2.vuejs.org/v2/api/#v-once

Also, I noticed a helpful thing you could be using as well, if you look at the reference below, you can scroll down to the events section. The plugin you are using could emit change, update, and hide:

https://github.com/ankurk91/vue-bootstrap-datetimepicker

So you could do stuff like this:

<date-picker v-model="date" @dp-hide="doSomethingOnHide" @dp-change="doSomethingOnChange"></date-picker>
tony19
  • 125,647
  • 18
  • 229
  • 307
sksallaj
  • 3,872
  • 3
  • 37
  • 58
  • the update caused we lost reactiveness on endDate selection of different date. – dascorp Mar 12 '18 at 17:28
  • yeh I think that's the downside of this, you lose the reactivity, I suspect it really does have to do with the datepicker, there's something in it causing needless rerenders. The v-once is proof that rendering once makes your code works, taking it off means there is an iteration of renders. I don't think it's cyclic, I think it's infinite, as there are infinite days to loop through. You might wanna ask the developers of the plugin to help mediate this problem. In my solution, both datepickers update the computed property of enddate.. which is pretty close to what you want. – sksallaj Mar 14 '18 at 00:44
  • I posted a new strategy, I removed computed properties and just rebuilt your functionality using the plugin's event emitters. – sksallaj Mar 14 '18 at 11:53
  • i noticed rev 396 on jsfiddle is the most recent could you confirm its the right one? I am posting it on Datepicker github however i made another approach and removed datepicker all together and the issue is still there so i suspect it could be momentjs or vue reactive angine itself https://jsfiddle.net/863tpnxo/ – dascorp Mar 14 '18 at 13:26
  • You can see reactivnes works there, it's calc issue probably, not reactivnes if you put 950000 period it will be stuck for a period of time but then unfreezes, i suspect datepicker and momentjs now :) – dascorp Mar 14 '18 at 13:36
  • I will be posting a solution soon, there's only one problem that needs to be fixed. Its the way the interval is being updated by the startdate. This problem occurs in your original solution, but you were not addressing it previously. My solution basically uncovers it, so you have to find a way to fix it. – sksallaj Mar 14 '18 at 16:26
  • answer is posted. This is the final solution as it fixes the reasoning of why your browser crashes. As stated previously, it uncovers a new problem out of scope to your question here. You'd have to create a new stackoverflow question to address it. You have two formulas that are dependent on each other when you select the startdate date picker. It is this reason why so many problems were occurring. – sksallaj Mar 14 '18 at 16:33
  • Actually forget about making a new question, I fixed it by creating flags.This prevents startdate and interval from updating the interval. Please review the latest changes and the new fiddle, and lemme know if that works for you. – sksallaj Mar 15 '18 at 00:52
0

Analysis This is not the answer to the posted question. It is to show the guideline of finding the root cause of why the browser crashes, along with possible solutions on how to fix it.

https://jsfiddle.net/ghLmnm2c/227/

I followed the strategy I mentioned in my 4th conclusion (mentioned in the old answer section below). I created a reactive proxy variable called endDateProxy, and simply set the endDate to that endDateProxy, and I update that value in getter. I still believe my other conclusions would have helped as well. But this answer gets you what you're looking for.

<date-picker v-model="endDateProxy" :config="config"></date-picker>

...

data: {
    .... // other stuff

   endDateProxy: ''   //proxy reactive variable
},

....

get() {
   //set the reactive endDateProxy property
    this.endDateProxy = moment(this.startDate).add(this.interval * this.periods, 'days');
    return this.endDateProxy
  }

Old Answer


It's actually a really weird situation you are in. I really did the best I could and it was just gnawing at me. But luckily I came up with possible solutions that could help solve your problem or point you in the right direction.

Conclusions

  1. I came to is you have to give up using the VueBootstrapDatetimePicker, and create your own wrapper component of Moment js into vue js. Here's an example of how they do it with select2: https://v2.vuejs.org/v2/examples/select2.html

  2. Or give up on momentjs all together and use javascript's old fashion Date object.

  3. Or you can keep your code, and wrap your setter with Math.random( ), but not sure if this would fix your problem. I found out that worked in the analysis below.

  4. My final conclusion is perhaps you shouldn't use computed property tied into the v-model of the plugin. You will have to create a reactive data variable called "endDate" like you do for startdate, tie the v-model to that variable, then explicitly create a setEndDate vue method, and add event listeners to all controls that directly sets the data for endDate. So for example, you can post "onchange" or "onfocus" event on the startdate control, and tie that to the setEndDate method, and likewise for period and interval controls.

Analysis:

My attempt at a complete analysis, with tons of browser crashes :-D

When I saw your code, I was having a heck of a time trying to find out what was crashing the browser. I then checked if there was circular dependency involved, and found out it wasn't the case, the browser would have realized that and ended the call stack size. If you changed the calculated setter's formula to:

set(value) {
    this.interval = 8
  }

This worked.

I tried doing this (removing the divisor -- "/ this.periods"):

set(value) {
    this.interval = (moment(value).diff(this.startDate, 'days')+1)
}

And that worked too.

I checked to see what the value would be if you added back the divisor:

set(value) {
    console.log((moment(value).diff(this.startDate, 'days')+1) / this.periods)
}

I discovered that the value was 7.25 (I used console.log on the formula)

Then I did something like this:

set(value) {
    this.interval = 7.25
}

and that worked too!

Then I wanted to see if there's a way to preserve all the variables in the formula, and using Math.round formula (I also accounted for whether or not this.periods would equal zero as you don't want to divide by zero):

set(value) {
    if (this.periods > 0) {
         this.interval = Math.round((moment(value).diff(this.startDate, 'days')+1) / this.periods)
    }
}

This worked too

I also found out that there are deprecated values and not following ISO 8601 nor RFC 2822, so I change the code to reflect that, and the browser still freezes, but you don't have the deprecated errors anymore:

format: 'YYYY-MM-DD'
set(value) {
   var newEndDateMomentObj = moment(value);
   var startDateMomentObj = moment(this.startDate);
   this.interval = (newEndDateMomentObj.diff(startDateMomentObj, 'days')+1) / this.periods
}
tony19
  • 125,647
  • 18
  • 229
  • 307
sksallaj
  • 3,872
  • 3
  • 37
  • 58
  • Thank you for thorough analysis @sksallaj I also tried one more scenario where I don't have date picker but button and method to change startDate, the same result so I would rule out date picker issue it must be the moments or vie itself – dascorp Mar 04 '18 at 19:30
0

New fiddle answer: https://jsfiddle.net/ghLmnm2c/483/

I thought about this more, and went with an older strategy I mentioned when I first tried to help you, and removed the computed properties as this might have something to do with the way the component renders. So instead I replicated the functionality using the plugin's event emitters, and added a watcher on the interval plugin to get you the same functionality.

Here's what the html looks like:

Start Date: <date-picker v-model="startDate" :config="config" @dp-change="updateEndDateInterval"></date-picker>
End Date: <date-picker v-model="endDate" :config="config" @dp-update="updateInterval"></date-picker>
Interval: <input class="form-control" type="text" v-model="interval" @keyup="updateEndDate">

I figured that the only inputs that will change the endDate is the startdate and the interval.

So here are the methods:

methods: {
   updateEndDateInterval: function() {
      this.updateEndDate(); //this automatically hits updateInterval because of @dp-change="updateInterval" on the enddate component
  },
  updateEndDate: function() {
      this.endDate = moment(this.startDate).add(this.interval * this.periods, 'days');
  },
  updateInterval: function(v) {
      if (this.periods > 0) {
         var dateVal = ""
         if (v.target) {
              dateVal = v.target.value;
         } else {
            dateVal = v;
         }
         this.interval = (moment(dateVal).diff(this.startDate, 'days')+1) / this.periods;
      }
  }
}

The updateInterval accepts a v parameter, the v will take in the passed moment object from updateEndDateInterval, or the event object when changing the value of the interval input textbox. If you pass in the this.endDate from the interval textbox, you run into a circular reference, where interval will update enddate, enddate will update interval and so on and so forth.

Also, you need to set endDate to have a reactive property

data: {
    interval: 7,
    startDate: moment(),
    endDate: ''   // leave this empty
}

when you load the component, update the endDate property to whatever the startdate value (defined in updateEndDate):

mounted: function(){
      this.updateEndDate();
}

I checked all functionality compared to your old fiddle, and I checked for the reactivity. Personally, I like this strategy more as you are using the prefered methods mentioned in the plugin, I didn't really see any examples of computing properties being used. This also makes things more modular as you can tweak when necessary without accidently modifying other components.


I was going to give the interval textbox a vue watcher and have it set to the updateEndDate like this:

//don't do this
watch: {
    interval: function() {
        this.updateEndDate()
    }
}

But this code doesn't work, because of the circular reference problem. This will crash your browser. So that's why I went with the @keyup event. When you change the interval from the startdate or enddate, the interval changes, and because there's a watch on it, it creates a circular reference as it continuously updates itself. That's why triggering the updateEndDate via keyup event is triggered based on key inputs, so selecting dates won't interfere with the update of the interval.


Answer to the underlying problem (fixed).

Used this answer as a reference:

Passing event and argument to v-on in Vue.js

I put a flag to prevent you from updating the interval when you change the startdate, and to prevent the interval from changing itself when setting the enddate.

<date-picker v-model="endDate" :config="config" @dp-change="updateInterval($event, intervalUpdate)"></date-picker>

then add reactive property to data:

data: {
   intervalUpdate: true
}

update the methods:

updateEndDateInterval: function() {
      this.intervalUpdate = false;  //do not update interval
        ...
},
updateEndDate: function() {
      this.intervalUpdate = false;
       ...
},
updateInterval: function(v, shouldUpdate) {
      console.log(shouldUpdate);
      if (shouldUpdate) {
         this.intervalUpdate = shouldUpdate;
      }
      if (this.intervalUpdate) {
          ....
      }
      this.intervalUpdate = true; //reset the flag
  }

(FIXED) Now, the underlying problem you have. Updating enddate and interval date using startdate. You couldn't see it when you were using computer properties. Which is the reason why you couldn't edit your interval correctly.

If you change only the enddate, the formula for calculating the interval works correctly, because you're ONLY changing the interval. However, changing the startdate changes the enddate AND interval.

When selecting startDate both needs to be updated, but it can't because both formulas depends on each other, you get stale data if you switch the order of execution:

So, if you try to update the enddate first, you can't because this.interval is stale, as this will give you the incorrect value of this.endDate, and using the incorrect value of enddate in the next line for calculating the interval will give you incorrect result.

 //stale this.interval value as it isn't updated before
 this.endDate = moment(this.startDate).add(this.interval * this.periods, 'days').format('DD MMM YYYY');
 //incorrect this.endDate to give incorrect this.interval
 this.interval = (moment(this.endDate).diff(this.startDate, 'days')+1) / this.periods;

if you try to reverse the order, you can't because this.endDate is stale, it's not updated so your interval value becomes incorrect. And when you execute the next line, this.endDate becomes incorrect because of the incorrect value of this.interval

 //stale this.endDate value as it isn't updated before
 this.interval = (moment(this.endDate).diff(this.startDate, 'days')+1) / this.periods;
 //incorrect this.interval used to give incorrect this.endDate
 this.endDate = moment(this.startDate).add(this.interval * this.periods, 'days').format('DD MMM YYYY');
sksallaj
  • 3,872
  • 3
  • 37
  • 58