When should I use key vs. v-model for an input? - javascript

I am new to vue.js and stumbled across this section in the Conditional Rendering documentation, describing how vue tries to reuse as many components as possible. In the example an input is used, if you only would like to override different properties of the object, its straight forward. If you would like to exchange the whole element, you can use the key property.
Using key="whatever" on an input element works just fine, but when I use v-model="myAnswer" it has the same effect in the examples from the documentation.
let data = {happy: true, why: '', whyN: ''}
let standart = new Vue({
el: '#standart',
data: data
});
let withKey = new Vue({
el: '#withKey',
data: data
});
let withModel = new Vue({
el: '#withModel',
data: data
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="standart">
<i>Without anything</i>
<template v-if="happy">
<label>Why are you happy?</label>
<input placeholder="Because it's cool">
</template>
<template v-else>
<label>Why are you unhappy?</label>
<input placeholder="It's to slow">
</template>
<button #click="happy = !happy">Toggle</button>
</div>
<hr>
<div id="withKey">
<i>With key</i>
<template v-if="happy">
<label>Why are you happy?</label>
<input placeholder="Because it's cool" key="happy">
</template>
<template v-else>
<label>Why are you unhappy?</label>
<input placeholder="It's to slow" key="unhappy">
</template>
<button #click="happy = !happy">Toggle</button>
</div>
<hr>
<div id="withModel">
<i>With model</i>
<template v-if="happy">
<label>Why are you happy?</label>
<input placeholder="Because it's cool" v-model="why">
</template>
<template v-else>
<label>Why are you unhappy?</label>
<input placeholder="It's to slow" v-model="whyN">
</template>
<button #click="happy = !happy">Toggle</button>
</div>
I would like to know, how these functions compare, when to use which and overall what the advantages of each of them are. To me, the documentation doeesn't make the best job explaining.

Well, usually you need a key in lists and the keys need to come from a source so that a key of a given entry does not change over the time.
If you use a UUID generator, the keys will change at every re-render and you don't want this. Therefore the best solution is to use something like json-server for mocking things up or using a real backend api.
There are situations like with OAuth2 where by default you cannot send a POST object with Content-type application/json and the data in the body object since the protocol expects something www-form-urlencoded.
In this case you need something like
<form ref="someForm">
<input name="nameofinput" ref="myFancyInput" />
</form>
this way by accessing this.$refs you can do something like:
this.$refs["myFancyInput"]="somevalue"
this.$refs["someForm"].submit();
The contents of the input's value can be also a JSON, since JSON is just a string formatted in a given way.
Then in the backend you can process that as you want.
http://www.davidepugliese.com/csv-download-ajax-and-spring/
As for the v-model annotation, that is just shortcut for something like:
<input :value="someVar" #input="functionName($event)" />
functionName(event) {
console.log(event)
}

Related

Vue doesnt update component on dynamic variable condition change

I am working with Vuejs. I want to render components based on value of variable val.
My component looks like this
<template v-if="this.$val===1">
<component1 />
</template>
<template v-if="this.$val===2">
<component2 />
</template>
I have defined a global variable val using Vue.prototype and I am updating it using onclick function,where I am changing value of val to 2 but after clicking it doesnt show component2 instead of component 1.
Define val globally in main.js using following line of code
Vue.prototype.$val = 1;
Can someone please help me with this. Thanks
td,dr; Vue.prototypeis not reactive.
I'm going to enumerate issues as I observe them, hoping you'll find them useful.
You're not specifying which version of Vue you're using. Since you're using Vue.prototype, I'm going to guess you're using Vue 2.
Never use this in a <template>.
Inside templates, this is implicit (sometimes formulated: "inside templates this doesn't exist"). What would be this.stuff in controller, is stuff in the template.
You can't conditionally swap the top level <template> of a Vue component. You need to take the conditional either one level up or one level down:
one level up would be: you create separate components, one for each template; declare them and have the v-if in their parent component, rendering one, or the other
one level down would be: you move the v-if inside the top level <template> tag of the component. Example:
<template><!-- top level can't have `v-if` -->
<div v-if="val === 1">
val is 1
<input v-model="val">
</div>
<div v-else>
val is not 1
<input v-model="val">
</div>
</template>
<script>
export default {
data: () => ({ val: 1 })
}
</script>
Note <template> tags don't render an actual tag. They're just virtual containers which help you logically organise/group their contents, but what gets rendered is only their contents.1 So I could have written the above as:
<template><!-- top level can't have v-if -->
<template v-if="val === 1">
<div>
val is 1
<input v-model="val">
</div>
</template>
<template v-else>
<template>
<template>
<div>
val is not 1
<input v-model="val">
</div>
</template>
</template>
</template>
</template>
And get the exact same DOM output.
For obvious reasons, <template> tags become useful when you're working with HTML structures needing to meet particular parent/child constraints (e.g: ul + li, tr + td, tbody + tr, etc...).
They're also useful when combining v-if with v-for, since you can't place both on a single element (Vue needs to know which structural directive has priority, since applying them in different order could produce different results).
Working example with what you're trying to achieve:
Vue.prototype.$state = Vue.observable({ foo: true })
Vue.component('component_1', {
template: `
<div>
This is <code>component_1</code>.
<pre v-text="$state"/>
<button #click="$state.foo = false">Switch</button>
</div>
`})
Vue.component('component_2', {
template: `
<div>
This is <code>component_2</code>.
<pre v-text="$state"/>
<button #click="$state.foo = true">Switch back</button>
</div>
`})
new Vue({
el: '#app'
})
<script src="https://unpkg.com/vue#2.7.10/dist/vue.min.js"></script>
<div id="app">
<component_1 v-if="$state.foo"></component_1>
<component_2 v-else></component_2>
</div>
Notes:
<div id="app">...</div> acts as <template> for the app instance (which is, also, a Vue component)
Technically, I could have written that template as:
<div id="app">
<template v-if="$state.foo">
<component_1 />
</template>
<template v-else>
<component_2 />
</template>
</div>
, which is pretty close to what you were trying. But it would be slightly more verbose than what I used, without any benefit.
I'm using a Vue.observable()2 for $state because you can't re-assign a Vue global. I mean, you can, but the change will only affect Vue instances created after the change, not the ones already created (including current one). In other words, Vue.prototype is not reactive. This, most likely, answers your question.
To get past the problem, I placed a reactive object on Vue.prototype, which can be updated without being replaced: $state.
1 - there might be an exception to this rule: when you place text nodes inside a <template>, a <div> wrapper might be created to hold the text node(s). This behaviour might not be consistent across Vue versions.
2 - Vue.Observable() was added in 2.6.0. It's a stand-alone export of Vue's reactivity module (like a component's data(), but without the component). In v3.x Vue.Observable() was renamed Vue.reactive(), to avoid confusion/conflation with rxjs's Observable.
global variables are accessed in template without this keyword which means $val===1 will work.
Solution 1:
<template>
<component1 v-if='$val === 1' />
<component2 v-else/>
</template>
This will work.
But you could make use of dynamic components in your case.
Solution 2:
<template>
<component :is='currentComponent'/>
</template>
<script>
\\imports of your components will go here
export default {
name: 'app',
components: {
component1, component2
},
computed:{
currentComponent(){
return this.$val === 1?component1:component2;
}
}
}
</script>
Dynamic components are more performant and helps you maintain state of component.

Vue 3 Add Wrapper Around Each Child

I have a form component as the following:
<form-component>
<text-component name="test1" />
<select-component name="test2" />
</form-component>
I need FormComponent to be able to apply a wrapper div around each child
From the above code, FormComponent output should be something like this:
<form>
<div class="mb-3">
<text-component name="test1" />
</div>
<div class="mb-3">
<select-component name="test2" />
</div>
</form>
Here's one way to go about it:
const formChildren = [{
name: 'test1',
is: TextComponent
}, {
name: 'test2',
is: SelectComponent
}]
<form-component :children="formChildren" />
FormComponent.vue
<template>
<form>
<div
v-for="(child, index) in children"
:key="index"
class="mb-3"
>
<component v-bind="child" />
</div>
</form>
</template>
<script setup>
defineProps(['children'])
</script>
And here's a working demo of the suggestion you made in the comments, to loop through contents of $slots.default().
If you prefer writing your logic in template syntax, that's the way to go, I see nothing wrong with it.
I personally prefer the first approach, as my tendency is (in general), to limit the template syntax to minimum. Keeping components in object or map structures allows me to have granular control and automate tasks such as:
validation
event management
applying dynamic default values coming from configuration objects
handling browser/device quirks
My preference probably comes from having worked a lot in config driven environments, where the business logic is typically stored in objects. There's nothing wrong with writing it in template syntax, though but, overall, I find it limiting.

About Vue two-way data bindings: what if form field content doesn't come from direct user input?

So I've got some code like this:
<input name="date" v-model="date" id="date" type="text" value="" />
...then somewhere else:
<li><b>Date:</b> {{ date }}</li>
...And Vue:
var vueapp = new Vue({
el: '#form'
,data:{
date:''
}
});
Normally, this code will update li's content in real time while the user types into the input field. But in my case, I've assigned a javascript date picker to it, and the li's content doesn't get updated. I guess because there are no keyup (or the like) events. How to elegantly overcome this lovely issue?
You can use components for it, which will handle such scenarios gracefully.
For example using vuejs-datepicker:
const app = new Vue({
el: '#app',
components: {
vuejsDatepicker
},
data: {
date: ''
}
})
.as-console-wrapper{max-height:0!important;bottom:0;}
<script src="https://unpkg.com/vue"></script>
<script src="https://unpkg.com/vuejs-datepicker"></script>
<div id="app">
<vuejs-datepicker v-model="date"></vuejs-datepicker>
<br/>
<p><b>Date:</b> {{ date }}</p>
</div>
You can also create your own component for it and use it anywhere in your app.
Ok my own answer comes from #numbers1311407's suggestion. Essentially, I hooked into the plugins events from the mounted Vue's function, and then used the $set instance method to change the property I needed. So, to recap, here's my html:
<input name="project[deadline]" v-model="form.project.deadline" data-toggle="datepicker" id="project-deadline" type="text" value="" />
And here's my js:
var vueapp = new Vue({
el: '#form'
,data:{
form:{ // for tech reasons I'm using several nested empty objects here
contact:{}
project:{}
component:{}
// [...] other several objects ...
}
}
,mounted: function(){
$('[data-toggle="datepicker"]').on('hide.datepicker', function (e) {
_.delay(function(){ // from lodash.js
this.$set(this.form.project, 'deadline', $(e.target).val());
}.bind(this), 150); // because the plugin's event fires before the field gets updated
}.bind(this));
}
});
Note that I'm slightly delaying the assignment, but this is not a Vue thing. It's just because the plugin's event I'm using fires just before changing the value, and the plugin itself doesn't expose any event that fires after. So I have to wait a bit for the field to be updated and then I can pick up its value and assign it to Vue's model.
For reference, the plugin currently in use is this one.

Can I change element tag name with Polymer data binding?

I have this application which contains many types of Polymer elements that can be added to a main Polymer app element. This main element manages instances of these elements and shows them in a UI.
eg.
item1.html
item2.html
item3.html
my-app.html
As I add new types of items (eg. item4.html), I need to make several changes to the main UI to handle creating, managing, and showing them. Each type is unique enough that I do not want to merge them into a single item type.
What I'd like to do is have each Polymer element 'register' itself into my-app by calling a function which can add a new object to an array.
To do this, my-app will have a property called itemMap which is an array of objects. One property in this object is the type of item.
itemMap: [
{
type: 'item-1',
instances: []
}, {
type: 'item-2',
instances: []
}
...
]
This implementation works in code. When adding a new instance, I can add a new object to the instances array for that type. However, I do not know how to show the items in the UI. As each type is a different Polymer element, I cannot use a simple dom-repeat template. At the same time, I do not want to hardcode each type in the main UI to improve modularity.
Right now I have:
<iron-list id="item-1-list" items="[[item1_array]]" as="item" grid>
<template>
<div class="item">
<item-1 properties=[[item]]></item-1>
</div>
</template>
</iron-list>
<iron-list id="item-2-list" items="[[item2_array]]" as="item" grid>
<template>
<div class="item">
<item-2 properties=[[item]]></item-2>
</div>
</template>
</iron-list>
What I want to do is something like the snippet below, which would work for any type I create.
<template is="dom-repeat" items="{{itemMap}}" as="itemType" id="item-grid">
<iron-list id="[[itemType.type]]-list" as="item" grid items="[[itemType.instances]]">
<template>
<div class="item">
<[[itemType.type]] properties=[[item]]></[[itemType.type]]>
</div>
</template>
</iron-list>
</template>
However, this does not work.
Is this possible, or something equivalent, or am I going down the wrong path completely?
Polymer data binding does not work for tag names. What you might do to implement this kind of behavior is to create another custom element that accepts the item type as a property and dynamically creates an element of that type:
<template is="dom-repeat" items="{{itemMap}}" as="itemType" id="item-grid">
<iron-list id="[[itemType.type]]-list" as="item" grid items="[[itemType.instances]]">
<template>
<div class="item">
<x-item-selector type=[[itemType.type]] properties=[[item]]></x-item-selector>
</div>
</template>
</iron-list>
</template>
There could be several ways to implement the x-item-selector element: declarative and imperative:
Declarative: Create a set of <dom-if>s--one per type
If there are only a few element types and you can list them all, you could create a template for the x-item-selector like below:
<template>
<template is="dom-if" if="[[_isEqual(type, 'item-1')]]" restamp>
<item-1 properties="[[properties]]"></item-1>
</template>
...
</template>
Imperative: Observe the type property and update the child element manually
If you are going to support many types of elements, you might want to avoid a large set of <dom-if>s, and update the children of the x-item-selector element imperatively whenever the type property changes. As a downside, the mapping for the properties property you'll also have to establish manually.
_onTypeChanged(newType, oldType) {
if (newType !== oldType) {
for (let child of this.children) {
this.removeChild(child);
}
const newChild = document.createElement(newType);
newChild.properties = this.properties;
// also need to add some code to update newChild.properties
// when this.properties change
this.appendChild(newChild);
}
}

Using custom element content as item template

I'm writing reusable components for our internal framework that abstract away some monkey code. Most of the scenario's are implemented with slots and work great. However, some scenario's require rendering templates inside for loops, and unfortunately slots aren't supported there.
I came up with the following (working) code:
<template>
<div class="form-group">
<label for.bind="titleSafe" class="control-label">{title}</label>
<select id.bind="titleSafe" value.bind="value" class="form-control">
<option repeat.for="item of itemsSource" >
<template replaceable part="item-template" containerless>${item}</template>
</option>
</select>
</div>
</template>
This code has, IMO, multiple issues that make it a bad candidate for including it in a framework:
It doesn't support default templates like slots does, so when you have only 1 replacable part the syntax is needlessly verbose
Having to use 2 different templating systems (slots + replace-part) in my project seems really counter intuitive and will certainly create confusion/bugs in my dev team
When you use template parts in the example I provided above, you need to know I declared 'item' as iterator in my for loop in order to construct your template correctly
Therefore I went looking for alternatives. After some research I came up with something like this:
<template>
<div class="form-group">
<label for.bind="titleSafe" class="control-label">{title}</label>
<select id.bind="titleSafe" value.bind="value" class="form-control">
<option repeat.for="item of itemsSource" >
<!-- I want to insert my custom element here -->
</option>
</select>
</div>
<slot></slot>
</template>
The above is my select-item custom element. Then I would also create another custom element for the templating of the repeatable item, like select-item-template, I would then use the two together like this:
<select-item title="myTitle" items-source="myItems">
<select-item-template><span>${myItemsProperty}</span></select-item-template>
</select-item>
The strength of this approach would be that you can create complex 'root' custom elements with one default slot. In this slot, you could then define multiple custom 'child' elements that the root element can search for when it's initialized (I know you can do this with the #child and #children decorators, so that part is covered). I'm a bit lost on how I would have to use these custom child element's content in my root custom element though.. How would I take my span element in the above example and prepare it's content to be rendered in the repeater? And would it be possible to take the repeated item set it as the template's datasource so I don't have to specify itemin my templates?
I hope I didn't make this too verbose, but I wanted to explain what my functional requirement is. If you have any resource that can point me in the right direction I would be very grateful!
Use the processContent attribute to transform the element content into a part replacement. The component will still use replace-part internally but consumers of the component won't be exposed to this implementation detail.
https://gist.run?id=2686e551dc3b93c494fa9cc8a2aace09
picker.html
<template>
<label repeat.for="item of itemsSource" style="display: block">
<input type="radio" value.bind="item" checked.bind="value">
<template replaceable part="item-template">${item}</template>
</label>
</template>
picker.js
import {bindable, processContent} from 'aurelia-templating';
import {bindingMode} from 'aurelia-binding';
import {FEATURE} from 'aurelia-pal';
#processContent(makePartReplacementFromContent)
export class Picker {
#bindable itemsSource = null;
#bindable({ defaultBindingMode: bindingMode.twoWay }) value = null;
}
function makePartReplacementFromContent(viewCompiler, viewResources, element, behaviorInstruction) {
const content = element.firstElementChild;
if (content) {
// create the <template>
const template = document.createElement('template');
// support browsers that do not have a real <template> element implementation (IE)
FEATURE.ensureHTMLTemplateElement(template);
// indicate the part this <template> replaces.
template.setAttribute('replace-part', 'item-template');
// replace the element's content with the <template>
element.insertBefore(template, content);
element.removeChild(content);
template.content.appendChild(content);
return true;
}
}
usage
<template>
<require from="picker"></require>
<h1>Default Item Template</h1>
<picker items-source.bind="colors" value.bind="color"></picker>
<h1>Custom Item Template</h1>
<picker items-source.bind="colors" value.bind="color">
<em css="color: ${item}">
${item}
</em>
</picker>
</template>

Categories