What is proper Vue 3 <Setup Script/> syntax for Watchers? - vue.js

What is proper Vue 3 syntax for Watchers? It seems to be omitted from the docs. I'm very excited about the new features seen here:
https://vuejs.org/guide/essentials/watchers.html#deep-watchers
But what's the intended syntax?

This is a basic example of a watch inside the script setup syntax:
<template>
<div>
<input type="text" v-model="user.name" placeholder="Name" />
<input type="text" v-model="user.lastname" placeholder="Lastname" />
{{ nameUpdatesCount }}
</div>
</template>
<script setup>
import { reactive, ref, watch } from "vue";
const user = reactive({ name: "", lastname: "" });
const nameUpdatesCount = ref(0);
const increaseNameUpdatesCount = () => {
nameUpdatesCount.value++;
};
watch(user, (newValue, oldValue) => {
console.log(newValue, oldValue);
increaseNameUpdatesCount();
});
</script>
In the example above, the watcher will be triggered every time that you write or delete something in the inputs. This happens because the name property changes its value. After that, the increaseNameUpdatesCount method is called, and you will see the nameUpdatesCount be incremented by one.

Related

How to declare local property from composition API in Vue 3?

In Vue 2 I would do this:
<script>
export default {
props: ['initialCounter'],
data() {
return { counter: this.initialCounter }
}
}
</script>
In Vue 3 I tried this:
<script setup>
import { ref } from 'vue';
defineProps({ 'initialCounter': Number })
const counter = ref(props.initialCounter)
</script>
This obviously doesn't work because props is undefined.
How can I bind one-way properties to a local variable in Vue 3?
It seems the result of defineProps is not assigned as a variable. check Vue3 official doc on defineProps. Not really sure what is the use case of ref() here but toRef API can be used as well.
import { ref } from 'vue';
const props = defineProps({ 'initialCounter': Number })
const counter = ref(props.initialCounter)
Retrieving a read-only value and assigning it to another one is a bad practice if your component is not a form but for example styled input. You have an answer to your question, but I want you to point out that the better way to change props value is to emit update:modalValue of parent v-model passed to child component.
And this is how you can use it:
<template>
<div>
<label>{{ label }}</label>
<input v-bind="$attrs" :placeholder="label" :value="modelValue" #input="$emit('update:modelValue', $event.target.value)">
<span v-for="item of errors">{{ item.value }}</span>
</div>
</template>
<script setup>
defineProps({ label: String, modelValue: String, errors: Array })
defineEmits(['update:modelValue'])
</script>
v-bind="$attrs" point where passed attributes need to be assigned. Like type="email" attribute/property of a DOM element.
And parent an email field:
<BaseInput type="email" v-model="formData.email" :label="$t('sign.email')" :errors="formErrors.email" #focusout="validate('email')"/>
In this approach, formdata.email in parent will be dynamically updated.

Using tiptap with v-model and <script setup> in Vue 3

I'm trying to use tiptap with Vue.js with the <script setup> approach of creating a Single File Component (SFC).
TextEditor.vue
<template>
<editor-content :editor="editor" class="editor" />
</template>
<script lang="ts" setup>
import { useEditor, EditorContent } from '#tiptap/vue-3'
import StarterKit from '#tiptap/starter-kit'
const props = defineProps({
modelValue: {
type: String,
default: "",
}
})
const emit = defineEmits(['update:modelValue'])
const editor = useEditor({
content: props.modelValue,
extensions: [StarterKit],
onUpdate: ({editor}) => {
let content = editor.getHTML()
emit('update:modelValue', content)
}
})
</script>
I then use this component like this:
<template>
<text-editor v-model="myModel.content" />
</template>
This works when <text-editor> is loaded after myModel.content is defined.
However, if <text-editor> loads before myModel.content is set from my database API, then the text content remains blank. From what I understand from looking at the examples in the tiptap docs, I need to somehow use watch to update my editor when props.modelValue is changed using something like this:
watch(() => props.modelValue, (newValue, oldValue) => {
const isSame = newValue === oldValue
console.log(`Same: ${isSame}`)
if (isSame) {
return
}
editor.commands.setContent(newValue, false)
})
However, in the snippet above, editor is a ShallowRef type and doesn't have a reference to commands to call setContent.
What is the best way to get the above example to work when loading tiptap with the <script setup> approach?
You need to access the ref actual value with .value
editor.value?.commands.setContent('<p>test</p>', false)

How to correctly pass a v-model down to a Quasar q-input base component?

I am using Quasar to build my Vue app and I want to create a base component (a.k.a. presentational, dumb, or pure component) using q-input.
I have a created a SFC named VInput.vue as my base component, it looks like this:
<template>
<q-input
outlined
class="q-mb-md"
hide-bottom-space
/>
</template>
Then I created a SFC named TestForm.vue that looks like this:
<template>
<q-form>
<v-input label="Email" v-model="email" />
</q-form>
</template>
<script setup lang="ts">
import VInput from './VInput.vue';
import { ref } from 'vue';
const email = ref('john#example.com');
</script>
The label="Email" v-model="email" parts are passed down to my VInput.vue base component and correctly rendered on the page.
But there is a typescript error on q-input of the VInput.vue base component because q-input requires a v-model:
`Type '{ outlined: true; class: string; hideBottomSpace: true; "hide-bottom-space": boolean; }' is not assignable to type 'IntrinsicAttributes & VNodeProps & AllowedComponentProps & ComponentCustomProps & QInputProps'.`
`Property 'modelValue' is missing in type '{ outlined: true; class: string; hideBottomSpace: true; "hide-bottom-space": boolean; }' but required in type 'QInputProps'.ts(2322)`.
So how do I code the VInput.vue base component without knowing the v-model value head of time?
I have come up with the below solution, which seems to work because I think the v-model passed down is overiding the base component v-model.
But I wanted to ask to make sure I wasn't screwing something up.
Is this the correct way of doing things? It seems hacky.
<template>
<q-input v-model="inputText" outlined class="q-mb-md" hide-bottom-space />
</template>
<script setup lang="ts">
const inputText = '';
</script>
I found a couple of solutions:
Solution 1
It involves splitting the v-model into it seperate parts (:model-value and #update:model-value, and then passing in the text value as a prop.
Base component VInput.vue:
<template>
<q-input
outlined
class="q-mb-md"
hide-bottom-space
:model-value="text"
#update:model-value="(value) => emit('update:text', value)"
/>
</template>
<script setup lang="ts">
defineProps({
text: {
required: false,
type: String,
},
});
const emit = defineEmits(['update:text']);
</script>
Solution 2
Extracting the prop and using toRef on it.
<template>
<q-input outlined class="q-mb-md" hide-bottom-space v-model="textProp" />
</template>
<script setup lang="ts">
import { toRef } from 'vue';
const props = defineProps({
text: {
required: false,
type: String,
default: '',
},
});
const textProp = toRef(props, 'text');
</script>

Async loading child component doesn't trigger v-if

Hi everyone and sorry for the title, I'm not really sure of how to describe my problem. If you have a better title feel free to edit !
A little bit of context
I'm working on a little personal project to help me learn headless & micro-services. So I have an API made with Node.js & Express that works pretty well. I then have my front project which is a simple one-page vue app that use vuex store.
On my single page I have several components and I want to add on each of them a possibility that when you're logged in as an Administrator you can click on every component to edit them.
I made it works well on static elements :
For example, here the plus button is shown as expected.
However, just bellow this one I have some components, that are loaded once the data are received. And in those components, I also have those buttons, but they're not shown. However, there's no data in this one except the title but that part is working very well, already tested and in production. It's just the "admin buttons" part that is not working as I expect it to be :
Sometimes when I edit some codes and the webpack watcher deal with my changes I have the result that appears :
And that's what I expect once the data are loaded.
There is something that I don't understand here and so I can't deal with the problem. Maybe a watch is missing or something ?
So and the code ?
First of all, we have a mixin for "Auth" that isn't implemented yet so for now it's just this :
Auth.js
export default {
computed: {
IsAdmin() {
return true;
}
},
}
Then we have a first component :
LCSkills.js
<template>
<div class="skills-container">
<h2 v-if="skills">{{ $t('skills') }}</h2>
<LCAdmin v-if="IsAdmin" :addModal="$refs.addModal" />
<LCModal ref="addModal"></LCModal>
<div class="skills" v-if="skills">
<LCSkillCategory
v-for="category in skills"
:key="category"
:category="category"
/>
</div>
</div>
</template>
<script>
import LCSkillCategory from './LCSkillCategory.vue';
import { mapState } from 'vuex';
import LCAdmin from '../LCAdmin.vue';
import LCModal from '../LCModal.vue';
import Auth from '../../mixins/Auth';
export default {
name: 'LCSkills',
components: {
LCSkillCategory,
LCAdmin,
LCModal,
},
computed: mapState({
skills: (state) => state.career.skills,
}),
mixins: [Auth],
};
</script>
<style scoped>
...
</style>
This component load each skills category with the LCSkillCategory component when the data is present in the store.
LCSkillCategory.js
<template>
<div class="skillsCategory">
<h2 v-if="category">{{ name }}</h2>
<LCAdmin
v-if="IsAdmin && category"
:editModal="$refs.editModal"
:deleteModal="$refs.deleteModal"
/>
<LCModal ref="editModal"></LCModal>
<LCModal ref="deleteModal"></LCModal>
<div v-if="category">
<LCSkill
v-for="skill in category.skills"
:key="skill"
:skill="skill"
/>
</div>
<LCAdmin v-if="IsAdmin" :addModal="$refs.addSkillModal" />
<LCModal ref="addSkillModal"></LCModal>
</div>
</template>
<script>
import LCSkill from './LCSkill.vue';
import { mapState } from 'vuex';
import LCAdmin from '../LCAdmin.vue';
import LCModal from '../LCModal.vue';
import Auth from '../../mixins/Auth';
export default {
name: 'LCSkillCategory',
components: { LCSkill, LCAdmin, LCModal },
props: ['category'],
mixins: [Auth],
computed: mapState({
name: function() {
return this.$store.getters['locale/getLocalizedValue']({
src: this.category,
attribute: 'name',
});
},
}),
};
</script>
<style scoped>
...
</style>
And so each category load a LCSkill component for each skill of this category.
<template>
<div class="skill-item">
<img :src="img(skill.icon.hash, 30, 30)" />
<p>{{ name }}</p>
<LCAdmin
v-if="IsAdmin"
:editModal="$refs.editModal"
:deleteModal="$refs.deleteModal"
/>
<LCModal ref="editModal"></LCModal>
<LCModal ref="deleteModal"></LCModal>
</div>
</template>
<script>
import LCImageRendering from '../../mixins/LCImageRendering';
import { mapState } from 'vuex';
import Auth from '../../mixins/Auth';
import LCAdmin from '../LCAdmin.vue';
import LCModal from '../LCModal.vue';
export default {
name: 'LCSkill',
mixins: [LCImageRendering, Auth],
props: ['skill'],
components: { LCAdmin, LCModal },
computed: mapState({
name: function() {
return this.$store.getters['locale/getLocalizedValue']({
src: this.skill,
attribute: 'name',
});
},
}),
};
</script>
<style scoped>
...
</style>
Then, the component with the button that is added everywhere :
LCAdmin.js
<template>
<div class="lc-admin">
<button v-if="addModal" #click="addModal.openModal()">
<i class="fas fa-plus"></i>
</button>
<button v-if="editModal" #click="editModal.openModal()">
<i class="fas fa-edit"></i>
</button>
<button v-if="deleteModal" #click="deleteModal.openModal()">
<i class="fas fa-trash"></i>
</button>
</div>
</template>
<script>
export default {
name: 'LCAdmin',
props: ['addModal', 'editModal', 'deleteModal'],
};
</script>
Again and I'm sorry it's not that I haven't look for a solution by myself, it's just that I don't know what to lookup for... And I'm also sorry for the very long post...
By the way, if you have some advice about how it is done and how I can improve it, feel free, Really. That how I can learn to do better !
EDIT :: ADDED The Store Code
Store Career Module
import { getCareer, getSkills } from '../../services/CareerService';
const state = () => {
// eslint-disable-next-line no-unused-labels
careerPath: [];
// eslint-disable-next-line no-unused-labels
skills: [];
};
const actions = {
async getCareerPath ({commit}) {
getCareer().then(response => {
commit('setCareerPath', response);
}).catch(err => console.log(err));
},
async getSkills ({commit}) {
getSkills().then(response => {
commit('setSkills', response);
}).catch(err => console.log(err));
}
};
const mutations = {
async setCareerPath(state, careerPath) {
state.careerPath = careerPath;
},
async setSkills(state, skills) {
state.skills = skills;
}
}
export default {
namespaced: true,
state,
actions,
mutations
}
Career Service
export async function getCareer() {
const response = await fetch('/api/career');
return await response.json();
}
export async function getSkills() {
const response = await fetch('/api/career/skill');
return await response.json();
}
Then App.vue, created() :
created() {
this.$store.dispatch('config/getConfigurations');
this.$store.dispatch('certs/getCerts');
this.$store.dispatch('career/getSkills');
this.$store.dispatch('projects/getProjects');
},
Clues
It seems that if I remove the v-if on the buttons of the LCAdmin, the button are shown as expected except that they all show even when I don't want them to. (If no modal are associated)
Which give me this result :
Problem is that refs are not reactive
$refs are only populated after the component has been rendered, and they are not reactive. It is only meant as an escape hatch for direct child manipulation - you should avoid accessing $refs from within templates or computed properties.
See simple demo below...
const vm = new Vue({
el: "#app",
components: {
MyComponent: {
props: ['modalRef'],
template: `
<div>
Hi!
<button v-if="modalRef">Click!</button>
</div>`
}
},
data() {
return {
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<my-component :modal-ref="$refs.modal"></my-component>
<div ref="modal">I'm modal placeholder</div>
</div>
The solution is to not pass $ref as prop at all. Pass simple true/false (which button to display). And on click event, $emit the event to the parent and pass the name of the ref as string...

VueJS: Not printing data returned in method

I'm successfully getting data into the console. When I try to print that data to the page by calling the method in double moustache braces it doesn't appear on screen. All other data in template appears just fine.
Template:
<template>
<div>
<div v-for="data in imageData" :key="data.id">
<div class="card">
<img :src="data.source" :alt="data.caption" class="card-img" />
<div class="text-box">
<p>{{ moment(data.timestamp.toDate()).format("MMM Do YYYY") }}</p>
<p>{{ data.caption }}</p>
// The Geocoding method is the problem
<p>{{reverseGeocode(data.location.df, data.location.wf)}}</p>
</div>
</div>
</div>
</div>
</template>
Method:
methods: {
reverseGeocode: (lat, long) => {
fetch(`https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${long}&key=API_KEY&result_type=locality`
).then((res) =>
res.json().then((data) => {
console.log(data.results[0].formatted_address); // works fine
return data.results[0].formatted_address;
})
);
},
},
Here's the image data I'm getting in props
Your problem is a common problem when you start making requests in JavaScript.
The date requests are asynchronous so the method cannot return a value after the execution of the method has finished.
Imagine the following call stack:
Start method.
Throw fetch. <- Asynchronous
Finish method.
Fetch ends.
You are trying to do a return in step 4 and it should be in 3.
To solve this you should use async with await. You could also solve it by making a component and passing the data (this is my favorite since you are using vue).
Component parent
<template>
<div>
<component-card v-for="data in imageData" :key="data.id" :dataItem="data">
</component-card>
</div>
</template>
Child component
<template>
<div class="card">
<img :src="dataItem.source" :alt="dataItem.caption" class="card-img" />
<div class="text-box">
<p>{{ moment(dataItem.timestamp.toDate()).format("MMM Do YYYY") }}</p>
<p>{{ dataItem.caption }}</p>
<p>{{formattedAddress}}</p>
</div>
</div>
</template>
<script>
export default {
props: {
dataItem: {
type: {},
default: () => ({})
}
},
data() {
return {
formattedAddress: ""
};
},
created() {
this.reverseGeocode(this.dataItem.location.df, dataItem.location.wf)
},
methods: {
reverseGeocode(lat, long) {
fetch(
`https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${long}&key=API_KEY&result_type=locality`
).then(res =>
res.json().then(data => {
console.log(data.results[0].formatted_address); // works fine
this.formattedAddress = data.results[0].formatted_address;
})
);
}
}
};
</script>
I have not tried it, surely some things are missing but the template should be that.
The above I think is correct as well, but I would push for async
async reverseGeocode(lat, long) {
const response = await fetch(
`https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${long}&key=API_KEY&result_type=locality`
);
const data = response.json();
return data.results[0].formatted_address;
}
You should change your approach to the following:
Do all requests in the created() lifecycle method and store the results in a data attribute then iterate over the data attribute. The created() lifecycle method executes before the DOM is mounted so all data fetching APIs should be called there. FYR: https://v2.vuejs.org/v2/guide/instance.html
Please also refer to Vue.js - Which component lifecycle should be used for fetching data?