昨天我們已經讓元件能夠與表單產生關聯:
static formAssociated = true
告訴瀏覽器這是一個表單元件。this._internals = this.attachInternals()
建立 internals 物件來跟表單互動。this._internals.setFormValue()
把值寫入表單,並且可以用 formData.get() 拿到內容。到這裡,元件已經能參與表單的提交流程了 (*´∀)~♥ !
但我們如果仔細想想,原生的 <input>
並不是這麼簡單,一個小小的欄位就包含了許多的方法、屬性。
看看我們日常使用的 <input>
,這個元件不僅僅只是一個文字框,還有很多的方法與功能:
required
、最小值 min
...。但是!自訂元件沒有這些內建功能,所以我們要自己加入。
接下來我們就透過實作一個 輸入框
,一步一步學習建立自訂表單元件吧!
今天我們也把之前學過的概念也帶入:
this._internals.setFormValue()
,讓瀏覽器知道值已經改變。value='值'
在一開始就給予預設值。_defaultValue
。placeholder='值'
在一開始就給予 placeholder。_placeholder
。值
加入 FormData。如果沒有寫 name 的話,就不會被加入。其實在上一篇文章已經有先偷跑了,我們呼叫了 this._internals.setFormValue()
給予元件值。現在我們要加入預設值以及 getter、setter,讓外部也可以取得欄位的值。
先回憶一下前面 attribute
的那篇文章,在裡面,我們有提到一個方法 getAttribute('屬性')
,在當時有提到,使用這個屬性是一次性的,無法監聽屬性的變化。
但是當要設定 預設值
或是 placeholder
的時候,這個方法就派上用場了!
我們直接看程式碼吧!
input.js
class CustomInput extends HTMLElement {
// 宣告與表單產生關聯
static formAssociated = true;
// 監聽的屬性
static get observedAttributes() {
return ['value', 'disabled'];
}
constructor() {
super();
this._internals = this.attachInternals();
const shadowRoot = this.attachShadow({ mode: 'open' });
const cloneNode = this.render().cloneNode(true);
shadowRoot.appendChild(cloneNode);
this._input = this.shadowRoot.querySelector('.custom-input');
}
connectedCallback() {
// 使用 getAttribute 取得預設值
this._defaultValue = this.getAttribute('value');
this._placeholder = this.getAttribute('placeholder') || '請輸入文字...';
// 初始化 input value
this.initInputValue();
// 加入 input 事件
this.handleValueChange();
}
// 提供外部可取值/設值
get value() {
return this._value;
}
set value(value) {
this._value = value.trim();
this._input.textContent = this._value;
this._internals.setFormValue(this._value);
}
render() {
const template = document.createElement('template');
template.innerHTML = `
<style>
div {
border: 1px solid #999999;
border-radius: 4px;
padding: 2px 4px;
}
</style>
<!-- 使用 contenteditable 來做一個假的 input -->
<div class="custom-input" contenteditable="true"></div>
`
return template.content;
}
// 監聽 value, disabled 屬性變更
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) {
return;
}
if (name === 'value') {
this.value = newValue; // 透過 setter 同步
}
if (name === 'disabled') {
this._input.setAttribute('contenteditable', newValue === null ? 'true' : 'false');
}
}
// 初始化欄位的值
initInputValue() {
if (this._defaultValue) {
this._value = this._defaultValue;
this._input.innerText = this._value;
this._internals.setFormValue(this._value); // 記得呼叫,不然無法寫入 formData
} else {
this._input.innerText = this._placeholder;
this._input.classList.add('placeholder');
}
}
handleValueChange() {
// 因為我們有加入 `attachInternals` 所以可以呼叫表單元件的方法
this._input.addEventListener('input', e => {
// 利用表單方法 `setFormValue` 寫入欄位的值,將值同步到 form internals
const value = e.target.innerText.trim() || '';
this._value = value;
this._internals.setFormValue(value);
// 發送事件給外部
this.dispatchEvent(new CustomEvent('value-changed', {
detail: { value: this._value },
bubbles: true,
composed: true
}));
});
// 當 focus 時,清空 placeholder
this._input.addEventListener('focus', () => {
if (this._input.classList.contains('placeholder')) {
this._input.innerText = '';
this._input.classList.remove('placeholder');
}
});
// 當 blur 時,如果沒有輸入,回填 placeholder
this._input.addEventListener('blur', () => {
if (!this._value) {
this._input.innerText = this._placeholder;
this._input.classList.add('placeholder');
}
});
}
}
customElements.define('custom-input', CustomInput);
index.html
<body>
<form>
<custom-input name="content" value="defaultValue"></custom-input>
<button type="submit">Submit</button>
</form>
<script src="input.js"></script>
<script>
const form = document.querySelector('form');
const customInput = document.querySelector('custom-input');
customInput.addEventListener('value-changed', e => {
console.log('事件取得的值:', e.detail.value);
});
form.addEventListener('submit', e => {
e.preventDefault();
const formData = new FormData(form);
const valueFromFormData = formData.get('content');
const valueFromGetter = customInput.value;
console.log(valueFromGetter, valueFromFormData);
})
</script>
</body>
完整程式碼請看這:https://codepen.io/unlinun/pen/MYKazeK