Vue 3 replacing the HTML tags where v-html is called with the provided HTML - vue.js

This is about a Vue 3 app with Vite, not webpack.
For now, as you can see from this issue on vite's issue page, vite doesn't have a convenient way of inlining SVGs without using external plugins. Vite does however, support importing files as raw text strings. As such, I had an idea to use this feature and to inline SVG's by passing the raw SVG strings into an element's v-html.
It actually works great, the SVG shows up on the page as expected and I can do the usual CSS transforms (the whole purpose of inlining them like this), but it's not perfect. As it currently stands, the element that receives the v-html directive simply places the provided HTML nested as a child. For example, if I do <span v-html="svgRaw" />, the final HTML comes out something like this
<span>
<svg>
<!-- SVG attributes go here -->
</svg>
</span>
Is there any way for me to essentially replace the parent element on which v-html is declared with the top-level element being passed to it? In the above example, it would mean the <span> just becomes an <svg>
EDIT:
Thanks to tony19 for mentioning custom directives.
My final result looks like this:
// main.ts
import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App);
app.directive("inline", (element) => {
element.replaceWith(...element.children);
});
app.mount("#app");
Then, in the component I simply use the directive, <svg v-html="svgRaw" v-inline /> and it works great!

You could create a custom directive that replaces the wrapper element with its contents:
Use app.directive() to create a global directive, named v-inline-svg:
// main.js
import { createApp } from 'vue'
import App from './App.vue'
createApp(App)
.directive('inline-svg', el => {
if (!el) {
return
}
// copy attributes to first child
const content = el.tagName === 'TEMPLATE' ? el.content : el
if (content.children.length === 1) {
;[...el.attributes].forEach((attr) => content.firstChild.setAttribute(attr.name, attr.value))
}
// replace element with content
if (el.tagName === 'TEMPLATE') {
el.replaceWith(el.content)
} else {
el.replaceWith(...el.children)
}
})
.mount('#app')
In your component, include v-inline-svg on the v-html wrapper element (also works on <template> in Vue 3):
<svg v-html="svgRaw" v-inline-svg />
<!-- OR -->
<template v-html="svgRaw" v-inline-svg />
demo

I found that using the method above works but is only good for a single rendering of the svg... The element starts throwing errors if I try to change the svg contents dynamically, not sure why but assuming that the dom replacement has something to do with it.
I modified the code slightly for my use case.
app.directive('inline-svg', {
updated: (element) => {
if (element.children.length === 0) {
return
}
const svg = element.children[0]
if(svg.tagName.toLowerCase() !== 'svg') {
return
}
for (let i = 0; i < svg.attributes.length; i++) {
const attr = svg.attributes.item(i)
element.setAttribute(attr.nodeName, attr.nodeValue)
}
svg.replaceWith(...svg.children)
}
})
In my component I have.
<svg v-if="linkType !== null" v-html="linkType" v-inline-svg></svg>
The directive now copies the svg attributes across from the child to the parent and then replaces the child with it's children.

Coming from Vue2. I think this still works:
Instead of span you can use the special Vue tag template:
<template v-html="svgRaw" />
This will not render <template /> as a tag itself, but render the elements given in v-html without a parent element.

Related

Vue 3 dynamically loaded component hooks not called

I have this (shorten for the question) single file component (vue 3.2.31):
<template lang="pug">
.test Hello world!
</template>
<style lang="sass" scoped>
.test
font-weight: bold
</style>
<script setup lang="ts">
onMounted(() => {
console.log('Mounted');
});
</script>
It is bundled via vitejs, exported as (let's say) NamedExport and served on demand as a base64 encoded string to be imported client-side.
const component = await defineAsyncComponent(async () => {
// A module that exports multiple components.
const module = await import(base64StringSentFromTheServer);
// Choose one.
return module['NamedExport']);
})
then, the result is bound to:
<component :is="component" />
It works well, except two things, one of these is that hooks are not called (onMounted in this case), the other being that styles importer is not called either.
Is it an expected behavior, or do I miss something? Is it the <script setup> way of writing the component that is responsible?
It appears that I had two instances of Vue running (one bundled with my package, with rollup, and one imported in the script itself), and for an unknown reason, none of the two was calling hooks.
By removing one of the instances (actually, passing vue as external in rollup build configuration) it now works well.

Testing visibility of React component with Tailwind CSS transforms using jest-dom

How can I test whether or not a React component is visible in the DOM when that component is hidden using a CSS transition with transform: scale(0)?
jest-dom has a .toBeVisible() matcher, but this doesn't work because transform: scale(0) is not one of the supported visible/hidden triggers. Per the docs:
An element is visible if all the following conditions are met:
it is present in the document
it does not have its css property display set to none
it does not have its css property visibility set to either hidden or collapse
it does not have its css property opacity set to 0
its parent element is also visible (and so on up to the top of the DOM tree)
it does not have the hidden attribute
if <details /> it has the open attribute
I am not using the hidden attribute because it interfered with my transition animations. I am using aria-hidden, but that is also not one of the supported triggers.
The simplified version of my component is basically this. I am using Tailwind CSS for the transform and the transition.
import React from "react";
import clsx from "clsx";
const MyComponent = ({isSelected = true, text}) => (
<div
className={clsx(
isSelected ? "transform scale-1" : "transform scale-0",
"transition-all duration-500"
)}
aria-hidden={!isSelected}
>
<span>{text}</span>
</div>
)
I could potentially check for hidden elements with:
toHaveClass("scale-0")
toHaveAttribute("aria-hidden", true)
But unlike toBeVisible, which evaluates the entire parent tree, these matchers only look at the element itself.
If I use getByText from react-testing-library then I am accessing the <span> inside the <div> rather than the <div> which I want to be examining. So this doesn't work:
import React from "react";
import { render } from "#testing-library/react";
import "#testing-library/jest-dom/extend-expect";
import { MyComponent } from "./MyComponent";
it("is visible when isSelected={true}", () => {
const {getByText} = render(
<MyComponent
isSelected={true}
text="Hello World"
/>
);
expect(getByText("Hello World")).toHaveClass("scale-1");
});
What's the best way to approach this?

Is it possible to globally define links to use a specific component?

I'm currently trying to use Nav with react-router. The default behavior reloads the page, so I'm trying to use the Link component from react-router-dom.
It's quite difficult to preserve the default styling when overriding linkAs.
Is there any global way to override link navigation behavior?
Like defining a global link render function, which I can then set to render the Link component from react-router-dom?
Yes, it's possible!
2 things are required:
Make a wrapper component that translates the Nav API to react-router-dom links.
Specify the linkAs prop to the Nav component.
Wrapper component
This is a simple component that creates a react-router-dom link while using styles from Fabric:
import { Link } from "react-router-dom";
const LinkTo = props => {
return (
<Link to={props.href} className={props.className} style={props.style}>
{props.children}
</Link>
);
};
Specify component for use in Nav
<Nav groups={links} linkAs={LinkTo} />
Have also created a full working example at https://codesandbox.io/s/xenodochial-wozniak-y10tr?file=/src/index.tsx:605-644

v-show alternative for Svelte

The case is that I'm showing Loading component on fetch request. I use store to set $loading to true and inside conditions is the Loading component. The problem is that the Loading component seems to be taking some time to show. It feels/looks like the reason is re-rendering of Loading component. So, I was looking for v-show like thing in Svelte, which I cannot find in Docs. (Don't get angry if its there, just tell me.)
Can anyone help with this case?
Either wrap it in an {#if someCondition} block, or slap a hidden={!someCondition} attribute on an element.
If you want a block of HTML that does not re-render when the condition is changed, here is a simple solution:
<script>
// Show.svelte
export let show = true;
</script>
<div class:hide={!show}>
<slot />
</div>
<style>
.hide {
display: none !important;
}
</style>
And then use the Show component to create that block:
<script>
import Show from "Show.svelte";
let show = true;
</script>
<button on:click={() => { show = !show}}>
Click to Show/Hide Content
</button>
<Show {show}>
<div>Content</div>
</Show>
I have posted the Show component as an npm package https://www.npmjs.com/package/svelte-show

Vue.js How to define (override) css style in a Component?

The default style for the p tag on my page has some bottom margin. My component uses p tags, and accordingly, the p tags in my component text show the corresponding bottom margin. How can I override/define new css style for the p tags in my component. I define my component like this:
Vue.component ('activity-component', {
props: {
customer_id:{},
is_admin:{},
isAdmin:{},
isKitsActionplan:{},
....
template:
`<div
class="row msDashboard-box"
style="cursor:default;padding-top:12px;
padding-bottom:12px;"
>
...
<p> ... </p>
});
Maybe u can try this approach,
Pass a variable with the class name to the component
<my-component v-bind:class="variable with class name"></my-component>
Then apply a rule to all p elements inside it, something like this i guess:
.test p{
your styles
}
U can see more here: vue api class and style bindings
I dont know for sure if this was what you wanted, but i gave it a shot :)
You have several options - choose your own adventure:
Use a global utility style
Somewhere globally, define a utility class like:
.u-margin-reset {
margin: 0;
}
Then in your template:
<p class="u-margin-reset">hello</p>
Use scoped CSS
If you are using single file components, you can use scoped css:
<template>
<p class="special-p">hello</p>
</template>
<style scoped>
.special-p {
margin: 0;
}
</style>
Use inline styles
Vue.component('activity-component', {
template: `<p style="margin:0;"></p>`,
});
or
Vue.component('activity-component', {
computed: {
myStyle() {
return {
margin: 0,
};
},
},
template: `<p :style="myStyle"></p>`,
});
As an aside, I'd recommend using a CSS reset that globally resets the margins of all elements to 0. Then each component should set the margins as needed for its child elements/components. This may not be reasonable if you already have a large codebase, however.