When to split your code into components in Nuxt? - vue.js

I'm coding a project in Vue, Nuxt and vuetify as UI library.
In my index page I have to show a lot of static contents and images with slides and carousels. I have coded it and everything works fine but my index.vue file in pages directory looks messy and I feel there is too many lines of code in there.
For instance I have the following code for showing a carousel in index.vue:
<v-carousel
height="90vh"
vertical
vertical-delimiters="right"
interval="9000"
cycle
:show-arrows="false"
hide-delimiter-background
>
<v-carousel-item
v-for="(item,i) in carouselItems" :key="i"
:src="item.src"
reverse-transition="scroll-y-transition"
transition="scroll-y-transition"
>
<div
class="d-flex transition-fade-in white--text"
style="height: 100%;background: linear-gradient(rgba(0,0,0,.1), rgba(0,0,0,.9));"
>
<v-row
class="fill-height pb-9 mx-5 flex-column"
align="start"
justify="end"
>
<div class="text-h3 white--text d-block mb-2">
{{ item.title }}
</div>
<div class="text-h6 white--text mx-1 text-justify"> {{ item.description }} </div>
</v-row>
</div>
</v-carousel-item>
</v-carousel>
Now my question is should I separate this code into a component and pass the options and items as props to that component or should I leave it as it is and it's OK to have a page component with many lines of code?

It all depends on you, the way you work and how you/your team feel like about splitting into components. There is no standard as of to say something like "a component needs to be at max 300 lines" or something alike.
It also comes down to logic with your components, make them do 1 thing well rather than too much at the same time. But don't over-engineer it by having 50 lines of components that could totally be a single component neither because it will also introduce some complexity that is not needed.
Just code and split when you feel like it's doing too much or it's to big. I usually have components from 5 lines of code up to 300~400 as a maximum depending of their complexity/implementation.

Related

How to properly move helper components into children components in Vuetify?

I have a component with a v-card in it which looks like this:
<!-- CurrentFile.vue -->
<v-card color="primary">
<v-card-title class="secondary--text text-wrap">Flashing title</v-card-title>
<v-card-subtitle>
<span class="secondary--text text-h6">A fancy subtitle</span>
</v-card-subtitle>
<v-card-text class="mt-n3">
...
</v-card-text>
</v-card>
While refactoring, I wanted to create a new component that would encapsulate both v-card-title and v-card-subtitle helper components, since they use the same text color.
<!-- CurrentFile.vue -->
<v-card color="primary">
<my-new-header textColor="secondary--text" title="Flashing title" subtitle="A fancy subtitle"/>
<v-card-text class="mt-n3">
...
</v-card-text>
</v-card>
I went on to create this child component
<!-- MyNewHeader.vue -->
<template>
<span>
<v-card-title class="text-wrap" :class="textColor">{{ title }}</v-card-title>
<v-card-subtitle>
<span class="text-h6" :class="textColor">{{ subtitle }}</span>
</v-card-subtitle>
</span>
</template>
Issue
With the inclusion of a <span> tag, the use of v-card-title and v-card-subtitle components is almost irrelevant. It seems to me that both containers lose their properties (margins and paddings).
Of course, it's logical, since it adds an undesired level of hierarchy. Once the child component is injected, the structure ends up looking like this:
<v-card>
<span>
<v-card-title/>
<v-card-subtitle>
<span/>
</v-card-subtitle>
</span>
<v-card-text/>
</v-card>
I would very much remove that <span> tag to avoid all these problems, but there must only be one element at the template root.
Is there another way of doing what I want to do that I'm missing? Or maybe is it overkill to want that many sub-components?
There are 3 possible solutions for your issue:
Using fragments - https://github.com/privatenumber/vue-frag
Using render function instead of a component template - https://v2.vuejs.org/v2/guide/render-function.html
Using CSS display: contents to effectively replace the wrapper with its content - https://developer.mozilla.org/en-US/docs/Web/CSS/display-box

How to properly create a popup component in Vue 3

As part of becoming a better Vue programmer, I am trying to implement a popup similar to Popper with a clean and Vueish architecture. Here is a simple schematic that I came up with:
So basically there is a target component, which is the reference for the popup's position. The popup can be positioned above, below, right and left of the target, therefore I will need to have access to the target element in my popup. Also, the target can be an arbitrary component. It can be a simple button or span, but also something much more complex.
Then there is the popup itself, which will be put into a modal at the end of the body, It contains the actual content. The content again can be an arbitrary component.
I have a working implementation of the popup, but the basic structure seems to be far from perfect. I am using two slots, one for the target element and one for the content.
Here is what I have come up with so far for the template:
<template>
<div ref="targetContainer">
<slot name="target"></slot>
</div>
<teleport to="body">
<div v-show="show" class="modal" ref="modal">
<div ref="popover" class="popover" :style="{top: popoverTop + 'px', left: popoverLeft + 'px'}">
<slot name="content"></slot>
</div>
</div>
</teleport>
</template>
There are several issues with this that I am not really happy with.
Using the popup is not very simple
When using this popup in another component, two <template> tags are rquired. This is ungly and not very intuitive. A very simple use case looks like this:
<modal :show="showPopup" #close="showPopup=false">
<template v-slot:target>
<button #click="showPopup=true"></button>
</template>
<template v-slot:content>
<div>Hello World!</div>
</template>
</modal>
The target is wrapped in another <div>
This is done to get access to the target element, that I need for the layout. In mounted() I am referencing the target element like this:
let targetElement = this.$refs.targetContainer.children[0];
Is this really the best way to do this? I would like to get rid of the wrapping <div> element, which just asks for unintended side effects.
The best solution would be to get rid of one slot and somehow reference the target element in another way because I only need its layout information, it does not have to be rendered inside the popover component.
Can someone point me in the right direction?
Here is my solution, which was inspired by a comment on my question and which I think is worth sharing.
Instead of putting the target element into a slot, I am now passing its ref as a prop, which makes things much cleaner.
The popover component's template now looks like this.
<template>
<teleport to="body">
<div v-show="show" class="modal" ref="modal">
<div ref="popover" class="popover" :style="{top: popoverTop + 'px', left: popoverLeft + 'px'}">
<slot ref="content"></slot>
</div>
</div>
</teleport>
</template>
I has a targetRefprop, so the component can be simply used like this:
<div ref="myTargetElement" #click="isPopupVisible=true">
</div>
<modal :show="isPopupVisible" #close="isPopupVisible=false" targetRef="myTargetElement">
<!-- popup content goes here -->
</modal>
And after mounting I can access the target element like this:
let targetElement = this.$parent.$refs[this.targetRef];
I like this solution a lot. However, ideas, advice or words of caution are still highly welcome.

Passing slots through from Parent to Child Components

I have built a user-defined component (async-select) on top of another component (vue mutliselect) like this:
https://jsfiddle.net/2x7n4rL6/4/
Since the original vue-multiselect component offers a couple of slots, I don't want to loose the chance to use them. So my goal is to make these slots available from inside my custom component. In other words, I want to something like this:
https://jsfiddle.net/2x7n4rL6/3/
But that code oes not work.
However, if I add the slot to the child component itself, it works just fine (which you can see from the fact that options become red-colored).
https://jsfiddle.net/2x7n4rL6/1/
After surfing the web, I have come across this article, but it does not seem to work
Is there any way in VueJS to accomplish this ?
Slots can be confusing!
First, you need a template element to define the slot content:
<async-select :value="value" :options="options">
<template v-slot:option-tmpl="{ props }">
<div class="ui grid">
<div style="color: red">{{ props.option.name }}</div>
</div>
</template>
</async-select>
Then, in the parent component, you need a slot element. That slot element itself can be inside of another template element, so its contents can be put in a slot of its own parent.
<multiselect
label="name"
ref="multiselect"
v-model="localValue"
placeholder="My component"
:options="options"
:multiple="false"
:taggable="false">
<template slot="option" slot-scope="props">
<slot name="option-tmpl" :props="props"></slot>
</template>
</multiselect>
Working Fiddle: https://jsfiddle.net/thebluenile/ph0s1jda/

degree symbol not being rendered by vue component

I have a vue component which displays a gauge. I need to include units on the display and have this as one of the props of the component. However, because there are a number of gauges with different formatting it is all stored in a vuex store that reads its settings from an API. This all works nicely apart from when I want to bring special symbols (such as degree signs) across. The vuex object is storing the formatting object as:
{"min":0,"max":50,"dp":1,"units":"°C"}
and I use it in my component as follows:
<svg-gauge v-else
v-bind:g-value="device.value"
v-bind:g-min="device.format.min"
v-bind:g-max="device.format.max"
v-bind:g-decplace="device.format.dp"
v-bind:g-units="device.format.units"
>
The problem is that this simply displays °C rather than a degree symbol. If I hard code the last line as
g-units="°C"
It all works as expected. I suspect it is that I am having to use v-bind to pass the property and this is messing things up. Is there a way to ensure that v-bind is treating my characters as I would like?
EDIT: Here is the svg-gauge component template where the units are actually rendered.
<template>
<b-row>
<b-card no-body class="bg-dark text-light border-0" align="center">
<b-card-body class="m-0 pt-0 pb-0">
<h5><slot name="title">Title</slot></h5>
<div class="row mini-gauge pt-3" align="center">
<vue-svg-gauge
class="mini-gauge"
:start-angle="-90"
:end-angle="90"
:value="gValue"
:separator-step="0"
:min="gMin"
:max="gMax"
base-color="#595959"
:gauge-color="[{ offset: 0, color: '#347AB0'}, { offset: 100, color: '#D10404'}]"
:scale-interval="5"
>
<div style="line-height: 11rem">{{gValue.toFixed(gDecplace)}} {{gUnits}}</div>
</vue-svg-gauge>
</div>
<div class="row mini-gauge">
<div class="col" align="left">{{gMin}}</div>
<div class="col" align="right">{{gMax}}</div>
</div>
</b-card-body>
</b-card>
</b-row>
</template>
Change this line to have a span with a v-html. Then in the v-html pass the gUnits prop
<div style="line-height: 11rem">
{{gValue.toFixed(gDecplace)}}
<span v-html="gUnits"></span>
</div>
You can find the reason by looking here.
Hope this helps!

Aurelia component slotting in markup for components model

I am building an autocomplete component. The plan is that I can slot in some markup for what I know the component is going to bind to.
The idea is this could be any object rather than a simple display value and identifier.
I have this working using templates but I am wondering if there is a better approach.
So far it looks like this (options is hard coded for now within the components model):
// Usage:
<autocomplete>
<template replace-part="item">
//this is the content for each option within the component
<b>${option.lastName}<b/>, ${option.firstName}
</template>
</autocomplete>
//autocomplete
<template>
<input type="text" placeholder="Type 3 characters ...">
<ul>
<li repeat.for="option of options">
<template replaceable part="item"></template>
</li>
</ul>
</template>
I don't really like the templating boilerplate, slots are much nicer, is there any way to make slots work like this?
<autocomplete>
<li repeat.for="option of options">
${option.lastName}<b/>, ${option.firstName}
<li/>
</autocomplete>
//autocomplete
<template>
<input type="text" placeholder="Type 3 characters ...">
<ul>
<slot></slot>
</ul>
</template>
Slot in Aurelia is the emulation based on standard spec, which mean it doesn't work with repeat situation. repaceable was introduced to handle this scenario and I don't think we have any other options. Sometimes it feels weird but with a little documentation, probably you and your team will be fine. What you can do is for each replacement, what properties it can look for to get the item.