Vue - change input width according to content - javascript

So I can do this (Pug & CoffeeScript):
input(placeholder="0", v-model.number="order[index]" v-on:change="adjustInput")
...
adjustInput: ->
event.target.style.width = event.target.value.length + 'ch'
... but it only works if I change the input in the browser, by hand. The input does not change its width if the v-model changes.
How can I make it so that the input width changes even if the change is due to Vue reactivity?

Check this one, but you must check the font-size on input and on fake_div
var app = new Vue({
el: '#app',
data() {
return {
order: 1, // your value
fakeDivWidth: 10, // width from start, so input width = 10px
};
},
watch: {
order: { // if value from input changing
handler(val) {
this.inputResize();
},
},
},
methods: {
inputResize() {
setTimeout(() => {
this.fakeDivWidth = this.$el.querySelector('.fake_div').clientWidth;
}, 0);
},
},
})
.fake_div {
position: absolute;
left: -100500vw;
top: -100500vh;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input placeholder="0" v-model.number="order" v-bind:style="{width: fakeDivWidth + 'px'}" >
<div class="fake_div">{{ order }}</div> // fake_div needed to see this div width
// этот блок фейковый и нужен только для того, что бы наш input становился такого же размера как этот div
</div>

Try this
input(placeholder="0", v-model.number="order[index]" v-on:change="adjustInput" :style="{width: reactiveWidth}")
// Your secret Vue code
data() {
return function() {
reactiveWidth: 100px; // (some default value)
}
},
methods: {
adjustInput() {
this.reactiveWidth = event.target.value.length + 'ch'
}
},
computed: {
reactiveWidth() {
return this.number + 'ch';
}
}
Since I don't know all parts of the code, you might need to tweak this a bit. With just binding this.number to a order[index] you are not affecting the width of the input in any way. The computed property listens to changes in number

The easiest workaround to me is to replace the input field with a contenteditable span which will wrap around the text:
<script setup>
import {reactive} from 'vue'
const state = reactive({
input: 'My width will adapt to the text'
})
</script>
<template>
<span class="input" #input="e => state.input = e.target.innerText" contenteditable>{{state.input}}</span>
</template>
This works like v-model=state.input

Related

Use a variable defined in a method inside the template

it's the first time I use Vue (v2 not v3) and I'm stucked trying to use a variable (defined inside a methods) inside the template.
My semplified code:
<template>
<div class="container" #mouseover="isHovered = true" #mouseleave="isHovered = false">
<div class="c-container">
<div ref="topCContainerRef" class="top-c-container">
<div
:class="['top-c', ...]"
:style="{ height: `${isHovered ? 0 : this.scaledHeight}` }" // <-- HERE I need `scaledHeight`
>
</div>
</div>
</div>
</div>
</template>
<script>
import { scaleLinear } from 'd3-scale'
export default {
name: 'MyComponent',
components: { },
props: {
...,
datum: {
type: Number,
required: true,
},
...
},
data: function () {
return {
isHovered: false,
scaledHeight: {},
}
},
mounted() {
this.matchHeight()
},
methods: {
matchHeight() {
const topCContainerHeight = this.$refs.topCContainerRef.clientHeight
const heightScale = scaleLinear([0, 100], [20, topCContainerHeight])
const scaledHeight = heightScale(this.datum)
this.scaledHeight = scaledHeight // I want to use this value inside the template
},
},
}
</script>
How can I get the value of scaledHeight inside the template section?
If I didn't use this, I get no error but the height value is always 0, like scaledHeight is ignored..
I read the documentation but it doesn't help me
I encountered and solved this problem today.
You can change your styles like below.
<div
:class="['top-c', ...]"
:style="{ height: isHovered ? 0 : scaledHeight }"
>
It works fine for me, and hope it will help you~~
Fixed using computed
computed: {
computedHeight: function () {
return this.isHovered ? 0 : this.matchHeight()
},
},
methods: {
matchHeight() {
const topCContainerHeight = this.$refs.topCContainerRef.clientHeight
const heightScale = scaleLinear([0, 100], [20, topCContainerHeight])
return heightScale(this.datum)
},
},

vue warn - Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders

Here, I have a variable called total_price which I sent from laravel. I wanna do many things to it. When I use methods, when script runs them, I get the mutating error. Here is the script:
export default {
props: {
.//some other props here are cut for better reading
.
.
total_price:{
type:Number
},
.
.
.
},
data(){
return {
newValue:7,
total_price:1,
}
},
I use them in methods like this:
methods:{
getNotificationClass (notification) {
return `alert alert-${notification.type}`
},
mpminus: function () {
if ((this.newValue) > this.min) {
this.newValue = this.newValue - 1
this.$emit('input', this.newValue)
}
if(this.newValue < this.max_occupancy){
this.total_price = this.extra_price / ( this.newValue - this.base_capacity )
this.person_number =this.newValue - this.base_capacity
this.$emit('input', this.totalprice)
this.$emit('input', this.person_number)
}
},
mpplus: function () {
if (this.max === undefined || (this.newValue < this.max)) {
this.newValue = this.newValue + 1
this.$emit('input', this.newValue)
}
if(this.newValue > this.base_capacity){
this.total_price = this.extra_price * ( this.newValue - this.base_capacity )
this.person_number =this.newValue - this.base_capacity
this.$emit('input', this.totalprice)
this.$emit('input', this.person_number)
}
},
},
...using this template:
<div class="minusplusnumber">
<div class="mpbtn minus" v-on:click="mpminus()">
-
</div>
<div id="field_container">
<input type="number" v-model="newValue" disabled />
</div>
<div class="mpbtn plus" v-on:click="mpplus()">
+
</div>
</div>
When I click minus or plus, I get this warning:
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "total_price"
found in
---> <Reserve> at resources/js/components/Reserve.vue
<Root>
Here is an example of how to use props along with mutation - this is a good way of summarizing what you are trying to accomplish..
Just change the number in :default-value=X to simulate passing down a prop..
Full Link:
https://codepen.io/oze4/pen/PLMEab
HTML:
<!-- Main Vue instance (aka parent) -->
<div id="app">
<!-- ----------------------------------------- -->
<!-- CHANGE THE NUMBER 10 TO WHATEVER YOU WANT -->
<!-- ----------------------------------------- -->
<my-counter :default-value=10></my-counter>
</div>
<!-- Child component as x-template component -->
<script type="text/x-template" id="counter">
<div>
<div style="border: 1px solid black; width: 250px; margin: 40px 40px 40px 40px">
<v-btn #click="increase" color="blue">Increase</v-btn>
<v-btn #click="decrease" color="red">Decrease</v-btn>
</div>
<div>
<div>
<h3 style="margin-left: 40px;">Current Count: {{ currentValue }}</h3>
</div>
</div>
</div>
</script>
JS/Vue
/**
* Child component as x-template
*/
const appCounter = {
template: '#counter',
props: {
defaultValue: {
type: Number,
default: 0
}
},
data() {
return {
currentValue: '',
}
},
mounted() {
this.currentValue = this.defaultValue;
},
methods: {
increase(){
this.currentValue++;
},
decrease(){
this.currentValue--;
}
}
}
/**
* Main Vue Instance
*/
new Vue({
el: "#app",
components: {
myCounter: appCounter
}
});

Component inside Component - VueJS

I am having a hard time to understand this, so I have a component which is already complied which is a grid, now when I click on a button a modal pops-up and display another grid inside the modal at this point my code looks like this for the modal pop-up
<template>
<transition v-if="this.modalVisible" v-bind:title.sync="this.modalVisible" name="modal">
<div class="modal-mask">
<div class="modal-wrapper">
<div class="modal-container">
<div class="modal-header">
{{ modalHeaderName }}
</div>
<div class="modal-body">
//this is another component
<grid-data :grid-values="dummy" :tool-bar="false"></grid-data>
</div>
<div class="modal-footer">
</div>
</div>
</div>
</div>
</transition>
</template>
<script>
import DataTable from './core/gridTable.vue';
export default {
components:{
JqxButton,
'grid-data' : DataTable,
},
props : {
modalHeaderName : String,
modalVisible : Boolean
},
data: function () {
return {
buttonWidth: 120,
buttonHeight: '100%',
value: this.buttonName,
dummy : [
{ name: 'ProductName', type: 'string' },
{ name: 'QuantityPerUnit', type: 'int' },
{ name: 'UnitPrice', type: 'float' },
{ name: 'UnitsInStock', type: 'float' },
{ name: 'Discontinued', type: 'bool' }
],
}
}
}
</script>
Now, the grid is a vue component which was already complied and rendered, now will I import it again it says
[Vue warn]: Failed to mount component: template or render function not defined.
<template>
<div>
<!-- sync here is, getting the value from the updated modal-->
<custom-modal :modal-visible="this.showModal" v-bind:showModal.sync="showModal" :modal-header-name="this.modalHeaderName"></custom-modal>
<JqxGrid :width="width" :source="dataAdapter" :columns="gridValues"
:pageable="true" :autoheight="true" :sortable="true"
:altrows="true" :enabletooltip="true" :editable="true"
:selectionmode="'multiplecellsadvanced'" :showtoolbar="this.toolBar" :rendertoolbar="rendertoolbar">
</JqxGrid>
</div>
</template>
<script>
import JqxGrid from "../jqx-vue/vue_jqxgrid.vue";
import CustomModal from "../customModal";
export default {
components: {
JqxGrid,
'custom-modal' : CustomModal
},
// added the name here
name: 'jqx-grid',
props : {
gridValues : Array,
toolBar : Boolean
},
data: function () {
return {
showModal : false,
modalHeaderName : '',
width: '100%',
dataAdapter: new jqx.dataAdapter({
datatype: 'xml',
datafields : this.gridValues,
url: ''
}),
columns: []
}
},
mounted: function () {
this.createButtons();
},
methods: {
rendertoolbar: function (toolbar) {
let buttonsContainer = document.createElement('div');
buttonsContainer.style.cssText = 'overflow: hidden; position: relative; margin: 5px;';
let addButtonContainer = document.createElement('div');
let deleteButtonContainer = document.createElement('div');
addButtonContainer.id = 'addButton';
deleteButtonContainer.id = 'deleteButton';
addButtonContainer.style.cssText = 'float: left; margin-left: 5px;padding-bottom:25px;';
deleteButtonContainer.style.cssText = 'float: left; margin-left: 5px;padding-bottom:25px;';
buttonsContainer.appendChild(addButtonContainer);
buttonsContainer.appendChild(deleteButtonContainer);
toolbar[0].appendChild(buttonsContainer);
},
createButtons: function () {
let addButtonOptions = {
height: 25, value: ' <i class="fa fa-plus" style="padding-top:3px"></i> Add Items ',
};
let addButton = jqwidgets.createInstance('#addButton', 'jqxButton', addButtonOptions);
let deleteButtonOptions = {
height: 25, value: ' <i class="fa fa-ban" style="padding-top:3px"></i> Remove All ',
};
let deleteButton = jqwidgets.createInstance('#deleteButton', 'jqxButton', deleteButtonOptions);
// add new row.
addButton.addEventHandler('click', (event) => {
this.showModal = true;
this.modalHeaderName = 'Bulk Add Items';
});
// delete selected row.
deleteButton.addEventHandler('click', (event) => {
// alert('delete')
});
},
cellsrenderer: function (row, columnsfield, value, defaulthtml, columnproperties, rowdata) {
if (value < 20) {
return '<span style="margin: 4px; float: ' + columnproperties.cellsalign + '; color: #ff0000;">' + value + '</span>';
}
else {
return '<span style="margin: 4px; float: ' + columnproperties.cellsalign + '; color: #008000;">' + value + '</span>';
}
}
}
}
</script>
How can I overcome this issue?
I have seen question like this which says the component grid is trying to compile again and hence the error but I am not sure of that, so we should be using the complied version of the grid component.
NOTE: Using Vue with Laravel 5.4
Error Image
I didn't see an obvious error when you first posted the code. Right now I see JqxButton inside components of the upper code block, which would be undefined. In your code, you always import some components for which we can't see the code.
Generally, when I'm in a situation like this and everything seems to be looking okay, I remove all sub-components and see if the error goes away. Then, I re-add one component after each other until I hit the error again and try to debug it there.
From your description, I suspect you have some kind of cycle in your dependencies and you might find the documentation about circular references helpful.
Vue needs a lazy import for circular dependencies:
components: {
"my-circular-dependency": () => import("./my-circular-dependency.vue");
}

Input field unable to change model when component and model are dynamically created

I have a data structure with nested objects that I want to bind to sub-components, and I'd like these components to edit the data structure directly so that I can save it all from one place. The structure is something like
job = {
id: 1,
uuid: 'a-unique-value',
content_blocks: [
{
id: 5,
uuid: 'some-unique-value',
block_type: 'text',
body: { en: { content: 'Hello' }, fr: { content: 'Bonjour' } }
},
{
id: 9,
uuid: 'some-other-unique-value',
block_type: 'text',
body: { en: { content: 'How are you?' }, fr: { content: 'Comment ça va?' } }
},
]
}
So, I instantiate my sub-components like this
<div v-for="block in job.content_blocks" :key="block.uuid">
<component :data="block" :is="contentTypeToComponentName(block.block_type)" />
</div>
(contentTypeToComponentName goes from text to TextContentBlock, which is the name of the component)
The TextContentBlock goes like this
export default {
props: {
data: {
type: Object,
required: true
}
},
created: function() {
if (!this.data.body) {
this.data.body = {
it: { content: "" },
en: { content: "" }
}
}
}
}
The created() function takes care of adding missing, block-specific data that are unknown to the component adding new content_blocks, for when I want to dynamically add blocks via a special button, which goes like this
addBlock: function(block_type) {
this.job.content_blocks = [...this.job.content_blocks, {
block_type: block_type,
uuid: magic_uuidv4_generator(),
order: this.job.content_blocks.length === 0 ? 1 : _.last(this.job.content_blocks).order + 1
}]
}
The template for TextContentBlock is
<b-tab v-for="l in ['fr', 'en']" :key="`${data.uuid}-${l}`">
<template slot="title">
{{ l.toUpperCase() }} <span class="missing" v-show="!data.body[l] || data.body[l] == ''">(missing)</span>
</template>
<b-form-textarea v-model="data.body[l].content" rows="6" />
<div class="small mt-3">
<code>{{ { block_type: data.block_type, uuid: data.uuid, order: data.order } }}</code>
</div>
</b-tab>
Now, when I load data from the API, I can correctly edit and save the content of these blocks -- which is weird considering that props are supposed to be immutable.
However, when I add new blocks, the textarea above wouldn't let me edit anything. I type stuff into it, and it just deletes it (or, I think, it replaces it with the "previous", or "initial" value). This does not happen when pulling content from the API (say, on page load).
Anyway, this led me to the discovery of immutability, I then created a local copy of the data prop like this
data: function() {
return {
block_data: this.data
}
}
and adjusted every data to be block_data but I get the same behaviour as before.
What exactly am I missing?
As the OP's comments, the root cause should be how to sync textarea value between child and parent component.
The issue the OP met should be caused by parent component always pass same value to the textarea inside the child component, that causes even type in something in the textarea, it still bind the same value which passed from parent component)
As Vue Guide said:
v-model is essentially syntax sugar for updating data on user input
events, plus special care for some edge cases.
The syntax sugar will be like:
the directive=v-model will bind value, then listen input event to make change like v-bind:value="val" v-on:input="val = $event.target.value"
So adjust your codes to like below demo:
for input, textarea HTMLElement, uses v-bind instead of v-model
then uses $emit to popup input event to parent component
In parent component, uses v-model to sync the latest value.
Vue.config.productionTip = false
Vue.component('child', {
template: `<div class="child">
<label>{{value.name}}</label><button #click="changeLabel()">Label +1</button>
<textarea :value="value.body" #input="changeInput($event)"></textarea>
</div>`,
props: ['value'],
methods: {
changeInput: function (ev) {
let newData = Object.assign({}, this.value)
newData.body = ev.target.value
this.$emit('input', newData) //emit whole prop=value object, you can only emit value.body or else based on your design.
// you can comment out `newData.body = ev.target.value`, then you will see the result will be same as the issue you met.
},
changeLabel: function () {
let newData = Object.assign({}, this.value)
newData.name += ' 1'
this.$emit('input', newData)
}
}
});
var vm = new Vue({
el: '#app',
data: () => ({
options: [
{id: 0, name: 'Apple', body: 'Puss in Boots'},
{id: 1, name: 'Banana', body: ''}
]
}),
})
.child {
border: 1px solid green;
}
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<span> Current: {{options}}</span>
<hr>
<div v-for="(item, index) in options" :key="index">
<child v-model="options[index]"></child>
</div>
</div>

V-bind with dynamically rendered components

I have a simple code where I have got input fields for the user and the user can click on a button to render more input fields, and I want to store those input fields.
<div id='wordslist'>
<div class='announcement'>Voer hieronder uw woorden in</div>
<div class='wordsdiv'>
<div id='onedictspec' v-for='arrayspot in wordupload.wordcount'>
<input type='text' class='inputwordtext' v-model='arrayspot[wordupload.wordcount][0]'>
<input type='text' class='inputwordtext' v-model='arrayspot[wordupload.wordcount][1]'>
</div>
</div>
<div id='morewords'>
<button class='morewordsbtn active hover' v-on:click='morewords'>More words</button>
</div>
Vuejs
data{
wordupload: {
wordcount: 20,
wordinput: [],
}
}
methods: {
morewords: function () {
this.wordupload.wordcount += 30
}
}
In short it renderes wordupload.wordcount number of #wordsdiv, and I am tring to give the input of those wordsdiv a spot in the wordinput array. But I can't v-model their value to the spot if it doesn't exist. Any ideas on what would be the best way to store those dynamically rendered input values would be much appreciated.
Are you looking for something like this?
https://jsfiddle.net/2nn58fa8/1/
Vue.component('wordlist', {
data: function() {
return {
wordupload: {
wordcount: 20,
wordinput: [],
}
}
},
created: function() {
this.wordupload.wordinput.length = this.wordupload.wordcount;
},
methods: {
morewords: function() {
this.wordupload.wordcount += 30;
this.wordupload.wordinput.length = this.wordupload.wordcount;
},
checkwords: function() {
console.log(this.wordupload.wordinput);
}
}
});
var vue = new Vue({
el: '#app'
});

Categories