38 |
39 |
40 |
--------------------------------------------------------------------------------
/test/tests/module.appendTo.js:
--------------------------------------------------------------------------------
1 |
2 | describe('View#appendTo()', function() {
3 | var viewToTest;
4 |
5 | beforeEach(function() {
6 | viewToTest = helpers.createView();
7 | });
8 |
9 | test("Should return the view for a fluent interface.", function() {
10 | var sandbox = document.createElement('div'),
11 | sandbox2 = document.createElement('div'),
12 | existing = document.createElement('div');
13 |
14 | sandbox2.appendChild(existing);
15 |
16 | expect(viewToTest.render().appendTo(sandbox)).toBe(viewToTest);
17 | expect(viewToTest.render().insertBefore(sandbox2, existing)).toBe(viewToTest);
18 | });
19 |
20 | test("Should append the view element as a child of the given element.", function() {
21 | var sandbox = document.createElement('div');
22 |
23 | viewToTest
24 | .render()
25 | .appendTo(sandbox);
26 |
27 | expect(viewToTest.el).toBe(sandbox.firstElementChild);
28 | });
29 |
30 | test("Should not destroy existing element contents.", function() {
31 | var sandbox = document.createElement('div'),
32 | existing = document.createElement('div');
33 |
34 | sandbox.appendChild(existing);
35 |
36 | viewToTest
37 | .render()
38 | .appendTo(sandbox);
39 |
40 | expect(existing).toBe(sandbox.firstElementChild);
41 | expect(viewToTest.el).toBe(sandbox.lastElementChild);
42 | });
43 |
44 | test("Should insert before specified element.", function() {
45 | var sandbox = document.createElement('div'),
46 | existing1 = document.createElement('div'),
47 | existing2 = document.createElement('div');
48 |
49 | sandbox.appendChild(existing1);
50 | sandbox.appendChild(existing2);
51 |
52 | viewToTest
53 | .render()
54 | .insertBefore(sandbox, existing2);
55 |
56 | expect(existing1).toBe(sandbox.firstElementChild);
57 | expect(viewToTest.el).toBe(existing1.nextSibling);
58 | expect(existing2).toBe(viewToTest.el.nextSibling);
59 | });
60 |
61 | afterEach(function() {
62 | helpers.emptySandbox();
63 | helpers.destroyView();
64 | viewToTest = null;
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/test/tests/module.fire.js:
--------------------------------------------------------------------------------
1 |
2 | describe('View#fire()', function() {
3 | var viewToTest;
4 |
5 | beforeEach(function() {
6 | viewToTest = helpers.createView();
7 | });
8 |
9 | test("Should run on callbacks registered on the view", function() {
10 | var spy = jest.fn();
11 |
12 | viewToTest.on('testevent', spy);
13 | viewToTest.fire('testevent');
14 |
15 | expect(spy).toHaveBeenCalledTimes(1);
16 | });
17 |
18 | test("Events should bubble by default", function() {
19 | var spy = jest.fn();
20 | var child = viewToTest.module('orange');
21 |
22 | viewToTest.on('childtestevent', spy);
23 | child.fire('childtestevent');
24 |
25 | expect(spy).toHaveBeenCalledTimes(1);
26 | });
27 |
28 | test("Calling event.stopPropagation() should stop bubbling", function() {
29 | var spy = jest.fn();
30 | var child = viewToTest.module('orange');
31 |
32 | viewToTest.on('childtestevent', spy);
33 | child.on('childtestevent', function(){
34 | this.event.stopPropagation();
35 | });
36 |
37 | child.fire('childtestevent');
38 | expect(spy).toHaveBeenCalledTimes(0);
39 | });
40 |
41 | test("Should pass arguments to the callback", function() {
42 | var spy = jest.fn();
43 | var arg1 = 'arg1';
44 | var arg2 = 'arg2';
45 | var arg3 = 'arg3';
46 |
47 | viewToTest.on('childtestevent', spy);
48 | viewToTest.fire('childtestevent', arg1, arg2, arg3);
49 |
50 | expect(spy).toHaveBeenCalledWith(arg1, arg2, arg3);
51 | });
52 |
53 | test("Should allow multiple events to be in progress on the same view", function() {
54 | var layout = viewToTest;
55 | var apple = layout.module('apple');
56 | var event;
57 |
58 | apple.on('testevent1', function() {
59 | event = this.event;
60 | expect(this.event.target).toBe(apple);
61 | });
62 |
63 | apple.on('testevent1', function() {
64 | expect(event).toBe(this.event);
65 | apple.fire('testevent2');
66 | });
67 |
68 | apple.on('testevent2', function() {
69 | expect(event).not.toBe(this.event);
70 | expect(this.event.target).toBe(apple);
71 | });
72 |
73 | apple.fire('testevent1');
74 | });
75 |
76 | afterEach(function() {
77 | helpers.destroyView();
78 | viewToTest = null;
79 | });
80 | });
81 |
--------------------------------------------------------------------------------
/test/tests/helpers.js:
--------------------------------------------------------------------------------
1 |
2 | describe('fruitmachine#helpers()', function() {
3 | var testHelper;
4 |
5 | beforeEach(function() {
6 | testHelper = function(view) {
7 | view.on('before initialize', testHelper.beforeInitialize);
8 | view.on('initialize', testHelper.initialize);
9 | view.on('setup', testHelper.setup);
10 | view.on('teardown', testHelper.teardown);
11 | view.on('destroy', testHelper.destroy);
12 | };
13 |
14 | testHelper.beforeInitialize = jest.fn();
15 | testHelper.initialize = jest.fn();
16 | testHelper.setup = jest.fn();
17 | testHelper.teardown = jest.fn();
18 | testHelper.destroy = jest.fn();
19 | });
20 |
21 | test("helpers `before initialize` and `initialize` should have been called, in that order", function() {
22 | var view = fruitmachine({
23 | module: 'apple',
24 | helpers: [testHelper]
25 | });
26 |
27 | expect(testHelper.initialize).toHaveBeenCalled();
28 | expect(testHelper.beforeInitialize).toHaveBeenCalled();
29 | expect(testHelper.initialize.mock.invocationCallOrder).toEqual([2]);
30 | expect(testHelper.beforeInitialize.mock.invocationCallOrder).toEqual([1]);
31 | expect(testHelper.setup).toHaveBeenCalledTimes(0);
32 | expect(testHelper.teardown).toHaveBeenCalledTimes(0);
33 | expect(testHelper.destroy).toHaveBeenCalledTimes(0);
34 | });
35 |
36 | test("helper `setup` should have been called", function() {
37 | var view = fruitmachine({
38 | module: 'apple',
39 | helpers: [testHelper]
40 | });
41 |
42 | expect(testHelper.setup).toHaveBeenCalledTimes(0);
43 |
44 | view
45 | .render()
46 | .inject(sandbox)
47 | .setup();
48 |
49 | expect(testHelper.setup).toHaveBeenCalledTimes(1);
50 | });
51 |
52 | test("helper `teardown` and `destroy` should have been called", function() {
53 | var view = fruitmachine({
54 | module: 'apple',
55 | helpers: [testHelper]
56 | });
57 |
58 | view
59 | .render()
60 | .inject(sandbox)
61 | .setup()
62 | .teardown()
63 | .destroy();
64 |
65 | expect(testHelper.teardown).toHaveBeenCalled();
66 | expect(testHelper.destroy).toHaveBeenCalled();
67 | });
68 | });
69 |
--------------------------------------------------------------------------------
/docs/module-el.md:
--------------------------------------------------------------------------------
1 | ## The Module's Element
2 |
3 | Each module has a 'root' element (`myModule.el`). This is the single element that wraps the contents of a module. It is your handle on the module once it has been [injected](view-injection.md) into the DOM. You may be familiar with the `.el` concept from [Backbone](http://backbonejs.org/). In *FruitMachine* it is similar, but due to the 'DOM free' nested rendering techniques used, the `myModule.el` is not always accessible.
4 |
5 | In FruitMachine the `.el` property of a module is populated when `view.render()` is called.
6 |
7 | **NOTE:** As a safety measure we do not setup modules when a module's element could not be found. This means that `myModule.el` related setup logic wont error when `myModule.el` is `undefined`.
8 |
9 | #### Some examples
10 |
11 | 1.1. After render `module.el` will be defined
12 |
13 | ```js
14 | var apple = new Apple();
15 | var orange = new Orange();
16 |
17 | apple
18 | .add(orange)
19 | .render();
20 |
21 | apple.el
22 | //=> "[object HTMLDivElement]"
23 |
24 | orange.el
25 | //=> "[object HTMLDivElement]"
26 | ```
27 |
28 | 1.2. Without calling `.render()` no module elements are set.
29 |
30 | ```js
31 | var apple = new Apple();
32 | var orange = new Orange();
33 |
34 | apple.add(orange);
35 |
36 | apple.el
37 | //=> undefined
38 |
39 | orange.el
40 | //=> undefined
41 | ```
42 |
43 | ### FAQ
44 |
45 | #### Why is my module.el property undefined after .render()?
46 |
47 | The child module markup has failed to template into the parent module correctly. Check your child ids and parent markup to check they match up. See [template markup](view-template-markup.md).
48 |
49 | #### How are module root elements found?
50 |
51 | Internally each module has a private unique id (`myModule._fmid`). When the root element is templated, the html `id` attribute value is set to the `myModule._fmid`. When `.render()` is called on a module the HTML is templated and turned into a 'real' element in memory. We store this element and then search for descendant elements by id using `querySelector`. Server-side rendered modules being inflated on the client pick up their root element from the DOM when `.setup()` is called using `document.getElementById(module._fmid)` (super fast).
52 |
--------------------------------------------------------------------------------
/test/tests/define.js:
--------------------------------------------------------------------------------
1 |
2 | describe('fruitmachine.define()', function() {
3 | test("Should store the module in fruitmachine.store under module type", function() {
4 | fruitmachine.define({ module: 'my-module-1' });
5 | expect(fruitmachine.modules['my-module-1']).toBeDefined();
6 | });
7 |
8 | test("Should return an instantiable constructor", function() {
9 | var View = fruitmachine.define({ module: 'new-module-1' });
10 | var view = new View();
11 |
12 | expect(view._fmid).toBeDefined();
13 | expect(view.module()).toBe('new-module-1');
14 | });
15 |
16 | test("Should find module from internal module store if a `module` parameter is passed", function() {
17 | var apple = new fruitmachine({ module: 'apple' });
18 |
19 | expect(apple.module()).toBe('apple');
20 | expect(apple.template).toBeDefined();
21 | });
22 |
23 | test("Not defining reserved methods should not rewrite keys with prefixed with '_'", function() {
24 | var View = fruitmachine.define({
25 | module: 'foobar',
26 | });
27 |
28 | expect(View.prototype._setup).toBeUndefined();
29 | });
30 |
31 | test("Should be able to accept a Module class, so that a Module can be defined from extended modules", function() {
32 | var initialize1 = jest.fn();
33 | var initialize2 = jest.fn();
34 | var setup1 = jest.fn();
35 | var setup2 = jest.fn();
36 |
37 | var View1 = fruitmachine.define({
38 | module: 'new-module-1',
39 | random: 'prop',
40 | template: helpers.templates.apple,
41 | initialize: initialize1,
42 | setup: setup1
43 | });
44 |
45 | var View2 = fruitmachine.define(View1.extend({
46 | module: 'new-module-2',
47 | random: 'different',
48 | initialize: initialize2,
49 | setup: setup2
50 | }));
51 |
52 | var view1 = new View1()
53 | .render()
54 | .setup();
55 |
56 | var view2 = new View2()
57 | .render()
58 | .setup();
59 |
60 |
61 | expect(View1.prototype._module).toBe('new-module-1');
62 | expect(View2.prototype._module).toBe('new-module-2');
63 | expect(View2.prototype.random).toBe('different');
64 | expect(initialize1.mock.calls.length).toBe(1);
65 | expect(initialize2.mock.calls.length).toBe(1);
66 | expect(setup1.mock.calls.length).toBe(1);
67 | expect(setup2.mock.calls.length).toBe(1);
68 | });
69 | });
70 |
--------------------------------------------------------------------------------
/docs/defining-modules.md:
--------------------------------------------------------------------------------
1 | # Defining Modules
2 |
3 | ```js
4 | var Apple = fruitmachine.define({
5 | name: 'apple',
6 | template: templateFunction,
7 | tag: 'section',
8 | classes: ['class-1', 'class-2'],
9 |
10 | // Event callbacks (optional)
11 | initialize: function(options){},
12 | setup: function(){},
13 | mount: function(){},
14 | teardown: function(){},
15 | destroy: function(){}
16 | });
17 | ```
18 |
19 | Define does two things:
20 |
21 | - It registers a module internally for [Lazy](module-instantiation.md#lazy) module instantiation
22 | - It returns a constructor that can be [Explicitly](module-instantiation.md#explicit) instantiated.
23 |
24 | Internally `define` extends the default `fruitmachine.Module.prototype` with the parameters you define. Many of these parameters can be overwritten in the options passed to the constructor on a per instance basis. It is important you don't declare any parameters that conflict with `fruitmachine.Module.prototype` core API (check the [source]() if you are unsure).
25 |
26 | ### Options
27 |
28 | - `name {String}` Your name for this module.
29 | - `template {Function}` A function that will return the module's html (we like [Hogan](http://twitter.github.com/hogan.js/))
30 | - `tag {String}` The html tag to use on the root element (defaults to 'div') *(optional)*
31 | - `classes {Array}` A list of classes to add to the root element. *(optional)*
32 | - `initialize {Function}` Define a function to run when the module is first instantiated (only ever runs once) *(optional)*
33 | - `setup {Function}` A function to be run every time `Module#setup()` is called. You can safely assume the presence of `this.el` at this point; however, this element is not guaranteed to exist or be associated with the module in the future, for example if the module's parent is re-rendered. *(optional)*
34 | - `mount {Function}` A function to be run every time `Module#mount()` is called, i.e. when the module has been associated with a new DOM element. Should be used to bind any DOM event listeners. *(optional)*
35 | - `teardown {Function}` A function to be run when `Module#teardown()` or `Module#destroy()` is called. `teardown` will also run if you attempt to setup an already 'setup' module.
36 | - `destroy {Function}` Run when `Module#destroy()` is called (will only ever run once) *(optional)*
37 |
--------------------------------------------------------------------------------
/docs/module-helpers.md:
--------------------------------------------------------------------------------
1 | ## Helpers
2 |
3 | Helpers are small reusable plug-ins that you can write to add extra features to a View module ([working example](http://ftlabs.github.io/fruitmachine/examples/helpers)).
4 |
5 | ### Defining helpers
6 |
7 | A helper is simply a function accepting the View module instance as the first argument. The helper can listen to events on the View module and bolt functionality onto the view.
8 |
9 | Helpers should clear up after themselves. For example if they create variables or bind to events on `setup`, they should be unset and unbound on `teardown`.
10 |
11 | ```js
12 | var myHelper = function(module) {
13 |
14 | // Add functionality
15 | module.on('before setup', function() { /* 1 */
16 | module.sayName = function() {
17 | return 'My name is ' + module.name;
18 | };
19 | });
20 |
21 | // Tidy up
22 | module.on('teardown', function() {
23 | delete module.sayName;
24 | });
25 | };
26 | ```
27 |
28 | 1. *It is often useful to hook into the `before setup` event so that added functionality is available inside the module's `setup` function.*
29 |
30 | ### Attaching helpers
31 |
32 | At definition:
33 |
34 | ```js
35 | var Apple = fruitmachine.define({
36 | name: 'apple',
37 | helpers: [ myHelper ]
38 | });
39 | ```
40 |
41 | ...or instantiation:
42 |
43 | ```js
44 | var apple = new Apple({
45 | helpers: [ myHelper ]
46 | });
47 | ```
48 |
49 | ### Using features
50 |
51 | ```js
52 | apple.sayName();
53 | //=> 'My name is apple'
54 | ```
55 |
56 | ### Community Helpers ("Plugins")
57 |
58 | Helpers can be released as plugins, if you would like to submit your helper to this list [please raise an issue](https://github.com/ftlabs/fruitmachine/issues).
59 |
60 | - [fruitmachine-ftdomdelegate](https://github.com/ftlabs/fruitmachine-ftdomdelegate) provides [ftdomdelegate](https://github.com/ftlabs/ftdomdelegate) functionality within fruitmachine modules.
61 | - [fruitmachine-bindall](https://github.com/ftlabs/fruitmachine-bindall) automatically binds all the methods in a module to instances of that module.
62 | - [fruitmachine-media](https://github.com/ftlabs/fruitmachine-media) allows you to create responsive components. Set up media queries for different states and this plugin will allow you to hook into per state setup and teardown events when those media queries match.
63 |
--------------------------------------------------------------------------------
/test/tests/module.module.js:
--------------------------------------------------------------------------------
1 |
2 | describe('View#module()', function() {
3 | var viewToTest;
4 |
5 | beforeEach(function() {
6 | var layout = new Layout({});
7 | var apple = new Apple({ slot: 1 });
8 | var orange = new Orange({ slot: 2 });
9 | var pear = new Pear({ slot: 3 });
10 |
11 | layout
12 | .add(apple)
13 | .add(orange)
14 | .add(pear);
15 |
16 | viewToTest = layout;
17 | });
18 |
19 | test("Should return module type if no arguments given", function() {
20 | expect(viewToTest.module()).toBe('layout');
21 | });
22 |
23 | test("Should return the first child module with the specified type.", function() {
24 | var child = viewToTest.module('pear');
25 |
26 | expect(child).toBe(viewToTest.children[2]);
27 | });
28 |
29 | test("If there is more than one child of this module type, only the first is returned.", function() {
30 | viewToTest
31 | .add({ module: 'apple' });
32 |
33 | var child = viewToTest.module('apple');
34 | var firstChild = viewToTest.children[0];
35 | var lastChild = viewToTest.children[viewToTest.children.length-1];
36 |
37 | expect(child).toBe(firstChild);
38 | expect(child).not.toEqual(lastChild);
39 | });
40 |
41 | test("Should return the module name if defined with the name key", function() {
42 | var Henry = fruitmachine.define({ name: 'henry' });
43 | var henry = new Henry();
44 |
45 | expect(henry.module()).toBe('henry');
46 | expect(henry.name).toBe('henry');
47 | });
48 |
49 | test("Should walk down the fruitmachine tree, recursively", function() {
50 | var Elizabeth = fruitmachine.define({ name: 'elizabeth' });
51 | var elizabeth = new Elizabeth();
52 | viewToTest.module('apple').add(elizabeth);
53 |
54 | var elizabethInstance = viewToTest.module('elizabeth');
55 | expect(elizabethInstance.module()).toBe('elizabeth');
56 | expect(elizabethInstance.name).toBe('elizabeth');
57 | });
58 |
59 | test("Regression Test: Should still recurse even if the root view used to have a module of the same type", function() {
60 | var pear = viewToTest.module('pear').remove();
61 | viewToTest.module('apple').add(pear);
62 |
63 | var pearInstance = viewToTest.module('pear');
64 | expect(pearInstance.module()).toBe('pear');
65 | expect(pearInstance.name).toBe('pear');
66 | });
67 | });
68 |
--------------------------------------------------------------------------------
/test/tests/module.setup.js:
--------------------------------------------------------------------------------
1 |
2 | describe('View#setup()', function() {
3 | var viewToTest;
4 |
5 | beforeEach(function() {
6 | viewToTest = helpers.createView();
7 | });
8 |
9 | test("Setup should recurse.", function() {
10 | var setup = jest.spyOn(viewToTest.module('orange'), 'setup');
11 |
12 | viewToTest
13 | .render()
14 | .setup();
15 |
16 | expect(setup).toHaveBeenCalled();
17 | });
18 |
19 | test("Should not recurse if used with the `shallow` option.", function() {
20 | var setup = jest.spyOn(viewToTest.module('orange'), 'setup');
21 |
22 | viewToTest
23 | .render()
24 | .setup({ shallow: true });
25 |
26 | expect(setup).not.toHaveBeenCalled();
27 | });
28 |
29 | test("Custom `setup` logic should be called", function() {
30 | var setup = jest.spyOn(helpers.Views.Apple.prototype, 'setup');
31 | var apple = new helpers.Views.Apple();
32 |
33 | apple
34 | .render()
35 | .setup();
36 |
37 | expect(setup).toHaveBeenCalled();
38 | setup.mockReset();
39 | });
40 |
41 | test("Once setup, a View should be flagged as such.", function() {
42 | viewToTest
43 | .render()
44 | .setup();
45 |
46 | expect(viewToTest.isSetup).toBe(true);
47 | expect(viewToTest.module('orange').isSetup).toBe(true);
48 | });
49 |
50 | test("Custom `setup` logic should not be run if no root element is found.", function() {
51 | var setup = jest.spyOn(viewToTest, '_setup');
52 | var setup2 = jest.spyOn(viewToTest.module('orange'), '_setup');
53 |
54 | viewToTest
55 | .setup();
56 |
57 | // Check `onSetup` was not called
58 | expect(setup).not.toHaveBeenCalled();
59 | expect(setup2).not.toHaveBeenCalled();
60 |
61 | // Check the view hasn't been flagged as setup
62 | expect(viewToTest.isSetup).not.toBe(true);
63 | expect(viewToTest.module('orange').isSetup).not.toBe(true);
64 | });
65 |
66 | test("onTeardown should be called if `setup()` is called twice.", function() {
67 | var teardown = jest.spyOn(viewToTest, 'teardown');
68 | var teardown2 = jest.spyOn(viewToTest.module('orange'), 'teardown');
69 |
70 | //debugger;
71 | viewToTest
72 | .render()
73 | .inject(sandbox)
74 | .setup()
75 | .setup();
76 |
77 | expect(teardown).toHaveBeenCalled();
78 | expect(teardown2).toHaveBeenCalled();
79 | });
80 |
81 | afterEach(function() {
82 | helpers.destroyView();
83 | viewToTest = null;
84 | });
85 | });
86 |
--------------------------------------------------------------------------------
/docs/introduction.md:
--------------------------------------------------------------------------------
1 | ## Introduction
2 |
3 | FruitMachine is used to assemble nested views from defined modules. It can be used solely on the client, server (via Node), or both. Unlike other solutions, FruitMachine doesn't try to architect your application for you, it simply provides you with the tools to assemble and communicate with your view modules.
4 |
5 | #### What is a 'module'?
6 |
7 | When referring to a module we mean a reusable UI component. For example let's use the common 'tabbed container' component as an example module.
8 |
9 | Our tabbed container needs some markup, some styling and some basic JavaScript interactions. We might want to use this module in two different places within our app, but we don't want to have to write the markup, the styling or the interaction logic twice. When writing modular components we only have to write things once!
10 |
11 | #### What is a 'layout'?
12 |
13 | As far as FruitMachine is concerned there is no difference between layouts and modules, all modules are the same; they are a piece of the UI that has a template, maybe some interaction logic, and perhaps holds some child modules.
14 |
15 | When we talk about layout modules we are referring to the core page scaffolding; a module that usually fills the page, and defines gaps for other modules to sit in.
16 |
17 | #### Comparisons with the DOM
18 |
19 | A collection of FruitMachine modules is like a simplified DOM tree. Like elements, modules have properties, methods and can hold children. There is no limit to how deeply nested modules can be. When an event is fired on a module (`apple.fire('somethinghappened');`, it will bubble right to top of the structure, just like DOM events.
20 |
21 | #### What about my data/models?
22 |
23 | FruitMachine tries to stay as far away from your data as possible, but of course each module must have data associated with it, and FruitMachine must be able to drop this data into the module's template.
24 |
25 | FruitMachine comes with it's own Model class (`fruitmachine.Model`) out of the box, just in case you don't have you own; but we have built FruitMachine such that you can use your own types of Model should you wish. FruitMachine just requires you model to have a .`toJSON()` method so that it send its data into the module's template.
26 |
27 | #### What templating language does it use?
28 |
29 | FruitMachine doesn't care what type of templates you are using, it just expects to be given a function that will return a string. FruitMachine will pass any model data associated with the model as the first argument to this function. This means you can use any templates you like! We like to use [Hogan](http://twitter.github.io/hogan.js/).
30 |
--------------------------------------------------------------------------------
/test/tests/module.add.js:
--------------------------------------------------------------------------------
1 |
2 | describe('View#add()', function() {
3 | var viewToTest;
4 |
5 | beforeEach(function() {
6 | viewToTest = new helpers.Views.List();
7 | });
8 |
9 | test("Should throw when adding undefined module", function() {
10 | var thrown;
11 | try {
12 | viewToTest.add({module: 'invalidFruit'});
13 | } catch(e) {
14 | expect(e.message).toMatch('invalidFruit');
15 | thrown = true;
16 | }
17 | expect(thrown).toBe(true);
18 | });
19 |
20 | test("Should accept a View instance", function() {
21 | var pear = new helpers.Views.Pear();
22 | viewToTest.add(pear);
23 | expect(viewToTest.children.length).toBe(1);
24 | });
25 |
26 | test("Should store a reference to the child via slot if the view added has a slot", function() {
27 | var apple = new Apple({ slot: 1 });
28 | var layout = new Layout();
29 |
30 | layout.add(apple);
31 |
32 | expect(layout.slots[1]).toBe(apple);
33 | });
34 |
35 | test("Should aceept JSON", function() {
36 | viewToTest.add({ module: 'pear' });
37 | expect(viewToTest.children.length).toBe(1);
38 | });
39 |
40 | test("Should allow the second parameter to define the slot", function() {
41 | var apple = new Apple();
42 | var layout = new Layout();
43 |
44 | layout.add(apple, 1);
45 | expect(layout.slots[1]).toBe(apple);
46 | });
47 |
48 | test("Should be able to define the slot in the options object", function() {
49 | var apple = new Apple();
50 | var layout = new Layout();
51 |
52 | layout.add(apple, { slot: 1 });
53 | expect(layout.slots[1]).toBe(apple);
54 | });
55 |
56 | test("Should remove a module if it already occupies this slot", function() {
57 | var apple = new Apple();
58 | var orange = new Orange();
59 | var layout = new Layout();
60 |
61 | layout.add(apple, 1);
62 |
63 | expect(layout.slots[1]).toBe(apple);
64 |
65 | layout.add(orange, 1);
66 |
67 | expect(layout.slots[1]).toBe(orange);
68 | expect(layout.module('apple')).toBeUndefined();
69 | });
70 |
71 | test("Should remove the module if it already has parent before being added", function() {
72 | var apple = new Apple();
73 | var layout = new Layout();
74 | var spy = jest.spyOn(apple, 'remove');
75 |
76 | layout.add(apple, 1);
77 |
78 | expect(spy).toHaveBeenCalledTimes(0);
79 | expect(layout.slots[1]).toBe(apple);
80 |
81 | layout.add(apple, 2);
82 |
83 | expect(layout.slots[1]).not.toEqual(apple);
84 | expect(layout.slots[2]).toBe(apple);
85 | expect(spy).toHaveBeenCalled();
86 | });
87 |
88 | afterEach(function() {
89 | helpers.destroyView(viewToTest);
90 | viewToTest = null;
91 | });
92 | });
93 |
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | ## Getting started
2 |
3 | Let's start with a very simple example to demonstrate how to work with FruitMachine ([working example here](http://ftlabs.github.io/fruitmachine/examples/getting-started)):
4 |
5 | #### Define some modules
6 |
7 | ```js
8 | var Apple = fruitmachine.define({
9 | name: 'apple',
10 | template: function(){ return 'I am an apple'; } /* 1 */
11 | });
12 |
13 | var Layout = fruitmachine.define({
14 | name: 'layout',
15 | template: function(data){ return data.child1; } /* 1 */
16 | });
17 | ```
18 |
19 | 1. *For simplicity we are using plain functions for templating. These can be switched for more advanced templating functions (eg. Hogan/Mustache).*
20 |
21 | #### Assemble a view
22 |
23 | ```js
24 | var layout = new Layout();
25 | var apple = new Apple({ id: 'child1' }); /* 1 */
26 |
27 | layout.add(apple); /* 2 */
28 | ```
29 |
30 | 1. *Notice how we give this instance of apple an id. This is the module's identifier within this view, it should be unique. We use this id to print the child into the parent's template.*
31 | 2. *Here we add the apple view module as a child to the layout view module.*
32 |
33 | #### Rendering the view
34 |
35 | ```js
36 | layout.render(); /* 1 */
37 | ```
38 |
39 | 1. *At this point `layout.el` will be populated with a real DOM node*
40 |
41 | #### Injecting the view into the DOM
42 |
43 | ```js
44 | layout.inject(document.body); /* 1 */
45 | document.body.innerHTML; //=>
I am an apple
/* 2 */
46 | ```
47 |
48 | 1. *The contents of the element passed are replaced with the view module's element*
49 | 2. *You can see the `innerHTML` of the `` is now the generated markup of our view. Don't worry about the id attributes, they are generated by FruitMachine, and are used internally to retrieve views from the DOM.*
50 |
51 | ### Lazy Views
52 |
53 | FruitMachine was written for flexibility so there is usually more than one way to do something. In this case the above code could also be written like this:
54 |
55 | ```js
56 | var Apple = fruitmachine.define({
57 | name: 'apple',
58 | template: function(){ return 'I am an apple' } /* 1 */
59 | });
60 |
61 | var Layout = fruitmachine.define({
62 | name: 'layout',
63 | template: function(data){ return data.child1 } /* 1 */
64 | });
65 | ```
66 |
67 |
68 | ```js
69 | var layout = fruitmachine({
70 | module: 'layout', /* 1 */
71 | children: [
72 | {
73 | name: 'apple', /* 1 */
74 | id: 'child1'
75 | }
76 | ]
77 | });
78 |
79 | layout
80 | .render()
81 | .inject(document.body);
82 | ```
83 |
84 | 1. *Because we have defined modules under these names, FruitMachine is able to instantiate them internally.*
85 |
86 | It is useful to be able to assemble views in this way as it means that you can predefine your layouts as simple JSON, letting FruitMachine do the hard work. For some parts of your application this may not be preferable. In the FT Web App we use a combination of the two techniques.
87 |
--------------------------------------------------------------------------------
/test/helpers.js:
--------------------------------------------------------------------------------
1 | var Hogan = require('hogan.js');
2 | var fruitmachine = require('../lib/');
3 |
4 | var helpers = {};
5 |
6 | /**
7 | * Templates
8 | */
9 |
10 | var templates = helpers.templates = {
11 | 'apple': Hogan.compile('{{{1}}}'),
12 | 'layout': Hogan.compile('{{{1}}}{{{2}}}{{{3}}}'),
13 | 'list': Hogan.compile('{{#children}}{{{child}}}{{/children}}'),
14 | 'orange': Hogan.compile('{{text}}'),
15 | 'pear': Hogan.compile('{{text}}')
16 | };
17 |
18 | /**
19 | * Module Definitions
20 | */
21 |
22 | helpers.Views = {};
23 |
24 | var Layout = helpers.Views.Layout = fruitmachine.define({
25 | name: 'layout',
26 | template: templates.layout,
27 |
28 | initialize: function() {},
29 | setup: function() {},
30 | teardown: function() {},
31 | destroy: function() {}
32 | });
33 |
34 | var Apple = helpers.Views.Apple = fruitmachine.define({
35 | name: 'apple',
36 | template: templates.apple,
37 |
38 | initialize: function() {},
39 | setup: function() {},
40 | teardown: function() {},
41 | destroy: function() {}
42 | });
43 |
44 | var List = helpers.Views.List = fruitmachine.define({
45 | name: 'list',
46 | template: templates.list,
47 |
48 | initialize: function() {},
49 | setup: function() {},
50 | teardown: function() {},
51 | destroy: function() {}
52 | });
53 |
54 | var Orange = helpers.Views.Orange = fruitmachine.define({
55 | name: 'orange',
56 | template: templates.orange,
57 |
58 | initialize: function() {},
59 | setup: function() {},
60 | teardown: function() {},
61 | destroy: function() {}
62 | });
63 |
64 | var Pear = helpers.Views.Pear = fruitmachine.define({
65 | name: 'pear',
66 | template: templates.pear,
67 |
68 | initialize: function() {},
69 | setup: function() {},
70 | teardown: function() {},
71 | destroy: function() {}
72 | });
73 |
74 | /**
75 | * Create View
76 | */
77 |
78 | helpers.createView = function() {
79 | var layout = new Layout();
80 | var apple = new Apple({ slot: 1 });
81 | var orange = new Orange({ slot: 2 });
82 | var pear = new Pear({ slot: 3 });
83 |
84 | layout
85 | .add(apple)
86 | .add(orange)
87 | .add(pear);
88 |
89 | return this.view = layout;
90 | };
91 |
92 | /**
93 | * Destroy View
94 | */
95 |
96 | helpers.destroyView = function(view) {
97 | var viewToDestroy = this.view || view;
98 | viewToDestroy.destroy();
99 | this.view = null;
100 | };
101 |
102 | /**
103 | * Sandbox
104 | */
105 |
106 | helpers.createSandbox = function() {
107 | var el = document.createElement('div');
108 | return document.body.appendChild(el);
109 | };
110 |
111 | helpers.emptySandbox = function() {
112 | sandbox.innerHTML = '';
113 | };
114 |
115 | var sandbox = helpers.createSandbox();
116 |
117 | global.fruitmachine = fruitmachine;
118 | global.helpers = helpers;
119 | global.sandbox = sandbox;
120 | global.Layout = Layout;
121 | global.Apple = Apple;
122 | global.List = List;
123 | global.Orange = Orange;
124 | global.Pear = Pear;
125 |
--------------------------------------------------------------------------------
/test/tests/module.destroy.js:
--------------------------------------------------------------------------------
1 |
2 | describe('View#destroy()', function() {
3 | var viewToTest;
4 |
5 | beforeEach(function() {
6 | viewToTest = helpers.createView();
7 | });
8 |
9 | test("Should recurse.", function() {
10 | var destroy = jest.spyOn(viewToTest, 'destroy');
11 | var destroy2 = jest.spyOn(viewToTest.module('orange'), 'destroy');
12 |
13 | viewToTest
14 | .render()
15 | .inject(sandbox)
16 | .setup();
17 |
18 | viewToTest.destroy();
19 |
20 | expect(destroy).toHaveBeenCalled();
21 | expect(destroy2).toHaveBeenCalled();
22 | });
23 |
24 | test("Should call teardown once per view.", function() {
25 | var teardown1 = jest.spyOn(viewToTest, 'teardown');
26 | var teardown2 = jest.spyOn(viewToTest.module('orange'), 'teardown');
27 |
28 | viewToTest
29 | .render()
30 | .inject(sandbox)
31 | .setup()
32 | .destroy();
33 |
34 | expect(teardown1).toHaveBeenCalledTimes(1)
35 | expect(teardown2).toHaveBeenCalledTimes(1)
36 | });
37 |
38 | test("Should remove only the first view element from the DOM.", function() {
39 | var layout = viewToTest;
40 | var orange = viewToTest.module('orange');
41 |
42 | layout
43 | .render()
44 | .inject(sandbox)
45 | .setup();
46 |
47 | var layoutRemoveChild = jest.spyOn(layout.el.parentNode, 'removeChild');
48 | var orangeRemoveChild = jest.spyOn(orange.el.parentNode, 'removeChild');
49 |
50 | viewToTest.destroy();
51 |
52 | expect(layoutRemoveChild).toHaveBeenCalledTimes(1)
53 | expect(orangeRemoveChild).toHaveBeenCalledTimes(0)
54 |
55 | layoutRemoveChild.mockRestore();
56 | orangeRemoveChild.mockRestore();
57 | });
58 |
59 | test("Should fire `destroy` event.", function() {
60 | var spy = jest.fn();
61 |
62 | viewToTest.on('destroy', spy);
63 |
64 | viewToTest
65 | .render()
66 | .inject(sandbox)
67 | .setup()
68 | .destroy();
69 |
70 | expect(spy).toHaveBeenCalled();
71 | });
72 |
73 | test("Should unbind all event listeners.", function() {
74 | var eventSpy = jest.spyOn(viewToTest, 'off');
75 |
76 | viewToTest
77 | .render()
78 | .inject(sandbox)
79 | .setup()
80 | .destroy();
81 |
82 | expect(eventSpy).toHaveBeenCalled();
83 | });
84 |
85 | test("Should flag the view as 'destroyed'.", function() {
86 | viewToTest
87 | .render()
88 | .inject(sandbox)
89 | .setup()
90 | .destroy();
91 |
92 | expect(viewToTest.destroyed).toBe(true);
93 | });
94 |
95 | test("Should unset primary properties.", function() {
96 | viewToTest
97 | .render()
98 | .inject(sandbox)
99 | .setup()
100 | .destroy();
101 |
102 | expect(viewToTest.el).toBeNull()
103 | expect(viewToTest.model).toBeNull()
104 | expect(viewToTest.parent).toBeNull()
105 | expect(viewToTest._id).toBeNull()
106 | });
107 |
108 | afterEach(function() {
109 | helpers.destroyView();
110 | viewToTest = null;
111 | });
112 | });
113 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FruitMachine [](https://travis-ci.com/ftlabs/fruitmachine) [](https://coveralls.io/r/ftlabs/fruitmachine)
2 |
3 | A lightweight component layout engine for client and server.
4 |
5 | FruitMachine is designed to build rich interactive layouts from modular, reusable components. It's light and unopinionated so that it can be applied to almost any layout problem. FruitMachine is currently powering the [FT Web App](http://apps.ft.com/ftwebapp/).
6 |
7 | ```js
8 | // Define a module
9 | var Apple = fruitmachine.define({
10 | name: 'apple',
11 | template: function(){ return 'hello' }
12 | });
13 |
14 | // Create a module
15 | var apple = new Apple();
16 |
17 | // Render it
18 | apple.render();
19 |
20 | apple.el.outerHTML;
21 | //=>
hello
22 | ```
23 |
24 | ## Installation
25 |
26 | ```
27 | $ npm install fruitmachine
28 | ```
29 |
30 | or
31 |
32 | ```
33 | $ bower install fruitmachine
34 | ```
35 |
36 | or
37 |
38 | Download the [pre-built version][built] (~2k gzipped).
39 |
40 | [built]: http://wzrd.in/standalone/fruitmachine@latest
41 |
42 | ## Examples
43 |
44 | - [Article viewer](http://ftlabs.github.io/fruitmachine/examples/article-viewer/)
45 | - [TODO](http://ftlabs.github.io/fruitmachine/examples/todo/)
46 |
47 | ## Documentation
48 |
49 | - [Introduction](docs/introduction.md)
50 | - [Getting started](docs/getting-started.md)
51 | - [Defining modules](docs/defining-modules.md)
52 | - [Slots](docs/slots.md)
53 | - [View assembly](docs/layout-assembly.md)
54 | - [Instantiation](docs/module-instantiation.md)
55 | - [Templates](docs/templates.md)
56 | - [Template markup](docs/template-markup.md)
57 | - [Rendering](docs/rendering.md)
58 | - [DOM injection](docs/injection.md)
59 | - [The module element](docs/module-el.md)
60 | - [Queries](docs/queries.md)
61 | - [Helpers](docs/module-helpers.md)
62 | - [Removing & destroying](docs/removing-and-destroying.md)
63 | - [Extending](docs/extending-modules.md)
64 | - [Server-side rendering](docs/server-side-rendering.md)
65 | - [API](docs/api.md)
66 | - [Events](docs/events.md)
67 |
68 | ## Tests
69 |
70 | #### With PhantomJS
71 |
72 | ```
73 | $ npm install
74 | $ npm test
75 | ```
76 |
77 | #### Without PhantomJS
78 |
79 | ```
80 | $ node_modules/.bin/buster-static
81 | ```
82 |
83 | ...then visit http://localhost:8282/ in browser
84 |
85 | ## Author
86 |
87 | - **Wilson Page** - [@wilsonpage](http://github.com/wilsonpage)
88 |
89 | ## Contributors
90 |
91 | - **Wilson Page** - [@wilsonpage](http://github.com/wilsonpage)
92 | - **Matt Andrews** - [@matthew-andrews](http://github.com/matthew-andrews)
93 |
94 | ## License
95 | Copyright (c) 2018 The Financial Times Limited
96 | Licensed under the MIT license.
97 |
98 | ## Credits and collaboration
99 | FruitMachine is largely unmaintained/finished. All open source code released by FT Labs is licenced under the MIT licence. We welcome comments, feedback and suggestions. Please feel free to raise an issue or pull request.
100 |
--------------------------------------------------------------------------------
/test/tests/module.mount.js:
--------------------------------------------------------------------------------
1 |
2 | describe('View#mount()', function() {
3 | var viewToTest;
4 |
5 | beforeEach(function() {
6 | viewToTest = helpers.createView();
7 | });
8 |
9 | test("Should give a view an element", function() {
10 | var el = document.createElement('div');
11 | viewToTest.mount(el);
12 |
13 | expect(viewToTest.el).toBe(el);
14 | });
15 |
16 | test("Should be called when the view is rendered", function() {
17 | var mount = jest.spyOn(viewToTest, 'mount');
18 | viewToTest.render();
19 | expect(mount).toHaveBeenCalled();
20 | });
21 |
22 | test("Should be called on a child when its parent is rendered", function() {
23 | var mount = jest.spyOn(viewToTest.module('apple'), 'mount');
24 | viewToTest.render();
25 | expect(mount).toHaveBeenCalled();
26 | });
27 |
28 | test("Should be called on a child when its parent is rerendered", function() {
29 | var mount = jest.spyOn(viewToTest.module('apple'), 'mount');
30 | viewToTest.render();
31 | viewToTest.render();
32 | expect(mount).toHaveBeenCalledTimes(2);
33 | });
34 |
35 | test("Should call custom mount logic", function() {
36 | var mount = jest.fn();
37 |
38 | var Module = fruitmachine.define({
39 | name: 'module',
40 | template: function() {
41 | return 'hello';
42 | },
43 |
44 | mount: mount
45 | });
46 |
47 | var m = new Module();
48 | m.render();
49 |
50 | expect(mount).toHaveBeenCalled();
51 | });
52 |
53 | test("Should be a good place to attach event handlers that don't get trashed on parent rerender", function() {
54 | var handler = jest.fn();
55 |
56 | var Module = fruitmachine.define({
57 | name: 'module',
58 | tag: 'button',
59 | template: function() {
60 | return 'hello';
61 | },
62 |
63 | mount: function() {
64 | this.el.addEventListener('click', handler);
65 | }
66 | });
67 |
68 | var m = new Module();
69 |
70 | var layout = new Layout({
71 | children: {
72 | 1: m
73 | }
74 | });
75 |
76 | layout.render();
77 | m.el.click();
78 |
79 | expect(handler).toHaveBeenCalledTimes(1);
80 |
81 | layout.render();
82 | m.el.click();
83 |
84 | expect(handler).toHaveBeenCalledTimes(2);
85 | });
86 |
87 | test("before mount and mount events should be fired", function() {
88 | var beforeMountSpy = jest.fn();
89 | var mountSpy = jest.fn();
90 | viewToTest.on('before mount', beforeMountSpy);
91 | viewToTest.on('mount', mountSpy);
92 |
93 | viewToTest.render();
94 | expect(beforeMountSpy.mock.invocationCallOrder[0]).toBeLessThan(mountSpy.mock.invocationCallOrder[0]);
95 | });
96 |
97 | test("Should only fire events if the element is new", function() {
98 | var mountSpy = jest.fn();
99 | viewToTest.on('mount', mountSpy);
100 |
101 | viewToTest.render();
102 | viewToTest._getEl();
103 | expect(mountSpy).toHaveBeenCalledTimes(1)
104 | });
105 |
106 | afterEach(function() {
107 | helpers.destroyView();
108 | viewToTest = null;
109 | });
110 | });
111 |
--------------------------------------------------------------------------------
/lib/module/events.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * Module Dependencies
4 | */
5 |
6 | var events = require('evt');
7 |
8 | /**
9 | * Local vars
10 | */
11 |
12 | var listenerMap = {};
13 |
14 | /**
15 | * Registers a event listener.
16 | *
17 | * @param {String} name
18 | * @param {String} module
19 | * @param {Function} cb
20 | * @return {View}
21 | */
22 | exports.on = function(name, module, cb) {
23 | var l;
24 |
25 | // cb can be passed as
26 | // the second or third argument
27 | if (typeof module !== 'string') {
28 | cb = module;
29 | module = null;
30 | }
31 |
32 | // if a module is provided
33 | // pass in a special callback
34 | // function that checks the
35 | // module
36 | if (module) {
37 | if (!listenerMap[name]) listenerMap[name] = [];
38 | l = listenerMap[name].push({
39 | orig: cb,
40 | cb: function() {
41 | if (this.event.target.module() === module) {
42 | cb.apply(this, arguments);
43 | }
44 | }
45 | });
46 | events.prototype.on.call(this, name, listenerMap[name][l-1].cb);
47 | } else {
48 | events.prototype.on.call(this, name, cb);
49 | }
50 |
51 | return this;
52 | };
53 |
54 | /**
55 | * Unregisters a event listener.
56 | *
57 | * @param {String} name
58 | * @param {String} module
59 | * @param {Function} cb
60 | * @return {View}
61 | */
62 | exports.off = function(name, module, cb) {
63 |
64 | // cb can be passed as
65 | // the second or third argument
66 | if (typeof module !== 'string') {
67 | cb = module;
68 | module = null;
69 | }
70 |
71 | if (listenerMap[name]) {
72 | listenerMap[name] = listenerMap[name].filter(function(map) {
73 |
74 | // If a callback provided, keep it
75 | // in the listener map if it doesn't match
76 | if (cb && map.orig !== cb) {
77 | return true;
78 |
79 | // Otherwise remove it from the listener
80 | // map and unbind the event listener
81 | } else {
82 | events.prototype.off.call(this, name, map.cb);
83 | return false;
84 | }
85 | }, this);
86 | }
87 | if (!module) {
88 | events.prototype.off.call(this, name, cb);
89 | }
90 |
91 | return this;
92 | };
93 |
94 | /**
95 | * Fires an event on a view.
96 | *
97 | * @param {String} name
98 | * @return {View}
99 | */
100 | exports.fire = function(name) {
101 | var _event = this.event;
102 | var event = {
103 | target: this,
104 | propagate: true,
105 | stopPropagation: function(){ this.propagate = false; }
106 | };
107 |
108 | propagate(this, arguments, event);
109 |
110 | // COMPLEX:
111 | // If an earlier event object was
112 | // cached, restore the the event
113 | // back onto the view. If there
114 | // wasn't an earlier event, make
115 | // sure the `event` key has been
116 | // deleted off the view.
117 | if (_event) this.event = _event;
118 | else delete this.event;
119 |
120 | // Allow chaining
121 | return this;
122 | };
123 |
124 | function propagate(view, args, event) {
125 | if (!view || !event.propagate) return;
126 |
127 | view.event = event;
128 | events.prototype.fire.apply(view, args);
129 | propagate(view.parent, args, event);
130 | }
131 |
132 | exports.fireStatic = events.prototype.fire;
133 |
--------------------------------------------------------------------------------
/docs/templates/readme.hogan:
--------------------------------------------------------------------------------
1 | # {{pkg.title}} [](https://travis-ci.org/ftlabs/fruitmachine) [](https://coveralls.io/r/ftlabs/fruitmachine?branch=master) [](https://gemnasium.com/ftlabs/fruitmachine)
2 |
3 | {{pkg.description}}
4 |
5 | FruitMachine is designed to build rich interactive layouts from modular, reusable components. It's light and unopinionated so that it can be applied to almost any layout problem. FruitMachine is currently powering the [FT Web App](http://apps.ft.com/ftwebapp/).
6 |
7 | ```js
8 | // Define a module
9 | var Apple = fruitmachine.define({
10 | name: 'apple',
11 | template: function(){ return 'hello' }
12 | });
13 |
14 | // Create a module
15 | var apple = new Apple();
16 |
17 | // Render it
18 | apple.render();
19 |
20 | apple.el.outerHTML;
21 | //=>
hello
22 | ```
23 |
24 | ## Installation
25 |
26 | ```
27 | $ npm install fruitmachine
28 | ```
29 |
30 | or
31 |
32 | ```
33 | $ bower install fruitmachine
34 | ```
35 |
36 | or
37 |
38 | Download the [pre-built version][built] (~2k gzipped).
39 |
40 | [built]: http://wzrd.in/standalone/fruitmachine@latest
41 |
42 | ## Examples
43 |
44 | - [Article viewer](http://ftlabs.github.io/fruitmachine/examples/article-viewer/)
45 | - [TODO](http://ftlabs.github.io/fruitmachine/examples/todo/)
46 |
47 | ## Documentation
48 |
49 | - [Introduction](docs/introduction.md)
50 | - [Getting started](docs/getting-started.md)
51 | - [Defining modules](docs/defining-modules.md)
52 | - [Slots](docs/slots.md)
53 | - [View assembly](docs/layout-assembly.md)
54 | - [Instantiation](docs/module-instantiation.md)
55 | - [Templates](docs/templates.md)
56 | - [Template markup](docs/template-markup.md)
57 | - [Rendering](docs/rendering.md)
58 | - [DOM injection](docs/injection.md)
59 | - [The module element](docs/module-el.md)
60 | - [Queries](docs/queries.md)
61 | - [Helpers](docs/module-helpers.md)
62 | - [Removing & destroying](docs/removing-and-destroying.md)
63 | - [Extending](docs/extending-modules.md)
64 | - [Server-side rendering](docs/server-side-rendering.md)
65 | - [API](docs/api.md)
66 | - [Events](docs/events.md)
67 |
68 | ## Tests
69 |
70 | #### With PhantomJS
71 |
72 | ```
73 | $ npm install
74 | $ npm test
75 | ```
76 |
77 | #### Without PhantomJS
78 |
79 | ```
80 | $ node_modules/.bin/buster-static
81 | ```
82 |
83 | ...then visit http://localhost:8282/ in browser
84 |
85 | ## Author
86 |
87 | {{#pkg.author}}
88 | - **{{name}}** - [@{{github}}](http://github.com/{{github}})
89 | {{/pkg.author}}
90 |
91 | ## Contributors
92 |
93 | {{#pkg.contributors}}
94 | - **{{name}}** - [@{{github}}](http://github.com/{{github}})
95 | {{/pkg.contributors}}
96 |
97 | ## License
98 | Copyright (c) 2014 {{pkg.organization}}
99 | Licensed under the MIT license.
100 |
101 | ## Credits and collaboration
102 |
103 | The lead developer of {{pkg.title}} is [Wilson Page](http://github.com/wilsonpage) at FT Labs. All open source code released by FT Labs is licenced under the MIT licence. We welcome comments, feedback and suggestions. Please feel free to raise an issue or pull request.
104 |
--------------------------------------------------------------------------------
/test/tests/module.on.js:
--------------------------------------------------------------------------------
1 |
2 | describe('View#on()', function() {
3 | var viewToTest;
4 |
5 | beforeEach(function() {
6 | viewToTest = helpers.createView();
7 | });
8 |
9 | test("Should recieve the callback when fire is called directly on a view", function() {
10 | var spy = jest.fn();
11 |
12 | viewToTest.on('testevent', spy);
13 | viewToTest.fire('testevent');
14 | expect(spy).toHaveBeenCalled();
15 | });
16 |
17 | test("Should recieve the callback when event is fired on a sub view", function() {
18 | var spy = jest.fn();
19 | var apple = viewToTest.module('apple');
20 |
21 | viewToTest.on('testevent', spy);
22 | apple.fire('testevent');
23 | expect(spy).toHaveBeenCalled();
24 | });
25 |
26 | test("Should *not* recieve the callback when event is fired on a sub view that *doesn't* match the target", function() {
27 | var spy = jest.fn();
28 | var apple = viewToTest.module('apple');
29 |
30 | viewToTest.on('testevent', 'orange', spy);
31 | apple.fire('testevent');
32 | expect(spy).not.toHaveBeenCalled();
33 | });
34 |
35 | test("Should receive the callback when event is fired on a sub view that *does* match the target", function() {
36 | var spy = jest.fn();
37 | var apple = viewToTest.module('apple');
38 |
39 | viewToTest.on('testevent', 'apple', spy);
40 | apple.fire('testevent');
41 | expect(spy).toHaveBeenCalled();
42 | });
43 |
44 | test("Should pass the correct arguments to delegate event listeners", function() {
45 | var spy = jest.fn();
46 | var apple = viewToTest.module('apple');
47 |
48 | viewToTest.on('testevent', 'apple', spy);
49 | apple.fire('testevent', 'foo', 'bar');
50 | expect(spy).toHaveBeenCalledWith('foo', 'bar');
51 | });
52 |
53 | test("Should be able to unbind event listeners if initially bound with module name", function() {
54 | var spy = jest.fn();
55 | var apple = viewToTest.module('apple');
56 |
57 | viewToTest.on('testevent', 'apple', spy);
58 | apple.fire('testevent');
59 | expect(spy).toHaveBeenCalled();
60 |
61 | spy.mockClear();
62 | viewToTest.off('testevent', 'apple', spy);
63 | apple.fire('testevent', 'foo', 'bar');
64 | expect(spy).not.toHaveBeenCalled();
65 | });
66 |
67 | test("#off with module will unbind all matching listeners, regardless of how they are bound", function() {
68 | var spy = jest.fn();
69 | var spy2 = jest.fn();
70 | var apple = viewToTest.module('apple');
71 |
72 | viewToTest.on('testevent', 'apple', spy);
73 | viewToTest.on('testevent', spy2);
74 | apple.fire('testevent');
75 | expect(spy).toHaveBeenCalled();
76 | expect(spy2).toHaveBeenCalled();
77 |
78 | spy.mockClear();
79 | spy2.mockClear();
80 | viewToTest.off('testevent', 'apple');
81 | apple.fire('testevent');
82 | expect(spy).not.toHaveBeenCalled();
83 | expect(spy2).toHaveBeenCalled();
84 | });
85 |
86 | test("#off without a module should also unbind listeners, regardless of how they are bound", function() {
87 | var spy = jest.fn();
88 | var apple = viewToTest.module('apple');
89 |
90 | viewToTest.on('testevent', 'apple', spy);
91 | apple.fire('testevent');
92 | expect(spy).toHaveBeenCalled();
93 |
94 | spy.mockClear();
95 | viewToTest.off('testevent', spy);
96 | apple.fire('testevent');
97 | expect(spy).not.toHaveBeenCalled();
98 | });
99 |
100 | afterEach(function() {
101 | helpers.destroyView();
102 | viewToTest = null;
103 | });
104 | });
105 |
--------------------------------------------------------------------------------
/docs/template-markup.md:
--------------------------------------------------------------------------------
1 | ## Markup
2 |
3 | When FruitMachine renders your modules by calling the template function, it passes some important data, in addition to any that you have explicitly declared yourself. This data includes the rendered markup from each of the module's child modules. It's up to your template function to put it in the right place.
4 |
5 | It gives you the following data:
6 |
7 | - An array of child modules in the form of `children`.
8 | - A variable for each child View in the form of the child's `slot`.
9 |
10 | This gives you the ability to print child HTML exactly where you want it, or to loop and print all children of the current View. If you don't print a module's child module's into the markup then they will not appear in the final HTML markup.
11 |
12 | ### Place child modules by `slot`
13 |
14 | The following example demonstrates how you can place child modules by `slot`.
15 |
16 | ##### Template markup
17 |
18 | *layout.mustache*
19 |
20 | ```html
21 | {{{1}}}
22 | ```
23 |
24 | In `Layout`'s template we print slots by name. In this case we have named our slot '1' so we have printed `{{{1}}}` into our template.
25 |
26 | *apple.mustache*
27 |
28 | ```html
29 | I am Apple
30 | ```
31 |
32 | **Remember:** FruitMachine creates the module's root element for you, so your templates need only contain the markup for the module's contents.
33 |
34 | ##### Define modules
35 |
36 | ```js
37 | var Layout = fruitmachine.define({
38 | name: 'layout',
39 | template: layoutTemplate
40 | });
41 |
42 | var Apple = fruitmachine.define({
43 | name: 'apple',
44 | template: appleTemplate
45 | });
46 | ```
47 |
48 | ##### Create assemble modules
49 |
50 | ```js
51 | var apple = new Apple();
52 | var layout = new Layout();
53 |
54 | // Add a child view
55 | layout.add(apple, { slot: 1 });
56 | ```
57 |
58 | We created an instance of our `Layout` module, then an instance of our `Apple` module. We then added the `apple` module as a child of the `layout` module. In the options object we defined which slot we wanted the `apple` module to sit in.
59 |
60 | ##### Render
61 |
62 | ```js
63 | layout.render();
64 | layout.el.outerHTML;
65 | //=>
66 | //
I am Apple
67 | //
68 | ```
69 |
70 | ### Loop and place all child modules
71 |
72 | In some cases the number of child modules is not known, and we just want to render them all. The list.mustache template uses the special `children` (Array) and `child` (HTML string) keys to iterate and print each module's HTML. In this example we are using dummy `List` and `Item` module constructors.
73 |
74 | ##### Create the module
75 |
76 | ```js
77 | var list = new List();
78 | var item1 = new Item({ model: { name: 'Wilson' } });
79 | var item2 = new Item({ model: { name: 'Matt' } });
80 | var item3 = new Item({ model: { name: 'Jim' } });
81 |
82 | list
83 | .add(item1)
84 | .add(item2)
85 | .add(item3);
86 | ```
87 |
88 | ##### Template markup
89 |
90 | *list.mustache*
91 |
92 | ```html
93 | {{#children}}
94 | {{{child}}}
95 | {{#children}}
96 | ```
97 |
98 | **Note:** It's worth noting that within the scope of the loop, the current child's `model` is accessible. So `{{name}}` within the `{{#children}}` loop would work.
99 |
100 | *item.mustache*
101 |
102 | ```html
103 | My name is {{name}}
104 | ```
105 |
106 | ##### Render
107 |
108 | ```js
109 | layout.render();
110 | layout.el.outerHTML;
111 | //=>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam volutpat sem dictum, bibendum orci sed, auctor nulla. Nullam mauris eros, lobortis quis mi quis, commodo pellentesque dolor. Fusce purus odio, rutrum id malesuada in, volutpat ut augue. Vivamus in neque posuere, porta ipsum sed, lacinia sem. In tortor turpis, rhoncus consequat elit nec, condimentum accumsan ipsum. Vestibulum sed pellentesque urna. Duis rutrum pulvinar accumsan. Integer sagittis ante enim, ac porttitor ligula rutrum quis.
Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Integer vulputate aliquet quam at aliquam. Praesent pellentesque mauris ut augue congue, sit amet mattis sapien ultrices. Phasellus at semper massa. Pellentesque sollicitudin egestas enim ac rhoncus. Vestibulum quis vehicula turpis, hendrerit dapibus nunc. Etiam eget libero efficitur, vehicula risus id, efficitur neque. Maecenas accumsan tincidunt ultrices. Vestibulum sagittis, felis sed commodo pharetra, velit dolor congue velit, nec porta leo leo sit amet neque. Donec imperdiet porttitor neque, eget faucibus odio eleifend ut.
Curabitur eget feugiat leo. Nulla lorem nisl, malesuada vel erat eu, mattis viverra magna. Praesent facilisis ornare tristique. Sed congue accumsan lacus, non consequat augue hendrerit et. Maecenas imperdiet placerat leo, sed auctor neque suscipit eget. Aliquam a porttitor massa. Quisque porttitor sed urna eget auctor.
Vestibulum consectetur, nunc sit amet sodales pharetra, arcu diam molestie ante, ac viverra erat justo id velit. Maecenas consequat fringilla lectus, id pretium ipsum viverra quis. Sed tortor urna, tincidunt ac laoreet eu, finibus finibus sem. Pellentesque venenatis risus sem, eu lacinia neque fermentum eu. In at dui ut odio elementum venenatis at eu tortor. Curabitur vel dui felis. Maecenas sollicitudin, erat sit amet facilisis vehicula, dolor lectus mattis libero, in sagittis justo lorem et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vestibulum tincidunt ante eget ex gravida, vitae bibendum urna fermentum. Donec commodo magna vel malesuada volutpat. Etiam in ipsum nec est eleifend euismod. Mauris a justo justo. Aenean pulvinar aliquam ligula, at bibendum velit imperdiet at. Etiam euismod tristique ex quis placerat. Morbi mi lorem, cursus in tempus vitae, mollis in risus.
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium.
',
65 | author: 'John Smith'
66 | }
67 | };
68 |
69 | setTimeout(function() {
70 | callback(database[id]);
71 | }, 100);
72 | };
73 |
--------------------------------------------------------------------------------
/docs/events.md:
--------------------------------------------------------------------------------
1 | ## Events
2 |
3 | Events are at the core of fruitmachine. They allows us to decouple View interactions from one another. By default *FruitMachine* fires the following events on View module instances during creation, in the following order:
4 |
5 | - `before initialize` Before the module is instantiated
6 | - `initialize` On instantiation
7 | - `before render` At the very start of the render process, triggered by the view having `.render()` called on it
8 | - `before tohtml` Before toHTML is called. `render` events are only fired on the node being rendered - not any of the children so if you want to manipulate a module's data model prior to rendering, hook into this event)
9 | - `render` When the `.render()` process is complete
10 | - `before setup` Before the module is setup
11 | - `setup` When `.setup()` is called to set up events after render (remember 'setup' is recursive)
12 |
13 | And during destruction, in the following order:
14 | - `before teardown` Before the module is torn down
15 | - `teardown` When `.teardown()` or `.destroy()` are called (remember 'destroy' calls 'teardown' which recurses)
16 | - `before destroy` Before the module is destroyed
17 | - `destroy` When `.destroy()` is called (remember 'teardown' recurses)
18 |
19 | #### Bubbling
20 |
21 | FruitMachine events are interesting as they propagate (or bubble) up the view chain. This means that parent Views way up the view chain can still listen to events that happen in deeply nested View modules.
22 |
23 | This is useful because it means your app's controllers can listen and decide what to do when specific things happen within your views. The response logic doesn't have to be in the view module itself, meaning modules are decoupled from your app, and easily reused elsewhere.
24 |
25 | ```js
26 | var layout = new Layout();
27 | var apple = new Apple();
28 |
29 | layout.add(apple);
30 |
31 | layout.on('shout', function() {
32 | alert('layout heard apple shout');
33 | });
34 |
35 | apple.fire('shout');
36 | //=> alert 'layout heard apple shout'
37 | ```
38 |
39 | The FruitMachine default events (eg `initialize`, `setup`, `teardown`) do not bubble.
40 |
41 | #### Passing parameters
42 |
43 | ```js
44 | var layout = new Layout();
45 | var apple = new Apple();
46 |
47 | layout.add(apple);
48 |
49 | layout.on('shout', function(param) {
50 | alert('layout heard apple shout ' + param);
51 | });
52 |
53 | apple.fire('shout', 'hello');
54 | // alert - 'layout heard apple shout hello'
55 | ```
56 |
57 | #### Listening only for specific modules
58 |
59 | ```js
60 | var layout = new Layout();
61 | var apple = new Apple();
62 | var orange = new Orange();
63 |
64 | layout
65 | .add(apple)
66 | .add(orange);
67 |
68 | layout.on('shout', 'apple', function() {
69 | alert('layout heard apple shout');
70 | });
71 |
72 | apple.fire('shout');
73 | //=> alert 'layout heard apple shout'
74 |
75 | orange.fire('shout');
76 | //=> nothing
77 | ```
78 |
79 | #### Utilising the event object
80 |
81 | The event object can be found on under `this.event`. It holds a reference to the target view, where the event originated.
82 |
83 | ```js
84 | var layout = new Layout();
85 | var apple = new Apple();
86 | var orange = new Orange();
87 |
88 | layout
89 | .add(apple)
90 | .add(orange);
91 |
92 | layout.on('shout', function() {
93 | var module = this.event.target.module();
94 | alert('layout heard ' + module + ' shout');
95 | });
96 |
97 | apple.fire('shout');
98 | //=> alert 'layout heard apple shout'
99 |
100 | orange.fire('shout');
101 | //=> alert 'layout heard orange shout'
102 | ```
103 |
104 | It also allows you to stop the event propagating (bubbling) up the view by calling `this.event.stopPropagation()`, just like DOM events!
105 |
106 | ```js
107 | var layout = new Layout();
108 | var apple = new Apple();
109 | var orange = new Orange();
110 |
111 | layout
112 | .add(apple)
113 | .add(orange);
114 |
115 | layout.on('shout', function() {
116 | alert('layout heard apple shout');
117 | });
118 |
119 | apple.on('shout', function() {
120 | this.event.stopPropagation(); /* 1 */
121 | });
122 |
123 | apple.fire('shout');
124 | //=> nothing
125 | ```
126 |
127 | 1. *By stopping propagation here, we stop the event from ever reaching the parent view `layout`, and thus the alert is never fired.*
128 |
129 |
--------------------------------------------------------------------------------
/test/tests/module.remove.js:
--------------------------------------------------------------------------------
1 |
2 | describe('View#remove()', function() {
3 |
4 | test("Should remove the child passed from the parent's children array", function() {
5 | var list = new helpers.Views.Layout();
6 | var Apple = helpers.Views.Apple;
7 | var apple1 = new Apple();
8 | var apple2 = new Apple();
9 |
10 | list
11 | .add(apple1)
12 | .add(apple2);
13 |
14 | list.remove(apple1);
15 |
16 | expect(list.children.indexOf(apple1)).toBe(-1);
17 | });
18 |
19 | test("Should remove all lookup references", function() {
20 | var list = new helpers.Views.Layout();
21 | var Apple = helpers.Views.Apple;
22 | var apple = new Apple({ id: 'foo' });
23 |
24 | list.add(apple);
25 |
26 | expect(list._ids.foo).toBeTruthy();
27 | expect(list._modules.apple[0]).toBe(apple);
28 |
29 | list.remove(apple);
30 |
31 | expect(list._ids.foo).toBeUndefined();
32 | expect(list._modules.apple[0]).toBeUndefined();
33 | });
34 |
35 | test("Should remove the child from the DOM by default", function() {
36 | var sandbox = helpers.createSandbox();
37 | var list = new helpers.Views.Layout();
38 | var Apple = helpers.Views.Apple;
39 | var apple = new Apple({ slot: 1 });
40 |
41 | list
42 | .add(apple)
43 | .render()
44 | .inject(sandbox)
45 | .setup();
46 |
47 | expect(sandbox.querySelector('#' + apple._fmid)).toBeTruthy();
48 |
49 | list.remove(apple);
50 |
51 | expect(sandbox.querySelector('#' + apple._fmid)).toBeFalsy();
52 | });
53 |
54 | test("Should *not* remove the child from the DOM if `fromDOM` option is false", function() {
55 | var sandbox = document.createElement('div');
56 | var list = new helpers.Views.Layout();
57 | var Apple = helpers.Views.Apple;
58 | var apple = new Apple();
59 |
60 | list
61 | .add(apple, 1)
62 | .render()
63 | .setup()
64 | .inject(sandbox);
65 |
66 | expect(sandbox.querySelector('#' + apple._fmid)).toBeTruthy();
67 |
68 | list.remove(apple, { fromDOM: false });
69 |
70 | expect(sandbox.querySelector('#' + apple._fmid)).toBeTruthy();
71 | });
72 |
73 | test("Should unmount the view by default", function() {
74 | var list = new Layout({
75 | children: {
76 | 1: new Apple()
77 | }
78 | });
79 |
80 | var layoutSpy = jest.fn(); list.on('unmount', layoutSpy);
81 | var appleSpy = jest.fn(); list.module('apple').on('unmount', appleSpy);
82 |
83 | list.render().inject(sandbox).setup();
84 | list.remove();
85 |
86 | expect(layoutSpy).toBeCalled();
87 | expect(appleSpy).toBeCalled();
88 | });
89 |
90 | test("Should not unmount the view if `fromDOM` option is false", function() {
91 | var list = new Layout({
92 | children: {
93 | 1: new Apple()
94 | }
95 | });
96 |
97 | var layoutSpy = jest.fn(); list.on('unmount', layoutSpy);
98 | var appleSpy = jest.fn(); list.module('apple').on('unmount', appleSpy);
99 |
100 | list.render().inject(sandbox).setup();
101 | list.remove({fromDOM: false});
102 |
103 | expect(layoutSpy).not.toBeCalled();
104 | expect(appleSpy).not.toBeCalled();
105 | });
106 |
107 | test("Should remove itself if called with no arguments", function() {
108 | var list = new helpers.Views.Layout();
109 | var Apple = helpers.Views.Apple;
110 | var apple = new Apple({ id: 'foo' });
111 |
112 | list.add(apple);
113 | apple.remove();
114 |
115 | expect(list.children.indexOf(apple)).toBe(-1);
116 | expect(list._ids.foo).toBeUndefined();
117 | });
118 |
119 | test("Should remove the reference back to the parent view", function() {
120 | var layout = new Layout();
121 | var apple = new Apple({ slot: 1 });
122 |
123 | layout.add(apple);
124 |
125 | expect(apple.parent).toBe(layout);
126 |
127 | layout.remove(apple);
128 |
129 | expect(apple.parent).toBeUndefined();
130 | });
131 |
132 | test("Should remove slot reference", function() {
133 | var layout = new Layout();
134 | var apple = new Apple({ slot: 1 });
135 |
136 | layout.add(apple);
137 |
138 | expect(layout.slots[1]).toBe(apple);
139 |
140 | layout.remove(apple);
141 |
142 | expect(layout.slots[1]).toBeUndefined();
143 | });
144 |
145 | test("Should not remove itself if first argument is undefined", function() {
146 | var layout = new Layout();
147 | var apple = new Apple({ slot: 1 });
148 |
149 | layout.add(apple);
150 | apple.remove(undefined);
151 |
152 | expect(layout.module('apple')).toBeTruthy();
153 | });
154 | });
155 |
--------------------------------------------------------------------------------
/test/tests/module.js:
--------------------------------------------------------------------------------
1 | var Backbone = require('backbone');
2 |
3 | describe('View', function() {
4 |
5 | test("Should add any children passed into the constructor", function() {
6 | var children = [
7 | {
8 | module: 'pear'
9 | },
10 | {
11 | module: 'orange'
12 | }
13 | ];
14 |
15 | var view = new fruitmachine({
16 | module: 'apple',
17 | children: children
18 | });
19 |
20 | expect(view.children.length).toBe(2);
21 | });
22 |
23 | test("Should store a reference to the slot if passed", function() {
24 | var view = new fruitmachine({
25 | module: 'apple',
26 | children: [
27 | {
28 | module: 'pear',
29 | slot: 1
30 | },
31 | {
32 | module: 'orange',
33 | slot: 2
34 | }
35 | ]
36 | });
37 |
38 | expect(view.slots[1]).toBeTruthy();
39 | expect(view.slots[2]).toBeTruthy();
40 | });
41 |
42 | test("Should store a reference to the slot if slot is passed as key of children object", function() {
43 | var view = new fruitmachine({
44 | module: 'apple',
45 | children: {
46 | 1: { module: 'pear' },
47 | 2: { module: 'orange' }
48 | }
49 | });
50 |
51 | expect(view.slots[1]).toBeTruthy();
52 | expect(view.slots[2]).toBeTruthy();
53 | });
54 |
55 | test("Should store a reference to the slot if the view is instantiated with a slot", function() {
56 | var apple = new Apple({ slot: 1 });
57 |
58 | expect(apple.slot).toBe(1);
59 | });
60 |
61 | test("Should prefer the slot on the children object in case of conflict", function() {
62 | var apple = new Apple({ slot: 1 });
63 | var layout = new Layout({
64 | children: {
65 | 2: apple
66 | }
67 | });
68 |
69 | expect(layout.module('apple').slot).toBe('2');
70 | });
71 |
72 | test("Should create a model", function() {
73 | var view = new fruitmachine({ module: 'apple' });
74 | expect(view.model instanceof fruitmachine.Model).toBe(true);
75 | });
76 |
77 | test("Should adopt the fmid if passed", function() {
78 | var view = new fruitmachine({ fmid: '1234', module: 'apple' });
79 | expect(view._fmid).toBe('1234');
80 | });
81 |
82 | test("Should fire an 'inflation' event on fm instance if instantiated with an fmid", function() {
83 | var spy = jest.fn();
84 |
85 | fruitmachine.on('inflation', spy);
86 |
87 | var layout = new fruitmachine({
88 | fmid: '1',
89 | module: 'layout',
90 | children: {
91 | 1: {
92 | fmid: '2',
93 | module: 'apple'
94 | }
95 | }
96 | });
97 |
98 | expect(spy).toHaveBeenCalledTimes(2);
99 | });
100 |
101 | test("Should fire an 'inflation' event on fm instance with the view as the first arg", function() {
102 | var spy = jest.fn();
103 |
104 | fruitmachine.on('inflation', spy);
105 |
106 | var layout = new fruitmachine({
107 | fmid: '1',
108 | module: 'layout',
109 | children: {
110 | 1: {
111 | fmid: '2',
112 | module: 'apple'
113 | }
114 | }
115 | });
116 |
117 | expect(spy.mock.calls[0][0]).toBe(layout);
118 | expect(spy.mock.calls[1][0]).toBe(layout.module('apple'));
119 | });
120 |
121 | test("Should fire an 'inflation' event on fm instance with the options as the second arg", function() {
122 | var spy = jest.fn();
123 | var options = {
124 | fmid: '1',
125 | module: 'layout'
126 | };
127 |
128 | fruitmachine.on('inflation', spy);
129 |
130 | var layout = new fruitmachine(options);
131 | expect(spy.mock.calls[0][1]).toEqual(options);
132 | });
133 |
134 | test("Should be able to use Backbone models", function() {
135 | var orange = new Orange({
136 | model: new Backbone.Model({ text: 'orange text' })
137 | });
138 |
139 | orange.render();
140 | expect(orange.el.innerHTML.indexOf('orange text')).toBe(0);
141 | });
142 |
143 | test("Should define a global default model", function() {
144 | var previous = fruitmachine.Module.prototype.Model;
145 |
146 | fruitmachine.Module.prototype.Model = Backbone.Model;
147 |
148 | var orange = new Orange({
149 | model: { text: 'orange text' }
150 | });
151 |
152 | orange.render();
153 | expect(orange.model instanceof Backbone.Model).toBe(true);
154 | expect(orange.el.innerHTML.indexOf('orange text')).toBe(0);
155 |
156 | // Restore
157 | fruitmachine.Module.prototype.Model = previous;
158 | });
159 |
160 | test("Should define a module default model", function() {
161 | var Berry = fruitmachine.define({
162 | name: 'berry',
163 | Model: Backbone.Model
164 | });
165 |
166 | var berry = new Berry({ model: { foo: 'bar' }});
167 |
168 | expect(berry.model instanceof Backbone.Model).toBe(true);
169 | });
170 |
171 | test.skip("Should not modify the options object", function() {
172 | var options = {
173 | classes: ['my class']
174 | };
175 |
176 | var orange = new Orange(options);
177 | orange.classes.push('added');
178 |
179 | expect(['my class']).toBe(options.classes);
180 | });
181 | });
182 |
--------------------------------------------------------------------------------
/test/tests/module.render.js:
--------------------------------------------------------------------------------
1 |
2 | describe('View#render()', function() {
3 | var viewToTest;
4 |
5 | beforeEach(function() {
6 | viewToTest = helpers.createView();
7 | });
8 |
9 | test("The master view should have an element post render.", function() {
10 | viewToTest.render();
11 | expect(viewToTest.el).toBeDefined();
12 | });
13 |
14 | test("before render and render events should be fired", function() {
15 | var beforeRenderSpy = jest.fn();
16 | var renderSpy = jest.fn();
17 | viewToTest.on('before render', beforeRenderSpy);
18 | viewToTest.on('render', renderSpy);
19 |
20 | viewToTest.render();
21 | expect(beforeRenderSpy.mock.invocationCallOrder[0]).toBeLessThan(renderSpy.mock.invocationCallOrder[0]);
22 | });
23 |
24 | test("Data should be present in the generated markup.", function() {
25 | var text = 'some orange text';
26 | var orange = new Orange({
27 | model: {
28 | text: text
29 | }
30 | });
31 |
32 | orange
33 | .render()
34 | .inject(sandbox);
35 |
36 | expect(orange.el.innerHTML).toEqual(text);
37 | });
38 |
39 | test("Should be able to use Backbone models", function() {
40 | var orange = new Orange({
41 | model: {
42 | text: 'orange text'
43 | }
44 | });
45 |
46 | orange.render();
47 | expect(orange.el.innerHTML).toEqual('orange text');
48 | });
49 |
50 | test("Child html should be present in the parent.", function() {
51 | var layout = new Layout();
52 | var apple = new Apple();
53 |
54 | layout
55 | .add(apple, 1)
56 | .render();
57 |
58 | firstChild = layout.el.firstElementChild;
59 | expect(firstChild.classList.contains('apple')).toBe(true);
60 | });
61 |
62 | test("Should be of the tag specified", function() {
63 | var apple = new Apple({ tag: 'ul' });
64 |
65 | apple.render();
66 | expect('UL').toEqual(apple.el.tagName);
67 | });
68 |
69 | test("Should have classes on the element", function() {
70 | var apple = new Apple({
71 | classes: ['foo', 'bar']
72 | });
73 |
74 | apple.render();
75 | expect('apple foo bar').toEqual(apple.el.className);
76 | });
77 |
78 | test("Should have an id attribute with the value of `fmid`", function() {
79 | var apple = new Apple({
80 | classes: ['foo', 'bar']
81 | });
82 |
83 | apple.render();
84 |
85 | expect(apple._fmid).toEqual(apple.el.id);
86 | });
87 |
88 | test("Should have populated all child module.el properties", function() {
89 | var layout = new Layout({
90 | children: {
91 | 1: {
92 | module: 'apple',
93 | children: {
94 | 1: {
95 | module: 'apple',
96 | children: {
97 | 1: {
98 | module: 'apple'
99 | }
100 | }
101 | }
102 | }
103 | }
104 | }
105 | });
106 |
107 | var apple1 = layout.module('apple');
108 | var apple2 = apple1.module('apple');
109 | var apple3 = apple2.module('apple');
110 |
111 | layout.render();
112 |
113 | expect(apple1.el).toBeTruthy();
114 | expect(apple2.el).toBeTruthy();
115 | expect(apple3.el).toBeTruthy();
116 | });
117 |
118 | test("The outer DOM node should be recycled between #renders", function() {
119 | var layout = new Layout({
120 | children: {
121 | 1: { module: 'apple' }
122 | }
123 | });
124 | layout.render();
125 | layout.el.setAttribute('data-test', 'should-not-be-blown-away');
126 | layout.module('apple').el.setAttribute('data-test', 'should-be-blown-away');
127 |
128 | layout.render();
129 |
130 | // The DOM node of the FM module that render is called on should be recycled
131 | expect(layout.el.getAttribute('data-test')).toEqual('should-not-be-blown-away');
132 |
133 | // The DOM node of a child FM module to the one render is called on should not be recycled
134 | expect(layout.module('apple').el.getAttribute('data-test')).not.toEqual('should-be-blown-away');
135 | });
136 |
137 | test("Classes should be updated on render", function() {
138 | var layout = new Layout();
139 | layout.render();
140 | layout.classes = ['should-be-added'];
141 | layout.render();
142 | expect(layout.el.className).toEqual('layout should-be-added');
143 | });
144 |
145 | test("Classes added through the DOM should persist between renders", function() {
146 | var layout = new Layout();
147 | layout.render();
148 | layout.el.classList.add('should-persist');
149 | layout.render();
150 | expect(layout.el.className).toEqual('layout should-persist');
151 | });
152 |
153 | test("Should fire unmount on children when rerendering", function() {
154 | var appleSpy = jest.fn();
155 | var orangeSpy = jest.fn();
156 | var pearSpy = jest.fn();
157 |
158 | viewToTest.module('apple').on('unmount', appleSpy);
159 | viewToTest.module('orange').on('unmount', orangeSpy);
160 | viewToTest.module('pear').on('unmount', pearSpy);
161 |
162 | viewToTest.render();
163 | expect(appleSpy).not.toHaveBeenCalled();
164 | expect(orangeSpy).not.toHaveBeenCalled();
165 | expect(pearSpy).not.toHaveBeenCalled();
166 |
167 | viewToTest.render();
168 | expect(appleSpy).toHaveBeenCalled();
169 | expect(orangeSpy).toHaveBeenCalled();
170 | expect(pearSpy).toHaveBeenCalled();
171 | });
172 |
173 | afterEach(function() {
174 | helpers.destroyView();
175 | viewToTest = null;
176 | });
177 | });
178 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | // For a detailed explanation regarding each configuration property, visit:
2 | // https://jestjs.io/docs/en/configuration.html
3 |
4 | module.exports = {
5 | // All imported modules in your tests should be mocked automatically
6 | // automock: false,
7 |
8 | // Stop running tests after the first failure
9 | // bail: false,
10 |
11 | // Respect "browser" field in package.json when resolving modules
12 | // browser: false,
13 |
14 | // The directory where Jest should store its cached dependency information
15 | // cacheDirectory: "/var/folders/2z/qvbjmrlm8xjfgn006s69xpm80000gn/T/jest_dx",
16 |
17 | // Automatically clear mock calls and instances between every test
18 | // clearMocks: false,
19 |
20 | // Indicates whether the coverage information should be collected while executing the test
21 | // collectCoverage: false,
22 |
23 | // An array of glob patterns indicating a set of files for which coverage information should be collected
24 | // collectCoverageFrom: null,
25 |
26 | // The directory where Jest should output its coverage files
27 | coverageDirectory: "coverage",
28 |
29 | // An array of regexp pattern strings used to skip coverage collection
30 | // coveragePathIgnorePatterns: [
31 | // "/node_modules/"
32 | // ],
33 |
34 | // A list of reporter names that Jest uses when writing coverage reports
35 | // coverageReporters: [
36 | // "json",
37 | // "text",
38 | // "lcov",
39 | // "clover"
40 | // ],
41 |
42 | // An object that configures minimum threshold enforcement for coverage results
43 | // coverageThreshold: null,
44 |
45 | // Make calling deprecated APIs throw helpful error messages
46 | // errorOnDeprecated: false,
47 |
48 | // Force coverage collection from ignored files usin a array of glob patterns
49 | // forceCoverageMatch: [],
50 |
51 | // A path to a module which exports an async function that is triggered once before all test suites
52 | // globalSetup: null,
53 |
54 | // A path to a module which exports an async function that is triggered once after all test suites
55 | // globalTeardown: null,
56 |
57 | // A set of global variables that need to be available in all test environments
58 | // globals: {},
59 |
60 | // An array of directory names to be searched recursively up from the requiring module's location
61 | moduleDirectories: [
62 | "node_modules"
63 | ],
64 |
65 | // An array of file extensions your modules use
66 | // moduleFileExtensions: [
67 | // "js",
68 | // "json",
69 | // "jsx",
70 | // "node"
71 | // ],
72 |
73 | // A map from regular expressions to module names that allow to stub out resources with a single module
74 | // moduleNameMapper: {},
75 |
76 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
77 | // modulePathIgnorePatterns: [],
78 |
79 | // Activates notifications for test results
80 | // notify: false,
81 |
82 | // An enum that specifies notification mode. Requires { notify: true }
83 | // notifyMode: "always",
84 |
85 | // A preset that is used as a base for Jest's configuration
86 | // preset: null,
87 |
88 | // Run tests from one or more projects
89 | // projects: null,
90 |
91 | // Use this configuration option to add custom reporters to Jest
92 | // reporters: undefined,
93 |
94 | // Automatically reset mock state between every test
95 | // resetMocks: false,
96 |
97 | // Reset the module registry before running each individual test
98 | // resetModules: false,
99 |
100 | // A path to a custom resolver
101 | // resolver: null,
102 |
103 | // Automatically restore mock state between every test
104 | // restoreMocks: false,
105 |
106 | // The root directory that Jest should scan for tests and modules within
107 | // rootDir: null,
108 |
109 | // A list of paths to directories that Jest should use to search for files in
110 | // roots: [
111 | // ""
112 | // ],
113 |
114 | // Allows you to use a custom runner instead of Jest's default test runner
115 | // runner: "jest-runner",
116 |
117 | // The paths to modules that run some code to configure or set up the testing environment before each test
118 | setupFiles: [
119 | "./test/helpers.js"
120 | ],
121 |
122 | // The path to a module that runs some code to configure or set up the testing framework before each test
123 | // setupTestFrameworkScriptFile: null,
124 |
125 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing
126 | // snapshotSerializers: [],
127 |
128 | // The test environment that will be used for testing
129 | // testEnvironment: "jest-environment-jsdom",
130 |
131 | // Options that will be passed to the testEnvironment
132 | // testEnvironmentOptions: {},
133 |
134 | // Adds a location field to test results
135 | // testLocationInResults: false,
136 |
137 | // The glob patterns Jest uses to detect test files
138 | testMatch: [
139 | "**/__tests__/**/*.js?(x)",
140 | "**/?(*.)+(spec|test).js?(x)",
141 | "**/test/tests/*.js"
142 | ],
143 |
144 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
145 | // testPathIgnorePatterns: [
146 | // "/node_modules/"
147 | // ],
148 |
149 | // The regexp pattern Jest uses to detect test files
150 | // testRegex: "",
151 |
152 | // This option allows the use of a custom results processor
153 | // testResultsProcessor: null,
154 |
155 | // This option allows use of a custom test runner
156 | // testRunner: "jasmine2",
157 |
158 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
159 | // testURL: "http://localhost",
160 |
161 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
162 | // timers: "real",
163 |
164 | // A map from regular expressions to paths to transformers
165 | // transform: null,
166 |
167 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
168 | // transformIgnorePatterns: [
169 | // "/node_modules/"
170 | // ],
171 |
172 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
173 | // unmockedModulePathPatterns: undefined,
174 |
175 | // Indicates whether each individual test should be reported during the run
176 | // verbose: null,
177 |
178 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
179 | // watchPathIgnorePatterns: [],
180 |
181 | // Whether to use watchman for file crawling
182 | // watchman: true,
183 | };
184 |
--------------------------------------------------------------------------------
/docs/api.md:
--------------------------------------------------------------------------------
1 | # API
2 |
3 | ### fruitmachine.define()
4 |
5 | slint browser:true, node:true, laxbreak:true
6 | ### fruitmachine.define()
7 |
8 | Defines a module.
9 | \nOptions:
10 |
11 | - `name {String}` the name of the module
12 | - `tag {String}` the tagName to use for the root element
13 | - `classes {Array}` a list of classes to add to the root element
14 | - `template {Function}` the template function to use when rendering
15 | - `helpers {Array}` a list of helpers to apply to the module
16 | - `initialize {Function}` custom logic to run when module instance created
17 | - `setup {Function}` custom logic to run when `.setup()` is called (directly or indirectly)
18 | - `teardown {Function}` custom logic to unbind/undo anything setup introduced (called on `.destroy()` and sometimes on `.setup()` to avoid double binding events)
19 | - `destroy {Function}` logic to permanently destroy all references
20 |
21 | ### Module#undefined
22 |
23 | shint browser:true, node:true
24 | ### Module#util
25 |
26 | Module Dependencies
27 | ### Module#exports()
28 |
29 | Exports
30 | ### Module#Module
31 |
32 | Module constructor
33 | \nOptions:
34 |
35 | - `id {String}` a unique id to query by
36 | - `model {Object|Model}` the data with which to associate this module
37 | - `tag {String}` tagName to use for the root element
38 | - `classes {Array}` list of classes to add to the root element
39 | - `template {Function}` a template to use for rendering
40 | - `helpers {Array}`a list of helper function to use on this module
41 | - `children {Object|Array}` list of child modules
42 |
43 | ### Module#add()
44 |
45 | Adds a child view(s) to another Module.
46 | \nOptions:
47 |
48 | - `at` The child index at which to insert
49 | - `inject` Injects the child's view element into the parent's
50 | - `slot` The slot at which to insert the child
51 |
52 | ### Module#remove()
53 |
54 | Removes a child view from
55 | its current Module contexts
56 | and also from the DOM unless
57 | otherwise stated.
58 | \nOptions:
59 |
60 | - `fromDOM` Whether the element should be removed from the DOM (default `true`)
61 |
62 | *Example:*
63 |
64 | // The following are equal
65 | // apple is removed from the
66 | // the view structure and DOM
67 | layout.remove(apple);
68 | apple.remove();
69 |
70 | // Apple is removed from the
71 | // view structure, but not the DOM
72 | layout.remove(apple, { el: false });
73 | apple.remove({ el: false });
74 |
75 | ### Module#id()
76 |
77 | Returns a decendent module
78 | by id, or if called with no
79 | arguments, returns this view's id.
80 | \n*Example:*
81 |
82 | myModule.id();
83 | //=> 'my_view_id'
84 |
85 | myModule.id('my_other_views_id');
86 | //=> Module
87 |
88 | ### Module#module()
89 |
90 | Returns the first descendent
91 | Module with the passed module type.
92 | If called with no arguments the
93 | Module's own module type is returned.
94 | \n*Example:*
95 |
96 | // Assuming 'myModule' has 3 descendent
97 | // views with the module type 'apple'
98 |
99 | myModule.modules('apple');
100 | //=> Module
101 |
102 | ### Module#modules()
103 |
104 | Returns a list of descendent
105 | Modules that match the module
106 | type given (Similar to
107 | Element.querySelectorAll();).
108 | \n*Example:*
109 |
110 | // Assuming 'myModule' has 3 descendent
111 | // views with the module type 'apple'
112 |
113 | myModule.modules('apple');
114 | //=> [ Module, Module, Module ]
115 |
116 | ### Module#each()
117 |
118 | Calls the passed function
119 | for each of the view's
120 | children.
121 | \n*Example:*
122 |
123 | myModule.each(function(child) {
124 | // Do stuff with each child view...
125 | });
126 |
127 | ### Module#toHTML()
128 |
129 | Templates the view, including
130 | any descendent views returning
131 | an html string. All data in the
132 | views model is made accessible
133 | to the template.
134 | \nChild views are printed into the
135 | parent template by `id`. Alternatively
136 | children can be iterated over a a list
137 | and printed with `{{{child}}}}`.
138 |
139 | *Example:*
140 |
141 |
{{{}}}
142 |
{{{}}}
143 |
144 | // or
145 |
146 | {{#children}}
147 | {{{child}}}
148 | {{/children}}
149 |
150 | ### Module#_innerHTML()
151 |
152 | Get the view's innerHTML
153 |
154 | ### Module#render()
155 |
156 | Renders the view and replaces
157 | the `view.el` with a freshly
158 | rendered node.
159 | \nFires a `render` event on the view.
160 |
161 | ### Module#setup()
162 |
163 | Sets up a view and all descendent
164 | views.
165 | \nSetup will be aborted if no `view.el`
166 | is found. If a view is already setup,
167 | teardown is run first to prevent a
168 | view being setup twice.
169 |
170 | Your custom `setup()` method is called
171 |
172 | Options:
173 |
174 | - `shallow` Does not recurse when `true` (default `false`)
175 |
176 | ### Module#teardown()
177 |
178 | Tearsdown a view and all descendent
179 | views that have been setup.
180 | \nYour custom `teardown` method is
181 | called and a `teardown` event is fired.
182 |
183 | Options:
184 |
185 | - `shallow` Does not recurse when `true` (default `false`)
186 |
187 | ### Module#destroy()
188 |
189 | Completely destroys a view. This means
190 | a view is torn down, removed from it's
191 | current layout context and removed
192 | from the DOM.
193 | \nYour custom `destroy` method is
194 | called and a `destroy` event is fired.
195 |
196 | NOTE: `.remove()` is only run on the view
197 | that `.destroy()` is directly called on.
198 |
199 | Options:
200 |
201 | - `fromDOM` Whether the view should be removed from DOM (default `true`)
202 |
203 | ### Module#empty()
204 |
205 | Destroys all children.
206 | \nIs this needed?
207 |
208 | ### Module#mount()
209 |
210 | Associate the view with an element.
211 | Provide events and lifecycle methods
212 | to fire when the element is newly
213 | associated.
214 |
215 | ### Module#inject()
216 |
217 | Empties the destination element
218 | and appends the view into it.
219 |
220 | ### Module#appendTo()
221 |
222 | Appends the view element into
223 | the destination element.
224 |
225 | ### Module#insertBefore()
226 |
227 | Inserts the view element before the
228 | given child of the destination element.
229 |
230 | ### Module#toJSON()
231 |
232 | Returns a JSON represention of
233 | a FruitMachine Module. This can
234 | be generated serverside and
235 | passed into new FruitMachine(json)
236 | to inflate serverside rendered
237 | views.
238 |
239 | ### Module#events
240 |
241 | Module Dependencies
242 | ### Module#listenerMap
243 |
244 | Local vars
245 | ### Module#on()
246 |
247 | Registers a event listener.
248 |
249 | ### Module#off()
250 |
251 | Unregisters a event listener.
252 |
253 | ### Module#fire()
254 |
255 | Fires an event on a view.
256 |
257 |
--------------------------------------------------------------------------------
/examples/lib/delegate.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Create a circularly-linked list
3 | *
4 | * Adapted from original version by James Coglan.
5 | *
6 | * @fileOverview
7 | * @codingstandard ftlabs-jsv2
8 | * @copyright The Financial Times Limited [All Rights Reserved]
9 | */
10 |
11 | this.CircularList = (function () {
12 | 'use strict';
13 |
14 |
15 | /**
16 | * @constructor
17 | */
18 | function CircularList() {
19 |
20 |
21 | /**
22 | * The length of the linked list
23 | *
24 | * @type number
25 | */
26 | this.length = 0;
27 |
28 |
29 | /**
30 | * The first item in the linked list
31 | *
32 | * @type Object
33 | */
34 | this.first = null;
35 |
36 |
37 | /**
38 | * The last item in the linked list
39 | *
40 | * @type Object
41 | */
42 | this.last = null;
43 | }
44 |
45 |
46 | /**
47 | * Explicit item object to allow items to belong to more than linked list at a time
48 | *
49 | * To hold a reference to a CircularList.Item within a completely different CircularList the CircularList.Item should be passed as the data to a new CircularList.Item to be used in the new CircularList.
50 | * If you don't wrap the reference in a new Item, then if you append an already existing reference to a different CircularList the behaviour is undefined.
51 | *
52 | * @example
53 | * myList.append(new CircularList.Item(someObject));
54 | *
55 | * @constructor
56 | * @param {Object} data
57 | */
58 | CircularList.Item = function (data) {
59 | this.prev = null;
60 | this.next = null;
61 | this.list = null;
62 | this.data = data;
63 | };
64 |
65 |
66 | /**
67 | * Append an object to the linked list
68 | *
69 | * @param {Object} item The item to append
70 | */
71 | CircularList.prototype.append = function (item) {
72 | if (this.first === null) {
73 | item.prev = item;
74 | item.next = item;
75 | this.first = item;
76 | this.last = item;
77 | } else {
78 | item.prev = this.last;
79 | item.next = this.first;
80 | this.first.prev = item;
81 | this.last.next = item;
82 | this.last = item;
83 | }
84 |
85 | item.list = this;
86 | this.length++;
87 | };
88 |
89 |
90 | /**
91 | * Remove an item from the linked list
92 | *
93 | * @param {Object} item The item to remove
94 | */
95 | CircularList.prototype.remove = function (item) {
96 |
97 | // Exit early if the item isn't in the list
98 | if (!this.length || this !== item.list) {
99 | return;
100 | }
101 |
102 | if (this.length > 1) {
103 | item.prev.next = item.next;
104 | item.next.prev = item.prev;
105 |
106 | if (item === this.first) {
107 | this.first = item.next;
108 | }
109 |
110 | if (item === this.last) {
111 | this.last = item.prev;
112 | }
113 | } else {
114 | this.first = null;
115 | this.last = null;
116 | }
117 |
118 | item.prev = null;
119 | item.next = null;
120 | this.length--;
121 | };
122 |
123 |
124 | /**
125 | * Convert the linked list to an Array
126 | *
127 | * The first item in the list is the first item in the array.
128 | *
129 | * @return {Array}
130 | */
131 | CircularList.prototype.toArray = function () {
132 | var i, item, array, length = this.length;
133 |
134 | array = new Array(length);
135 | item = this.first;
136 |
137 | for (i = 0; i < length; i++) {
138 | array[i] = item;
139 | item = item.next;
140 | }
141 |
142 | return array;
143 | };
144 |
145 |
146 | /**
147 | * Insert an item after one already in the linked list
148 | *
149 | * @param {Object} item The reference item
150 | * @param {Object} newItem The item to insert
151 | */
152 | CircularList.prototype.insertAfter = function (item, newItem) {
153 | newItem.prev = item;
154 | newItem.next = item.next;
155 | item.next.prev = newItem;
156 | item.next = newItem;
157 |
158 | if (newItem.prev === this.last) {
159 | this.last = newItem;
160 | }
161 |
162 | newItem.list = this;
163 | this.length++;
164 | };
165 |
166 | return CircularList;
167 |
168 | }());
169 |
170 | /**
171 | * Create a DOM event delegator
172 | *
173 | * @fileOverview
174 | * @codingstandard ftlabs-jsv2
175 | * @copyright The Financial Times Limited [All Rights Reserved]
176 | */
177 |
178 | this.Delegate = (function(that) {
179 | "use strict";
180 |
181 | var
182 |
183 |
184 | /**
185 | * Event listener separator
186 | *
187 | * @private
188 | * @type string
189 | */
190 | SEPARATOR = ' ',
191 |
192 |
193 | /**
194 | * Event object property used to signal that event should be ignored by handler
195 | *
196 | * @private
197 | * @type string
198 | */
199 | EVENT_IGNORE = 'ftLabsDelegateIgnore',
200 |
201 |
202 | /**
203 | * Circular list constructor
204 | *
205 | * @private
206 | * @type function()
207 | */
208 | CircularList = that.CircularList,
209 |
210 |
211 | /**
212 | * Whether tag names are case sensitive (as in XML or XHTML documents)
213 | *
214 | * @type boolean
215 | */
216 | tagsCaseSensitive = document.createElement('i').tagName === 'i',
217 |
218 |
219 | /**
220 | * Check whether an element matches a generic selector
221 | *
222 | * @private
223 | * @type function()
224 | * @param {string} selector A CSS selector
225 | */
226 | matches = (function(p) {
227 | return (p.matchesSelector || p.webkitMatchesSelector || p.mozMatchesSelector || p.msMatchesSelector || p.oMatchesSelector);
228 | }(HTMLElement.prototype)),
229 |
230 |
231 | /**
232 | * Check whether an element matches a tag selector
233 | *
234 | * Tags are NOT case-sensitive, except in XML (and XML-based languages such as XHTML).
235 | *
236 | * @private
237 | * @param {string} tagName The tag name to test against
238 | * @param {Element} element The element to test with
239 | */
240 | matchesTag = function(tagName, element) {
241 | return tagName === element.tagName;
242 | },
243 |
244 |
245 | /**
246 | * Check whether the ID of the element in 'this' matches the given ID
247 | *
248 | * IDs are case-sensitive.
249 | *
250 | * @private
251 | * @param {string} id The ID to test against
252 | * @param {Element} element The element to test with
253 | */
254 | matchesId = function(id, element) {
255 | return id === element.id;
256 | };
257 |
258 |
259 | /**
260 | * Fire a listener on a target
261 | *
262 | * @private
263 | * @param {Event} event
264 | * @param {Node} target
265 | * @param {Object} listener
266 | */
267 | function fire(event, target, listener) {
268 | var returned, oldData;
269 |
270 | if (listener.d !== null) {
271 | oldData = event.data;
272 | event.data = listener.d;
273 | returned = listener.h.call(target, event, target);
274 | event.data = oldData;
275 | } else {
276 | returned = listener.h.call(target, event, target);
277 | }
278 |
279 | return returned;
280 | }
281 |
282 |
283 | /**
284 | * Internal function proxied by Delegate#on
285 | *
286 | * @private
287 | * @param {Object} lisenerList
288 | * @param {Node|DOMWindow} root
289 | * @param {Event} event
290 | */
291 | function handle(listenerList, root, event) {
292 | var listener, returned, specificList, target;
293 |
294 | if (event[EVENT_IGNORE] === true) {
295 | return;
296 | }
297 |
298 | target = event.target;
299 | if (target.nodeType === Node.TEXT_NODE) {
300 | target = target.parentNode;
301 | }
302 | specificList = listenerList[event.type];
303 |
304 | // If the fire function actually causes the specific list to be destroyed,
305 | // Need check that the specific list is still populated
306 | while (target && specificList.length > 0) {
307 | listener = specificList.first;
308 | do {
309 |
310 | // Check for match and fire the event if there's one
311 | // TODO:MCG:20120117: Need a way to check if event#stopImmediateProgagation was called. If so, break both loops.
312 | if (listener.m.call(target, listener.p, target)) {
313 | returned = fire(event, target, listener);
314 | }
315 |
316 | // Stop propagation to subsequent callbacks if the callback returned false
317 | if (returned === false) {
318 | event[EVENT_IGNORE] = true;
319 | return;
320 | }
321 |
322 | listener = listener.next;
323 |
324 | // If the fire function actually causes the specific list object to be destroyed,
325 | // need a way of getting out of here so check listener is set
326 | } while (listener !== specificList.first && listener);
327 |
328 | // TODO:MCG:20120117: Need a way to check if event#stopProgagation was called. If so, break looping through the DOM.
329 | // Stop if the delegation root has been reached
330 | if (target === root) {
331 | break;
332 | }
333 |
334 | target = target.parentElement;
335 | }
336 | }
337 |
338 |
339 | /**
340 | * Internal function proxied by Delegate#on
341 | *
342 | * @private
343 | * @param {Delegate} that
344 | * @param {Object} listenerList
345 | * @param {Node|DOMWindow} root
346 | */
347 | function on(that, listenerList, root, eventType, selector, eventData, handler) {
348 | var matcher, matcherParam;
349 |
350 | if (!eventType) {
351 | throw new TypeError('Invalid event type: ' + eventType);
352 | }
353 |
354 | if (!selector) {
355 | throw new TypeError('Invalid selector: ' + selector);
356 | }
357 |
358 | // Support a separated list of event types
359 | if (eventType.indexOf(SEPARATOR) !== -1) {
360 | eventType.split(SEPARATOR).forEach(function(eventType) {
361 | on.call(that, that, listenerList, root, eventType, selector, eventData, handler);
362 | });
363 |
364 | return;
365 | }
366 |
367 | if (handler === undefined) {
368 | handler = eventData;
369 | eventData = null;
370 |
371 | // Normalise undefined eventData to null
372 | } else if (eventData === undefined) {
373 | eventData = null;
374 | }
375 |
376 | if (typeof handler !== 'function') {
377 | throw new TypeError("Handler must be a type of Function");
378 | }
379 |
380 | // Add master handler for type if not created yet
381 | if (!listenerList[eventType]) {
382 | root.addEventListener(eventType, that.handle, (eventType === 'error'));
383 | listenerList[eventType] = new CircularList();
384 | }
385 |
386 | // Compile a matcher for the given selector
387 | if (/^[a-z]+$/i.test(selector)) {
388 | if (!tagsCaseSensitive) {
389 | matcherParam = selector.toUpperCase();
390 | } else {
391 | matcherParam = selector;
392 | }
393 |
394 | matcher = matchesTag;
395 | } else if (/^#[a-z0-9\-_]+$/i.test(selector)) {
396 | matcherParam = selector.slice(1);
397 | matcher = matchesId;
398 | } else {
399 | matcherParam = selector;
400 | matcher = matches;
401 | }
402 |
403 | // Add to the list of listeners
404 | listenerList[eventType].append({
405 | s: selector,
406 | d: eventData,
407 | h: handler,
408 | m: matcher,
409 | p: matcherParam
410 | });
411 | }
412 |
413 |
414 | /**
415 | * Internal function proxied by Delegate#off
416 | *
417 | * @private
418 | * @param {Delegate} that
419 | * @param {Object} listenerList
420 | * @param {Node|DOMWindow} root
421 | */
422 | function off(that, listenerList, root, eventType, selector, handler) {
423 | var listener, nextListener, firstListener, specificList, singleEventType;
424 |
425 | if (!eventType) {
426 | for (singleEventType in listenerList) {
427 | if (listenerList.hasOwnProperty(singleEventType)) {
428 | off.call(that, that, listenerList, root, singleEventType, selector, handler);
429 | }
430 | }
431 | return;
432 | }
433 | specificList = listenerList[eventType];
434 |
435 | if (!specificList) {
436 | return;
437 | }
438 |
439 | // Support a separated list of event types
440 | if (eventType.indexOf(SEPARATOR) !== -1) {
441 | eventType.split(SEPARATOR).forEach(function(eventType) {
442 | off.call(that, that, listenerList, root, eventType, selector, handler);
443 | });
444 | return;
445 | }
446 |
447 | // Remove only parameter matches if specified
448 | listener = firstListener = specificList.first;
449 | do {
450 | if ((!selector || selector === listener.s) && (!handler || handler === listener.h)) {
451 |
452 | // listener.next will be undefined after listener is removed, so save a reference here
453 | nextListener = listener.next;
454 | specificList.remove(listener);
455 | listener = nextListener;
456 | } else {
457 | listener = listener.next;
458 | }
459 | } while (listener && listener !== firstListener);
460 |
461 | // All listeners removed
462 | if (!specificList.length) {
463 | delete listenerList[eventType];
464 |
465 | // Remove the main handler
466 | root.removeEventListener(eventType, that.handle, false);
467 | }
468 | }
469 |
470 |
471 | /**
472 | * DOM event delegator
473 | *
474 | * The delegator will listen for events that bubble up to the root node.
475 | *
476 | * @constructor
477 | * @param {Node|DOMWindow|string} root The root node, a window object or a selector string
478 | */
479 | function Delegate(root) {
480 | var
481 |
482 |
483 | /**
484 | * Keep a reference to the current instance
485 | *
486 | * @internal
487 | * @type Delegate
488 | */
489 | that = this,
490 |
491 |
492 | /**
493 | * Maintain a list of listeners, indexed by event name
494 | *
495 | * @internal
496 | * @type Object
497 | */
498 | listenerList = {};
499 |
500 | if (typeof root === 'string') {
501 | root = document.querySelector(root);
502 | }
503 |
504 | if (!root || !root.addEventListener) {
505 | throw new TypeError('Root node not specified');
506 | }
507 |
508 |
509 | /**
510 | * Attach a handler to one event for all elements that match the selector, now or in the future
511 | *
512 | * The handler function receives three arguments: the DOM event object, the node that matched the selector while the event was bubbling
513 | * and a reference to itself. Within the handler, 'this' is equal to the second argument.
514 | * The node that actually received the event can be accessed via 'event.target'.
515 | *
516 | * @param {string} eventType Listen for these events (in a space-separated list)
517 | * @param {string} selector Only handle events on elements matching this selector
518 | * @param {Object} [eventData] If this parameter is not specified, the third parameter must be the handler
519 | * @param {function()} handler Handler function - event data passed here will be in event.data
520 | * @returns {Delegate} This method is chainable
521 | */
522 | this.on = function() {
523 | Array.prototype.unshift.call(arguments, that, listenerList, root);
524 | on.apply(that, arguments);
525 | return this;
526 | };
527 |
528 |
529 | /**
530 | * Remove an event handler for elements that match the selector, forever
531 | *
532 | * @param {string} eventType Remove handlers for events matching this type, considering the other parameters
533 | * @param {string} [selector] If this parameter is omitted, only handlers which match the other two will be removed
534 | * @param {function()} [handler] If this parameter is omitted, only handlers which match the previous two will be removed
535 | * @returns {Delegate} This method is chainable
536 | */
537 | this.off = function() {
538 | Array.prototype.unshift.call(arguments, that, listenerList, root);
539 | off.apply(that, arguments);
540 | return this;
541 | };
542 |
543 |
544 | /**
545 | * Handle an arbitrary event
546 | *
547 | * @private
548 | * @param {Event} event
549 | */
550 | this.handle = function(event) {
551 | handle.call(that, listenerList, root, event);
552 | };
553 | }
554 |
555 | return Delegate;
556 |
557 | }(this));
--------------------------------------------------------------------------------
/examples/lib/hogan.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2011 Twitter, Inc.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software
10 | * distributed under the License is distributed on an "AS IS" BASIS,
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | * See the License for the specific language governing permissions and
13 | * limitations under the License.
14 | */
15 |
16 | var HoganTemplate = (function () {
17 |
18 | function constructor(text) {
19 | this.text = text;
20 | }
21 |
22 | constructor.prototype = {
23 |
24 | // render: replaced by generated code.
25 | r: function (context, partials, indent) { return ''; },
26 |
27 | // variable escaping
28 | v: hoganEscape,
29 |
30 | render: function render(context, partials, indent) {
31 | return this.r(context, partials, indent);
32 | },
33 |
34 | // tries to find a partial in the curent scope and render it
35 | rp: function(name, context, partials, indent) {
36 | var partial = partials[name];
37 |
38 | if (!partial) {
39 | return '';
40 | }
41 |
42 | return partial.r(context, partials, indent);
43 | },
44 |
45 | // render a section
46 | rs: function(context, partials, section) {
47 | var buf = '',
48 | tail = context[context.length - 1];
49 |
50 | if (!isArray(tail)) {
51 | return buf = section(context, partials);
52 | }
53 |
54 | for (var i = 0; i < tail.length; i++) {
55 | context.push(tail[i]);
56 | buf += section(context, partials);
57 | context.pop();
58 | }
59 |
60 | return buf;
61 | },
62 |
63 | // maybe start a section
64 | s: function(val, ctx, partials, inverted, start, end, tags) {
65 | var pass;
66 |
67 | if (isArray(val) && val.length === 0) {
68 | return false;
69 | }
70 |
71 | if (!inverted && typeof val == 'function') {
72 | val = this.ls(val, ctx, partials, start, end, tags);
73 | }
74 |
75 | pass = (val === '') || !!val;
76 |
77 | if (!inverted && pass && ctx) {
78 | ctx.push((typeof val == 'object') ? val : ctx[ctx.length - 1]);
79 | }
80 |
81 | return pass;
82 | },
83 |
84 | // find values with dotted names
85 | d: function(key, ctx, partials, returnFound) {
86 |
87 | var names = key.split('.'),
88 | val = this.f(names[0], ctx, partials, returnFound),
89 | cx = null;
90 |
91 | if (key === '.' && isArray(ctx[ctx.length - 2])) {
92 | return ctx[ctx.length - 1];
93 | }
94 |
95 | for (var i = 1; i < names.length; i++) {
96 | if (val && typeof val == 'object' && names[i] in val) {
97 | cx = val;
98 | val = val[names[i]];
99 | } else {
100 | val = '';
101 | }
102 | }
103 |
104 | if (returnFound && !val) {
105 | return false;
106 | }
107 |
108 | if (!returnFound && typeof val == 'function') {
109 | ctx.push(cx);
110 | val = this.lv(val, ctx, partials);
111 | ctx.pop();
112 | }
113 |
114 | return val;
115 | },
116 |
117 | // find values with normal names
118 | f: function(key, ctx, partials, returnFound) {
119 | var val = false,
120 | v = null,
121 | found = false;
122 |
123 | for (var i = ctx.length - 1; i >= 0; i--) {
124 | v = ctx[i];
125 | if (v && typeof v == 'object' && key in v) {
126 | val = v[key];
127 | found = true;
128 | break;
129 | }
130 | }
131 |
132 | if (!found) {
133 | return (returnFound) ? false : "";
134 | }
135 |
136 | if (!returnFound && typeof val == 'function') {
137 | val = this.lv(val, ctx, partials);
138 | }
139 |
140 | return val;
141 | },
142 |
143 | // higher order templates
144 | ho: function(val, cx, partials, text, tags) {
145 | var t = val.call(cx, text, function(t) {
146 | return Hogan.compile(t, {delimiters: tags}).render(cx, partials);
147 | });
148 | var s = Hogan.compile(t.toString(), {delimiters: tags}).render(cx, partials);
149 | this.b = s;
150 | return false;
151 | },
152 |
153 | // higher order template result buffer
154 | b: '',
155 |
156 | // lambda replace section
157 | ls: function(val, ctx, partials, start, end, tags) {
158 | var cx = ctx[ctx.length - 1],
159 | t = val.call(cx);
160 |
161 | if (val.length > 0) {
162 | return this.ho(val, cx, partials, this.text.substring(start, end), tags);
163 | }
164 |
165 | if (typeof t == 'function') {
166 | return this.ho(t, cx, partials, this.text.substring(start, end), tags);
167 | }
168 |
169 | return t;
170 | },
171 |
172 | // lambda replace variable
173 | lv: function(val, ctx, partials) {
174 | var cx = ctx[ctx.length - 1];
175 | return Hogan.compile(val.call(cx).toString()).render(cx, partials);
176 | }
177 |
178 | };
179 |
180 | var rAmp = /&/g,
181 | rLt = //g,
183 | rApos =/\'/g,
184 | rQuot = /\"/g,
185 | hChars =/[&<>\"\']/;
186 |
187 | function hoganEscape(str) {
188 | str = String(str === null ? '' : str);
189 | return hChars.test(str) ?
190 | str
191 | .replace(rAmp,'&')
192 | .replace(rLt,'<')
193 | .replace(rGt,'>')
194 | .replace(rApos,''')
195 | .replace(rQuot, '"') :
196 | str;
197 | }
198 |
199 | var isArray = Array.isArray || function(a) {
200 | return Object.prototype.toString.call(a) === '[object Array]';
201 | };
202 |
203 | return constructor;
204 |
205 | })();
206 |
207 | var Hogan = (function () {
208 |
209 | // Setup regex assignments
210 | // remove whitespace according to Mustache spec
211 | var rIsWhitespace = /\S/,
212 | rQuot = /\"/g,
213 | rNewline = /\n/g,
214 | rCr = /\r/g,
215 | rSlash = /\\/g,
216 | tagTypes = {
217 | '#': 1, '^': 2, '/': 3, '!': 4, '>': 5,
218 | '<': 6, '=': 7, '_v': 8, '{': 9, '&': 10
219 | };
220 |
221 | function scan(text, delimiters) {
222 | var len = text.length,
223 | IN_TEXT = 0,
224 | IN_TAG_TYPE = 1,
225 | IN_TAG = 2,
226 | state = IN_TEXT,
227 | tagType = null,
228 | tag = null,
229 | buf = '',
230 | tokens = [],
231 | seenTag = false,
232 | i = 0,
233 | lineStart = 0,
234 | otag = '{{',
235 | ctag = '}}';
236 |
237 | function addBuf() {
238 | if (buf.length > 0) {
239 | tokens.push(new String(buf));
240 | buf = '';
241 | }
242 | }
243 |
244 | function lineIsWhitespace() {
245 | var isAllWhitespace = true;
246 | for (var j = lineStart; j < tokens.length; j++) {
247 | isAllWhitespace =
248 | (tokens[j].tag && tagTypes[tokens[j].tag] < tagTypes['_v']) ||
249 | (!tokens[j].tag && tokens[j].match(rIsWhitespace) === null);
250 | if (!isAllWhitespace) {
251 | return false;
252 | }
253 | }
254 |
255 | return isAllWhitespace;
256 | }
257 |
258 | function filterLine(haveSeenTag, noNewLine) {
259 | addBuf();
260 |
261 | if (haveSeenTag && lineIsWhitespace()) {
262 | for (var j = lineStart, next; j < tokens.length; j++) {
263 | if (!tokens[j].tag) {
264 | if ((next = tokens[j+1]) && next.tag == '>') {
265 | // set indent to token value
266 | next.indent = tokens[j].toString()
267 | }
268 | tokens.splice(j, 1);
269 | }
270 | }
271 | } else if (!noNewLine) {
272 | tokens.push({tag:'\n'});
273 | }
274 |
275 | seenTag = false;
276 | lineStart = tokens.length;
277 | }
278 |
279 | function changeDelimiters(text, index) {
280 | var close = '=' + ctag,
281 | closeIndex = text.indexOf(close, index),
282 | delimiters = trim(
283 | text.substring(text.indexOf('=', index) + 1, closeIndex)
284 | ).split(' ');
285 |
286 | otag = delimiters[0];
287 | ctag = delimiters[1];
288 |
289 | return closeIndex + close.length - 1;
290 | }
291 |
292 | if (delimiters) {
293 | delimiters = delimiters.split(' ');
294 | otag = delimiters[0];
295 | ctag = delimiters[1];
296 | }
297 |
298 | for (i = 0; i < len; i++) {
299 | if (state == IN_TEXT) {
300 | if (tagChange(otag, text, i)) {
301 | --i;
302 | addBuf();
303 | state = IN_TAG_TYPE;
304 | } else {
305 | if (text.charAt(i) == '\n') {
306 | filterLine(seenTag);
307 | } else {
308 | buf += text.charAt(i);
309 | }
310 | }
311 | } else if (state == IN_TAG_TYPE) {
312 | i += otag.length - 1;
313 | tag = tagTypes[text.charAt(i + 1)];
314 | tagType = tag ? text.charAt(i + 1) : '_v';
315 | if (tagType == '=') {
316 | i = changeDelimiters(text, i);
317 | state = IN_TEXT;
318 | } else {
319 | if (tag) {
320 | i++;
321 | }
322 | state = IN_TAG;
323 | }
324 | seenTag = i;
325 | } else {
326 | if (tagChange(ctag, text, i)) {
327 | tokens.push({tag: tagType, n: trim(buf), otag: otag, ctag: ctag,
328 | i: (tagType == '/') ? seenTag - ctag.length : i + otag.length});
329 | buf = '';
330 | i += ctag.length - 1;
331 | state = IN_TEXT;
332 | if (tagType == '{') {
333 | i++;
334 | }
335 | } else {
336 | buf += text.charAt(i);
337 | }
338 | }
339 | }
340 |
341 | filterLine(seenTag, true);
342 |
343 | return tokens;
344 | }
345 |
346 | function trim(s) {
347 | if (s.trim) {
348 | return s.trim();
349 | }
350 |
351 | return s.replace(/^\s*|\s*$/g, '');
352 | }
353 |
354 | function tagChange(tag, text, index) {
355 | if (text.charAt(index) != tag.charAt(0)) {
356 | return false;
357 | }
358 |
359 | for (var i = 1, l = tag.length; i < l; i++) {
360 | if (text.charAt(index + i) != tag.charAt(i)) {
361 | return false;
362 | }
363 | }
364 |
365 | return true;
366 | }
367 |
368 | function buildTree(tokens, kind, stack, customTags) {
369 | var instructions = [],
370 | opener = null,
371 | token = null;
372 |
373 | while (tokens.length > 0) {
374 | token = tokens.shift();
375 | if (token.tag == '#' || token.tag == '^' || isOpener(token, customTags)) {
376 | stack.push(token);
377 | token.nodes = buildTree(tokens, token.tag, stack, customTags);
378 | instructions.push(token);
379 | } else if (token.tag == '/') {
380 | if (stack.length === 0) {
381 | throw new Error('Closing tag without opener: /' + token.n);
382 | }
383 | opener = stack.pop();
384 | if (token.n != opener.n && !isCloser(token.n, opener.n, customTags)) {
385 | throw new Error('Nesting error: ' + opener.n + ' vs. ' + token.n);
386 | }
387 | opener.end = token.i;
388 | return instructions;
389 | } else {
390 | instructions.push(token);
391 | }
392 | }
393 |
394 | if (stack.length > 0) {
395 | throw new Error('missing closing tag: ' + stack.pop().n);
396 | }
397 |
398 | return instructions;
399 | }
400 |
401 | function isOpener(token, tags) {
402 | for (var i = 0, l = tags.length; i < l; i++) {
403 | if (tags[i].o == token.n) {
404 | token.tag = '#';
405 | return true;
406 | }
407 | }
408 | }
409 |
410 | function isCloser(close, open, tags) {
411 | for (var i = 0, l = tags.length; i < l; i++) {
412 | if (tags[i].c == close && tags[i].o == open) {
413 | return true;
414 | }
415 | }
416 | }
417 |
418 | function generate(tree, text, options) {
419 | var code = 'i = i || "";var c = [cx];var b = i + "";var _ = this;'
420 | + walk(tree)
421 | + 'return b;';
422 |
423 | if (options.asString) {
424 | return 'function(cx,p,i){' + code + ';}';
425 | }
426 |
427 | var template = new HoganTemplate(text);
428 | template.r = new Function('cx', 'p', 'i', code);
429 | return template;
430 | }
431 |
432 | function esc(s) {
433 | return s.replace(rSlash, '\\\\')
434 | .replace(rQuot, '\\\"')
435 | .replace(rNewline, '\\n')
436 | .replace(rCr, '\\r');
437 | }
438 |
439 | function chooseMethod(s) {
440 | return (~s.indexOf('.')) ? 'd' : 'f';
441 | }
442 |
443 | function walk(tree) {
444 | var code = '';
445 | for (var i = 0, l = tree.length; i < l; i++) {
446 | var tag = tree[i].tag;
447 | if (tag == '#') {
448 | code += section(tree[i].nodes, tree[i].n, chooseMethod(tree[i].n),
449 | tree[i].i, tree[i].end, tree[i].otag + " " + tree[i].ctag);
450 | } else if (tag == '^') {
451 | code += invertedSection(tree[i].nodes, tree[i].n,
452 | chooseMethod(tree[i].n));
453 | } else if (tag == '<' || tag == '>') {
454 | code += partial(tree[i]);
455 | } else if (tag == '{' || tag == '&') {
456 | code += tripleStache(tree[i].n, chooseMethod(tree[i].n));
457 | } else if (tag == '\n') {
458 | code += text('"\\n"' + (tree.length-1 == i ? '' : ' + i'));
459 | } else if (tag == '_v') {
460 | code += variable(tree[i].n, chooseMethod(tree[i].n));
461 | } else if (tag === undefined) {
462 | code += text('"' + esc(tree[i]) + '"');
463 | }
464 | }
465 | return code;
466 | }
467 |
468 | function section(nodes, id, method, start, end, tags) {
469 | return 'if(_.s(_.' + method + '("' + esc(id) + '",c,p,1),' +
470 | 'c,p,0,' + start + ',' + end + ', "' + tags + '")){' +
471 | 'b += _.rs(c,p,' +
472 | 'function(c,p){ var b = "";' +
473 | walk(nodes) +
474 | 'return b;});c.pop();}' +
475 | 'else{b += _.b; _.b = ""};';
476 | }
477 |
478 | function invertedSection(nodes, id, method) {
479 | return 'if (!_.s(_.' + method + '("' + esc(id) + '",c,p,1),c,p,1,0,0,"")){' +
480 | walk(nodes) +
481 | '};';
482 | }
483 |
484 | function partial(tok) {
485 | return 'b += _.rp("' + esc(tok.n) + '",c[c.length - 1],p,"' + (tok.indent || '') + '");';
486 | }
487 |
488 | function tripleStache(id, method) {
489 | return 'b += (_.' + method + '("' + esc(id) + '",c,p,0));';
490 | }
491 |
492 | function variable(id, method) {
493 | return 'b += (_.v(_.' + method + '("' + esc(id) + '",c,p,0)));';
494 | }
495 |
496 | function text(id) {
497 | return 'b += ' + id + ';';
498 | }
499 |
500 | return ({
501 | scan: scan,
502 |
503 | parse: function(tokens, options) {
504 | options = options || {};
505 | return buildTree(tokens, '', [], options.sectionTags || []);
506 | },
507 |
508 | cache: {},
509 |
510 | compile: function(text, options) {
511 | // options
512 | //
513 | // asString: false (default)
514 | //
515 | // sectionTags: [{o: '_foo', c: 'foo'}]
516 | // An array of object with o and c fields that indicate names for custom
517 | // section tags. The example above allows parsing of {{_foo}}{{/foo}}.
518 | //
519 | // delimiters: A string that overrides the default delimiters.
520 | // Example: "<% %>"
521 | //
522 | options = options || {};
523 |
524 | var t = this.cache[text];
525 |
526 | if (t) {
527 | return t;
528 | }
529 |
530 | t = generate(this.parse(scan(text, options.delimiters), options), text, options);
531 | return this.cache[text] = t;
532 | }
533 | });
534 | })();
535 |
536 | // Export the hogan constructor for Node.js and CommonJS.
537 | if (typeof module !== 'undefined' && module.exports) {
538 | module.exports = Hogan;
539 | module.exports.Template = HoganTemplate;
540 | } else if (typeof define === 'function' && define.amd) {
541 | define(function () { return Hogan; });
542 | } else if (typeof exports !== 'undefined') {
543 | exports.Hogan = Hogan;
544 | exports.HoganTemplate = HoganTemplate;
545 | }
546 |
--------------------------------------------------------------------------------