Decl is a simple library designed to enable more declarative and unobtrusive JavaScript.
Decl should work on any modern browser and most older browsers (provided that they can be polyfilled to support MutationObserver
); however it is actively tested against the browsers below.
I've tried to select a diverse range of browsers and platforms for maximum coverage. If you feel another configuration should be included, feel free to open an issue.
Decl is designed to be intuitive and reminiscent of SCSS. To get an intuition for how Decl works, check out this Fiddle which shows a simple implementation for the accordion effect.
Scopes are the central idea in Decl. A scope is a combination of some element and rules to be matched to that element. By default, the global Decl
behaves like a scope for the root of the document (document.documentElement
).
Select rules can be created on a scope by calling select
with a matcher (usually a CSS selector string) and callback function. The select rule will match any child of the scope's element that matches the matcher, and the callback will be invoked with a new scope for any element that matches the select rule.
Decl.select('.kitten', function(scope, kitten) {
// This callback will run any time an element has the "kitten" class. `kitten` is the element that matched, and `scope` is a new scope for that element.
scope.select('.ears', function(scope, ears) {
// This callback will run anytime a child of the `kitten` element has the "ears" class. Here, the `ears` is the element that has the "ears" class (nested within the `kitten` element), and `scope` is new scope for that element.
});
});
When rules are like select rules except they are created by calling when
on a scope and applied to the element itself rather than the children.
Decl.select('.kitten', function(scope, kitten) {
scope.when('.happy', function(scope) {
// This callback will run anytime there is `kitten` element which simultaneously has the "kitten" and "happy" classes.
});
scope.when('.playful', function(scope) {
// Similarly, this callback will run anytime there is `kitten` element which simultaneously has the "kitten" and "playful" classes.
});
});
Match rules are created by calling match
and unmatch
on a scope with a callback. The callback will be invoked with the element that has just match or stopped matching respectively.
For performance reasons, the callback to select and when rules should only be used to add rules to the new scope it is passed. To tap into the lifecycle of an element matching a particular scope chain, match rules can be used.
var playfulKittenCount = 0;
Decl.select('.kitten.playful', function(scope) {
// This callback should avoid any computations and have no side effects (except calling methods on scope).
scope.match(function(playfulKitten) {
// The match callback will be invoked with the matching element exactly once when the element matches after all rules has been processed. Any modifications to the DOM must be done here.
playfulKittenCount++;
});
scope.unmatch(function(playfulKitten) {
// The unmatch callback will be invoked exactly once when an element which had previously matched stops doing so but after all rules have been processed. If the match callback was called for an element, the unmatch callback is guaranteed to be called (unless the page is unloaded entirely).
playfulKittenCount--;
});
});
Event rules allow you to define behavior for the occurrence of a DOM event on an element of a particular scope. They can be created by calling on
on a scope with an event matcher (usually a string with the event name) and a callback to be invoked when a matching event occurs. The callback will receive the matching event and a reference to the underlying element to which the listener was attached.
Decl.select('.kitten', function(scope) {
scope.on('click', function(event, kitten) {
// This callback is invoked when a click event (`event`) occurs on an element with the "kitten" class (`kitten`).
});
// For connivence, jQuery style on syntax with an element matcher is also supported.
scope.on('click', '.nose', function(event, nose) {
// The callback is invoked when a click event (`event`) occurs on an element with the "nose" class (`nose`) that is the child of an element with the "kitten" class.
// This is equivalent to writing:
// scope.select('.nose', function(scope) {
// scope.on('click', function(event, nose) {
// // (implementation)
// });
// });
});
});
The global Decl
object is a constructor for instances of the Decl
class. It delegates all but a few of its methods to a default instance. Additionally, this default instance delegates select
and on
to a root scope. This is what allows the global Decl
object to be used as the starting point for constructing new scopes.
Instances of Decl
must be tied to a document and create a root scope for the root element of that document (the documentElement
of that document). At initialization, the default instance is configured for a decl with the document in the global document
reference; however, additional decls for other documents may be created and set as the default instance.
getDefaultInstance
returns the instance of Decl
to which the Decl
class is currently delegating.
getDefaultInstance
sets the instance of Decl
to which the Decl
class is currently delegating.
getRootScope
returns the scope with no parent for the documentElement
of the document to which the decl is attached and to which the select
and on
methods are delegated.
inspect
prints the current state of the decl object to the console. This may be useful for debugging.
pristine
resets this decl object to its initial state, fully cleaning up all scopes it contains in the process.
deactivate
causes all rules to unmatch and returns the document to its original state.
activate
reverses the effects of deactivate
by re-applying any matching rules.
Tubolinks 5 introduces a caching mechanism that works by taking a snapshot of the page. Since this snapshot does not include the event handlers and jQuery data for elements on the page, it will often leave interactive components of the page in a bad state.
One solution to this problem is to deactivate Decl before the snapshot is taken. This will cause the unmatch callbacks for any rules to run and place the page back into its pristine state.
// Deactivate Decl before the snapshot is taken
document.addEventListener('turbolinks:before-cache', function() {
Decl.deactivate();
}, false);
// Reactivate Decl after the snapshot is taken
document.addEventListener('turbolinks:render', function() {
Decl.activate();
}, false);
This project is setup using a bunch of tools -- most of which I don't really understand. Fortunately, cloning this repo and running npm install
from the project root on a standard Node setup seems to be sufficent to get the toolchain up and working.
The source is written in TypeScript and located in the src
folder. The browser-ready JavaScript ends up in dist
and can be generated with gulp build
. The specs are in test
and can be verified with gulp test
.
Bug reports and pull requests are welcome on GitHub.
This library is available as open source under the terms of the MIT License.