*********
score.dom
*********
.. image:: https://travis-ci.org/score-framework/js.dom.svg?branch=master
:target: https://travis-ci.org/score-framework/js.dom
This module provides a convenient wrapper for DOM nodes. Its API resembles that
of jQuery, since this has become the best known DOM manipulation API over time.
Quickstart
==========
.. code-block:: html
Details
=======
Constructor
-----------
The "constructor" [1]_ will return an object deriving from ``score.dom.proto``.
It accepts either of the following:
- *Nothing*: Creates an empty node list.
.. code-block:: javascript
score.dom()
- Selector: Selects all nodes matching the selector globally
.. code-block:: javascript
score.dom('body')
- NodeList object:
.. code-block:: javascript
score.dom(document.getElementsByTagName('a'))
- A ``score.dom`` object:
.. code-block:: javascript
var bodyNode = score.dom('body');
score.dom(bodyNode).addClass('foo');
.. [1] The term *constructor* is actually incorrect, as ``score.dom`` is just a
normal function.
Creating new nodes
------------------
The constructor above is for wrapping already existing DOM nodes. It is also
possible to create score.dom objects with brand new nodes by using the
``create()`` function:
.. code-block:: javascript
var node = score.dom.create('div');
score.dom Object
----------------
Whenever you call ``score.dom()``, you will receive an array-like object
containing a list of nodes. This object actually inherits the Array prototype,
allowing you to use all array features:
.. code-block:: javascript
var nodes = score.dom('.spam');
nodes.length; // 3
nodes[0]; // A native HTMLDivElement:
...
There are a few exceptions: we are overriding the ``forEach()``, ``filter()``,
``map()`` and ``reduce()`` functions to provide score.dom objects in the
callback:
.. code-block:: javascript
nodes.forEach(function(node) {
// node is a score.dom object
console.log(node.attr('class'));
});
For convenience ``filter()`` also accepts a selector instead of a callback
function:
.. code-block:: javascript
nodes.filter(':not(.spam)');
Since score.dom objects are actually arrays, all operations are always
performed on *all* nodes in your array. This might come as a surprise in
certain cases, where jQuery is a bit inconsistent:
.. code-block:: javascript
// Remove all nodes, that hava a "spam" class, from the document:
score.dom('.spam').detach();
// Select all child nodes of all ".knight" nodes, i.e. the nodes
// ".foo" and ".bar" in the following document:
//
//
score.dom('.knight').children();
// Select all parent nodes of all ".cheese" nodes; the resulting object
// will contain the "#cheese-shop" *twice*:
//
score.dom('.customer').parent().length; // 2
score.dom('.customer').parent().uniq().length; // 1
Safety Measures
---------------
Apart from simplifying the complex DOM API, this module also tries to aid
development by throwing errors, whenever it assumes that something might have
gone wrong. Currently, we have two distinct checks for some of the operations
available. Note, that the precautions are tied to *operations*, not whole
*functions*: Some usage of the function might have a different set of
constraints than another.
Minimum Length 1
````````````````
Some operations intend to change a node. These operations are marked (`min1 <#minimum-length-1>`__)
and will fail if the score.dom object is empty:
.. code-block:: javascript
// create a new node:
var banana = score.dom.create('span').addClass('banana');
// select an existing node from the document
var fruits = score.dom('#fruits')
// What we're not realizing at this point is that our colleague has changed
// the ID of the "fruits"-node to "weapons", and that our fruits-variable
// is nothing but an empty array. This is why the next function will cause
// an Error to be thrown:
fruits.append(banana); // Error: "Empty list"
Single Node Operations
``````````````````````
Some operations are marked as Single Node Operations (`sno <#single-node-operations>`__ for short). These
operations will fail if the score.dom object contains more than one element:
.. code-block:: javascript
try {
var nodes = score.dom('.spam');
if (nodes.length > 1) {
// this will throw an error since we
// have more than one node:
nodes.text()
}
} catch (e) {
console.log(e); // "Attempting Single-Node-Operation on multiple nodes"
}
Assertion
`````````
Sometimes it can be handy to check if your query will return exactly one result.
Therefor ``assertOne()`` will throw errors on empty or multiple results in a query:
.. code-block:: javascript
try {
score.dom('#fish-tank').find('.fish').assertOne();
} catch (e) {
console.log(e); // "Multiple results found"
}
try {
score.dom('#elusive-fish').assertOne();
} catch (e) {
console.log(e); // "No result found"
}
// Since `assertOne()` doesn't actually do anything,
// it will simply pass the object:
var oneMoreMint = score.dom('#mint');
oneMoreMint === oneMoreMint.assertOne(); // true
Filtering
---------
If you have a ``score.dom`` object, you can reduce its list of nodes using the
following methods:
* ``eq(index)`` will return a new ``score.dom`` object containing a single
node, the one at the given index. Will throw an Error, if the index it out of
range.
* The dynamic value ``x.first`` returns the same as ``x.eq(0)``, ``x.last``
returns the same as ``x.eq(x.length - 1)``:
.. code-block:: javascript
score.dom('.knight').first; // The first knight
score.dom('.samurai').last; // The last samurai
score.dom('#cheese-shop').find('.cheese').first; // throws an Error
* The function ``uniq()`` will remove duplicates from your node list:
.. code-block:: javascript
score.dom('.customer').parent().uniq();
Cloning
-------
The represented Nodes can be duplicated using ``clone()``:
.. code-block:: javascript
var spams = score.dom('.spam');
spams.first.parent().append(spams.clone());
The function makes deep clones of all elements unless called with ``false``:
.. code-block:: javascript
var spams = score.dom('.spam');
var shallowCopies = spams.clone(false);
Querying
--------
You can query, if *all* nodes in your list match a given selector using
``matches()`` (`min1 <#minimum-length-1>`__):
.. code-block:: javascript
var spams = score.dom('.spam');
spams.matches('.spam');
Node Operations
---------------
There are two operations you can perform on individual nodes:
* ``text()`` will return the textContent_ of the node (`sno <#single-node-operations>`__)
or set the textContent of all nodes to a given value:
.. code-block:: javascript
// setting the text content:
score.dom('body').text('hello world');
// retrieving the text content (sno):
score.dom('body').text();
* ``attr()`` does the same for the value of an attribute:
.. code-block:: javascript
// setting an attribute:
score.dom('#parrot').attr('data-state', 'deceased');
// getting the value of an attribute (sno)
// (returns null if attribute does not exist):
score.dom('.customer').attr('data-state');
// removing an attribute:
score.dom('.customer').attr('data-state', null);
.. _textContent: https://developer.mozilla.org/en/docs/Web/API/Node/textContent
Restructuring
-------------
You can remove nodes from the document using ``detach()``, removing them from
the DOM. You can then attach them beneath another given node using
``prepend()`` or ``append()``, depending on whether they should be inserted at the
beginning, or the end of the children list. Both functions are `single node
operations`_ and will throw an error, if the score.dom object
they were called on does not contain exactly one node:
.. code-block:: javascript
score.dom('.parrot').detach();
score.dom('.fruits').append(score.dom.fromString('Banana'));
score.dom('.fruits').prepend(score.dom.fromString('Carrot'));
score.dom('.fruits').children().first.text() // 'Carrot'
Both functions also accept a second node that can serve as an anchor, if the
insert operation should be performed at a specific position in the children
list:
.. code-block:: javascript
// insert spam *after* eggs
score.dom('.meal').append(spam, eggs);
// insert spam *before* eggs
score.dom('.meal').prepend(spam, eggs);
Traversal
---------
The function ``parent()`` returns a new ``score.dom`` containing each node's
parent. ``children()`` returns a new ``score.dom`` containing all child nodes
of every node.
``find()`` will find all nodes beneath the original nodes matching given
selector.
``closest()`` queries the document upward until the given selector matches.
This is done for each node in the original list.
.. code-block:: javascript
// Assuming the following document:
//
//
//
//
//
//
//
//
//
var bottoms = score.dom('.bottom');
bottoms.length === 4;
var bottomParents = bottoms.parent();
bottomParents.length === 4;
bottomParents.hasClass('lvl2');
bottomParents[0] === bottomParents[1];
bottomParents[2] === bottomParents[3];
bottomParents[0] !== bottomParents[2];
var tops = bottomParents.closest('#top');
tops.length == 4;
tops[0] === tops[1];
tops[0] === tops[2];
tops[0] === tops[3];
var secondLevels = score.dom('#top').find('.lvl2');
secondLevels.length === 2;
CSS Class Manipulation
----------------------
The module allows adding/removing css classes using the usual method names:
.. code-block:: javascript
if (score.dom('body').hasClass('spam')) {
score.dom('.knight').addClass('ni');
score.dom('#cheese-shop').removeClass('cheese');
score.dom('.self-defense').toggleClass('fruit');
}
Note that ``hasClass()`` will only return ``true``, if *all* nodes have the
given css class.
Acknowledgments
===============
Many thanks to BrowserStack_ and `Travis CI`_ for providing automated tests for
our open source projects! We wouldn't be able to maintain our high quality
standards without them!
.. _BrowserStack: https://www.browserstack.com
.. _Travis CI: https://travis-ci.org/