Component stays visible when using props in composition API - vue.js

I have a vue 3 app which display product information. The home component displays all available products. Each of them has a button which takes me to a component displaying information about said product called Single Box. It gets the product ID from the route params and then fetches additional information via graphql. It looks like this:
<template>
<BoxDisplay :box="box"></BoxDisplay>
</template>
<script>
import { useQuery, useResult } from '#vue/apollo-composable';
import { useRoute } from 'vue-router'
import singleBoxQuery from '../graphql/singleBox.query.gql'
import BoxDisplay from '../components/BoxDisplay'
export default {
components: {
BoxDisplay
},
setup() {
const route = useRoute();
const boxId = route.params.id
const {result } = useQuery(singleBoxQuery, {id: boxId})
const box = useResult(result, null)
return {
boxId,
box
}
}
}
</script>
The BoxDisplay component is then used to show the infos of the current box. There is going to be more going on this page later so the BoxDisplay is needed. It looks like this:
<template>
<div class="p-d-flex p-jc-center">
<div v-if="box">
<h1>{{box.Name}}</h1>
<div v-html="myhtml"></div>
</div>
<div v-else>
<ProgressSpinner />
</div>
</div>
</template>
<script>
export default {
props: {
box: {}
},
setup(props){
const myhtml = "<h1>hello</h1>" + props.box.Name
return {
myhtml
}
}
}
</script>
The problem is now, that as soon as I use props in setup() the component stay visible when I click back in my browser. It stays there until I refresh the page. What am I doing wrong?

Use ref and computed
https://v3.vuejs.org/guide/composition-api-introduction.html#reactive-variables-with-ref
```
import { ref, computed } from 'vue'
```
const myhtml = computed(() => '<h1>hello</h1>' + props.box.Name)
or
const myhtml = ref('<h1>hello</h1>' + props.box.Name)

Related

Passing a value via prop, editing it and saving the new value vue 3

I am trying to pass a value into a child component. The child component will then preform the save operation. The parent doesn't need to know anything about it. I am able to pass in the object but not save its updated form.
Parent
<template>
<div v-show="isOpened">
<EditModal #toggle="closeModal" #update:todo="submitUpdate($event)"
:updatedText="editText" :todo="modalPost" />
</div>
</template>
<script setup lang="ts">
import Post from "../components/Post.vue";
import { api } from "../lib/api";
import { ref } from "vue";
import { onMounted } from "vue-demi";
import EditModal from "../components/EditModal.vue";
const postArr = ref('');
const message = ref('');
let isOpened = ref(false);
let modalPost = ref('');
let editText = ref('');
function closeModal() {
isOpened.value = false
}
function openModal(value: string) {
isOpened.value = true
modalPost.value = value
}
// call posts so the table loads updated item
function submitUpdate(value: any) {
console.log("called update in parent " + value)
editText.value = value
posts()
}
</script>
Child EditModal
<template>
<div>
<div>
<textarea id="updateTextArea" rows="10" :value="props.todo.post"></textarea>
</div>
<!-- Modal footer -->
<div>
<button data-modal-toggle="defaultModal" type="button"
#click="update(props.todo.blogId, props.todo.post)">Save</button>
</div>
</div>
</template>
<script lang="ts" setup>
import { api } from "../lib/api";
import { reactive, ref } from "vue";
const props = defineProps({
todo: String,
updatedText: String,
})
const emit = defineEmits(
['toggle','update:todo']
);
function setIsOpened(value: boolean) {
emit('toggle', value);
}
function update(id: string, value: string) {
console.log('the value ' + value)
try {
api.updateBlog(id, value).then( res => {
emit('update:todo', value)
emit('toggle', false);
})
} catch (e) {
console.log('Error while updating post: '+ e)
}
}
</script>
I know the props are read only, therefore I tried to copy it I can only have one model.
I do not see the reason I should $emit to the parent and pass something to another variable to pass back to the child.
I am trying to pass in text to a modal component where it can edit the text and the child component saves it.
Advice?
First, your component should be named EditTodo no EditModal because you are not editing modal. All Edit components should rewrite props to new local variables like ref or reactive, so you can work on them, they won't be read only any more.
Child EditTodo.vue
<template>
<div>
<div>
<textarea id="updateTextArea" rows="10" v-model="data.todo.title"></textarea>
</div>
<div>
<button data-modal-toggle="defaultModal" type="button" #click="update()">Save</button>
</div>
</div>
</template>
<script setup lang="ts">
import { api } from "../lib/api";
import { reactive } from "vue";
const props = defineProps<{ id: number, todo: { title: string} }>()
const emit = defineEmits(['toggle']);
const data = reactive({ ...props })
// I assuming api.updateBlog will update data in database
// so job here should be done just need toogle false modal
// Your array with todos might be not updated but here is
// no place to do that. Well i dont know how your API works.
// Database i use will automaticly update arrays i updated objects
function update() {
try {
api.updateBlog(data.id, data.todo ).then(res => {
emit('toggle', false);
})
}
}
</script>
Parent
<template>
<div>
<BaseModal v-if="todo" :show="showModal">
<EditTodo :id="todo.id" :todo="todo.todo" #toggle="(value) => showModal = value"></EditTodo>
</BaseModal>
</div>
</template>
<script setup lang="ts">
const showModal = ref(false)
const todo = reactive({ id: 5, todo: { title: "Todo number 5"} })
</script>
I separated a modal object with edit form, so you can create more forms and use same modal. And here is a simple, not fully functional modal.
<template>
<div class="..."><slot></slot></div>
</template>
<script setup>
defineProps(['show'])
defineEmits(['toogle'])
</script>
You might want to close modal when user click somewhere outside of modal.

How to test "errorComponent" in "defineAsyncComponent" in Vue?

I was learning about Async Components in Vue. Unfortunately in that documentation Vue did not show any example of using Async Components in the <template> part of a Vue SFC. So after searching on the web and reading some articles like this one and also this one, I tried to use this code to my Vue component:
<!-- AsyncCompo.vue -->
<template>
<h1>this is async component</h1>
<button #click="show = true">login show</button>
<div v-if="show">
<LoginPopup></LoginPopup>
</div>
</template>
<script>
import { defineAsyncComponent, ref } from 'vue';
import ErrorCompo from "#/components/ErrorCompo.vue";
const LoginPopup = defineAsyncComponent({
loader: () => import('#/components/LoginPopup.vue'),
/* -------------------------- */
/* the part for error handling */
/* -------------------------- */
errorComponent: ErrorCompo,
timeout: 10
}
)
export default {
components: {
LoginPopup,
},
setup() {
const show = ref(false);
return {
show,
}
}, // end of setup
}
</script>
And here is the code of my Error component:
<!-- ErrorCompo.vue -->
<template>
<h5>error component</h5>
</template>
Also here is the code of my Route that uses this component:
<!-- test.vue -->
<template>
<h1>this is test view</h1>
<AsyncCompo></AsyncCompo>
</template>
<script>
import AsyncCompo from '../components/AsyncCompo.vue'
export default {
components: {
AsyncCompo
}
}
</script>
And finally the code of my actual Async component called LoginPopup.vue that must be rendered after clicking the button:
<!-- LoginPopup.vue -->
<template>
<div v-if="show1">
<h2>this is LoginPopup component</h2>
<p>{{retArticle}}</p>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
export default {
setup() {
const getArticleInfo = async () => {
// wait 3 seconds to mimic API call
await new Promise(resolve => setTimeout(resolve, 3000));
const article = "my article"
return article
}
const show1 = ref(false);
const retArticle = ref(null);
onMounted(
async () => {
retArticle.value = await getArticleInfo();
show1.value = true;
}
);
return {
retArticle,
show1
}
}
}
</script>
When I comment the part below from AsyncCompo.vue everything works correctly and my component loads after 3s when I clicks the button:
errorComponent: ErrorCompo,
timeout: 10
But I want to test the error situation that Vue says in my component. I am not sure that my code implementation is absolutely true, but with code above when I use the errorComponent, I receive this warning and error in my console:
I also know that we could handle these situations with <Suspense> component, but because my goal is learning Async Components, I don't want to use them here. Could anyone please help me that how I can see and test my "error component" in the page? is my code wrong or I must do something intentionally to make an error? I don't know but some articles said that with decreasing timeout option I could see error component, but for me it gives that error.

Is there a way to share reactive data between random components in Vue 3 Composition API?

Having some reactive const in "Component A," which may update after some user action, how could this data be imported into another component?
For example:
const MyComponent = {
import { computed, ref } from "vue";
setup() {
name: "Component A",
setup() {
const foo = ref(null);
const updateFoo = computed(() => foo.value = "bar");
return { foo }
}
}
}
Could the updated value of 'foo' be used in another Component without using provide/inject?
I am pretty new in the Vue ecosystem; kind apologies if this is something obvious that I am missing here.
One of the best things about composition API is that we can create reusable logic and use that all across the App. You create a composable functions in which you can create the logic and then import that into the components where you want to use it. Not only does this make your component much cleaner but also your APP much more maintainable. Below is a simple example of counter to show how they can be used. You can find working demo here:
Create a composable function for counter:
import { ref, computed } from "vue";
const counter = ref(0);
export const getCounter = () => {
const incrementCounter = () => counter.value++;
const decrementCounter = () => counter.value--;
const counterPositiveOrNegitive = computed(() =>
counter.value >= 0 ? " Positive" : "Negitive"
);
return {
counter,
incrementCounter,
decrementCounter,
counterPositiveOrNegitive
};
};
Then you can import this function into your components and get the function or you want to use. Component to increment counter.
<template>
<div class="hello">
<h1>Component To Increment Counter</h1>
<button #click="incrementCounter">Increment</button>
</div>
</template>
<script>
import { getCounter } from "../composables/counterExample";
export default {
name: "IncrementCounter",
setup() {
const { incrementCounter } = getCounter();
return { incrementCounter };
},
};
</script>
Component to decrement counter:
<template>
<div class="hello">
<h1>Component To Decrement Counter</h1>
<button #click="decrementCounter">Decrement</button>
</div>
</template>
<script>
import { getCounter } from "../composables/counterExample";
export default {
name: "DecrementCounter",
setup() {
const { decrementCounter } = getCounter();
return { decrementCounter };
},
};
</script>
Then in the main component, you can show the counter value.
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<div class="counters">
<IncrementCounter />
<DecrementCounter />
</div>
<h3>Main Component </h3>
<p>Counter: {{ counter }}</p>
<p>{{ counterPositiveOrNegitive }}</p>
</template>
<script>
import IncrementCounter from "./components/IncrementCounter.vue";
import DecrementCounter from "./components/DecrementCounter.vue";
import { getCounter } from "./composables/counterExample";
export default {
name: "App",
components: {
IncrementCounter: IncrementCounter,
DecrementCounter: DecrementCounter,
},
setup() {
const { counter, counterPositiveOrNegitive } = getCounter();
return { counter, counterPositiveOrNegitive };
},
};
Hope this was somewhat helpful. You can find a working example here:
https://codesandbox.io/s/vue3-composition-api-blfpj

How to get this.$el of a component correctly, in Vue3?

I have a component, its template looks like this:
<template>
<slot />
</template>
The function I'm trying to make, is about resize-observer-polyfill.
I'm trying to get this.$el in mounted(), then create an instance of ResizeObserver.
But the problem is, if the content of slot is an async-component, then I cannot get this.$el correctly.
How can I fix this?
In Vue 3, you can use a template ref:
<template>
<div ref="el"></div>
</template>
import { ref, onMounted } from 'vue'
export default {
setup() {
const el = ref(null);
// Not available until the component mounts:
onMounted(() => {
console.log(el.value)
})
return {
el
}
}
}

VueJs / Vuex : Rendering list of items

I'm trying to render a list of offers from my vuex store. The problem is when i'm loading my list of offers page, they are not rendered.
Here is my code :
offers.js
export const namespaced = true
export const state = {}
export const mutations = {
[types.UPDATE_OFFERS] (state, offers) {
Object.assign(state, offers)
}
}
export const actions = {
async fetchOffers ({ commit }) {
const { data } = await axios.get('/api/offers')
commit(types.UPDATE_OFFERS, data)
}
}
offers.vue (my page component)
<template>
<div>
<div v-if="offers"
v-for="offer in offers"
:key="offer.id">
<div>
<div>
<router-link :to="{ name: 'offer', params: { offer: offer.id } }">
{{ offer.project }}
</router-link>
</div>
<div>
<div v-if="offer.versions[0] && offer.versions[0].status == 'edition'">
Validate the offer
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
export default {
computed: {
...mapState(['offers'])
},
beforeMount () {
this.$store.dispatch('offers/fetchOffers')
}
}
</script>
When i'm loading the page, I can see that the store as loaded the offers but the page doesnt render them. The weird thing is when I load the page offers then another page (example createAnOffer) and then I come back to offers, it renders the offers proplery.
I tried beforemount, beforeCreate, mounted, created.
I admit I have no clue what's going on.
Any tip ?
Thank you for your answers.
Louis