Add Vue click event to HTML string fetched via API - vue.js

I have some HTML string that I get via API. Let’s say it looks like:
const msg = 'Lorem ipsum dolor sit google
consectetur adipiscing elit yahoo
lorem ipsum.'
Now I need to add Vues #click event on those a elements. Is it possible to parse somehow the string and add the Vue event on it and output it?
Basically I have an electron app and I want to some additional logic on the links other then redirect.

You do not need to use v-html here. You can parse the string easily using node-html-parser. Then you can use a v-for to show the html
Codesandbox
Code:
<template>
<div id="app">
<div>a tags go below</div>
<div v-for="(tag, key) of a_tags" :key="key">
<a :href="tag.attributes.href">{{ tag.text}}</a>
</div>
<button #click="showTags">show tags</button>
</div>
</template>
<script>
import HelloWorld from "./components/HelloWorld";
import { parse } from "node-html-parser";
const msg =
'Lorem ipsum dolor sit google consectetur adipiscing elit yahoo lorem ipsum.';
const tags = parse(msg);
export default {
name: "App",
components: {
HelloWorld
},
mounted() {
console.log(tags);
},
data() {
return {
a_tags: [tags.childNodes[1], tags.childNodes[3]]
};
},
methods: {
showTags() {
console.log(this.a_tags);
}
}
};
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

EDIT AFTER CHECKING THE FIDDLE:
It seems that appending the refs to the string is not working, therefore, you should put a ref to the parent as follows:
<div id="app">
<div v-html="msg" ref="msg">
</div>
</div>
And then add the event to each of its two children in this case as follows:
new Vue({
el: "#app",
data: {
msg: 'Lorem ipsum dolor sit google consectetur adipiscing elit yahoo lorem ipsum.'
},
mounted() {
this.$refs.msg.children[0].onclick = function(e) {
e.preventDefault();
alert('foo')
};
this.$refs.msg.children[1].onclick = function(e) {
e.preventDefault();
alert('bar')
};
}
})

Related

Nuxt not loading nested component

I've created a third party components library as described on this page https://nuxtjs.org/blog/improve-your-developer-experience-with-nuxt-components#third-party-component-library. Than I used the components in a new clean nuxt project. I have a BaseCard component which has 3 slots and I have a BaseImage component. Now I want to use the BaseImage component in a slot from the BaseCard component but it is not rendered. If I add an additional BaseImage component outside of the BaseCard component, than all BaseImage components are rendered (see screenshots below). Seems like that the components within a slot are not loaded.
Screenshots
without additional BaseImage
with additional BaseImage
Code
Don't work
<template>
<div>
<BaseCard>
<template v-slot:image>
<BaseImage
imgSrc="https://picsum.photos/400/400?random=1"
imgAlt="Some alt tag"
/>
</template>
<template v-slot:header>
Here might be a page title
</template>
<template v-slot:content>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Eum
pariatur distinctio cum. Ratione doloribus asperiores eaque
laboriosam repellendus perferendis iusto magni in necessitatibus
exercitationem eum expedita aliquam autem, tenetur itaque.
</p>
</template>
</BaseCard>
</div>
</template>
<script lang="ts">
import Vue from "vue";
export default Vue.extend({});
</script>
works
<template>
<div>
<BaseCard>
<template v-slot:image>
<BaseImage
imgSrc="https://picsum.photos/400/400?random=1"
imgAlt="Some alt tag"
/>
</template>
<template v-slot:header>
Here might be a page title
</template>
<template v-slot:content>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Eum
pariatur distinctio cum. Ratione doloribus asperiores eaque
laboriosam repellendus perferendis iusto magni in necessitatibus
exercitationem eum expedita aliquam autem, tenetur itaque.
</p>
</template>
</BaseCard>
<BaseImage
imgSrc="https://picsum.photos/400/400?random=1"
imgAlt="Some alt tag"
/>
</div>
</template>
<script lang="ts">
import Vue from "vue";
export default Vue.extend({});
</script>
nuxt.config.js (shared-components is my library for the components)
export default {
// Target: https://go.nuxtjs.dev/config-target
target: "static",
// Global page headers: https://go.nuxtjs.dev/config-head
head: {
title: "demo",
htmlAttrs: {
lang: "en",
},
meta: [
{ charset: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
{ hid: "description", name: "description", content: "" },
],
link: [{ rel: "icon", type: "image/x-icon", href: "/favicon.ico" }],
},
// Global CSS: https://go.nuxtjs.dev/config-css
css: ["#/assets/scss/variables.scss"],
styleResources: {
scss: ["./assets/scss/*.scss"],
},
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
plugins: [],
// Auto import components: https://go.nuxtjs.dev/config-components
components: true,
// Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
buildModules: [
// https://go.nuxtjs.dev/typescript
"#nuxt/typescript-build",
"shared-components",
],
// Modules: https://go.nuxtjs.dev/config-modules
modules: ["#nuxtjs/style-resources"],
// Build Configuration: https://go.nuxtjs.dev/config-build
build: {},
};
I use nuxt 2.15.2.

why when click on an item, all item opens

I have list with an accordion, when you click on a item, all items opens, I need to open just one,
I understand that a loop is needed to iterate over all the items and apply the class to a specific one, but how to do this, please help
component:
<ul class="accordion accordion__trigger"
:class="{'accordion__trigger_active': visible}"
#click="open">
<li class="accordion__item" v-for="category in MAIN_CATS">
<nuxt-link exact no-prefetch active-class="link-active"
:to="`/category/${category.id}`"
class="menu-button">
{{ category.title }}
</nuxt-link>
<div class="accordion__content">
<div class="menu-sub-list" v-show="visible">
<ul class="sub-list">
<li class="menu-item"
v-for="sub in SUB_CATS(category.id)"
:key="sub.id">
<nuxt-link :to="`/category/${sub.id}`" class="menu-button">
{{ sub.title }}
</nuxt-link>
</li>
</ul>
</div>
</div>
</li>
</ul>
code:
name: "acc",
data() {
return {
index: null,
Accordion: {
count: 0,
active: null
}
};
},
computed: {
...mapGetters([
'MAIN_CATS',
'SUB_CATS'
]),
visible() {
return this.index === this.Accordion.active;
}
},
methods: {
...mapActions([
'GET_CATEGORIES_LIST',
]),
open() {
if (this.visible) {
this.Accordion.active = null;
} else {
this.Accordion.active = this.index;
}
},
start(el) {
el.style.height = el.scrollHeight + "px";
},
end(el) {
el.style.height = "";
}
},
created() {
this.index = this.Accordion.count++;
},
mounted() {
this.GET_CATEGORIES_LIST()
},
I have list with an accordion, when you click on a item, all items opens, I need to open just one,
I understand that a loop is needed to iterate over all the items and apply the class to a specific one, but how to do this, please help
There are multiple differences between your code and code from the answer that you referred to.
You can notice that #click is placed in the same line as v-for.
The main reason for that is to be able to easily access index of each element in a loop.
Not to overcomplicate it for you, I created a basic use case scenario:
<template>
<div id="accordion" class="accordion-container">
<ul
v-for="(category, index) in items"
:key="index"
class="accordion accordion__trigger"
:class="{'accordion__trigger_active': visible===index}"
#click="visible=index"
>
<li class="accordion__item">
{{ category.title }}
<div class="accordion__content">
<div class="menu-sub-list" v-show="visible===index">
<ul class="sub-list">
<li class="menu-item">{{ category.sub }}</li>
</ul>
</div>
</div>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "trial-page",
data() {
return {
items: [
{
title: "Accordion 1",
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
sub: "Pellentesque risus mi"
},
{
title: "Accordion 2",
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
sub: "Pellentesque risus mi"
},
{
title: "Accordion 3",
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
sub: "Pellentesque risus mi"
}
],
visible: null
};
}
};
</script>
<style>
.accordion__trigger_active {
background-color: blue;
color: white;
}
</style>
You can see that the idea is to operate with index value that is assigned to visible data property in this case.
We simply check if the visible is equal to the currently pressed item with the value of index.
With that we conditionally v-show element and trigger the class :class="{'accordion__trigger_active': visible===index}".
Note that if you had more v-for loops in the same component then you would need to make sure the value used for visible is always unique, for that you could simply add some string to it like:
#click="visible=index+'category'"
Also remember to assign a :key when using v-for.
Example:
v-for="(category, index) in items" :key="index"

Prevent Vuetify v-tabs from change

Is there a way to prevent the v-tabs from actually changing when being clicked on?
In my case I first need to check if stuff on the page has changed and want to cancel the switch to another tab if it has.
Neither a event.prevent nor event.stop will stop the v-tabs from changing:
<v-tab #click.prevent.stop="..."> ... </v-tab>
At the moment I'm using a window.requestAnimationFrame to reset the tab index to the old value. It gets the job done but this feels like a really nasty technique to me.
HTML:
<v-tabs v-model="currentIndex">
<v-tab v-for="(route, index) in list" :key="index" #change="handleTabChange(route, $event)" >
{{ route.meta.title }}
</v-tab>
</v-tabs>
TS:
public handleTabChange(routeConf:RouteConfig):void {
let currentIndex:number = this.currentIndex;
window.requestAnimationFrame(() => {
this.currentIndex = currentIndex;
Store.app.router.goto(routeConf.name, null, this.$route.params);
// Once the page actually changes this.currentIndex is set to the correct index..
});
}
I solve this problem by using separate variable between v-tabs and v-tabs-items.
<v-tabs v-model="tab" #change="onTabChange">
<v-tab v-for="item in items" :key="item">
{{ item }}
</v-tab>
</v-tabs>
<v-tabs-items v-model="currentTab">
<v-tab-item v-for="item in items" :key="item">
<v-card>
<v-card-text>{{ item }}</v-card-text>
</v-card>
</v-tab-item>
</v-tabs-items>
methods: {
onTabChange() {
if (/* reject */) {
this.$nextTick(() => {
this.tab = this.currentTab
})
} else {
this.currentTab = this.tab
}
}
}
Demo
Another possible solution is to extend the v-tab component which is a bit more complicated but can actually override the behavior.
Create new file my-tab.js:
import { VTab } from 'vuetify/lib'
export default {
extends: VTab,
methods: {
async click (e) {
if (this.disabled) {
e.preventDefault()
return
}
// <-- your conditions
let ok = await new Promise(resolve => {
setTimeout(() => {
resolve(false)
}, 2000)
})
if (!ok) {
this.$el.blur()
return
}
// -->
if (this.href &&
this.href.indexOf('#') > -1
) e.preventDefault()
if (e.detail) this.$el.blur()
this.$emit('click', e)
this.to || this.toggle()
}
}
}
The original source code is here. You can also override the render function to change the styles.
Then just use it as normal component:
<v-tabs v-model="tab">
<my-tab v-for="item in items" :key="item">
{{ item }}
</my-tab>
</v-tabs>
<v-tabs-items v-model="tab">
<v-tab-item v-for="item in items" :key="item">
<v-card>
<v-card-text
>{{ item }} ipsum dolor sit amet, consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat.</v-card-text
>
</v-card>
</v-tab-item>
</v-tabs-items>
For me it works the following way:
...
<v-tab href="#tab-1">Tab-1</v-tab>
<v-tab href="#tab-2" #click.native.prevent.stop.capture="goto2()">Tab-2</v-tab>
...
...
private goto2() {
if(tab-1Changes) {
// do something
return;
}
this.tab = "tab-2";
}
You should have to follow this way in your code this is example which will help you:
In ts file:
<template>
<v-tabs v-model="activeTab">
<v-tab v-for="tab in tabs" :key="tab.id" :to="tab.route">{{ tab.name }}
</v-tab>
<v-tabs-items v-model="activeTab" #change="updateRouter($event)">
<v-tab-item v-for="tab in tabs" :key="tab.id" :to="tab.route">
<router-view />
</v-tab-item>
</v-tabs-items>
</v-tabs>
</template>
Script:
export default {
data: () => ({
activeTab: '',
tabs: [
{id: '1', name: 'Tab A', route: 'component-a'},
{id: '2', name: 'Tab B', route: 'component-b'}
]
}),
methods: {
updateRouter(val){
this.$router.push(val)
}
}
}

How to route dynamic content?

I'm front end designer and trying to figure out how to use VueJS.
My github: https://github.com/soraname/mangarou
Here is my problem:
User click Soraname page
Route to Soraname page - (soraname.vue)
Load component Autor.vue - send prop or parameter
Load Soraname content
Basically, Page A route to Page A View, load Component with Page A content.
Valid if click Page B, C, D. Everyone use the same Component but change the content data.
I don't know how to do it, pass a variable to set what Data should load.
I've tried this:
soraname.vue (View)
<template>
<div class="soraname content-box">
<Autor v-bind="soraname"></Autor>
</div>
</template>
Load the Component Autor.vue
<template>
<div id="Autor">
<div class="row">
<div class="col s12 autor-header">
<img :src="pageautor.Logo" width="128"> <h1>{{pageautor.Nome}}</h1>
</div>
</div>
<div class="row">
<div class="col s12 autor-header">
<h3>Bio</h3>
<p>{{pageautor.Bio}}</p>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'autor',
components: {
},
data() {
return {
pageautor: {
Nome: "Soraname3333",
Bio: '/soraname',
Logo: '/assets/media/logos/soraname.png',
Color: 'purple accent-3',
links: [
{
Item: 'LINKSoraname',
Elo: '/soraname',
Text: 'lorem ipsum lorem ipsum lorem ipsum lorem ipsum ',
Img: '/autores/soraname/link01.png'
}
],
mangas: [
{
Titulo: 'MANGASoraname',
Elo: '/soraname',
Sinopse: 'lorem ipsum lorem ipsum lorem ipsum lorem ipsum ',
Img: '/assets/media/logos/soraname.png'
}
]
},
soraname: {
Nome: "SoranamDDDe",
Bio: '/soraname',
Logo: '/assets/media/logos/soraname.png',
Color: 'purple accent-3',
links: [
{
Item: 'LINKSoraname',
Elo: '/soraname',
Text: 'lorem ipsum lorem ipsum lorem ipsum lorem ipsum ',
Img: '/autores/soraname/link01.png'
}
],
mangas: [
{
Titulo: 'MANGASoraname',
Elo: '/soraname',
Sinopse: 'lorem ipsum lorem ipsum lorem ipsum lorem ipsum ',
Img: '/assets/media/logos/soraname.png'
}
]
}
}
},
methods: {
}
};
</script>
Router: https://github.com/soraname/mangarou/blob/master/src/router.js
Soraname View (Page A): https://github.com/soraname/mangarou/blob/master/src/views/soraname.vue
Autor.vue (Component): https://github.com/soraname/mangarou/blob/master/src/components/Autores/Autor.vue
Thank you!
In most cases, the data comes from a remote server, you can use your component to make an AJAX call and store the result however a better approach will be to use VUEX as a centralized state - with this approach you will also be able to use the author data across multiple components without the need to pass it over and over.
To make it clear, your route should have a parameter to identify the selected author (surname, id, whatever) - you do that with :surname when specifying the route. Then your component is loaded and read the parameter from the route this.$route.params.surname then use it to load the data from a remote server / vuex
In my opinion you don't need vuex you need a parent component (maybe layout) that gets the data has a computed property which it passes to the child component, the computed property is the data you want, it will update when your child component emits a change that triggers an event to change one of it's dependencies.
So basically parent handles data passes down to child and changes based on events emitted from child.

vue.js components : How to truncate the text in the slot element in a component?

Is there a way to apply a filter to the slot content in a Vue component?
To clarify, I would like to truncate the text included manually in the HTML. For example I would like to transform this:
<!-- In the view -->
<my-component>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Cumque,
laboriosam quasi rerum obcaecati dignissimos autem laudantium error
quas voluptatibus debitis?
</my-component>
into this:
<!-- Generated component -->
<div>
Lorem ipsum dolor sit amet, consectetur adipisicing ...
</div
I can't seem to find this information in the documentation.
Thank you.
The same thing on similar way can be:
in your main.js file:
var filter = function(text, length, clamp){
clamp = clamp || '...';
var node = document.createElement('div');
node.innerHTML = text;
var content = node.textContent;
return content.length > length ? content.slice(0, length) + clamp : content;
};
Vue.filter('truncate', filter);
in your template:
{{data.content | truncate(300, '...')}}
You can use a filter to truncate it.
//credit to #Bill Criswell for this filter
Vue.filter('truncate', function (text, stop, clamp) {
return text.slice(0, stop) + (stop < text.length ? clamp || '...' : '')
})
Then give the filter the length you want the string to be
<my-component>
{{'Lorem ipsum dolor sit amet, consectetur adipisicing' | truncate(50) }}
</my-component>
Within the child component, content from a slot is passed through as-is, and isn't available as a variable that you could truncate from the child end.
A small fix to #community answer:
Within component:
export default {
data: () => {
return {}
},
created() {
},
filters: {
truncate: function (text, length, suffix) {
if (text.length > length) {
return text.substring(0, length) + suffix;
} else {
return text;
}
},
}
}
or globally:
/** Vue Filters Start */
Vue.filter('truncate', function (text, length, suffix) {
if (text.length > length) {
return text.substring(0, length) + suffix;
} else {
return text;
}
});
/** Vue Filters End */
It still can be used the same way:
<div id="app">
<span>{{ text | truncate(10, '...') }}</span>
</div>
you can also do it like this:
export default {
data: () => {
return {
}
},
created(){
},
filters: {
truncate: function (text, length, suffix) {
if (text.length > length) {
return text.substring(0, length) + suffix;
} else {
return text;
}
},
}
}
or
Vue.filter('truncate', function (text, length, suffix) {
if (text.length > length) {
return text.substring(0, length) + suffix;
} else {
return text;
}
});
then use it like this:
<div id="app">
<span>{{ text | truncate(10, '...') }}</span>
</div>
If you want to know more vue filters, I suggest you read this: How to Create Filters in Vue.js with Examples
You can just use slice js method indicating begin and end positions of the string. More info
<my-component>{{ lorem.slice(0, 180) }}...</my-component>
<script>
export default {
data() {
return {
lorem:
"Lorem ipsum dolor sit amet, mel at clita quando. Te sit oratio vituperatoribus, nam ad ipsum posidonium mediocritatem, explicari dissentiunt cu mea. Repudiare disputationi vim in, mollis iriure nec cu, alienum argumentum ius ad. Pri eu justo aeque torquatos."
};
}
};
</script>
For nuxt applications this worked for me :
<div v-html="$options.filters.truncate(post.body)"></div>
And this is my Filter
filters: {
truncate: function (text, length) {
if (text.length > 30) {
return text.substring(0, 30) + '...'
} else {
return text
}
},
},