How to specify multiple dynamic attributes by single computed prop in VueJS - vue.js

I have this html element:
Link text
I want to add data-tooltip and title attributes dynamically by condition:
Link text
Is there any way in VueJS to add multiple dynamic attributes at same time:
<!-- instead of this: -->
Link text
<!-- something like this: -->
<a href="javascript:" ...tooltipAttributes >Link text</a>

You could take advantage of v-bind on the DOM element you wish to apply multiple attributes to based on some dynamically changing condition.
Here's a Plunker example demonstrating how you might go about it.
Take note of the object returned:
computed: {
multiAttrs() {
return this.showAttrs ? {
'data-toggle': 'tooltip',
title: 'Some tooltip text',
} : null;
}
}

You should be able to use v-bind="tooltipAttributes"
the docs here https://v2.vuejs.org/v2/api/#v-bind have more info, but the key part is under usage
Dynamically bind one or more attributes, or a component prop to an expression.

From the Docs:
1. You can dynamically bind multiple attributes/props to a single element by using v-bind:
(no colon, no extra attribute, just v-bind)
<a href="#" v-bind="tooltipAttributes" >Link text</a>
2. And then declare the variable in the computed section:
(you can also declare it in the data section, but that would require manual direct value changes)
computed() {
return {
tooltipAttributes: {
title: 'Title',
'data-toggle': this.toggle === true && !disabled
}
}
}
Note: Attributes with dashes/hyphens - in them (e.g. data-toggle) need to be a string because Javascript doesn't recognize - as a valid symbol in variable naming.
This is THE SAME AS:
<a href="#" title="Title" :data-toggle="this.toggle === true && !disabled" >Link text</a>

Related

Vue class components dynamically add component depending on answer from backend

So from the backend I get a array of objects that look kind of like this
ItemsToAdd
{
Page: MemberPage
Feature: Search
Text: "Something to explain said feature"
}
So i match these values to enums in the frontend and then on for example the memberpage i do this check
private get itemsForPageFeatures(): ItemsToAdd[] {
return this.items.filter(
(f) =>
f.page== Pages.MemberPage &&
f.feature != null
);
}
What we get from the backend will change a lot over time and is only the same for weeks at most. So I would like to avoid to have to add the components in the template as it will become dead code fast and will become a huge thing to have to just go around and delete dead code. So preferably i would like to add it using a function and then for example for the search feature i would have a ref on the parent like
<SearchBox :ref="Features.Search" />
and in code just add elements where the ItemsToAdd objects Feature property match the ref
is this possible in Vue? things like appendChild and so on doesn't work in Vue but that is the closest thing i can think of to kind of what I want. This function would basically just loop through the itemsForPageFeatures and add the features belonging to the page it is run on.
For another example how the template looks
<template>
<div class="container-fluid mt-3">
<div
class="d-flex flex-row justify-content-between flex-wrap align-items-center"
>
<div class="d-align-self-end">
<SearchBox :ref="Features.Search" />
</div>
</div>
<MessagesFilter
:ref="Features.MessagesFilter"
/>
<DataChart
:ref="Features.DataChart"
/>
So say we got an answer from backend where it contains an object that has a feature property DataChart and another one with Search so now i would want components to be added under the DataChart component and the SearchBox component but not the messagesFilter one as we didnt get that from the backend. But then next week we change in backend so we no longer want to display the Search feature component under searchbox. so we only get the object with DataChart so then it should only render the DataChart one. So the solution would have to work without having to make changes to the frontend everytime we change what we want to display as the backend will only be database configs that dont require releases.
Closest i can come up with is this function that does not work for Vue as appendChild doesnt work there but to help with kind of what i imagine. So the component to be generated is known and will always be the same type of component. It is where it is to be placed that is the dynamic part.
private showTextBoxes() {
this.itemsForPageFeatures.forEach((element) => {
let el = this.$createElement(NewMinorFeatureTextBox, {
props: {
item: element,
},
});
var ref = `${element.feature}`
this.$refs.ref.appendChild(el);
});
}
You can use dynamic components for it. use it like this:
<component v-for="item in itemsForPageFeatures" :is="getComponent(item.Feature)" :key="item.Feature"/>
also inside your script:
export default {
data() {
return {
items: [
{
Page: "MemberPage",
Feature: "Search",
Text: "Something to explain said feature"
}
]
};
},
computed: {
itemsForPageFeatures() {
return this.items.filter(
f =>
f.Page === "MemberPage" &&
f.Feature != null
);
}
},
methods: {
getComponent(feature) {
switch (feature) {
case "Search":
return "search-box";
default:
return "";
}
}
}
};

Can I pass metadata through Vue Formulate's schema API without affecting input attributes?

The goal:
generate form fields from JSON/CMS
have a param in the JSON that allows two fields to sit next to each other on a single line
The solution so far:
I’m using Vue Formulate's schema API to generate fields. In Vue Formulate's options, I can conditionally add a class to the outer container based on a parameter in the context.
classes: {
outer(context, classes) {
if (context.attrs.colspan === 1) {
return classes.concat('col-span-1')
}
return classes.concat('col-span-2')
},
I’m using Tailwind, which requires no classname concatenation and actually want the default to be col-span-2, so if you’re inclined to copy this, your logic may vary.
With a few classes applied to the FormulateForm, this works really well. No additional wrapper rows required thanks to CSS grid:
<FormulateForm
v-model="values"
class="sm:grid sm:grid-cols-2 sm:gap-2"
:schema="schema"
/>
The schema now looks something like this:
[
{
type: 'text',
name: 'first_name',
label: 'First name',
validation: 'required',
required: true,
colspan: 1,
},
The problem/question
Vue Formulate’s schema API passes all attributes defined (other than some reserved names) down to the input element. In my case, that results in:
<div
data-classification="text"
data-type="text"
class="formulate-input col-span-1"
data-has-errors="true"
>
<div class="formulate-input-wrapper">
<label
for="formulate-global-1"
class="formulate-input-label formulate-input-label--before"
>
First name
</label>
<div
data-type="text"
class="formulate-input-element formulate-input-element--text"
>
<input
type="text"
required="required"
colspan="1" <--------------- hmm…
id="formulate-global-1"
name="first_name"
>
</div>
</div>
</div>
I recognize that I can name my attribute data-colspan so that I’m not placing a td attribute on an input, but I think of colspan as metadata that I don’t want applied to the template. Is there a way to prevent this from being applied to the input—perhaps a reserved word in the schema API that allows an object of metadata to be accessed via context without getting applied to v-bind="$attrs"?
The vue-formulate team helped me out on this one. Very grateful. Much love.
There is a way to prevent it from landing on the input, and that's to use the reserved outer-class property in the schema:
[
{
type: 'text',
name: 'first_name',
label: 'First name',
validation: 'required',
required: true,
'outer-class': ['col-span-1'],
},
This means that I don't need to do this at all:
classes: {
outer(context, classes) {
if (context.attrs.colspan === 1) {
return classes.concat('col-span-1')
}
return classes.concat('col-span-2')
},
vue-formulate supports replacing or concatenating classes via props. I managed to overlook it because I didn't recognize that everything you pass into the schema API is ultimately the same as applying a prop of that name.
Classes can be applied to several other parts of the component as well—not just the outer/container. More information here:
https://vueformulate.com/guide/theming/customizing-classes/#changing-classes-with-props

Event handling after HTML injection with 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.

How to dynamically generate css class inside an each statement for an Ember View

<div>
{{#each value in controller}}
<div {{classNameBindings "col-lg-{{value}}"}}>{{value}}</div>
{{/each}}
</div>
Above is my partial view.
I want to generate classes like: col-lg-1, col-lg-2 etc
My controller is:
App.circleController = Ember.ArrayController.extend({
setupController: function(controller) {
controller.set('content', [1,2,3,4,5,6,7]);
}
});
why I get error: assertion failed: an Ember.CollectionView's content must implement Ember.Array. ?
I use a custom view to apply dynamically-named classes to items inside of an each helper. The class name is generated inside the view by a property than depends on a supplied index.
App.ItemView = Ember.View.extend({
classNameBindings: ['itemClass'],
index: null,
itemClass: function() {
return 'class-'+this.get('index');
}.property('index')
});
In the template, I supply the index through a {{view}} helper inside each iteration.
{{#each value in controller}}
{{#view App.ItemView indexBinding="value"}}
Item #{{value}}
{{/view}}
{{/each}}
For a closer look, check out this jsfiddle.

Can I bind data to data-win-options?

I use the control MicrosoftNSJS.Advertising.AdControl in the ItemTemplate of a ListView.
I would like to bind some datas to the following data-win-options properties : ApplicationId and AdUnitId
The source datas are correctly set and are visible in my item template, I can display them with an h2 + a classic data-win-bind on innerText property
Ads are displayed correctly if I put directly static IDs in html code but these IDs need to be loaded from a config file...
Is it possible ? Thanks
If it's not possible, can I modify directly the item template in the JS code before to be injected in the listview ?
Come to find out this is possible (I was trying to do something similar)
The syntax for the control properties must be prefixed with winControl.
Example (I'm setting the application id here but binding the html element's className and the ad control's adUnitId)
<div id="adItemTemplate" data-win-control="WinJS.Binding.Template">
<div data-win-bind="className:css; winControl.adUnitId: adUnitId"
data-win-control="MicrosoftNSJS.Advertising.AdControl"
data-win-options="{ applicationId: 'd25517cb-12d4-4699-8bdc-52040c712cab'}">
</div>
</div>
I finally found a way to perform this without real binding, by using the itemTemplateSelector function like this :
function itemTemplateSelector(itemPromise)
{
return itemPromise.then(function (item)
{
if (item.type == "ad")
{
var template = _$(".adTemplate").winControl.render(item, null);
// Access to the AdControl through the DOM
var adControl = template._value.childNodes[1].childNodes[1].winControl;
// Set options that are specified in the item
WinJS.UI.setOptions(adControl, { applicationId: item.AdAppId, adUnitId: item.AdUnitId });
return template;
}
else
{
return _$(".itemTemplate").winControl.render(item, null);
}
}
}
I had this problem in ratings:
<div data-win-control="WinJS.UI.Rating" data-win-options="{averageRating: 3.4, onchange: basics.changeRating}"></div>
I bind it via winControl:
<div data-win-control="WinJS.UI.Rating" data-win-bind="winControl.averageRating: myrating" data-win-options="{onchange: basics.changeRating}"></div>
It worked fine.
<div data-win-bind="this['data-list_item_index']:id WinJS.Binding.setAttribute" >