Vue render function causes maximum call stack - vue.js

So I am migrating application from vue 2 to vue 3 and now I am getting RangeError: Maximum call stack size exceeded. This is happening inside this template:
<div>
<comp-form :errors="form.$e" #submit="onSubmit">
content
</comp-form>
<div>
And I have this <comp-form> component which uses render() function.
render() {
return (
<form>
{this.$slots.default}
</form>
);
}
I managed to make error fix by changing render() function to <template> layout:
<template>
<form>
<slot/>
</form>
</template>
And this doesnt cause the error. But I want to keep render() function but I dont understand why it doesn't work with render but works with template layout.. Would appreciate some help. :)

Here is how to achieve a render function in Vue:
App.vue
<script setup>
import Comp from './Comp.vue'
</script>
<template>
<comp>
cool slot
</comp>
</template>
Comp.vue
<script>
import { h } from 'vue'
export default {
setup(props, { slots }) {
return () => h('form', slots.default())
}
}
</script>
Here is the doc if you want more details (+ the approach for JSX).

Related

Render slot as v-html (Vue 3)

Goal
How to implement a component that renders an html string (eg fetched from a CMS) passed as a slot like this :
// app.vue
<script setup>
import MyComponent from "./MyComponent.vue"
const htmlStr = `not bold <b>bold</b>`
</script>
<template>
<MyComponent>{{htmlStr}}</MyComponent>
</template>
Explanation
To render an html string (eg fetch from a CMS) we can use v-html :
// app.vue
<script setup>
const htmlStr = `not bold <b>bold</b>`
</script>
<template>
<p v-html="htmlStr"></p>
</template>
Failed attempts
I have tried with no success :
// component.vue
<script>
import { h } from "vue";
export default {
setup(props, { slots }) {
return () =>
h("p", {
innerHTML: slots.default(),
});
},
};
</script>
Renders
[object Object]
Link to playground
Workaround with props
As a workaround, we can of course use props but it's verbose.
// app.vue
<template>
<MyComponent :value="htmlStr">{{htmlStr}}</MyComponent>
</template>
// component.vue
<template>
<p v-html="value"></p>
</template>
<script setup>
import { defineProps } from 'vue'
defineProps(['value'])
</script>
slots.default() returns an array of your passed slot elements, try to map that content and render it :
h("p", {
innerHTML: slots.default().map(el=>el.children).join(''),
});
Playground

Vue 3, $emit never firing

The documentation is not enough to be able to do the emit. I have seen many tutorials and nothing works, now I am testing this
Child component
<div #click="$emit('sendjob', Job )"></div>
With the Vue DevTools plugin I can see that the data is sent in the PayLoad, but I can't find a way to receive this emit from the other component.
Many people do this
Any other component
<template>
<div #sendjob="doSomething"></div>
</template>
<script>
export default {
methods:{
doSomething(){
console.log('It works')
}
}
}
</script>
In my case it doesn't work
You should import the child component in the parent component and use it instead of the regular div tag.
I'm sharing examples for your reference to achieve emits in Vue 3 using <script setup> and Composition API. I strongly suggest going with <script setup if you are going to use Composition API in Single File Component. However, the choice is yours.
Example with <script setup>: https://v3.vuejs.org/api/sfc-script-setup.html
<!-- App.vue -->
<template>
<UserDetail #user-detail-submitted="userDetailSubmitted"/>
</template>
<script setup>
import UserDetail from './components/UserDetail';
function userDetailSubmitted(name) {
console.log({ name })
}
</script>
<!-- UserDetail.vue -->
<template>
<input type="text" v-model="name" #keyup.enter="$emit('user-detail-submitted', name)" />
</template>
<script setup>
import { ref } from 'vue';
const name = ref('');
</script>
Example using Composition API: https://v3.vuejs.org/api/composition-api.html
<!-- App.vue -->
<template>
<UserDetail #user-detail-submitted="userDetailSubmitted"/>
</template>
<script>
import UserDetail from "./components/UserDetail";
export default {
components: {
UserDetail,
},
setup() {
function userDetailSubmitted(name) {
console.log({ name });
}
return {
userDetailSubmitted
}
},
};
</script>
<!-- UserDetail.vue -->
<template>
<input type="text" v-model="name" #keyup.enter="$emit('user-detail-submitted', name)" />
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const name = ref('');
return {
name,
}
}
}
</script>
You should import this child-component in the parent. And don't rename it to the html's original tag.vue3. You'd better use the Composition API.

How to pass vmodel property to child component in nuxjs

I can't seem to pass dynamically modified properties from layouts into the <Nuxt /> component.
This is my ~/layouts/default.vue
<template>
<div>
<input v-model="myprop" />
<span>{{myprop}}</span>
<Nuxt />
</div>
</template>
<script>
export default {
provide: function () {
return {myprop: this.myprop};
},
data: () => ({
myprop: 'hello galaxy',
}),
}
</script>
This is my ~/pages/index.vue
<template>
<div>My Prop is: {{myprop}}</div>
</template>
<script>
export default {
inject: ["myprop"]
}
</script>
On the web page I see hello galaxy printed 3 times, once in the input, once in a span, and once in the Nuxt component. But when I edit the input field, only the span is updated. The Nuxt component does not capture the changes in myprop. The Nuxt component continues to show only hello galaxy while put the input and span shows changes as I type on my keyboard
What am I doing wrong?
The provide/inject is useful for simple situation, but if you've some reactive stuff the vuex store is more convenient :
in store/index.js
add a state called search and its mutations and actions :
export const state=()=>({
search:''
})
export const mutations ={
SET_SEARCH(state,payload){
state.search=payload
}
}
export const actions ={
setSearch(context,payload){
context.commit('SET_SEARCH',payload) ​
}
}
in layout/default.vue add computed property with setter/getter bound to the store:
<template>
<div>
<input v-model="search" />
<span>{{search}}</span>
<Nuxt />
</div>
</template>
<script>
export default {
computed:{
search:{
get(){
return this.$store.state.search
},
set(val){
this.$store.dispatch('setSearch',val)
}
}
}
}
</script>
in pages/index.vue :
<template>
<div>My search is: {{search}}</div>
</template>
<script>
export default {
computed:{
search(){
return this.$store.state.search
}
}
}
</script>

parent component is not getting data from child in Nuxt app

This is driving me crazy so I hope that anyone can help.
I made a Nuxt app with #nuxt/content and I'm using Netlify-CMS to create content. That all seems to work fine. However I'm trying to display a component that contains a loop of the MD-files that I have, but in the index.vue nothing of the loop is displayed.
I know (a little) about props and $emit, but as I am not triggering an event this dosen't seem to work.
Component code:
<template>
<section>
<h1>Releases</h1>
<li v-for="release of rfhreleases" :key="release.slug">
<h2>{{ release.artist }}</h2>
</li>
</section>
</template>
<script>
export default {
components: {},
async asyncData({ $content, params }) {
const rfhreleases = await $content('releases', params.slug)
.only(['artist'])
.sortBy('createdAt', 'asc')
.fetch()
return {
rfhreleases,
}
},
}
</script>
And index.vue code:
<template>
<div>
<Hero />
<Releases />
<About />
<Contact />
</div>
</template>
<script>
export default {
head() {
return {
script: [
{ src: 'https://identity.netlify.com/v1/netlify-identity-widget.js' },
],
}
},
}
</script>
If I place my component code as part of index.vue, everything work, but I would love to avoid that and thats why I'm trying to place the loop in a component.
As stated on the Nuxt documentation:
This hook can only be placed on page components.
That means asyncData only works on components under pages/ folder.
You have several options:
You use fetch instead. It's the other asynchronous hook but it's called from any component. It won't block the rendering as with asyncData so the component it will instanciated with empty data first.
You fetch your data from the page with asyncData and you pass the result as a prop to your component
<template>
<div>
<Hero />
<Releases :releases="rfhreleases" />
<About />
<Contact />
</div>
</template>
<script>
export default {
async asyncData({ $content, params }) {
const rfhreleases = await $content('releases', params.slug)
.only(['artist'])
.sortBy('createdAt', 'asc')
.fetch()
return {
rfhreleases,
}
},
}
</script>

Passing props to Vue root instance via attributes on element the app is mounted on

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')