Vue基础教程(93)表单输入绑定之多行文本输入框:别再用input憋大招了!textarea才是你表单开发的隐藏BOSS

一、为什么你写的textarea总在摸鱼?

还记得被表单支配的恐惧吗?刚学Vue时,我天真地以为所有输入框都是input的天下。直到产品经理微笑着说出那句经典台词:“这里能不能让用户多输点文字?比如500字?”

然后我写下了这段祖传代码:

<input type="text" v-model="message" placeholder="快来吐槽吧!">

结果?用户反馈:“你们这个输入框是给蚂蚁用的吗?打两行字就没了!”

这时候,textarea就像救世主一样登场了!这货简直就是输入框里的加长林肯——空间大、坐着舒服,专门对付话痨用户。

但问题是,很多新手拿到textarea后,依然在用input的思维去操作它。最常见的翻车现场:

<!-- 错误示范:这货根本不是这么玩的! -->
<textarea v-model="message" value="初始值"></textarea>

兄弟,textarea是自闭标签啊!它不像input那样用value属性,内容得写在标签中间:

<!-- 正确姿势 -->
<textarea v-model="message">这里是初始值</textarea>

不过,在Vue里我们更推荐用v-model统一管理,这才是真·高效做法。

二、v-model和textarea的“相亲”现场

v-model遇见textarea,就像西红柿遇见了鸡蛋——天生一对。来看看它们怎么擦出火花的:

基础绑定(小白版):

<template>
  <div>
    <h3>吐槽箱(当前字数:{{ message.length }})</h3>
    <textarea 
      v-model="message"
      placeholder="有什么不爽的,尽管吐出来吧!"
      rows="4"
      cols="50"
    ></textarea>
    <p>你的吐槽预览:{{ message }}</p>
  </div>
</template>
<script>
export default {
  data() {
    return {
      message: ''
    }
  }
}
</script>

就这?就这?你以为这就完了?图样图森破!真正的战场才刚刚开始。

三、textarea的“装备升级”之路

1. 自动增高——拒绝滚动条噩梦

用户哐哐哐输入小作文,结果还要在狭小的文本框里滚来滚去?不存在的!

<template>
  <div>
    <textarea
      v-model="message"
      @input="autoResize"
      ref="myTextarea"
      placeholder="随便写,这框会自己变大..."
      style="resize: none; overflow: hidden;"
    ></textarea>
  </div>
</template>
<script>
export default {
  methods: {
    autoResize() {
      const textarea = this.$refs.myTextarea;
      textarea.style.height = 'auto';
      textarea.style.height = textarea.scrollHeight + 'px';
    }
  }
}
</script>

这招简直绝了!textarea会根据内容自动调整高度,用户体验直接拉满。

2. 字数统计——防患于未然

<template>
  <div>
    <textarea
      v-model="message"
      :maxlength="maxLength"
      placeholder="最多输入{{ maxLength }}字"
    ></textarea>
    <p :class="{'warning': isNearLimit}">
      字数:{{ message.length }} / {{ maxLength }}
      <span v-if="isNearLimit" style="color: orange;">快超了!手下留情!</span>
    </p>
  </div>
</template>
<script>
export default {
  data() {
    return {
      message: '',
      maxLength: 200
    }
  },
  computed: {
    isNearLimit() {
      return this.message.length > this.maxLength * 0.8;
    }
  }
}
</script>

3. 防手抖提交——给用户反悔的机会

<template>
  <div>
    <textarea v-model="draftMessage" placeholder="写下你的心声..."></textarea>
    <button @click="saveDraft">存草稿</button>
    <button @click="submit" :disabled="!draftMessage">提交</button>
    
    <!-- 草稿列表 -->
    <div v-if="drafts.length">
      <h4>草稿箱(点击继续编辑)</h4>
      <div 
        v-for="draft in drafts" 
        :key="draft.id"
        @click="loadDraft(draft)"
        class="draft-item"
      >
        {{ draft.content.substring(0, 30) }}...
      </div>
    </div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      draftMessage: '',
      drafts: [],
      nextId: 1
    }
  },
  methods: {
    saveDraft() {
      if (!this.draftMessage.trim()) return;
      
      this.drafts.push({
        id: this.nextId++,
        content: this.draftMessage,
        time: new Date().toLocaleString()
      });
      alert('草稿保存成功!');
    },
    loadDraft(draft) {
      this.draftMessage = draft.content;
    },
    submit() {
      if (!this.draftMessage.trim()) return;
      
      // 这里写提交逻辑
      console.log('提交内容:', this.draftMessage);
      alert('提交成功!');
      this.draftMessage = '';
    }
  }
}
</script>
四、进阶玩法——让你的textarea秀起来

1. Markdown实时预览

<template>
  <div class="markdown-editor">
    <div class="editor-panel">
      <textarea
        v-model="markdownText"
        placeholder="输入Markdown语法..."
        @input="updatePreview"
      ></textarea>
    </div>
    <div class="preview-panel">
      <div v-html="compiledMarkdown"></div>
    </div>
  </div>
</template>
<script>
// 简单Markdown解析(实际项目建议使用marked.js等库)
export default {
  data() {
    return {
      markdownText: '# 标题\n\n写点什么吧...',
    }
  },
  computed: {
    compiledMarkdown() {
      return this.markdownText
        .replace(/# (.*)/g, '<h1>$1</h1>')
        .replace(/\*\*(.*)\*\*/g, '<strong>$1</strong>')
        .replace(/\n/g, '<br>');
    }
  }
}
</script>
<style scoped>
.markdown-editor {
  display: flex;
  gap: 20px;
}
.editor-panel, .preview-panel {
  flex: 1;
  border: 1px solid #ccc;
  padding: 10px;
  min-height: 300px;
}
</style>

2. @提及功能

<template>
  <div>
    <textarea
      v-model="commentText"
      @input="checkMention"
      @keydown.tab.prevent="completeMention"
      ref="commentArea"
      placeholder="输入@提及用户..."
    ></textarea>
    
    <div v-if="showUserList" class="user-list">
      <div
        v-for="user in filteredUsers"
        :key="user.id"
        @click="selectUser(user)"
        class="user-item"
      >
        {{ user.name }}
      </div>
    </div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      commentText: '',
      showUserList: false,
      users: [
        { id: 1, name: '张三' },
        { id: 2, name: '李四' },
        { id: 3, name: '王五' }
      ],
      currentMentionStart: -1
    }
  },
  computed: {
    filteredUsers() {
      if (this.currentMentionStart === -1) return [];
      
      const currentText = this.commentText.substring(this.currentMentionStart + 1);
      return this.users.filter(user => 
        user.name.includes(currentText)
      );
    }
  },
  methods: {
    checkMention() {
      const cursorPos = this.$refs.commentArea.selectionStart;
      const textBeforeCursor = this.commentText.substring(0, cursorPos);
      const lastAtPos = textBeforeCursor.lastIndexOf('@');
      
      if (lastAtPos > -1 && /[\s]@[\w]*$/.test(textBeforeCursor)) {
        this.currentMentionStart = lastAtPos;
        this.showUserList = true;
      } else {
        this.showUserList = false;
        this.currentMentionStart = -1;
      }
    },
    
    selectUser(user) {
      const textBefore = this.commentText.substring(0, this.currentMentionStart);
      const textAfter = this.commentText.substring(this.$refs.commentArea.selectionStart);
      
      this.commentText = textBefore + '@' + user.name + ' ' + textAfter;
      this.showUserList = false;
      this.currentMentionStart = -1;
      
      this.$nextTick(() => {
        this.$refs.commentArea.focus();
      });
    },
    
    completeMention() {
      if (this.filteredUsers.length > 0) {
        this.selectUser(this.filteredUsers[0]);
      }
    }
  }
}
</script>
五、避坑指南——那些年我们踩过的textarea坑

坑1:v-model和value属性混用

<!-- 大坑!Vue会发出警告 -->
<textarea v-model="message" value="初始值"></textarea>
<!-- 正确姿势 -->
<textarea v-model="message">{{ message }}</textarea>

坑2:忘记处理空白字符

// 用户可能输入一堆空格
submit() {
  // 错误:直接提交
  // axios.post('/api', { content: this.message })
  
  // 正确:先trim一下
  const content = this.message.trim();
  if (!content) {
    alert('请输入有效内容!');
    return;
  }
  // 再提交
}

坑3:移动端适配问题

<textarea
  v-model="message"
  :rows="isMobile ? 3 : 5"
  :class="{ 'mobile-textarea': isMobile }"
  @focus="handleFocus"
></textarea>
六、完整实战:打造一个智能评论框

是时候展示真正的技术了!我们来搞一个功能齐全的评论框:

<template>
  <div class="smart-comment">
    <!-- 工具栏 -->
    <div class="toolbar">
      <button @click="insertText('**粗体**')">粗体</button>
      <button @click="insertText('*斜体*')">斜体</button>
      <button @click="insertText('`代码`')">代码</button>
      <button @click="showEmoji = !showEmoji">😊</button>
    </div>
    
    <!-- 表情选择 -->
    <div v-if="showEmoji" class="emoji-picker">
      <span
        v-for="emoji in emojis"
        :key="emoji"
        @click="insertText(emoji)"
        class="emoji"
      >{{ emoji }}</span>
    </div>
    
    <!-- 文本输入区 -->
    <textarea
      v-model="comment"
      ref="textarea"
      :placeholder="placeholder"
      @input="handleInput"
      @paste="handlePaste"
      :maxlength="maxLength"
      class="comment-textarea"
    ></textarea>
    
    <!-- 底部信息 -->
    <div class="footer">
      <span :class="{
        'normal': lengthRatio < 0.8,
        'warning': lengthRatio >= 0.8,
        'danger': lengthRatio >= 0.95
      }">
        {{ comment.length }} / {{ maxLength }}
      </span>
      
      <button 
        @click="submitComment" 
        :disabled="!canSubmit"
        class="submit-btn"
      >
        {{ submitting ? '提交中...' : '发表评论' }}
      </button>
    </div>
    
    <!-- 实时预览 -->
    <div v-if="comment" class="preview">
      <h4>预览:</h4>
      <div class="preview-content">{{ comment }}</div>
    </div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      comment: '',
      showEmoji: false,
      submitting: false,
      maxLength: 1000,
      emojis: ['😊', '😂', '🤔', '👍', '❤️', '🔥', '👏', '🎉']
    }
  },
  computed: {
    lengthRatio() {
      return this.comment.length / this.maxLength;
    },
    canSubmit() {
      return this.comment.trim().length > 0 && 
             this.comment.length <= this.maxLength &&
             !this.submitting;
    },
    placeholder() {
      const placeholders = [
        '说点什么吧...',
        '分享你的想法...', 
        '这里可以畅所欲言...',
        '友好的讨论是交流的第一步...'
      ];
      return placeholders[Math.floor(Math.random() * placeholders.length)];
    }
  },
  methods: {
    handleInput() {
      // 实时保存到本地存储
      localStorage.setItem('commentDraft', this.comment);
    },
    
    insertText(text) {
      const textarea = this.$refs.textarea;
      const start = textarea.selectionStart;
      const end = textarea.selectionEnd;
      
      this.comment = this.comment.substring(0, start) + 
                   text + 
                   this.comment.substring(end);
      
      // 移动光标到插入内容后
      this.$nextTick(() => {
        textarea.focus();
        textarea.setSelectionRange(start + text.length, start + text.length);
      });
    },
    
    handlePaste(event) {
      // 处理粘贴文本,比如清理格式等
      const pastedText = event.clipboardData.getData('text');
      event.preventDefault();
      
      // 这里可以添加文本处理逻辑
      document.execCommand('insertText', false, pastedText);
    },
    
    async submitComment() {
      this.submitting = true;
      
      try {
        // 模拟API调用
        await new Promise(resolve => setTimeout(resolve, 1000));
        
        console.log('提交评论:', this.comment);
        alert('评论发表成功!');
        
        // 清空内容
        this.comment = '';
        localStorage.removeItem('commentDraft');
        
      } catch (error) {
        console.error('提交失败:', error);
        alert('提交失败,请重试!');
      } finally {
        this.submitting = false;
      }
    }
  },
  mounted() {
    // 加载草稿
    const draft = localStorage.getItem('commentDraft');
    if (draft) {
      this.comment = draft;
    }
  }
}
</script>
<style scoped>
.smart-comment {
  border: 1px solid #e1e1e1;
  border-radius: 8px;
  padding: 16px;
  background: white;
}

.toolbar {
  margin-bottom: 10px;
}

.toolbar button {
  margin-right: 8px;
  padding: 4px 8px;
  border: 1px solid #ddd;
  background: white;
  border-radius: 4px;
  cursor: pointer;
}

.emoji-picker {
  margin-bottom: 10px;
  padding: 8px;
  border: 1px solid #e1e1e1;
  border-radius: 4px;
}

.emoji {
  cursor: pointer;
  margin-right: 4px;
  font-size: 1.2em;
}

.comment-textarea {
  width: 100%;
  min-height: 100px;
  padding: 12px;
  border: 1px solid #e1e1e1;
  border-radius: 4px;
  font-family: inherit;
  resize: vertical;
}

.footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 10px;
}

.normal { color: #666; }
.warning { color: orange; }
.danger { color: red; }

.submit-btn {
  padding: 8px 16px;
  background: #007cba;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.submit-btn:disabled {
  background: #ccc;
  cursor: not-allowed;
}

.preview {
  margin-top: 16px;
  padding: 12px;
  border: 1px dashed #e1e1e1;
  border-radius: 4px;
}

.preview-content {
  white-space: pre-wrap;
}
</style>
结语

看到这里,你是不是对textarea刮目相看了?这货根本不是什么简单的多行输入框,而是一个可以玩出各种花样的瑞士军刀!

从最基本的v-model绑定,到自动增高、字数统计、Markdown预览、@提及功能,再到完整的智能评论框——textarea的潜力远超你的想象。

记住,好的表单体验不是让用户去适应你的代码,而是让你的代码去适应用户的习惯。下次遇到多文本输入场景时,别再只会用input了,拿出你的textarea绝活,让用户体验直接起飞!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

值引力

持续创作,多谢支持!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值