I have a Vue page which loads an json array from an api and displays the content in a list of multiple s by using v-for.
If you focus on one of the textarea's or change the text a function automatically resize's the textarea to fit the content.
<div v-for="post in posts">
<textarea v-model="post.body" rows="1" #focus="resizeTextarea" #keyup="resizeTextarea"></textarea>
</div>
resizeTextarea(e) {
let area = e.target;
area.style.overflow = 'hidden';
area.style.height = area.scrollHeight + 'px';
}
With my limited Vue knowledge, I can't find a solution to automatically resize all textarea's after loading the data from the API. There is no #load on a textarea.
I was trying to reach the same goal with using a watcher on the data but it feels like a long workaround.
Anyone a descent solution? Thank you!
https://jsfiddle.net/oehoe83/c1b8frup/19/
One solution would be to create a component for your textarea element and then resize it in the mounted() hook. Here's an example using single-file components:
// CustomTextarea.vue
<template>
<textarea
v-model="value"
ref="textarea"
rows="1"
#focus="resize"
#keyup="resize"
>
</textarea>
</template>
<script>
export default {
props: {
value: {
type: String,
required: true,
}
},
mounted() {
this.resize();
},
methods: {
resize() {
const { textarea } = this.$refs;
textarea.style.height = textarea.scrollHeight - 4 + 'px';
}
}
}
</script>
Then in your parent:
<template>
<div v-for="post in posts">
<CustomTextarea v-model="post.body" />
</div>
</template>
<script>
import CustomTextarea from './CustomTextarea.vue';
export default {
components: {
CustomTextarea,
}
// etc.
}
</script>
Note: if you're using Vue 3, replace value with modelValue in the child component.
Alternatively you could use a watch like you suggested, there's nothing wrong with that. Something like this:
watch: {
posts() {
// Wait until the template has updated
this.$nextTick(() => {
[...document.querySelectorAll('textarea')].forEach(textarea => {
this.resizeTextarea({ target: textarea });
});
});
}
}
you can add the ref attribute :
<div id="app">
<div v-for="post in posts" ref="container">
<textarea v-model="post.body" rows="1"#focus="resizeTextarea" #keyup="resizeTextarea" ></textarea>
</div>
</div>
and add the following code at the end of mounted() :
this.$nextTick(()=>{
this.$refs.container.forEach( ta => {
ta.firstChild.dispatchEvent(new Event("keyup"));
});
});
Related
I have an input component that should call a method getUserSearch when the user types, and update the variable v_on_search with the variable v_model_search value.
I am displaying the content with the variables: v_model_search and v_on_search to test, although only the v-model seems to be working.
<template>
<div>
<input type="text" placeholder="search here" v-model="v_model_search" v-on:oninput="getUserSearch()">
<p>{{v_model_search}}</p>
<p>{{v_on_search}}</p>
</div>
</template>
<script>
export default {
name: 'SearchBox',
data () {
return {
v_model_search: '',
v_on_search: ''
}
},
methods: {
getUserSearch ()
{
this.v_on_search = this.v_model_search
}
}
}
</script>
Try
v-on:input="getUserSearch()"
or shorter
#input="getUserSearch()"
Vue recognizes HTML events but they don't have the "on" prefix
I am using Laravel + Vue.js to create a SPA. In the SPA, I am creating a form where user can write markdown content in it and click button to submit it. I want to show an error toaster at the bottom right corner of the screen if the user didn't input any content when they clicked the send button.
I am using boostrap vue toast to implement the error toaster.
However, when I clicked the send button, the error toaster will just blink for 1 second and dissapear immediately. Also, the error toaster blinks at top left corner which is different from what I wrote in the code below.
The mixin that contains the method to invoke the toast:
ErrorMessage.vue
# ErrorMessage.vue
<script>
export default {
methods: {
showErrorMessage (msg) {
this.$bvToast.toast(msg, {
title: ["Error!"],
variant: "danger",
toaster: "b-toaster-bottom-right"
});
}
}
};
</script>
I imported the above mixin in this vue component ArticleForm.vue.
ArticleForm.vue
<template>
<form #submit.prevent="passArticleData">
<div id="editor-markdown-editor">
<div id="editor-markdown">
<div
id="editor-markdown-tag-input"
#click="toggleTagModal"
>
<ul v-if="insertedTags.length">
<li v-for="tag in insertedTags"
:key="tag.id"
class="tag"
>
{{ tag.name }}
</li>
</ul>
<span v-else>タグを入力してください</span>
</div>
<div id="editor-markdown-textarea">
<textarea :value="input" #input="update" ref="textarea"></textarea>
<div id="drop-here"></div>
</div>
</div>
<div id="editor-preview">
<article v-html="compiledMarkdown(input)" class="markdown-render" ref="articleHtml"></article>
</div>
</div>
</form>
</template>
<script>
import _ from "lodash";
import ErrorMessage from "./mixins/ErrorMessage";
import { markdownTable } from "markdown-table";
export default {
props: {
article: Object,
tags: Array
},
mixins: [ErrorMessage],
data () {
return {
articleToPass: {
title: "",
body: "",
isDraft: true
},
input: "",
tagsToPass: [],
insertedTags: [],
tagModalOpen: false
};
},
methods: {
update: _.debounce(function (e) {
this.input = e.target.value;
}, 300),
passArticleData (e) {
// Get title from input
try {
this.articleToPass.title = this.$refs.articleHtml.getElementsByTagName("h1").item(0).innerHTML;
} catch (e) {
this.showErrorMessage(["Body md can't be blank"]);
}
// Remove first line(title) from users' input
this.articleToPass.body = this.input.substring(this.input.indexOf("\n") + 1);
// tag id of written article
const tagIds = this.insertedTags.map(obj => obj.id);
this.tagsToPass = tagIds;
this.$emit("handle-new-data", this.articleToPass, this.tagsToPass);
}
}
Parent component of the above vue component:
ArticleCreate.vue
<template>
<div id="content-area">
<header-component></header-component>
<main id="editor-area">
<article-form :article="article" :tags="tags" #handle-new-data="postArticle"></article-form>
</main>
</div>
</template>
<script>
import HeaderComponent from "./Header.vue";
import ArticleForm from "./ArticleForm.vue";
export default {
data () {
return {
article: {
title: "",
body: "",
is_draft: true
},
tags: []
};
},
components: {
HeaderComponent,
ArticleForm
},
methods: {
postArticle (articleObj, tagsData) {
const data = { article: articleObj, tags: tagsData };
axios.post("/api/articles", data)
.then((res) => {
this.$router.push({ name: "article.show", params: { article: res.data } });
});
}
}
};
</script>
I tried:
changed this.$bvToast to this.$root.$bvToast (based on this issue)
downgrade my bootstrap version from 4.6.0 to 4.5.3 (based on this question)
I have spent trying to solve this but failed. Does anyone know why is this happening and how can I make it work? Please let me know if you need any extra information. Thank you in advanced.
As stated in this comment on stackoverflow that is usually a sign of a bootstrap version mismatch.
I was actually able to reproduce that issue and also fix it with rolling back to bootstrap v4
Broken with bootstrap 5
https://codesandbox.io/s/bootstrap-vue-toasting-broken-with-bootstrap-5-bqe2c
Working with bootstrap 4
https://codesandbox.io/s/bootstrap-vue-toasting-working-with-bootstrap-4-jk2jl
I have figured out the problem.
The problem is that bootstrap-vue css is not loaded properly in my project.
Just add
import "bootstrap-vue/dist/bootstrap-vue.css";
to app.js and it works perfectly fine now.
Thank you
What i'm trying to do is have a root element with a prop that holds inner html like: hello<b>hey</b>
but i can't use v-html because this element also has children for example:
<template>
<component :is="element.tag" contenteditable="true">
<div contenteditable="false">
<span class="delete-obj" :id="'delete'+element.id" >delete</span>
</div>
<RenderString :string="element.content" />
</component>
</template>
<script>
import Vue from "vue";
Vue.component("RenderString", {
props: {
string: {
required: true,
type: String
}
},
render(h) {
const render = {
template: this.string ,
methods: {
markComplete() {
console.log('the method called')
}
}
}
return h(render)
}
})
export default {
name: "VElement",
props: {
element: {
required: false,
default: null
},
},
}
</script>
I have tried the above and I have tried using slots. I can solve it with vanilla JavaScript like element.innerText, but I don't want to. The main goal is that the element is editable when they type they are editing element.content that will be rendered and the div that's inside it is normal HTML that I also need.
The main problem is that the inner HTML that I want doesn't have a root element.
The element is something like:
{id:1,tag:"div",content:"hello<b>hey</b>"}
I want the final result to be:
<div contenteditable="true">
<div contenteditable="false">
<span class="delete-obj" :id="'delete'+element.id" >delete</span>
</div>
hello<b>hey</b>
<div>
And I want to edit hello<b>hey</b> when I click inside I don't want it wrapped in anything else and if I put v-html on the outer div the inner div is gone.
If you really aren't able to wrap the content in a parent element, maybe a good approach is to write a VUE directive that render the text.
JS FIDDLE FULL DEMO
//DIRECTIVE
Vue.directive('string', {
inserted(el, bind) {
/**
* Here you can manipulate the element as you need.
*/
el.insertAdjacentText('beforeend', bind.value);
}
});
//TEMPLATE
<template>
<component :is="element.tag" contenteditable="true" v-string="element.content">
<div contenteditable="false">
<span>delete</span>
</div>
</component>
</template>
Like Seblor said, v-html will work with nested html strings.
Simply replacing the RenderString component with a <div v-html="element.content"/> should get you what you want.
Tested with the given example of hello<b>hey</b>:
Vue.component('VElement', {
name: "VElement",
props: {
element: {
required: false,
default: null
},
},
template: '\
<component :is="element.tag" contenteditable="true">\
<div contenteditable="false">\
<span class="delete-obj" :id="\'delete\'+element.id">delete</span>\
</div>\
<div v-html="element.content"/>\
</component>'
})
new Vue({
el: '#app'
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<v-element :element="{id:1, tag:'div', content:'hello<b>hey</b>'}" />
</div>
I want to search for elements using an input field within a form. I am passing a value from a component to another and the content changes but only after pressing enter. Is there a way to automatically update the list, after every new letter is typed?
Here is my code:
Parent(App):
<template>
<div id="app">
<Header v-on:phrase-search="passPhrase" />
</div>
</template>
<script>
import Header from ...
export default {
name: "App",
components: {
Header,
},
data() {
return {
posts: [],
searchedPhrase: ""
};
}
computed: {
filteredPosts() {
let temp_text = this.searchedPhrase;
temp_text.trim().toLowerCase();
return this.posts.filter(post => {
return post.name.toLowerCase().match(temp_text);
});
}
},
methods: {
passPhrase(phrase) {
this.searchedPhrase = phrase;
}
}
};
</script>
Child(Header):
<template>
<div class="child">
<p>Search:</p>
<form #submit.prevent="phrasePassed">
<input type="text" v-model="phrase" />
</form>
</div>
</template>
<script>
export default {
name: "search",
data() {
return {
phrase: ""
};
},
methods: {
phrasePassed() {
this.$emit("phrase-search", this.phrase);
}
}
};
</script>
passPhrase() brings the value from the child to the parent and then filteredPosts() find appropriate elements. I suspect that the form might be guilty of this issue but I do not know precisely how to get rid of it and still be able to pass the value to the parent.
Thanks
in the child you use submit event which called on enter. you should use #input on the input itself. and btw you didnt need even to declare pharse in the data because you didnt use it in the child. you just pass it up
you it like so
<template>
<div class="child">
<p>Search:</p>
<form>
<input type="text" #input="phrasePassed">
</form>
</div>
</template>
<script>
export default {
name: "search",
methods: {
phrasePassed(e) {
this.$emit("phrase-search", e.target.value);
}
}
};
</script>
I am looking to add semi-transparent stamp type image over a particular part of the text.
We are using vue to build our new website.
I've not really found anything about how to do this in vue.
I'm looking to place the image over the 4th line of the code shown below. Basically what it does is display the 'Report accepted by:' text along with the person who accepted the report taken from the database. I'd like to display that with an overlapping image that resembles a stamp of approval.
<template>
<div class="clearfix">
<span>Report accepted by:</span>
<span v-if="report_info.accepted && report_info.accepted_by !== null">{{ memberById(report_info.accepted_by).callsign }}</span>
<button
v-if="isAdmin"
class="float-right"
v-on:click="acceptRejectReport"
>{{ acceptButtonText }}</button>
</div>
</template>
<script>
import { mapState, mapGetters } from "vuex"
export default {
name: "ReportApprovalComp",
mounted () {
this.checkAdmin();
},
data () {
return {
isAdmin: false
}
},
computed: {
acceptButtonText() {
if(this.report_info.accepted){
return "Revoke report acceptance";
} else {
return "Approve report";
}
},
...mapState("missionStore", {
report_info: state => state.report,
}),
...mapGetters("missionStore", [
"memberById"
])
},
methods: {
checkAdmin: async function () {
this.isAdmin = await this.$auth.isAdmin(this.$options.name);
},
acceptRejectReport: async function () {
this.$store.dispatch('missionStore/acceptRejectReport',
{
caller: this.$options.name,
member_id: await this.$auth.getUserId(),
});
}
}
}
</script>
<style scoped>
</style>
already has the logic ... you just need to actually provide your img element as the last child inside that span. Your span will need to have css position:relative and your img needs css position:absolute;top:0;right:0; ... you might need display:inline-block on your span
<span v-if="report_info.accepted && report_info.accepted_by !== null"
style="position:relative;display:inline-block;">
{{ memberById(report_info.accepted_by).callsign }}
<img style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
width:100%;height:auto;" src="web path to img file"/>
</span>