Vue component loses input when asynchronous call updates another property - vue.js

I have a component that has an <input> with a :value set to a certain property. The component has another property unrelated to this input, which can get updated asynchronously (AJAX call). Whenever you're typing inside the input and the asynchronous call finishes, updating the other property, your typed input is reset.
To recreate this problem I've created a jsfiddle using a setInterval to simulate the async call and increment the other passed property. Try typing in the input, it will get reset every second. If you're quick enough, you can tab out and cause the #change to trigger the actual update.
The question is: why is the update to the other prop invalidating/rerendering the component and how can I work around this?
Note that v-model="person.name" is not a valid solution here - I need to know the old and new value, which is why I'm using a manual :value/#change combo.
Edit: The updateName method also really only needs to be triggered when the user leaves the input field. This is because the code run inside it is relatively CPU intensive and only needs to run when the user is done with the input and leaves it (in my actual code, not the jsfiddle).
Edit2: Is there some way to not let it re-render the entire component, but only the relevant pieces?

Because the parent component is changing a property of the child component, it has to re-render (parts of) the child component. Since you are using #change, instead of #input, your changes are not saved yet to the reactive variable person.name, it only works if you click tab quick enough. One solution would be to change #change to #input (which better resembles v-model):
https://jsfiddle.net/mf67xq1e/
Another (better) option is to use v-model and use a watcher to retrieve both the old and the new value:
watch: {
// whenever person.name changes, this function will run
"person.name": function (newName, oldName) {
console.log("newName:", newName);
console.log("newName:", oldName);
}
}
https://jsfiddle.net/mf67xq1e/1/
EDIT:
As you mentioned you only need to trigger something when you blur/leave the input field, seperate the reactivity of the variable and the triggering of your other method (e.g. updating some other variable or something), in two seperate variables:
https://jsfiddle.net/ta9cgnx0/
EDIT 2: Cleaner option with v-model and a seperate call for your other trigger on blur:
https://jsfiddle.net/ay1g63u8/

Related

Default-expand-all doesn't work for q-tree? Vue.Js

I have to code a web application and the most important element is the q-tree. I'm already able to load and show data (passing an array called list), but I want that all nodes are expanded.
The vue.js examples of the official documentation show that you're be able to do this with the 'default-expand-all' attribute but this isn't working for me.
It only shows me the root node with an arrow, where I have to expand the children nodes manually.
<q-tree
:nodes="list"
:selected.sync="selected"
#update:selected="onSelectionChangedNode"
node-key="NodeNr"
label-key="NodeTxt"
default-expand-all
></q-tree>
Taking a cue from the accepted answer, I realised that the dom has already been created with the tree component on first render.
In my use case, I want to update the Tree when data comes back from the server.
So, I had to force it to re-render with the expanded functionality using:
this.$nextTick(function () {
this.$refs.nodes.expandAll();
})
The nextTick function will update the dom in the next window of execution, by which time the nodes will get expanded by calling the expandAll function.
And NB: For those confused by the astericks on the ref attribute or how to add it to the component, here goes:
<q-tree :nodes="list"
:selected.sync="selected"
#update:selected="onSelectionChangedNode"
node-key="NodeNr"
label-key="NodeTxt"
ref="nodes"
>
Solved my problem as following:
I have added a ref attribute to the QTree DOM Element which makes it possible to access predefined methods of QTree API.
<q-tree
:nodes="list"
:selected.sync="selected"
#update:selected="onSelectionChangedNode"
node-key="NodeNr"
label-key="NodeTxt"
**ref="nodes"**
>
The function I have been using is expandAll().
updated() {
this.$refs.nodes.expandAll();
}
The most important thing for me was, I had to find out which lifecycle hook was the right one for me. The update() hook was the one I was looking for.
The reason:
Called after a data change causes the virtual DOM to be re-rendered and
patched.
The component’s DOM will have been updated when this hook is called, so you
can perform DOM-dependent operations here.
The default-expand-all is only applied on the first rendering of that Component.
So if your Component renders when the nodes aren't assigned they wont expand if assigned afterwards.
https://v1.quasar-framework.org/vue-components/tree
You have to work with scoped slots and an expanded attribute if you dont have the nodes on first rendering.

Vue Component - How to handle non-reactive data?

I'm losing track of reactivity overhead best practices in a Vue 2 Component. I need to generate a one time string with genId() and assign it to the component' id attribute. It seems like overkill to have any watching going on after that.
Is :id="myID" the right way to insert this into the html id?
And then when setting up the source where do I put the non-reactive data? I had 3 thoughts:
Add property myID: genId() to data. But doesn't that add it to the watchlist automatically even though it won't change? Causing overhead?
I read on Vue Forum from a year old answer that myID: genId() should go in the created hook. Are using hooks for this kind of thing a best practice? I thought hooks were discouraged.
Or I could put it in the component methods and then call it directly with :id="genId()
Is there a Vue way to do this?
Use method 2 for non-reactive data (and you use that component many many times on the page that the small overhead of adding the change listeners has any impact.)
created() {
this.myId = genId()
}
The 3 methods behave differently:
Method 1: data
This will call the genId() when the data object is created and add change listeners.
Method 2: created hook
This will call the genId() when the component object is created and doesn't add change listeners.
Method 3: method
This will call the genId() every time the template re-renders. Which happens every time a change is detected on a variable the view is listening to or a $forceUpdate() is called.
Ps. Vue objects already have a unique id: this._uid
but this is a private property and might change in a future version of Vue.

Vuejs - Set "global" prop to edit all properties without using v-model

Assuming that I have this component below:
<c-attachs v-for="item in attachs" v-bind:path="item.path"></c-attachs>
And try to edit some property directly from some method, such as:
methods: {
changeProp: function ()
{
this.path = 'myNewString';
}
}
Vuejs warns on the console with the message:
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders
But... if I set "v-bind:allprops="item" and edit property directly through "allprops" object (such as code below), it works fine without error. My doubt is... Is this the correct way to edit property on events without using v-model?
this.allprops.path = 'myNewString';
There is no correct way to edit props, because you are not supposed to edit them.
Every component should have complete control over its own data. That keeps behavior easy to reason about. Items that are passed to children as props should be considered read-only, so that the owner of those items retains control. That is why Vue has events.
When something happens in a component that should affect data that the component doesn't own, the component should issue an event so that the owner of the data item can handle it. If the owner of the data item changes its value, that change will flow down through the props.

Route binding not updating sub component

I have a a hard time understanding how components and routes works together. In the documentation, they only talk about one level of components. In case there is multiple level, it does not look like it is working.
I made this http://jsfiddle.net/uvqpracr/7/
and when you click on init(1) it initialize the counter with 1 and when you click on init(5) it initialize the counter with 5. In the route component, I declare v-bind:init-counter="$route.params.initCounter so when I am in counter-container, writing {{init-counter}} works, but in the subcomponent counter, event if I wrote v-bind:init-counter="initCounter" it does not work.
In this documentation, I can read:
One thing to note when using routes with params is that when the user navigates from /user/foo to /user/bar, the same component instance will be reused. Since both routes render the same component, this is more efficient than destroying the old instance and then creating a new one. However, this also means that the lifecycle hooks of the component will not be called.
I wonder if this is the reason why what I am trying to do does not work. If so I really wonder how I should do it in a simple way.
First of all, yes, the components (counter-container and its child counter) are created just once.
See the log at this demo JSFiddle. No matter how many times you click the links, the created()s are only called once each (see the console).
and when you click on init(1) it initialize the counter with 1 and when you click on init(5) it initialize the counter with 5
Not quite. I mean, the clicks don't always initialize the counter variable.
Actually, when you click them, the route changes and then initCounter (not counter, not total) changes.
At the first click, because the components haven't been created before, then the value of initCounter will be used to initialize the counter (and total).
But in subsequent clicks, even though the initCounter does change even for nested components, it won't affect the counter/total variables because they have already been created.
Check the demo JSFiddle. I added the displaying of counter: {{ counter }} / initCounter: {{ initCounter }}, so you'll see initCounter changes in the counter component as well.
Updating every time
So, you now know that initCounter will be used to set counter/total only once, only when the components are first created.
If you want to update them whenever initCounter changes, the solution is to watch the route (using watch: { '$route' (to, from) { /* react here */ } }) or, more specifically, watch initCounter.
Check this other demo JSFiddle. This one uses watch and updates counter/total whenever initCounter is updated.

Vue.js 2.0 - creating reusable components

I am trying to create fully reusable component using Vue.js 2 and single file components, and right now my approach seems to be impossible to realize.
The goal is to create component for creating forms for a complex, nested JSON structure. This structure is supposed to be edited and then sent to the server. The component itself displays a header and submit button but the fields along with their placing is entirely the responsibility of the user of my component. (front-end engineer)
The MyForm component (implementation is not relevant here) is passed the JSON data and url to post them to.
The form is supposed to be reusable by many other users and the contents of the form itself is supposed to be not relevant. It may have a mix of html/inputs/custom components as children.
Let's imagine a simple scenario without data nesting with the following data:
var mymodel={ name : "My name", surname : "My surname" }
And a form i would like to create using my component:
<MyForm :model="mymodel" :url="http://localhost/post">
<div>
<MyTextInput v-model="model.name" label="Name"/>
<MyPanel>
<MyTextInput v-model="model.surname" label="Surname"/>
</MyPanel>
</div>
</MyForm>
Therefore:
MyForm gets passed a model to submit, stores it in data
MyTextInput is a custom component for displaying input with label
Second MyTextInput is the same component but created in another component contained called 'MyPanel' since this field needs to be placed differently.
As we can see there are many problems with passing variables and composition itself:
Composition:
If i put a <slot></slot> in the tempplate of MyForm for displaying the fields it would be compiled in parent scope, therefore all children (including MyTextField) would not have access to the "model"
If i try to use <MyForm inline-template> i cannot automatically display the form header and footer since all content is being replaced. Additionally when using single file components the compiler will look for all components inside the inline-template which means that i would have to import MyTextInput and MyPanel into MyForm which is not practical. I do not know in advance all components that will never end up in my form!
Passing variables:
If i use the variables directly from "model" (in first TextInput) i receive warning that i am modifying a variable from parent and it will be overwritten on next render (but in this case it will not be overwritten since i am INTENTIONALLY modifying the parent)
I cannot pass the model into second MyTextInput without passing it to MyPanel first. Actually i would have to pass it into EVERY custom component in between. And i do not know in advance how many custom components will there be. Which means that i would have to modify the code of every component that would ever be put into MyForm and require users to pass the data for each custom component they include.
If i would try to properly inform the parent about changes i would need to add v-on: event to every textinput and every custom component in between in order for the event to reach MyForm.
As i have said the component was supposed to be simple and easilly reusable. Requiring users of this component to modify code of every child they put into it and requiring them to add v-on: to every component inside does not seem practical.
Is my idea solvable using Vue.js 2.0 ? I have designed the same component before for AngularJS (1.5) and it was working fine and did not require to add modifications to each child of the form.
I've been using a ui framework based on vue 2.0 and you may get some ideas from its implementation. Based on its implementaion and my little experience with it, I think it's the person who uses your framework's responsibility to assemble the form-model. Also, for a form, we can always easily get all the data to be sent by using fields' value props without v-model's help.
The framework's doc on form element may also be helpful but it's currently only available in Chinese except for the code samples.
I suggest you to use Form Input Components using Custom Events to pass variables in your form.
Mutating a prop locally is now considered an anti-pattern, e.g.
declaring a prop a and then set this.a = someOtherValue in the
component. Due to the new rendering mechanism, whenever the parent
component re-renders, the child component's local changes will be
overwritten. In general, in 2.0 you should treat props as immutable.
Most use cases of mutating a prop can be replaced by either a data
property or a computed property.