Vue Dynamic Component Ordering? - vue.js

Thanks in advance for the help.
I'm using the Vue's dynamic component tag in an application.
https://v2.vuejs.org/v2/guide/components.html#Dynamic-Components
The component list is provided via a computed property which I simply iterate over creating <component /> elements.
<component v-for="component in myComponents" :key="component" is="component />
The issue i'm running into is the components are loaded asynchronously and some components can take longer at times than others to load. Due to this sometimes the components load out of the desired order.
I was curious if anyone had a suggestion on how I might be able to force the components to display in a desired order?

You can try to use an ordered computed property, for exemple if you want to order your components by name.
In the template :
<component v-for="component in orderedComponents" :key="component" is="component />
In the script
computed: {
orderedComponents: function () {
return _.orderBy(this.myComponents, 'name')
}
}
This exemple is using Lodash but of course you can order it the way you want.
You can check the documentation : https://v2.vuejs.org/v2/guide/migration.html#Replacing-the-orderBy-Filter

Yes, you can enforce the order by setting :key to a number.
First, modify your for loop to use indexes:
v-for="(index, value) in myComponents"
Then set key to the index
:key="index"
This should keep the order in tact - if it doesn't, you could always create placeholders first then replace them in the runtime.

Related

Vue 2 - How / is it possible to create a unique layout during a v-for loop?

I am trying to create a questionnaire. I have an array of questions. Each question is an object. During the loop the <component :is> checks the component property inside the question object. If the property equals an Input for example then an input will be shown and so on.
This works for simple questions. However the last question requires a more complex layout. Here 'Please add items' needs to have two inputs and an add button. Once pressed a table will appear with each row displaying the values passed into the fields from above. At the moment I can't do this as I am looping though a sub set of questions.
There could be 1000+ questions eventually and I am not sure whether creating a component for each question is the right approach?
I know my current approach isn't right some how but I am completely stuck how to approach this. Is there a way of looping through data and providing unique layouts for each question? The data structure isn't set in stone so feel free to change it.
https://codesandbox.io/embed/blazing-wood-ifnxym?fontsize=14&hidenavigation=1&theme=dark
Within the v-for, you can wrap the elements in a <template>, and then just use v-if to determine which element is displayed.
For example :
<template v-for="question in question.questions" :key="question.id">
<Input v-if="quetion.type === 'text'" :question="question" />
<Radio v-if="quetion.type === 'choice'" :question="question" />
...
</template>

Vue: Event does not emit several values. Is there something wrong with v-model?

I'm Vue beginner and have tried setting up a simple app that takes in some user input to display a result on an extra page / component.
Component A has 2 sliders. I'd like to pass both values to Component C. Currently only one value is passed.
I spent hours on this and have already received super valuable support on another question by #RoboKozo, but this keeps me from progressing any further.
Please find my current code here.
Bind both props, and emits
<component
:is="selectedComponent"
:modelValue="value"
:modelValue2="value2"
#update:modelValue="value = $event"
#update2:modelValue2="value2 = $event"
>
</component>
The event name for updating the model value of modelValue2 should be update:modelValue2 (not update2:modelValue2):
// ComponentC.vue
this.$emit('update:modelValue2', this.local2)
Then to bind component.modelValue2 to App.value2, specify modelValue2 as the v-model binding argument:
<component v-model="value" v-model:modelValue2="value2">
Note you don't need to specify the binding argument for modelValue because that's the default.
demo

How to use `v-if` and `v-for` on the same element?

Hi I'm trying to figure out how to use v-if on a iterated element which also uses v-for. I need to check if the current element has any of a series of classes, which are numbers.
so the classes of each article would be:
<article class="post-item post-guide 12 22 19 30 55">...
this is the HTML that renders all:
<article v-if="showIfHasClass" :class="'post-item post-guide ' + guide.categories.toString().replace(/,/g, ' ')"
v-for="(guide, index) in guides" :key="index">
<header>
<h1 class="post-title">
{{ guide.title.rendered}}
</h1>
</header>
</article>
I have tried with methods that check the class of all elements, that works, but i'm trying to use a clean Vue built-in solution with v-if without success, i'm not able to retrieve the class of this in a successful way.
Should showIfHasClass be a computed property? I have tried with that too... but it seems, I'm missing something along the way.
my data I have to check against is an array:
data:{
guides: [...]
selectedCategories: [1, 22, 33, 100, 30];
}
or maybe it is better to directly loop over the guides and check if they have the selectedCategory or not, then remove the element from the guides data array?
What is more effective?
Besides the option to create an additional filtered computed (effectively eliminating the need to use v-for and v-if on the same element), you also have a template level way of dealing with such edge-cases: the <template> tag.
The <template> tag allows you to use arbitrary template logic without actually rendering an extra element. Just remember that, because it doesn't render any element, you have to place the keys from the v-for on the actual elements, like this:
<template v-for="(guide, index) in guides">
<article v-if="isGuideVisible(guide)"
:key="index"
class="post-item post-guide"
:class="[guide.categories.toString().replace(/,/g, ' ')]">
<header>
<h1 v-text="guide.title.rendered" />
</header>
</article>
</template>
isGuideVisible should be a method returning whether the item is rendered, so you don't have to write that logic inside your markup. One advantage of this method is that you can follow your v-if element with a fallback v-else element, should you want to replace the missing items with fallback content. Just remember to also :key="index" the fallback element as well.
Apart from the above use-case, <template> tags come in handy when rendering additional wrapper elements is not an option (would result in invalid HTML markup) (i.e: table > tr > td relations or ol/ul > li relations).
It's mentioned here as "invisible wrapper", but it doesn't have a dedicated section in the docs.
Side note: since you haven't actually shown what's inside guide.categories, I can't advise on it, but there's probably a cleaner way to deal with it than .toString().replace(). If guide.categories is an array of strings, you could simply go: :class="guide.categories".
I think the most Vue way is to create a computed property with filtered items from selected categories, then use that in v-for loop (the idea is to move the business logic away from template).
computed: {
filteredItems(){
return this.guides.filter(e => this.selectedCategories.includes(e.category))
}
}
Also, as a note, it is not recommended to use v-if and v-for on the same element, as it may interfere with the rendering and ordering of loop elements. If you don't want to add another level of nesting, you can loop on a 'template' element.
<template v-for="item in items">
// Note the key is defined on real element, and not on template
<item-element v-if='condition' :key="item.key"></item-element>
</template>

Component mounted twice

I have a simple component which is rendered by a click function, but it gets rendered twice, this is my code.
<SeeCompany
:is="create"
v-bind:companyId="companySelected"
#closeChild="closeModule"
/>
when i clicked in the button i change the create value to 'SeeCompany' so it gets mounted, but it repeats the same component text twice on the screen.
<b-button block
#click="create = 'SeeCompany'"
class="m-sides"
variant="outline-primary">
Ver
</b-button>
here is the image:
EDIT: Here is the code in the mounted
export default class SeeCompany extends Vue {
#Prop({ default: 0 }) private companyId !: number;
constructor() {
super();
}
private mounted() {
console.log(this.companyId); --> This is consoling two ceros (0) and the passed value for instance = 1;
}
}
There are two main uses for is.
Working around limitations in in-DOM templates.
Dynamic components.
For more information see https://v2.vuejs.org/v2/api/#is.
We can ignore the former case as it isn't relevant here.
Typically the second case looks a bit like this:
<component :is="childName" />
Here childName is a property of the component and determines the name of the child component to use. In your example you called it create.
The actual tag name used in the template doesn't really matter. It is common to use the dummy tag <component> for this purpose to avoid misleading future maintainers who may not immediately notice the :is. Whenever you see <component> you know you're in a dynamic component scenario.
When we talk about dynamic components it is important to appreciate exactly what we mean by 'dynamic' in this context. We are specifically talking about which component to use. We are not talking about determining whether or not to create the component in the first place.
In the code in the question the value of create is initially set to an empty string, ''. This is then passed to :is. If you inspect the DOM you'll find that this creates a comment node. While this does make some sense I am unclear if this is officially supported. I've not seen this behaviour documented anywhere and I suspect you may be getting lucky by falling down an internal code path that's intended for other things. It is not something I would be confident relying on in future versions of Vue.
The specific code of interest is:
<SeeCompany
v-bind:is="create"
v-bind:companyId="1"
/>
<SeeOther
v-bind:is="create"
v-bind:companyId="1"
/>
So if you inspect the DOM when create is '' you should find two comment nodes.
When create gets set to SeeCompany this is equivalent to:
<SeeCompany
is="SeeCompany"
v-bind:companyId="1"
/>
<SeeOther
is="SeeCompany"
v-bind:companyId="1"
/>
In turn this is equivalent to:
<SeeCompany
v-bind:companyId="1"
/>
<SeeCompany
v-bind:companyId="1"
/>
The result is the creation of two SeeCompany components. The original SeeOther tag is irrelevant here. This is why, as noted earlier, the convention exists to use a <component> tag to avoid being misleading.
Of course this isn't what you actually wanted the code to do. I'm unclear what the target behaviour is so I'm going to cover a few variations.
If you just want to show the components conditionally you'd use v-if instead:
<SeeCompany
v-if="create"
v-bind:companyId="1"
/>
<SeeOther
v-if="create"
v-bind:companyId="1"
/>
Usually you'd want create to be a proper boolean, false or true. So set the initial value to false with #click="create = true".
Of course this would show both SeeCompany and SeeOther at the same time. That may not be what you want either. Perhaps you only want to show one at once. For that you might do something like this:
<SeeCompany
v-if="create === 'SeeCompany'"
v-bind:companyId="1"
/>
<SeeOther
v-if="create === 'SeeOther'"
v-bind:companyId="1"
/>
Here the initial value of create should be a falsey value of some kind, possibly '', with #click="create = 'SeeCompany'" and #click="create = 'SeeOther'" on appropriate buttons.
If the props for the components are all the same, and especially if there are more than two components involved, you could try to simplify this using is:
<component
:is="create"
v-if="create"
v-bind:companyId="1"
/>
This is shorter but arguably not as clear.

How to set default value for bindable properties in Aurelia HTML only element

Supposed I have the following html only element:
<template bindable='value: Math.random()'>
${value}
</template>
<!-- In another component {Main} -->
<template>
<require from='./that-element'></require>
<that-element></that-element>
</template>
The result is an empty string in main component. The question is how can I define a default value for html only element?
EDIT: Default value for element will not work if an 2 bindings need to have the same unique default value. I can make it unique myself but then I have to do it manually. Was looking for something like Math.random()
The contents of bindable attribute on <template> tag is handled as comma-separated list of strings. Therefore, you won't be able to set default values there.
I can think of two approaches:
Set default values in parent viewmodel class and use explicit binding. Considering above scenario, this will work for you.
<that-element value.bind="someVarFromParent"></that-element>
In some really simple cases, when default value should be a static string, you can use JavaScript expressions within an interpolation.
${value || 'default value'}
Gist demo: https://gist.run/?id=f0698d5b0066c3674c29c40d850217dc
Just putting my 2 cents in here...I think Marton nailed it on the head with the html only solution. I generally use the value.bind method because I usually always have a vm along with my view. So, to simplify JayDi's response (which is a valid response), I've created a GistRun for you.
Basically, you'll create a custom-elem html file and a custom-elem js file. In the html file, you would have something like this:
<template>
<h1>${value}</h1>
</template>
and in the js file, you'd have something like this:
import {bindable} from "aurelia-framework";
export class CustomElem {
#bindable value = "default value";
}
Finally, you'd just require it in your main parent file like so:
<template>
<require from="./custom-elem"></require>
<custom-elem></custom-elem>
<custom-elem value.bind="'myVal'"></custom-elem>
</template>
I just added a default value for the custom element. You can leave it undefined if you'd like. Also, you can add as many properties as you would like. This is the simplest way to tackle this in my opinion, unless your situation lets you get by with the html only method.
Also, in this case, I'm using a string of "myVal". If you wanted to use a variable that you have in your parent vm, you would just take the single quotes off and use that variable name.
Here's the running Gist so that you can fool around with it:
https://gist.run/?id=2eb14ecd2b0b859fd5d1a33b5ee229bd
You can use || statement in html-only element for default value:
<template bindable="param1, param2, param3">
<h1>${param1 || 'default 1'}</h1>
<h2>${param2 || 'default 2'}</h2>
<h3>${param3 || 'default 3'}</h3>
</template>