Event handling after HTML injection with Vue.js - vue.js

Vue is not registering event handler for HTML injected objects. How do I do this manually or what is a better way to work around my problem?
Specifically, I send a query to my server to find a token in text and return the context (surrounding text) of that token as it exists in unstructured natural language. The server also goes through the context and finds a list of those words that also happen to be in my token set.
When I render to my page I want all of these found tokens in the list to be clickable so that I can send the text of that token as a new search query. The big problem I am having is my issue does not conform to a template. The clickable text varies in number and positioning.
An example of what I am talking about is that my return may look like:
{
"context": "When in the Course of human events, it becomes necessary for one people to dissolve the political bands which have connected",
"chunks": ['human events', 'one people', 'political bands']
}
And the resulting output I am looking for is the sentence looks something like this in psuedocode:
When in the Course of <a #click='search("human events")'>human events</a>, it becomes necessary for <a #click='search("one people")'>one people</a> to dissolve the <a #click='search("political bands")'>political bands</a> which have connected
This is what I have tried so far though the click handler is not registered and the function never gets called:
<v-flex xs10 v-html="addlink(context.context, context.chunks)"></v-flex>
and in my methods section:
addlink: function(words, matchterms){
for(var index in matchterms){
var regquery = matchterms[index].replace(this.regEscape, '\\$&');
var query = matchterms[index];
var regEx = new RegExp(regquery, "ig");
words = words.replace(regEx, '<a href=\'#\' v-on:click.prevent=\'doSearch("'+ query +'")\'>' + query + '</a>');
}
return words;
}
As I said, this does not work and I know why. This is just showing that because of the nature of the problem is seems like regex is the correct solution but that gets me into a v-html injection situation. Is there something I can do in Vue to register the event handlers or can some one tell me a better way to load this data so I keep my links inline with the sentence and make them functional as well?

I've already posted one answer but I've just realised that there's a totally different approach that might work depending on your circumstances.
You could use event delegation. So rather than putting click listeners on each <a> you could put a single listener on the wrapper element. Within the listener you could then check whether the clicked element was an <a> (using event.target) and act accordingly.

Here's one way you could approach it:
<template>
<div>
<template v-for="segment in textSegments">
<a v-if="segment.link" href="#" #click.prevent="search(segment.text)">
{{ segment.text }}
</a>
<template v-else>
{{ segment.text }}
</template>
</template>
</div>
</template>
<script>
export default {
data () {
return {
"context": "When in the Course of human events, it becomes necessary for one people to dissolve the political bands which have connected",
"chunks": ['human events', 'one people', 'political bands']
}
},
computed: {
textSegments () {
const chunks = this.chunks
// This needs escaping correctly
const re = new RegExp('(' + chunks.join('|') + ')', 'gi')
// The filter removes empty strings
const segments = this.context.split(re).filter(text => text)
return segments.map(segment => {
return {
link: segment.match(re),
text: segment
}
})
}
},
methods: {
search (chunk) {
console.log(chunk)
}
}
}
</script>
I've parsed the context text into an array of segments that can then be handled cleanly using Vue's template syntax.
I've used a single RegExp and split, which will not discard matches if you wrap them in a capture group, (...).
Going back to your original example, v-html only supports native HTML, not Vue template syntax. So you can add events using onclick attributes but not #click or v-on:click. However, using onclick wouldn't provide easy access to your search method, which is scoped to your component.

Related

vue js append parameters to URL

I am using vuejs3 and I want to make a filter. When user click to the link I want to append the url and push it to browser address bar for now. Later I will do ajax request to update page with product list.
So far I am able to send parameters to URL, but only one item from one group.From first color group I want user to select only one but from second size group I want user to select multiple.
I want this type of URL: localhost:8080/product?color=red&size=medium&size=large
<template>
<div class="products">
<div class="multi_filters">
<h1>Multi Filter By Color</h1>
Red color
Blue color
</div>
<div class="single_filter">
<h1>Multi Size</h1>
Medium
Large
</div>
</div>
</template>
<script>
export default {
data() {
return {
filters:{},
selectedFilters:{}
}
},
methods:{
activateFilter(key,value){
this.selectedFilters = Object.assign({},this.selectedFilters,{[key]:value})
console.log(this.selectedFilters)
this.$router.replace({
query: {
...this.selectedFilters
}
})
}
}
}
</script>
You are expecting size to be an array, this post will helps.
Submitting multi-value form fields, i.e. submitting arrays through GET/POST vars, can be done several different ways, as a standard is not necessarily spelled out.
Three possible ways to send multi-value fields or arrays would be:
?cars[]=Saab&cars[]=Audi (Best way- PHP reads this into an array)
?cars=Saab&cars=Audi (Bad way- PHP will only register last value)
?cars=Saab,Audi (Haven't tried this)

how to style html escaped data in Vue 2 or 3

I have user-generated data I'm displaying in a Vue app, so the default Vue behavior of html-escaping the data is perfect. Except, now I'd like users to be able to search that data, and I'd like to highlight the matching text in the search result. That means I need my own styling to not be escaped, even though all the original data should still be escaped.
In other words I need to apply my styling after the data has been html-escaped, eg:
1. user inputs data:
some original data that has special characters like > and <
2. Vue html-escapes this for safe display:
some original data that has special characters like > and <
3. dynamically style the search results
Eg if user searched for "original data" it becomes:
some <span class="my-highlight-style">original data</span> that has special characters like > and <
Notice how my dynamic styling was not html escaped even though the user input was.
I could of course just use v-html to bypass the html escape entirely, but then I lose all the safety and benefit of html escaping which I don't want to lose. Ideally I want to explicitly call Vue's html escape routine, then apply my styling so that it does not get escaped, then finally render all of that unescaped (since I already applied appropriate escaping programmatically).
Does Vue offer programmatic access to its html escape routine? (And I'm not talking about $sanitize which strips out special characters entirely, I want to preserve them just like normal Vue templating does). I could of course write my own escape routine, just wondered if I could leverage Vue's instead.
Vue uses the Browser's API for encoding HTML content, as mentioned here: https://v2.vuejs.org/v2/guide/security.html#HTML-content.
So, something like this should offer you the same kind of protection as Vue would from the raw user input. In the computed property, we pass the user data through the p element to encode it. Then we chain on top of that our own highlight computed property where we can inject our own HTML, and then show that with v-html.
<template>
<div id="app">
<div><label>Raw text:<br /><textarea v-model="text" cols="50" rows="10" /></label></div>
<div><label>Search for: <input type="text" v-model="search" /></label></div>
<p><label>v-html: <span v-html="text" /></label></p>
<p><label>Highlighted: <span v-html="highlight" /></label></p>
</div>
</template>
<script>
export default {
data() {
return {
text: "some original data that has special characters like > and <",
search: "original data"
}
},
computed: {
highlight() {
const html = this.safeHtml;
return html.replace(this.search, "<span class='my-highlight-style'>$&</span>");
},
safeHtml() {
var p = document.createElement("p");
p.textContent = this.text;
return p.innerHTML;
}
}
}
</script>
<style>
.my-highlight-style {
background: orange;
padding: 5px;
}
</style>

Vue.js: binding select boxes, but don't want to ajax all the options

Good day. I'm using Vue.js to render an arbitrary number of select elements from the data in a component.
Here's sample JSON data that indicates there are two select elements, each with one or more options.
{
"dropdowns":[
{
"cd":"UG9ydGZvbGlv",
"formname":"sp_filter_UG9ydGZvbGlv",
"nm":"Portfolio",
"selected":"1a",
"options":[
{
"cd":"1a",
"val":"Option 1A"
}
]
},
{
"cd":"UHJvZHVjdCBOYW1l",
"formname":"sp_filter_UHJvZHVjdCBOYW1l",
"nm":"Product Name",
"selected":"2b",
"options":[
{
"cd":"2a",
"val":"Option 2A"
},
{
"cd":"2b",
"val":"Option 2B"
}
]
}
]
}
Here's the template HTML:
<form>
<div v-for="dropdown in dropdowns">
<div v-if="dropdown.availableToView">
<h4>{{dropdown.nm}}</h4>
<select v-model="dropdown.selected" v-on:change="triggerUpdate">
<option value="">(Make a selection)</option>
<option v-for="option in dropdown.options" :value="option.cd">{{option.val}}</option>
</select>
</div>
</div>
</form>
So far so good.
I've got the data loading and Vue is building the dropdowns.
When the user changes any select box (remember there can be an arbitrary number of them), the trigger action needs to submit ALL of the elements in the form via ajax. It sounds like the most correct option is to bind the form fields to the underlying component data, as I've done.
My triggerUpdate looks like this:
methods: {
triggerUpdate: function() {
axios({
method: "post",
url: actionURL,
data: this.dropdowns
})
.then(response => (this.data = response));
}
}
...but this submits the entire dropdowns data element, including all of the options in each select box. It's unnecessary to send all of the options in. I just want to send each field name along with its selected option (i.e. the "value").
I know i could serialize the whole form and make that my ajax payload. But that seems to be making an "end run" around Vue.js. Everyone talks about having your form fields bound to the Vue model...is it then correct to basically ignore the model when making an ajax request whose purpose is to then update the model?
I'm relatively new to Vue.js so I'd appreciate help with what I'm overlooking here. How should I go about sending in the data from the form (a) while using proper Vue.js binding and (b) without sending extraneous data?
Thanks for your time.
If you need to post only the selected values, and you store those in each dropdown's selected property, the sensible approach seems to be just mapping it to a simple array of name/value objects.
Try this (it assumes the name of each field is the formname property, if it isn't you can just replace it):
var submitData = this.dropdowns.map((dropdown) => {
return { name: dropdown.formname, value: dropdown.selected };
});
Then you send submitData in your ajax request.

Is there a way to bind a variable number of queries?

I'm coding an app for managing shift work. The idea is pretty simple: the team is shared between groups. In those groups are specific shifts. I want to get something like that:
Group 1
- shift11
- shift12
- shift13
Group 2
- shift21
- shift22
- shift23
I already made a couple of tests, but nothing is really working as I would like it to: everything reactive, and dynamic.
I'm using vue.js, firestore (and vuefire between them).
I created a collection "shiftGroup" with documents (with auto IDs) having fields "name" and "order" (to rearrange the display order) and another collection "shift" with documents (still auto IDs) having fields "name", "order" (again to rearrange the display order, inside the group) and "group" (the ID of the corresponding shiftGroup.)
I had also tried with firestore.References of shifts in groups, that's when I was the closest to my goal, but then I was stuck when trying to sort shifts inside groups.
Anyway, with vuefire, I can easily bind shiftGroup like this:
{
data () {
return {
shiftGroup: [], // to initialize
}
},
firestore () {
return {
shiftGroup: db.collection('shiftGroup').orderBy('order'),
}
},
}
Then display the groups like this:
<ul>
<li v-for="(group, idx) in shiftGroup" :key="idx">{{group.name}}</li>
</ul>
So now time to add the shifts...
I thought I could get a reactive array of shifts for each of the groups, like that:
{
db.collection('shift').where('group', '==', group.id).orderBy('order').onSnapshot((querySnapshot) => {
this.shiftCollections[group.id] = [];
querySnapshot.forEach((doc) => {
this.shiftCollections[group.id].push(doc.data());
});
});
}
then I'd call the proper list like this:
<ul>
<li v-for="(group, idx) in shiftGroup" :key="idx">
{{group.name}}
<ul>
<li v-for="(shift, idx2) in shiftCollections[group.id]" :key="idx1+idx2">{{shift.name}}</li>
</ul>
</li>
</ul>
This is very bad code, and actually, the more I think about it, the more I think that it's just impossible to achieve.
Of course I thought of using programmatic binding like explained in the official doc:
this.$bind('documents', documents.where('creator', '==', this.id)).then(
But the first argument has to be a string whereas I need to work with dynamic data.
If anyone could suggest me a way to obtain what I described.
Thank you all very much
So I realize this is an old question, but it was in important use case for an app I am working on as well. That is, I would like to have an object with an arbitrary number of keys, each of which is bound to a Firestore document.
The solution I came up with is based off looking at the walkGet code in shared.ts. Basically, you use . notation when calling $bind. Each dot will reference a nested property. For example, binding to docs.123 will bind to docs['123']. So something along the lines of the following should work
export default {
name: "component",
data: function () {
return {
docs: {},
indices: [],
}
},
watch: {
indices: function (value) {
value.forEach(idx => this.$bind(`docs.${idx}`, db.doc(idx)))
}
}
}
In this example, the docs object has keys bound to Firestore documents and the reactivity works.
One issue that I'm trying to work through is whether you can also watch indices to get updates if any of the documents changes. Right now, I've observed that changes to the Firestore documents won't trigger a call to any watchers of indices. I presume this is related to Vue's reactivity, but I'm not sure.

How to change contents of a virtual dom element in Mithril?

How do I access a virtual dom element to change its contents using Mithril? I am new to Mithril and still trying to figure things out. For example, I want to access the third div with id "three" and change it's contents to "Blue Jays" without touching any of the other div's.
Thanks.
<div id='main'>
<div id='one'>Yankees</div><br>
<div id='two'>Red Sox</div><br>
<div id='three'>Orioles</div>
</div>
In mithril, like in react/vue/angular, you dont act on the actual DOM directly. Instead, you define the outcome that you want, so for example, to render the DOM tree that you posted you would do something like this:
var my_view = {
view: vnode => m('div#main', [
m('div#one', 'Yankees'),
m('div#two', 'Red Sox'),
m('div#three', 'Orioles')
])
}
m.mount(root, my_view)
<script src="https://cdnjs.cloudflare.com/ajax/libs/mithril/2.0.4/mithril.js"></script>
<div id="root"></div>
the m(...) functions inside the array have a string as their second argument, that makes the output static, but we can change that to a variable:
var my_view = {
oninit: vnode => vnode.state.fave_team = 'Orioles',
view: vnode => m('div#main', [
m('div#one', 'Yankees'),
m('div#two', 'Red Sox'),
m('div#three', vnode.state.fave_team)
])
}
m.mount(root, my_view)
<script src="https://cdnjs.cloudflare.com/ajax/libs/mithril/2.0.4/mithril.js"></script>
<div id="root">
</div>
In this case I used the state property of the vnode argument, but you can also use a third party state manager like flux or any other.
Now that we have it as a variable, it will show the current value on every call m.redraw(), most of the times we dont have to do this call ourselves, for example:
var my_view = {
oninit: vnode => {
vnode.state.fave_team = 'Orioles'
},
view: vnode => m('div#main', [
m('div#one', 'Yankees'),
m('div#two', 'Red Sox'),
m('div#three', vnode.state.fave_team),
m('button', { onclick: () => vnode.state.fave_team = 'Dodgers' }, 'Change')
])
}
m.mount(root, my_view)
<script src="https://cdnjs.cloudflare.com/ajax/libs/mithril/2.0.4/mithril.js"></script>
<div id="root"></div>
And thats it, any dynamic content in your DOM elements you set it as a variable/property in an object.
One of the beautiful things about mithril is that it doesnt force you to do things one specific way, so if you really want to work on the actual DOM node, there are lifecycle events that you can attach to any virtual node ("vnode")
You can easily capture the HTMLElement (i.e., HTMLInputElement) with the Mithril Lifecycle event of oncreate(). This is an actual example from my code (in TypeScript) where I need to hook up a few event listneres after the canvas element was created and its underlying DOM is available to me at "raw" HTML level. Once you get a hold of dom, then I manipulate that element directly. Many people think that why not use oninit(), but oninit() is before the generation of dom, so you will not get the element back at that stage.
Now, if you just do that, you will likely be posting another question - "Why the browser views not updating?" And that's because you do have to manually do a m.redraw() in your event handlers. Otherwise Mithril would not know when the view diffs to be computed.
const canvas = m(`.row[tabIndex=${my.tabIndex}]`, {
oncreate: (element: VnodeDOM<any, any>) => {
const dom = element.dom;
dom.addEventListener("wheel", my.eventWheel, false);
dom.addEventListener("keydown", my.eventKeyDown, false);
}
},