Dayjs with Nuxt/Vue: Button to change timezone - vue.js

I've been tinkering around with a booking system using dayjs. Currently it imports objects with a DateTime in the 'Australia/Melbourne' timezone, and I'm looking to make a dropdown that changes the timezone that it's currently being displayed in. However, I haven't had any luck getting the display to change. I've even got it to reload the component, but it doesn't seem to do the trick.
Component that will change the timezone:
<div>
<button #click="changeTimezone('SA')" >
Set timezone to Adelaide
</button>
<button #click="changeTimezone('WA')" >
Set timezone to Perth
</button>
<button #click="changeTimezone('NSW')" >
Set timezone to Sydney
</button>
<div><span>data timezone</span>{{ currentTimeZoneOutput }}</div>
<div><span>computed timezone</span>{{ computedTimeZoneOutput }}</div>
</div>
Script:
export default {
data() {
return {
currentTimeZoneOutput: this.$dayjs(Date.now()).format("h:mm:ss A"),
};
},
methods: {
changeTimezone(state){
this.$store.commit('SET_TIMEZONE', state)
this.$dayjs.tz.setDefault(this.$store.state.timezone)
},
forceRerender(){
this.$emit("forceRerender");
}
},
computed: {
computedTimeZoneOutput (){
return this.$dayjs(Date.now()).format("h:mm:ss A")
}
}
}
The store mutation:
SET_TIMEZONE: (state, newTimezone) => {
switch(newTimezone){
case 'NT':
state.timezone = 'Australia/Darwin'
break;
case 'WA':
state.timezone = 'Australia/Perth'
break;
case 'VIC':
state.timezone = 'Australia/Victoria'
break;
case 'QLD':
state.timezone = 'Australia/Brisbane'
break;
case 'NSW':
state.timezone = 'Australia/Sydney'
break;
case 'ACT':
state.timezone = 'Australia/Canberra'
break;
case 'TAS':
state.timezone = 'Australia/Tasmania'
break;
case 'SA':
state.timezone = 'Australia/Adelaide'
break;
default:
state.timezone = newTimezone
}
console.log('Timezone is now: ' + state.timezone)}
Something tells me I'm not actually able to change the timezone dynamically. If anyone could point me in the right direction that would be great, cheers.

You need to use mapState in this situation:
Script
import { mapState } from 'vuex'
export default {
data() {
return {
currentTimeZoneOutput: this.$dayjs(Date.now()).format("h:mm:ss A"),
};
},
methods: {
changeTimezone(state){
this.$store.commit('SET_TIMEZONE', state)
this.$dayjs.tz.setDefault(this.timezone) // EDITED
},
/**
* No need for this. By force re-rendering you run data() again
* too. So it will be the same each time.
*/
/* forceRerender(){
this.$emit("forceRerender");
} */
},
computed: {
...mapState(['timezone'])
}
}
Now inside your template use timezone from computed property.
...
<div><span>data timezone</span>{{ timezone }}</div>
A very good explanation about mapState is in this link.

Okay turns out - needed to add UTC to the dates first.
So instead of (for example):
this.$dayjs(Date.now()).tz("America/New_York").format("h:mm:ss A")
It becomes:
this.$dayjs.utc(Date.now()).tz("America/New_York").format("h:mm:ss A")
Now my dates are finally reactive!

Related

Vue.js 3 Pinia store is only partially reactive. Why?

I'm using Pinia as Store for my Vue 3 application. The problem is that the store reacts on some changes, but ignores others.
The store looks like that:
state: () => {
return {
roles: [],
currentRole: 'Administrator',
elements: []
}
},
getters: {
getElementsForCurrentRole: (state) => {
let role = state.roles.find((role) => role.label == state.currentRole);
if (role) {
return role.permissions.elements;
}
}
},
In the template file, I communicate with the store like this:
<template>
<div>
<draggable
v-model="getElementsForCurrentRole"
group="elements"
#end="onDragEnd"
item-key="name">
<template #item="{element}">
<n-card :title="formatElementName(element.name)" size="small" header-style="{titleFontSizeSmall: 8px}" hoverable>
<n-switch v-model:value="element.active" size="small" />
</n-card>
</template>
</draggable>
</div>
</template>
<script setup>
import { NCard, NSwitch } from 'naive-ui';
import draggable from 'vuedraggable'
import { usePermissionsStore } from '#/stores/permissions';
import { storeToRefs } from 'pinia';
const props = defineProps({
selectedRole: {
type: String
}
})
const permissionsStore = usePermissionsStore();
const { getElementsForCurrentRole, roles } = storeToRefs(permissionsStore);
const onDragEnd = () => {
permissionsStore.save();
}
const formatElementName = (element) => {
let title = element.charAt(0).toUpperCase() + element.slice(1);
title = title.replace('-', ' ');
title = title.split(' ');
if (title[1]) {
title = title[0] + ' ' + title[1].charAt(0).toUpperCase() + title[1].slice(1);
}
if (typeof title == 'object') {
return title[0];
}
return title;
}
</script>
My problem is the v-model="getElementsForCurrentRole". When making changes, for example changing the value for the switch, the store is reactive and the changes are made successfully. But: If I try to change the Array order by dragging, the store does not update the order. I'm confused, because the store reacts on other value changes, but not on the order change.
What can be the issue here? Do I something wrong?
-Edit- I see the following warning on drag: Write operation failed: computed value is readonly
Workaround
As workaround I work with the drag event and write the new index directly to the store variable. But...its just a workaround. I would really appreciate a cleaner solution.
Here is the workaround code:
onDrag = (event) => {
if (event && event.type == 'end') {
// Is Drag Event. Save the new order manually directly in the store
let current = permissionsStore.roles.find((role) => role.value == permissionsStore.currentRole);
var element = current.permissions.elements[event.oldIndex];
current.permissions.elements.splice(event.oldIndex, 1);
current.permissions.elements.splice(event.newIndex, 0, element);
}
}
You should put reactive value on v-model.
getElementsForCurrentRole is from getters, so it is treated as computed value.
Similar to toRefs() but specifically designed for Pinia stores so
methods and non reactive properties are completely ignored.
https://pinia.vuejs.org/api/modules/pinia.html#storetorefs
I think this should work for you.
// template
v-model="elementsForCurrentRole"
// script
const { getElementsForCurrentRole, roles } = storeToRefs(permissionsStore);
const elementsForCurrentRole = ref(getElementsForCurrentRole.value);

Vue2 : v-show and changind data dynamically

I have this code that works but outputs an error
this.hideButton is not a function
here a part of the code
<template>
<div>
<v-select
#open="openSelect"
#search="applySearch"
>
<b-button variant="none" class="selectAllButton"
v-on:click="clickSelectAll" v-show="hiddenBtn">Select All</b-button>
</div>
</template>
export default {
data() {
return {
hiddenBtn: false
}
},
methods: {
applySearch(search, loading) {
if (search.length > 0 && search.length < 3) {
this.hideBtn();
return;
}
this.showBtn();
this.retrieveEntities(search, loading)
},
showBtn() {
this.hiddenBtn = true;
},
hideBtn(){
this.hiddenBtn = false;
}
}
I think this is the wrong way to update my hiddenBtn property to show and hide the button, but It works even if I get an error, so I don't understand what happens
you are calling this.hideBtn() which is not a function
applySearch(search, loading) {
if (search.length > 0 && search.length < 3) {
this.hideBtn(); // <-- you are calling this.hideBtn() which isn't a function. just remove this and try
return;
}
this.showBtn();
this.retrieveEntities(search, loading)
}
This code should work properly, maybe you have somewhere else in code something that is trying to execute this.hideButton() while your method's name is this.hideBtn().

BootstrapVue: Check database if username already exists: Optimal way to use debounce?

Can I get a second set of eyes on using a <b-form-input> with debounce prop?
Use case: I am making an expensive API call to check if a username already exist in a database:
<b-form-input
id="username_input"
v-model="formValues.username"
type="text"
debounce="500"
#input="usernameCheck"
></b-form-input>
and here's the input handler usernameCheck:
async usernameCheck() {
const username = this.formValues.username
if (username.length >= 3 && username.length <= 15) {
const ref = this.$fire.firestore.doc(`usernames/${username}`)
const { exists } = await ref.get() // here I'm checking if document exists already
this.usernameAvailable = !exists
} else {
...
}
Is this a good approach?
Or should I be using a watcher?
I think there is a better way to approach this. First, the key point I discovered is that there is a difference between using #input and #update as mentioned here:
The input event is emitted on user update. You have to use the update event which is triggered to update the v-model to get your expected result.
So, I have now updated my code as follows (with some additional validation UX messaging):
<b-form-input
id="username_input"
v-model="formValues.username"
type="text"
debounce="500"
:state="usernameValid"
trim
#update="usernameCheck"
></b-form-input>
valid: {{ state }}
<p v-show="formValues.username != '' && !usernameValid">
Username must be between 3 and 15 characters
</p>
<p v-show="usernameAvailable && usernameValid">Username is available!</p>
import { BFormInput } from 'bootstrap-vue'
export default {
components: {
BFormInput
},
data() {
return {
formValues: {
username: ''
},
usernameAvailable: false,
state: null,
}
},
computed: {
usernameValid() {
return (
this.formValues.username.length >= 3 &&
this.formValues.username.length <= 15
)
}
},
methods: {
async usernameCheck() {
const username = this.formValues.username
if (this.usernameValid) {
const ref = this.$fire.firestore.doc(`usernames/${username}`)
const { exists } = await ref.get() // checks if doc exists in firebase db
this.usernameAvailable = !exists
this.state = true // input now in valid state
} else {
console.log('not a valid username')
this.state = false
}
So this works for me, but I have not done a full test. I do see that the network polling is reduced due to debounce, too.
I'd be curious if anyone has any ways I can improve this (if needed!). Thanks!

Prevent Vue Multiple Select to Store an Empty Array

I want this select multiple to pre-select one option, and not be able to deselect all options.
Whenever the last selected option is deselected it should be reselected. In other words when the user tries to deselect the last selected option it should visually not be deselected.
<template>
<b-select
if="Object.keys(doc).length !== 0 /* wait until firebase has loaded */"
:options="computedOptions"
v-model="model"
multiple
#input="onChange"
/>
</template>
<script>
//import Vue from 'vue'
import { fb } from "../fbconf";
export default {
name: "MyMultiSelect",
props: {
doc: Object, // firestore document
},
data() {
return {
options: []
};
},
firestore() {
var options = fb.db.collection("options");
return {
options: options
};
},
computed: {
computedOptions: function() {
return this.options.map(function(option) {
return {
text: option.name,
value: option.id
};
});
},
// to make sure mySelectedOptions is an array, before this.doc is loaded
// I use the following custom model
// because not using 'get' below causes a warning:
// [Vue warn]: <select multiple v-model="localValue"> expects an Array value for its binding, but got Undefined
model: {
get: function() {
if (!this.doc.hasOwnProperty('mySelectedOptions')) return []; // empty array before this.doc is loaded
else return this.doc['mySelectedOptions'];
},
set: function(newValue) {
// here I can prevent the empty array from being stored
// but visually the user can deselect all options, which is bad UX
//if (Array.isArray(newValue) && newValue.length > 0) this.doc['mySelectedOptions'] = newValue;
}
},
},
methods: {
onChange: function(newValue){
// I can manually store the array as I want here
// but I cannot in any way prevent the user from deselecting all options
if (Array.isArray(newValue) && newValue.length > 0) this.doc['mySelectedOptions'] = newValue;
else {
// none of these reselects the last selected option
var oldValue = this.doc['mySelectedOptions'];
this.doc['mySelectedOptions'] = this.doc['mySelectedOptions'];
//this.$forceUpdate();
//this.$emit("change", newValue);
//Vue.set(this.doc, 'mySelectedOptions', this.doc['mySelectedOptions']);
}
}
}
};
</script>
You could add watcher and when length becomes 0 just add previous value.
watch: {
model(val, oldVal) {
if(val.length == 0 && oldVal.length > 0) {
// take only one item in case there's clear button or etc.
this.model = [oldval[0]];
}
}
}

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>