How to avoid vue component redraw? - vuejs2

I have prepared tag input control in Vue with tag grouping. Templates includes:
<script type="text/x-template" id="tem_vtags">
<div class="v-tags">
<ul>
<li v-for="(item, index) in model.items" :key="index" :data-group="getGroupName(item)"><div :data-value="item"><span v-if="typeof model.tagRenderer != 'function'">{{ item }}</span><span v-if="typeof model.tagRenderer == 'function'" v-html="tagRender(item)"></span></div><div data-remove="" #click="remove(index)">x</div></li>
</ul>
<textarea v-model="input" placeholder="type value and hit enter" #keydown="inputKeydown($event,input)"></textarea>
<button v-on:click="add(input)">Apply</button>
</div>
</script>
I have defined component method called .getGroupName() which relays on other function called .qualifier() that can be set over props.
My problem: once I add any tags to collection (.items) when i type anything into textarea for each keydown .getGroupName() seems to be called. It looks like entering anything to textarea results all component rerender?
Do you know how to avoid this behavior? I expect .getGroupName to be called only when new tag is added.
Heres the full code:
https://codepen.io/anon/pen/bKOJjo?editors=1011 (i have placed debugger; to catch when runtime enters .qualifier().
Any help appriciated.
It Man

TL;DR;
You can't, what you can do is optimize to reduce function calls.
the redraws are dynamic, triggered by data change. because you have functions (v-model and #keydown) you will update the data. The issue is that when you call a function: :data-group="getGroupName(item)" it will execute every time, because it makes no assumptions on what data may have changed.
One way of dealing with is is setting groupName as a computed key-val object that you can look up without calling the function. Then you can use :data-group="getGroupName[item]" without calling the function on redraw. The same should be done for v-html="tagRender(item)"

Instead of trying to fight how the framework handles data input events and rendering, instead use it to your advantage:
new Vue({
el: '#app',
template: '#example',
data() {
return {
someInput: '',
someInputStore: []
};
},
methods: {
add() {
if (this.someInputStore.indexOf(this.someInput) === -1) {
this.someInputStore.push(this.someInput);
this.someInput = '';
}
},
}
});
<html>
<body>
<div id="app"></div>
<template id="example">
<div>
<textarea v-model="someInput" #keyup.enter.exact="add" #keyup.shift.enter=""></textarea>
<button #click="add">click to add new input</button>
<div>
{{ someInputStore }}
</div>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.16/dist/vue.js"></script>
</body>
</html>
Event modifiers modifying
In the example, you can see that I am using 4 different event modifiers in order to achieve the desired outcome, but I will focus on the combination of them here:
#keyup.enter.exact - allows control of the exact combination of system modifiers needed to trigger an event. In this case, we are looking for the enter button.
#keyup.shift.enter - this is the interesting bit. Instead of trying to hackily prevent the framework from firing on both events, we can use this (and a blank value) to prevent the event we added into #keyup.enter.exact from firing. I must note that ="" is critical to the whole setup working properly. Without it, you aren't giving vue an alternative to firing the add method, as shown by my example.
I hope this helps!

Related

b-field value is getting updated only #select and not #input?

I am using the Buefy UI components in my VueJS project. I have a drop-down in a page:
<b-field label="Business Unit">
<b-autocomplete
:data="dataBusinessUnit"
placeholder="select a business unit..."
field="businessUnit"
:loading="isFetching"
:value="this.objectData.businessUnit"
#typing="getAsyncDataBusinessUnit"
#select="(option) => {updateValue(option.id,'businessUnit')}"
>
<template slot-scope="props">
<div class="container">
<p>
<b>ID:</b>
{{props.option.id}}
</p>
<p>
<b>Description:</b>
{{props.option.description}}
</p>
</div>
</template>
<template slot="empty">No results found</template>
</b-autocomplete>
</b-field>
As you can see from the above code, the updateValue function is responsible for updating the value, but it will currently be called only when the user selects something from the drop-down suggestions. I want the value to be updated even when the user starts to type something. Example: #input="(newValue)=>{updateValue(newValue,'businessUnit')}". However, there is already a debounce function called getAsyncDataBusinessUnit that I am calling to fetch the filtered autocomplete results based on what the user has typed during the #typing event.
According to the Buefy Autocomplete API documentation found here, you could probably use v-model instead of using value directly.
Alternatively you could actually implement the #input like you wrote yourself, the #typing event shouldn't interfere with it.
Or you could just handle the value updating in #typing:
#typing="onTyping"
// then later in JS...
methods: {
onTyping(value) {
this.updateValue(value, 'businessUnit')
this.getAsyncDataBusinessUnit(value)
},
}

How to render VueJs component based child node text?

======Updated with more background=====
I am working on a tool to convert text to a sequence diagram like this:
In the current implementation, the code (on the left) is provisioned programmatically by calling store.dispatch. I would like to make it simpler for other projects to integrate. What I wanted to achieve is to create a web component: <sequence-diagram />. It can be used in this way:
// Format A
<sequence-diagram>
response = BookController.Get(id)
....
</sequence-diagram>
The above DOM element would be rendered as a sequence diagrams (as shown on the right side of the above picture.
For the component to render properly, it needs to know what the "code" is. To pass the code (response = ....) to the component, I know I can use attributes and access it via props like this:
// Format B
<sequence-diagram code="response = ..." />
However, when the code is very long the above format is not as readable (imagine multiline code) as putting the code as child node text. If I use "Format A", how can I get the code content in my web component?
======Original question=====
What I want to implement is like this:
<my-component>
some text
</my-component>
I have managed to make it work by using attributes:
<my-component code="some text" />
Using child node text is much more readable in my case, as the text can be very long.
In the template, it already has a child component. The current template is like
<div> <myChildComponent/> </div>
I don't need to keep the text in the result dom.
I think what you want are slots. (See https://v2.vuejs.org/v2/guide/components.html#Content-Distribution-with-Slots).
The code of your component would look like this:
<div>
<slot></slot>
<myChildComponent/>
</div>
A runnable example
Below we have an alert-box component that displays an error message inside a <div> styled with a red background. This is how we use it:
<alert-box> This email address is already in use. </alert-box>
And it generates HTML that looks like this:
<div>
<strong>Error!</strong>
This email address is already in use.
</div>
See it in action:
Vue.component('alert-box', {
template: `
<div class="demo-alert-box" style="background-color: red; color: white;">
<strong>Error!</strong>
<slot></slot>
</div>
`
})
new Vue({
el: "#app"
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<alert-box> This email address is already in use. </alert-box>
<p> I'm a normal paragraph. </p>
</div>

VueJS - pass properties from one component to another in the template

I'm new to Vue, and am trying to build up a set of business components that rely on other lower-level components, and pass properties to them. None of the usual data-binding v-bind:propName="xxx" or :propName="xxx" seem to work from inside a component template however, and all the documentation treats prop passing from a top-level HTML page instead of from one component to another.
For example, with the below I have a lower-level component called "exchange-panel" which takes a "title" property. I want my higher-level component to use that generic exchange-panel and pass in a title to use. I get the following error:
[Vue warn]: Error compiling template:
<exchange-panel :title="The Panel Title">
<h1>test</h1>
</exchange-panel>
- invalid expression: :title="The Panel Title"
found in
---> <OrderbookDetail>
Here is the sample code (also at https://jsfiddle.net/ns1km8fh/
Vue.component("orderbook-detail", {
template:`
<exchange-panel :title="Public order book">
<h1>test</h1>
</exchange-panel>
`
});
Vue.component("exchange-panel", {
template:`
<div class="panel panel-default">
<div class="panel-heading">Title: {{ title }}</div>
<div class="panel-body">
<slot></slot>
</div>
</div>`,
props: ["title"]
})
new Vue({
el: "#exchange-container",
created: function () {
window.Vue = this
}
})
You need to quote Public order book.
<exchange-panel :title="'Public order book'">
The value of a bound attribute is treated as a javascript expression. Javascript strings need to be enclosed in quotes of some kind (single, double, backticks).
Updated fiddle.
As #RoyJ points out, you can also simply not bind it and use it as a normal attribute.
<exchange-panel title="Public order book">

Handling form submission without replacing template in VueJS

First of all, please be kind. I'm new to VueJS coming from the Angular world where things are different ;-)
I am creating a multi-page website using VueJS for simple things like a floaty header and submission of forms etc. I'd like the markup for my contact form to be in my HTML (rendered by the CMS) and I'd like to have VueJS handle the form submission and replacing the form with a thank-you message. So, a simplified version of the form would look like this.
<contact-form>
<form class="contact-form_form">
...
<input name="emailaddress" type="text" />
...
<button></button>
</form>
<div class="contact-form_thanks">
Thanks for filling in this lovely form
</div>
</contact-form>
So, the obvious thing to do is to create a VueJS component, but I don't want it to introduce a new template, I just want it to submit the form when the button is pressed (using Axios) and hide the form and show the thank you message.
I know how to do all of this in angular using attribute directives and ng-show/hide etc. but can't really see how to do this in VueJS because all the tutorials are geared to wards SPAs and Single file components with templates.
Any kick in the right direction would be appreciated.
Seems like you just want a data item indicating whether the form has been submitted, and v-if and v-else to control what displays in either case.
new Vue({
el: 'contact-form',
components: {
contactForm: {
data() {
return { hasBeenSubmitted: false };
},
methods: {
doSubmit() {
console.log("Some behind-the-scenes submission action...");
this.hasBeenSubmitted = true;
}
}
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<contact-form inline-template>
<div v-if="hasBeenSubmitted" class="contact-form_thanks">
Thanks for filling in this lovely form
</div>
<form v-else class="contact-form_form">
...
<input name="emailaddress" type="text" /> ...
<button #click.prevent="doSubmit">Submit</button>
</form>
</contact-form>

Using $refs with Element UI's input component

Is it possible to use ref with el-input component from Element-UI? I am trying to use $refsto focus on the input when my Vue instance is mounted. Here is my code:
<div id="app">
<el-input type="text" ref="test" placeholder="enter text"></el-input>
</div>
And in my Vue instance:
new Vue({
el: "#app",
mounted(){
this.$refs.test.focus()
}
})
The focus method is not working at all, even if I move this.$refs.test.focus() into a method and try to trigger it through an event.
The $refs object stores Vue components and should be working fine. The problem is that you are attempting to invoke a focus method which doesn't exist in the component, but rather on an input somewhere inside the component's template.
So, to actually find the input you'd have to do something like this:
this.$refs.test.$el.getElementsByTagName('input')[0].focus();
Not the prettiest line of code ever made, right? So, instead of calling anything in your app's mounted method, if you want to autofocus on the input, just do this:
<el-input type="text" placeholder="enter text" autofocus></el-input>
This can be also solved your problem.
// Focus the component, but we have to wait
// so that it will be showing first.
this.$nextTick(() => {
this.$refs.inputText.focus();
});
Now you can use it like this
<div id="app">
<el-input type="text" ref="test" placeholder="enter text"></el-input>
</div>
this.$refs.test.focus();
https://element.eleme.io/#/en-US/component/input#input-methods