Vue JS autosizing textarea on load - vue.js

Learning Vue so this may be a basic question. I have a component that renders a textarea input:
<textarea
v-if="$attrs.type === 'textarea'"
v-bind="$attrs"
:value="value"
#input="inputChange"
:class="[
error ? 'text-red-500' : 'text-gray-600',
textareaAutoResized ? 'h-32' : '',
inputClass,
]"
></textarea>
My inputChange method works well:
methods: {
inputChange(evt) {
let value = evt.target.value
this.$emit('input', value)
if (this.$attrs.type === 'textarea' && this.textareaAutoResized) {
evt.target.style.height = "auto";
let newHeight = evt.target.scrollHeight;
evt.target.style.height = `${newHeight}px`;
}
},
},
But I cannot get the textarea to auto size on page load based on initial content. How do I access the element on mounted? Thanks!
mounted() {
if (this.$attrs.type === 'textarea' && this.textareaAutoResized) {
this.$el.style.height = "auto";
let newHeight = this.$el.scrollHeight;
this.$el.style.height = `${newHeight}px`;
}
},

You can use $ref on your <textarea>:
<textarea
ref="textarea"
v-if="$attrs.type === 'textarea'"
v-bind="$attrs"
:value="value"
#input="inputChange"
:class="[
error ? 'text-red-500' : 'text-gray-600',
textareaAutoResized ? 'h-32' : '',
inputClass,
]"
></textarea>
and get it on mounted:
mounted() {
if (this.$attrs.type === 'textarea' && this.textareaAutoResized) {
this.$refs.textarea.style.height = "auto";
let newHeight = this.$refs.textarea.scrollHeight;
this.$refs.textarea.style.height = `${newHeight}px`;
}
},

Related

VueJS, using Computed property instead of Template Literal

I was trying to pass a custom css class to tailwindCSS through a template literal. However, being the component rendered on Storybook, this doesn't seem to work due to some conflict.
Instead, I'd like to pass a computed property, but I'm not sure how to write it in the most scalable way, since maybe there will be more css properties in the future.
This doesn't work:
const textSize = computed(() => { return `text-${props.textSize}`}
This works but it's pretty ugly:
const textSize = computed(() => {
return props.textSize === "xs"
? "text-xs"
: props.textSize === "sm"
? "text-sm"
: props.textSize === "base"
? "text-base"
: props.textSize === "lg"
? "text-lg"
: props.textSize === "xl"
? "text-xl"
: props.textSize === "2xl"
? "text-2xl"
: "text-3xl"
})
The whole component:
<template>
<div :class="[`h-full w-full p-2`, !props.isDisabled || `opacity-30`]">
<div
:class="[
'flex relative items-center h-full w-full',
props.checkBoxType === 'reverse-between' ? 'justify-between' : 'justify-start',
props.direction === 'vertical' ? 'flex-col justify-start gap-2' : 'items-center',
]"
>
<input
type="checkbox"
v-model="checked"
:class="[
'absolute z-50 order-1 w-6 h-6 opacity-0',
props.checkBoxType === 'reverse-between' && ' right-0',
props.checkBoxType === 'standard' ? 'order-0' : 'order-1',
props.direction === 'vertical' && 'top-0 right-4',
]"
:disabled="props.isDisabled"
/>
<div
:class="[
'bg-white w-6 h-6 flex flex-shrink-0 justify-center items-center p-1 border-2 border-gray',
props.checkBoxType === 'standard' && props.direction === 'horizontal' && 'order-0 mr-2',
props.checkBoxType === 'reverse-between' && props.direction === 'horizontal' && 'order-1 ml-2',
props.direction === 'vertical' && 'mr-0',
inputShape === 'square' ? 'rounded' : 'rounded-full',
props.isToggleEnabled || checked ? 'border-3' : 'border-1',
]"
>
<div
:class="[
'bg-gray w-3 h-3',
props.inputShape === 'circle' ? 'rounded-full' : 'rounded-sm',
props.isToggleEnabled || checked ? 'visible' : 'hidden',
]"
></div>
</div>
<div
:class="[
'select-none font-base flex justify-center items-center',
props.checkBoxType === 'standard' ? 'order-1' : 'order-0',
props.direction === 'vertical' && 'text-center',
]"
>
<span :class="[checked || isToggleEnabled ? 'font-bold' : 'font-normal', 'text-gray', textSize]">{{
props.label
}}</span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { defineProps, ref, computed } from "vue"
const checked = ref(false)
const textSize = computed(() => {
return props.textSize === "xs"
? "text-xs"
: props.textSize === "sm"
? "text-sm"
: props.textSize === "base"
? "text-base"
: props.textSize === "lg"
? "text-lg"
: props.textSize === "xl"
? "text-xl"
: props.textSize === "2xl"
? "text-2xl"
: "text-3xl"
})
const props = defineProps({
/**
* Sets the label for the input element
*/
label: {
type: String,
default: "example",
},
/**
* Changes the layout between text and checkbox button
*/
checkBoxType: {
default: "standard",
},
/**
* Sets the shape of the checkbox as squared or circled
*/
inputShape: {
default: "square",
},
/**
* Sets the direction of the component as horizontal or vertical
*/
direction: {
default: "horizontal",
},
/**
* Sets the text size
*/
textSize: {
// type: String as PropType<FontSize>,
type: String,
default: "base",
},
/**
* Toggles or untoggles the checkbox
*/
isToggleEnabled: {
type: Boolean,
},
/**
* If false, the whole component is greyed out
*/
isDisabled: {
type: Boolean,
default: false,
},
})
</script>
As #kissu mentioned in his comment Interpolation of class names is indeed not feasible in Tailwind , so you need to create a clean object with prop value as field and the class name as value :
const textSizes = {
'xs':'text-xs',
'sm':'text-sm',
'base':'text-base',
'lg':'text-lg',
'xl':'text-xl',
'2xl':'text-2xl',
'3xl':'text-3xl',
}
const textSize = computed(() => textSizes[props.textSize])

VueJS toggle password visibilty without mutating the "type" property

I have a basic input component I am working with that has type as a property and up until now has been working very well. However, trying to use it for passwords and implementing obfuscation has been sort of tricky.
How can I toggle hide/show of the password without mutating the prop? I figured making it type = 'password' to type = 'text was the best way, but clearly not.
I've made a Codesandbox to replicate that part of the component, but any advice or direction would be greatly appreciated!
PasswordInput.vue:
<template>
<div>
<input :type="type" />
<button #click="obfuscateToggle" class="ml-auto pl-sm _eye">
<div>
<img :src="`/${eyeSvg}.svg`" alt="" />
</div>
</button>
</div>
</template>
<script>
export default {
name: "HelloWorld",
data() {
return {
passwordVisible: false,
eyeSvg: "eye-closed",
};
},
props: {
type: { type: String, default: "text" },
},
methods: {
obfuscateToggle() {
if (this.eyeSvg === "eye-closed") {
this.eyeSvg = "eye";
} else this.eyeSvg = "eye-closed";
// this.eyeSvg = "eye-closed" ? "" : (this.eyeSvg = "eye");
if ((this.type = "password")) {
this.type = "text";
} else this.type = "password";
},
},
};
</script>
App.vue
<template>
<div id="app">
<PasswordInput type="password" />
</div>
</template>
The only way to do it is by mutating the type attribute. As that is how the browser decides the render it as either just a textbox or as a password. Therefore you are doing this the right way.
The one issue that you will encounter is that you will have errors thrown in your console because you are attempting to mutate a prop.
This is quick and easy to fix. First, you will create a new data property, and assign it to the default of type
data(){
return{
fieldType:'text'
}
}
Then you will use the on mounted lifecycle hook, and update your data property to match your prop's value`
mounted(){
this.fieldType = this.type;
}
If you know the type prop will change from the parent component you can also use a watcher for changes and assign type
watch:{
type(val){
this.fieldType = val;
}
}
You will then update your obfuscateToggle method to use the fieldtype variable:
obfuscateToggle() {
if (this.eyeSvg === "eye-closed") {
this.eyeSvg = "eye";
} else this.eyeSvg = "eye-closed";
//You can simplify this by using this.fieldType = this.fieldType == "text" ? "password" : "text"
if (this.fieldType == "password") {
this.fieldType = "text";
} else this.fieldType = "password";
}
Finally, in your template, you will want to change type to fieldType
<template>
<div>
<input :type="fieldType" />
<button #click="obfuscateToggle" class="ml-auto pl-sm _eye">
<div>
<img :src="`/${eyeSvg}.svg`" alt="" />
</div>
</button>
</div>
</template>
Putting it all together
<script>
export default {
name: "HelloWorld",
data() {
return {
passwordVisible: false,
eyeSvg: "eye-closed",
fieldType: "text"
};
},
props: {
type: { type: String, default: "text" },
},
methods: {
obfuscateToggle() {
if (this.eyeSvg === "eye-closed") {
this.eyeSvg = "eye";
} else this.eyeSvg = "eye-closed";
//You can simplify this by using this.fieldType = this.fieldType == "text" ? "password" : "text"
if (this.fieldType == "password") {
this.fieldType = "text";
} else this.fieldType = "password";
},
},
watch:{
type(val){
this.fieldType = val;
}
},
mounted(){
this.fieldType = this.type;
},
};
</script>
Here is an example on CodeSandBox
Also, you had a small typo in your obfuscateToggle method.
if(this.type = 'password')
this was assigning type instead of comparing it against a literal :)

Code works but still getting warning in Vue

i am getting warning "Avoid mutating a prop directly", i know this can be solved through data property or computed usage like mentioned in Vue Official Documentation. But i do not know how to change my code to any of those methods. Please help me with proper code to get rid of this Warning.
My Code looks like this:
<template>
<div class="track-rating">
<span :key="note" v-for="note in maxNotes" :class="{ 'active': note <= rating || note <= hoveredNote }" #mouseover="hoveredNote = note" #mouseleave="hoveredNote = false" #click="rate(note)" class="material-icons mr-1">
audiotrack
</span>
</div>
</template>
<script>
export default {
name: "Rating",
props: {
rating: {
type: Number,
required: true
},
maxNotes: {
type: Number,
default: 3
},
hasCounter: {
type: Boolean,
default: true
},
itemId: {
type: String
}
},
data() {
return {
hoveredNote: false
};
},
methods: {
rate(note) {
if (typeof note === 'number' && note <= this.maxNotes && note >= 0)
this.rating = this.rating === note ? note - 1 : note
this.$emit('onRate', this.itemId, this.rating);
}
}
};
You are modifing rating prop on the rate method, so to avoid the warning you should avoid to modify this prop, one easy solution although not very elegant is to make a local copy of your prop:
<template>
<div class="track-rating">
<span :key="note" v-for="note in maxNotes" :class="{ 'active': note <= copiedrating || note <= hoveredNote }" #mouseover="hoveredNote = note" #mouseleave="hoveredNote = false" #click="rate(note)" class="material-icons mr-1">
audiotrack
</span>
</div>
</template>
<script>
export default {
name: "Rating",
props: {
rating: {
type: Number,
required: true
},
maxNotes: {
type: Number,
default: 3
},
hasCounter: {
type: Boolean,
default: true
},
itemId: {
type: String
}
},
mounted () {
this.copiedrating = this.rating;
},
data() {
return {
hoveredNote: false,
copiedrating: null,
};
},
methods: {
rate(note) {
if (typeof note === 'number' && note <= this.maxNotes && note >= 0)
this.copiedrating = this.copiedrating === note ? note - 1 : note
this.$emit('onRate', this.itemId, this.copiedrating);
}
}
Don't modify this.rating directly, just send the event and make the change in parent component....
methods: {
rate(note) {
if (typeof note === 'number' && note <= this.maxNotes && note >= 0) {
const newRating = this.rating === note ? note - 1 : note
this.$emit('onRate', this.itemId, newRating);
}
}
}
...then in parent handle the event (I don't know how parent component or data looks like so this is more pseudo-code...)
<Rating :itemId="track.itemId" :rating="track.rating" #onRate="changeRating" />
methods:
changeRating: function(id, rating) {
let trackIndex = this.tracks.indexOf(track => track.itemId === id)
this.tracks[trackIndex].rating = rating
}

Browser was freeze when i update array items in v-for block

I use v-autocomplete component and updating items list after http request. But browser was freeze when i set items. What's wrong?
{
mixins: [search_field_block],
data: function () {
return {
item_component: {
props: ['item'],
template: '<div v-html="item.full_name"></div>'
}
};
},
methods: {
search: function (text) {
this.search_text = text.trim();
if (this.search_text) {
this.doGET('/api-method/search_place/', {'query': this.search_text}, this.update_items);
}
},
update_items: function (data) {
this.items = data;
}
}
}
I use a mixin for other components. It contained universal template with v-autocomplete:
<field-block :label="label" :error="field_error" :description="item_description">
<v-autocomplete slot="input"
class="page-form__field required"
:class="{ focused: focused, 'not-empty': not_empty, error: field_error != null, 'list-open': is_list_open }"
v-model="value"
ref="autocomplete"
:required="required"
:inputAttrs="{ref: 'input', autocomplete: 'off'}"
:items="items"
:component-item="item_component"
:get-label="getLabel"
:min-len="2"
#update-items="search"
#item-selected="onSelect"
#input="onInput"
#blur="onBlur"
#focus="onFocus">
</v-autocomplete>
</field-block>
I was find v-autocomplete on the github. It contain a v-for block for rendering search results
I found the problem. I have "computed" property and set value to parent app:
computed: {
is_list_open: function () {
this.$parent.list_opened = this.focused && this.items.length > 0 || (this.$refs.autocomplete ? this.$refs.autocomplete.show : false);
return this.$parent.list_opened;
}
},
This is incorrect behavior.

Vue.js - data in component instead of a generated modal

I have a list of terms that have a description. I need these description to be displayed right after the term name instead of in a modal.
this is the vue component:
Vue.component('taxonomy-list', {
template : ''+
'<span><template v-for="(room, index) in event.terms[tax]">' +
'<template v-if="room.url"><a :href="room.url">{{room.name}}</a></template>' +
//HERE
'<template v-else-if="room.desc">{{room.name}}</template>' +
//END
'<template v-else>{{room.name}}</template>' +
'<template v-if="index !== (event.terms[tax].length - 1)">, </template>' +
'</template></span>' ,
props : [ 'tax', 'options', 'event', ],
methods : {
openModal: function( item, options, $event ){
this.$emit( 'open-modal', item, options, $event );
},
}
});
on the second case on click it generate a modal with the description that i need.
This is the generated modal:
Vue.component( 'modal-taxonomy', {
template: '#wcs_templates_modal--taxonomy',
props: [ 'data', 'options', 'content', 'classes' ],
mixins: [wcs_modal_mixins]
});
<script type="text/x-template" id="wcs_templates_modal--taxonomy">
<div class="wcs-modal" :class="classes" v-on:click="closeModal">
<div class="wcs-modal__box">
<div class="wcs-modal__inner">
<div class="wcs-modal__content wcs-modal__content--full">
<h2 v-html="data.name"></h2>
<div v-html="data.content"></div>
</div>
</div>
</div>
</div>
</script>
I just need to print the "data.content" right after the name of the {{room.name}}, but i cant manage to do so...
Thanks guys.
EDIT: thats the modalOpen function:
openModal: function( data, options ){
var $self = this;
this.data = data;
this.options = options;
if( ! this.visible ){
this.visible = true;
}
this.loading = true;
if( typeof data.start !== 'undefined' ){
if( typeof this.events[data.id] === 'undefined' ){
this.getClass( data.id );
} else {
this.data.content = this.events[data.id].content;
this.data.image = this.events[data.id].image;
this.loading = ! this.loading;
}
} else {
if( typeof this.taxonomies[data.id] === 'undefined' ){
this.getTaxonomy( data.id );
} else {
this.data.content = this.taxonomies[data.id];
this.loading = ! this.loading;
}
}
}
The modal component uses room.content to display the description, but that property is initialised only if you call openModel, see this.data.content = this.events[data.id].content; and this.data.content = this.taxonomies[data.id];.
IMO you have two options, either duplicate the openModel logic into the HTML or create a computed property that returns the description (I'd choose the latter).
EDIT
Actually, a method should be better because you extract the description from room. You'll need something like:
Vue.component('taxonomy-list', {
template : ''+
'<span><template v-for="(room, index) in event.terms[tax]">' +
'<template v-if="room.url"><a :href="room.url">{{room.name}}</a></template>' +
//HERE
'<template v-else-if="room.desc">{{room.name}}</template>' +
//END
'<template v-else>{{room.name}}</template>' +
// DESCRIPTION
'{{ description(room) }}' +
// END
'<template v-if="index !== (event.terms[tax].length - 1)">, </template>' +
'</template></span>' ,
props : [ 'tax', 'options', 'event', ],
methods : {
openModal: function( item, options, $event ){
this.$emit( 'open-modal', item, options, $event );
},
description: function( room ) {
// insert the logic used by openModal to set content here and return that string
// this.events[data.id].content or this.taxonomies[data.id]
return 'description...';
},
},
},
});