Creating Plugins for SnackJS
My SnackJS: A Tiny-But-Tasty JavaScript Framework post exposed you to Ryan Florence's new JavaScript framework called SnackJS. SnackJS' motto is "Because sometimes, all you need is a snack" , and for good reason: this 3KB micro-framework provides the functionality you need for most websites without the huge overhead of jQuery or other frameworks. As SnackJS is in its infancy and its aim is to be tiny, the framework does not include some functionalities found in other frameworks; in short, SnackJS assumes you know how to accomplish some tasks (like modifying element styles, etc.) using basic JavaScript.
There's no denying that even if you know JavaScript, shortcut methods are hugely helpful -- if they weren't, smart JavaScript developers wouldn't spend time creating JavaScript frameworks. One shortcut functionality that SnackJS doesn't have is DOM node creation...and by extension, node attribute modification and placements. Since I believe in the SnackJS project and the functionality mentioned is ideal for my projects, I've created create
, attr
, and place
plugins which I'll share with the community.
snack.attr and snack.wrap.attr
Modifying node attributes was the most difficult of the three tasks because I couldn't simply use the node's setAttribute
method. The most obvious of those is style
because that node property is a CSSStyleDeclaration object containing the complete list of node styles. For that purpose, as well as the purposes of innerHTML
, I've created a few special definitions to accommodate for setting those properties as well:
// The "attr" plugin !function(snack){ // Will hold special attributes, privately var props = {}; // The main method snack.attr = function(node,attr,value) { // Does the actual setting var doSet = function(val,key) { props[key] && props[key].set ? props[key].set(node,val) : node.setAttribute(key,val); }; // Setter var isObj = typeof attr == "object"; if(value != undefined || isObj) { isObj ? snack.each(attr,doSet) : doSet(value,attr); } else { // Getter return props[attr] ? props[attr].get(node) : node.getAttribute(attr); } // Return the node return node; }; // Creates a method by which one can define special node attributes snack.attr.define = function(name, obj){ if (typeof name === 'string'){ props[name] = obj; return; } // takes an object of key:values for (i in name) { if (name.hasOwnProperty(i)) { snack.attr.define(i, name[i]); } } }; // Define the special attributes now snack.attr.define({ html: { set: function(node,value) { node.innerHTML = value; }, get: function(node) { return node.innerHTML; } }, style: { set: function(node,value) { node.setAttribute("style",value); }, get: function(node) { return node.getAttribute("style"); } } }); // Extend to the "wrap" method snack.wrap.define('attr', function(attr, value){ this.each(function(node){ snack.attr(node, attr, value); }); }); }(snack);
The attr
property is used for both getting and setting attributes. Providing three arguments always acts as a setter, providing a key/value object as the second argument is a setter, otherwise it acts as a getter. Here are sample uses of attr:
// Retrieve the title attribute of a node var title = snack.attr(node,"title"); // Then retrieve the node's innerHTML var html = snack.attr(node,"html"); // Set a node's "style" attribute snack.attr(node,"style","background-color:green;color:#fff;padding:20px;"); // Set multiple attributes at once snack.arr(node,{ tabindex: 1, value: "davidwalsh", placeholder: "username" });
The attr
method even allows you to create custom setters and getters:
// Define the special attributes now snack.attr.define({ "class": { set: function(node,value) { node.className = value; }, get: function(node) { return node.className; } } });
I plan of reworking attr
just a bit in the future, but what's presented is the current state.
snack.place and snack.wrap.place
The place
method inserts nodes in specific locations within the DOM. In evaluating how many of the larger JavaScript frameworks place nodes, I found Dojo's to be the most concise for the number of positions it allows you to inject nodes. My method is largely based on the Dojo Tookit's:
// Fun this function immediately after creation !function(snack) { // Places a node at a given position snack.place = function(node,domReference,position) { // Create functions for insertion var before = function(node,domReference) { var parent = domReference.parentNode; if(parent){ parent.insertBefore(node, domReference); } }; var after = function(node,domReference) { // summary: // Try to insert node after ref var parent = domReference.parentNode; if(parent){ if(parent.lastChild == domReference){ parent.appendChild(node); }else{ parent.insertBefore(node, domReference.nextSibling); } } }; if(typeof position == "number"){ // inline'd type check var cn = domReference.childNodes; if(!cn.length || cn.length <= position){ domReference.appendChild(node); }else{ before(node, cn[position < 0 ? 0 : position]); } }else{ switch(position){ case "before": before(node, domReference); break; case "after": after(node, domReference); break; case "replace": domReference.parentNode.replaceChild(node, domReference); break; case "first": if(domReference.firstChild){ before(node, domReference.firstChild); break; } // else fallthrough... default: // aka: last domReference.appendChild(node); } } return node; }; // Extend to the "wrap" method snack.wrap.define("place", function(domRef,pos){ this.each(function(node){ snack.place(node,domRef,pos); }); }); }(snack);
Armed with the place
method, you can place nodes in several positions:
// Place the node into the BODY snack.place(node,document.body); // Place the node above the another node snack.place(node,otherNode,"before"); // Replace one node with another snack.place(node,otherNode,"replace");
You can also use snack.wrap.place
to move multiple nodes at a time:
// Create a UL var list = snack.create("ul"); // Place the element above the node snack.place(list,node,"before"); var arr = []; for(x = 0; x <= 4; x++) { arr.push(snack.create("li",{ html: "List item " + (x + 1) })); } snack.wrap(arr).place(list);
Like I said, I've borrowed quite heavily from Dojo. My reason for doing so is that Dojo's been proven for years and offers the most flexibility. Hooray for not reinventing the wheel!
snack.create
The create
method was the easiest of the three, even employing attr
and place
when available. Simply provide the tag, optional properties, and optional placement:
!function(snack) { // If not already created... snack.create = function(nodeType,props,nodeRef,where) { // Create the node var node = document.createElement(nodeType); // Add properties if(props && snack.attr) { // Set properties snack.attr(node,props); } // Inject into parent if(nodeRef && snack.place) { snack.place(node,nodeRef,where); } // Return the node return node; }; }(snack);
snack.create
would be used as follows:
// Create a UL var list = snack.create("ul"); // Add an LI to the list snack.create("li",{ html: "List item " + (x + 1) },list);
If attr
and place
plugins aren't loaded, snack.create
simply acts as a document.createElement
wrapper.
Creating SnackJS Plugins
Creating Snack.js plugins is extremely simple. Simply add your method to the snack
object or use SnackJS' define
method:
!function(snack) { snack.pluginName = function(arg1,arg2/*, etc.*/) { }; }(snack);
Voila -- your custom SnackJS plugin is now available. And if you want your plugin to work with snack.wrap
, that's as easy as a snack.each
loop:
!function(snack) { snack.pluginName = function(arg1,arg2/*, etc.*/) { }; // Extend to the "wrap" method snack.wrap.define("pluginName", function(arg1,arg2/*, etc.*/){ this.each(function(arg){ snack.pluginName(arg,arg1,arg2/*, etc.*/); }); }); }(snack);
Adding wrap
support is useful in many cases but there are always exceptions. For example, adding snack.wrap.create
doesn't make sense.
Remember that one of SnackJS' goals is to be small and concise, so write your plugins that way.
Contributing SnackJS Plugins
SnackJS is a new, growing framework so your contributions are always welcome. While they may not make the SnackJS core, there's no downside to creating simple but useful JavaScript plugins for SnackJS. The steps to creating SnackJS plugins are much like any other framework:
- Research your plugin; look at how other JS frameworks accomplish the same goal and decide which you think is best
- Code your plugin
- Test. Test. Test.
- Fork the official SnackJS repo and commit the code. Feel free to send a pull request to Ryan.
Please, please consider contributing to SnackJS. It's a project with great intentions, coded beautifully by Ryan, which could vastly improve the speed of the web. It seems that jQuery is the default JavaScript frameworks for most websites at this point; it shouldn't be. Don't make more of a meal about your JavaScript framework when all you need is a snack.
Nice to read about another JS library.
Could you eplain why you’ve put ! before function? Just for fun?
Great question Bartek. Adding “!” before the function declaration instructs the parser to execute the function immediately. It’s very much like:
Using “!” saves characters.
!
forces a function expression which can then be immediately called.function (){ ... }()
is a syntax error whereas:!function (){ ... }()
is not! in javascript is similar to !important in css :-)
Hi David,
why didn’t you send the code as pull request to Snack repository?
It would be helpful to manage and further update the code.
Maybe you could at least post the code in one piece? Or as separate github repository? :) It would be helpful for developers trying to abandon jQuery in favor of something lighter.
Thanks.