vue3 update image src dynamically - vuejs3

I'm trying to update an image dynamically but it is not updated.
According to the doc I have a template:
<template>
<section class="relative">
<div class="">
<img ref="heroImg" class="" src='../images/hero-bg-01.jpg' width="1440" height="577" />
</div>
</section>
</template>
Now I would like to update the src of my img and I do:
import { ref, onMounted } from "vue";
export default {
name: 'HeroTestimonials',
props:["source", "dataV"],
setup(){
const heroImg = ref(null);
onMounted(() => {
const imgUrl = new URL('../images/hero-bg-02.jpg', import.meta.url).href;
heroImg.src = imgUrl;
console.log(heroImg);
})
return {heroImg}
},
}
Into the console I have the message:
{
"_rawValue": null,
"_shallow": false,
"__v_isRef": true,
"_value": null,
"src": "http://localhost:3001/src/images/hero-bg-02.jpg" }
No errors but the image is not updated yet.
What's wrong?
Thanks for any suggestion!
[EDIT] - I've added the line return {heroImg} which was missing.

If you want to make the src attribute dynamic, you must use a v-bind in front of the attribute, for exemple v-bind:src="yourVariableHere" or use the shorthand :src. (you can see more here : https://v3.vuejs.org/api/directives.html#v-bind)
In your exemple you should do something like that :
<img class="" :src='imgUrl' width="1440" height="577" />
Then, in your script section :
<script>
import { onMounted, ref } from 'vue';
export default {
name: 'App',
setup() {
const imgUrl = ref('../images/hero-bg-01.jpg')
onMounted(() => {
imgUrl.value = '../images/hero-bg-02.jpg';
})
return {
imgUrl
}
}
}
</script>
However I'm not sure about doing that in the onMounted hook because the image would get replaced instantly

Related

Vue3 composition API and pinia | submit data then reload the data

I'm learning Vue 3 composition API and Pinia. I'm making a todo.
When I submit a todo data through Pinia, I can submit to the DB, but it won't re-render until reload the page.
Do I need to use 'watch' to watch the state todos:[] and execute fetchTodos()?
any good solution?
here both codes, hope someone can help me. Thank you in advance.
----- VUE -----
<script setup>
import { ref, onMounted } from 'vue'
import { storeToRefs } = from 'pinia'
import { useTodoStore } from '../store/todo'
const store = useTodoStore()
const { getTodos } = storeToRefs(store)
onMounted(() => {
store.fetchTodos()
})
const todo = ref('')
const initForm = () => {
todo.value = ''
}
// submit via Pinia
const onSubmitToPinia = () => {
const payload = {
todo: todo.value,
}
store.addTodoFromPinia(payload)
initForm()
store.fetchTodo()
}
</script>
<template>
<h4>TODO</h4>
<!-- form addTodo -->
<form class="row g-4">
<div class="col-auto">
<input
class="form-control"
v-model="newName"
type="text"
placeholder="todo">
</div>
<div>
<button
class="btn btn-primary"
type="button"
#click="onSubmitToPinia(payload)">
submit through pinia</button>
</div>
</form>
<!-- render data from pinia -->
<div class="todo"
v-for="getTodo in getTodoss.todo"
:key="getTodo.id">
<b class="ms-2">{{ getTodo.todo }}</b>
</div>
</template>
---- PINIA ----
import { defineStore } from 'pinia'
import axios from "axios"
export const useAboutStore = defineStore('todo',{
state: () => {
return {
todos: []
}
},
getters: {
getTodos(state) {
return state.todos
}
},
actions: {
async fetchTodos() {
try {
const data = await axios.get('http://localhost:5000/todo')
this.todos = data.data
}
catch (error) {
alert(error)
console.log(error)
}
},
addTodoFromPinia(payload) {
const path = 'http://localhost:5000/todo'
axios.post(path, payload)
}
},
})
You don't need to use storeToRefs to accomplish what you want nor do you need to watch the state of the store.
<template>
<div class="todo"
v-for="getTodo in store.todos"
:key="getTodo.id">
<b class="ms-2">{{ getTodo.todo }}</b>
</div>
</template>
If for any reason the vue complains that the array is empty put a v-if checking if the store.todos.length is != 0.
And also fix your typos.
If the problem persists show me your new code and I help you again.

How to correctly reference HTMLElement in VueJs component

I have simple component, that wraps text area. And I've another simple component, that renders a button. I want to set focus to text area when clicking the button.
This simplified example fails:
<template>
<MyCommand #resize="testResize" />
<TextArea ref="refElement" />
</template>
<script lang="ts">
// ...
export default defineComponent({
name: 'SimpleComponent',
setup(props, context) {
const refElement = ref<HTMLElement | null>(null)
const testResize = () => {
console.log('resize test')
if (refElement.value !== null) {
refElement.value.focus()
}
}
return {
refElement,
testResize,
}
}
</script>
TextArea is very simple component, some input normalization, oversimplified:
<template>
<textarea v-model.trim="value" />
</template>
I get "resize test" in console, so testResize method is running, but refElement is null.
When referencing component, not a HTML element, component type should be DefineComponent instead of HTMLElement.
Wrapped element could be referenced through $el property:
<template>
<MyCommand #resize="testResize" />
<TextArea ref="refElement" />
</template>
<script lang="ts">
// ...
export default defineComponent({
name: 'SimpleComponent',
setup(props, context) {
const refElement = ref<DefineComponent>()
const testResize = () => {
console.log('resize test')
if (refElement.value) {
refElement.value.$el.focus()
}
}
return {
refElement,
testResize,
}
}
</script>
I'm not sure if this is the "best practice", it looks to me as a hack. If anyone knows better solution, please comment.
You're missing the refElement in your return
return {
testResize, refElement
}
Update
If you are dealing with a component it becomes a bit trickier. while you can use refElement.value.$el, I'd say it's not a good idea. This will only work if the component has the first child the textarea. This will make for a brittle implementation, where if you need to change that at some point, it will break. IMHO, you're better off passing the ref as a prop to the child component. This is also not best practice, because you're supposed to pass props down and emit events up, but that that would be quite the overhead to implement. Passing ref as a prop comes with it's own issues though. If you have a ref in the template, it gets automagicaly converted from the ref/propxy to a value. To get around that, you can pass the prop in a function refElement: () => refElement in the setup(can't do it template). Of course, YMMV, but this is the path I'd chose.
const app = Vue.createApp({
setup(props, context) {
const refElement = Vue.ref(null)
const testResize = () => {
if (refElement.value !== null) {
refElement.value.focus()
}
}
return {
testResize,
refElement: () => refElement
}
}
});
app.component("text-area", {
template: `<textarea ref="taRef"></textarea></div></div>`,
props: {
textarearef: {
type: Function
}
},
setup(props) {
const taRef = props.textarearef()
return {
taRef
}
}
})
app.mount("#app");
<script src="https://unpkg.com/vue#3.0.3/dist/vue.global.prod.js"></script>
<div id="app">
<text-area :textarearef="refElement"></text-area>
<button #click="testResize">🦄</button>
</div>

Dynamic component in Vue3 Composition API

A simple working example of a Vue2 dynamic component
<template>
<div>
<h1>O_o</h1>
<component :is="name"/>
<button #click="onClick">Click me !</button>
</div>
</template>
<script>
export default {
data: () => ({
isShow: false
}),
computed: {
name() {
return this.isShow ? () => import('./DynamicComponent') : '';
}
},
methods: {
onClick() {
this.isShow = true;
}
},
}
</script>
Everything works, everything is great. I started trying how it would work with the Composition API.
<template>
<div>
<h1>O_o</h1>
<component :is="state.name"/>
<button #click="onClick">Click me !</button>
</div>
</template>
<script>
import {ref, reactive, computed} from 'vue'
export default {
setup() {
const state = reactive({
name: computed(() => isShow ? import('./DynamicComponent.vue') : '')
});
const isShow = ref(false);
const onClick = () => {
isShow.value = true;
}
return {
state,
onClick
}
}
}
</script>
We launch, the component does not appear on the screen, although no errors are displayed.
You can learn more about 'defineAsyncComponent' here
https://labs.thisdot.co/blog/async-components-in-vue-3
or on the official website
https://v3.vuejs.org/api/global-api.html#defineasynccomponent
import { defineAsyncComponent, defineComponent, ref, computed } from "vue"
export default defineComponent({
setup(){
const isShow = ref(false);
const name = computed (() => isShow.value ? defineAsyncComponent(() => import("./DynamicComponent.vue")): '')
const onClick = () => {
isShow.value = true;
}
}
})
Here is how you can load dynamic components in Vue 3. Example of dynamic imports from the icons collection inside /icons folder prefixed with "icon-".
BaseIcon.vue
<script>
import { defineComponent, shallowRef } from 'vue'
export default defineComponent({
props: {
name: {
type: String,
required: true
}
},
setup(props) {
// use shallowRef to remove unnecessary optimizations
const currentIcon = shallowRef('')
import(`../icons/icon-${props.name}.vue`).then(val => {
// val is a Module has default
currentIcon.value = val.default
})
return {
currentIcon
}
}
})
</script>
<template>
<svg v-if="currentIcon" width="100%" viewBox="0 0 24 24" :aria-labelledby="name">
<component :is="currentIcon" />
</svg>
</template>
You don't need to use computed or watch. But before it loads and resolved there is nothing to render, this is why v-if used.
UPD
So if you need to change components (icons in my case) by changing props use watchEffect as a wrapper around the import function.
watchEffect(() => {
import(`../icons/icon-${props.name}.vue`).then(val => {
currentIcon.value = val.default
})
})
Don't forget to import it from vue =)
The component should be added to components option then just return it name using the computed property based on the ref property isShow :
components:{
MyComponent:defineAsyncComponent(() => import("./DynamicComponent.vue"))
},
setup(){
const isShow = ref(false);
const name = computed (() => isShow.value ? 'MyComponent': '')
const onClick = () => {
isShow.value = true;
}
}
Instead of string you should provide Component
<script setup>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script>
<template>
<component :is="Foo" />
<component :is="someCondition ? Foo : Bar" />
</template>

Vue.js how can i loop throw an array to add components to the dom according to array items

I am making an app that communicate with an api and fetch data,home page changes every day so i can't just add static components to it,
i need to create it according to the data that comes from the api.
i have a component for the home page called Home.vue
this component can have one or more Carousels depending on the data that i'am fetching.
i also have Carousel.vue which is responsible about displaying images and it had it's own props.
the question is :
How to add component to the dom from loop
this is Home.vue where i am making the loop :
<template>
<div>
<!--I Need The Loop right here-->
</div>
</template>
<script>
export default {
components: {},
data() {
return {
page_content: [],
widgets: [],
}
},
created() {
this.getHomeContent();
},
methods:
{
getHomeContent() {
window.axios.get(window.main_urls["home-content"]).then(response => {
this.page_content = JSON.parse(JSON.stringify(response.data));
console.log(this.page_content);
for (let index in this.page_content) {
switch (this.page_content[index].type) {
// if type is banner
case 'banner':
switch (this.page_content[index].display) {
// if display is carousel
case 'carousel':
console.log('carousel')
// end if display is carousel
this.widgets.push({
'type': 'Carousel',
'images': this.page_content[index].items,
})
}
// end if type is banner
}
}
});
}
}
}
</script>
and this is Carousel.vue which i need to be imported when needed with passing props :
<template>
<div>
<div >
<VueSlickCarousel>
<div v-for="image in images">
<img src="{{img}}">
</div>
</VueSlickCarousel>
</div>
</div>
</template>
<script>
import VueSlickCarousel from 'vue-slick-carousel'
import 'vue-slick-carousel/dist/vue-slick-carousel.css'
import 'vue-slick-carousel/dist/vue-slick-carousel-theme.css'
export default
{
components: {VueSlickCarousel},
name:'Carousel',
props:[
'images'
],
methods:
{
}
}
</script>
how to add Carousel.vue component to Home.vue dynamically some thing like:
if(data.display == 'carousel')
{
<carousel images="data.images"></carousel>
}
Import the component to your Home.vue :
import Carousel from './Carousel.vue'
export default {
components: {Carousel},
}
Then loop in your template:
<carousel v-for="(widget,index) in widgets" :key="index" :images="widget.images"/>
Best to use a widget.id rather than index for the key prop
This is the correct answer !
<template>
<div>
<template v-for="widget in widgets">
<div v-if="widget.type == 'carousel'" :key="widget.type">
<carousel
:images="widget.images"
:arrows ="widget.arrows"
:dots = "widget.dots"
>
</carousel>
</div>
</template>
</div>
</template>
<script>
import Carousel from './widgets/Carousel.vue'
export default {
components: {Carousel},
data() {
return {
page_content: [],
widgets: [],
}
},
created() {
this.getHomeContent();
},
methods:
{
getHomeContent() {
window.axios.get(window.main_urls["home-content"]).then(response => {
this.page_content = JSON.parse(JSON.stringify(response.data));
console.log(this.page_content);
for (let index in this.page_content) {
switch (this.page_content[index].type) {
// if type is banner
case 'banner':
switch (this.page_content[index].display) {
// if display is carousel
case 'carousel':
console.log('carousel')
// end if display is carousel
this.widgets.push({
'type': 'carousel',
'arrows':true,
'dots':true,
'images': this.page_content[index].items,
})
}
// end if type is banner
}
}
});
}
}
}
</script>

How to use a Meteor document object from container to react component?

I just want to pass an object document from the Container to my component and use it. The code of container is this:
import { Meteor } from 'meteor/meteor';
import { withTracker } from 'meteor/react-meteor-data';
import { Projects } from '/imports/api/projects.js';
import ProjectFormUpdate from './ProjectFormUpdate.jsx';
export default ProjectFormUpdateContainer = withTracker(({ key1 }) => {
Tracker.autorun(() => {
const sub = Meteor.subscribe('projects');
if (sub.ready()){
const oneProject = Projects.findOne(key1);
console.log(oneProject.nombre);
}})
return {
oneProject,
};
})(ProjectFormUpdate);
And i use it in my presentational component on this way:
render() {
const { oneProject, isLoading } = this.props;
if (!isLoading)
return (
<div className="col-xs-11">
<div className="box box-solid">
<form className="form" onSubmit={this.handleSubmit.bind(this)} >
<div className="box-body">
<div className="row">
<div className="col-xs-2">
<input
className = "form-control input-sm"
type="text"
ref="codigoInput"
placeholder="Código del Proyecto"
//THE PROBLEM HERE!!!!!
value = {this.props.oneProject.nombre}
onChange = {this.handleUpdate.bind(this)}
/>
</div>
...
But i get this error:
TypeError: Cannot read property 'nombre' of undefined
The problem is line:
//THE PROBLEM HERE!!!!!
value = {this.props.oneProject.nombre}
This will work fine once things have loaded. You need to return isLoading from the container component.
I would also recommend using oneProject by itself since you get it at the top of render in any case:
const { oneProject, isLoading } = this.props;

Resources