全手打原創,轉載請標明出處:https://www.cnblogs.com/dreamsqin/p/11466197.html
先看最終實現的demo效果圖:
(1)上面看似文本域的大框是通過給div添加contenteditable=true屬性實現的Vue組件DivEditable.vue;
(2)下面的輸入框是父組件中與DivEditable綁定相同變量的輸入框,用於展示數據的雙向綁定效果;
(3)按鈕實現綁定變量的賦值操作;
(4)DivEditable的blur事件可觸發文本過濾或樣式的變更等操作(專門留的組件接口);
可以看到,DivEditable中值的改變會影響輸入框中的值,同樣的,輸入框中值改變也會影響DivEditable中的值,通過按鈕給綁定變量賦值同時觸發了輸入框及DivEditable中值的改變。
1、contenteditable屬性
用於設置或返回元素的內容是否可編輯。:
疑問:這時你可以能會想,這么麻煩,怎么不直接使用可編輯元素?比如我們最常見的有input、textarea。
解答:但如果你想要在輸入的內容中加入html代碼,並且還要正常渲染,就要與v-html結合使用,所以我們只能采用不可編輯元素並為其添加contenteditable為true的屬性。
2、怎么實現DivEditable數據的雙向綁定
犯傻1:一開始我天真的以為v-html與v-model一樣,變量賦值后自帶雙向綁定,=.=事實證明還是太嫩;
犯傻2:於是我想那我再加一個v-model不就完事兒了,結果證明還是太嫩,瀏覽器直接報錯'v-model' directives aren't supported on <div> elements;
最終只能自己上了:
(1)首先可以通過@input事件監聽到輸入值的變化,此時就可以獲取到變化后的值並將其傳遞給父組件;
(2)雖然div不能添加v-model,但是在父組件中我調用DivEditable時卻可以為其添加v-model;
(3)v-model中傳入的值可以在子組件prop中獲取的到;
(4)這時你再監聽獲取到的prop值,並將該值賦值給子組件中的v-html參數,雙向綁定就搞定啦。
這里引入Vue官方描述:
自定義組件的v-model:一個組件上的 v-model 默認會利用名為 value 的 prop 和名為 input 的事件,v-model的值將會傳入子組件中的prop。
DivEditable.vue組件源碼(可以結合我上面的步驟描述看會更容易理解):
<!-- Created by dreamsqin on 2019/9/5 --> <template> <div class="div-editable" contenteditable="true" v-html="innerText" @input="changeText" @focus="isChange = false" @blur="blurFunc"></div> </template> <script> export default { name: 'DivEditable', props: { value: { type: String, default: '' } }, data() { return { innerText: this.value, isChange: true } }, watch: { value() { if (this.isChange) { this.innerText = this.value } } }, methods: { changeText() { this.$emit('input', this.$el.innerHTML) }, blurFunc() { this.isChange = true this.$emit('blurFunc') } } } </script> <style lang="scss"> .div-editable{ width: 100%; height: 100%; overflow-y: auto; word-break: break-all; outline: none; user-select: text; white-space: pre-wrap; text-align: left; &[contenteditable=true]{ user-modify: read-write-plaintext-only; &:empty:before { content: attr(placeholder); display: block; color: #ccc; } } } </style>
重點說明一下isChange參數的作用:
你可以先嘗試拿掉它以及相關邏輯,看看最終會出現什么效果(輸入一個字母光標就跑到前面去了,並且輸入不了中文);
分析一下原因:
(1)通過打斷點可以看到,當你輸入的時候觸發input事件,提交值給父組件中的v-model;
(2)但因為在子組件中又監聽了v-model的值,所以整體形成了閉環;
(3)還需要重點說明的是光標問題,contenteditable與v-html所在的元素值的改變如果不是通過輸入而是通過賦值實現,光標就會跑到最前面;
所以以輸入中文為例,你剛打了一個字母,立馬就觸發了監聽與變動,光標移到最前面,自然無法完成整個正常的輸入。
解決辦法:
只有當blur的時候再做賦值操作(isChange為true),focus狀態下不做賦值(isChange為false);
至於初始為true的原因是在父組件中直接給綁定的變量賦值時子組件中還是需要觸發賦值的(isChange為true);
除此之外,我還為組件提供了一個blur事件的函數接口,你可以做一些數據的過濾或者樣式的變更,例如我demo中要高亮標簽。
3、父組件調用
直接上源碼:
<template> <div class="test-page"> <div class="contain"> <div-editable v-model="testContent" @blurFunc="blurHighLight"></div-editable> <el-input class="input-style" v-model="testContent"></el-input> <el-button class="button-style" @click="changeText">改變值</el-button> </div> </div> </template> <script> import DivEditable from '@/components/DivEditable' export default { name: 'TestPage', data() { return { testContent: 'dreamsqin' } }, components: { DivEditable }, methods: { blurHighLight() { // 這里做數據過濾或樣式變更操作 }, changeText() { this.testContent = '【標簽1】dreamsqin' this.blurHighLight() } } } </script> <style lang="scss"> .test-page{ height: 100%; display: flex; align-items: center; justify-content: center; .contain{ width: 600px; height: 250px; border: 2px solid #000; .input-style,.button-style{ margin-top: 10px; } .text-blue{ color: #2080F7; } } } </style>