Unexpected side effect in "isExpiryComing" computed property - vue.js

I tried to make the nearExpiry attribute to become TRUE if it is within the range of 30 days. But I hit an error which is Unexpected side effect in "isExpiryComing" computed property, is there any way I can overcome this?
I'm not sure how do I use slice at the isExpiryComing computed properties. Is there any workaround for this error?
<template>
<div class="container wrapper d-flex flex-column justify-content-center align-items-center">
<h1 class="text-info">Ingredients List</h1>
<ingredients-list class="justify-content-center"
v-for="(ingredient,index) in sortedItems"
:key="index"
:index='index'
:food-name="ingredient.food"
:food-expiry="ingredient.expiryDate"
:is-expiry="isExpiryComing.nearExpiry"></ingredients-list>
</div>
</template>
<script>
export default {
data() {
return {
ingredients: [
{
food: 'CARROT',
expiryDate: '2020-12-12',
nearExpiry: false
},
{
food: 'PAPAYA',
expiryDate: '2018-1-15',
nearExpiry: false
},
{
food: 'ORANGE',
expiryDate: '2021-10-13',
nearExpiry: false
},
{
food: 'CHICKEN',
expiryDate: '2019-4-23',
nearExpiry: false
},
{
food: 'MEAT',
expiryDate: '2021-5-23',
nearExpiry: false
},
],
}
},
computed: {
sortedItems() {
return this.ingredients.slice().sort((a, b) => {
return new Date(a.expiryDate) - new Date(b.expiryDate);
});
},
isExpiryComing() {
const now = new Date().getTime()
const expiryDate = new Date(this.expiryDate).getTime()
if (now - expiryDate > (30 * 24 * 60 * 60 * 1000)) {
this.nearExpiry = false
} else {
this.nearExpiry = true
}
return this.nearExpiry
}
},
}
</script>

It's because you're chagning another variable inside a computed property, this.nearExpiry - your use case doesn't need it.
I assume you want to check the expiration date for every product inside v-for.
I would suggest removing isExpiryComing entirely and changing the sortedItems to:
return this.ingredients
.slice()
.sort((a, b) => {
return new Date(a.expiryDate) - new Date(b.expiryDate)
})
.map((ingredient) => {
const now = new Date().getTime()
const expiryDate = new Date(ingredient.expiryDate).getTime()
return { ...ingredient, nearExpiry: now - expiryDate > 30 * 24 * 60 * 60 * 1000 }
})
Might be also a good idea to move const now = new Date().getTime() higher, to data().

A couple of issues I see with this:
isExpiryComing is scoped to your current component (like ingredients and sortedItems), not to the iteration of your v-for (like ingredient and index).
Setting to this.nearExpiry = true is likely what is causing the error. Computed properties should be pure-functions, i.e. they should not affect the state (normally this wouldn't be the case, as this should be scoped to the function with the method signature you used, but this is Vue so good luck with upholding that). If you want to affect state, you should use a watched property. (and since you are just returning this.nearExpiry, why not just make nearExpiry a local variable?).
To fix the error, just make nearExpiry a local variable instead of a vue-instance variable (this.nearExpiry). But, per my first point, that's probably not what you want either. Probably move the computed property to your ingredients-list component.

Related

Waiting time counter for each object in array Vue js

To get in context, I have a table that shows incoming calls and the waiting time for every call. The data array looks like:
[
{
id: 1,
patient_name: lorem ipsum,
created_at: 2022-02-02 09:10:35,
...
},
{
id: 2,
patient_name: dolor ipsum,
created_at: 2022-02-02 09:00:35,
...
}
]
I'm trying to figure out how to assign a setTimeout for each object, but I'm completely lost.
So far, I found that a counter can be made through a watcher, but of course this only acts as a "global" counter.
watch: {
timerCount: {
handler (value) {
if (value > 0) {
setTimeout(() => {
this.timerCount++
}, 1000)
}
},
immediate: true // This ensures the watcher is triggered upon creation
}
},
Is there a way to use a function to show a counter on each object? I was thinking in something like this:
<template>
<span v-for="call in calls" :key="call.id">
Requested at: {{ call.created_at }}
waiting time: {{ showWaitingTime(call.created_at) }} // <- Not sure if this can be done
</span>
</template>
...
<script>
....
methods: {
showWaitingTime (created_at) {
// get diff in seconds between created_at and now
// do something here with a timeOut() to increment the object property...
}
}
</script>
In addition, I would like to return the waiting time in HH:mm:ss format.
One solution is to wrap the {{ showWaitingTime(call.created_at) }} with a <span> that is keyed on timerCount, so that the <span> is re-rendered when timerCount changes (thus calling showWaitingTime again to compute the new time string):
In the template, wrap the timestamp string with a <span> that has its key bound to timerCount:
waiting time: <span :key="timerCount">{{ showWaitingTime(call.created_at) }}</span>
In a watcher on calls, use setInterval to start a periodic timer. Be sure to stop the timer with clearInterval before starting a new timer and when unmounting the component.
export default {
beforeUnmount() {
clearInterval(this._updateTimer)
},
// Vue 2 equivalent of beforeUnmount()
beforeDestroy() {
clearInterval(this._updateTimer)
},
watch: {
calls: {
handler(calls) {
clearInterval(this._updateTimer)
if (calls.length) {
this._updateTimer = setInterval(() => this.timerCount++, 1000)
}
},
immediate: true,
},
},
}
The watcher you have on timerCount is effectively implementing setInterval. Remove that code since it's obviated by the code in step 2.
export default {
watch: {
// timerCount: {⋯} // ⛔️ remove this watch
}
}
In showWaitingTime(), calculate the HH:mm:ss from the difference between the given time and now:
export default {
methods: {
showWaitingTime(created_at) {
const diff = new Date() - new Date(created_at)
const twoD = x => `${Math.floor(x)}`.padStart(2, '0')
const HH = twoD((diff / (60 * 60 * 1000)) % 24)
const mm = twoD((diff / (60 * 1000)) % 60)
const ss = twoD((diff / 1000) % 60)
return `${HH}:${mm}:${ss}`
},
},
}
demo

Do Vue.js render functions allow return of an array of VNodes?

I am working on extending a Vue.js frontend application. I am currently inspecting a render function within a functional component. After looking over the docs, I had the current understanding that the render function within the functional component will return a single VNode created with CreateElement aka h.
My confusion came when I saw a VNode being returned as an element in an array. I could not find any reference to this syntax in the docs. Does anyone have any insight?
export default {
name: 'OfferModule',
functional: true,
props: {
data: Object,
placementInt: Number,
len: Number
},
render (h, ctx) {
let adunitTheme = []
const isDev = str => (process.env.dev ? str : '')
const i = parseInt(ctx.props.placementInt)
const isDevice = ctx.props.data.Component === 'Device'
const Component = isDevice ? Device : Adunit
/* device helper classes */
const adunitWrapper = ctx.props.data.Decorate?.CodeName === 'AdunitWrapper'
if (!isDevice /* is Adunit */) {
const offerTypeInt = ctx.props.data.OfferType
adunitTheme = [
'adunit-themes',
`adunit-themes--${type}`.toLowerCase(),
`adunit-themes--${theme}`.toLowerCase(),
`adunit-themes--${type}-${theme}`.toLowerCase(),
]
}
const renderOfferModuleWithoutDisplayAdContainersWithAboveTemplate =
ctx.props.data.Decorate?.Position === 'AboveAdunit' || false
const renderOfferModuleWithoutDisplayAdContainers =
ctx.props.data.Decorate?.RemoveAds /* for adunits */ ||
ctx.props.data.DeviceData?.RemoveAds /* for devices */ ||
false
const getStyle = (className) => {
try {
return ctx.parent.$style[className]
} catch (error) {
console.log('$test', 'invalid style not found on parent selector')
}
}
const PrimaryOfferModule = (aboveAdunitSlot = {}) =>
h(Component, {
props: {
data: ctx.props.data,
itemIndex: i,
adunitTheme: adunitTheme.join('.')
},
attrs: {
class: [
...adunitTheme,
getStyle('product')
]
.join(' ')
.trim()
},
scopedSlots: {
...aboveAdunitSlot
}
})
if (renderOfferModuleWithoutDisplayAdContainersWithAboveTemplate) {
return [
PrimaryOfferModule({
aboveAdunit (props) {
return h({
data () {
return ctx.props.data.Decorate
},
template: ctx.props.data.Decorate?.Template.replace(
'v-show="false"',
''
)
})
}
})
]
} else if (renderOfferModuleWithoutDisplayAdContainers) {
return [PrimaryOfferModule()]
} else {
const withAd = i > 0 && i % 1 === 0
const adWrap = (placement, position, className) => {
return h(
'div',
{
class: 'm4d-wrap-sticky'
},
[
h(Advertisement, {
props: {
placement,
position: String(position)
},
class: getStyle(className)
})
]
)
}
return [
withAd && adWrap('inline-sticky', i, 'inlineAd'),
h('div', {
class: 'm4d-wrap-sticky-adjacent'
}),
h(
'div',
{
attrs: {
id: `inline-device--${String(i)}`
},
class: 'inline-device'
},
isDev(`inline-device id#: inline-device--${String(i)}`)
),
withAd &&
i !== ctx.props.len - 1 &&
h(EcomAdvertisement, {
props: {
placement: 'inline-static',
position: String(i)
},
class: getStyle('inlineStaticAd')
}),
PrimaryOfferModule()
]
}
}
}
It turns out that returning an array of VNodes actually predates the scopedSlots update.
I couldn't find it documented anywhere in the docs either, but via this comment on a Vue GitHub issue by a member of the Vue.js core team (which predates the scopedSlots commit by ~1 year), render() can return an Array of VNodes, which Vue will take and render in order. However, this only works in one, singular case: functional components.
Trying to return an array of VNodes with greater than 1 element in a normal (non-functional, stateful) component results in an error:
Vue.config.productionTip = false;
Vue.config.devtools = false;
Vue.component('render-func-test', {
render(h, ctx) {
return [
h('h1', "I'm a heading"),
h('h2', "I'm a lesser heading"),
h('h3', "I'm an even lesser heading")
];
},
});
new Vue({
el: '#app',
});
<script src="https://unpkg.com/vue#2/dist/vue.js"></script>
<div id="app">
Test
<render-func-test></render-func-test>
</div>
[Vue warn]: Multiple root nodes returned from render function. Render function should return a single root node.
But doing this in a functional component, as your example does, works just fine:
Vue.config.productionTip = false;
Vue.config.devtools = false;
Vue.component('render-func-test', {
functional: true, // <--- This is the key
render(h, ctx) {
return [
h('h1', "I'm a heading"),
h('h2', "I'm a lesser heading"),
h('h3', "I'm an even lesser heading")
];
},
});
new Vue({
el: '#app',
});
<script src="https://unpkg.com/vue#2/dist/vue.js"></script>
<div id="app">
Test
<render-func-test></render-func-test>
</div>
If you're interested in the why, another member of the Vue core team explained this limitation further down in the thread.
It basically boils down to assumptions made by the Vue patching and diffing algorithm, with the main one being that "each child component is represented in its parent virtual DOM by a single VNode", which is untrue if multiple root nodes are allowed.
The increase in complexity to allow this would require large changes to that algorithm which is at the very core of Vue. This is a big deal, since this algorithm must not only be good at what it does, but also very, very performant.
Functional components don't need to conform to this restriction, because "they are not represented with a VNode in the parent, since they don't have an instance and don't manage their own virtual DOM"– they're stateless, which makes the restriction unnecessary.
It should be noted, however, that this is possible on non-functional components in Vue 3, as the algorithm in question was reworked to allow it.
It seems this was implemented in:
https://github.com/vuejs/vue/commit/c7c13c2a156269d29fd9c9f8f6a3e53a2f2cac3d
This was a result of an issue raised in 2018 (https://github.com/vuejs/vue/issues/8056) , because this.$scopedSlots.default() returned both a VNode or an array of VNodes depending on the content.
The main argument was that this is inconsistent with how regular slots behave in render functions, and means any render function component rendering scoped slots as children needs to type check the result of invoking the slot to decide if it needs to be wrapped in an array
So Evan comments on the issue thread here, explaining that this.$scopedSlots.default would always return Arrays beginning v2.6 to allow for consistency, but to avoid breaking changes for how $scopedSlots was being used, the update would also allow return of an Array of a single VNode from render functions as well.

How Do I update the Dom (Re-render element) when value changes?

So I have a variable startWeek, that is being displayed and when I click a button, a Have a method that will add 7 days to the date. I want the page to show the new date but not sure how to go about doing this?
I want to get the dates to transition in and out everytime the date range is updated, but Im not sure how to do it. I have thought about using two v-if statements and transitioning back and fourth between them but im sure there is a better way. I have looked into watchers and computed properties but im not quite sure if they are the answer or how to implement them in this given situation.
Example:
<template>
<b-button #click="subtractWeek(7)></b-button>
<span id="thingIwantToUpdateInDOM">{{ startWeek + " - " + endWeek}}</span>
<b-button #click="addWeek(7)></b-button>
</template>
export default {
data(){
return{
startWeek: null,
endWeek: null,
}
},
methods: {
addWeek(days){
this.startWeek.setDate(this.startWeek.getDate() + days)
this.endWeek.setDate(this.endWeek.getDate() + days)
},
substractWeek(7){
this.startWeek.setDate(this.startWeek.getDate() - days)
this.endWeek.setDate(this.endWeek.getDate() - days)
},
getInitialDate(){
this.startWeek = new Date();
var tempEndWeek = new Date();
this.endWeek = tempEndWeek;
},
created() {
this.getInitialDate();
}
}
}
My ultimate goal is to have the date range to swipe our transition out similar to a carousel effect on every button click or value change. Any bit of advise is greatly appreciated!
Your end goal is lots of optimizations away, but the snippet below should get you started. Your date operations are fine, so a few notes about the implementation:
I don't exactly know the internals of how Vue does state change tracking in the data objects, but I'm fairly sure it involves getter and setter property accessors. When you do this.x = new Date(); this.x.setDate(this.x.getDate() + 7);, this.x tracks the date object and not its value, so the change will not be seen by Vue. You need to clone the date first, set a new date, and then reassign it to this.x (see the navigateWeeks method below).
watch is useful when you want to react to a single, specific property change, in your case, the dynamic startWeek is a perfect candidate. If the fact that something changed is more important than what exactly, use the updated hook (typical use-case: destroying & re-initializing 3rd party library widgets with new parameters).
computed is useful for keeping a property derived from another property in sync at all times, in your example the endDate is always 7 days after the startDate, so it is a perfect candidate for this. In the snippet I also used a computed value for the ISO date format that HTML date inputs expect.
Finally, you can do quite advanced stuff with setTimeout, some CSS keyframes, and toggling a .transitioning class
Vue.component('fading-date', {
template: `
<span><input :class="className" type="date" :value="htmlValue"></span>
`,
props: {
value: { type: Date },
fadeDuration: { type: Number, default: 1 }
},
data() {
return { transitioning: false, timer: null };
},
computed: {
htmlValue() {
return this.value.toISOString().split('T')[0];
},
className() {
return this.transitioning ? 'transitioning' : '';
}
},
watch: {
value() {
clearTimeout(this.timer);
this.transitioning = true;
this.timer = setTimeout(() => {
this.transitioning = false;
}, 1000 * this.fadeDuration);
}
}
});
new Vue({
el: '#app',
data: {
selectedWeek: new Date()
},
computed: {
weekAfterSelected() {
const date = this.selectedWeek;
const endDate = new Date(date);
endDate.setDate(date.getDate() + 7);
return endDate;
}
},
methods: {
navigateWeeks(numWeeks = 1) {
const newDate = new Date(this.selectedWeek);
newDate.setDate(newDate.getDate() + (7 * numWeeks));
this.selectedWeek = newDate;
}
}
});
input[type="date"] {
background: transparent;
border: none;
}
#keyframes fade{
0% {
opacity:1;
}
50% {
opacity:0;
}
100% {
opacity:1;
}
}
.transitioning {
animation: fade ease-out 1s;
animation-iteration-count: infinite;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<fading-date :value="selectedWeek" :fade-duration="1"></fading-date>
<fading-date :value="weekAfterSelected" :fade-duration="1"></fading-date>
<br>
<button type="button" #click="navigateWeeks(-1)">A week sooner</button>
<button type="button" #click="navigateWeeks(1)">A week later</button>
</div>

Why it is hard to use vue-i18n in vue data() (why it is not reactive)

I am using vue-i18n in a vue project. And I found it really confusing when using some data in vue data with i18n. Then if I change locale, that data is not reactive. I tried to return that data from another computed data but anyways it is not reactive because i18n is written in data. *My situation - * I want to show table with dropdown(list of columns with checkbox) above it. When user checks a column it will be showed in table if unchecks it won't. It is working fine until I change locale. After changing locale table columns is not translated but dropdown items is reactively translated and my code won't work anymore. Here is some code to explain better: In my myTable.vue component I use bootstrap-vue table -
template in myTable.vue
<vs-dropdown vs-custom-content vs-trigger-click>
<b-link href.prevent class="card-header-action btn-setting" style="font-size: 1.4em">
<i class="fa fa-th"></i>
</b-link>
<vs-dropdown-menu class="columns-dropdown">
<visible-columns :default-fields="columns" #result="columnListener"></visible-columns>
</vs-dropdown-menu>
</vs-dropdown>
<b-table class="generalTableClass table-responsive" :fields="computedFieldsForTable">custom content goes here</b-table>
script in myTable.vue
data(){
return {
fieldsForTable: [];
}
},
computed: {
computedFieldsForTable () {
return this.fieldsForTable;
},
columns() {
return [
{
key: 'id',
label: this.$t('id'),,
visible: true,
changeable: true
},
{
key: 'fullName',
label: this.$t('full-name'),,
visible: true,
changeable: true
},
{
key: 'email',
label: this.$t('email'),,
visible: true,
changeable: true
}
]
}
},
mounted () {
this.fieldsForTable = this.filterColumns(this.columns);
},
methods: {
filterColumns(columns = []) {
return columns.filter(column => {
if (column.visible) {
return column
}
})
},
columnListener ($event) {
this.fieldsForTable = this.filterColumns($event)
}
}
Can someone give me some advice for this situation ?
*EDIT AFTER SOME DEBUGGING: I think when filtering columns(in computed) and returning it for fieldsForTable inside filterColumns(columns) method, it actually returning array(of objects) with label='Label Name' not label=this.$t('labelName'). So after filtering the new array has nothing to do with vue-i18n. My last chance is reloading the page when locale changes.
Trying modify computedFieldsForTable as follows. You need to reference this.columns in computedFieldsForTable, so that Vue can detect the change of labels in this.columns.
computedFieldsForTable () {
return this.filterColumns(this.columns);
},
EDITED: put your this.columns in data. Then
columnListener ($event) {
this.columns = $event;
}
I hope i didn't misunderstand what you mean.
EDITED (again):
Maybe this is the last chance that I think it can work. Put columns in computed() still and remove computedFieldsForTable. Finally, just leave fieldsForTable and bind it on fields of <b-table>.
watch: {
columns(val) {
this.fieldsForTable = this.filterColumns(val)
}
},
method: {
columnListener ($event) {
this.fieldsForTable = this.filterColumns($event)
}
}
However, I think it is better and easier to reload page whenever local change. Especially when your columns have a more complex data structure.

Vue v-for list not re-rendering after computed data update

I am implementing pagination for a huge list of cards, I display 10 cards at once and wish to show the 10 next (or 10 previous) by clicking on two buttons.
Here's how I do it:
export default {
...
data() {
return {
pois: [], // My list of elements
pageNumber: 0, // Current page number
};
},
props: {
size: {
type: Number,
required: false,
default: 10, // 10 cards per page
},
},
computed: {
pageCount() {
// Counts the number of pages total
const l = this.pois.length;
const s = this.size;
return Math.floor(l / s);
},
paginatedData() {
// Returns the right cards based on the current page
const start = this.pageNumber * this.size;
const end = start + this.size;
return this.pois.slice(start, end);
},
},
methods: {
nextPage() {
this.pageNumber += 1;
},
prevPage() {
this.pageNumber -= 1;
},
}
...
};
And my template:
<div v-for="poi in paginatedData" :key="poi.id">
<card :poi="poi"/>
</div>
Everything should work (and a page change does output the correct cards in the console) but my list is not updated even though the computed method is called on each click.
What is causing this issue? I've read it could be linked to a :key value missing, but it's there, and no data is being updated directly and manually in the array, only sliced out.
First, try this change, just for sure, and let me know in comment it works or not.
export default {
...
computed: {
paginatedData() {
...
const end = start + this.size - 1;
...
},
...
};
And yes: instead of id, try to use index:
<div v-for="(poi, idx) in paginatedData" :key="idx">
<card :poi="poi"/>
</div>