Vue.js composition API <script setup> - how to two-way bind props - javascript

I am struggling to find a way how to create two-way props binding between parent and child components using Vue.js3. Below you can find an example of my code. Unfortunately, the "emit" function does nothing at this point. Thank you for your help.
ParentComponent.vue
<template>
<child-component :loading="isChildLoading">
{{ isChildLoading ? 'Child component is loading' : 'Child component is NOT loading' }}
</template>
<script setup>
import {ref} from "vue";
import ChildComponent from 'ChildComponent';
const isChildLoading = ref(false);
</script>
ChildComponent.vue
<script setup>
import {defineProps, defineEmits, onMounted} from "vue";
const props = defineProps({
loading: Boolean
});
const emit = defineEmits(['update:loading']);
onMounted(() => {
emit('update:loading', true)
//After some kind of XMLHttpRequest request set loading to 'false'
setTimeout(() => {
emit('update:loading', false)
}, 5000);
});
</script>

The emit does nothing because you are not listening for the event. You need to do something like this:
<child-component :loading="isChildLoading" #update:loading="isChildLoading = $event">
You can also setup v-model binding, but it is probably not the correct way here:
https://vuejs.org/guide/components/events.html#usage-with-v-model
Btw.: You don't need to import defineProps or defineEmits since those are compiler macros and automatically available inside the <setup setup> block (https://vuejs.org/api/sfc-script-setup.html#defineprops-defineemits)

Related

Conditional template rendering with props in Vue3?

I'm trying to make a toggle button so that a hamburger menu opens when clicked.
I made the boolean "clicked" property in "App.vue", passed it down to "Navbar.vue", and now I want to be able to click in the navbar to toggle the "clicked" property to "true" or "false" to make the backdrop and drawer show or not show.
I tried to use an "emit", and it seems to work, but the template isn't responding to the "clicked" variable and is showing even though it's false.
In the code below, what part did I get wrong? How do you implement conditional rendering with props? Can someone help?
App.vue
<template>
<NavBar :clicked="clicked" #toggleDrawer="toggleMenu()" />
<BackDrop :clicked="clicked" />
<SideDrawer :clicked="clicked" />
<router-view></router-view>
</template>
<script>
export default {
name: "App",
components: { NavBar, BackDrop, SideDrawer },
setup() {
const clicked = ref(false);
const toggleMenu = () => {
clicked.value = !clicked.value;
};
return { clicked, toggleMenu };
},
};
</script>
NavBar.vue
<template>
<nav class="navbar">
/* MORE CODE */
<div class="hamburger_menu" #click="toggleEvent">
<div></div>
<div></div>
<div></div>
</div>
</nav>
</template>
<script setup>
import { defineEmits, defineProps } from "vue";
const props = defineProps({
clicked: Boolean,
});
const emit = defineEmits(["toggleDrawer"]);
const toggleEvent = () => {
console.log("toggleEvent running");
emit("toggleDrawer", !props.clicked);
};
</script>
Backdrop.vue
<template v-if="props.clicked">
<div class="backdrop"></div>
</template>
<script setup>
import { defineProps } from "vue";
// eslint-disable-next-line no-unused-vars
const props = defineProps({
clicked: Boolean,
});
</script>
SideDrawer.vue
<template v-if="props.clicked">
<div class="sidedrawer"></div>
</template>
<script setup>
import { defineProps } from "vue";
const props = defineProps({
clicked: Boolean,
});
</script>
Am I passing in the prop wrong? Does "props.clicked" not work in "v-if"'s or templates? How should I implement the "v-if" with the "clicked" property I have?
As #neha-soni said,
After running the code, it is working fine
Vue recommends to use kebab-cased event listeners in templates. Your toggleDrawer will auto-converted into kebab case when you use it in the parent component. So In app.vue you can use it like #toggle-drawer,
<NavBar :clicked="clicked" #toggle-drawer="toggleMenu()" />
From vue doc link
event names provide an automatic case transformation. Notice we emitted a camelCase event, but can listen for it using a kebab-cased listener in the parent. As with props casing, we recommend using kebab-cased event listeners in templates.
After running the code, it is working fine. I have a few feedbacks to remove unnecessary code which is creating confusion and then you can see its working.
Because props are immutable (read-only) in the child component that means their value will not be changed so there is no point to pass props value back (by doing emit("toggleDrawer", !props.clicked)) to the parent because the parent already has their original status.
Another point is, you are passing the data (props data) from the event by doing emit("toggleDrawer", !props.clicked) but not using it when calling the function #toggleDrawer="toggleMenu()" in App.vue, so better to remove this data passing code.
The clicked property is updating in the parent (App.vue) as well as inside the child components. Just console and print the clicked property in the child and parent template like {{ clicked }} at the top and you can see the updated status-
const toggleMenu = () => {
console.log('Before______', clicked.value)
clicked.value = !clicked.value;
console.log('After______', clicked.value)
};

onActivated wont trigger in Quasar (vue3) component

For some reason the onActivated function in vue3 wont trigger and cannot figure out why. It is not the first time I am implementing this but for some reason nothing happens. I am using vue 3.0.0. Here is my code and component
Index.vue
<template>
<MyComp />
</template>
<script setup>
import MyComp from "../components/MyComp.vue";
</script>
MyComp.vue
<template>
<div>{{ el }}</div>
</template>
<script setup>
import { ref, onMounted, onActivated } from "vue";
const el = ref("Hello");
onMounted(() => {
console.log("onMounted");
});
onActivated(() => {
console.log("onActivated");
});
</script>
onActivated only triggers for components inside a KeepAlive tag, but you don't have any KeepAlive tags in the code you posted, so onMounted should be all you need. See https://vuejs.org/api/composition-api-lifecycle.html#onactivated

How to bind TipTap to parent v-model in Vue 3 script setup?

I'm trying to use tiptap as a child component and pass its content to the parent's v-model but tiptap's documentation only seems to provide info on how to do this without script setup, which uses a different API.
This is my parent component:
<template>
<cms-custom-editor v-model:modelValue="state.content"/>
<p>{{state.content}}</p>
</template>
<script setup>
import CmsCustomEditor from '../../components/backend/cms-custom-editor.vue'
import {reactive} from "vue";
const state = reactive({
content: '<p>A Vue.js wrapper component for tiptap to use <code>v-model</code>.</p>',
})
</script>
and this the child component with tiptap:
<template>
<div id="cms-custom-editor" class="cms-custom-editor">
<editor-content :editor="editor"/>
</div>
</template>
<script 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({
extensions: [StarterKit],
content: props.modelValue,
onUpdate: () => {
emit('update:modelValue', editor.getHTML())
}
})
</script>
As soon as I type something into the editor field, this code line fails:
emit('update:modelValue', editor.getHTML())
and throws this error:
Uncaught TypeError: editor.getHTML is not a function
at Editor2.onUpdate (cms-custom-editor.vue?t=1654253729389:28:42)
at chunk-RCTGLYYN.js?v=89d16c61:11965:48
at Array.forEach (<anonymous>)
at Editor2.emit (chunk-RCTGLYYN.js?v=89d16c61:11965:17)
at Editor2.dispatchTransaction (chunk-RCTGLYYN.js?v=89d16c61:12252:10)
at EditorView.dispatch (chunk-RCTGLYYN.js?v=89d16c61:9138:27)
at readDOMChange (chunk-RCTGLYYN.js?v=89d16c61:8813:8)
at DOMObserver.handleDOMChange (chunk-RCTGLYYN.js?v=89d16c61:8924:77)
at DOMObserver.flush (chunk-RCTGLYYN.js?v=89d16c61:8575:12)
at DOMObserver.observer (chunk-RCTGLYYN.js?v=89d16c61:8455:14)
I've used the approach from the docs (chapter 5. v-model), which like I said, is not designed for script setup.
Man, the docs are confusing. They mix up standard composition api and script setup. Anyway this is how it works:
const editor = useEditor({
extensions: [StarterKit],
content: props.modelValue,
onUpdate: ({editor}) => {
emit('update:modelValue', editor.getHTML())
}
})

onUpdated is not called with vuejs 3 with render function

I am facing a problem using the onUpdated lifecycle hook in Vuejs3 Composition API.
It is not called when a reactive value is updated.
I have reproduced the issue with a very simple app.
It has one child component:
<script setup lang="ts">
import { h, ref, onUpdated } from 'vue'
const open = ref(false)
const toggle = () => {
open.value = !open.value
}
defineExpose({ toggle })
onUpdated(() => {
console.log("Updated")
})
const render = () =>
open.value ? h(
'DIV',
"Child Component"
) :
null
</script>
<template>
<render />
</template>
Then this component is used by the app:
<script setup lang="ts">
import { ref } from 'vue'
import Child from './components/Child.vue'
const menu = ref(null)
</script>
<template>
<main>
<button #click="menu.toggle()">Click Me</button>
<Child ref="menu" />
</main>
</template>
<style>
</style>
But when the button is clicked in the app, although the "Child Component" text is shown, proving that the render function is called, the onUpdated callback is not executed.
This must have something to do with the way the render function is called, or the conditional rendering because if I use v-if in a template instead, it works fine. But in my case, I do need an explicit render function.
Can anyone help?
This is probably a bug in <script setup>, as that same code works in setup().
A workaround is to switch to setup() if you need to use onUpdated():
<script lang="ts">
import { h, ref, onUpdated } from 'vue'
export default {
setup(props, { expose }) {
const open = ref(false)
const toggle = () => {
open.value = !open.value
}
expose({ toggle })
onUpdated(() => {
console.log("Updated")
})
const render = () => open.value ? h('DIV', "Child Component") : null
return render
}
}
</script>
demo

Pass data attribute to vue 3 root instance [duplicate]

I am terribly new to Vue, so forgive me if my terminology is off. I have a .NET Core MVC project with small, separate vue pages. On my current page, I return a view from the controller that just has:
#model long;
<div id="faq-category" v-bind:faqCategoryId="#Model"></div>
#section Scripts {
<script src="~/scripts/js/faqCategory.js"></script>
}
Where I send in the id of the item this page will go grab and create the edit form for. faqCategory.js is the compiled vue app. I need to pass in the long parameter to the vue app on initialization, so it can go fetch the full object. I mount it with a main.ts like:
import { createApp } from 'vue'
import FaqCategoryPage from './FaqCategoryPage.vue'
createApp(FaqCategoryPage)
.mount('#faq-category');
How can I get my faqCategoryId into my vue app to kick off the initialization and load the object? My v-bind attempt seems to not work - I have a #Prop(Number) readonly faqCategoryId: number = 0; on the vue component, but it is always 0.
My FaqCategoryPAge.vue script is simply:
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { Prop } from 'vue-property-decorator'
import Card from "#/Card.vue";
import axios from "axios";
import FaqCategory from "../shared/FaqCategory";
#Options({
components: {
Card,
},
})
export default class FaqCategoryPage extends Vue {
#Prop(Number) readonly faqCategoryId: number = 0;
mounted() {
console.log(this.faqCategoryId);
}
}
</script>
It seems passing props to root instance vie attributes placed on element the app is mounting on is not supported
You can solve it using data- attributes easily
Vue 2
const mountEl = document.querySelector("#app");
new Vue({
propsData: { ...mountEl.dataset },
props: ["message"]
}).$mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app" data-message="Hello from HTML">
{{ message }}
</div>
Vue 3
const mountEl = document.querySelector("#app");
Vue.createApp({
props: ["message"]
}, { ...mountEl.dataset }).mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0/vue.global.js"></script>
<div id="app" data-message="Hello from HTML">
{{ message }}
</div>
Biggest disadvantage of this is that everything taken from data- attributes is a string so if your component expects something else (Number, Boolean etc) you need to make conversion yourself.
One more option of course is pushing your component one level down. As long as you use v-bind (:counter), proper JS type is passed into the component:
Vue.createApp({
components: {
MyComponent: {
props: {
message: String,
counter: Number
},
template: '<div> {{ message }} (counter: {{ counter }}) </div>'
}
},
}).mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0/vue.global.js"></script>
<div id="app">
<my-component :message="'Hello from HTML'" :counter="10" />
</div>
Just an idea (not a real problem)
Not really sure but it can be a problem with Props casing
HTML attribute names are case-insensitive, so browsers will interpret any uppercase characters as lowercase. That means when you're using in-DOM templates, camelCased prop names need to use their kebab-cased (hyphen-delimited) equivalents
Try to change your MVC view into this:
<div id="faq-category" v-bind:faq-category-id="#Model"></div>
Further to Michal LevĂ˝'s answer regarding Vue 3, you can also implement that pattern with a Single File Component:
app.html
<div id="app" data-message="My Message"/>
app.js
import { createApp } from 'vue';
import MyComponent from './my-component.vue';
const mountEl = document.querySelector("#app");
Vue.createApp(MyComponent, { ...mountEl.dataset }).mount("#app");
my-component.vue
<template>
{{ message }}
</template>
<script>
export default {
props: {
message: String
}
};
</script>
Or you could even grab data from anywhere on the parent HTML page, eg:
app.html
<h1>My Message</h1>
<div id="app"/>
app.js
import { createApp } from 'vue';
import MyComponent from './my-component.vue';
const message = document.querySelector('h1').innerText;
Vue.createApp(MyComponent, { message }).mount("#app");
my-component.vue
<template>
{{ message }}
</template>
<script>
export default {
props: {
message: String
}
};
</script>
To answer TheStoryCoder's question: you would need to use a data prop. My answers above demonstrate how to pass a value from the parent DOM to the Vue app when it is mounted. If you wanted to then change the value of message after it was mounted, you would need to do something like this (I've called the data prop myMessage for clarity, but you could also just use the same prop name message):
<template>
{{ myMessage }}
<button #click="myMessage = 'foo'">Foo me</button>
</template>
<script>
export default {
props: {
message: String
},
data() {
return {
myMessage: this.message
}
}
};
</script>
So I'm not at all familiar with .NET and what model does, but Vue will treat the DOM element as a placeholder only and it does not extend to it the same functionality as the components within the app have.
so v-bind is not going to work, even without the value being reactive, the option is not there to do it.
you could try a hack to access the value and assign to a data such as...
const app = Vue.createApp({
data(){
return {
faqCategoryId: null
}
},
mounted() {
const props = ["faqCategoryId"]
const el = this.$el.parentElement;
props.forEach((key) => {
const val = el.getAttribute(key);
if(val !== null) this[key] = (val);
})
}
})
app.mount('#app')
<script src="https://unpkg.com/vue#3.0.0-rc.11/dist/vue.global.prod.js"></script>
<div id="app" faqCategoryId="12">
<h1>Faq Category Id: {{faqCategoryId}}</h1>
</div>
where you get the value from the html dom element, and assign to a data. The reason I'm suggesting data instead of props is that props are setup to be write only, so you wouldn't be able to override them, so instead I've used a variable props to define the props to look for in the dom element.
Another option
is to use inject/provide
it's easier to just use js to provide the variable, but assuming you want to use this in an mvc framework, so that it is managed through the view only. In addition, you can make it simpler by picking the exact attributes you want to pass to the application, but this provides a better "framework" for reuse.
const mount = ($el) => {
const app = Vue.createApp({
inject: {
faqCategoryId: {
default: 'optional'
},
},
})
const el = document.querySelector($el)
Object.keys(app._component.inject).forEach(key => {
if (el.getAttribute(key) !== null) {
app.provide(key, el.getAttribute(key))
}
})
app.mount('#app')
}
mount('#app')
<script src="https://unpkg.com/vue#3.0.0-rc.11/dist/vue.global.prod.js"></script>
<div id="app" faqCategoryId="66">
<h1>Faq Category Id: {{faqCategoryId}}</h1>
</div>
As i tried in the following example
https://codepen.io/boussadjra/pen/vYGvXvq
you could do :
mounted() {
console.log(this.$el.parentElement.getAttribute("faqCategoryId"));
}
All other answers might be valid, but for Vue 3 the simple way is here:
import {createApp} from 'vue'
import rootComponent from './app.vue'
let rootProps = {};
createApp(rootComponent, rootProps)
.mount('#somewhere')

Categories

Resources