I'm creating a component that needs to render out of a bunch of concated strings. Vue's compile and h functions accomplish this quite nicely while having access to the parent context (unlike the template option).
However, the compiled text itself has no access to the parent context. Here's my simplified code:
<script setup>
import {compile, h, computed } from 'vue'
const props = defineProps(['commTemplate', 'content']);
const textStyle = computed(() => ({
fontSize: props.commTemplate.textSize + 'px',
}));
const titleTyle = computed(() => ({
fontSize: props.commTemplate.titleSize + 'px',
}));
const structure = [];
const parts = [];
structure[0] = `
<table>
<tr>
<td :style="textStyle"></td>
<td>
`; parts['thing'] = `
</td>
<td :style="titleStyle">Optional Part</td>
<td>
`; structure[1] = `
</td>
</tr>
</table>
`;
const render = () => {
return h(compile(structure[0] + parts['thing'] + structure[1]));
};
</script>
<template>
<render />
</template>
After running this, Vue complains about textStyle not being defined when it encounters :style="textStyle".
Is there any way to render/compile this along with the parent context?
Any props can be passed on to the rendered component using something like this:
return h(compile('template'), { prop1, prop2 });
Related
I am trying to populate a table of people with their name and a profile picture. The name is sourced from a Firestore database, and the picture uses the name to find a related picture in a Firebase Storage bucket.
I have watched hours of videos and have scoured nearly a hundred articles at this point, so my example code has pieces from each as I've been trying every combination and getting mixed but unsuccessful results.
In this current state which returns the least amount of errors, I am able to successfully populate the table with the names, however in that same component it is not able to pull the profile picture. The value used for the profile picture is updated, but it is updated from the placeholder value to undefined.
GamePlanner.vue
<template>
<div>
<Field />
<Bench />
<!-- <Suspense>
<template #default> -->
<PlanSummary />
<!-- </template>
<template #fallback>
<div class="loading">Loading...</div>
</template>
</Suspense> -->
</div>
</template>
PlanSummary.vue
<template>
<div class="summaryContainer">
<div class="inningTableContainer">
<table class="inningTable">
<tbody>
<!-- <Suspense> -->
<!-- <template #default> -->
<PlayerRow v-for="player in players.value" :key="player.id" :player="(player as Player)" />
<!-- </template> -->
<!-- <template #fallback>
<tr data-playerid="0">
<img class="playerImage" src="https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50" />
Loading players...
<span class="playerNumber">00</span>
</tr>
</template> -->
<!-- </Suspense> -->
</tbody>
</table>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onErrorCaptured, ref } from "vue";
import { useFirestore, useCollection } from "vuefire";
import { collection } from "firebase/firestore";
import { Player, Inning } from "#/definitions/GamePlanner";
import PlayerRow from "./PlayerRow.vue";
const db = useFirestore();
const gamePlanID = "O278vlB9Xx39vkZvIsdP";
// const players = useCollection(collection(db, `/gameplans/${gamePlanID}/participants`));
// const players = ref(useCollection(collection(db, `/gameplans/${gamePlanID}/participants`)));
// Unhandled error during execution of scheduler flush
// Uncaught (in promise) DOMException: Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.
const players = ref();
players.value = useCollection(collection(db, `/gameplans/${gamePlanID}/participants`));
// Seeminly infinite loop with "onServerPrefetch is called when there is no active component instance to be associated with."
// Renders 5 (??) undefined players
// One error shown: Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'substring')
// const players = computed(() => useCollection(collection(db, `/gameplans/${gamePlanID}/participants`)));
// onErrorCaptured((error, vm, info) => {
// console.log("Error loading Summary component: ", error, "vm: ", vm, "info: ", info);
// throw error;
// });
</script>
PlayerRow.vue
<template>
<tr :key="player2.id" :data-playerid="player2.id">
<td>
<img class="playerImage" :src="playerPictureURL" />
{{ player2.nickname || player2.firstName + " " + player2.lastName }}
<span class="playerNumber">{{ player2.playerNumber }}</span>
</td>
</tr>
</template>
<script lang="ts" setup>
import { ref, PropType, computed, onMounted, watch } from "vue";
import { useFirebaseStorage, useStorageFileUrl } from "vuefire";
import { ref as storageRef } from 'firebase/storage';
import { Player, Inning } from "#/definitions/GamePlanner";
const fs = useFirebaseStorage();
const props = defineProps({
'player': { type: Object as PropType<Player>, required: true },
// 'innings': Array<Inning>
});
const player2 = ref(props.player);
// const innings = computed(() => props.innings);
// const playerPictureURL = computed(() => {
// const playerPictureFilename = `${player2.value.firstName.substring(0,1)}${player2.value.lastName}.png`.toLowerCase();
// const playerPictureResource = storageRef(fs, `playerPictures/${playerPictureFilename}`);
// return useStorageFileUrl(playerPictureResource).url.value as string;
// });
// const playerPictureURL = ref(() => {
// const playerPictureFilename = `${player2.value.firstName.substring(0,1)}${player2.value.lastName}.png`.toLowerCase();
// const playerPictureResource = storageRef(fs, `playerPictures/${playerPictureFilename}`);
// return useStorageFileUrl(playerPictureResource).url.value as string;
// });
const playerPictureURL = ref("https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50");
async function getPlayerPictureURL() {
console.log("PlayerRow.ts getPlayerPictureURL");
const playerPictureFilename = `${player2.value.firstName.substring(0,1)}${player2.value.lastName}.png`.toLowerCase();
const playerPictureResource = await storageRef(fs, `playerPictures/${playerPictureFilename}`);
playerPictureURL.value = await useStorageFileUrl(playerPictureResource).url.value as string;
}
onMounted(() => {
console.log("PlayerRow.ts onMounted");
getPlayerPictureURL();
});
watch(playerPictureURL, (newVal, oldVal) => {
console.log("PlayerRow.ts watch playerPictureURL");
console.log("newVal: " + newVal);
console.log("oldVal: " + oldVal);
});
</script>
I was under the impression that <Suspense> would need to wrap the <PlayerRow> component since I am using the storageRef and useStorageUrl methods, but it seems to introduce more issues. Based on the vuefire documentation and inspecting the definitions int he code itself, it does not appear that they are asynchronous, however trying to to immediately invoke them does not produce an immediate/actual result.
Relevant Package Versions
{
"vue": "^3.2.45"
"firebase": "^9.15.0",
"typescript": "^4.9.3",
"vite": "^4.0.0",
"vue-router": "^4.1.6",
"vue-tsc": "^1.0.11",
"vuefire": "3.0.0-beta.6"
}
According to this documentation we are supposed to use useCollection with including collection as a argument as follows:
const todos = useCollection(collection(db, `/gameplans/${gamePlanID}/participants`))
And I see you are using it the correct way but instead of assigning to a ref you can assign it directly to players variable. As you are trying to use this reactive object as the value of a ref object, which is not supported.
You can solve your issue with changing this line:
const players = ref();
players.value = useCollection(collection(db, `/gameplans/${gamePlanID}/participants`));
To :
const players = useCollection(collection(db, `/gameplans/${gamePlanID}/participants`));
For more information about this topic you can go through the following docs
i've got an image loader component, which is getting the image id as prop, normally, i should use the ref like this:
<template>
<img
ref="foo"
/>
</template>
<script setup>
const foo = ref(null);
onMounted( () => {
const img = (foo.value as HTMLImageElement);
if (img) {
img.addEventListener("load", () => fitImage(img));
}
});
</script>
But how do i do it with dynamic ref?
<template>
<img
:ref="dynamicRef()"
/>
</template>
<script setup>
const dynamicRef = () => {
return 'image'+props.imageId;
}
</script>
I've already tried to place this inside an array, like
const refs = [];
refs[dynamicRef] = ref(null)
also
const refs = ref([])
refs.value[dynamicRef] = ref(null);
But nothing seems to work
I have problem with shallowMount in Vue.
My function looks like this:
describe('ParentComponent.vue', () => {
it('renders a ParentComponent', () => {
const wrapper = shallowMount(ParentComponent, {
propsData: {
propOne: 'someUrl',
propTwo: 'someText'
}
});
expect(wrapper.find('.some-class').html()).toContain(
'<div alt="someText" class="some-class" style="width: 2rem; height: 2rem; background-image: url(propOne);"></div>'
);
});
});
ParentComponent looks like this:
<template>
<div
:style="basicStyles"
:alt="title"
class="some-class"
>
<ChildComponent v-if="someCondition"
:someProp="something"
:anotherProp="alsoSomething"
/>
</div>
</template>
In previous version of my ParentComponent there wasn't ChildComponent inside. Now I have to do it, but I don't know what should be inside toContain() method now. Because now it doesn't work, because expected substring is different from received string in describe method. How to inject ChildComponet inside toContain() method if I want to test only ParentComponent?
shallowMount stubs all the children components. You either use mount instead or do this
expect(wrapper.findComponent(ChildComponent).exists()).toBe(true)
Is there any way to do this kind of conditional nesting with Vue?
(Apparently <component is="template"> outputs a non parsed <template> tag into the DOM but does not render anything)
<component :is="condition ? 'div' : 'template'">
<!-- 2 elements here -->
</component>
The purpose is to avoid unneeded markup or repeating my 2 elements code twice in a v-if v-else.
Also having a sub component with the 2 elements would not help as Vue components need only 1 root, so a wrapper would be needed there too.
What I am looking for is an equivalent to:
<div v-if="condition">
<span>element 1</span>
<span>element 2</span>
</div>
<template v-else>
<span>element 1</span>
<span>element 2</span>
</template>
but without rewriting twice the span elements.
(Also posted it on Vue.js forum https://forum.vuejs.org/t/how-to-conditionally-nest-elements/95384)
Thanks for any help!
Using Vue 2:
There is no straight forward solution to this using Vue 2, but you can use Functional Components for this purpose, as functional components do not have the single-root limitation.
So first, create a my-span functional component which will be rendered in DOM with multiple nodes like:
<span>element 1</span>
<span>element 2</span>
using:
Vue.component('my-span', {
functional: true,
render: function (createElement, context) {
const span1 = createElement('span', 'element 1');
const span2 = createElement('span', 'element 2');
return [span1, span2]
},
})
You can create as many nodes you want, with any element you want and simply return that as an array.
In Vue 2.5.0+, if you are using single-file components, template-based functional components can be declared with:
<template functional>
</template>
Next, create a component just to wrap the <my-span> above like:
Vue.component('my-div', {
template: '<div><my-span /></div>'
})
Then using Vue’s <component> element with the is special attribute, we can dynamically switch between the <my-div> and <my-span> components like:
<component :is="condition ? 'my-div' : 'my-span'"></component>
This will result in the desired behaviour you are looking for. You can also inspect the rendered DOM to verify this.
Working Demo:
Vue.component('my-span', {
functional: true,
render: function (createElement, context) {
const span1 = createElement('span', 'element 1');
const span2 = createElement('span', 'element 2');
return [span1, span2]
},
})
Vue.component('my-div', {
template: '<div><my-span /></div>'
})
new Vue({
el: "#myApp",
data: {
condition: true
},
methods: {
toggle() {
this.condition = !this.condition;
}
}
})
#myApp{padding:20px}
#myApp div{padding:10px;border:2px solid #eee}
#myApp span{padding:5px;margin:5px;display:inline-flex}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="myApp">
<button #click="toggle">Toggle</button><br>
<component :is="condition ? 'my-div' : 'my-span'"></component>
</div>
Using Vue 3:
In Vue 3, it would ve very easy to implement as we can have multiple root nodes in Vue 3, as you can see MySpan component has a template with multiple spans:
const MySpan = { template: '<span>element 1</span><span>element 2</span>' };
Working Demo:
const { createApp, ref } = Vue;
const MySpan = { template: '<span>element 1</span><span>element 2</span>' };
const MyDiv = {
components: { MySpan },
template: '<div><my-span /></div>'
};
const App = {
components: { MyDiv, MySpan },
setup() {
const condition = ref(true);
const toggle = () => {
condition.value = !condition.value;
};
return { condition, toggle };
}
};
createApp(App).mount("#myApp");
#myApp{padding:20px}
#myApp div{padding:10px;border:2px solid #eee}
#myApp span{padding:5px;margin:5px;display:inline-flex}
<script src="//unpkg.com/vue#next"></script>
<div id="myApp">
<button #click="toggle">Toggle</button><br>
<component :is="condition ? 'my-div' : 'my-span'"></component>
</div>
I have multiple drop divs and so every time i drop the image of a component into those div i import the component corresponding to this image and i need to put it into the dom in the target div
drops[i].addEventListener('drop', function () {
if (this.draggedElement === null) {
return false
}
// get the component conf of this image
let conf = BlocksStore.conf[this.draggedElement.dataset.category].blocks[this.draggedElement.dataset.type]
// get the component itself (./blocks/SimpleTitle.vue)
let component = require('./blocks/' + conf.component)
drops[i].classList.remove('drag-enter')
// here is where i don't know what to do ...
drops[i].innerHTML = component
this.draggedElement = null
}.bind(this))
Here the code of ./blocks/SimpleTitle.vue
<template>
<tr>
<td align="center">
Lorem Ipsum
</td>
</tr>
</template>
<script>
export default {
name: 'simple-title'
}
</script>
<style>
</style>
I already tried to append the tag instead of the component but the dom don't comprehend it as a component