Vue 3 Add Wrapper Around Each Child - javascript

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.

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.

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

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)
}

Dynamic $refs can't be referenced in Javascript if a function is used on them [VUE]

I am dynamically generating $ref names using a template for-loop.
If I console log a specific item in the for-loop (see example), it can be reached. However when I take that same reference and call a function on it like focus() or scrollToView(), the console becomes woefully confused and throws an error:
vue.js:1743 TypeError: this.$refs.element2.focus is not a function
So just to be clear:
console.log(this.$refs['element2']); <-- WORKS!
this.$refs['element2'].focus(); <-- DOESNT WORK :(
https://jsfiddle.net/3xj7afgh/238/
Here's your template from the fiddle:
<div v-for="(result, index) in data" :key="index">
<input type="text" type="file" :ref="'element' + result.id" />
</div>
The following excerpt from the docs applies in this situation:
When used on elements/components with v-for, the registered reference will be an Array containing DOM nodes or component instances.
So you'd need to do this:
this.$refs['element2'][0].focus()
Mixing dynamic refs and v-for can get messy, as you can see.
Alternatively you can use a static ref to achieve the same sort of thing:
<div v-for="(result, index) in data" :key="index">
<input type="text" type="file" ref="elements" />
</div>
// Index 1 is the second element in the DOM
this.$refs.elements[1].focus()

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>

KnockoutJs - handling several areas with the same fields

I have several regions with repeatable content which is generated on the server-side. I use knockout-js to dynamically hide/show regions within areas. My markup is like the following:
<div>
<input type="checkbox" data-bind="checked: a1" />
<div data-bind="visible: a1">region0</div>
</div>
<div>
<input type="checkbox" data-bind="checked: a2" />
<div data-bind="visible: a2">region1</div>
</div>
<script>
var viewModel = {
a1: ko.observable(false),
a2: ko.observable(false)
};
ko.applyBindings(viewModel);
</script>
Lets say I have 10 such regions. Is there a more convenient/better way to achieve the same?
Lets say, that I explicitly do not want to use foreach binding and generate markup on the client (for site to be accessible with disabled js).
Is there any way to omit viewModel specification (part within script tags), since it feels to me that knockout could detect and auto-create fields for me?
P.S. I'm a JS-novice, so excuse me for simple questions :)
Is there any way to omit viewModel specification (part within script tags), since it feels to me that knockout could detect and auto-create fields for me?
Although I prefer Knockout personally, you might want to take a look at Angular. Angular does automatically create view model properties as this example shows.

Categories