如果你构建过Web引用程序,你可能处理过很多DOM操作。访问和操作DOM元素几乎是每一个Web应用程序的通用需求。我们我们经常从不同的控件收集信息,我们需要设置value值,修改div或span标签的内容。当然有许多库能帮助处理这些行为,其中最流行的当属jQuery,已经成为事实上的标准。有事你并不需要jQuery提供每一样东西,所以在这篇文章中,我们将看看如何创建自己的类库来操作DOM元素。

API

身为开发者的我们每天都要做决定。我相信在测试驱动开发中,我真的非常喜欢的一个事实是它迫使你在开始实际编码之前必须做出设计决定。沿着这些思路,我想我想要的DOM操作类库的API最终看起来可能像这样:

//返回 DOM 元素
dom('.selector').el
//返回元素的值/内容
dom('.selector').val()
//设置元素的值/内容
dom('.selector').val('value')

这应该包括了大多数可能用到的操作。然而如何我们可以一次操作多个对象会显得个更好。如果能生成一个JavaScript对象,那将是伟大之举。

//生成包装DOM元素的对象
dom({
structure: {
propA: '.selector',
propB: '.selector'
},
propC: '.selector'
})

一旦我们将元素存下来,我们能很容易对它们执行val方法。

//检索DOM元素的值
dom({
structure: {
propA: '.selector',
propB: '.selector'
},
propC: '.selector'
}).val()

这将是将数据直接从DOM转换为JavaScript对象的有效方法。

现在我们心理已经清楚我们的API看起来的样子,我们类库代码看起来像下面这样:

var dom = function(el) {
var api = { el: null }
api.val = function(value) {
// ...
}
return api;
}

作用域

很明显,我们打算使用类似getElementById,querySelector或querySelectorAll这样的方法。通常情况下,你可以像下面这样访问DOM:

var header = document.querySelector('.header');

querySeletor是非常有趣的,例如,它不仅仅是document对象的方法,同时也是其他DOM元素的方法。这意味着,我们可以在特定上下文中运行查询。比如:

<header>
<p>Big</p>
</header>
<footer>
<p>Small</p>
</footer> var header = document.querySelector('header');
var footer = document.querySelector('footer');
console.log(header.querySelector('p').textContent); // Big
console.log(footer.querySelector('p').textContent); // Small

我们能在特定的DOM树上操作,并且我们的类库应该支持传递作用域。所以,如果它接受一个父元素选择符是非常棒的。

var dom = function(el, parent) {
var api = { el: null }
api.val = function(value) {
// ...
}
return api;
}

查询DOM元素

按照我们上面所说的,我们将使用querySelector和querySelectorAll查询DOM元素。让我们为这些函数创建两个快捷方式。

var qs = function(selector, parent) {
parent = parent || document;
return parent.querySelector(selector);
};
var qsa = function(selector, parent) {
parent = parent || document;
return parent.querySelectorAll(selector);
};

在那之后我们应该传递el参数。通常情况下将是一个(选择符)字符串,但我们也应该支持:

  • DOM元素——类库的val方法会非常方便,所以我们可能需要使用已经引用的元素;
  • JavaScript对象——为了创建包含多个DOM元素的JavaScript对象。

下面的switch包括这两种情况:

switch(typeof el) {
case 'string':
parent = parent && typeof parent === 'string' ? qs(parent) : parent;
api.el = qs(el, parent);
break;
case 'object':
if(typeof el.nodeName != 'undefined') {
api.el = el;
} else {
var loop = function(value, obj) {
obj = obj || this;
for(var prop in obj) {
if(typeof obj[prop].el != 'undefined') {
obj[prop] = obj[prop].val(value);
} else if(typeof obj[prop] == 'object') {
obj[prop] = loop(value, obj[prop]);
}
}
delete obj.val;
return obj;
}
var res = { val: loop };
for(var key in el) {
res[key] = dom.apply(this, [el[key], parent]);
}
return res;
}
break;
}
如果开发者传递字符串将执行第一个case。我们转换parent并且调用querySelector的快捷方式。第二个case将会被执行如果我们传递一个DOM元素或JavaScript对象。我们检查对象是否有nodeName属性,如果有这个属性,我们直接将它的值作为api.el的值。如果没有,那么我们遍历对象的所有属性并且为每个属性初始化为类库实例。这里有一些测试用例:
<p>text</p>
<header>
<p>Big</p>
</header>
<footer>
<p>Small</p>
</footer>

访问第一个段落:

dom('p').el

访问header节点里的段落:

dom('p', 'header').el

传递一个DOM元素:

dom(document.querySelector('header')).el

传递一个JavaScript对象:

var els = dom({
footer: 'footer',
paragraphs: {
header: 'header p',
footer: 'footer p'
}
}))
// 最后我们在此得到JavaScript对象。
// 它的属性是实际的结果
// 执行dom函数。例如,获取值
// footer是paragraphs的属性
els.paragraphs.footer.el

获取或设置元素的值

表单元素的值如input或select可以被很容易的检索到——我们可以使用元素的value属性。我们我们已经有一个能访问的DOM元素了——存储在api.el。然而,当我们碰到单选框或复选框是有些棘手。对于其他HTML节点像div,section或span我们获取元素的值实际上是获取textContent属性。如果textContent是undefined那么可以用innerHTML代替(相似)。让我们写出另一个switch语句:
api.val = function(value) {
if(!this.el) return null;
var set = !!value;
var useValueProperty = function(value) {
if(set) { this.el.value = value; return api; }
else { return this.el.value; }
}
switch(this.el.nodeName.toLowerCase()) {
case 'input':
break;
case 'textarea':
break;
case 'select':
break;
default:
}
return set ? api : null;
}

首先我们需要确保api.el属性存在。set是布尔类型变量告诉我们是获取还是设置元素的value属性。有.value属性的元素包括一个辅助方法。switch语句将包含方法的实际逻辑。最后我们返回api本身,为了保持链式操作。当然我们这样做仅当我们使用设置器函数时。

让我们看看如何处理不能同类型的元素。例如input节点:
case 'input':
var type = this.el.getAttribute('type');
if(type == 'radio' || type == 'checkbox') {
var els = qsa('[name="' + this.el.getAttribute('name') + '"]', parent);
var values = [];
for(var i=0; i<els.length; i++) {
if(set && els[i].checked && els[i].value !== value) {
els[i].removeAttribute('checked');
} else if(set && els[i].value === value) {
els[i].setAttribute('checked', 'checked');
els[i].checked = 'checked';
} else if(els[i].checked) {
values.push(els[i].value);
}
}
if(!set) { return type == 'radio' ? values[0] : values; }
} else {
return useValueProperty.apply(this, [value]);
}
break;
这可能是最有趣的例子了。有两种类型的元素需要不同的处理——单选框和复选框。这些元素实际上是一组,我们要牢记这点。这就是为什么我们使用querySelectorAll获取整组并找出哪个是被选择/选中的。更复杂的是,复选框可能不止被选中一个。上面的方法完美处理所有这些情况。
处理textarea元素非常简单,这要得益于我们上面写的辅助函数。
case 'textarea':
return useValueProperty.apply(this, [value]);
break;

下面看我们如何处理下拉列表(select):

case 'select':
if(set) {
var options = qsa('option', this.el);
for(var i=0; i<options.length; i++) {
if(options[i].getAttribute('value') === value) {
this.el.selectedIndex = i;
} else {
options[i].removeAttribute('selected');
}
}
} else {
return this.el.value;
}
break;

最后是默认操作:

default:
if(set) {
this.el.innerHTML = value;
} else {
if(typeof this.el.textContent != 'undefined') {
return this.el.textContent;
} else if(typeof this.el.innerText != 'undefined') {
return typeof this.el.innerText;
} else {
return this.el.innerHTML;
}
}
break;

上面这些代码我们完成了我们的val方法。这里有一个简单的HTML表单和相应的测试:

<form>
<input type="text" value="sample text" />
<input type="radio" name="options" value="A">
<input type="radio" name="options" checked value="B">
<select>
<option value="10"></option>
<option value="20"></option>
<option value="30" selected></option>
</select>
<footer>version: 0.3</footer>
</form>

如果我们写下面的:

dom({
name: '[type="text"]',
data: {
options: '[type="radio"]',
count: 'select'
},
version: 'footer'
}, 'form').val();

我们会得到:

{
data: {
count: "30",
options: "B"
},
name: "sample text",
version: "version: 0.3"
}

这方法对于把数据冲HTML导成JavaScript对象非常有帮助。这正是我们很多人每天都很常见的任务。

最后结果

最后完成的类库代码仅有100行代码,但它仍然满足我们所需的访问 DOM元素并且获取和设置value值/内容。

var dom = function(el, parent) {
var api = { el: null }
var qs = function(selector, parent) {
parent = parent || document;
return parent.querySelector(selector);
};
var qsa = function(selector, parent) {
parent = parent || document;
return parent.querySelectorAll(selector);
};
switch(typeof el) {
case 'string':
parent = parent && typeof parent === 'string' ? qs(parent) : parent;
api.el = qs(el, parent);
break;
case 'object':
if(typeof el.nodeName != 'undefined') {
api.el = el;
} else {
var loop = function(value, obj) {
obj = obj || this;
for(var prop in obj) {
if(typeof obj[prop].el != 'undefined') {
obj[prop] = obj[prop].val(value);
} else if(typeof obj[prop] == 'object') {
obj[prop] = loop(value, obj[prop]);
}
}
delete obj.val;
return obj;
}
var res = { val: loop };
for(var key in el) {
res[key] = dom.apply(this, [el[key], parent]);
}
return res;
}
break;
}
api.val = function(value) {
if(!this.el) return null;
var set = !!value;
var useValueProperty = function(value) {
if(set) { this.el.value = value; return api; }
else { return this.el.value; }
}
switch(this.el.nodeName.toLowerCase()) {
case 'input':
var type = this.el.getAttribute('type');
if(type == 'radio' || type == 'checkbox') {
var els = qsa('[name="' + this.el.getAttribute('name') + '"]', parent);
var values = [];
for(var i=0; i<els.length; i++) {
if(set && els[i].checked && els[i].value !== value) {
els[i].removeAttribute('checked');
} else if(set && els[i].value === value) {
els[i].setAttribute('checked', 'checked');
els[i].checked = 'checked';
} else if(els[i].checked) {
values.push(els[i].value);
}
}
if(!set) { return type == 'radio' ? values[0] : values; }
} else {
return useValueProperty.apply(this, [value]);
}
break;
case 'textarea':
return useValueProperty.apply(this, [value]);
break;
case 'select':
if(set) {
var options = qsa('option', this.el);
for(var i=0; i<options.length; i++) {
if(options[i].getAttribute('value') === value) {
this.el.selectedIndex = i;
} else {
options[i].removeAttribute('selected');
}
}
} else {
return this.el.value;
}
break;
default:
if(set) {
this.el.innerHTML = value;
} else {
if(typeof this.el.textContent != 'undefined') {
return this.el.textContent;
} else if(typeof this.el.innerText != 'undefined') {
return typeof this.el.innerText;
} else {
return this.el.innerHTML;
}
}
break;
}
return set ? api : null;
}
return api;
}

我创建了一个jsbin的例子,你可以看看类作品。

总结

我上面讨论的类库是AbsurdJS客户端组件的一部分。该模块的完成文档可以在这里找到。这代码的目的并非要取代jQuery或其他可以访问DOM的流行类库。函数的思想是自成一体,一个函数只做一件事并把它做好。这是AbsurdJS背后的主要思想,它也是基于模块化建设的,如routerAjax模块。

原文http://flippinawesome.org/2014/03/10/a-dom-manipulation-class-in-100-lines-of-javascript/

Q群推荐

JavaScript 家园 159973528,JavaScript开发者的天堂,欢迎有兴趣的同学加入

GitHub家园225932282,GitHub爱好者的天堂,欢迎有兴趣的同学加入

码农之家203145707,码农的天堂,欢迎有兴趣的同学加入

仅100行的JavaScript DOM操作类库的更多相关文章

  1. javascript DOM 操作基础知识小结

    经常用到javascript对dom,喜欢这方便的朋友也很多,要想更好的对dom进行操作,这些基础一定要知道的.   DOM添加元素,使用节点属性  <!DOCTYPE html PUBLIC ...

  2. javascript DOM 操作

    在javascript中,经常会需要操作DOM操作,在此记录一下学习到DOM操作的知识. 一.JavaScript DOM 操作 1.1.DOM概念 DOM :Document Object Mode ...

  3. javascript DOM 操作 attribute 和 property 的区别

    javascript DOM 操作 attribute 和 property 的区别 在做 URLRedirector 扩展时,注意到在使用 jquery 操作 checkbox 是否勾选时,用 at ...

  4. javascript DOM操作之 querySelector,querySelectorAll

    javascript DOM操作之 querySelector,querySelectorAll

  5. javascript DOM操作HTML文档

    文档对象模型(DOM)是W3C为解决浏览器混战时代不同浏览器环境之间的差别而制定的模型标准.W3C将文档对象模型定义为:是一个能让程序和脚本动态 访问和更新文档内容.结构和样式的语言平台.提供了标准的 ...

  6. SVG基础以及使用Javascript DOM操作SVG

    SVG 不依赖分辨率 支持事件处理器 最适合带有大型渲染区域的应用程序(比如谷歌地图) 复杂度高会减慢渲染速度(任何过度使用 DOM 的应用都不快) 不适合游戏应用 Canvas 依赖分辨率 不支持事 ...

  7. Javascript DOM操作实例

          最近在学DOM,但是还是没有办法很好的记住API,想找些例子来练习,网上的例子将一个个DOM对象方法挨个举例,并没有集合在一起用,效果不尽人意.所以自己写一份实例,顺便巩固下学到的知识. ...

  8. JavaScript——DOM操作——Window.document对象

    一.找到元素: docunment.getElementById("id"):根据id找,最多找一个:    var a =docunment.getElementById(&qu ...

  9. javascript DOM操作 第19节

    <html> <head> <title>DOM对象</title> <script type="text/javascript&quo ...

随机推荐

  1. ECMAScript——(二)

    1.语法 区分大小写 变量是弱类型(定义变量时只用 var 运算符,可以将它初始化为任意值.) 每行结尾的分号可有可无(建议写上) 注释与Java一样 括号表示代码块 2.变量 变量可以不用声明,变量 ...

  2. bind系统调用

    /* * Bind a name to a socket. Nothing much to do here since it's * the protocol's responsibility to ...

  3. linux音频alsa-uda134x驱动文档阅读之一转自http://blog.csdn.net/wantianpei/article/details/7817293

    前言 目前,linux系统常用的音频驱动有两种形式:alsa oss alsa:现在是linux下音频驱动的主要形式,与简单的oss兼容.oss:过去的形式而我们板子上的uda1341用的就是alsa ...

  4. MVVM模式的 数据绑定

    数据绑定要达到的效果 数据绑定要达到什么效果呢,就是在界面中绑定了数据源之后,数据在界面上的修改能反映到绑定源,同时绑定源的修改也能反映到界面上.从界面反映到绑定的数据源是很容易理解的,因为在绑定过程 ...

  5. Python操作Excle

    python操作excel主要用到xlrd和xlwt这两个库,即xlrd是读excel,xlwt是写excel的库.可从这里下载https://pypi.python.org/pypi.下面分别记录p ...

  6. Flask:初次使用Flask-SQLAlchemy读取SQLite3

    Windows 10家庭中文版,Python 3.6.4,Flask 1.0.2,Eclipse Oxygen.1a Release (4.7.1a),PyDev 6.3.2 SQLAlchemy是一 ...

  7. 洛谷P1491集合位置

    传送门啦 这个题说白了就是求一个次短路. 方法是我们先跑一遍最短路,记录下最短路上每一个点的前驱.然后我们将最短路上每一条边都标记一次,分别跑一边最短路,求出最短路径即可. 在这我们不用特殊判断是否是 ...

  8. ROSCon 2017通知 Announcing ROSCon 2017: September 21st and 22nd in Vancouver

    ROSCon 2017通知:9月21日和22日在温哥华 我们很高兴地宣布,2017年ROSCon将在举行9月21-22日,2017年温哥华会议中心在加拿大温哥华.2017年IROS将在同一地点9月24 ...

  9. InstallShield在MySQL和Oracle中执行SQL脚本的方法InstallShield在MySQL和Oracle中执行SQL脚本的方法

    简述 InstallShield已经内建了对MySQL和Oracle的支持.但是这个功能是通过ODBC实现的,它对SQL脚本的格式要求非常严格,因此已经通过官方客户端测试的脚本在IS中执行时往往就会报 ...

  10. 如何解决谷歌Chrome浏览器空白页的问题

    如何解决谷歌Chrome浏览器空白页的问题   谷歌Chrome浏览器突然不打开任何网页,无论是任何站点(如http://www.baidu.com), 还是Chrome浏览器的设置页面(chrome ...