最简 Radio
最简的自定义Radio,只是封装了label:
封装前:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="radio" name="lan" checked value="react" id="react">
<label for="react">react</label>
<input type="radio" name="lan" value="vue" id="vue">
<label for="vue">vue</label>
<script>
var $input =document.querySelector("input[name='lan'][checked]");
console.log($input.checked);
</script>
</script>
</body>
</html>
封装后:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DC-RADIO</title>
<script src="./dc-radio.js" defer></script>
</head>
<body>
<dc-radio name="lan" value="react" disabled>react</dc-radio>
<dc-radio name="lan" value="vue" checked>vue</dc-radio>
<dc-radio name="lan" value="ng">ng</dc-radio>
<button onclick="handleClick()">TEST</button>
<script>
function handleClick() {
const $radio = document.querySelector("dc-radio[name='lan'][checked]");
// $radio.checked = true;
console.log($radio.value);
}
</script>
</body>
</html>
Radio 实现:
/**
* dc-radio
*
* @description A radio button component
*
* 1. name属性相同的radio按钮,会被认为是一组,只能选择一个:添加事件监听change事件,其它Radio checked=false
* (input checked -> radio checked)
* (radio checked -> input checked)
*
*/
class DCRadio extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
</style>
<label for="radio">
<input type="radio" id="radio"/>
<slot></slot>
</label>
`
this.$input = this.shadowRoot.querySelector('input')
}
get value() {
return this.getAttribute('value') || this.textContent
}
get name() {
return this.getAttribute('name')
}
set name(value) {
this.setAttribute('name', value)
}
set disabled(value) {
this.toggleAttribute('disabled', value)
}
set checked(value) {
this.toggleAttribute('checked', value)
}
static get observedAttributes() {
return ['checked', 'disabled']
}
connectedCallback() {
this.$input.addEventListener('change', () => {
this.checked = true
this.dispatchEvent(new CustomEvent('change'))
})
}
attributeChangedCallback(name, oldValue, newValue) {
this.$input[name] = newValue !== null
if (name === 'checked' && newValue !== null) {
document.querySelectorAll(`dc-radio[name=${this.name}][checked]`).forEach(radio => {
if (radio !== this) {
radio.checked = false
}
})
}
}
}
customElements.define('dc-radio', DCRadio);
- 定义自定义元素:通过继承HTMLElement创建自定义元素,通过customElements.define()定义元素标签;
- 定制元素内容:通过shadowDom来承载元素内容,包括样式和结构;
- 读取属性:由于属性不是原生标签的属性,所以需要通过 getAttribute() 获取属性值;
- 监听属性变化:通过observedAttributes()监听属性变化,当属性改变调用attributeChangedCallback()方法;
- 子元素和父元素通信:子元素通过dispatchEvent()向父元素发送消息,父元素通过监听相应事件接收消息。
有样式的 Radio
class DCStyledRadio extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
:host {
display: inline-flex;
align-items: center;
gap: 8px;
}
input {
appearance: none;
margin: 0;
width: 16px;
height: 16px;
border-radius: 50%;
border: 1px solid #d9d9d9;
background-color: #fff;
}
input:checked {
border: 5px solid #1677ff;
}
input:disabled {
opacity: 0.6;
cursor: default;
}
input:not(:disabled):is(:hover) {
border-color: #1677ff;
cursor: pointer;
}
label {
cursor: pointer;
}
:disabled+label {
cursor: default;
}
</style>
<input type="radio" id="radio"/>
<label for="radio">
<slot></slot>
</label>
`
this.$input = this.shadowRoot.querySelector('input')
}
get value() {
return this.getAttribute('value') || this.textContent
}
get name() {
return this.getAttribute('name')
}
set name(value) {
this.setAttribute('name', value)
}
set disabled(value) {
this.toggleAttribute('disabled', value)
}
set checked(value) {
this.toggleAttribute('checked', value)
}
static get observedAttributes() {
return ['checked', 'disabled']
}
connectedCallback() {
this.$input.addEventListener('change', () => {
this.checked = true
this.dispatchEvent(new CustomEvent('change'))
})
}
attributeChangedCallback(name, oldValue, newValue) {
this.$input[name] = newValue !== null
if (name === 'checked' && newValue !== null) {
document.querySelectorAll(`dc-styled-radio[name=${this.name}][checked]`).forEach(radio => {
if (radio !== this) {
radio.checked = false
}
})
}
}
}
customElements.define('dc-styled-radio', DCStyledRadio);
Radio Group
嵌套在RadioGroup下的Radio是同一组,value属性标明当前选中的Radio。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DC-RADIO-GROUP</title>
<script src="./dc-radio.js" defer></script>
<script src="./dc-radio-group.js" defer></script>
</head>
<body>
<dc-radio-group name="lan" value="vue">
<dc-radio value="react">react</dc-radio>
<dc-radio value="vue">vue</dc-radio>
<dc-radio value="ng">ng</dc-radio>
</dc-radio-group>
<button onclick="handleClick()">TEST</button>
<script>
function handleClick() {
const $radioGroup = document.querySelector("dc-radio-group");
$radioGroup.addEventListener("change", (e) => {
console.log(1111, e);
});
// $radio.checked = true;
console.log($radioGroup.value);
}
</script>
</body>
</html>
RadioGroup 实现
/**
* 1. 根据 name 属性,设置 dc-radio 的 name 属性
* 2. 根据 value 属性,设置 dc-radio 的 checked 属性
*/
class DCRadioGroup extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<style>
</style>
<slot></slot>
`
}
get name() {
return this.getAttribute("name");
}
get value() {
return this.getAttribute("value")
}
set value(val) {
this.setAttribute("value", val);
}
static get observedAttributes() {
return ["value"];
}
connectedCallback() {
this.querySelectorAll('dc-radio').forEach(radio => {
radio.name = this.name
radio.addEventListener('change', () => {
this.value = radio.value
this.dispatchEvent(new CustomEvent('change', {
detail: {
value: radio.value
}
}))
})
})
}
attributeChangedCallback(name, oldValue, newValue) {
this.querySelectorAll('dc-radio').forEach(radio => {
radio.checked = radio.value === newValue
})
}
}
customElements.define("dc-radio-group", DCRadioGroup);
知识点
- 自定义属性:不能像自有属性(如onclick等)那样直接通过点语法(如element.data - custom)在大多数浏览器中访问。只能通过getAttribute方法来获取其值。
- 脚本执行顺序:加 defer。
- 伪元素: ::part() 参考 xy-ui。