Dynamically render Vue component within another component - vue.js

Using Vue 3.2.41 - #heroicons/vue 2.0.14 - inertiajs 1.0 - vite 4.0.0
I'm calling a Vue component using this:
<TimelineItem icon="CalendarDaysIcon" />
The component looks like this:
<template>
<component :is="icon" /> <!-- doesn't work -->
<CalendarDaysIcon /> <!-- works -->
</template>
<script setup>
import {
CalendarDaysIcon,
} from '#heroicons/vue/20/solid'
const props = defineProps(['icon'])
</script>
The HTML being rendered is like this:
<calendardaysicon></calendardaysicon> <!-- not what I want -->
<svg> ... </svg> <!-- correct but not dynamic -->
In other words, the <component :is /> is just rendering some empty <calendardaysicon> tags when I'd expect it to render the component. I can see that it has made it lowercase and have no idea how to force it back to PascalCase and I'm not even sure if that would help the situation.
I've simplified the code somewhat, but the full version would have a list of 10 different icons (all part of the Heroicons package which uses PascalCase names) which I'd like to be able to call easily from the main component.

If you are authoring your templates directly in a DOM (e.g. as the content of a native element), the template will be subject to the browser's native HTML parsing behavior. In such cases, you will need to use kebab-case and explicit closing tags for components https://vuejs.org/guide/essentials/component-basics.html#dom-template-parsing-caveats
Try: <calendar-days-icon> </calendar-days-icon>

const { createApp, createElementVNode, openBlock, createElementBlock } = Vue;
const CalendarDaysIcon = {
render(_ctx, _cache) {
return (openBlock(), createElementBlock("svg", {
xmlns: "http://www.w3.org/2000/svg",
viewBox: "0 0 20 20",
fill: "currentColor",
"aria-hidden": "true"
}, [
createElementVNode("path", { d: "M5.25 12a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H6a.75.75 0 01-.75-.75V12zM6 13.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V14a.75.75 0 00-.75-.75H6zM7.25 12a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H8a.75.75 0 01-.75-.75V12zM8 13.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V14a.75.75 0 00-.75-.75H8zM9.25 10a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H10a.75.75 0 01-.75-.75V10zM10 11.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V12a.75.75 0 00-.75-.75H10zM9.25 14a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H10a.75.75 0 01-.75-.75V14zM12 9.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V10a.75.75 0 00-.75-.75H12zM11.25 12a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H12a.75.75 0 01-.75-.75V12zM12 13.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V14a.75.75 0 00-.75-.75H12zM13.25 10a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H14a.75.75 0 01-.75-.75V10zM14 11.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V12a.75.75 0 00-.75-.75H14z" }),
createElementVNode("path", {
"fill-rule": "evenodd",
d: "M5.75 2a.75.75 0 01.75.75V4h7V2.75a.75.75 0 011.5 0V4h.25A2.75 2.75 0 0118 6.75v8.5A2.75 2.75 0 0115.25 18H4.75A2.75 2.75 0 012 15.25v-8.5A2.75 2.75 0 014.75 4H5V2.75A.75.75 0 015.75 2zm-1 5.5c-.69 0-1.25.56-1.25 1.25v6.5c0 .69.56 1.25 1.25 1.25h10.5c.69 0 1.25-.56 1.25-1.25v-6.5c0-.69-.56-1.25-1.25-1.25H4.75z",
"clip-rule": "evenodd"
})
]))
}
}
const TimeLineItem = {
props: ['icon'],
components: {
CalendarDaysIcon
},
template: '<component :is="icon" class="icon"/>'
}
const App = {
components: {
TimeLineItem
}
}
const app = createApp(App)
app.mount('#app')
.icon {
width: 36px;
height: 36px;
}
<div id="app">
<time-line-item icon="CalendarDaysIcon"></time-line-item>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>

Using <component :is="icon" /> is only using a string containing CalendarDaysIcon
Instead, in the main component, pass the actual component reference like this:
<template>
<TimelineItem :icon="CalendarDaysIcon" />
</template>
<script setup>
import {
CalendarDaysIcon,
} from '#heroicons/vue/20/solid'
const props = defineProps(['icon'])
</script>
Then, in the TimelineItem component, there is no need to reference any icons:
<template>
<component :is="icon" /> <!-- now works -->
</template>
<script setup>
const props = defineProps(['icon'])
</script>
Thanks to #Robert Boes on the Inertia Discord server for the guidance.

Related

Using SVG properly in Vue 3 Script Setup

I have the following code, that prints the menu Icon but doesn't adapt to the size of the screen:
<template>
<menuIcon />
</template>
<script setup>
let menuIcon = <svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="bars" class="svg-inline--fa fa-bars fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M436 124H12c-6.627 0-12-5.373-12-12V80c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12zm0 160H12c-6.627 0-12-5.373-12-12v-32c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12zm0 160H12c-6.627 0-12-5.373-12-12v-32c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12z"></path></svg>;
</script>
Previously you could do the following:
<script>
import menuIcon from '#assets/Icons/bars.svg';
export default {
name: "Navigation",
components: {
menuIcon,
},
};
</script>
And it worked properly; where is the best option to use script setup to use an svg image?
thanks in advance!
I'm trying to use svg image in script setup cpnfiguration.

Some of Quasar's styles not loading

I have added the Quasar plugin onto an existing Vuejs project.
The problem I am experiencing is that when I use Quasar components, it seems that some of it's styling does not work.
Will provide pics at the bottom of the page.
In my main.js I use Quasar :
projectAuth.onAuthStateChanged(()=>{
if(!app){
app = createApp(App).use(Quasar, quasarUserOptions).use(store).use(router).mount('#app')
}
})
quasarUserOptions:
import './styles/quasar.sass'
import '#quasar/extras/material-icons/material-icons.css'
// To be used on app.use(Quasar, { ... })
export default {
config: {},
plugins: {
}
}
and finally the component in which I wish to use a Quasar component:
<template>
<div id="q-app" style="min-height: 100vh;">
<div class="q-pa-md">
<q-select
filled
hide-bottom-space
options-dense
hide-dropdown-icon
v-model="model"
use-input
clearable
multiple
input-debounce="500"
:options="filterOptions"
#filter="filterFn"
style="width: 200px"
counter
hint="Selected items"
label="Actors"
dense
hide-selected
></q-select>
</div>
</div>
<template/>
<script >
import {ref} from 'vue'
export default {
setup(){
const stringOptions = ['Google', 'Facebook', 'Twitter', 'Apple', 'Oracle']
const filterOptions = ref(stringOptions)
const model = ref(null)
const filterFn = (val, update) => {
update(() => {
if (val === '') {
filterOptions.value = stringOptions
}
else {
const needle = val.toLowerCase()
filterOptions.value = stringOptions.filter(
v => v.toLowerCase().indexOf(needle) > -1
)
}
})
}
return {filterFn, model, filterOptions, stringOptions}
}
}
</script>
<style scoped>
</style>
The following img is what It is supposed to look like and how it displays in codepen :
The following img is what It looks like in my vuejs app :
Both images aren't at the same scale but that is not the problem.
Any advice will be very appreciated

Password Strength meter for Vue.js 3

I've started working with Vue.js version 3 and making a simple signup form. I need to implement a password strength meter for my password field but seems there isn't any compatible such component with Vue.js 3 version.
I've found few good components for password strength meter to use with Vue.js but they all seems to have compatibility with Vue.js 2.
I've tried
https://awesomeopensource.com/project/skegel13/vue-password
its working good in DEMO but not compatible with my Vue.js 3.
I'm stuck here. Any help/suggestions ?
Are you looking for a visual component or something that actually computes password strength?
zxcvbn is fairly well-known as a strength calculator - it outputs a score from 0-4 for how strong a password is. You could then roll a simple Vue component that outputs a different value depending on that score.
Below example uses Tailwind CSS classes for styling the visual meter. I wrote this in the browser and haven't tested the Vue but it's fairly simple and you should be able to get the idea.
<!-- PasswordStrengthMeter.vue -->
<template>
<div>
<div class="w-full h-4 flex">
<div :class="style"></div>
<div class="flex-1"></div>
</div>
<div>{{ strength }}</div>
</div>
</template>
<script>
props: {
score: {
required: true,
default: 0,
}
},
computed: {
strength() {
return [
'Very Weak', // 0
'Weak', // 1
'Moderate', // 2
'Strong', // 3
'Very Strong' // 4
][this.score];
},
style() {
return [
'w-1 bg-red-500', // 0
'w-1/4 bg-yellow-500', // 1
'w-1/2 bg-yellow-300', // 2
'w-3/4 bg-green-500', // 3
'w-full bg-blue-500' // 4
][this.score];
},
},
</script>
Here's what it might look like.
This one works nicely with Vue3.
https://github.com/miladd3/vue-simple-password-meter/tree/next
Sample code from the repository:
<template>
<div id="app">
<label>Password</label>
<input type="password" v-model="password" />
<password-meter :password="password" />
</div>
</template>
<script>
import { defineComponent, ref } from 'vue';
import PasswordMeter from 'vue-simple-password-meter';
export default defineComponent({
components: {
PasswordMeter,
},
setup() {
const password = ref('');
return {
password,
};
},
});
</script>

v-html receiving a prop does not render (Vue, SVG)

I'm passing a string containing a sequence of elements as a prop, which I try to render using :v-html="prop":
<template>
<svg
version="1.1"
baseProfile="full"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 26 26"
:class="sizeClass"
:height="size"
:width="size"
>
<g :v-html="sequence" />
</svg>
</template>
<script lang="ts">
import Vue from "vue";
export default Vue.extend({
name: "Avatar",
props: {
sizeClass: String,
size: String,
sequence: String,
},
});
</script>
However inspecting the result on dev tools, the html is not rendered, but appears only in the v-html attribute :
What am I missing ?
directives are bound by default they don't need binding sign : or v-bind::
<g v-html="sequence" />

How to make non table pagination with Buefy

I wish to make pagination for my class project, currently using Vue.js and Bulma + Buefy.
I have an array of objects and I want to display 5 per page.
Already have the layout done and everything, I just want to know how to make the pagination work. Sorry can't post the code because it's not displaying correctly in here but I'm using v-for to display each iteration.
Much thanks!
You can use <b-pagination> component
Without any code sample from you, this would be minimal approach to use that component
const example = {
data() {
return {
items: [],
current: 1,
perPage: 5,
}
},
created() {
// populate array
for(var i = 1; i <= 100; i++){
this.items.push(i)
}
},
computed: {
total() {
return this.items.length
},
/*
Filtered items that are shown in the table
*/
paginatedItems() {
let page_number = this.current-1
return this.items.slice(page_number * this.perPage, (page_number + 1) * this.perPage);
}
},
}
Vue.config.devtools = false
Vue.config.productionTip = false
const app = new Vue(example)
app.$mount('#app')
<link href="https://cdn.materialdesignicons.com/2.0.46/css/materialdesignicons.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<!-- Buefy CSS -->
<link rel="stylesheet" href="https://unpkg.com/buefy/dist/buefy.min.css">
<!-- Buefy JavaScript -->
<script src="https://unpkg.com/buefy/dist/buefy.min.js"></script>
<div id="app" class="container">
<div v-for="(item, index) in paginatedItems">
{{ item }}
</div>
<b-pagination
:total="total"
:current.sync="current"
:per-page="perPage"
>
</b-pagination>
</div>
Do a filter with that displays your first 5. When you click in the number of the page or next page and have a var with that value.
The your filter will need to read that var and give you the right number or objects.
Example:
Page 1 -> 1 to 5
Page 2 -> 6 to 10 (2 = +5, 3= +10)
Your filter will update and show the new ones