├── .gitignore
├── example4
├── fake-bonzo
│ ├── main.js
│ ├── package.json
│ └── bonzo-ender-bridge.js
├── example4.html
└── bonzo.js
├── example2
├── example2.html
└── bonzo.js
├── example1
├── example1.html
└── bonzo.js
├── example3
├── example3.html
├── bonzo-ender-bridge.js
├── bonzo.js
└── ender.js
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | *.min.js
3 |
--------------------------------------------------------------------------------
/example4/fake-bonzo/main.js:
--------------------------------------------------------------------------------
1 | provide('bonzo', bonzo)
2 |
--------------------------------------------------------------------------------
/example4/fake-bonzo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fake-bonzo",
3 | "version": "0.0.0",
4 | "description": "Fake Bonzo",
5 | "main": "main.js",
6 | "ender": "bonzo-ender-bridge.js"
7 | }
--------------------------------------------------------------------------------
/example2/example2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Example 2
6 |
7 |
8 |
9 |
24 |
25 |
--------------------------------------------------------------------------------
/example4/example4.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Example 4
6 |
7 |
8 |
9 |
10 |
25 |
26 |
--------------------------------------------------------------------------------
/example1/example1.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Example 1
6 |
7 |
8 |
9 |
28 |
29 |
--------------------------------------------------------------------------------
/example3/example3.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Example 3
6 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
32 |
33 |
--------------------------------------------------------------------------------
/example3/bonzo-ender-bridge.js:
--------------------------------------------------------------------------------
1 | (function ($) {
2 |
3 | var b = require('bonzo')
4 | b.setQueryEngine($)
5 | $.ender(b)
6 | $.ender(b(), true)
7 | $.ender({
8 | create: function (node) {
9 | return $(b.create(node))
10 | }
11 | })
12 |
13 | $.id = function (id) {
14 | return $([document.getElementById(id)])
15 | }
16 |
17 | function indexOf(ar, val) {
18 | for (var i = 0; i < ar.length; i++) if (ar[i] === val) return i
19 | return -1
20 | }
21 |
22 | function uniq(ar) {
23 | var r = [], i = 0, j = 0, k, item, inIt
24 | for (; item = ar[i]; ++i) {
25 | inIt = false
26 | for (k = 0; k < r.length; ++k) {
27 | if (r[k] === item) {
28 | inIt = true; break
29 | }
30 | }
31 | if (!inIt) r[j++] = item
32 | }
33 | return r
34 | }
35 |
36 | $.ender({
37 | parents: function (selector, closest) {
38 | if (!this.length) return this
39 | ;!selector && (selector = '*')
40 | var collection = $(selector), j, k, p, r = []
41 | for (j = 0, k = this.length; j < k; j++) {
42 | p = this[j]
43 | while (p = p.parentNode) {
44 | if (~indexOf(collection, p)) {
45 | r.push(p)
46 | if (closest) break;
47 | }
48 | }
49 | }
50 | return $(uniq(r))
51 | }
52 |
53 | , parent: function() {
54 | return $(uniq(b(this).parent()))
55 | }
56 |
57 | , closest: function (selector) {
58 | return this.parents(selector, true)
59 | }
60 |
61 | , first: function () {
62 | return $(this.length ? this[0] : this)
63 | }
64 |
65 | , last: function () {
66 | return $(this.length ? this[this.length - 1] : [])
67 | }
68 |
69 | , next: function () {
70 | return $(b(this).next())
71 | }
72 |
73 | , previous: function () {
74 | return $(b(this).previous())
75 | }
76 |
77 | , appendTo: function (t) {
78 | return b(this.selector).appendTo(t, this)
79 | }
80 |
81 | , prependTo: function (t) {
82 | return b(this.selector).prependTo(t, this)
83 | }
84 |
85 | , insertAfter: function (t) {
86 | return b(this.selector).insertAfter(t, this)
87 | }
88 |
89 | , insertBefore: function (t) {
90 | return b(this.selector).insertBefore(t, this)
91 | }
92 |
93 | , replaceWith: function (t) {
94 | return b(this.selector).replaceWith(t, this)
95 | }
96 |
97 | , siblings: function () {
98 | var i, l, p, r = []
99 | for (i = 0, l = this.length; i < l; i++) {
100 | p = this[i]
101 | while (p = p.previousSibling) p.nodeType == 1 && r.push(p)
102 | p = this[i]
103 | while (p = p.nextSibling) p.nodeType == 1 && r.push(p)
104 | }
105 | return $(r)
106 | }
107 |
108 | , children: function () {
109 | var i, l, el, r = []
110 | for (i = 0, l = this.length; i < l; i++) {
111 | if (!(el = b.firstChild(this[i]))) continue;
112 | r.push(el)
113 | while (el = el.nextSibling) el.nodeType == 1 && r.push(el)
114 | }
115 | return $(uniq(r))
116 | }
117 |
118 | , height: function (v) {
119 | return dimension.call(this, 'height', v)
120 | }
121 |
122 | , width: function (v) {
123 | return dimension.call(this, 'width', v)
124 | }
125 | }, true)
126 |
127 | /**
128 | * @param {string} type either width or height
129 | * @param {number=} opt_v becomes a setter instead of a getter
130 | * @return {number}
131 | */
132 | function dimension(type, opt_v) {
133 | return typeof opt_v == 'undefined'
134 | ? b(this).dim()[type]
135 | : this.css(type, opt_v)
136 | }
137 | }(ender));
--------------------------------------------------------------------------------
/example4/fake-bonzo/bonzo-ender-bridge.js:
--------------------------------------------------------------------------------
1 | (function ($) {
2 |
3 | var b = require('bonzo')
4 | b.setQueryEngine($)
5 | $.ender(b)
6 | $.ender(b(), true)
7 | $.ender({
8 | create: function (node) {
9 | return $(b.create(node))
10 | }
11 | })
12 |
13 | $.id = function (id) {
14 | return $([document.getElementById(id)])
15 | }
16 |
17 | function indexOf(ar, val) {
18 | for (var i = 0; i < ar.length; i++) if (ar[i] === val) return i
19 | return -1
20 | }
21 |
22 | function uniq(ar) {
23 | var r = [], i = 0, j = 0, k, item, inIt
24 | for (; item = ar[i]; ++i) {
25 | inIt = false
26 | for (k = 0; k < r.length; ++k) {
27 | if (r[k] === item) {
28 | inIt = true; break
29 | }
30 | }
31 | if (!inIt) r[j++] = item
32 | }
33 | return r
34 | }
35 |
36 | $.ender({
37 | parents: function (selector, closest) {
38 | if (!this.length) return this
39 | ;!selector && (selector = '*')
40 | var collection = $(selector), j, k, p, r = []
41 | for (j = 0, k = this.length; j < k; j++) {
42 | p = this[j]
43 | while (p = p.parentNode) {
44 | if (~indexOf(collection, p)) {
45 | r.push(p)
46 | if (closest) break;
47 | }
48 | }
49 | }
50 | return $(uniq(r))
51 | }
52 |
53 | , parent: function() {
54 | return $(uniq(b(this).parent()))
55 | }
56 |
57 | , closest: function (selector) {
58 | return this.parents(selector, true)
59 | }
60 |
61 | , first: function () {
62 | return $(this.length ? this[0] : this)
63 | }
64 |
65 | , last: function () {
66 | return $(this.length ? this[this.length - 1] : [])
67 | }
68 |
69 | , next: function () {
70 | return $(b(this).next())
71 | }
72 |
73 | , previous: function () {
74 | return $(b(this).previous())
75 | }
76 |
77 | , appendTo: function (t) {
78 | return b(this.selector).appendTo(t, this)
79 | }
80 |
81 | , prependTo: function (t) {
82 | return b(this.selector).prependTo(t, this)
83 | }
84 |
85 | , insertAfter: function (t) {
86 | return b(this.selector).insertAfter(t, this)
87 | }
88 |
89 | , insertBefore: function (t) {
90 | return b(this.selector).insertBefore(t, this)
91 | }
92 |
93 | , replaceWith: function (t) {
94 | return b(this.selector).replaceWith(t, this)
95 | }
96 |
97 | , siblings: function () {
98 | var i, l, p, r = []
99 | for (i = 0, l = this.length; i < l; i++) {
100 | p = this[i]
101 | while (p = p.previousSibling) p.nodeType == 1 && r.push(p)
102 | p = this[i]
103 | while (p = p.nextSibling) p.nodeType == 1 && r.push(p)
104 | }
105 | return $(r)
106 | }
107 |
108 | , children: function () {
109 | var i, l, el, r = []
110 | for (i = 0, l = this.length; i < l; i++) {
111 | if (!(el = b.firstChild(this[i]))) continue;
112 | r.push(el)
113 | while (el = el.nextSibling) el.nodeType == 1 && r.push(el)
114 | }
115 | return $(uniq(r))
116 | }
117 |
118 | , height: function (v) {
119 | return dimension.call(this, 'height', v)
120 | }
121 |
122 | , width: function (v) {
123 | return dimension.call(this, 'width', v)
124 | }
125 | }, true)
126 |
127 | /**
128 | * @param {string} type either width or height
129 | * @param {number=} opt_v becomes a setter instead of a getter
130 | * @return {number}
131 | */
132 | function dimension(type, opt_v) {
133 | return typeof opt_v == 'undefined'
134 | ? b(this).dim()[type]
135 | : this.css(type, opt_v)
136 | }
137 | }(ender));
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # How Ender bundles libraries for the browser
2 | ***By [Rod Vagg](https://twitter.com/rvagg)***
3 |
4 | I was asked an interesting Ender question on IRC (#enderjs on Freenode) and as I was answering it, it occurred to me that the subject would be an ideal way to explain how Ender's multi-library bundling works. So here is that explanation!
5 |
6 | The original question went something like this:
7 |
8 | > When a browser first visits my page, they only get served Bonzo (a DOM manipulation library) as a stand-alone library, but on returning visits they are also served Qwery (a selector engine), Bean (an event manager) and a few other modules in an Ender build. Can I integrate Bonzo into the Ender build on the browser for repeat visitors?
9 |
10 | ## Wait, what's Ender?
11 |
12 | Let's step back a bit and start with some basics. The way I generally explain Ender to people is that it's two different things:
13 |
14 | 1. It's a build tool, for bundling JavaScript libraries together into a single file. The resulting file constitutes a new "framework" based around the jQuery-style DOM element collection pattern: `$('selector').method()`. The constituent libraries provide the functionality for the *methods* and may also provide the selector engine functionality.
15 | 2. It's an *ecosystem* of JavaScript libraries. Ender promotes a small collection of libraries as a base, called **The Jeesh**, which together provide a large portion of the functionality normally required of a JavaScript framework, but there are many more libraries compatible with Ender that add extra functionality. Many of the libraries available for Ender are also usable outside of Ender as stand-alone libraries.
16 |
17 | The Jeesh is made up of the following libraries, each of these also works as a stand-alone library:
18 |
19 | * **[domReady](https://github.com/ded/domready)**: detects when the DOM is ready for manipulation. Provides `$.domReady(callback)` and `$.ready(callback)` methods.
20 | * **[Qwery](https://github.com/ded/qwery)**: a small and fast CSS3-compatible selector engine. Does the work of looking up DOM elements when you call `$('selector')` and also provides `$(elements).find('selector')`, `$(elements).and(elements)` and `$(elements).is('selector')`.
21 | * **[Bonzo](https://github.com/ded/bonzo)**: a DOM manipulation library, providing some of the most commonly used methods, such as `$(elements).css('property', 'value')`, `$(elements).empty()`, `$(elements).after(elements||html)`, and many more.
22 | * **[Bean](https://github.com/fat/bean)**: an event manager, provides jQuery-style `$(elements).bind('event', callback)` and others.
23 |
24 | The Jeesh gives you the features of these four libraries bundled into a neat package for only *11.7 kB* minified and gzipped.
25 |
26 | ## Starting with the basics, Bonzo stand-alone
27 |
28 | Bonzo is a great way to start getting your head around Ender because it's so useful by itself. Let's include it in a page and do some really simple DOM manipulation with it.
29 |
30 | ```html
31 |
32 |
33 |
34 |
35 | Example 1
36 |
37 |
38 |
39 |
58 |
59 |
60 | ```
61 |
62 | *You can run this as [example1](http://rvagg.github.com/ender-tutorial-guts/example1/example1.html), also available in my GitHub [repository](https://github.com/rvagg/ender-tutorial-guts) for this article.*
63 |
64 | This should look relatively familiar to a jQuery user -- you can see that Bonzo is providing some of the important utilities you need for modifying the DOM.
65 |
66 | ## Jumping straight in, Bonzo inside Ender
67 |
68 | Let's see what happens when we use a simple Ender build that includes Bonzo. We'll also include Qwery so we can skip the `document.getElementById()` noise, and we'll also use Bean to demonstrate how neatly the libraries can mesh together.
69 |
70 | This is done on the command line with: `ender build qwery bean bonzo`. A file named *ender.js* will be created that can be loaded on a suitable HTML page.
71 |
72 | Our script becomes:
73 |
74 | ```js
75 | $('')
76 | .text($('#scr').text())
77 | .css({
78 | fontWeight: 'bold',
79 | border: 'solid 1px red',
80 | margin: 10,
81 | padding: 10
82 | })
83 | .bind('click', function () {
84 | alert('Clickety clack');
85 | })
86 | .appendTo('body');
87 | ```
88 |
89 | *You can run this as [example2](http://rvagg.github.com/ender-tutorial-guts/example2/example2.html), also available in my GitHub [repository](https://github.com/rvagg/ender-tutorial-guts) for this article.*
90 |
91 | Bonzo performs most of the work here but it's bundled up nicely into the `$` object (also available as `ender`). The previous example can be summarised as follows:
92 |
93 | * `bonzo.create()` is now working when HTML is passed to `$()`.
94 | * Qwery does the work when `$()` is called with anything else, in this case `$('#scr')` is used as a selector for the script element.
95 | * We're using the no-argument variant of `bonzo.text()` to fetch the `innerHTML` of the script element.
96 | * Bean makes a showing with the `.bind()` call, but the important point is that it's integrated into our call-chain even though it's a separate library. This is where Ender's bundling magic shines.
97 | * `bonzo.appendTo()` takes the selector argument which is in turn passed to Qwery to fetch the selected element from the DOM (`document.body`).
98 |
99 | Also important here, which we haven't demonstrated, is we can do all of this on multiple elements in the same collection. The first line could be changed to `$('')` and we'd end up with two blocks, both responding to the click event.
100 |
101 | ## Getting educational, pull it apart again!
102 |
103 | It's possible to pull Bonzo out of the Ender build and manually stitch it back together again. Just like we used to do with our toys when we were children! (Or was that just me?)
104 |
105 | First, our Ender build is now created with: `ender build qwery bean` (or we could run `ender remove bonzo` to remove Bonzo from the previous example's `ender.js` file). The new `ender.js` file will contain the selector engine goodness from Qwery, and event management from Bean, but not much else.
106 |
107 | Bonzo can be loaded separately, but we'll need some special glue to do this. In Ender parlance, this glue is called an Ender **Bridge**.
108 |
109 | ### The Ender Bridge
110 |
111 | Ender follows the basic CommonJS Module pattern -- it sets up a simple module registry and gives each module a `module.exports` object and a `require()` method that can be used to fetch any other modules in the build. It also uses a `provide('name', module.exports)` method to insert exports into the registry with the name of your module. The exact details here aren't important and I'll cover how you can build your own Ender module in a later article, for now we just need a basic understanding of the module registry system.
112 |
113 | Using our Qwery, Bean and Bonzo build, the file looks something like this:
114 |
115 | ```
116 | |========================================|
117 | | Ender initialisation & module registry |
118 | | (we call this the 'client library') |
119 | |========================================|
120 | | 'module.exports' setup |
121 | |----------------------------------------|
122 | | Qwery source |
123 | |----------------------------------------|
124 | | provide('qwery', module.exports) |
125 | |----------------------------------------|
126 | | Qwery bridge |
127 | ==========================================
128 | | 'module.exports' setup |
129 | |----------------------------------------|
130 | | Bean source |
131 | |----------------------------------------|
132 | | provide('bean', module.exports) |
133 | |----------------------------------------|
134 | | Bean bridge |
135 | ==========================================
136 | | 'module.exports' setup |
137 | |----------------------------------------|
138 | | Bonzo source |
139 | |----------------------------------------|
140 | | provide('bonzo', module.exports) |
141 | |----------------------------------------|
142 | | Bonzo bridge |
143 | ==========================================
144 | ```
145 |
146 | To be a useful Ender library, the code should be able to adhere to the CommonJS Module pattern if a `module.exports` or `exports` object exists. Many libraries already do this so they can operate both in the browser and in a CommonJS environment such as Node. Consider Underscore.js for example, it [detects the existence of `exports`](https://github.com/documentcloud/underscore/blob/ca0df9076079a3b2c45475ddb2299fb901a29989/underscore.js#L56-63) and inserts itself onto that object if it exists, otherwise it inserts itself into the global (i.e. `window`) object. This is how Ender compatible libraries that can also be used as stand-alone libraries work too.
147 |
148 | So, skipping over the complexities here, our libraries are registered within Ender and then we encounter the **Bridge**. Technically the bridge is just an arbitrary piece of code that Ender-compatible libraries are allowed to provide the Ender CLI tool; it could be anything. The intention, though, is to use it as a glue to bind the library into the core `ender` / `$` object. A bridge isn't necessary and can be omitted -- in this case everything found on `module.exports` is automatically bound to the `ender` / `$` object. Underscore.js doesn't need a bridge because it conforms to the standard CommonJS pattern and its methods are utilities that logically belong on `$` -- for example, `$.each(list, callback)`. If a module needs to operate on `$('selector')` collections then it needs a special binding for its methods. Many modules also require quite complex bindings to make them work nicely inside the Ender environment.
149 |
150 | Bonzo has one of the most complex bridges that you'll find in the Endersphere, so we won't be looking into it here. If you're interested in digging deeper, a simpler bridge with some interesting features can be found in [Morpheus](https://github.com/ded/morpheus/blob/master/src/ender.js), an animation framework for Ender. Morpheus adds a `$.tween()` method and also an `$('selector').animate()` and some additional helper methods.
151 |
152 | The simplest form of Ender bridge is one that lifts the `module.exports` methods to a new *namespace*. Consider [Moment.js](http://momentjs.com/), the popular date and time library. When used in a CommonJS environment it adds all of its methods to `module.exports`. Without a bridge, when added to an Ender build you'd end up with `$.utc()`, `$.unix()`, `$.add()`, `$.subtract()` and other methods that don't have very meaningful names outside of Moment.js. They are also likely to conflict with other libraries that you may want to add to your Ender build. The logical solution is to lift them up to `$.moment.utc()` etc., then you also get to use the exported main function as `$.moment(Date|String|Number)`. To achieve this, Moment.js' [bridge](https://github.com/timrwood/moment/blob/master/ender.js) looks like this:
153 |
154 | ```js
155 | $.ender({ moment: require('moment') })
156 | ```
157 |
158 | The `$.ender()` method is the way that a bridge can add methods to the global `ender` / `$` object, it takes an optional boolean argument to indicate whether the methods can operate on DOM element collections, i.e. `$('selector').method()`.
159 |
160 | ### Bonzo in parts
161 |
162 | Back to what we were originally trying to achieve: we're loading Bonzo as a stand-alone library and we want to integrate it into an Ender build in the browser. There are two important things we need to do to achieve this: (1) load Bonzo's bridge so it can wire Bonzo into Ender, and (2) make Ender aware of Bonzo so a `require('bonzo')` will do the right thing because this is how the bridge fetches Bonzo.
163 |
164 | Let's first do this the easy way. With an Ender build that just contains Qwery and Bean and Bonzo's bridge in a separate file named *bonzo-ender-bridge.js*, we can do the following:
165 |
166 | ```html
167 |
168 |
169 |
170 |
173 |
174 | ```
175 |
176 | If you look at the diagram of the Ender file structure above you'll see that we're replicating it with our `
228 |
229 |
244 |