9

<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?

J W
  • 165
  • 1
  • 1
  • 6

9 Answers9

10

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>
Bert
  • 80,741
  • 17
  • 199
  • 164
  • I ended up breaking it into a .Vue component with your code : – J W Sep 19 '17 at 13:06
  • – J W Sep 19 '17 at 13:06
  • This doesn't work as when you hit the limit you can no longer delete characters or add characters. – swade Dec 27 '17 at 19:40
  • @StevenWade You're right, there was a kind of a bug. I modified the code to allow deleting characters once the max is reached. If specific keycodes need to be exlcluded, then the fix would have to be more elaborate, but this answer is not meant to be bulletproof, just give an idea of how to accomplish the desired effect. – Bert Dec 27 '17 at 20:41
  • 3
    Also note that the characters that are not keys 1 to 0 or letters a-z will still be added (anything outside the range 48 -> 90) for keyCode, really – Andy Jul 03 '18 at 15:09
  • My answer solves the 2 mentioned problems. Please look there. – Nils-o-mat Oct 16 '18 at 08:48
10

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

Alphons
  • 303
  • 2
  • 6
3

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();
       }
   }
} 
J. Rambo
  • 125
  • 1
  • 2
  • 8
3

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...

Nils-o-mat
  • 1,132
  • 17
  • 31
1

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);
    }
})
  1. Event() has browser compatibility issue.
  2. Unfortunately for me, keydown approach seems not working good with CJK.
  3. there can be side effects since this method fires input event double time.
0

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
            }
        }
    }

}
Skatox
  • 4,237
  • 12
  • 42
  • 47
J W
  • 165
  • 1
  • 1
  • 6
0

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
        }
    },
},
  • 2
    The problem with this solution is that you are modifying a watched property inside that properties watcher. While the condition may prevent infinite recursion from happening, it is still a discouraged practice. – J. Rambo May 31 '18 at 15:10
0

Simply use maxlength attribute like this:

<textarea v-model="value" maxlength="50" />
technophyle
  • 7,972
  • 6
  • 29
  • 50
0

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: ''
       }
  }