Vue watch value from parent is in reverse - vue.js

I have a custom component that lets users type in text and sends it to the backend where I do some computation and spit the new text back out with html in it.
My problem is when the user types into this textarea, it reverses all the text and keeps the cursors at the beginning of the textarea. So now 'foo bar' becomes 'rab oof'... This only has happened since I added in watch. I could delete the watcher, but I need it (or need another way) to apply my updates to the textarea, via the foo variable when I set foo equal to something from the parent.
console.log(v) writes out the reverse text.
Any idea how to change this?
Custom componet:
<template>
<div contenteditable="true" #input="updateHTML" class="textareaRoot"></div>
</template>
<script>
export default {
name: 'htmlTextArea',
props:['value'],
mounted: function () {
this.$el.innerHTML = this.value;
},
watch: {
value(v) {
this.$el.innerHTML = v; //v is the reverse text.
}
},
methods: {
updateHTML: function(e) {
this.$emit('input', e.target.innerHTML);
}
}
}
</script>
Parent that uses custom component:
<htmlTextArea id="textarea" v-model="foo"></htmlTextArea>
...
<script>
...
methods: {
triggerOnClick() {
this.foo = 'something';//Without the watcher, when I change this.foo to something the actual textarea does not display the new data that I assigned to foo. But in Vue dev tools I can see the new change.
}
UPDATE:
Vue.component('html-textarea',{
template:'<div contenteditable="true" #input="updateHTML"></div>',
props:['value'],
mounted: function () {
this.$el.innerHTML = this.value;
},
watch: {
value(v) {
this.$el.innerHTML = v;
}
},
methods: {
updateHTML: function(e) {
this.$emit('input', e.target.innerHTML);
}
}
});
new Vue({
el: '#app',
data () {
return {
foo: '',
}
}
});
<script src="https://unpkg.com/vue"></script>
<div id="app">Type here:
<html-textarea spellcheck="false" id="textarea" v-model="foo"> </html-textarea>
</div>

The problem is that when you set the innerHTML of a contenteditable element, you lose the selection (cursor position).
So you should perform the following steps when setting:
save the current cursor position;
set the innerHTML;
restore the cursor position.
Saving and restoring is the tricky part. Luckily I got these two handy functions that do the job for latest IE and newer. See below.
function saveSelection(containerEl) {
var range = window.getSelection().getRangeAt(0);
var preSelectionRange = range.cloneRange();
preSelectionRange.selectNodeContents(containerEl);
preSelectionRange.setEnd(range.startContainer, range.startOffset);
var start = preSelectionRange.toString().length;
return {
start: start,
end: start + range.toString().length
}
}
function restoreSelection(containerEl, savedSel) {
var charIndex = 0, range = document.createRange();
range.setStart(containerEl, 0);
range.collapse(true);
var nodeStack = [containerEl],
node, foundStart = false,
stop = false;
while (!stop && (node = nodeStack.pop())) {
if (node.nodeType == 3) {
var nextCharIndex = charIndex + node.length;
if (!foundStart && savedSel.start >= charIndex && savedSel.start <= nextCharIndex) {
range.setStart(node, savedSel.start - charIndex);
foundStart = true;
}
if (foundStart && savedSel.end >= charIndex && savedSel.end <= nextCharIndex) {
range.setEnd(node, savedSel.end - charIndex);
stop = true;
}
charIndex = nextCharIndex;
} else {
var i = node.childNodes.length;
while (i--) {
nodeStack.push(node.childNodes[i]);
}
}
}
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
Vue.component('htmltextarea', {
template: '#hta',
name: 'htmlTextArea',
props:['value'],
mounted: function () {
this.$el.innerHTML = this.value;
},
watch: {
value(v) {
if (v === 'yes') {
let selection = saveSelection(this.$el);
this.$el.innerHTML = 'no!';
this.$emit('input', 'no!');
restoreSelection(this.$el, selection);
}
}
},
methods: {
updateHTML: function(e) {
this.$emit('input', e.target.innerHTML);
}
}
});
new Vue({
el: '#app',
data: {
foo: 'Clear this and type "yes" (without the quotes). It should become "no!".'
}
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<htmltextarea id="textarea" v-model="foo"></htmltextarea>
<hr>
Result: <pre>{{ foo }}</pre>
</div>
<template id="hta">
<div contenteditable="true" #input="updateHTML" class="textareaRoot"></div>
</template>
In your app, I recommend you place them in a dedicated .js file, just for better organization.

Related

VUE Js child is not updating when parent updates

I am using VUE JS and I want to have group of checkboxes as below. When someone clicked on main checkbox all the checkboxes under that check box should be selected. Please find the attached image for your reference
To accomplish this scenario I am using 2 main components. When someone clicked on a component I am adding that component to selectedStreams array. Selected stream array structure is similar to below structure
checked: {
g1: ['val1'],
g2: []
}
When I click on heading checkbox I am triggering function
clickAll and try to change the selectedStreams[keyv].
But this action doesn't trigger the child component and automatically checked the checkbox.
Can I know the reason why when I changed the parent v-model value it is not visible in child UI.
Parent Component
<template>
<div>
<b-form-group>
<StreamCheckBox
v-for="(opts, keyv) in loggedInUsernamesf"
:key="keyv"
:name="opts"
:main="keyv"
v-model="selectedStreams[keyv]"
#clickAll="clickAll($event, keyv)"
></StreamCheckBox>
</b-form-group>
</div>
</template>
<script>import StreamCheckBox from "./StreamCheckBox";
export default {
name: "GroupCheckBox",
components: {StreamCheckBox},
data(){
return {
selectedStreams:{}
}
},
computed: {
loggedInUsernamesf() {
var username_arr = JSON.parse(this.$sessionStorage.access_info_arr);
var usernames = {};
if (!username_arr) return;
for (let i = 0; i < username_arr.length; i++) {
usernames[username_arr[i].key] = [];
var payload = {};
payload["main"] = username_arr[i].val.name;
payload["type"] = username_arr[i].val.type;
if (username_arr[i].val.permissions) {
for (let j = 0; j < username_arr[i].val.permissions.length; j++) {
payload["value"] = username_arr[i].key + username_arr[i].val.permissions[j].streamId;
payload["text"] = username_arr[i].val.permissions[j].streamId;
}
}
usernames[username_arr[i].key].push(payload);
}
return usernames;
},
},
methods: {
clickAll(e, keyv) {
if (e && e.includes(keyv)) {
this.selectedStreams[keyv] = this.loggedInUsernamesf[keyv].map(
opt => {
return opt.value
}
);
}
console.log(this.selectedStreams[keyv]);
}
}
}
</script>
<style scoped>
</style>
Child Component
<template>
<div style="text-align: left;">
<b-form-checkbox-group style="padding-left: 0;"
id="flavors"
class="ml-4"
stacked
v-model="role"
:main="main"
>
<b-form-checkbox
class="font-weight-bold main"
:main="main"
:value="main"
#input="checkAll(main)"
>{{ name[0].main }}</b-form-checkbox>
<b-form-checkbox
v-for="opt in displayStreams"
:key="opt.value"
:value="opt.value"
>{{ opt.text }}</b-form-checkbox>
</b-form-checkbox-group>
</div>
</template>
<script>
export default {
name:"StreamCheckBox",
props: {
value: {
type: Array,
},
name: {
type: Array,
},
main:{
type:String
}
},
computed:{
role: {
get: function(){
return this.value;
},
set: function(val) {
this.$emit('input', val);
}
},
displayStreams: function () {
return this.name.filter(i => i.value)
},
},
methods:{
checkAll(val)
{
this.$emit('clickAll', val);
}
}
}
</script>
First of all, I am not sure what is the purpose of having props in your parent component. I think you could just remove props from your parent and leave in the child component only.
Second of all, the reason for not triggering the changes could be that you are dealing with arrays, for that you could set a deep watcher in your child component:
export default {
props: {
value: {
type: Array,
required: true,
},
},
watch: {
value: {
deep: true,
//handle the change
handler() {
console.log('array updated');
}
}
}
}
You can find more info here and here.

Replace tag dynamically returns the object instead of the contents

I'm building an chat client and I want to scan the messages for a specific tag, in this case [item:42]
I'm passing the messages one by one to the following component:
<script>
import ChatItem from './ChatItem'
export default {
props :[
'chat'
],
name: 'chat-parser',
data() {
return {
testData: []
}
},
methods : {
parseMessage(msg, createElement){
const regex = /(?:\[\[item:([0-9]+)\]\])+/gm;
let m;
while ((m = regex.exec(msg)) !== null) {
msg = msg.replace(m[0],
createElement(ChatItem, {
props : {
"id" : m[1],
},
}))
if (m.index === regex.lastIndex) {
regex.lastIndex++;
}
}
return msg
},
},
render(createElement) {
let user = "";
let msg = this.parseMessage(this.$props.chat.Message, createElement)
return createElement(
'div',
{
},
[
// "hello",// createElement("render function")
createElement('span', '['+ this.$props.chat.Time+'] '),
user,
msg,
]
)
}
};
</script>
I thought passing createElement to the parseMessage method would be a good idea, but it itsn't working properly as it replaces the tag with [object object]
The chatItem looks like this :
<template>
<div>
<span v-model="item">chatITem : {{ id }}</span>
</div>
</template>
<script>
export default {
data: function () {
return {
item : [],
}
},
props :['id'],
created() {
// this.getItem()
},
methods: {
getItem: function(){
obj.item = ["id" : "42", "name": "some name"]
},
},
}
</script>
Example :
if the message looks like this : what about [item:42] OR [item:24] both need to be replaced with the chatItem component
While you can do it using a render function that isn't really necessary if you just parse the text into a format that can be consumed by the template.
In this case I've kept the parser very primitive. It yields an array of values. If a value is a string then the template just dumps it out. If the value is a number it's assumed to be the number pulled out of [item:24] and passed to a <chat-item>. I've used a dummy version of <chat-item> that just outputs the number in a <strong> tag.
new Vue({
el: '#app',
components: {
ChatItem: {
props: ['id'],
template: '<strong>{{ id }}</strong>'
}
},
data () {
return {
text: 'Some text with [item:24] and [item:42]'
}
},
computed: {
richText () {
const text = this.text
// The parentheses ensure that split doesn't throw anything away
const re = /(\[item:\d+\])/g
// The filter gets rid of any empty strings
const parts = text.split(re).filter(item => item)
return parts.map(part => {
if (part.match(re)) {
// This just converts '[item:24]' to the number 24
return +part.slice(6, -1)
}
return part
})
}
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<template v-for="part in richText">
<chat-item v-if="typeof part === 'number'" :id="part"></chat-item>
<template v-else>{{ part }}</template>
</template>
</div>
If I were going to do it with a render function I'd do it pretty much the same way, just replacing the template with a render function.
If the text parsing requirements were a little more complicated then I wouldn't just return strings and numbers. Instead I'd use objects to describe each part. The core ideas remain the same though.

Vue JS: Setting computed property not invoking v-if

When a method sets a computed property, v-ifs are not getting invoked. I thought a computed property logically worked just like a 'regular' property.
// theState can't be moved into Vue object, just using for this example
var theState = false;
var app = new Vue({
el: '#demo',
data: {
},
methods: {
show: function() {
this.foo = true;
},
hide: function() {
this.foo = false;
}
},
computed: {
foo: {
get: function() {
return theState;
},
set: function(x) {
theState = x;
}
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="demo">
<input type=button value="Show" #click="show()">
<input type=button value="Hide" #click="hide()">
<div v-if="foo">Hello</div>
</div>
Am I doing something wrong?
Vue doesn't observe changes in variables outside the component; you need to import that value into the component itself in order for the reactivity to work.
var theState = false; // <-- external variable Vue doesn't know about
var app = new Vue({
el: '#demo',
data: {
myState: theState // <-- now Vue knows to watch myState for changes
},
methods: {
show: function() {
this.foo = true;
theState = true; // <-- this won't affect the component, but will keep your external variable in synch
},
hide: function() {
this.foo = false;
theState = false; // <-- this won't affect the component, but will keep your external variable in synch
}
},
computed: {
foo: {
get: function() {
return this.myState;
},
set: function(x) {
this.myState = x;
}
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="demo">
<input type=button value="Show" #click="show()">
<input type=button value="Hide" #click="hide()">
<div v-if="foo">Hello</div>
</div>
(Edited to remove incorrect info; I forgot computed property setters existed for a while there)
You need to move theState into data. Otherwise it wont be reactive, so vue wont know when its changed, so v-if or any other reactivity wont work.
var app = new Vue({
el: '#demo',
data: {
foo2: false,
theState: false
// 1
},
methods: {
show: function() {
this.foo = true;
},
hide: function() {
this.foo = false;
}
},
computed: {
foo: { // 2
get: function() {
return this.theState
},
set: function(x) {
this.theState = x;
}
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="demo">
<input type=button value="Show" #click="show()">
<input type=button value="Hide" #click="hide()">
<div v-if="foo">Hello</div>
</div>

Filtering a list of objects in Vue without altering the original data

I am diving into Vue for the first time and trying to make a simple filter component that takes a data object from an API and filters it.
The code below works but i cant find a way to "reset" the filter without doing another API call, making me think im approaching this wrong.
Is a Show/hide in the DOM better than altering the data object?
HTML
<button v-on:click="filterCats('Print')">Print</button>
<div class="list-item" v-for="asset in filteredData">
<a>{{ asset.title.rendered }}</a>
</div>
Javascript
export default {
data() {
return {
assets: {}
}
},
methods: {
filterCats: function (cat) {
var items = this.assets
var result = {}
Object.keys(items).forEach(key => {
const item = items[key]
if (item.cat_names.some(cat_names => cat_names === cat)) {
result[key] = item
}
})
this.assets = result
}
},
computed: {
filteredData: function () {
return this.assets
}
},
}
Is a Show/hide in the DOM better than altering the data object?
Not at all. Altering the data is the "Vue way".
You don't need to modify assets to filter it.
The recommended way of doing that is using a computed property: you would create a filteredData computed property that depends on the cat data property. Whenever you change the value of cat, the filteredData will be recalculated automatically (filtering this.assets using the current content of cat).
Something like below:
new Vue({
el: '#app',
data() {
return {
cat: null,
assets: {
one: {cat_names: ['Print'], title: {rendered: 'one'}},
two: {cat_names: ['Two'], title: {rendered: 'two'}},
three: {cat_names: ['Three'], title: {rendered: 'three'}}
}
}
},
computed: {
filteredData: function () {
if (this.cat == null) { return this.assets; } // no filtering
var items = this.assets;
var result = {}
Object.keys(items).forEach(key => {
const item = items[key]
if (item.cat_names.some(cat_names => cat_names === this.cat)) {
result[key] = item
}
})
return result;
}
},
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<button v-on:click="cat = 'Print'">Print</button>
<div class="list-item" v-for="asset in filteredData">
<a>{{ asset.title.rendered }}</a>
</div>
</div>

Vue : Limit characters in text area input, truncate filter?

<textarea name="" id="" cols="30" rows="10" v-model="$store.state.user.giftMessage | truncate 150"></textarea>
I tried creating a custom filter :
filters: {
truncate(text, stop, clamp) {
return text.slice(0, stop) + (stop < text.length ? clamp || '...' : '')
}
}
but that didn't broke the build when I put it on the v-model for the input...
Any advice?
This is one of those cases, where you really want to use a component.
Here is an example component that renders a textarea and limits the amount of text.
Please note: this is not a production ready, handle all the corner cases component. It is intended as an example.
Vue.component("limited-textarea", {
props:{
value:{ type: String, default: ""},
max:{type: Number, default: 250}
},
template: `
<textarea v-model="internalValue" #keydown="onKeyDown"></textarea>
`,
computed:{
internalValue: {
get() {return this.value},
set(v){ this.$emit("input", v)}
}
},
methods:{
onKeyDown(evt){
if (this.value.length >= this.max) {
if (evt.keyCode >= 48 && evt.keyCode <= 90) {
evt.preventDefault()
return
}
}
}
}
})
This component implements v-model and only emits a change to the data if the length of the text is less than the specified max. It does this by listening to keydown and preventing the default action (typing a character) if the length of the text is equal to or more than the allowed max.
console.clear()
Vue.component("limited-textarea", {
props:{
value:{ type: String, default: ""},
max:{type: Number, default: 250}
},
template: `
<textarea v-model="internalValue" #keydown="onKeyDown"></textarea>
`,
computed:{
internalValue: {
get() {return this.value},
set(v){ this.$emit("input", v)}
}
},
methods:{
onKeyDown(evt){
if (this.value.length >= this.max) {
if (evt.keyCode >= 48 && evt.keyCode <= 90) {
evt.preventDefault()
return
}
}
}
}
})
new Vue({
el: "#app",
data:{
text: ""
}
})
<script src="https://unpkg.com/vue#2.4.2"></script>
<div id="app">
<limited-textarea v-model="text"
:max="10"
cols="30"
rows="10">
</limited-textarea>
</div>
Another issue with the code in the question is Vuex will not allow you set a state value directly; you have to do it through a mutation. That said, there should be a Vuex mutation that accepts the new value and sets it, and the code should commit the mutation.
mutations: {
setGiftMessage(state, message) {
state.user.giftMessage = message
}
}
And in your Vue:
computed:{
giftMessage:{
get(){return this.$store.state.user.giftMessage},
set(v) {this.$store.commit("setGiftMessage", v)}
}
}
Technically the code should be using a getter to get the user (and it's giftMessage), but this should work. In the template you would use:
<limited-textarea cols="30" rows="10" v-model="giftMessage"></limited-textarea>
Here is a complete example using Vuex.
console.clear()
const store = new Vuex.Store({
state:{
user:{
giftMessage: "test"
}
},
getters:{
giftMessage(state){
return state.user.giftMessage
}
},
mutations:{
setGiftMessage(state, message){
state.user.giftMessage = message
}
}
})
Vue.component("limited-textarea", {
props:{
value:{ type: String, default: ""},
max:{type: Number, default: 250}
},
template: `
<textarea v-model="internalValue" #keydown="onKeyDown"></textarea>
`,
computed:{
internalValue: {
get() {return this.value},
set(v){ this.$emit("input", v)}
}
},
methods:{
onKeyDown(evt){
if (this.value.length >= this.max) {
if (evt.keyCode >= 48 && evt.keyCode <= 90) {
evt.preventDefault()
return
}
}
}
}
})
new Vue({
el: "#app",
store,
computed:{
giftMessage:{
get(){ return this.$store.getters.giftMessage},
set(v){ this.$store.commit("setGiftMessage", v)}
}
}
})
<script src="https://unpkg.com/vue#2.4.2"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/2.4.0/vuex.js"></script>
<div id="app">
<limited-textarea v-model="giftMessage"
:max="10"
cols="30"
rows="10">
</limited-textarea>
Message: {{giftMessage}}
</div>
Sorry to break in. Was looking for a solution. Looked at all of them.
For me they look too complicated. I'm always looking for symplicity.
Therefor I like the answer of #Даниил Пронин. But it has the by #J. Rambo noted potential problem.
To stay as close as possible to the native html textelement. The solution I came up with is:
Vue Template
<textarea v-model="value" #input="assertMaxChars()">
JavaScript
let app = new Vue({
el: '#app',
data: {
value: 'Vue is working!',
maxLengthInCars: 25
},
methods: {
assertMaxChars: function () {
if (this.value.length >= this.maxLengthInCars) {
this.value = this.value.substring(0,this.maxLengthInCars);
}
}
}
})
Here is a REPL link: https://repl.it/#tsboh/LimitedCharsInTextarea
The upside I see is:
the element is as close as possible to the native element
simple code
textarea keeps focus
delete still works
works with pasting text as well
Anyway
happy coding
While I agree with the selected answer. You can also easily prevent the length using a keydown event handler.
Vue Template
<input type="text" #keydown="limit( $event, 'myModel', 3)" v-model="myModel" />
JavaScript
export default {
name: 'SomeComponent',
data () {
return {
myModel: ''
};
},
methods: {
limit( event, dataProp, limit ) {
if ( this[dataProp].length >= limit ) {
event.preventDefault();
}
}
}
}
Doing this way, you can also use regular expression to event prevent the type of keys accepted. For instance, if you only wanted to accept numeric values you can do the following.
methods: {
numeric( event, dataProp, limit ) {
if ( !/[0-9]/.test( event.key ) ) {
event.preventDefault();
}
}
}
I have improved on #J Ws answer. The resulting code does not have to define how to react on which keypress, which is why it can be used with any character in contrast to the accepted answer. It only cares about the string-length of the result. It also can handle Copy-Paste-actions and cuts overlong pastes to size:
Vue.component("limitedTextarea", {
props: {
value: {
type: String,
default: ""
},
max: {
type: Number,
default: 25
}
},
computed: {
internalValue: {
get: function () {
return this.value;
},
set: function (aModifiedValue) {
this.$emit("input", aModifiedValue.substring(0, this.max));
}
}
},
template: '<textarea v-model="internalValue" #keydown="$forceUpdate()" #paste="$forceUpdate()"></textarea>'
});
The magic lies in the #keydown and #paste-events, which force an update. As the value is already cut to size correctly, it assures that the internalValue is acting accordingly.
If you also want to protect the value from unchecked script-changes, you can add the following watcher:
watch: {
value: function(aOldValue){
if(this.value.length > this.max){
this.$emit("input", this.value.substring(0, this.max));
}
}
}
I just found a problem with this easy solution: If you set the cursor somewhere in the middle and type, transgressing the maximum, the last character is removed and the cursor set to the end of the text. So there is still some room for improvement...
My custom directive version. Simple to use.
<textarea v-model="input.textarea" v-max-length="10"></textarea>
Vue.directive('maxlength',{
bind: function(el, binding, vnode) {
el.dataset.maxLength = Number(binding.value);
var handler = function(e) {
if (e.target.value.length > el.dataset.maxLength) {
e.target.value = e.target.value.substring(0, el.dataset.maxLength);
var event = new Event('input', {
'bubbles': true,
'cancelable': true
});
this.dispatchEvent(event);
return;
}
};
el.addEventListener('input', handler);
},
update: function(el, binding, vnode) {
el.dataset.maxLength = Number(binding.value);
}
})
Event() has browser compatibility issue.
Unfortunately for me, keydown approach seems not working good with CJK.
there can be side effects since this method fires input event double time.
Best way is to use watch to string length and set old value if string is longer than you want:
watch: {
'inputModel': function(val, oldVal) {
if (val.length > 250) {
this.inputModel = oldVal
}
},
},
Simply use maxlength attribute like this:
<textarea v-model="value" maxlength="50" />
I used your code and broke it out into a .Vue component, thanks!
<template>
<textarea v-model="internalValue" #keydown="onKeyDown"></textarea>
</template>
<script>
export default {
props:{
value:{ type: String, default: ""},
max:{type: Number, default: 250}
},
computed:{
internalValue: {
get() {return this.value},
set(v){ this.$emit("input", v)}
}
},
methods:{
onKeyDown(evt){
if (this.value.length >= this.max) {
evt.preventDefault();
console.log('keydown');
return
}
}
}
}
I did this using tailwind 1.9.6 and vue 2:
<div class="relative py-4">
<textarea
v-model="comment"
class="w-full border border-gray-400 h-16 bg-gray-300 p-2 rounded-lg text-xs"
placeholder="Write a comment"
:maxlength="100"
/>
<span class="absolute bottom-0 right-0 text-xs">
{{ comment.length }}/100
</span>
</div>
// script
data() {
return {
comment: ''
}
}