├── .bowerrc ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jshintrc ├── .travis.yml ├── CHANGELOG.md ├── Gruntfile.js ├── LICENSE.md ├── README.md ├── bower.json ├── contributing.md ├── dist └── aura.js ├── index.html ├── lib ├── aura.extensions.js ├── aura.js ├── base.js ├── ext │ ├── components.js │ ├── debug.js │ └── mediator.js ├── logger.js └── platform.js ├── package.json └── spec ├── aura_components ├── .gitkeep └── dummy │ └── main.js ├── index.html ├── lib ├── aura.extensions_spec.js ├── aura_spec.js └── ext │ ├── components_spec.js │ └── mediator_spec.js ├── runner.js └── support └── spec_helper.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "json": "bower.json" 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | bower_components 4 | components 5 | npm-debug.log 6 | .sass-cache 7 | /docs/ 8 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": true, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "eqnull": true, 11 | "browser": true, 12 | "nomen": false, 13 | "expr": true, 14 | "globals": { 15 | "module": true, 16 | "console": true, 17 | "require": true, 18 | "define": true, 19 | "_": true, 20 | "$": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | - '0.8' 5 | before_script: 6 | - npm install -g grunt-cli 7 | branches: 8 | only: 9 | - master 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.9.3/0.9.4 - 01/2015 2 | 3 | This is a periodic maintenance release which includes minor fixes: 4 | 5 | * Improvements to [callbacks](https://github.com/aurajs/aura/commit/bb3f377a46de119c0f2539a9044e7ffa4dea4d8e) 6 | * [IE8 Fixes](https://github.com/aurajs/aura/commit/a1db9e0a7dbd820a2faced50addf1de11409efe6) 7 | * [Typo improvements](https://github.com/aurajs/aura/commit/e066fe230e0b3840763d2590852644032e95a5ce) 8 | * Simplified [examples](https://github.com/aurajs/aura/commit/748cabaa3c08d822c1defea5847edc0912fc463f) 9 | * [Fixes for IE8 console](https://github.com/aurajs/aura/commit/f42e6b172f4b09f1f2d4fc5b18e00f547ee7c7e6) 10 | * Added [module](https://github.com/aurajs/aura/commit/ba883dc463358b8c227fb0f20916dda35a437943) to globals 11 | 12 | # v0.9.2 - 10/01/2013 13 | 14 | * Component.prototype.invokeWithCallback handles the correct signature (#316 by @xcambar) 15 | * Harmonizes init of app.logger and sandbox.logger (#314 by @xcambar) 16 | 17 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 'use strict'; 3 | 4 | grunt.loadNpmTasks('grunt-contrib-jshint'); 5 | grunt.loadNpmTasks('grunt-contrib-connect'); 6 | grunt.loadNpmTasks('grunt-contrib-watch'); 7 | grunt.loadNpmTasks('grunt-contrib-requirejs'); 8 | grunt.loadNpmTasks('grunt-contrib-yuidoc'); 9 | grunt.loadNpmTasks('grunt-contrib-concat'); 10 | grunt.loadNpmTasks('grunt-mocha'); 11 | 12 | var PORT = 8899; 13 | 14 | grunt.initConfig({ 15 | pkg: grunt.file.readJSON('package.json'), 16 | meta: { 17 | banner: '/*!\n' + 18 | '* <%= pkg.name %>\n' + 19 | '* v<%= pkg.version %> - ' + 20 | '<%= grunt.template.today("yyyy-mm-dd") %>\n' + 21 | '<%= pkg.homepage ? "* " + pkg.homepage + "\n" : "" %>' + 22 | '* (c) <%= pkg.author.name %>;' + 23 | ' <%= _.pluck(pkg.licenses, "type").join(", ") %> License\n' + 24 | '* Created by: <%= _.pluck(pkg.maintainers, "name").join(", ") %>\n' + 25 | '* Contributors: <%= _.pluck(pkg.contributors, "name").join(", ") %>\n' + 26 | '*/' 27 | }, 28 | connect: { 29 | server: { 30 | options: { 31 | port: PORT, 32 | base: '.' 33 | } 34 | } 35 | }, 36 | requirejs: { 37 | compile: { 38 | options: { 39 | baseUrl: '.', 40 | optimize: 'uglify2', 41 | preserveLicenseComments: false, 42 | paths: { 43 | aura: 'lib', 44 | jquery: 'empty:', 45 | underscore: 'empty:', 46 | eventemitter: 'bower_components/eventemitter2/lib/eventemitter2' 47 | }, 48 | shim: { 49 | underscore: { 50 | exports: '_' 51 | } 52 | }, 53 | include: [ 54 | 'aura/aura', 55 | 'aura/aura.extensions', 56 | 'aura/ext/debug', 57 | 'aura/ext/mediator', 58 | 'aura/ext/components' 59 | ], 60 | exclude: ['jquery'], 61 | out: 'dist/aura.js' 62 | } 63 | } 64 | }, 65 | concat: { 66 | options: { 67 | stripBanners: true, 68 | banner: 69 | "/*! Aura v<%= pkg.version %> | " + 70 | "(c) 2013 The Aura authors | " + 71 | "MIT License " + 72 | "*/\n" 73 | }, 74 | dist: { 75 | src: ['dist/aura.js'], 76 | dest: 'dist/aura.js', 77 | }, 78 | }, 79 | yuidoc: { 80 | compile: { 81 | name: "<%= pkg.name %>", 82 | description: "<%= pkg.description %>", 83 | version: "<%= pkg.version %>", 84 | url: "<%= pkg.homepage %>", 85 | options: { 86 | paths: [ "lib" ], 87 | outdir: "docs", 88 | parseOnly: true 89 | } 90 | } 91 | }, 92 | jshint: { 93 | all: { 94 | options: { 95 | jshintrc: '.jshintrc' 96 | }, 97 | files: { 98 | src: [ 99 | 'lib/**/*.js', 100 | 'spec/lib/**/*.js' 101 | ] 102 | } 103 | } 104 | }, 105 | mocha: { 106 | all: { 107 | options: { 108 | urls: ['http://localhost:<%= connect.server.options.port %>/spec/index.html'] 109 | } 110 | } 111 | }, 112 | watch: { 113 | files: [ 114 | 'lib/**/*.js', 115 | 'spec/lib/**/*.js' 116 | ], 117 | tasks: ['spec'] 118 | } 119 | }); 120 | 121 | grunt.registerTask('spec', ['jshint', 'mocha']); 122 | grunt.registerTask('build', ['connect', 'spec', 'requirejs', 'concat']); 123 | grunt.registerTask('default', ['connect', 'spec', 'watch']); 124 | }; 125 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) the Aura team. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aura 0.9.4 2 | 3 | 4 | 5 | # [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] 6 | 7 | Aura is an event-driven architecture for developing scalable applications using reusable components. It works great with [Backbone.js](http://backbonejs.org), but is framework-agnostic, adapts many best-practice patterns for developing maintainable apps and has first-class support for modern tools like [Bower](http://bower.io), [Grunt](http://gruntjs.com) and [Yeoman](http://yeoman.io). 8 | 9 | Aura has been used to develop applications like [MIT's Reap](http://www.bobholt.me/2012/09/how-it-was-built-mit-reap/) and is currently under active development. 10 | 11 | ## Project updates 12 | 13 | ### January, 2015 14 | 15 | At this time, we are aware that a number of developers are using Aura in production and welcome help with improving 16 | the core codebase via patches, feedback or improvements to our documentation. We have not yet had time to refactor 17 | the codebase into a set of Web Components (per the last periodic update), but are still interested in doing this. 18 | 19 | If you are interested in taking over core maintenance of the project please feel free to get in touch. 20 | 21 | ### December, 2013 22 | 23 | We first started AuraJS two years ago and have evolved it over time (with the help of engineers at [Hull.io](http://hull.io) and our contributors) to meet the needs of the RequireJS community. Today is is an excellent reference framework for how to structure a large-scale application with many of the core patterns necessary to build a system of decoupled modules that cleanly speak to each other. 24 | 25 | Two years on, the maintainers of AuraJS agree that the future of decoupled, scalable applications lie in Web 26 | Components - a set of standards composed of Custom Elements, Templates, Imports and ShadowDOM, aimed at offering 27 | a way to build encapsulated components using features found in the browser. 28 | 29 | To this end, our efforts on AuraJS moving forward will be focused on how it can help enable patterns for scalability 30 | in applications built with Web Components (using polyfills and libraries such as [Polymer](http://polymer-project.org). This may take the form of several Aura 'elements' that can be easily included and reused in large projects. 31 | 32 | Developers using AuraJS 0.9.2 and below will still be able to contribute to the current stable version of the project (there are large projects already built with it), however support for this version will be limited as we work on the designs for our next major version. We are more than happy to accept any contributions that meet our guidelines and will be reviewing the issue tracker for this version as time allows. 33 | 34 | Our team are excited about the future direction of the project and look forward to announcing more news about our work here in the future. 35 | 36 | ## Why Aura? 37 | 38 | We've seen a large shift in the JavaScript community for the past 3 years, with people starting to write web apps in a much more structured way. Yet, assembling the bits and pieces and actually starting to make apps is still a challenge. Another challenge is that most of the time you end up doing the same stuff all over again : you need a way to authenticate users, give them ways to communicate, exchange ideas, work or play together. You have to integrate with external services or APIs like Facebook or Twitter. 39 | 40 | Web apps are all about the end user experience (UI, DOM elements). The web development ecosystem is all about much more low level stuff. We need a way to package higher level abstractions and make them truly reusable, and that's what Aura is all about. 41 | 42 | Need some more reasons to use Aura?: 43 | 44 | * It's basically **glue** for your application components, making it trivial to tie together a number of independently created components into a fully functional application. 45 | * A complete event-bus supporting **application-level and component-level communication** mean you have control over what is getting triggered in your app 46 | * Specify an API end-point for components easily and just **use data-attributes to include any component** or components. Minimal JavaScript for more capabilities. 47 | * **Abstract away utility libraries** you are using (templating, DOM manipulation) so that you can swap them out for alternatives at any time without a great deal of effort 48 | * Hit the ground running quickly components into **reusable modules using AMD**. 49 | * Bower is a first-class citizen in Aura, making it easier to **manage your application dependencies** 50 | * The web platform is moving towards using scoped styles and shadow DOM for keeping parts of your page safe from third-party content that might affect it. Aura does the same for communications by introducing per-component **sandboxes** for your events 51 | * Tooling for **scaffolding** out new components without having to write as much boilerplate 52 | * Can be used with your MVC framework of choice - we're just there as a helper. 53 | * First-class support for the Hull.io platform. If you don't want to create a component yourself, you can easily use them as a components-source and create apps in less time. 54 | * Extensible via the extensions system, which make a good basis for a rich ecosystem around the project. 55 | 56 | 57 | ## Concepts 58 | 59 | #### The `Aura` object 60 | 61 | Your application will be an instance of the `Aura` object. 62 | 63 | Its responsibilities are to load extensions when the app starts and clean them up when the app stops. 64 | 65 | #### Extension 66 | 67 | Extensions are loaded in your application when it starts. They allow you to add features to the application, and are available to the components through their `sandbox`. 68 | 69 | #### Core 70 | 71 | The `core` implements aliases for DOM manipulation, templating and other lower-level utilities that pipe back to a library of choice. Aliases allow switching libraries with minimum impact on your application. 72 | 73 | #### Sandbox 74 | 75 | A `sandbox` is just a way to implement the [facade](http://addyosmani.com/resources/essentialjsdesignpatterns/book/#facadepatternjavascript) pattern on top of features provided by `core`. It lets you expose the parts of a JavaScript library that are safe to use instead of exposing the entire API. This is particularly useful when working in teams. 76 | 77 | When your app starts, it will create an instance of `sandbox` in each of your components. 78 | 79 | #### Component 80 | 81 | A component represents a unit of a page. Each component is independent. 82 | This means that they know nothing about each other. To make them communicate, a [Publish/Subscribe (Mediator)](http://addyosmani.com/resources/essentialjsdesignpatterns/book/#mediatorpatternjavascript) pattern is used. 83 | 84 | 85 | ## Getting started 86 | 87 | The simplest usable Aura app using a component and extension can be found in our [boilerplate](https://github.com/aurajs/boilerplate) repo. We do however recommend reading the rest of the getting started guide below to get acquainted with the general workflow. 88 | 89 | #### Requirements 90 | 91 | 1. [bower](http://bower.io/): run `npm install -g bower` if needed 92 | 2. [grunt-cli](https://github.com/gruntjs/grunt-cli): run `npm install -g grunt-cli` if needed 93 | 94 | #### Building Aura.js 95 | 96 | 1. Run `npm install` to install build dependencies. 97 | 2. Run `bower install` to install lib dependencies. 98 | 3. Run `grunt build` and `aura.js` will be placed in `dist/`. 99 | 100 | ### Running Tests 101 | 102 | #### Browser 103 | 104 | Run `grunt`. Then visit `http://localhost:8899/spec/`. 105 | 106 | #### CLI 107 | 108 | Run `npm test`. 109 | 110 | ## Creating an Application 111 | 112 | The first step in creating an Aura application is to make an instance of `Aura`. 113 | 114 | ```js 115 | var app = new Aura(); 116 | ``` 117 | 118 | Now that we have our `app`, we can start it. 119 | 120 | ```js 121 | app.start({ 122 | components: 'body' 123 | }); 124 | ``` 125 | 126 | This starts the app by saying that it should search for components anywhere in the `body` of your HTML document. 127 | 128 | ## Creating a Component 129 | 130 | By default, components are retrieved from a directory called `components/` that must be at the same level as your HTML document. 131 | 132 | Let's say we want to create a "hello" component. To do that, we need to create a `components/hello/` directory 133 | 134 | This directory must contain: 135 | 136 | - A `main.js` file. It will bootstrap and describe the component. It is mandatory, no matter how small it can be. 137 | - All the other files that your component needs (models, templates, ...). 138 | 139 | For our "hello" component the `main.js` will be: 140 | 141 | ```js 142 | define({ 143 | initialize: function () { 144 | this.$el.html('

Hello Aura

'); 145 | } 146 | }); 147 | ``` 148 | 149 | ## Declaring a Component 150 | 151 | Add the following code to your HTML document. 152 | 153 | ```html 154 |
155 | ``` 156 | 157 | Aura will call the `initialize` method that we have defined in `components/hello/main.js`. 158 | 159 | ## Creating an extension 160 | 161 | Imagine that we need an helper to reverse a string. In order to accomplish that we'll need to create an extension. 162 | 163 | ```js 164 | define('extensions/reverse', { 165 | initialize: function (app) { 166 | app.core.util.reverse = function (string) { 167 | return string.split('').reverse().join(''); 168 | }; 169 | } 170 | }); 171 | ``` 172 | 173 | 174 | ## Using extensions 175 | 176 | Extensions can then be loaded by your app by referencing them with their module name. 177 | 178 | To make our `reverse` helper available in our app, run the following code: 179 | 180 | This will call the `initialize` function of our `reverse` extension. 181 | 182 | ```js 183 | var app = Aura(); 184 | app.use('extensions/reverse'); 185 | app.start({ components: 'body' }); 186 | ``` 187 | 188 | Calling `use` when your `app` is already started will throw an error. 189 | 190 | ## Emitting and listening for event notifications 191 | 192 | The Aura [Mediator](https://github.com/aurajs/aura/blob/master/lib/ext/mediator.js) allows components to communicate with each other by subscribing, unsubscribing and emitting sandboxed event notifications. The signatures for these three methods are: 193 | 194 | * `sandbox.on(name, listener, context)` 195 | * `sandbox.off(name, listener)` 196 | * `sandbox.emit(name, data)` 197 | 198 | Below we can see an example of a Backbone view using the Mediator to emit a notification when tasks have been cleared and subscribing to changes from `tasks.stats` in order to render when they are updated. 199 | 200 | ```js 201 | define(['hbs!./stats'], function(template) { 202 | return { 203 | type: 'Backbone', 204 | events: { 205 | 'click button': 'clearCompleted' 206 | }, 207 | initialize: function() { 208 | this.render(); 209 | this.sandbox.on('tasks.stats', this.render, this); 210 | }, 211 | render: function(stats) { 212 | this.html(template(stats || {})); 213 | }, 214 | clearCompleted: function() { 215 | this.sandbox.emit('tasks.clear'); 216 | } 217 | } 218 | }); 219 | ``` 220 | 221 | ## Debugging 222 | 223 | To enable debug extension and logging pass `{debug: {enable: true}}` into the Aura constructor: 224 | 225 | ```js 226 | var app = new Aura({ 227 | debug: { 228 | enable: true 229 | } 230 | }); 231 | ``` 232 | Logger usage: 233 | 234 | ```js 235 | // You can use logger from components or extensions 236 | var logger = sandbox.logger; 237 | 238 | logger.log('Hey'); 239 | logger.warn('Hey'); 240 | logger.error('Hey'); 241 | 242 | // Or directly from Aura app 243 | 244 | var logger = app.logger; 245 | ``` 246 | Below we can see an example how to enable logging in specific ext/components. 247 | By default all loggers are enabled. 248 | 249 | ```js 250 | var app = new Aura({ 251 | debug: { 252 | enable: true, 253 | components: 'aura:mediator login signup info' 254 | } 255 | }); 256 | ``` 257 | 258 | Built-in components: 259 | 260 | * aura:mediator - event logging. 261 | 262 | Also, when `debug mode` is enabled, you can declare following function for any debug purposes: 263 | 264 | ```js 265 | // Function will be called for all Aura apps in your project 266 | window.attachDebugger = function (app) { 267 | // Do cool stuff with app object 268 | console.log(app); 269 | 270 | // Maybe you want to have access to Aura app via developer console? 271 | window.aura = app; 272 | }; 273 | ``` 274 | 275 | # Resources 276 | 277 | ## Yeoman generator 278 | 279 | An Aura scaffolding generator (for Yeoman) is also available at [Aura generator](https://github.com/dotCypress/generator-aura). 280 | 281 | ## Usage 282 | 283 | ```bash 284 | # First make a new directory, and `cd` into it: 285 | mkdir my-awesome-project && cd $_ 286 | 287 | # Then install `generator-aura`: 288 | npm install -g generator-aura 289 | 290 | # Run `yo aura`, optionally passing an app name: 291 | yo aura [app-name] 292 | 293 | # Finally, install npm and bower dependencies: 294 | npm install && bower install --dev 295 | ``` 296 | 297 | ## Generators 298 | 299 | Available generators: 300 | 301 | * [aura:component](#component) 302 | * [aura:extension](#extension) 303 | * [aura:styles](#styles) 304 | 305 | ### Component 306 | Generates a component in `app/components`. 307 | 308 | Example: 309 | 310 | ```bash 311 | yo aura:component sample 312 | ``` 313 | 314 | Produces `app/components/sample/main.js` 315 | 316 | ### Extension 317 | Generates a extension in `app/extensions`. 318 | 319 | Example: 320 | ```bash 321 | yo aura:extension storage 322 | ``` 323 | 324 | Produces `app/extensions/storage.js` 325 | 326 | ### Styles 327 | Generates cool styles. 328 | 329 | Example: 330 | ```bash 331 | yo aura:styles 332 | ``` 333 | 334 | ##### Supported types: 335 | 336 | * Default (normalize.css) 337 | * Twitter Bootstrap 338 | * Twitter Bootstrap for Compass 339 | * Zurb Foundation 340 | 341 | 342 | # Examples 343 | 344 | Want to look at some sample apps built with Aura? Check out: 345 | 346 | ### The [GitHub client](https://github.com/sbellity/aura-github) 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | ### The [GitHub Mobile client](https://github.com/hull/Github-Mobile/tree/with-hull) 361 | 362 | 363 | ### [Hullagram](https://github.com/hull/hullagram) - an Instagram clone built with Aura and [Hull.io](http://hull.io). 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | ### An Aura [TodoMVC](https://github.com/sbellity/aura-todos/) app implemented [two](https://github.com/alexanderbeletsky/todomvc-aura) ways 373 | 374 | 375 | 376 | 377 | ### [How to build your own Twitter-like "Open Source" page](http://blog.hull.io/post/46504817377/how-to-build-your-own-twitter-like-open-source-page) using Aura. 378 | 379 | 380 | 381 | 382 | ### Writing a simple [GitHub component](https://gist.github.com/sbellity/b44364f29fd89679ca39) using Aura. 383 | 384 | ### Aura Development docs 385 | 386 | * [Notes](https://github.com/aurajs/aura/tree/master/notes) 387 | 388 | # FAQs 389 | 390 | * [Where does Aura fit in the MVC workflow?](https://github.com/aurajs/aura/issues/223) 391 | * [How do you initialize a component with with data objects?](https://github.com/aurajs/aura/issues/222) 392 | * [Using multiple views and models in a component](https://github.com/aurajs/aura/issues/224) 393 | * [Sharing collections of data](https://github.com/karlwestin/aura-example) 394 | 395 | 396 | # Why do developers use us? 397 | 398 | * "The architecture and the fact that Aura Components are completely decoupled, will allow us to build an ecosystem of components that people can reuse internally or share with others." 399 | * "With ComponentSources and Require, we can load only the components that are needed by the app... at runtime." 400 | * "No JS is required to wire everything up, just include components with data-attributes in their markup" 401 | * "Mediation, same thing here it's a prerequisite to make everything decoupled... but in addition, it allows us to write much less code..." 402 | * "Template overrides FTW" 403 | 404 | # Contribute 405 | 406 | See the [contributing docs](https://github.com/aurajs/aura/blob/master/contributing.md) 407 | 408 | [npm-url]: https://npmjs.org/package/aura 409 | [npm-image]: https://badge.fury.io/js/aura.svg 410 | [travis-url]: https://travis-ci.org/addyosmani/aura 411 | [travis-image]: https://travis-ci.org/addyosmani/aura.svg?branch=master 412 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aura", 3 | "description": "Aura is an event-driven architecture for developing scalable applications using reusable components", 4 | "homepage": "http://aurajs.com", 5 | "version": "0.9.4", 6 | "main": [ 7 | "lib/aura.js" 8 | ], 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/aurajs/aura.git" 12 | }, 13 | "dependencies": { 14 | "jquery": "~1.10.x", 15 | "underscore": "~1.5.x", 16 | "eventemitter2": "~0.4.11", 17 | "requirejs": "~2.1.4", 18 | "requirejs-text": "~2.0.5" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We are more than happy to accept external contributions to the project in the form of feedback, bug reports and even better - pull requests :) At this time we are primarily focusing on improving the stability of AuraJS. Please keep this in mind if submitting feature requests, which we're happy to consider for future versions. 4 | 5 | ## Issue submission 6 | 7 | In order for us to help you please check that you've completed the following steps: 8 | 9 | * Made sure you're on the latest version in master 10 | * Used the search feature to ensure that the bug hasn't been reported before 11 | * Included as much information about the bug as possible, including any output you've received, what OS and version you're on, etc. 12 | * If making a bug report, please post a test case reproducing your issue on jsFiddle.net or jsBin. We will do our best to assist if this is not possible, but please understand this would greatly help improve or ability to help. 13 | 14 | [Submit your issue](https://github.com/aurajs/aura/issues/new) 15 | 16 | ## Pull Request Guidelines 17 | 18 | * Please check to make sure that there aren't existing pull requests attempting to address the issue mentioned. We also recommend checking for issues related to the issue on the tracker, as a team member may be working on the issue in a branch or fork. 19 | * Non-trivial changes should be discussed in an issue first 20 | * Develop in a topic branch, not master 21 | * Add relevant tests to cover the change 22 | * Squash your commits 23 | * Write a convincing description of your PR and why we should land it -------------------------------------------------------------------------------- /dist/aura.js: -------------------------------------------------------------------------------- 1 | /*! Aura v0.9.3 | (c) 2013 The Aura authors | MIT License */ 2 | define("aura/platform",[],function(){"function"!=typeof Function.prototype.bind&&(Function.prototype.bind=function(e){var t=this,n=Array.prototype.slice.call(arguments,1);return function(){return t.apply(e,Array.prototype.concat.apply(n,arguments))}}),"function"!=typeof Array.isArray&&(Array.isArray=function(e){return"[object Array]"===Object.prototype.toString.call(e)}),Object.create||(Object.create=function(e){function t(){}if(arguments.length>1)throw new Error("Object.create implementation only accepts the first parameter.");return t.prototype=e,new t}),Object.keys||(Object.keys=function(){var e=Object.prototype.hasOwnProperty,t=!{toString:null}.propertyIsEnumerable("toString"),n=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],r=n.length;return function(i){if("object"!=typeof i&&"function"!=typeof i||null===i)throw new TypeError("Object.keys called on non-object");var o=[];for(var s in i)e.call(i,s)&&o.push(s);if(t)for(var a=0;r>a;a++)e.call(i,n[a])&&o.push(n[a]);return o}}())}),function(){window.jQuery?define("jquery",[],function(){return window.jQuery}):require.config({paths:{jquery:"bower_components/jquery/jquery"},shim:{jquery:{exports:"$"}}}),window._?define("underscore",[],function(){return window._}):require.config({paths:{underscore:"bower_components/underscore/underscore"},shim:{underscore:{exports:"_"}}}),define("aura/base",["module","underscore","jquery","./platform"],function(e,t,n){require.s.contexts._.config.paths.aura||require.config({paths:{aura:e.id.replace(/base$/,"")}});var r={};return r.dom={find:function(e,t){return t=t||document,n(t).find(e)},data:function(e,t){return n(e).data(t)}},r.data={deferred:n.Deferred,when:n.when},r.util={each:n.each,extend:n.extend,uniq:t.uniq,_:t,decamelize:function(e,t){return t=void 0===t?"_":t,e.replace(/([A-Z])/g,t+"$1").toLowerCase()}},r.events={listen:function(e,t,r,i){return n(e).on(t,r,i)},bindAll:function(){return t.bindAll.apply(this,arguments)}},r.template={parse:t.template},r})}(),define("aura/logger",[],function(){function e(e){return this.name=e,this._log=t,this._warn=t,this._error=t,this._enabled=!1,this}var t=function(){},n=window.console||{};return e.prototype.isEnabled=function(){return this._enabled},e.prototype.setName=function(e){this.name=e},e.prototype.enable=function(){this._log=n.log||t,this._warn=n.warn||this._log,this._error=n.error||this._log,this._enabled=!0;try{if(Function.prototype.bind&&"object"==typeof n)for(var e=["log","warn","error"],r=0;rn;n++)if(e=t[n],"function"==typeof e)return e;return function(){}}function i(e){return"function"==typeof e?e.apply(void 0,c.call(arguments,1)):e}function o(e){var t=u(),n=e.ref,i=e.context,o=s(n,i);return o.fail(t.reject),o.done(function(e){if(!e)return t.resolve();var n=l(r(e,e.initialize)(i));n.done(function(){t.resolve(e)}),n.fail(t.reject)}),t.promise()}function s(e,t){var n=u(),r=function(e){if(e=i(e,t),e&&e.require&&e.require.paths){var r=Object.keys(e.require.paths)||[];require.config(e.require),require(r,function(){n.resolve(e)},o)}else n.resolve(e)},o=function(t){f.error("Error loading ext:",e,t),n.reject(t)};return"string"==typeof e?require([e],r,o):r(e),n}var a=e.util._,c=Array.prototype.slice,u=e.data.deferred,l=e.data.when,f=new t("Extensions").enable();return n.prototype.add=function(e){if(a.include(this._extensions,e)){var t=e.ref.toString()+" is already registered.";throw t+="Extensions can only be added once.",new Error(t)}if(this.initStarted)throw new Error("Init extensions already called");return this._extensions.push(e),this},n.prototype.onReady=function(e){return this.initStatus.then(e),this},n.prototype.onFailure=function(e){return this.initStatus.fail(e),this},n.prototype.init=function(){if(this.initStarted)throw new Error("Init extensions already called");this.initStarted=!0;var e=a.compact(this._extensions.slice(0)),t=[],n=this.initStatus;return function r(i){if(i){var s=o(i);t.push(s),s.done(function(){r(e.shift())}),s.fail(function(e){e||(e="Unknown error while loading an extension"),e instanceof Error||(e=new Error(e)),n.reject(e)})}else 0===e.length&&l.apply(void 0,t).done(function(){n.resolve(Array.prototype.slice.call(arguments))})}(e.shift()),n.promise()},n}),define("aura/aura",["./base","./aura.extensions","./logger"],function(e,t,n){function r(o){function s(e){if("string"==typeof e&&(e=c.sandboxes.get(e)),e){var t=["aura","sandbox","stop"].join(c.config.mediator.delimiter);return i.invoke(e._children,"stop"),c.core.mediator.emit(t,e),e._component&&e._component.invokeWithCallbacks("remove"),e.stopped=!0,e.el&&c.core.dom.find(e.el).remove(),u[e.ref]=null,delete u[e.ref],e}}if(!(this instanceof r))return new r(o);var a=new t,c=this;c.ref=i.uniqueId("aura_"),c.config=o=o||{},c.config.sources=c.config.sources||{"default":"./aura_components"};var u={},l=Object.create(e);c.sandboxes={},c.sandboxes.create=function(e,t){if(e=e||i.uniqueId("sandbox-"),u[e])throw new Error("Sandbox with ref "+e+" already exists.");var r=Object.create(l);r.ref=e||i.uniqueId("sandbox-"),r.logger=new n(r.ref),u[r.ref]=r;var s=o.debug;return(s===!0||s.enable&&(0===s.components.length||-1!==s.components.indexOf(e)))&&r.logger.enable(),i.extend(r,t||{}),r},c.sandboxes.get=function(e){return u[e]},c.use=function(e){return a.add({ref:e,context:c}),c},c.components={},c.components.addSource=function(e,t){if(o.sources[e])throw new Error("Components source '"+e+"' is already registered");return o.sources[e]=t,c},c.core=Object.create(e),c.start=function(t){if(c.started)return c.logger.error("Aura already started!"),a.initStatus;c.logger.log("Starting Aura");var n=t||{};return"string"==typeof t?n={components:c.core.dom.find(t)}:Array.isArray(t)?n={components:t}:t&&t.widgets&&!t.components?n.components=t.widgets:void 0===n.components&&(n.components=c.core.dom.find(c.config.components||"body")),a.onReady(function(t){e.util.each(t,function(e,t){t&&"function"==typeof t.afterAppStart&&t.afterAppStart(c)})}),a.onFailure(function(){c.logger.error("Error initializing app:",c.config.name,arguments),c.stop()}),c.startOptions=n,c.started=!0,a.init()},c.stop=function(){c.started=!1},c.sandbox=l,c.logger=new n(c.ref),c.sandbox.stop=function(e){e?c.core.dom.find(e,this.el).each(function(e,t){var n=c.core.dom.find(t).data("__sandbox_ref__");s(n)}):s(this)},o.debug=o.debug||{};var f=o.debug;return f===!0&&(o.debug=f={enable:!0}),f.enable&&(f.components=f.components?f.components.split(" "):[],c.logger.enable(),c.use("aura/ext/debug")),c.use("aura/ext/mediator"),c.use("aura/ext/components"),c}{var i=e.util._,o=function(){};Object.freeze||o}return r}),define("aura/ext/debug",[],function(){return{name:"debug",initialize:function(e){"function"==typeof window.attachDebugger&&(e.logger.log("Attaching debugger ..."),window.attachDebugger(e))}}}),!function(){function e(){this._events={},this._conf&&t.call(this,this._conf)}function t(e){e&&(this._conf=e,e.delimiter&&(this.delimiter=e.delimiter),e.maxListeners&&(this._events.maxListeners=e.maxListeners),e.wildcard&&(this.wildcard=e.wildcard),e.newListener&&(this.newListener=e.newListener),this.wildcard&&(this.listenerTree={}))}function n(e){this._events={},this.newListener=!1,t.call(this,e)}function r(e,t,n,i){if(!n)return[];var o,s,a,c,u,l,f,p=[],h=t.length,d=t[i],m=t[i+1];if(i===h&&n._listeners){if("function"==typeof n._listeners)return e&&e.push(n._listeners),[n];for(o=0,s=n._listeners.length;s>o;o++)e&&e.push(n._listeners[o]);return[n]}if("*"===d||"**"===d||n[d]){if("*"===d){for(a in n)"_listeners"!==a&&n.hasOwnProperty(a)&&(p=p.concat(r(e,t,n[a],i+1)));return p}if("**"===d){f=i+1===h||i+2===h&&"*"===m,f&&n._listeners&&(p=p.concat(r(e,t,n,h)));for(a in n)"_listeners"!==a&&n.hasOwnProperty(a)&&("*"===a||"**"===a?(n[a]._listeners&&!f&&(p=p.concat(r(e,t,n[a],h))),p=p.concat(r(e,t,n[a],i))):p=p.concat(a===m?r(e,t,n[a],i+2):r(e,t,n[a],i)));return p}p=p.concat(r(e,t,n[d],i+1))}if(c=n["*"],c&&r(e,t,c,i+1),u=n["**"])if(h>i){u._listeners&&r(e,t,u,h);for(a in u)"_listeners"!==a&&u.hasOwnProperty(a)&&(a===m?r(e,t,u[a],i+2):a===d?r(e,t,u[a],i+1):(l={},l[a]=u[a],r(e,t,{"**":l},i+1)))}else u._listeners?r(e,t,u,h):u["*"]&&u["*"]._listeners&&r(e,t,u["*"],h);return p}function i(e,t){e="string"==typeof e?e.split(this.delimiter):e.slice();for(var n=0,r=e.length;r>n+1;n++)if("**"===e[n]&&"**"===e[n+1])return;for(var i=this.listenerTree,a=e.shift();a;){if(i[a]||(i[a]={}),i=i[a],0===e.length){if(i._listeners){if("function"==typeof i._listeners)i._listeners=[i._listeners,t];else if(o(i._listeners)&&(i._listeners.push(t),!i._listeners.warned)){var c=s;"undefined"!=typeof this._events.maxListeners&&(c=this._events.maxListeners),c>0&&i._listeners.length>c&&(i._listeners.warned=!0,console.error("(node) warning: possible EventEmitter memory leak detected. %d listeners added. Use emitter.setMaxListeners() to increase limit.",i._listeners.length),console.trace())}}else i._listeners=t;return!0}a=e.shift()}return!0}var o=Array.isArray?Array.isArray:function(e){return"[object Array]"===Object.prototype.toString.call(e)},s=10;n.prototype.delimiter=".",n.prototype.setMaxListeners=function(t){this._events||e.call(this),this._events.maxListeners=t,this._conf||(this._conf={}),this._conf.maxListeners=t},n.prototype.event="",n.prototype.once=function(e,t){return this.many(e,1,t),this},n.prototype.many=function(e,t,n){function r(){0===--t&&i.off(e,r),n.apply(this,arguments)}var i=this;if("function"!=typeof n)throw new Error("many only accepts instances of Function");return r._origin=n,this.on(e,r),i},n.prototype.emit=function(){this._events||e.call(this);var t=arguments[0];if("newListener"===t&&!this.newListener&&!this._events.newListener)return!1;if(this._all){for(var n=arguments.length,i=new Array(n-1),o=1;n>o;o++)i[o-1]=arguments[o];for(o=0,n=this._all.length;n>o;o++)this.event=t,this._all[o].apply(this,i)}if("error"===t&&!(this._all||this._events.error||this.wildcard&&this.listenerTree.error))throw arguments[1]instanceof Error?arguments[1]:new Error("Uncaught, unspecified 'error' event.");var s;if(this.wildcard){s=[];var a="string"==typeof t?t.split(this.delimiter):t.slice();r.call(this,s,a,this.listenerTree,0)}else s=this._events[t];if("function"==typeof s){if(this.event=t,1===arguments.length)s.call(this);else if(arguments.length>1)switch(arguments.length){case 2:s.call(this,arguments[1]);break;case 3:s.call(this,arguments[1],arguments[2]);break;default:for(var n=arguments.length,i=new Array(n-1),o=1;n>o;o++)i[o-1]=arguments[o];s.apply(this,i)}return!0}if(s){for(var n=arguments.length,i=new Array(n-1),o=1;n>o;o++)i[o-1]=arguments[o];for(var c=s.slice(),o=0,n=c.length;n>o;o++)this.event=t,c[o].apply(this,i);return c.length>0||!!this._all}return!!this._all},n.prototype.on=function(t,n){if("function"==typeof t)return this.onAny(t),this;if("function"!=typeof n)throw new Error("on only accepts instances of Function");if(this._events||e.call(this),this.emit("newListener",t,n),this.wildcard)return i.call(this,t,n),this;if(this._events[t]){if("function"==typeof this._events[t])this._events[t]=[this._events[t],n];else if(o(this._events[t])&&(this._events[t].push(n),!this._events[t].warned)){var r=s;"undefined"!=typeof this._events.maxListeners&&(r=this._events.maxListeners),r>0&&this._events[t].length>r&&(this._events[t].warned=!0,console.error("(node) warning: possible EventEmitter memory leak detected. %d listeners added. Use emitter.setMaxListeners() to increase limit.",this._events[t].length),console.trace())}}else this._events[t]=n;return this},n.prototype.onAny=function(e){if("function"!=typeof e)throw new Error("onAny only accepts instances of Function");return this._all||(this._all=[]),this._all.push(e),this},n.prototype.addListener=n.prototype.on,n.prototype.off=function(e,t){if("function"!=typeof t)throw new Error("removeListener only takes instances of Function");var n,i=[];if(this.wildcard){var s="string"==typeof e?e.split(this.delimiter):e.slice();i=r.call(this,null,s,this.listenerTree,0)}else{if(!this._events[e])return this;n=this._events[e],i.push({_listeners:n})}for(var a=0;al;l++)if(n[l]===t||n[l].listener&&n[l].listener===t||n[l]._origin&&n[l]._origin===t){u=l;break}if(0>u)continue;return this.wildcard?c._listeners.splice(u,1):this._events[e].splice(u,1),0===n.length&&(this.wildcard?delete c._listeners:delete this._events[e]),this}(n===t||n.listener&&n.listener===t||n._origin&&n._origin===t)&&(this.wildcard?delete c._listeners:delete this._events[e])}return this},n.prototype.offAny=function(e){var t,n=0,r=0;if(e&&this._all&&this._all.length>0){for(t=this._all,n=0,r=t.length;r>n;n++)if(e===t[n])return t.splice(n,1),this}else this._all=[];return this},n.prototype.removeListener=n.prototype.off,n.prototype.removeAllListeners=function(t){if(0===arguments.length)return!this._events||e.call(this),this;if(this.wildcard)for(var n="string"==typeof t?t.split(this.delimiter):t.slice(),i=r.call(this,null,n,this.listenerTree,0),o=0;owindow.location = 'spec/index.html' 2 | -------------------------------------------------------------------------------- /lib/aura.extensions.js: -------------------------------------------------------------------------------- 1 | define(['./base', './logger'], function(base, Logger) { 2 | 3 | var _ = base.util._, 4 | slice = Array.prototype.slice, 5 | deferred = base.data.deferred, 6 | when = base.data.when, 7 | logger = new Logger('Extensions').enable(); 8 | 9 | function ExtManager() { 10 | this._extensions = []; 11 | this.initStatus = deferred(); 12 | return this; 13 | } 14 | 15 | //--------------------------------------------------------------------------- 16 | // Public API 17 | //--------------------------------------------------------------------------- 18 | 19 | ExtManager.prototype.add = function(ext) { 20 | if (_.include(this._extensions, ext)) { 21 | var msg = ext.ref.toString() + " is already registered."; 22 | msg += "Extensions can only be added once."; 23 | throw new Error(msg); 24 | } 25 | 26 | if (this.initStarted) { 27 | throw new Error("Init extensions already called"); 28 | } 29 | 30 | this._extensions.push(ext); 31 | return this; 32 | }; 33 | 34 | ExtManager.prototype.onReady = function(fn) { 35 | this.initStatus.then(fn); 36 | return this; 37 | }; 38 | 39 | ExtManager.prototype.onFailure = function(fn) { 40 | this.initStatus.fail(fn); 41 | return this; 42 | }; 43 | 44 | ExtManager.prototype.init = function() { 45 | 46 | if (this.initStarted) { 47 | throw new Error("Init extensions already called"); 48 | } 49 | 50 | this.initStarted = true; 51 | 52 | var extensions = _.compact(this._extensions.slice(0)), 53 | initialized = [], 54 | initStatus = this.initStatus; 55 | 56 | // Enforce sequencial loading of extensions. 57 | // The `initStatus` promise resolves to the 58 | // actually resolved and loaded extensions. 59 | (function _init(extDef) { 60 | if (extDef) { 61 | var ext = initExtension(extDef); 62 | initialized.push(ext); 63 | ext.done(function () { _init(extensions.shift()); }); 64 | ext.fail(function (err) { 65 | if (!err) { 66 | err = "Unknown error while loading an extension"; 67 | } 68 | if (!(err instanceof Error)) { 69 | err = new Error(err); 70 | } 71 | initStatus.reject(err); 72 | }); 73 | } else if (extensions.length === 0) { 74 | when.apply(undefined, initialized).done(function () { 75 | initStatus.resolve(Array.prototype.slice.call(arguments)); 76 | }); 77 | } 78 | })(extensions.shift()); 79 | 80 | return initStatus.promise(); 81 | }; 82 | 83 | //--------------------------------------------------------------------------- 84 | // Private API 85 | //--------------------------------------------------------------------------- 86 | 87 | /*! 88 | * Helper function that returns the first function found among its arguments. 89 | * If no function if found, it return a noop (empty function). 90 | * 91 | * @return {[type]} [description] 92 | */ 93 | function getFn() { 94 | var funcs = slice.call(arguments), fn; 95 | for (var f = 0, l = funcs.length; f < l; f++) { 96 | fn = funcs[f]; 97 | if (typeof(fn) === 'function') { return fn; } 98 | } 99 | return function () {}; 100 | } 101 | 102 | /*! 103 | * If the value of the first argument is a function then invoke 104 | * it with the rest of the args, otherwise, return it. 105 | */ 106 | function getVal(val) { 107 | if (typeof val === 'function') { 108 | return val.apply(undefined, slice.call(arguments, 1)); 109 | } else { 110 | return val; 111 | } 112 | } 113 | 114 | /*! 115 | * Actual extension loading. 116 | * 117 | * The sequence is: 118 | * 119 | * * resolves the extension reference 120 | * * register and requires its dependencies if any 121 | * * init the extension 122 | * 123 | * This method also returns a promise that allows 124 | * to keep track of the app's loading sequence. 125 | * 126 | * If the extension provides a `afterAppStart` method, 127 | * the promise will resolve to that function that 128 | * will be called at the end of the app loading sequence. 129 | * 130 | * @param {String|Object|Function} extDef the reference and context of the extension 131 | */ 132 | 133 | function initExtension(extDef) { 134 | var dfd = deferred(), 135 | ref = extDef.ref, 136 | context = extDef.context; 137 | 138 | var req = requireExtension(ref, context); 139 | req.fail(dfd.reject); 140 | req.done(function (ext) { 141 | 142 | // The extension did not return anything, 143 | // but probably already did what it had to do. 144 | if (!ext) { return dfd.resolve(); } 145 | 146 | // Let's initialize it then... 147 | // If ext is a function, call it 148 | // Else If ext has a init method, call it 149 | var init = when(getFn(ext, ext.initialize)(context)); 150 | init.done(function () { dfd.resolve(ext); }); 151 | init.fail(dfd.reject); 152 | }); 153 | return dfd.promise(); 154 | } 155 | 156 | /*! 157 | * Extension resolution before actual loading. 158 | * If `ext` is a String, it is considered as a reference 159 | * to an AMD module that has to be loaded. 160 | * 161 | * This method returns a promise that resolves to the actual extension, 162 | * With all its dependencies already required' too. 163 | * 164 | * @param {String|Object|Function} ext the reference of the extension 165 | * @param {Object} context the thing this extension is supposed to extend 166 | */ 167 | 168 | function requireExtension(ext, context) { 169 | var dfd = deferred(); 170 | 171 | var resolve = function (ext) { 172 | ext = getVal(ext, context); 173 | if (ext && ext.require && ext.require.paths) { 174 | var deps = Object.keys(ext.require.paths) || []; 175 | require.config(ext.require); 176 | require(deps, function() { 177 | dfd.resolve(ext); 178 | }, reject); 179 | } else { 180 | dfd.resolve(ext); 181 | } 182 | }; 183 | 184 | var reject = function (err) { 185 | logger.error("Error loading ext:", ext, err); 186 | dfd.reject(err); 187 | }; 188 | 189 | if (typeof ext === 'string') { 190 | require([ext], resolve, reject); 191 | } else { 192 | resolve(ext); 193 | } 194 | 195 | return dfd; 196 | } 197 | 198 | return ExtManager; 199 | }); 200 | -------------------------------------------------------------------------------- /lib/aura.js: -------------------------------------------------------------------------------- 1 | define([ 2 | './base', 3 | './aura.extensions', 4 | './logger' 5 | ], function(base, ExtManager, Logger) { 6 | 7 | var _ = base.util._, 8 | noop = function() {}, 9 | freeze = Object.freeze || noop; 10 | 11 | /** 12 | * Aura constructor and main entry point 13 | * 14 | * Every instance of Aura defines an Aura application. 15 | * An Aura application is in charge of loading the various 16 | * extensions that will apply to it (defined either 17 | * programmatically or by way of configuration). 18 | * 19 | * An Aura application is the glue between all the extensions 20 | * and components inside its instance. 21 | * 22 | * Internally an Aura application wraps 4 important objects: 23 | * 24 | * - `config` is the object passed as the first param of the apps constructor 25 | * - `core` is a container where the extensions add new features 26 | * - `sandbox` is an object that will be used as a prototype, to create fresh sandboxes to the components 27 | * - `extensions` An instance of the ExtensionManager. It contains all the extensions that will be loaded in the app. 28 | * 29 | * Extensions are here to provide features that will be used by the components... 30 | * They are meant to extend the apps' core & sandbox. 31 | * They also have access to the apps's config. 32 | * 33 | * Example of a creation of an Aura Application: 34 | * 35 | * var app = aura({ key: 'value' }); 36 | * app.use('ext1').use('ext2'); 37 | * app.components.addSource('supercomponents', 'https://my.extern.al/components/store'); 38 | * app.start('body'); 39 | * 40 | * @class Aura 41 | * @param {Object} [config] Main App config. 42 | * @method constructor 43 | */ 44 | function Aura(config) { 45 | 46 | if (!(this instanceof Aura)) { 47 | return new Aura(config); 48 | } 49 | 50 | var extManager = new ExtManager(), 51 | app = this; 52 | 53 | /** 54 | * The App's internal unique reference. 55 | * 56 | * @property {String} ref 57 | */ 58 | app.ref = _.uniqueId('aura_'); 59 | 60 | /** 61 | * The App's config object 62 | * 63 | * @property {Object} config 64 | */ 65 | app.config = config = config || {}; 66 | app.config.sources = app.config.sources || { "default" : "./aura_components" }; 67 | 68 | 69 | /*! 70 | * App Sandboxes 71 | */ 72 | 73 | var appSandboxes = {}; 74 | var baseSandbox = Object.create(base); 75 | 76 | /** 77 | * Namespace for sanboxes related methods. 78 | * 79 | * @type {Object} 80 | */ 81 | app.sandboxes = {}; 82 | 83 | /** 84 | * Creates a brand new sandbox, using the App's `sandbox` object as a prototype. 85 | * 86 | * @method sandboxes.create 87 | * @param {String} [ref] the Sandbox unique ref 88 | * @param {Object} [options] an object to that directly extends the Sandbox 89 | * @return {Sandbox} 90 | */ 91 | app.sandboxes.create = function (ref, options) { 92 | 93 | // Making shure we have a unique ref 94 | ref = ref || _.uniqueId('sandbox-'); 95 | if (appSandboxes[ref]) { 96 | throw new Error("Sandbox with ref " + ref + " already exists."); 97 | } 98 | 99 | // Create a brand new sandbox based on the baseSandbox 100 | var sandbox = Object.create(baseSandbox); 101 | 102 | // Give it a ref 103 | sandbox.ref = ref || _.uniqueId('sandbox-'); 104 | 105 | // Attach a Logger 106 | sandbox.logger = new Logger(sandbox.ref); 107 | 108 | // Register it in the app's sandboxes registry 109 | appSandboxes[sandbox.ref] = sandbox; 110 | 111 | var debug = config.debug; 112 | if(debug === true || (debug.enable && (debug.components.length === 0 || debug.components.indexOf(ref) !== -1))){ 113 | sandbox.logger.enable(); 114 | } 115 | 116 | // Extend if we have some options 117 | _.extend(sandbox, options || {}); 118 | 119 | return sandbox; 120 | }; 121 | 122 | /** 123 | * Get a sandbox by reference. 124 | * 125 | * @method sandboxes.get 126 | * @param {String} ref the Sandbox ref to retreive 127 | * @return {Sandbox} 128 | */ 129 | app.sandboxes.get = function(ref) { 130 | return appSandboxes[ref]; 131 | }; 132 | 133 | /** 134 | * Tells the app to load the given extension. 135 | * 136 | * Aura extensions are loaded in the app during init. 137 | * they are responsible for : 138 | * 139 | * - resolving & loading external dependencies via requirejs 140 | * - they have direct access to the app's internals 141 | * - they are here to add new features to the app... that are made available through the sandboxes to the components. 142 | * 143 | * This method can only be called before the App is actually started. 144 | * Note that the App is started when its `start` method is called. 145 | * 146 | * @method use 147 | * @param {String | Object | Function} ref the reference of the extension 148 | * @return {Aura} the Aura app object 149 | * @chainable 150 | */ 151 | app.use = function(ref) { 152 | extManager.add({ ref: ref, context: app }); 153 | return app; 154 | }; 155 | 156 | /** 157 | * Namespace for components related methods. 158 | * 159 | * @type {Object} 160 | */ 161 | app.components = {}; 162 | 163 | /** 164 | * Adds a new source for components. 165 | * A Component source is an endpoint (basically a URL) 166 | * that is the root of a component repository. 167 | * 168 | * @method components.addSource 169 | * @param {String} name the name of the source. 170 | * @param {String} baseUrl the base url for those components. 171 | */ 172 | app.components.addSource = function(name, baseUrl) { 173 | if (config.sources[name]) { 174 | throw new Error("Components source '" + name + "' is already registered"); 175 | } 176 | config.sources[name] = baseUrl; 177 | return app; 178 | }; 179 | 180 | /** 181 | * `core` is just a namespace used by the extensions to add features to the App. 182 | * 183 | * @property {Object} core 184 | */ 185 | app.core = Object.create(base); 186 | 187 | /** 188 | * Application start. 189 | * 190 | * Bootstraps the extensions loading process. 191 | * All the extensions are resolved and loaded when `start` id called. 192 | * Start returns a promise that shall fail if any of the 193 | * extensions fails to load. 194 | * 195 | * The app's responsibility is to load its extensions, 196 | * make sure they are properly loaded when the app starts 197 | * and eventually make sure they are properly cleaned up when the app stops 198 | * 199 | * @method start 200 | * @param {Object | String | Array} options start options. 201 | * @return {Promise} a promise that resolves when the app is started. 202 | */ 203 | app.start = function(options) { 204 | 205 | if (app.started) { 206 | app.logger.error("Aura already started!"); 207 | return extManager.initStatus; 208 | } 209 | 210 | app.logger.log("Starting Aura"); 211 | 212 | var startOptions = options || {}; 213 | 214 | // Support for different types of options (string, DOM Selector or Object) 215 | if (typeof options === 'string') { 216 | startOptions = { components: app.core.dom.find(options) }; 217 | } else if (Array.isArray(options)) { 218 | startOptions = { components: options }; 219 | } else if (options && options.widgets && !options.components) { 220 | startOptions.components = options.widgets; 221 | } else if (startOptions.components === undefined) { 222 | startOptions.components = app.core.dom.find(app.config.components || 'body'); 223 | } 224 | 225 | extManager.onReady(function(exts) { 226 | // Then we call all the `afterAppStart` provided by the extensions 227 | base.util.each(exts, function(i, ext) { 228 | if (ext && typeof(ext.afterAppStart) === 'function') { 229 | ext.afterAppStart(app); 230 | } 231 | }); 232 | }); 233 | 234 | // If there was an error in the boot sequence we 235 | // reject every body and perform a cleanup 236 | // TODO: Provide a better error message to the user. 237 | extManager.onFailure(function() { 238 | app.logger.error("Error initializing app:", app.config.name, arguments); 239 | app.stop(); 240 | }); 241 | 242 | app.startOptions = startOptions; 243 | app.started = true; 244 | 245 | // Finally... we return a promise that allows 246 | // to keep track of the loading process... 247 | return extManager.init(); 248 | }; 249 | 250 | /** 251 | * Stops the application and unregister its loaded dependencies. 252 | * 253 | * @method stop 254 | * @return {void} 255 | */ 256 | app.stop = function() { 257 | // TODO: We ne to actually do some cleanup here. 258 | app.started = false; 259 | }; 260 | 261 | /** 262 | * Sandboxes are a way to implement the facade pattern on top of the features provided by `core`. 263 | * 264 | * The `sandbox` property of an Aura App is just an Object that will be used 265 | * as a blueprint, once the app is started, to create new instances 266 | * of sandboxed environments for the Components. 267 | * @property {Object} sandbox 268 | */ 269 | app.sandbox = baseSandbox; 270 | 271 | /** 272 | * App Logger 273 | * 274 | * @property {Logger} logger 275 | */ 276 | app.logger = new Logger(app.ref); 277 | 278 | /** 279 | * @class Sandbox 280 | */ 281 | 282 | /** 283 | * Stop method for a Sandbox. 284 | * If no arguments provided, the sandbox itself and all its children are stopped. 285 | * If a DOM Selector is provided, all matching children will be stopped. 286 | * 287 | * @param {undefined|String} DOM Selector 288 | */ 289 | app.sandbox.stop = function(selector) { 290 | if (selector) { 291 | app.core.dom.find(selector, this.el).each(function(i, el) { 292 | var ref = app.core.dom.find(el).data('__sandbox_ref__'); 293 | stopSandbox(ref); 294 | }); 295 | } else { 296 | stopSandbox(this); 297 | } 298 | }; 299 | 300 | function stopSandbox(sandbox) { 301 | if (typeof sandbox === 'string') { 302 | sandbox = app.sandboxes.get(sandbox); 303 | } 304 | if (sandbox) { 305 | var event = ['aura', 'sandbox', 'stop'].join(app.config.mediator.delimiter); 306 | _.invoke(sandbox._children, 'stop'); 307 | app.core.mediator.emit(event, sandbox); 308 | if (sandbox._component) { 309 | sandbox._component.invokeWithCallbacks('remove'); 310 | } 311 | sandbox.stopped = true; 312 | sandbox.el && app.core.dom.find(sandbox.el).remove(); 313 | appSandboxes[sandbox.ref] = null; 314 | delete appSandboxes[sandbox.ref]; // do we need to actually call `delete` ? 315 | return sandbox; 316 | } 317 | } 318 | 319 | // Register core extensions : debug, mediator and components. 320 | config.debug = config.debug || {}; 321 | var debug = config.debug; 322 | if (debug === true) { 323 | config.debug = debug = { 324 | enable: true 325 | }; 326 | } 327 | if (debug.enable) { 328 | if(debug.components){ 329 | debug.components = debug.components.split(' '); 330 | }else{ 331 | debug.components = []; 332 | } 333 | app.logger.enable(); 334 | app.use('aura/ext/debug'); 335 | } 336 | 337 | app.use('aura/ext/mediator'); 338 | app.use('aura/ext/components'); 339 | 340 | return app; 341 | } 342 | 343 | return Aura; 344 | }); 345 | -------------------------------------------------------------------------------- /lib/base.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | if (window.jQuery) { 3 | define('jquery', [], function () { 4 | return window.jQuery; 5 | }); 6 | } else { 7 | require.config({ 8 | paths: { 9 | jquery: 'bower_components/jquery/jquery' 10 | }, 11 | shim: { 12 | jquery: { exports: '$' } 13 | } 14 | }); 15 | } 16 | 17 | if (window._) { 18 | define('underscore', [], function () { 19 | return window._; 20 | }); 21 | } else { 22 | require.config({ 23 | paths: { 24 | underscore: 'bower_components/underscore/underscore' 25 | }, 26 | shim: { 27 | underscore: { exports: '_' } 28 | } 29 | }); 30 | } 31 | 32 | define(['module', 'underscore', 'jquery', './platform'], function(module, _, $, platform) { 33 | 34 | // Auto configure aura path... if not set yet... 35 | if (!require.s.contexts._.config.paths.aura) { 36 | require.config({ paths: { aura: module.id.replace(/base$/, '') }}); 37 | } 38 | 39 | var base = {}; 40 | 41 | base.dom = { 42 | find: function(selector, context) { 43 | context = context || document; 44 | return $(context).find(selector); 45 | }, 46 | data: function(selector, attribute) { 47 | return $(selector).data(attribute); 48 | } 49 | }; 50 | 51 | base.data = { 52 | deferred: $.Deferred, 53 | when: $.when 54 | }; 55 | 56 | base.util = { 57 | each: $.each, 58 | extend: $.extend, 59 | uniq: _.uniq, 60 | _: _, 61 | decamelize: function(camelCase, delimiter) { 62 | delimiter = (delimiter === undefined) ? '_' : delimiter; 63 | return camelCase.replace(/([A-Z])/g, delimiter + '$1').toLowerCase(); 64 | } 65 | }; 66 | 67 | base.events = { 68 | listen: function(context, events, selector, callback) { 69 | return $(context).on(events, selector, callback); 70 | }, 71 | bindAll: function() { 72 | return _.bindAll.apply(this, arguments); 73 | } 74 | }; 75 | 76 | base.template = { 77 | parse: _.template 78 | }; 79 | 80 | return base; 81 | 82 | }); 83 | 84 | })(); 85 | -------------------------------------------------------------------------------- /lib/ext/components.js: -------------------------------------------------------------------------------- 1 | define('aura/ext/components', function() { 2 | 3 | 'use strict'; 4 | 5 | return function(app) { 6 | 7 | var ownProp = function(obj, key) { 8 | return Object.prototype.hasOwnProperty.call(obj, key); 9 | }; 10 | 11 | var core = app.core; 12 | var _ = app.core.util._; 13 | core.Components = core.Components || {}; 14 | 15 | /** 16 | * Components registry 17 | * @type {Object} 18 | */ 19 | var registeredComponents = {}; 20 | 21 | /** 22 | * Components Callbacks 23 | */ 24 | var componentsCallbacks = {}; 25 | 26 | function invokeCallbacks(stage, fnName, context, args) { 27 | var callbacks = componentsCallbacks[stage + ":" + fnName] || []; 28 | var dfds = []; 29 | app.core.util.each(callbacks, function(i, cb) { 30 | if (typeof cb === 'function') { 31 | dfds.push(cb.apply(context, args)); 32 | } 33 | }); 34 | var ret = app.core.data.when.apply(undefined, dfds).promise(); 35 | return ret; 36 | } 37 | 38 | function invokeWithCallbacks(fn, context) { 39 | var fnName; 40 | if (typeof fn === 'string') { 41 | fnName = fn; 42 | fn = context[fnName] || function() {}; 43 | } else if (typeof fn.name === 'string') { 44 | fnName = fn.name; 45 | fn = fn.fn || function() {}; 46 | } else { 47 | throw new Error("Error invoking Component with callbacks: ", context.options.name, ". first argument should be either the name of a function or of the form : { name: 'fnName', fn: function() { ... } } "); 48 | } 49 | 50 | var dfd = app.core.data.deferred(); 51 | var before, after, args = [].slice.call(arguments, 2); 52 | before = invokeCallbacks("before", fnName, context, args); 53 | 54 | before.then(function() { 55 | return fn.apply(context, args); 56 | }).then(function(_res) { 57 | return invokeCallbacks("after", fnName, context, args.concat(_res)).then(function() { 58 | dfd.resolve(_res); 59 | }, dfd.reject); 60 | }).fail(function(err) { 61 | app.logger.error("Error in Component " + context.options.name + " " + fnName + " callback", err); 62 | dfd.reject(err); 63 | }); 64 | 65 | return dfd.promise(); 66 | } 67 | 68 | /** 69 | * The base Component constructor... 70 | * @class Component 71 | * @constructor 72 | * @param {Object} options the options to init the component... 73 | */ 74 | function Component(options) { 75 | var opts = _.clone(options); 76 | 77 | /** 78 | * The Components' options Object, passed to its constructor. 79 | * TODO: Add explanation of how options are set from HTML API + which default options are 80 | * set during the Component initialization. 81 | * 82 | * @property {Object} options 83 | */ 84 | this.options = _.defaults(opts || {}, this.options || {}); 85 | 86 | /** 87 | * Internal, unique reference for a Component instance 88 | * 89 | * @property {String} _ref 90 | */ 91 | this._ref = opts._ref; 92 | 93 | /** 94 | * A cached jQuery object for the Components's element. 95 | * 96 | * @property {Object} $el 97 | */ 98 | this.$el = core.dom.find(opts.el); 99 | 100 | this.invokeWithCallbacks('initialize', this.options); 101 | return this; 102 | } 103 | 104 | /** 105 | * Method called on Component's initialization. 106 | * 107 | * @method initialize 108 | * @param {Object} options options Object passed on Component initialization 109 | */ 110 | Component.prototype.initialize = function() {}; 111 | 112 | /** 113 | * A helper function to render markup and recursively start nested components. 114 | * 115 | * @method html 116 | * @param {String} markup the markup to render in the component's root el 117 | * @return {Component} the Component instance to allow methods chaining... 118 | */ 119 | Component.prototype.html = function(markup) { 120 | var el = this.$el; 121 | el.html(markup); 122 | var self = this; 123 | _.defer(function() { 124 | self.sandbox.start(el, { reset: true }); 125 | }); 126 | return this; 127 | }; 128 | 129 | /** 130 | * A helper function to find matching elements within the Component's root element. 131 | * 132 | * @method $find 133 | * @param {Selector | jQuery Object | Element} selector CSS selector or jQuery object. 134 | * @return {jQuery Object} 135 | */ 136 | Component.prototype.$find = function(selector) { 137 | return this.$el.find(selector); 138 | }; 139 | 140 | /** 141 | * 142 | * @method invokeWithCallbacks 143 | * @param {String} methodName the name of the method to invoke with callbacks 144 | * @return {Promise} a Promise that will resolve to the return value of the original function invoked. 145 | */ 146 | Component.prototype.invokeWithCallbacks = function(methodName) { 147 | return invokeWithCallbacks.apply(undefined, [methodName, this].concat([].slice.call(arguments, 1))); 148 | }; 149 | 150 | // Stolen from Backbone 0.9.9 ! 151 | // Helper function to correctly set up the prototype chain, for subclasses. 152 | // Similar to `goog.inherits`, but uses a hash of prototype properties and 153 | // class properties to be extended. 154 | var extend = function(protoProps, staticProps) { 155 | var parent = this; 156 | var child; 157 | if (protoProps && ownProp(protoProps, 'constructor')) { 158 | child = protoProps.constructor; 159 | } else { 160 | child = function(){ parent.apply(this, arguments); }; 161 | } 162 | core.util.extend(child, parent, staticProps); 163 | var Surrogate = function(){ this.constructor = child; }; 164 | Surrogate.prototype = parent.prototype; 165 | child.prototype = new Surrogate(); 166 | if (protoProps) { core.util.extend(child.prototype, protoProps); } 167 | child.__super__ = parent.prototype; 168 | return child; 169 | }; 170 | 171 | Component.extend = extend; 172 | 173 | /** 174 | * Component loader. 175 | * @param {String} name The name of the Component to load 176 | * @param {Object} options The options to pass to the new component instance. 177 | * @return {Promise} A Promise that resolves to the loaded component instance. 178 | */ 179 | Component.load = function(name, opts) { 180 | // TODO: Make it more simple / or break it down 181 | // in several functions... 182 | // it's too big ! 183 | 184 | var dfd = core.data.deferred(), 185 | ref = opts.ref, 186 | component, 187 | ComponentConstructor, 188 | el = opts.el; 189 | 190 | opts._ref = core.util._.uniqueId(ref + '+'); 191 | 192 | var options = _.clone(opts); 193 | 194 | app.logger.log("Start loading component:", name); 195 | 196 | dfd.fail(function(err) { 197 | app.logger.error("Error loading component:", name, err); 198 | }); 199 | 200 | // Apply requirejs map / package configuration before the actual loading. 201 | require.config(options.require); 202 | 203 | // Here, we require the component's package definition 204 | require([ref], function(componentDefinition) { 205 | 206 | if (!componentDefinition) { 207 | return dfd.reject("component " + ref + " Definition is empty !"); 208 | } 209 | 210 | try { 211 | 212 | // Ok, the component has already been loaded once, we should already have it in the registry 213 | if (registeredComponents[ref]) { 214 | ComponentConstructor = registeredComponents[ref]; 215 | } else { 216 | 217 | if (componentDefinition.type) { 218 | // If `type` is defined, we use a constructor provided by an extension ? ex. Backbone. 219 | ComponentConstructor = core.Components[componentDefinition.type]; 220 | } else { 221 | // Otherwise, we use the stock Component constructor. 222 | ComponentConstructor = Component; 223 | } 224 | 225 | if (!ComponentConstructor) { 226 | throw new Error("Can't find component of type '" + componentDefinition.type + "', did you forget to include the extension that provides it ?"); 227 | } 228 | 229 | if (core.util._.isObject(componentDefinition)) { 230 | ComponentConstructor = registeredComponents[ref] = ComponentConstructor.extend(componentDefinition); 231 | } 232 | } 233 | 234 | var sandbox = app.sandboxes.create(opts._ref, { el: el }); 235 | 236 | sandbox.logger.setName("Component '" + name + "'(" + sandbox.logger.name + ')'); 237 | 238 | // Here we inject the sandbox in the component's prototype... 239 | var ext = { sandbox: sandbox }; 240 | 241 | // If the Component is just defined as a function, we use it as its `initialize` method. 242 | if (typeof componentDefinition === 'function') { 243 | ext.initialize = componentDefinition; 244 | } 245 | 246 | ComponentConstructor = ComponentConstructor.extend(ext); 247 | 248 | var newComponent = new ComponentConstructor(options); 249 | 250 | // Sandbox owns its el and vice-versa 251 | newComponent.$el.data('__sandbox_ref__', sandbox.ref); 252 | 253 | var initialized = core.data.when(newComponent); 254 | 255 | initialized.then(function(ret) { dfd.resolve(ret); }); 256 | initialized.fail(function(err) { dfd.reject(err); }); 257 | 258 | return initialized; 259 | } catch(err) { 260 | app.logger.error(err.message); 261 | dfd.reject(err); 262 | } 263 | }, function(err) { dfd.reject(err); }); 264 | 265 | return dfd.promise(); 266 | }; 267 | 268 | /** 269 | * Parses the component's options from its element's data attributes. 270 | * 271 | * @private 272 | * @param {String|DomNode} el the element 273 | * @param {String} namespace current Component's detected namespace 274 | * @param {String} opts an Object containing the base Component's options to extend. 275 | * @return {Object} An object that contains the Component's options 276 | */ 277 | function parseComponentOptions(el, namespace, opts) { 278 | 279 | var options = _.clone(opts || {}); 280 | options.el = el; 281 | options.require = {}; 282 | 283 | var name, data = core.dom.data(el); 284 | 285 | // Here we go through all the data attributes of the element to build the options object 286 | core.util.each(data, function(k, v) { 287 | k = k.replace(new RegExp("^" + namespace), ""); 288 | k = k.charAt(0).toLowerCase() + k.slice(1); 289 | if (k !== "component" && k !== 'widget') { 290 | options[k] = v; 291 | } else { 292 | name = v; 293 | } 294 | }); 295 | 296 | return buildComponentOptions(name, options); 297 | } 298 | 299 | /** 300 | * Parses the component's options from its element's data attributes. 301 | * 302 | * @private 303 | * @param {String} name the Component's name 304 | * @param {Object} opts an Object containing the base Component's options to extend. 305 | * @return {Object} An object that contains the component's options 306 | */ 307 | function buildComponentOptions(name, options) { 308 | var ref = name.split("@"), 309 | componentName = core.util.decamelize(ref[0]), 310 | componentSource = ref[1] || "default", 311 | requireContext = require.s.contexts._, 312 | componentsPath = app.config.sources[componentSource] || "./aura_components"; 313 | 314 | // Register the component as a requirejs package... 315 | // TODO: packages are not supported by almond, should we find another way to do this ? 316 | options.name = componentName; 317 | options.ref = '__component__$' + componentName + "@" + componentSource; 318 | options.baseUrl = componentsPath + "/" + componentName; 319 | options.require = options.require || {}; 320 | options.require.packages = options.require.packages || []; 321 | options.require.packages.push({ name: options.ref, location: componentsPath + "/" + componentName }); 322 | 323 | return options; 324 | } 325 | 326 | /**! 327 | * Returns a list of components. 328 | * If the first argument is a String, it is considered as a DomNode reference 329 | * We then parse its content to find aura-components inside of it. 330 | * 331 | * @static 332 | * @param {Array|String} components a list of components or a reference to a root dom node 333 | * @return {Array} a list of component with their options 334 | */ 335 | Component.parseList = function(components) { 336 | var list = []; 337 | 338 | if (Array.isArray(components)) { 339 | _.map(components, function(w) { 340 | var options = buildComponentOptions(w.name, w.options); 341 | list.push({ name: w.name, options: options }); 342 | }); 343 | } else if (components && core.dom.find(components)) { 344 | var appNamespace = app.config.namespace; 345 | 346 | // Support for legacy data-*-widget 347 | var selector = ["[data-aura-component]", "[data-aura-widget]"]; 348 | if (appNamespace) { 349 | selector.push("[data-" + appNamespace + "-component]"); 350 | selector.push("[data-" + appNamespace + "-widget]"); 351 | } 352 | selector = selector.join(","); 353 | core.dom.find(selector, components || 'body').each(function() { 354 | var ns = "aura"; 355 | if (appNamespace && (this.getAttribute('data-' + appNamespace +'-component') || this.getAttribute('data-' + appNamespace +'-widget'))) { 356 | ns = appNamespace; 357 | } 358 | var options = parseComponentOptions(this, ns); 359 | list.push({ name: options.name, options: options }); 360 | }); 361 | } 362 | return list; 363 | }; 364 | 365 | /** 366 | * Actual start method for a list of components. 367 | * 368 | * @static 369 | * @param {Array|String} components cf. `Component.parseList` 370 | * @return {Promise} a promise that resolves to a list of started components. 371 | */ 372 | Component.startAll = function(components) { 373 | var componentsList = Component.parseList(components); 374 | var list = []; 375 | core.util.each(componentsList, function(i, w) { 376 | var ret = Component.load(w.name, w.options); 377 | list.push(ret); 378 | }); 379 | var loadedComponents = core.data.when.apply(undefined, list); 380 | return loadedComponents.promise(); 381 | }; 382 | 383 | return { 384 | name: 'components', 385 | 386 | require: { paths: { text: 'bower_components/requirejs-text/text' } }, 387 | 388 | initialize: function(app) { 389 | 390 | // Components 'classes' registry... 391 | app.core.Components.Base = Component; 392 | 393 | /** 394 | * @class Aura 395 | */ 396 | 397 | 398 | /** 399 | * Registers a function that will be executed before the associated 400 | * Component method is called. 401 | * 402 | * Once registered, every time a component method will be called with the 403 | * `invokeWithCallbacks` method the registered callbacks will execute accordingly. 404 | * 405 | * The callback functions can return a promise if required in order to 406 | * postpone the execution of the called function up to when the all 407 | * registered callbacks results are resolved. 408 | * 409 | * The arguments passed to the callbacks are the same than those which 410 | * with the component will be called. The scope of the callback is 411 | * the component instance itself, as per the component method. 412 | * 413 | * @method components.before 414 | * @param {String} methodName eg. 'initialize', 'remove' 415 | * @param {Function} fn actual function to run 416 | */ 417 | app.components.before = function(methodName, fn) { 418 | var callbackName = "before:" + methodName; 419 | registerComponentCallback(callbackName, fn); 420 | }; 421 | 422 | /** 423 | * Same as components.before, but executed after the method invocation. 424 | * 425 | * @method components.after 426 | * @param {[type]} methodName eg. 'initialize', 'remove' 427 | * @param {Function} fn actual function to run 428 | */ 429 | app.components.after = function(methodName, fn) { 430 | var callbackName = "after:" + methodName; 431 | registerComponentCallback(callbackName, fn); 432 | }; 433 | 434 | // Actually registering the callbacks in the registry. 435 | function registerComponentCallback(callbackName, fn) { 436 | componentsCallbacks[callbackName] = componentsCallbacks[callbackName] || []; 437 | componentsCallbacks[callbackName].push(fn); 438 | } 439 | 440 | /** 441 | * Register a Component Type (experimental). 442 | * Components type ca be used to encapsulate many custom components behaviours. 443 | * They will be then used while declaring your components as follows: 444 | * 445 | * ```js 446 | * define({ 447 | * type: 'myComponentType', 448 | * // component declaration... 449 | * }); 450 | * ``` 451 | * 452 | * @method components.addType 453 | * @param {String} type a string that will identify the component type. 454 | * @param {Function} def A constructor the this component type 455 | */ 456 | app.components.addType = function(type, def) { 457 | if (app.core.Components[type]) { 458 | throw new Error("Component type " + type + " already defined"); 459 | } 460 | app.core.Components[type] = Component.extend(def); 461 | }; 462 | 463 | 464 | /** 465 | * @class Sandbox 466 | */ 467 | 468 | /** 469 | * Start method. 470 | * This method takes either an Array of Components to start or or DOM Selector to 471 | * target the element that will be parsed to look for Components to start. 472 | * 473 | * @method start 474 | * @param {Array|DOM Selector} list Array of Components to start or parent node. 475 | * @param {Object} options Available options: `reset` : if true, all current children 476 | * will be stopped before start. 477 | */ 478 | app.sandbox.start = function (list, options) { 479 | var event = ['aura', 'sandbox', 'start'].join(app.config.mediator.delimiter); 480 | app.core.mediator.emit(event, this); 481 | var children = this._children || []; 482 | if (options && options.reset) { 483 | _.invoke(this._children || [], 'stop'); 484 | children = []; 485 | } 486 | var self = this; 487 | 488 | Component.startAll(list).done(function () { 489 | var components = Array.prototype.slice.call(arguments); 490 | _.each(components, function (w) { 491 | w.sandbox._component = w; 492 | w.sandbox._parent = self; 493 | children.push(w.sandbox); 494 | }); 495 | self._children = children; 496 | }); 497 | 498 | return this; 499 | }; 500 | 501 | }, 502 | 503 | /** 504 | * When all of an application's extensions are finally loaded, the 'extensions' 505 | * afterAppStart methods are then called. 506 | * 507 | * @method components.afterAppStart 508 | * @param {Object} an Aura application object 509 | */ 510 | afterAppStart: function(app) { 511 | if (app.config.components !== false && app.startOptions.components !== false) { 512 | var el; 513 | if (Array.isArray(app.startOptions.components)) { 514 | el = core.dom.find('body'); 515 | } else { 516 | el = core.dom.find(app.startOptions.components); 517 | } 518 | app.core.appSandbox = app.sandboxes.create(app.ref, { el: el }); 519 | app.core.appSandbox.start(app.startOptions.components); 520 | } 521 | } 522 | }; 523 | }; 524 | }); 525 | -------------------------------------------------------------------------------- /lib/ext/debug.js: -------------------------------------------------------------------------------- 1 | define('aura/ext/debug', function() { 2 | 'use strict'; 3 | 4 | return { 5 | name: 'debug', 6 | 7 | initialize: function(app) { 8 | if (typeof window.attachDebugger === 'function') { 9 | app.logger.log('Attaching debugger ...'); 10 | window.attachDebugger(app); 11 | } 12 | } 13 | }; 14 | }); 15 | -------------------------------------------------------------------------------- /lib/ext/mediator.js: -------------------------------------------------------------------------------- 1 | define('aura/ext/mediator', function () { 2 | 'use strict'; 3 | 4 | return { 5 | name: 'mediator', 6 | 7 | require: { 8 | paths: { 9 | eventemitter: 'bower_components/eventemitter2/lib/eventemitter2', 10 | underscore: 'bower_components/underscore/underscore' 11 | }, 12 | shim: { 13 | underscore: { 14 | exports: '_' 15 | } 16 | } 17 | }, 18 | 19 | initialize: function (app) { 20 | var EventEmitter = require('eventemitter'); 21 | var _ = require('underscore'); 22 | 23 | app.config.mediator = _.defaults(app.config.mediator || {}, { 24 | wildcard: true, 25 | delimiter: '.', 26 | newListener: true, 27 | maxListeners: 20 28 | }); 29 | 30 | var mediator = new EventEmitter(app.config.mediator); 31 | 32 | app.core.mediator = mediator; 33 | 34 | var attachListener = function(listenerType) { 35 | return function (name, listener, context) { 36 | if (!_.isFunction(listener) || !_.isString(name)) { 37 | throw new Error('Invalid arguments passed to sandbox.' + listenerType); 38 | } 39 | context = context || this; 40 | var callback = function() { 41 | var args = Array.prototype.slice.call(arguments); 42 | try { 43 | listener.apply(context, args); 44 | } catch(e) { 45 | app.logger.error("Error caught in listener '" + name + "', called with arguments: ", args, "\nError:", e.message, e, args); 46 | } 47 | }; 48 | 49 | this._events = this._events || []; 50 | this._events.push({ name: name, listener: listener, callback: callback }); 51 | 52 | mediator[listenerType](name, callback); 53 | }; 54 | }; 55 | 56 | /** 57 | * @class Sandbox 58 | * 59 | */ 60 | 61 | /** 62 | * @method on 63 | * @param {String} name Pattern of event to subscrbibe to. 64 | */ 65 | app.sandbox.on = attachListener('on'); 66 | 67 | /** 68 | * @method once 69 | * @param {String} name Pattern of event to subscrbibe to. 70 | */ 71 | app.sandbox.once = attachListener('once'); 72 | 73 | /** 74 | * @method off 75 | * @param {String} name Pattern of event to subscrbibe to. 76 | * @param {Function} listener Listener function to stop. 77 | */ 78 | app.sandbox.off = function (name, listener) { 79 | if(!this._events) { return; } 80 | this._events = _.reject(this._events, function (evt) { 81 | var ret = (evt.name === name && evt.listener === listener); 82 | if (ret) { mediator.off(name, evt.callback); } 83 | return ret; 84 | }); 85 | }; 86 | 87 | 88 | /** 89 | * @method emit 90 | * @param {String} name Event name to emit 91 | * @param {Object} payload Payload emitted 92 | */ 93 | app.sandbox.emit = function () { 94 | var debug = app.config.debug; 95 | if(debug.enable && (debug.components.length === 0 || debug.components.indexOf("aura:mediator") !== -1)){ 96 | var eventData = Array.prototype.slice.call(arguments); 97 | eventData.unshift('Event emitted'); 98 | app.logger.log.apply(app.logger, eventData); 99 | } 100 | mediator.emit.apply(mediator, arguments); 101 | }; 102 | 103 | /** 104 | * @method stopListening 105 | */ 106 | 107 | app.sandbox.stopListening = function () { 108 | if (!this._events) { return; } 109 | _.each(this._events, function (evt) { 110 | mediator.off(evt.name, evt.callback); 111 | }); 112 | }; 113 | 114 | var eventName = ['aura', 'sandbox', 'stop'].join(app.config.mediator.delimiter); 115 | app.core.mediator.on(eventName, function (sandbox) { 116 | sandbox.stopListening(); 117 | }); 118 | } 119 | }; 120 | }); 121 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | define([], function() { 2 | 'use strict'; 3 | 4 | var noop = function() {}, 5 | console = window.console || {}; 6 | 7 | function Logger(name) { 8 | this.name = name; 9 | this._log = noop; 10 | this._warn = noop; 11 | this._error = noop; 12 | this._enabled = false; 13 | return this; 14 | } 15 | 16 | Logger.prototype.isEnabled = function () { 17 | return this._enabled; 18 | }; 19 | 20 | Logger.prototype.setName = function(name){ 21 | this.name = name; 22 | }; 23 | 24 | Logger.prototype.enable = function() { 25 | if (Function.prototype.bind && typeof console === "object") { 26 | var logFns = ["log", "warn", "error"]; 27 | for (var i = 0; i < logFns.length; i++) { 28 | console[logFns[i]] = Function.prototype.call.bind(console[logFns[i]], console); 29 | } 30 | } 31 | 32 | this._log = (console.log || noop); 33 | this._warn = (console.warn || this._log); 34 | this._error = (console.error || this._log); 35 | this._enabled = true; 36 | 37 | return this; 38 | }; 39 | 40 | Logger.prototype.write = function(output, args){ 41 | var parameters = Array.prototype.slice.call(args); 42 | parameters.unshift(this.name + ":"); 43 | output.apply(console, parameters); 44 | }; 45 | 46 | Logger.prototype.log = function() { 47 | this.write(this._log, arguments); 48 | }; 49 | 50 | Logger.prototype.warn = function() { 51 | this.write(this._warn, arguments); 52 | }; 53 | 54 | Logger.prototype.error = function() { 55 | this.write(this._error, arguments); 56 | }; 57 | 58 | return Logger; 59 | }); 60 | -------------------------------------------------------------------------------- /lib/platform.js: -------------------------------------------------------------------------------- 1 | define(function() { 2 | // The bind method is used for callbacks. 3 | // 4 | // * (bind)[https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind] 5 | // * (You don't need to use $.proxy)[http://www.aaron-powell.com/javascript/you-dont-need-jquery-proxy] 6 | // * credits: taken from bind_even_never in this discussion: https://prototype.lighthouseapp.com/projects/8886/tickets/215-optimize-bind-bindaseventlistener#ticket-215-9 7 | if (typeof Function.prototype.bind !== "function") { 8 | Function.prototype.bind = function(context) { 9 | var fn = this, args = Array.prototype.slice.call(arguments, 1); 10 | return function(){ 11 | return fn.apply(context, Array.prototype.concat.apply(args, arguments)); 12 | }; 13 | }; 14 | } 15 | 16 | // Returns true if an object is an array, false if it is not. 17 | // 18 | // * (isArray)[https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/isArray] 19 | if (typeof Array.isArray !== "function") { 20 | Array.isArray = function(vArg) { 21 | return Object.prototype.toString.call(vArg) === "[object Array]"; 22 | }; 23 | } 24 | 25 | // Creates a new object with the specified prototype object and properties. 26 | // 27 | // * (create)[https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create] 28 | 29 | if (!Object.create) { 30 | Object.create = function (o) { 31 | if (arguments.length > 1) { 32 | throw new Error('Object.create implementation only accepts the first parameter.'); 33 | } 34 | function F() {} 35 | F.prototype = o; 36 | return new F(); 37 | }; 38 | } 39 | // Returns an array of a given object's own enumerable properties, in the same order as that provided by a for-in loop 40 | // (the difference being that a for-in loop enumerates properties in the prototype chain as well). 41 | // 42 | // (keys)[https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys] 43 | if (!Object.keys) { 44 | Object.keys = (function () { 45 | var ownProp = Object.prototype.hasOwnProperty, 46 | hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'), 47 | dontEnums = [ 48 | 'toString', 49 | 'toLocaleString', 50 | 'valueOf', 51 | 'hasOwnProperty', 52 | 'isPrototypeOf', 53 | 'propertyIsEnumerable', 54 | 'constructor' 55 | ], 56 | dontEnumsLength = dontEnums.length; 57 | 58 | return function (obj) { 59 | if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) { 60 | throw new TypeError('Object.keys called on non-object'); 61 | } 62 | 63 | var result = []; 64 | 65 | for (var prop in obj) { 66 | if (ownProp.call(obj, prop)) { 67 | result.push(prop); 68 | } 69 | } 70 | 71 | if (hasDontEnumBug) { 72 | for (var i=0; i < dontEnumsLength; i++) { 73 | if (ownProp.call(obj, dontEnums[i])) { 74 | result.push(dontEnums[i]); 75 | } 76 | } 77 | } 78 | return result; 79 | }; 80 | })(); 81 | } 82 | 83 | 84 | }); 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aura", 3 | "version": "0.9.4", 4 | "homepage": "http://aurajs.com", 5 | "url": "https://github.com/aurajs/aura", 6 | "description": "An event-driven architecture for developing scalable applications", 7 | "devDependencies": { 8 | "bower": "^1.3.12", 9 | "chai": "^1.10.0", 10 | "grunt": "^0.4.5", 11 | "grunt-contrib-clean": "^0.6.0", 12 | "grunt-contrib-concat": "^0.5.0", 13 | "grunt-contrib-connect": "^0.9.0", 14 | "grunt-contrib-jshint": "^0.10.0", 15 | "grunt-contrib-requirejs": "^0.4.4", 16 | "grunt-contrib-watch": "^0.6.1", 17 | "grunt-contrib-yuidoc": "^0.6.0", 18 | "grunt-mocha": "^0.4.11", 19 | "mocha": "^2.0.1", 20 | "sinon": "^1.12.2", 21 | "sinon-chai": "^2.6.0" 22 | }, 23 | "scripts": { 24 | "pretest": "bower install", 25 | "test": "grunt build" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /spec/aura_components/.gitkeep: -------------------------------------------------------------------------------- 1 | .gitkeep -------------------------------------------------------------------------------- /spec/aura_components/dummy/main.js: -------------------------------------------------------------------------------- 1 | define({ 2 | initialize: function () { 3 | this.html('dummy !'); 4 | } 5 | }); 6 | -------------------------------------------------------------------------------- /spec/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mocha specs 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /spec/lib/aura.extensions_spec.js: -------------------------------------------------------------------------------- 1 | define(['aura/aura.extensions'], function (ExtManager) { 2 | 'use strict'; 3 | /*global describe:true, it:true, before:true, sinon:true */ 4 | 5 | describe('ExtManager', function () { 6 | it('should be a constructor', function () { 7 | ExtManager.should.be.a('function'); 8 | }); 9 | 10 | describe('ExtManager.prototype', function () { 11 | var proto = ExtManager.prototype; 12 | 13 | it('should have a init function', function () { 14 | proto.init.should.be.a('function'); 15 | }); 16 | 17 | it('should have a add function', function () { 18 | proto.add.should.be.a('function'); 19 | }); 20 | 21 | it('should have a onReady function', function () { 22 | proto.onReady.should.be.a('function'); 23 | }); 24 | 25 | it('should have a onFailure function', function () { 26 | proto.onFailure.should.be.a('function'); 27 | }); 28 | }); 29 | 30 | describe('Adding extensions', function () { 31 | it('Should be possible to add extensions', function (done) { 32 | var mgr = new ExtManager(); 33 | var ext1 = { ref: sinon.spy(), context: 'ext1' }; 34 | var ext2 = { ref: sinon.spy(), context: 'ext2' }; 35 | 36 | mgr.add(ext1); 37 | mgr.add(ext2); 38 | 39 | var init = mgr.init(); 40 | init.done(function () { 41 | ext1.ref.should.have.been.calledWith(ext1.context); 42 | ext2.ref.should.have.been.calledWith(ext2.context); 43 | done(); 44 | }); 45 | }); 46 | 47 | it('Should not be possible to add an extension twice', function () { 48 | var mgr = new ExtManager(); 49 | var ext = { ref: sinon.spy(), context: '123' }; 50 | var addExt = function () { mgr.add(ext); }; 51 | addExt(); 52 | addExt.should.Throw(Error); 53 | }); 54 | 55 | it('Should ensure extensions are loaded sequentially', function (done) { 56 | var mgr = new ExtManager(); 57 | var ctx = { foo: 'Bar' }; 58 | var ext1 = { 59 | initialize: function (c) { 60 | var later = $.Deferred(); 61 | _.delay(function () { 62 | c.ext1Loaded = true; 63 | later.resolve(); 64 | }, 100); 65 | return later; 66 | } 67 | }; 68 | var ext2 = function (c) { c.ext1Loaded.should.equal(true); }; 69 | mgr.add({ ref: ext1, context: ctx }); 70 | mgr.add({ ref: ext2, context: ctx }); 71 | mgr.init().done(function () { 72 | done(); 73 | }); 74 | }); 75 | 76 | it('Should be possible to add an extension via its module ref name', function (done) { 77 | var mgr = new ExtManager(); 78 | var ext = { initialize: sinon.spy(), foo: 'bar' }; 79 | var ctx = { foo: 'bar' }; 80 | 81 | define('myExt', ext); 82 | mgr.add({ ref: 'myExt', context: ctx }); 83 | mgr.init().done(function (extResolved) { 84 | extResolved[0].foo.should.equal('bar'); 85 | ext.initialize.should.have.been.calledWith(ctx); 86 | done(); 87 | }); 88 | }); 89 | }); 90 | 91 | describe('Error handling', function () { 92 | it('Should fail init if a ext ref is not found', function (done) { 93 | var mgr = new ExtManager(); 94 | mgr.add({ ref: 'nope' }).init().fail(function (err) { 95 | err.should.be.instanceOf(Error); 96 | done(); 97 | }); 98 | }); 99 | 100 | it('Should fail init if a dependency is not found', function (done) { 101 | var ext = { require: { paths: { not_here: 'not_here' } }, initialize: sinon.spy() }; 102 | var mgr = new ExtManager(); 103 | mgr.add({ ref: ext }); 104 | mgr.init().fail(function () { 105 | done(); 106 | }); 107 | }); 108 | 109 | }); 110 | 111 | describe('Lifecycle', function() { 112 | it('Should call onReady callbacks when all extensions have been loaded', function (done) { 113 | var mgr = new ExtManager(); 114 | var ctx = {}; 115 | var ready = sinon.spy(); 116 | var alsoReady = sinon.spy(); 117 | 118 | define('ext1', { init: function (c) { c.one = true; } }); 119 | define('ext2', { init: function (c) { c.two = true; } }); 120 | mgr.add({ ref: 'ext1', context: ctx }); 121 | mgr.add({ ref: 'ext2', context: ctx }); 122 | mgr.onReady(ready); 123 | mgr.onReady(alsoReady); 124 | var init = mgr.init(); 125 | init.then(function () { 126 | ready.should.have.been.called; 127 | alsoReady.should.have.been.called; 128 | done(); 129 | }); 130 | }); 131 | 132 | it('Should call onFailure callbacks when init has failed', function (done) { 133 | var onFail = sinon.spy(); 134 | var genErr = new Error('FAIL'); 135 | var mgr = new ExtManager().add({ 136 | ref: { 137 | initialize: function () { 138 | var dfd = $.Deferred(); 139 | dfd.reject(genErr); 140 | return dfd; 141 | } 142 | } 143 | }); 144 | mgr.onFailure(onFail); 145 | mgr.init().always(function (err) { 146 | err.should.be.equal(genErr); 147 | onFail.should.have.been.called; 148 | done(); 149 | }); 150 | }); 151 | }); 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /spec/lib/aura_spec.js: -------------------------------------------------------------------------------- 1 | define(['aura/aura'], function (aura) { 2 | 'use strict'; 3 | /*global describe:true, it:true, before:true, sinon:true */ 4 | 5 | describe('Aura Apps', function () { 6 | describe('App Public API', function () { 7 | var ext = { 8 | initialize: sinon.spy(function (app) { 9 | app.sandbox.foo = 'bar'; 10 | }), 11 | afterAppStart: sinon.spy() 12 | }; 13 | 14 | var App = aura(); 15 | 16 | App.use(ext); 17 | 18 | var startOptions = { foo: 'bar', components: false }; 19 | var initStatus = App.start(startOptions); 20 | 21 | // Make sure the app is started before... 22 | before(function (done) { 23 | initStatus.done(function () { 24 | done(); 25 | }); 26 | initStatus.fail(done); 27 | }); 28 | 29 | it('Should have loaded its core dependencies', function () { 30 | App.core.data.deferred.should.be.a('function'); 31 | }); 32 | 33 | it('Should have a public API', function () { 34 | App.use.should.be.a('function'); 35 | App.start.should.be.a('function'); 36 | App.stop.should.be.a('function'); 37 | }); 38 | 39 | it('Should call initialize method on extension', function () { 40 | ext.initialize.should.have.been.calledWith(App); 41 | }); 42 | 43 | it('Should call afterAppStart method on extension', function () { 44 | ext.afterAppStart.should.have.been.called; //With(App); 45 | }); 46 | 47 | it('Should have extended the sandbox', function () { 48 | App.sandbox.foo.should.equal('bar'); 49 | }); 50 | 51 | it('Should complain if I try to use a new extension and the app is already started', function () { 52 | var use = function () { 53 | App.use(function () {}); 54 | }; 55 | use.should.Throw(Error); 56 | }); 57 | }); 58 | 59 | describe('Defining and loading extensions', function () { 60 | it('Should be able to use extensions defined as objects', function (done) { 61 | var ext = { initialize: sinon.spy() }; 62 | aura().use(ext).start({ components: false }).done(function () { 63 | ext.initialize.should.have.been.called; 64 | done(); 65 | }); 66 | }); 67 | 68 | it('Should be able to use extensions defined as functions', function (done) { 69 | var insideExt = sinon.spy(); 70 | var ext = sinon.spy(function (appEnv) { 71 | insideExt('foo'); 72 | }); 73 | var App = aura().use(ext); 74 | App.start({ components: false }).done(function () { 75 | ext.should.have.been.calledWith(App); 76 | insideExt.should.have.been.calledWith('foo'); 77 | done(); 78 | }); 79 | }); 80 | 81 | it('Should pass the start options to the extensions...', function (done) { 82 | var startOptions = { foo: 'bar', components: false }; 83 | var insideExt = sinon.spy(); 84 | var ext = sinon.spy(function (app) { 85 | insideExt(app.startOptions); 86 | }); 87 | var App = aura().use(ext); 88 | App.start(startOptions).done(function () { 89 | ext.should.have.been.calledWith(App); 90 | insideExt.should.have.been.calledWith(startOptions); 91 | done(); 92 | }); 93 | }); 94 | 95 | it('Should be able to use extensions defined as amd modules', function (done) { 96 | var ext = { initialize: sinon.spy() }; 97 | define('myExtensionModule', ext); 98 | aura().use('myExtensionModule').start({ components: false }).done(function () { 99 | ext.initialize.should.have.been.called; 100 | done(); 101 | }); 102 | }); 103 | 104 | }); 105 | 106 | describe('Logging', function() { 107 | 108 | it('logger should be available on app', function() { 109 | var App = aura(); 110 | App.logger.log.should.be.a('function'); 111 | App.logger.name.should.equal(App.ref); 112 | }); 113 | 114 | it('logger should be available on sandboxes', function() { 115 | var App = aura(); 116 | var sandbox = App.sandboxes.create(); 117 | sandbox.logger.log.should.be.a('function'); 118 | sandbox.logger.name.should.equal(sandbox.ref); 119 | }); 120 | 121 | it('logger should be disabled by default', function() { 122 | var App = aura(); 123 | var sandbox = App.sandboxes.create(); 124 | App.logger.isEnabled().should.not.be.ok; 125 | sandbox.logger.isEnabled().should.not.be.ok; 126 | }); 127 | 128 | it('logger should be initialized with options to the Aura constructor', function () { 129 | var App1 = aura({debug: true}); 130 | var sandbox1 = App1.sandboxes.create(); 131 | App1.logger.isEnabled().should.be.ok; 132 | sandbox1.logger.isEnabled().should.be.ok; 133 | var App = aura({debug: {enable: true}}); 134 | var sandbox = App.sandboxes.create(); 135 | App.logger.isEnabled().should.be.ok; 136 | sandbox.logger.isEnabled().should.be.ok; 137 | }); 138 | }); 139 | 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /spec/lib/ext/components_spec.js: -------------------------------------------------------------------------------- 1 | define(['aura/aura', 'aura/ext/components'], function (aura, ext) { 2 | 'use strict'; 3 | /*global describe:true, it:true, beforeEach:true, before:true, alert:true, sinon:true */ 4 | 5 | var appConfig = { components: { sources: { 'default' : 'spec/components' } } }; 6 | var appsContainer = $('
').attr('style', 'display:none'); 7 | $('body').append(appsContainer); 8 | 9 | function buildAppMarkup(markup) { 10 | var container = $('
').attr('id', _.uniqueId('components_spec_')).html(markup); 11 | appsContainer.append(container); 12 | return container; 13 | } 14 | 15 | function makeSpyComponent(name, definition) { 16 | // sourceName = sourceName || "default"; 17 | if (!/\@/.test(name)) { 18 | name = name + '@default'; 19 | } 20 | definition = definition || { initialize: sinon.spy() }; 21 | var spyComponent = sinon.spy(function () { return definition; }); 22 | define('__component__$' + name, spyComponent); 23 | return spyComponent; 24 | } 25 | 26 | describe('Components API', function () { 27 | var app, BaseComponent; 28 | var yeahInit = sinon.spy(); 29 | var yeahComponent = makeSpyComponent('yeah', { initialize: yeahInit }); 30 | 31 | describe('Playing with Components', function () { 32 | beforeEach(function (done) { 33 | app = aura(appConfig); 34 | var container = buildAppMarkup('
'); 35 | app.start({ components: container }).then(function () { 36 | BaseComponent = app.core.Components.Base; 37 | done(); 38 | }); 39 | }); 40 | 41 | describe('Components Extension', function () { 42 | it('Should define the components registry and base Component on core', function () { 43 | app.core.Components.should.be.a('object'); 44 | app.core.Components.Base.should.be.a('function'); 45 | }); 46 | }); 47 | 48 | describe('Loading Components', function () { 49 | it('Should call the component\'s initialize method on start', function () { 50 | yeahInit.should.have.been.called; 51 | }); 52 | }); 53 | 54 | describe('Starting Components', function () { 55 | 56 | }); 57 | 58 | describe('Starting a list of components', function () { 59 | it('Should start the provided list of components', function () { 60 | var el = '#dummy-' + app.ref; 61 | var list = [{ name: 'dummy', options: { el: el } }]; 62 | // TODO... 63 | }); 64 | }); 65 | }); 66 | 67 | describe('Using legacy data-aura-widget attributes...', function () { 68 | 69 | var app, options; 70 | 71 | var myLegacyWidget = makeSpyComponent('legacy_widget', { 72 | initialize: function () {} 73 | }); 74 | 75 | before(function (done) { 76 | var markup = '
'; 77 | 78 | var container = buildAppMarkup(markup); 79 | 80 | app = aura(); 81 | app.start({ components: container }).done(function () { 82 | setTimeout(done, 0); 83 | }); 84 | }); 85 | 86 | it('Legacy data-aura-widget attribute should be recognized', function () { 87 | myLegacyWidget.should.have.been.called; 88 | }); 89 | 90 | }); 91 | 92 | describe('Using alternate namespace for data-attributes...', function () { 93 | var app, options; 94 | 95 | var myAltComponent = makeSpyComponent('alt_namespace', { 96 | options: { 97 | foo: 'bar', 98 | another: 'toutafait' 99 | }, 100 | initialize: function () { 101 | options = this.options; 102 | } 103 | }); 104 | 105 | before(function (done) { 106 | var markup = '
'; 114 | 115 | var container = buildAppMarkup(markup); 116 | 117 | app = aura({ namespace: 'super' }); 118 | app.start({ components: container }).done(function () { 119 | setTimeout(done, 0); 120 | }); 121 | }); 122 | 123 | it('Data attributes with alternate namespace should be recognized', function () { 124 | myAltComponent.should.have.been.called; 125 | }); 126 | 127 | it('It should take the right options too...', function () { 128 | options.another.should.equal('toutafait'); 129 | options.foo.should.equal('notbar'); 130 | options.sourceurl.should.equal('url'); 131 | options.paramName.should.equal('value'); 132 | options.funnyCaseParam.should.equal('yep'); 133 | options.param_nameWithUn_der_scores.should.equal('underscore'); 134 | }); 135 | }); 136 | 137 | describe('Creating new Component Types', function () { 138 | // a very simple component type... 139 | var NewComponentType = { 140 | foo: 'bar', 141 | initialize: function () { 142 | this.render(this.foo); 143 | } 144 | }; 145 | 146 | // An extension to load it 147 | var ext = { 148 | initialize: function (app) { 149 | app.components.addType('NewComponentType', NewComponentType); 150 | } 151 | }; 152 | 153 | // the render method of the component which will inherit from NewComponentType 154 | var render = sinon.spy(function (content) { 155 | this.html(content); 156 | }); 157 | 158 | // the actual component 159 | var my_component = { 160 | type: 'NewComponentType', 161 | render: render, 162 | foo: 'nope' 163 | }; 164 | 165 | makeSpyComponent('my_component', my_component); 166 | 167 | before(function (done) { 168 | var container = buildAppMarkup('
'); 169 | var app = aura(appConfig); 170 | app.use(ext).start({ components: container }).done(function () { 171 | // Hum... a little bit hacky 172 | // The promise resolves when the app is loaded... 173 | // not when the components are started... 174 | // TODO: what should we do ? 175 | setTimeout(done, 0); 176 | }); 177 | }); 178 | 179 | it('Should use the right type if asked to...', function () { 180 | // The render method should have been called 181 | // from the initialize method of the parent type... 182 | render.should.have.been.calledWith('nope'); 183 | }); 184 | }); 185 | 186 | describe('Nesting Components', function () { 187 | // Nesting means that if a component's markup contains data-aura-component elements, 188 | // They should be started recursively 189 | var container = buildAppMarkup('
'); 190 | var childComponent = makeSpyComponent('child'); 191 | before(function(done) { 192 | var parentComponent = makeSpyComponent('parent', { 193 | initialize: function() { 194 | this.html('
'); 195 | setTimeout(done, 0); 196 | } 197 | }); 198 | 199 | app = aura(); 200 | app.start({ components: container }); 201 | }); 202 | 203 | it('Should should auto start the child component once parent is rendered', function() { 204 | childComponent.should.have.been.called; 205 | }); 206 | 207 | }); 208 | 209 | describe('Adding new components source locations...', function () { 210 | var app; 211 | var myExternalComponent = makeSpyComponent('ext_component@anotherSource'); 212 | 213 | before(function (done) { 214 | app = aura(); 215 | 216 | // Adding the source 217 | app.components.addSource('anotherSource', 'remoteComponents'); 218 | 219 | // app start... 220 | var container = buildAppMarkup('
'); 221 | app.start({ components: container }).done(function () { 222 | setTimeout(done, 0); 223 | }); 224 | }); 225 | 226 | it('Should be possible to add new sources locations for components', function () { 227 | myExternalComponent.should.have.been.called; 228 | }); 229 | 230 | it('Should complain if we try to add a source that has already been registered', function () { 231 | var err = function () { app.components.addSource('anotherSource', '...'); }; 232 | err.should.Throw('Components source \'anotherSource\' is already registered'); 233 | }); 234 | }); 235 | 236 | describe('Adding new components source via an extension', function () { 237 | var anExternalComponent = makeSpyComponent('ext_component@aSource'); 238 | 239 | var app, ext = { 240 | initialize: function (app) { 241 | app.components.addSource('aSource', 'aUrl'); 242 | } 243 | }; 244 | 245 | before(function (done) { 246 | var container = buildAppMarkup('
'); 247 | app = aura(); 248 | app.use(ext).start({ components: container }).done(function () { setTimeout(done, 0); }); 249 | }); 250 | 251 | it('Should load the source via the extension', function () { 252 | anExternalComponent.should.have.been.called; 253 | }); 254 | }); 255 | 256 | describe('Components Callbacks', function(done) { 257 | var spy = sinon.spy(); 258 | var spyAfter = sinon.spy(); 259 | var spyCustom = sinon.spy(); 260 | 261 | before(function(done) { 262 | var container = buildAppMarkup('
'); 263 | var aComponent = makeSpyComponent('a_component_with_callbacks', { 264 | initialize: function() { 265 | if (this.beforeInitializeCalled) { 266 | spy(); 267 | this.initializedCalled = true; 268 | } 269 | }, 270 | myCustomMethod: function() {} 271 | }); 272 | var app = aura(); 273 | 274 | app.use(function(app) { 275 | app.components.before('initialize', function() { 276 | var self = this; 277 | var dfd = app.core.data.deferred(); 278 | setTimeout(function() { 279 | self.beforeInitializeCalled = true; 280 | dfd.resolve(); 281 | }, 10); 282 | return dfd.promise(); 283 | }); 284 | app.components.after('initialize', function() { 285 | if (this.initializedCalled) { 286 | spyAfter(); 287 | } 288 | this.invokeWithCallbacks('myCustomMethod'); 289 | }); 290 | 291 | app.components.after('myCustomMethod', function() { 292 | spyCustom(); 293 | this.sandbox.stop(); 294 | }); 295 | 296 | app.components.after('remove', function() { 297 | done(); 298 | }); 299 | }); 300 | 301 | var yeah = { name: 'a_component_with_callbacks', options: { el: '#a-component', formidable: 'Tout a Fait' } }; 302 | app.start([yeah]); 303 | }); 304 | 305 | it('should have called the before callback before initialize', function() { 306 | spy.should.have.been.called; 307 | spyAfter.should.have.been.called; 308 | spyCustom.should.have.been.called; 309 | }); 310 | }); 311 | 312 | describe('sandbox', function () { 313 | var app; 314 | var sandbox; 315 | var mediator; 316 | before(function (done) { 317 | app = aura(); 318 | app.start({ components: false }).done(function () { 319 | mediator = app.core.mediator; 320 | sandbox = app.sandboxes.create(); 321 | setTimeout(done, 0); 322 | }); 323 | }); 324 | 325 | describe('#start', function () { 326 | it('should augment sandbox', function () { 327 | sandbox.start.should.be.a('function'); 328 | }); 329 | 330 | var spy; 331 | before(function () { 332 | spy = sinon.spy(); 333 | mediator.on('aura.sandbox.start', spy); 334 | sandbox.start(); 335 | }); 336 | 337 | it('should emit aura.sandbox.start', function() { 338 | spy.should.have.been.called; 339 | }); 340 | }); 341 | 342 | describe('#stop', function () { 343 | it('should augment sandbox', function () { 344 | sandbox.stop.should.be.a('function'); 345 | }); 346 | 347 | var spy; 348 | before(function () { 349 | spy = sinon.spy(); 350 | mediator.on('aura.sandbox.stop', spy); 351 | sandbox.stop(); 352 | }); 353 | 354 | it('should emit aura.sandbox.stop', function () { 355 | spy.should.have.been.called; 356 | }); 357 | }); 358 | }); 359 | }); 360 | }); 361 | -------------------------------------------------------------------------------- /spec/lib/ext/mediator_spec.js: -------------------------------------------------------------------------------- 1 | /*global describe:true, it:true, before:true, beforeEach:true, afterEach:true, sinon:true */ 2 | 3 | define(['aura/aura', 'aura/ext/mediator'], function (aura, extension) { 4 | 'use strict'; 5 | 6 | describe('Mediator', function () { 7 | var app; 8 | var mediator; 9 | var config = { mediator: { maxListeners: 32 }, components: false }; 10 | 11 | beforeEach(function (done) { 12 | app = aura(config); 13 | app.use(extension); 14 | app.start().done(function () { 15 | mediator = app.core.mediator; 16 | setTimeout(done, 0); 17 | }); 18 | }); 19 | 20 | describe('config', function() { 21 | it('should pass aura config to mediator', function() { 22 | mediator._conf.maxListeners.should.equal(32); 23 | }); 24 | }); 25 | 26 | describe('core', function () { 27 | describe('.mediator', function () { 28 | it('should augment app.core', function () { 29 | mediator.should.be.defined; 30 | }); 31 | }); 32 | }); 33 | 34 | describe('sandbox', function () { 35 | var sandbox; 36 | beforeEach(function () { 37 | sandbox = app.sandboxes.create(); 38 | }); 39 | 40 | describe('#on', function () { 41 | it('should augment sandbox', function () { 42 | sandbox.on.should.be.a('function'); 43 | }); 44 | 45 | it('should add listener', function () { 46 | sandbox.on('test', function () {}); 47 | mediator.listeners('test').should.have.length(1); 48 | }); 49 | }); 50 | 51 | describe('#off', function () { 52 | it('should augment sandbox', function () { 53 | sandbox.off.should.be.a('function'); 54 | }); 55 | 56 | it('should remove listener', function () { 57 | var listener = function () {}; 58 | sandbox.on('test', listener); 59 | sandbox.off('test', listener); 60 | mediator.listeners('test').should.have.length(0); 61 | }); 62 | }); 63 | 64 | describe('#once', function() { 65 | it('should augment sandbox', function() { 66 | sandbox.once.should.be.a('function'); 67 | }); 68 | 69 | it('should add listener', function() { 70 | sandbox.once('test', function() {}); 71 | mediator.listeners('test').should.have.length(1); 72 | }); 73 | 74 | it('should remove listener after its called once', function(done) { 75 | sandbox.once('test', function() { 76 | mediator.listeners('test').should.have.length(0); 77 | done(); 78 | }); 79 | mediator.listeners('test').should.have.length(1); 80 | sandbox.emit('test'); 81 | }); 82 | }); 83 | 84 | describe('#emit', function () { 85 | it('should augment sandbox', function () { 86 | sandbox.emit.should.be.a('function'); 87 | }); 88 | 89 | var spy; 90 | beforeEach(function () { 91 | spy = sinon.spy(); 92 | sandbox.on('test', spy); 93 | }); 94 | 95 | it('should trigger listener without param', function () { 96 | sandbox.emit('test'); 97 | spy.should.have.been.called; 98 | }); 99 | 100 | it('should trigger listener with params', function () { 101 | sandbox.emit('test', 'foo', 'bar'); 102 | spy.should.have.been.calledWith('foo', 'bar'); 103 | }); 104 | }); 105 | 106 | describe('#stopListening', function () { 107 | it('should augment sandbox', function () { 108 | sandbox.stopListening.should.be.a('function'); 109 | }); 110 | 111 | it('should remove all listeners when called without params', function () { 112 | var spy1 = sinon.spy(); 113 | var spy2 = sinon.spy(); 114 | sandbox.on('test1', spy1); 115 | sandbox.on('test2', spy2); 116 | sandbox.stopListening(); 117 | sandbox.emit('test1'); 118 | sandbox.emit('test2'); 119 | 120 | spy1.should.not.have.been.called; 121 | spy2.should.not.have.been.called; 122 | }); 123 | }); 124 | }); 125 | 126 | describe('catch exceptions in listeners', function() { 127 | var sandbox, first, second, troublemaker, spy1, spy2, spy3, spy4; 128 | 129 | beforeEach(function() { 130 | spy1 = sinon.spy(); 131 | spy2 = sinon.spy(); 132 | spy3 = sinon.spy(); 133 | spy4 = sinon.spy(); 134 | var calledFirst = false; 135 | first = function() { 136 | calledFirst = true; 137 | spy1(); 138 | }; 139 | second = function() { 140 | if (calledFirst) { 141 | spy2(); 142 | } 143 | }; 144 | troublemaker = function() { 145 | spy3(); 146 | if (calledFirst) { 147 | throw new Error('You die !'); 148 | } else { 149 | throw new Error('You really die !'); 150 | } 151 | spy4(); 152 | }; 153 | sandbox = app.sandboxes.create(); 154 | }); 155 | 156 | it('should call them in order', function() { 157 | sandbox.on('test', first); 158 | sandbox.on('test', second); 159 | sandbox.emit('test'); 160 | spy2.should.have.been.called; 161 | }); 162 | 163 | it('should really call them in order', function() { 164 | sandbox.on('test', second); 165 | sandbox.on('test', first); 166 | sandbox.emit('test'); 167 | spy2.should.not.have.been.called; 168 | }); 169 | 170 | it('should not calling callbacks if an exception is raised somewhere', function() { 171 | sandbox.on('test', first); 172 | sandbox.on('test', troublemaker); 173 | sandbox.on('test', second); 174 | sandbox.emit('test'); 175 | spy2.should.have.been.called; 176 | spy3.should.have.been.called; 177 | spy4.should.not.have.been.called; 178 | }); 179 | 180 | }); 181 | 182 | describe('attaching context in listeners', function() { 183 | it('the sandbox should be the default context', function() { 184 | var context, sandbox = app.sandboxes.create(); 185 | sandbox.on('test', function() { 186 | context = this; 187 | }); 188 | sandbox.emit('test'); 189 | context.should.equal(sandbox); 190 | }); 191 | 192 | it('should be possible to override the default context', function() { 193 | var context, sandbox = app.sandboxes.create(), ctx = 'ctx'; 194 | sandbox.on('test', function() { 195 | context = this; 196 | }, ctx); 197 | sandbox.emit('test'); 198 | context.should.equal(ctx); 199 | }); 200 | }); 201 | 202 | describe('events', function () { 203 | describe('aura.sandbox.stop', function () { 204 | var spy; 205 | before(function () { 206 | spy = sinon.spy(); 207 | var mock = { stopListening: spy }; 208 | var event = ['aura', 'sandbox', 'stop'].join(app.config.mediator.delimiter); 209 | mediator.emit(event, mock); 210 | }); 211 | 212 | it('should call sandbox.stopListening', function () { 213 | spy.should.have.been.called; 214 | }); 215 | }); 216 | }); 217 | }); 218 | }); 219 | -------------------------------------------------------------------------------- /spec/runner.js: -------------------------------------------------------------------------------- 1 | /*global define mocha */ 2 | var should; 3 | 4 | require.config({ 5 | baseUrl: '../', 6 | paths: { 7 | components: 'components', 8 | aura: 'lib', 9 | aura_components: 'spec/aura_components', 10 | chai: 'node_modules/chai/chai', 11 | sinonChai:'node_modules/sinon-chai/lib/sinon-chai' 12 | } 13 | }); 14 | 15 | define(['chai', 'sinonChai'], function (chai, sinonChai) { 16 | window.chai = chai; 17 | window.expect = chai.expect; 18 | window.assert = chai.assert; 19 | window.should = chai.should(); 20 | window.sinonChai = sinonChai; 21 | window.notrack = true; 22 | 23 | chai.use(sinonChai); 24 | mocha.setup('bdd'); 25 | 26 | require([ 27 | 'spec/lib/aura_spec', 28 | 'spec/lib/aura.extensions_spec', 29 | 'spec/lib/ext/components_spec', 30 | 'spec/lib/ext/mediator_spec' 31 | ], function () { 32 | mocha.run(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /spec/support/spec_helper.js: -------------------------------------------------------------------------------- 1 | define(function () { 2 | 3 | }); 4 | --------------------------------------------------------------------------------