├── History.md
├── .gitignore
├── test
├── support
│ ├── via.js
│ ├── chai.js
│ ├── mocha.css
│ ├── mocha.js
│ ├── fake.resource.js
│ ├── fake.api.js
│ └── cats.api.js
├── browser
│ ├── data-list.test.js
│ ├── data-text.test.js
│ ├── data-toggle.test.js
│ ├── data-class.test.js
│ ├── index.html
│ ├── element.test.js
│ └── page.test.js
├── window.test.js
├── resource.test.js
├── utils.test.js
├── uri.test.js
└── object.test.js
├── index.js
├── lib
├── elements
│ ├── index.js
│ ├── page.js
│ └── page_links.js
├── attributes
│ ├── data-toggle.js
│ ├── index.js
│ ├── data-onclick.js
│ ├── data-val.js
│ ├── data-html.js
│ ├── data-text.js
│ ├── data-class.js
│ ├── data-require.js
│ ├── data-select.js
│ └── data-list.js
├── via.js
├── array.js
├── window.js
├── resource.js
├── events.js
├── uri.js
├── element.js
├── utils.js
└── object.js
├── package.json
├── components
└── component-domify
│ ├── component.json
│ └── index.js
├── Makefile
├── component.json
└── README.md
/History.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/test/support/via.js:
--------------------------------------------------------------------------------
1 | ../../build/via.js
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./lib/via');
2 |
--------------------------------------------------------------------------------
/test/support/chai.js:
--------------------------------------------------------------------------------
1 | ../../node_modules/chai/chai.js
--------------------------------------------------------------------------------
/test/support/mocha.css:
--------------------------------------------------------------------------------
1 | ../../node_modules/mocha/mocha.css
--------------------------------------------------------------------------------
/test/support/mocha.js:
--------------------------------------------------------------------------------
1 | ../../node_modules/mocha/mocha.js
--------------------------------------------------------------------------------
/lib/elements/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | page: require('./page')
3 | , page_links: require('./page_links')
4 | }
5 |
--------------------------------------------------------------------------------
/lib/attributes/data-toggle.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Make the element toggle a property when clicked.
3 | */
4 | module.exports = function(ui,attr) {
5 | this.addEventListener('click', function() {
6 | var cur = ui.data.get(attr);
7 | ui.data.set(attr, !cur);
8 | });
9 | };
10 |
11 |
--------------------------------------------------------------------------------
/lib/attributes/index.js:
--------------------------------------------------------------------------------
1 | var index = [
2 | 'list'
3 | , 'text'
4 | , 'html'
5 | , 'require'
6 | , 'class'
7 | , 'val'
8 | , 'toggle'
9 | , 'select'
10 | , 'onclick'
11 | ];
12 |
13 | for(var i=0,l=index.length; i < l; ++i) {
14 | module.exports[index[i]] = require('./data-'+index[i]);
15 | }
16 |
--------------------------------------------------------------------------------
/lib/attributes/data-onclick.js:
--------------------------------------------------------------------------------
1 | module.exports = function(ui,attr) {
2 |
3 | this.addEventListener('click', function(e) {
4 | var fn = ui.data.get(attr);
5 |
6 | console.log(fn,attr);
7 |
8 | if(typeof fn === 'function') {
9 | fn();
10 | }
11 | else {
12 | // error?
13 | }
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/lib/attributes/data-val.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Bind an input val to a synthetic attribute
3 | */
4 | module.exports = function(ui,value) {
5 | var elem = this;
6 |
7 | ui.data.watch(value, function(v) {
8 | $elem.val(v);
9 | });
10 |
11 | $elem.change(function() {
12 | console.log(ui, value, $elem.val());
13 | ui.data.set(value, $elem.val());
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/lib/via.js:
--------------------------------------------------------------------------------
1 | var Via = module.exports = {};
2 | Via.utils = require('./utils');
3 | Via.Object = require('./object');
4 | Via.Array = require('./array');
5 | Via.URI = require('./uri');
6 | Via.Resource = require('./resource');
7 | Via.Window = require('./window');
8 | Via.Element = require('./element');
9 |
10 | if(typeof window !== 'undefined') {
11 | Via.window = new Via.Window(window);
12 | }
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "via"
3 | , "version": "0.0.1"
4 | , "dependencies": {
5 | "request": "*"
6 | , "domify": "*"
7 | },
8 | "devDependencies": {
9 | "mocha": "1.12"
10 | , "mocha-phantomjs": "3.1"
11 | , "nib": "*"
12 | , "should": "*"
13 | , "expect": "*"
14 | , "chai": "*"
15 | , "stylus": "0.x.x"
16 | , "jade": "0.17.x"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/lib/attributes/data-html.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Bind the innerHTML of an element to a synthetic attribute
3 | */
4 | module.exports = function(ui,attr) {
5 | var elem = this;
6 |
7 | ui.data.watch(attr, function(value) {
8 | if(value.then) {
9 | value.then(function(html) {
10 | elem.innerHTML = html;
11 | });
12 | }
13 | else {
14 | elem.innerHTML = value;
15 | }
16 |
17 | });
18 | };
19 |
20 |
21 |
--------------------------------------------------------------------------------
/lib/attributes/data-text.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Bind the innerText of a single element to a synthetic attribute
3 | */
4 | module.exports = function(ui,value) {
5 | var elem = this;
6 |
7 | elem.addEventListener('click', function() {
8 | console.log(ui);
9 | });
10 |
11 | ui.data.watch(value, function(v,p) {
12 | if(v === null) v = '';
13 | elem.innerText = ''+v;
14 | elem.innerHTML = elem.innerHTML.replace(/\n/g,' ');
15 | });
16 | };
17 |
18 |
19 |
--------------------------------------------------------------------------------
/lib/attributes/data-class.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Bind a class name to a synthetic attribute
3 | */
4 | module.exports = function(ui,value) {
5 | var elem = this;
6 |
7 | var staticClass = elem.className;
8 | ui.data.watch(value, function(newV,preV) {
9 | var newClass = newV;
10 |
11 | var endOfPath = value.split('.').slice(-1)[0];
12 | if(newV === true) { newClass = endOfPath; }
13 | if(!newV) { newClass = '';}
14 |
15 | elem.className = [staticClass,newClass].join(' ').trim();
16 | });
17 | };
18 |
--------------------------------------------------------------------------------
/components/component-domify/component.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "domify",
3 | "version": "1.0.0",
4 | "description": "turn HTML into DOM elements",
5 | "scripts": [
6 | "index.js"
7 | ],
8 | "development": {
9 | "component/assert": "*",
10 | "visionmedia/mocha": "*",
11 | "visionmedia/mocha-cloud": "*"
12 | },
13 | "keywords": [
14 | "dom",
15 | "html",
16 | "client",
17 | "browser",
18 | "component"
19 | ],
20 | "license": "MIT",
21 | "repo": "https://raw.github.com/component/domify"
22 | }
--------------------------------------------------------------------------------
/test/browser/data-list.test.js:
--------------------------------------------------------------------------------
1 | describe('[data-list]', function() {
2 | // TODO: You should be able to use top level primitives
3 | // without wrapping them up in objects like this.
4 | var numbers = new Via.Array([{i:1},{i:2},{i:3}]);
5 |
6 | it('creates elements for each item', function() {
7 | var template = '
';
8 | var ui = new Via.Element({numbers: numbers}, template);
9 |
10 | var html = '';
11 | expect(ui.rootElement.outerHTML).eq(html);
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/lib/attributes/data-require.js:
--------------------------------------------------------------------------------
1 | module.exports = function(ui,attr) {
2 | var elem = this;
3 | var empty = this.querySelector('empty');
4 |
5 | if(empty) {
6 | empty.parentNode.removeChild(empty);
7 | }
8 |
9 | ui.data.watch(attr+'._http_status', function(status,prev) {
10 | if(status == 404) {
11 | elem.parentNode.insertBefore(empty, elem);
12 | elem.parentNode.removeChild(elem);
13 | }
14 | else if(prev == 404) {
15 | empty.parentNode.insertBefore(elem, empty);
16 | empty.parentNode.removeChild(empty);
17 | }
18 | });
19 | };
20 |
21 |
--------------------------------------------------------------------------------
/test/support/fake.resource.js:
--------------------------------------------------------------------------------
1 | if(typeof module !== 'undefined') {
2 | module.exports = FakeResource;
3 | ReactiveResource = require('../../lib/resource');
4 | FakeAPI = require('./fake.api');
5 | }
6 |
7 |
8 | function FakeResource(init,mock) {
9 | ReactiveResource.call(this, init);
10 |
11 | if(mock instanceof FakeAPI)
12 | this.FakeAPI = mock;
13 | else
14 | this.fakeAPI = new FakeAPI(mock);
15 | }
16 |
17 | FakeResource.prototype = new ReactiveResource({
18 | request: function() {
19 | return this.fakeAPI.request.apply(this.fakeAPI, arguments);
20 | }
21 | });
22 |
--------------------------------------------------------------------------------
/test/browser/data-text.test.js:
--------------------------------------------------------------------------------
1 | describe('[data-text]', function() {
2 | it('initializes', function() {
3 | var ui = new Via.Element({test:123}, 'empty
');
4 | expect(ui.rootElement.innerText).eq('123');
5 | });
6 |
7 | it('updates', function() {
8 | var ui = new Via.Element({}, 'empty
');
9 | ui.data.set('test', 123);
10 | expect(ui.rootElement.innerText).eq('123');
11 | });
12 |
13 | it('uses empty string for null', function() {
14 | var ui = new Via.Element({test:null}, 'empty
');
15 | expect(ui.rootElement.innerText).eq('');
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | STYLUS = ./node_modules/stylus/bin/stylus -u nib
2 | JS_SOURCES = *.js lib/*.js lib/elements/*.js lib/attributes/*.js
3 | HTML_SOURCES = lib/elements/templates/*.html
4 | COMPONENT_SOURCES = component.json $(JS_SOURCES)
5 |
6 | all: node_modules build/via.js
7 |
8 | build/via.js: $(COMPONENT_SOURCES)
9 | mkdir -p build
10 | component build -s Via -o build -n via
11 |
12 | test-client:
13 | @node_modules/.bin/mocha test -R list
14 |
15 | test-browser: build/via.js
16 | @node_modules/.bin/mocha-phantomjs -R list test/browser/index.html
17 |
18 | test: test-client test-browser
19 |
20 | clean:
21 | rm -fr build components template.js
22 |
23 | .PHONY: clean test test-browser test-client
24 |
--------------------------------------------------------------------------------
/test/window.test.js:
--------------------------------------------------------------------------------
1 | describe('Via.Window', function() {
2 | var Via = require('../lib/via'),
3 | expect = require('chai').expect;
4 |
5 | var rwin;
6 | beforeEach(function() {
7 | var fakeWindow = {
8 | location: {
9 | href: 'http://example.net/path?hello=world'
10 | }
11 | , addEventListener: function() {}
12 | };
13 | rwin = new Via.Window(fakeWindow);
14 | });
15 |
16 | it('gets url from location', function() {
17 | expect(rwin.location.href).eq(rwin.actual.location.href);
18 | });
19 |
20 | it('treats initial params as defaults', function() {
21 | });
22 |
23 | it('does a pushState when query params change', function() {
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/lib/array.js:
--------------------------------------------------------------------------------
1 | module.exports = ReactiveArray;
2 |
3 | var ReactiveObject = require('./object')
4 | , utils = require('./utils');
5 |
6 | function ReactiveArray(obj) {
7 | this.set(obj);
8 |
9 | this.synth('length', /\n+/, function(i) {
10 | return Math.max(this.root.length, i);
11 | });
12 |
13 | this.watch('length', function(newv,prev) {
14 | for(var i=newv, l=prev; i < l; ++i) {
15 | this.root.set(i, undefined);
16 | }
17 | });
18 | }
19 |
20 | ReactiveArray.prototype = new ReactiveObject({
21 | slice: function(start,end) {
22 | var result = new ReactiveArray();
23 | this.watch(/\n+/, function(i,v) {
24 | result.set(i,v);
25 | });
26 | return result;
27 | }
28 | });
29 |
--------------------------------------------------------------------------------
/test/browser/data-toggle.test.js:
--------------------------------------------------------------------------------
1 | describe('[data-toggle]', function() {
2 | it('toggles a boolean property when clicked', function() {
3 | var ui = new Via.Element({}, 'empty
');
4 | click(ui.rootElement);
5 | expect(ui.data.bool).eq(true);
6 | click(ui.rootElement);
7 | expect(ui.data.bool).eq(false);
8 | });
9 | });
10 |
11 | function click(el){
12 | var ev = document.createEvent("MouseEvent");
13 | ev.initMouseEvent(
14 | "click",
15 | true /* bubble */, true /* cancelable */,
16 | window, null,
17 | 0, 0, 0, 0, /* coordinates */
18 | false, false, false, false, /* modifier keys */
19 | 0 /*left*/, null
20 | );
21 | el.dispatchEvent(ev);
22 | }
23 |
--------------------------------------------------------------------------------
/lib/elements/page.js:
--------------------------------------------------------------------------------
1 | module.exports = function page() {
2 |
3 | this.data.synth('page', 'src');
4 | this.data.synth('page.size', 'size');
5 | this.data.synth('page.number', 'number');
6 | this.data.set('page.number', this.data.page.number || 1);
7 |
8 | this.data.synth('page.length', 'page.number page.size page.total',
9 | function(n,s,t) {
10 | if(n * s >= t) {
11 | return t - n * s;
12 | }
13 | return s;
14 | });
15 |
16 | this.data.synth('info', 'page.total page.size page.number',
17 | function(total, size, n) {
18 | if(total <= size)
19 | return 'Showing ' + total + ' of ' + total;
20 |
21 | var a = (n-1)*size+1;
22 | var b = Math.min(n*size, total);
23 |
24 | return 'Showing ' + a + '-' + b + ' of ' + total;
25 | });
26 | };
27 |
28 |
--------------------------------------------------------------------------------
/test/support/fake.api.js:
--------------------------------------------------------------------------------
1 | if(typeof module !== 'undefined') {
2 | module.exports = FakeAPI;
3 | }
4 |
5 | function FakeAPI(mock) {
6 | this.mock = mock || {};
7 | this.baseUrl = '';
8 | }
9 |
10 | FakeAPI.prototype.request = function(method, uri, data, callback) {
11 | var str = ''+method+' '+uri;
12 |
13 | if(!this.mock.hasOwnProperty(str)) {
14 | throw 'fake API "'+str+'" not mocked';
15 | }
16 | else {
17 | var res = this.mock[str];
18 | if(typeof res === 'function') {
19 | res = res(data);
20 | }
21 | res = res || {};
22 |
23 | setTimeout(function() {
24 | if(res.error) {
25 | callback(res.error);
26 | }
27 | else {
28 | res.meta = res.meta || {};
29 | res.meta.status = res.meta.status || 200;
30 | callback(null, res);
31 | }
32 | });
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/lib/attributes/data-select.js:
--------------------------------------------------------------------------------
1 | var ReactiveElement = require('../element');
2 |
3 |
4 | /**
5 | * Bind a property to selection which can be set by clicking
6 | * other [data-select="invoice"]
7 | * TODO: Use delegation on the element associated
8 | * with the data that actually changes.
9 | * The path tells us the delegation root!
10 | */
11 |
12 | module.exports = function(ui,attr) {
13 |
14 | ui.data.watch('parent.selection', function(newV, preV) {
15 |
16 | if( same(ui.data.get(attr), newV) ) {
17 | ui.data.set('selected', true);
18 | }
19 | else {
20 | ui.data.set('selected', false);
21 | }
22 | });
23 |
24 | this.addEventListener('click', function(event) {
25 | ui.data.set('parent.selection', ui.data.get(attr));
26 | });
27 | }
28 |
29 | function same(a,b) {
30 | if(a === b) return true;
31 |
32 | if(a && typeof a.compare === 'function') {
33 | return a.compare(b);
34 | }
35 |
36 | return false;
37 | }
38 |
--------------------------------------------------------------------------------
/test/browser/data-class.test.js:
--------------------------------------------------------------------------------
1 | describe('[data-class]', function() {
2 | it('adds a class named after the value of the property', function() {
3 | var ui = new Via.Element({hello:'world'}, 'empty
');
4 | expect(ui.rootElement.className).eq('dontmindme world');
5 | });
6 |
7 | it('treats boolean values as a class toggle of the property name', function() {
8 | var ui = new Via.Element({hello:true}, 'empty
');
9 | expect(ui.rootElement.className).eq('dontmindme hello');
10 | ui.data.set('hello', false);
11 | expect(ui.rootElement.className).eq('dontmindme');
12 | });
13 |
14 | it('only uses the last property in a path for boolean classes', function() {
15 | var ui = new Via.Element({classes:{hello:true}}, 'empty
');
16 | expect(ui.rootElement.className).eq('dontmindme hello');
17 | });
18 |
19 |
20 | });
21 |
--------------------------------------------------------------------------------
/component.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "via",
3 | "description": "RFP library for node and browser",
4 | "repo": "em/via",
5 | "version": "0.0.0",
6 | "keywords": [],
7 | "dependencies": {
8 | "component/domify": "1.0.0"
9 | },
10 | "development": {},
11 | "license": "MIT",
12 | "scripts": [
13 | "index.js",
14 | "lib/via.js",
15 | "lib/utils.js",
16 | "lib/events.js",
17 | "lib/object.js",
18 | "lib/array.js",
19 | "lib/resource.js",
20 | "lib/uri.js",
21 | "lib/window.js",
22 | "lib/element.js",
23 | "lib/elements/index.js",
24 | "lib/elements/page.js",
25 | "lib/elements/page_links.js",
26 | "lib/attributes/index.js",
27 | "lib/attributes/data-class.js",
28 | "lib/attributes/data-text.js",
29 | "lib/attributes/data-html.js",
30 | "lib/attributes/data-list.js",
31 | "lib/attributes/data-require.js",
32 | "lib/attributes/data-val.js",
33 | "lib/attributes/data-toggle.js",
34 | "lib/attributes/data-select.js",
35 | "lib/attributes/data-onclick.js"
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/test/support/cats.api.js:
--------------------------------------------------------------------------------
1 | if(typeof module !== 'undefined') {
2 | FakeAPI = require('./fake.api');
3 | }
4 |
5 | catsAPI = new FakeAPI({
6 | // Model stuff
7 | 'GET cats/1.json': {
8 | cat: {id:1, name:'Fluffy'}
9 | },
10 | 'PUT cats/1.json': function(data) {
11 | data.cat.was = "updated";
12 | return data;
13 | },
14 | 'GET cats/2.json': {
15 | cat: {id:2, name: 'Scratch'}
16 | },
17 |
18 | // Collection stuff
19 | 'GET cats/': function(params) {
20 | var cursor = params.cursor || 0;
21 | params.per_page = parseInt(params.per_page)
22 | if(params.page && params.per_page) {
23 | cursor = (params.page-1) * params.per_page;
24 | }
25 |
26 | var response = {
27 | cats: [
28 | {id:1, name: 'Fluffy'},
29 | {id:2, name: 'Scratch'},
30 | {id:3, name: 'Leonard'}
31 | ].slice(cursor,cursor+params.per_page),
32 | meta: {
33 | status: 200
34 | , count: 3
35 | }
36 | };
37 |
38 | return response;
39 | },
40 | 'POST cats/': function() {
41 | return {cat: {id: 2, was:'created'}}
42 | },
43 |
44 | });
45 |
46 | if(typeof module !== 'undefined') {
47 | module.exports = catsAPI;
48 | }
49 |
--------------------------------------------------------------------------------
/lib/window.js:
--------------------------------------------------------------------------------
1 | var ReactiveObject = require('./object'),
2 | ReactiveURI = require('./uri');
3 |
4 | module.exports = ReactiveWindow;
5 |
6 | function ReactiveWindow(window) {
7 | this.actual = window;
8 | this.location = new ReactiveURI({href:window.location.href});
9 |
10 | var self = this;
11 | var pushState = new ReactiveURI(window.location);
12 | var popping = false;
13 | window.addEventListener('popstate', function() {
14 | popping = true;
15 | self.location.set('href', window.location.href);
16 | popping = false;
17 | });
18 |
19 | pushState.watch('pathname search',
20 | function(path,search) {
21 | if(!popping) {
22 | window.history.pushState(null, null, path+search);
23 | }
24 | });
25 |
26 | var defaultParams = {};
27 | this.watch('location.params', function(params) {
28 | var uniqueParams = {};
29 |
30 | for(var k in params) {
31 | defaultParams[k] = defaultParams[k] || params[k];
32 |
33 | if(defaultParams[k] != params[k]) {
34 | uniqueParams[k] = params[k];
35 | }
36 | }
37 |
38 | pushState.set('params', uniqueParams);
39 | });
40 | }
41 |
42 | ReactiveWindow.prototype = new ReactiveObject();
43 |
--------------------------------------------------------------------------------
/test/browser/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/lib/attributes/data-list.js:
--------------------------------------------------------------------------------
1 | var ReactiveElement = require('../element')
2 | , utils = require('../utils');
3 |
4 | /**
5 | * Bind a collection to a repeating block scoped per item
6 | */
7 | module.exports = function(ui,value,template) {
8 | var target = this;
9 | target.innerHTML = '';
10 |
11 | ui.data.watch(value, function(collection) {
12 | if(!collection) return;
13 |
14 | // Remove any element beyond the collection length
15 | collection.watch('length',function(length) {
16 | for(var i=length, l = target.children.length-1; l >= i; --l) {
17 | target.removeChild(target.children[l]);
18 | }
19 | });
20 |
21 | // Update or append as any numeric index changes
22 | collection.watch(/\d+/,function(i,item,prev) {
23 | if(item === undefined) return;
24 |
25 | var oldElem = target.children[i];
26 |
27 | if(item) {
28 | var itemui = new ReactiveElement(item, template);
29 | itemui.data.set('parent', ui.data);
30 |
31 | if(oldElem) {
32 | target.insertBefore(itemui.rootElement, oldElem);
33 | target.removeChild(oldElem);
34 | }
35 | else {
36 | target.appendChild(itemui.rootElement);
37 | }
38 | }
39 | });
40 |
41 | })
42 | };
43 |
44 |
45 |
--------------------------------------------------------------------------------
/lib/resource.js:
--------------------------------------------------------------------------------
1 | module.exports = ReactiveResource;
2 |
3 | var ReactiveObject = require('./object')
4 | , ReactiveURI = require('./uri')
5 | , utils = require('./utils');
6 |
7 |
8 | function ReactiveResource(init) {
9 | this.set(init);
10 | this.location = new ReactiveURI(this.location);
11 | this.synth('url', 'location.href');
12 |
13 | // this.watch('autoload url', function(autoload, url) {
14 | // if(autoload) this.root.reload();
15 | // });
16 |
17 | this.watch('autosave data', function(autosave, data) {
18 | if(autosave) this.root.save();
19 | });
20 |
21 | this.synth('id', 'location.file');
22 | }
23 |
24 | ReactiveResource.prototype = new ReactiveObject({
25 | save: function(callback) {
26 | this.set('invalid', true);
27 |
28 | if(this.url) {
29 | var body = this.format.stringify(res);
30 | this.request('put', body, function(err, res) {
31 | this.set('response', this.format.parse(res));
32 | });
33 | }
34 | else if(this.parent) {
35 | var body = this.format.stringify(res);
36 | this.request('post', body, function(err, res) {
37 | this.set('response', this.format.parse(res));
38 | });
39 | }
40 |
41 | }
42 | , load: function(callback) {
43 | if(this.__loaded__) return;
44 | this.__loaded__ = true;
45 |
46 | var self = this;
47 | this.watch('url', function(url) {
48 | self.request('get', url, null, function(err, res) {
49 | self.set('response', res);
50 | callback && callback(err, res);
51 | });
52 | });
53 | }
54 | , reload: function() {
55 | this.set('outdated', true);
56 | }
57 | , request: function(method, url, data, callback) {
58 | // var request = require('request');
59 | // api.request(method, url, data, callback);
60 | }
61 | , format: JSON
62 | });
63 |
--------------------------------------------------------------------------------
/test/resource.test.js:
--------------------------------------------------------------------------------
1 | describe('Via.Resource', function() {
2 | var Via = require('../lib/via'),
3 | expect = require('chai').expect,
4 | FakeResource = require('./support/fake.resource');
5 |
6 | beforeEach(function() {
7 | });
8 |
9 | var mock = {
10 | 'get http://example.net/greeting.json': {
11 | hello: 'world'
12 | }
13 | };
14 |
15 | it('binds url and location.href', function() {
16 | var url = 'http://example.net/greeting.json';
17 | var r = new FakeResource({
18 | url: url
19 | });
20 | expect(url).eq(r.url).eq(r.location.href);
21 | });
22 |
23 | it('loads', function(done) {
24 | var r = new FakeResource({
25 | url: 'http://example.net/greeting.json'
26 | }, mock);
27 |
28 | r.load(function() {
29 | expect(r.response.hello).eq('world');
30 | done();
31 | });
32 | });
33 |
34 | it('can autoload when params changes', function(done) {
35 | var stub = {
36 | 'get http://example.net/greeting.json': {
37 | hello:'world'
38 | }
39 | , 'get http://example.net/greeting.json?message=dude': {
40 | hello:'dude'
41 | }
42 | };
43 |
44 | var resource = new FakeResource({
45 | url: 'http://example.net/greeting.json'
46 | }, stub);
47 |
48 | resource.load();
49 |
50 | var count = 0;
51 | resource.watch('response.hello', function(hello) {
52 | count++;
53 |
54 | if(count === 1) {
55 | expect(hello).eq('world');
56 | }
57 | if(count === 2) {
58 | expect(hello).eq('dude');
59 | done();
60 | }
61 | });
62 |
63 | resource.set('location.params.message', 'dude');
64 | });
65 |
66 | it('default post does http post', function() {
67 | });
68 |
69 | it('default get does http get', function() {
70 | });
71 |
72 | });
73 |
--------------------------------------------------------------------------------
/lib/elements/page_links.js:
--------------------------------------------------------------------------------
1 | module.exports = function(ui,attrs) {
2 |
3 | // TODO: Should this be a generic thing?
4 | // Maybe this elem exists outside of page,
5 | // but nesting only makes "page" default to parent
6 | ui.data.synth('page', attrs.page || 'parent.page');
7 |
8 | ui.rootElement.addEventListener('click', function(event) {
9 | ui.data.set('page.number', event.target.getAttribute('data-page'));
10 | ui.data.set('selected', event.target);
11 | });
12 |
13 | ui.data.synth('links', 'page.total page.size page.number', function(total, size, n) {
14 | if(total <= size) return '';
15 |
16 | n = parseInt(n);
17 |
18 | var result = '';
19 | var l = Math.ceil(total/size);
20 | var max = 5;
21 | var start = Math.max(1, n - max);
22 | var end = Math.min(start+l, start+max);
23 | var hops = {};
24 |
25 | if(start > 1) {
26 | hops[1] = start+1;
27 | }
28 |
29 | if(end < l) {
30 | hops[end] = l-1;
31 | }
32 |
33 | if(l > 1) {
34 | for(var i = 1; i <= l; ++i) {
35 | if(i === n) {
36 | result += ''+i+' ';
37 | }
38 | else {
39 | result += ''+i+' ';
40 | }
41 |
42 | if(hops[i]) {
43 | result += '… '
44 | i = hops[i];
45 | }
46 | }
47 | }
48 |
49 | if(n !== 1) {
50 | result = 'Prev ' + result;
51 | }
52 | else {
53 | result = 'Prev ' + result;
54 | }
55 |
56 | if(n < l) {
57 | var i = n+1;
58 | result += 'Next ';
59 | }
60 | else {
61 | result += 'Next ';
62 | }
63 |
64 | return result;
65 | });
66 | };
67 |
68 | module.exports.template = '
';
69 |
--------------------------------------------------------------------------------
/components/component-domify/index.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * Expose `parse`.
4 | */
5 |
6 | module.exports = parse;
7 |
8 | /**
9 | * Wrap map from jquery.
10 | */
11 |
12 | var map = {
13 | option: [1, '', ' '],
14 | optgroup: [1, '', ' '],
15 | legend: [1, '', ' '],
16 | thead: [1, ''],
17 | tbody: [1, ''],
18 | tfoot: [1, ''],
19 | colgroup: [1, ''],
20 | caption: [1, ''],
21 | tr: [2, ''],
22 | td: [3, ''],
23 | th: [3, ''],
24 | col: [2, ''],
25 | _default: [0, '', '']
26 | };
27 |
28 | /**
29 | * Parse `html` and return the children.
30 | *
31 | * @param {String} html
32 | * @return {Array}
33 | * @api private
34 | */
35 |
36 | function parse(html) {
37 | if ('string' != typeof html) throw new TypeError('String expected');
38 |
39 | // tag name
40 | var m = /<([\w:]+)/.exec(html);
41 | if (!m) throw new Error('No elements were generated.');
42 | var tag = m[1];
43 |
44 | // body support
45 | if (tag == 'body') {
46 | var el = document.createElement('html');
47 | el.innerHTML = html;
48 | return el.removeChild(el.lastChild);
49 | }
50 |
51 | // wrap map
52 | var wrap = map[tag] || map._default;
53 | var depth = wrap[0];
54 | var prefix = wrap[1];
55 | var suffix = wrap[2];
56 | var el = document.createElement('div');
57 | el.innerHTML = prefix + html + suffix;
58 | while (depth--) el = el.lastChild;
59 |
60 | var els = el.children;
61 | if (1 == els.length) {
62 | return el.removeChild(els[0]);
63 | }
64 |
65 | var fragment = document.createDocumentFragment();
66 | while (els.length) {
67 | fragment.appendChild(el.removeChild(els[0]));
68 | }
69 |
70 | return fragment;
71 | }
72 |
--------------------------------------------------------------------------------
/lib/events.js:
--------------------------------------------------------------------------------
1 | var utils = require('./utils');
2 |
3 | module.exports = Events;
4 |
5 | /**
6 | * Simple lightweight event emmitter
7 | */
8 |
9 | function Events(){};
10 |
11 | Events.prototype = {
12 | on: function(events, fct){
13 | if(!this.hasOwnProperty('_events')) {
14 | this._events = {};
15 | }
16 |
17 | var split = events.split(' ');
18 | for(var i = 0, l = split.length; i 1) {
51 | for(var i = 0, l = split.length; i < l; ++i) {
52 | event = split[i];
53 | this.trigger.apply(this, arguments);
54 | }
55 | return;
56 | }
57 |
58 | if( event in this._events === false ) return;
59 | var i = this._events[event].length, ret;
60 | while(i-- && ret !== false) {
61 | ret = this._events[event][i].apply(this, Array.prototype.slice.call(arguments, 1));
62 | }
63 |
64 | return ret;
65 | }
66 | };
67 |
68 | Events.mixin = function(destObject){
69 | var props = ['on', 'once', 'unbind', 'trigger'];
70 | var proto = destObject.prototype || destObject;
71 | for(var i = 0; i < props.length; i ++){
72 | proto[props[i]] = this.prototype[props[i]];
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/test/utils.test.js:
--------------------------------------------------------------------------------
1 | var utils = require('../lib/utils.js'),
2 | expect = require('chai').expect;
3 |
4 | describe('utils#traverse', function(){
5 | it('accesses simple direct properties', function() {
6 | var test = {a: 123};
7 | expect( utils.traverse(test, 'a') ).eq(123);
8 | });
9 |
10 | it('accesses deeply nested properties', function() {
11 | var test = {a: {b: 123}};
12 | expect( utils.traverse(test, 'a.b') ).eq(123);
13 | });
14 |
15 | it('returns undefined for non-existent paths', function() {
16 | var test = {a: {b: 123}};
17 | expect( utils.traverse(test, 'a.c') ).eq(undefined);
18 | });
19 |
20 | it('returns undefined when non-object nodes in path', function() {
21 | var test = {a: undefined};
22 | expect( utils.traverse(test, 'a.b') ).eq(undefined);
23 | });
24 |
25 | it('calls function for every object in path', function() {
26 | var test = {a: {b: 123}};
27 | var sequence = [
28 | [test, 'a', 'b.c'],
29 | [test.a, 'b', 'c'],
30 | [test.a.b, 'c', false]
31 | ];
32 |
33 | var i = 0;
34 | utils.traverse(test, 'a.b.c', function(obj,key,
35 | nearestEmitter, shortestPath, remaining) {
36 | expect( [obj, key, remaining] ).eql( sequence[i++] );
37 | });
38 | });
39 |
40 | it('passes nearest emitter and path from it', function() {
41 | var test = {
42 | a: {
43 | trigger: function(){},
44 | b: {
45 | c: 123
46 | }
47 | }
48 | };
49 |
50 | var sequence = [
51 | [test, 'a', undefined, undefined, 'b.c']
52 | , [test.a, 'b', test.a, 'b', 'c']
53 | , [test.a.b, 'c', test.a, 'b.c', false]
54 | ];
55 |
56 | var i = 0;
57 | utils.traverse(test, 'a.b.c', function(deepObj, deepAttr,
58 | nearestEmitter, shortestPath, remaining) {
59 | expect( arguments ).eql( sequence[i++] );
60 | });
61 | });
62 |
63 | it('stops traversal and returns current value if callback returns false', function() {
64 | var test = {a: {b: 123}};
65 |
66 | var count = 0;
67 | var result = utils.traverse(test, 'a.b', function() {
68 | expect(count).eq(0);
69 | count++;
70 | return false;
71 | });
72 |
73 | expect(result).eq(test);
74 | });
75 | });
76 |
77 |
--------------------------------------------------------------------------------
/test/browser/element.test.js:
--------------------------------------------------------------------------------
1 | describe('Via.Element', function() {
2 | it('accepts dom element as template using its innerHTML', function() {
3 | var elem = document.createElement('test');
4 | var ui = new Via.Element({}, elem);
5 | expect(ui.rootElement.outerHTML).eq(' ');
6 | });
7 |
8 | it('uses default template defined on custom element', function() {
9 | var custom = function() {};
10 | custom.template = '
';
11 | var template = ' ';
12 | var ui = new Via.Element({}, {elements: {custom: custom}, template: template});
13 | expect(ui.rootElement.outerHTML).eq(custom.template);
14 | });
15 |
16 | it('can be replaced directly with another custom element', function() {
17 | var a = function() {};
18 | a.template = '
';
19 | var b = function() {};
20 | b.template = '
';
21 |
22 |
23 | var template = ' ';
24 | var ui = new Via.Element({}, {elements: {a:a,b:b}, template: template});
25 | expect(ui.rootElement.outerHTML).eq(b.template);
26 | });
27 |
28 | it('accepts template as second argument', function() {
29 | var template = ' ';
30 | var ui = new Via.Element({}, template);
31 | expect(ui.rootElement.outerHTML).eq(template);
32 | });
33 |
34 | it('can handle descendent custom elements', function() {
35 | var template = '
';
36 | var custom = function() {
37 | return '
';
38 | };
39 | var ui = new Via.Element({}, {elements: {custom: custom}, template: template});
40 | expect(ui.rootElement.outerHTML).eq('');
41 | });
42 |
43 | it('can handle custom attrs on root element', function() {
44 | var template = '
';
45 | var ui = new Via.Element({test:123}, template);
46 | expect(ui.rootElement.outerHTML).eq('123
');
47 | });
48 |
49 | it('can handle custom element as root', function() {
50 | var template = ' ';
51 | var custom = function(ui, data) {
52 | return '
';
53 | };
54 | var ui = new Via.Element({}, {elements: {custom: custom}, template: template});
55 | expect(ui.rootElement.outerHTML).eq('
');
56 | });
57 |
58 | it('synthesizes element data from attributes', function() {
59 | var template = ' ';
60 | var custom = function(ui, data) {
61 | return '
';
62 | };
63 | var ui = new Via.Element({parent:{path: 'value'}}, {elements: {custom: custom}, template: template});
64 | expect(ui.data.a).eq('value');
65 | expect(ui.data.b).eq('literal');
66 | });
67 |
68 | });
69 |
--------------------------------------------------------------------------------
/test/uri.test.js:
--------------------------------------------------------------------------------
1 | describe('Via.URI', function() {
2 | var Via = require('../lib/via'),
3 | expect = require('chai').expect;
4 |
5 | var uri;
6 | beforeEach(function() {
7 | uri = new Via.URI({});
8 | });
9 |
10 | // it('parses authority', function() {
11 | // uri.set('authority', '//user:pass@example.net');
12 | // expect(uri.host).eq('example.net');
13 | // console.log(uri);
14 | // expect(uri.credentials).eq('user:pass');
15 | // uri.set('authority', '//apikey@example.net');
16 | // expect(uri.credentials).eq('apikey');
17 | // uri.set('authority', 'missingslashes.net');
18 | // expect(uri.host).eq('missingslashes.net');
19 | // expect(uri.authority).eq('//missingslashes.net');
20 | // });
21 |
22 | // it('builds authority', function() {
23 | // uri.set({
24 | // credentials: 'user:pass'
25 | // , host: 'example.net'
26 | // });
27 |
28 | // expect(uri.authority).eq('//user:pass@example.net');
29 | // });
30 |
31 |
32 | // it('host via hostname and port', function() {
33 | // uri.set({
34 | // hostname: 'example.net'
35 | // , port: '8080'
36 | // });
37 | // expect(uri.host).eq('example.net:8080');
38 | // });
39 |
40 | // it('host via hostname with no port', function() {
41 | // uri.set({
42 | // hostname: 'example.net'
43 | // });
44 | // expect(uri.host).eq('example.net');
45 | // });
46 | //
47 | // it('hostname and port via host', function() {
48 | // uri.set('host', 'example.net:8080');
49 | // expect(uri.hostname).eq('example.net');
50 | // expect(uri.port).eq('8080');
51 | // });
52 |
53 | it('query via search', function() {
54 | uri.set('search', '?a=1#fragment');
55 | expect(uri.query).eq('a=1');
56 | });
57 |
58 | it('search via query', function() {
59 | uri.set('query', 'a=1');
60 | expect(uri.search).eq('?a=1');
61 | });
62 |
63 | it('query via params', function() {
64 | uri.set('params', {a:1,b:2});
65 | expect(uri.query).eq('a=1&b=2');
66 | });
67 |
68 | it('params via query', function() {
69 | uri.set('query', 'a=1&b=2');
70 | expect(uri.params).eql({a:'1',b:'2'});
71 | });
72 |
73 | it('parses href', function() {
74 | uri.set('href', 'http://user:pass@example.net:8080/oh/hi.txt?q=there#lol');
75 | expect(uri.scheme).eq('http');
76 | expect(uri.authority).eq('user:pass@example.net:8080');
77 | expect(uri.path).eq('/oh/hi.txt');
78 | expect(uri.query).eq('q=there');
79 | expect(uri.search).eq('?q=there');
80 | expect(uri.fragment).eq('lol');
81 | });
82 |
83 | it('builds href', function() {
84 | uri.set({
85 | scheme: 'http'
86 | , authority: 'user:pass@example.net:8080'
87 | , path: '/oh/hi.txt'
88 | , query: 'q=there'
89 | , fragment: 'lol'
90 | });
91 | expect(uri.href).eq('http://user:pass@example.net:8080/oh/hi.txt?q=there#lol');
92 | });
93 | });
94 |
--------------------------------------------------------------------------------
/lib/uri.js:
--------------------------------------------------------------------------------
1 | module.exports = ReactiveURI;
2 |
3 | var ReactiveObject = require('./object'),
4 | utils = require('./utils');
5 |
6 | // RFC 3986 Appendix B
7 | var regex = new RegExp("^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?");
8 | // scheme = $2
9 | // authority = $4
10 | // path = $5
11 | // query = $7
12 | // fragment = $9
13 |
14 | function ReactiveURI(init) {
15 | if(typeof init === "string") init = {href: init};
16 |
17 | this.set(init);
18 |
19 | this.synth('href', 'scheme authority path query? fragment?',
20 | function(scheme, authority, path, query, fragment) {
21 | // RFC 3986 Section 5.3
22 | var result = '';
23 |
24 | if(scheme)
25 | result += scheme + ':';
26 | if(authority)
27 | result += '//' + authority;
28 | result += path;
29 | if(query)
30 | result += '?' + query;
31 | if(fragment)
32 | result += '#' + fragment;
33 |
34 | return result;
35 | });
36 |
37 | this.synth('scheme authority path query fragment', 'href',
38 | function(href) {
39 | var m = regex.exec(href);
40 | return [m[2], m[4], m[5], m[7], m[9]];
41 | });
42 |
43 | this.synth('path', 'dir file ext?', function(dir, file, ext) {
44 | return [dir, file].join('') + (ext ? '.' + ext : '');
45 | });
46 |
47 | this.synth('dir file ext', 'path', function(path) {
48 | var parts = path.split('/');
49 | var file = parts[parts.length-1];
50 | if(file) {
51 | parts.pop();
52 | parts.push('');
53 | return [parts.join('/')].push( file.split('.').slice(-1) );
54 | }
55 |
56 | return [parts.join('/')];
57 | });
58 |
59 | this.synth('authority', 'credentials host', function(c,h) {
60 | c = c && (c + '@') || '';
61 | return c + h;
62 | });
63 |
64 | this.synth('credentials host', 'authority', function(auth) {
65 | var match = auth.match(/(?:(.+?)@)?(.*)/);
66 | return match && match.slice(1);
67 | });
68 |
69 | this.synth('query', 'search', function(search, prev) {
70 | var match = (''+search).match(/\?(.+?)(?:#|$)/);
71 | return match && match[1] || '';
72 | });
73 |
74 | this.synth('search', 'query', function(query) {
75 | if(!query) return '';
76 | return '?'+query;
77 | });
78 |
79 | this.synth('query', 'params', function(params) {
80 | var str = [];
81 | for(var p in params) {
82 | if(params[p] !== undefined) {
83 | str.push(encodeURIComponent(p) + "=" + encodeURIComponent(params[p]));
84 | }
85 | }
86 | return str.join("&");
87 | });
88 |
89 | this.synth('params', 'query', function(query) {
90 | if(!query) return {};
91 |
92 | query = query.replace('+',' ');
93 |
94 | var params = {}, tokens,
95 | re = /[?&]?([^=]+)=([^&]*)/g;
96 |
97 | while (tokens = re.exec(query)) {
98 | params[decodeURIComponent(tokens[1])]
99 | = decodeURIComponent(tokens[2]);
100 | }
101 |
102 | return params;
103 | });
104 |
105 | }
106 |
107 | ReactiveURI.prototype = new ReactiveObject({
108 | rel: function(path) {
109 | var result = new ReactiveURI({source:this});
110 | this.watch('href', function(href) {
111 | result.set('href', href);
112 | });
113 | result.synth('path', 'source.path', function(base) {
114 | return absolute(base, path);
115 | });
116 | return result;
117 | }
118 | });
119 |
120 | function absolute(base, relative) {
121 | var stack = base.split("/"),
122 | parts = relative.split("/");
123 | stack.pop(); // remove current file name (or empty string)
124 | // (omit if "base" is the current folder without trailing slash)
125 | for (var i=0; i 2.055555555555556
61 |
62 | ```
63 |
64 | This also provides us with a means of separating concerns, where
65 | independent parts of code can manipulate each other's
66 | data through a common interface.
67 |
68 | e.g. We can independently model a resistor:
69 |
70 | ```
71 | function Resistor(init) {
72 | this.set(init);
73 |
74 | this.synth('color_bands', 'resistance', function(i) {
75 | // ...
76 | });
77 |
78 | this.synth('resistance', 'color_bands', function(i) {
79 | // ...
80 | });
81 |
82 | this.synth('heat', 'current resistance', function(i,r) {
83 | return i*i*r;
84 | });
85 | }
86 | Resistor.prototype = new Via.Object();
87 |
88 | var circuit = new Via.Object({
89 | conductor: new Conductor()
90 | , resistor = new Resistor()
91 | });
92 | circuit.synth('conductor.resistance', 'resistor.resistance');
93 | ```
94 |
95 | ## Reversible computing
96 | Synth can accept an additional function to convert the output
97 | back to the input.
98 |
99 | ```
100 | synth('a', 'b', function(b) { return b*2; }, function(a) { return a/2; });
101 | // No matter what changes, b will always be 1/2 of a
102 | ```
103 |
104 | Additionally, when synth() is given a simple direct 1-1 binding,
105 | i.e. no function is given, and no information is lost.
106 | It will also do the reverse, and keep the input updated with the output.
107 |
108 | In this way synth() can be used to interlink data across libraries
109 | without any transformation.
110 |
111 | ## Unbinding
112 | Since all data flows are bound by specifying paths within an objected structure,
113 | breaking bindings is simply a matter of breaking that path. Via automatically cleans up watch() events when the path becomes unreachable.
114 |
115 | Using our circuit example from before:
116 | ```
117 | circuit.set('resistor', false);
118 | ```
119 |
120 | ## Testing
121 | $ make test
122 |
123 |
124 | ## License
125 |
126 | (MIT License)
127 |
128 | Copyright (C) 2013 by Emery Denuccio
129 |
130 | Permission is hereby granted, free of charge, to any person obtaining a copy
131 | of this software and associated documentation files (the "Software"), to deal
132 | in the Software without restriction, including without limitation the rights
133 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
134 | copies of the Software, and to permit persons to whom the Software is
135 | furnished to do so, subject to the following conditions:
136 |
137 | The above copyright notice and this permission notice shall be included in
138 | all copies or substantial portions of the Software.
139 |
140 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
141 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
142 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
143 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
144 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
145 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
146 | THE SOFTWARE.
147 |
--------------------------------------------------------------------------------
/test/browser/page.test.js:
--------------------------------------------------------------------------------
1 | describe('', function() {
2 | var template = ' ';
3 |
4 | var data, ui;
5 |
6 | beforeEach(function() {
7 | // FIXME: page has to be a Via.Object for links and info to be bound
8 | // It's counter-intuitive, with many possible solutions
9 | data = {page: new Via.Object({
10 | total: 3
11 | })};
12 |
13 | ui = new Via.Element(data, template);
14 | });
15 |
16 | context('[size=1]', function() {
17 | beforeEach(function() {
18 | ui.data.set('page.size', 1)
19 | });
20 |
21 | context('page 1 of 3', function() {
22 | beforeEach(function() {
23 | ui.data.set('page.number', 1)
24 | });
25 |
26 | it('has correct info', function() {
27 | expect(ui.data.get('info')).eq('Showing 1-1 of 3');
28 | });
29 |
30 | it('has correct links', function() {
31 | var linksui = $(ui.rootElement).find('.page_links')[0]._viaElement;
32 | var html = 'Prev 1 2 3 Next ';
33 | expect(linksui.data.get('links')).eq(html);
34 | });
35 | });
36 |
37 | context('page 2 of 3', function() {
38 | beforeEach(function() {
39 | ui.data.set('page.number', 2)
40 | });
41 |
42 | it('has correct info', function() {
43 | expect(ui.data.get('info')).eq('Showing 2-2 of 3');
44 | });
45 |
46 | it('has correct links', function() {
47 | var linksui = $(ui.rootElement).find('.page_links')[0]._viaElement;
48 | var html = 'Prev 1 2 3 Next ';
49 | expect(linksui.data.get('links')).eq(html);
50 | });
51 | });
52 |
53 | context('page 3 of 3', function() {
54 | beforeEach(function() {
55 | ui.data.set('page.number', 3)
56 | });
57 |
58 | it('has correct info', function() {
59 | expect(ui.data.get('info')).eq('Showing 3-3 of 3');
60 | });
61 |
62 | it('has correct links', function() {
63 | var linksui = $(ui.rootElement).find('.page_links')[0]._viaElement;
64 | var html = 'Prev 1 2 3 Next ';
65 | expect(linksui.data.get('links')).eq(html);
66 | });
67 | });
68 | });
69 |
70 | context('[size=2]', function() {
71 | beforeEach(function() {
72 | ui.data.set('page.size', 2)
73 | });
74 |
75 | context('page 1 of 2', function() {
76 | beforeEach(function() {
77 | ui.data.set('page.number', 1)
78 | });
79 |
80 | it('has correct info', function() {
81 | expect(ui.data.get('info')).eq('Showing 1-2 of 3');
82 | });
83 |
84 | it('has correct links', function() {
85 | var linksui = $(ui.rootElement).find('.page_links')[0]._viaElement;
86 | var html = 'Prev 1 2 Next ';
87 | expect(linksui.data.get('links')).eq(html);
88 | });
89 | });
90 |
91 | context('page 2 of 2', function() {
92 | beforeEach(function() {
93 | ui.data.set('page.number', 2)
94 | });
95 |
96 | it('has correct info', function() {
97 | expect(ui.data.get('info')).eq('Showing 3-3 of 3');
98 | });
99 |
100 | it('has correct links', function() {
101 | var linksui = $(ui.rootElement).find('.page_links')[0]._viaElement;
102 | var html = 'Prev 1 2 Next ';
103 | expect(linksui.data.get('links')).eq(html);
104 | });
105 | });
106 | });
107 |
108 | context('[size=3]', function() {
109 | beforeEach(function() {
110 | ui.data.set('page.size', 3)
111 | });
112 |
113 | it('has correct info', function() {
114 | expect(ui.data.get('info')).eq('Showing 3 of 3');
115 | });
116 |
117 | it('has correct links', function() {
118 | var linksui = $(ui.rootElement).find('.page_links')[0]._viaElement;
119 | var html = '';
120 | expect(linksui.data.get('links')).eq(html);
121 | });
122 | });
123 |
124 | context('[size=1000]', function() {
125 | var template = ' ';
126 | beforeEach(function() {
127 | ui.data.set('page.size', 1000)
128 | });
129 |
130 | it('has correct info', function() {
131 | expect(ui.data.get('info')).eq('Showing 3 of 3');
132 | });
133 |
134 | it('has correct links', function() {
135 | var linksui = $(ui.rootElement).find('.page_links')[0]._viaElement;
136 | var html = '';
137 | expect(linksui.data.get('links')).eq(html);
138 | });
139 | });
140 | });
141 |
--------------------------------------------------------------------------------
/lib/element.js:
--------------------------------------------------------------------------------
1 | module.exports = ReactiveElement;
2 |
3 | var ReactiveObject = require('./object')
4 | , Via = require('./via')
5 | , utils = require('./utils')
6 | , globalElements = require('./elements')
7 | , globalAttributes = require('./attributes');
8 |
9 | /**
10 | * Represents a single UI instance for a model's ever changing data.
11 | */
12 | function ReactiveElement(data, options) {
13 | options = options || {};
14 |
15 | var parent = options.parent || this;
16 | var document = options.document || window.document;
17 |
18 | this.data = new ReactiveObject({
19 | ui: this
20 | , url: Via.window.location
21 | });
22 |
23 | function Elements() {}
24 | function Attributes() {}
25 | Elements.prototype = parent.elements;
26 | Attributes.prototype = parent.attributes;
27 | this.elements = new Elements();
28 | this.attributes = new Attributes();
29 |
30 | // Accept new custom elements from options
31 | if(options.elements) {
32 | this.elements.set(options.elements);
33 | }
34 |
35 | // Accept new custom elements from options
36 | if(options.attributes) {
37 | this.attributes.set(options.attributes);
38 | }
39 |
40 | // Allow string in lieu of options for a template
41 | if(typeof options === 'string') {
42 | options = {template: options};
43 | }
44 | // Handle dom elements (anything with an outerHTML)
45 | else if(options && options.outerHTML) {
46 | options = {template: options};
47 | }
48 |
49 | // If the template is an element, take the outerHTML
50 | if(options.template && options.template.outerHTML) {
51 | options.template = options.template.outerHTML;
52 | }
53 |
54 | // Accept direct data if it has a symbolic name
55 | // by assigning it to an empty object with a single
56 | // property of that name
57 | if(data.symbolicName) {
58 | var name = data.symbolicName();
59 | this.data.set(name, data);
60 | }
61 | else if(typeof data === 'object') {
62 | this.data.set(data);
63 | }
64 | else {
65 | // fail
66 | }
67 |
68 | this.template = options.template;
69 | this.rootElement = utils.domify(this.template);
70 |
71 | var rootTagName = this.rootElement.tagName.toLowerCase();
72 | var implFn = options.impl || this.elements[rootTagName];
73 |
74 | if(implFn) {
75 | var elemattrs = {};
76 | for(var i=0, attrs=this.rootElement.attributes,
77 | l=attrs.length; i < l; i++) {
78 | var attr = attrs.item(i)
79 | , k = attr.nodeName, v = attr.nodeValue;
80 | elemattrs[k] = v;
81 |
82 | this.data.synth(k, 'parent.'+v);
83 |
84 | // if(k === 'array') { debugger; }
85 | // Default any undefined values to the literal attribute value
86 | if(!this.data[k]) {
87 | this.data.set(k,v);
88 | }
89 | }
90 |
91 |
92 | if(this.rootElement.children.length) {
93 | this.template = this.rootElement.innerHTML;
94 | }
95 | else if(implFn.template) {
96 | this.template = implFn.template;
97 | }
98 |
99 | this.rootElement = utils.domify(this.template);
100 |
101 | var result = implFn.call(this,
102 | this, elemattrs, this.template);
103 |
104 | if(typeof result === 'string') {
105 | this.rootElement = utils.domify(result);
106 | }
107 | else if(result && result.nodeName) {
108 | this.rootElement = result;
109 | }
110 | }
111 |
112 | this.rootElement._viaElement = this;
113 |
114 | this.build();
115 | return this;
116 | };
117 |
118 | ReactiveElement.find = function(domElement) {
119 | return domElement && domElement._viaElement;
120 | };
121 |
122 | ReactiveElement.prototype = new ReactiveObject({
123 | build: function(options) {
124 | var self = this;
125 |
126 | // TODO: More efficient approach using querySelectorAll if available
127 | function recurseChildren(node) {
128 | var tagName = node.tagName && node.tagName.toLowerCase();
129 |
130 | if(self.elements[tagName]) {
131 | var template = node.outerHTML;
132 | var newElem = new ReactiveElement({parent: self.data}, {template: template, parent: self});
133 | if(node === self.rootElement) {
134 | self.rootElement = newElem.rootElement;
135 | }
136 | else {
137 | node.parentNode.insertBefore(newElem.rootElement, node);
138 | node.parentNode.removeChild(node);
139 | }
140 | }
141 | else {
142 | for (var i = 0; i < node.childNodes.length; i++) {
143 | var child = node.childNodes[i];
144 | recurseChildren(child);
145 | }
146 | }
147 | }
148 |
149 | recurseChildren(this.rootElement);
150 |
151 | // Any data- attributes we find in the template but don't have handlers for.
152 | // Make them map to the equivalent non-data attribute of the same name.
153 | var matchedTplAttrs = this.rootElement.outerHTML;
154 | matchedTplAttrs = matchedTplAttrs && matchedTplAttrs.match(/data-(\w+)[^\w]/gi) || [];
155 | matchedTplAttrs = utils.map(matchedTplAttrs, function(match) {
156 | return match.match(/-(\w+)/)[1]; } );
157 | utils.each(matchedTplAttrs, function(i,attrName) {
158 | if(!self.attributes[attrName]) {
159 | self.attributes[attrName] = function(ui, keypath) {
160 | var self = this;
161 | ui.data.watch(keypath, function(value) {
162 | self.setAttribute(attrName, value);
163 | });
164 | }
165 | }
166 | });
167 |
168 | // This is a temporary hack for data-list
169 | // Run attributes that use the third template argument last
170 | // See issue #34
171 | for(var i = 3; i > 1; --i) {
172 |
173 | // Handle all custom attribute in template
174 | for(var attrName in this.attributes) {
175 | var impl = this.attributes[attrName];
176 |
177 | if(typeof impl !== 'function') continue;
178 | if(impl.length !== i) continue;
179 |
180 | function buildAttr() {
181 | var value = this.getAttribute('data-'+attrName);
182 | var template = this.innerHTML;
183 | self.attributes[attrName].call(this, self, value, template);
184 | this.removeAttribute('data-'+attrName);
185 | }
186 |
187 | if(this.rootElement.getAttribute('data-'+attrName)) {
188 | buildAttr.call(this.rootElement);
189 | }
190 |
191 | var found = this.rootElement.querySelectorAll('[data-'+attrName+']');
192 | for(var i2=0, l2 = found.length; i2 < l2; ++i2) {
193 | buildAttr.call(found[i2]);
194 | }
195 | }
196 | }
197 | }
198 | , elements: new ReactiveObject(globalElements)
199 | , attributes: new ReactiveObject(globalAttributes)
200 | });
201 |
--------------------------------------------------------------------------------
/test/object.test.js:
--------------------------------------------------------------------------------
1 | var Via = require('../lib/via'),
2 | expect = require('chai').expect;
3 |
4 | describe('Via.Object', function() {
5 | var cat;
6 | beforeEach(function() {
7 | cat = new Via.Object({
8 | first_name: 'Fluffy',
9 | last_name: 'Brown'
10 | });
11 | });
12 |
13 | describe('#get', function() {
14 | it('gets simple direct attributes', function() {
15 | expect(cat.get('first_name')).eq('Fluffy');
16 | });
17 |
18 | it('accepts callback to require arguments ', function() {
19 | expect(cat.get('first_name')).eq('Fluffy');
20 | });
21 | });
22 |
23 |
24 | describe('#set', function() {
25 | it('accepts simple key/value arguments', function() {
26 | cat.set('hello', 'world');
27 | expect(cat.hello).eq('world');
28 | });
29 |
30 | it('accepts object and sets for each property', function() {
31 | cat.set({
32 | hello: 'world'
33 | , goodbye: 'cruel world'
34 | });
35 | expect(cat.hello).eq('world');
36 | expect(cat.goodbye).eq('cruel world');
37 | });
38 |
39 | it('accepts space separated list of keys and array of values', function() {
40 | cat.set('hello goodbye', ['world', 'cruel world']);
41 | expect(cat.hello).eq('world');
42 | expect(cat.goodbye).eq('cruel world');
43 | });
44 | });
45 |
46 | describe('#watch', function() {
47 | it('walks keypaths', function(done) {
48 | var stray = new Via.Object({best_friend: cat});
49 |
50 | stray.watch('best_friend.first_name', function(first_name) {
51 | expect(first_name).eq(cat.first_name);
52 | done();
53 | });
54 | });
55 |
56 | it('callsback immediately', function() {
57 | var called;
58 | cat.watch('first_name', function(first) {
59 | expect(first).eq('Fluffy');
60 | called = true;
61 | });
62 | expect(called).eq(true);
63 | });
64 |
65 | it('only calls if all arguments are defined', function() {
66 | var called = false;
67 | cat.watch('a b c', function() {
68 | called = true;
69 | });
70 | expect(called).eq(false);
71 | cat.set({a: 1, b:2});
72 | expect(called).eq(false);
73 | cat.set({c: 3});
74 | expect(called).eq(true);
75 | });
76 |
77 | it('adds only one set event per input', function() {
78 | var preEventCount = Object.keys(cat._events).length;
79 | cat.watch('cat_code first_name', function(){});
80 | expect(cat._events['set:cat_code'].length).eq(1);
81 | var postEventCount = Object.keys(cat._events).length;
82 | expect(postEventCount - preEventCount).eq(2);
83 | });
84 |
85 | it('cleans up events on old subpaths when high level nodes change', function() {
86 | var strayA = new Via.Object({name: 'Fluffy'});
87 | var strayB = new Via.Object({name: 'Scratch'});
88 | var obj = new Via.Object({friend: strayA});
89 | obj.watch('friend.name', function() {});
90 | expect(strayA._events['set:name'].length).eq(1);
91 | expect(strayB._events['set:name']).eq(undefined);
92 | obj.set('friend', strayB);
93 | expect(strayA._events['set:name'].length).eq(0);
94 | expect(strayB._events['set:name'].length).eq(1);
95 | });
96 |
97 | it('callsback on set', function(done) {
98 | cat.watch('cat_code', function(code) {
99 | expect(code).eq('test');
100 | done();
101 | });
102 |
103 | cat.set('cat_code', 'test');
104 | });
105 |
106 | it('constructs callback arguments from input values', function(done) {
107 | cat.watch('first_name last_name', function(first,last) {
108 | expect(first).eq('Fluffy');
109 | expect(last).eq('Brown');
110 | done();
111 | });
112 | });
113 |
114 | it('monitors any change on the model if no attribute specified', function(done) {
115 | cat.watch(function(arg) {
116 | expect(arg).eq(undefined);
117 | expect(this).eq(cat);
118 | done();
119 | });
120 | });
121 |
122 | it('interpolates strings with embedded {keypath} notation', function(done) {
123 | cat.watch('Hello, {first_name}', function(greeting) {
124 | expect(greeting).eq('Hello, Fluffy');
125 | done();
126 | });
127 | });
128 |
129 | it('requires all inputs to be defined', function() {
130 | var called = false;
131 | cat.watch('first_name something_undefined', function(a,b) {
132 | called = true;
133 | });
134 |
135 | expect(called).eq(false);
136 | });
137 |
138 | it('allows undefined inputs with a "?" suffix', function(done) {
139 | cat.watch('first_name something_undefined?', function(a,b) {
140 | expect(b).eq(undefined);
141 | done();
142 | });
143 | });
144 |
145 | it('forces literals with "!" suffix', function(done) {
146 | cat.watch('first_name 10!', function(name,num) {
147 | expect(name).eq('Fluffy');
148 | expect(num).eq('10');
149 | done();
150 | });
151 | });
152 |
153 | describe('#stop', function() {
154 | it('removes all events', function() {
155 | var obj = new Via.Object({name: 'Fluffy'});
156 | var mtr = obj.watch('name', function(){});
157 | mtr.stop();
158 | expect(obj._events['set:name'].length).eq(0);
159 | });
160 |
161 | it('does not propagate undefined', function(done) {
162 | var obj = new Via.Object({name: 'Fluffy'});
163 | obj.watch('name', function(name){
164 | expect(name).eq('Fluffy');
165 | done(); // Only once!
166 | this.stop();
167 | });
168 | });
169 | });
170 | });
171 |
172 | describe('#synth', function() {
173 | it('sets resultant attribute', function() {
174 | var obj = new Via.Object({first_name: 'Fluffy', last_name: 'Brown'});
175 | obj.synth('full_name', 'first_name last_name', function(first,last) {
176 | return first + ' ' + last;
177 | });
178 | expect( obj.get('full_name') ).eq('Fluffy Brown');
179 | });
180 |
181 | it('defaults to straight equality', function() {
182 | var obj = new Via.Object({a:1});
183 | obj.synth('b','a');
184 | expect(obj.get('b')).eq(1);
185 | });
186 |
187 | it('defaults to reverse straight equality', function() {
188 | var obj = new Via.Object({a:1});
189 | obj.synth('b','a');
190 | obj.set('b',2);
191 | expect(obj.get('a')).eq(2);
192 | });
193 |
194 |
195 | it('can handle sets', function() {
196 | var obj = new Via.Object({a:1});
197 | obj.synth('b c','a', function(c) {
198 | return [c+1,c+2];
199 | });
200 |
201 | expect(obj.b).eq(2);
202 | expect(obj.c).eq(3);
203 | });
204 |
205 | it('can handle reversed sets', function() {
206 | var obj = new Via.Object({a:4});
207 | obj.synth('a','b c', function(b,c) {
208 | return b+c;
209 | }, function(a) {
210 | return [a/2,a/2];
211 | });
212 |
213 | expect(obj.a).eq(4);
214 | expect(obj.b).eq(2);
215 | expect(obj.c).eq(2);
216 | });
217 |
218 | });
219 |
220 | });
221 |
222 |
223 |
--------------------------------------------------------------------------------
/lib/utils.js:
--------------------------------------------------------------------------------
1 | var utils = module.exports = {};
2 |
3 | /**
4 | * Find a nested property from a object with a "keypath"
5 | * e.g. traverse({foo: {bar: 123}}, 'foo.bar');
6 | * The last argument is a function that will be call
7 | * for every object in the path that receives:
8 | *
9 | * deepObj: The parent object
10 | * deepKey: The key of the next child object
11 | * nearestEmitter: Nearest ancestor that can trigger events
12 | * shortestPath: Path from nearestEmitter to deepObj
13 | * remainingPath: The remaining keypath from this object down
14 | *
15 | * Returning false from the callback stops traversal.
16 | */
17 | utils.traverse = function(obj, keypath, fn) {
18 | if(!keypath) return;
19 |
20 | var arr = keypath.split('.');
21 | var k;
22 |
23 | var nearestEmitter;
24 | var shortestPath;
25 |
26 | for(var i=0,l=arr.length; i < l; ++i) {
27 | k = arr[i];
28 |
29 | if(fn && obj && typeof obj !== 'undefined') {
30 | if(obj.trigger) {
31 | nearestEmitter = obj;
32 | shortestPath = k;
33 | }
34 | else if(nearestEmitter) {
35 | shortestPath += '.' + k;
36 | }
37 |
38 | var keepGoing = fn(obj, k,
39 | nearestEmitter,
40 | shortestPath,
41 | (i+1 < l ? arr.slice(i+1).join('.') : false));
42 | if(keepGoing === false) break;
43 | }
44 |
45 | if(obj && Object.prototype.hasOwnProperty.call(obj, k)){
46 | obj = obj[k];
47 | }
48 | else {
49 | return;
50 | }
51 | }
52 |
53 | return obj;
54 | }
55 |
56 | var _class2type = {};
57 |
58 | var _type = function( obj ) {
59 | return obj == null ?
60 | String( obj ) :
61 | _class2type[ toString.call(obj) ] || "object";
62 | };
63 |
64 | var _isWindow = function( obj ) {
65 | return obj != null && obj == obj.window;
66 | };
67 |
68 | var _isFunction = function(obj){
69 | return typeof obj === "function";
70 | };
71 |
72 |
73 | utils.inArray = function( elem, arr, i ) {
74 | var len,
75 | core_indexOf = Array.prototype.indexOf;
76 |
77 | if ( arr ) {
78 | if ( core_indexOf ) {
79 | return core_indexOf.call( arr, elem, i );
80 | }
81 |
82 | len = arr.length;
83 | i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0;
84 |
85 | for ( ; i < len; i++ ) {
86 | // Skip accessing in sparse arrays
87 | if ( i in arr && arr[ i ] === elem ) {
88 | return i;
89 | }
90 | }
91 | }
92 |
93 | return -1;
94 | };
95 |
96 | /**
97 | * Based on jQuery.isArray
98 | */
99 | var _isArray = Array.isArray || function( obj ) {
100 | return _type(obj) === "array";
101 | };
102 |
103 |
104 | /**
105 | * Based on jQuery.isPlainObject
106 | */
107 | var _isPlainObject = function( obj ) {
108 |
109 | return typeof obj === "object";
110 |
111 | // Must be an Object.
112 | // Because of IE, we also have to check the presence of the constructor property.
113 | // Make sure that DOM nodes and window objects don't pass through, as well
114 | if ( !obj || _type(obj) !== "object" || obj.nodeType || _isWindow( obj ) ) {
115 | return false;
116 | }
117 |
118 | try {
119 | // Not own constructor property must be Object
120 | if ( obj.constructor &&
121 | !hasOwn.call(obj, "constructor") &&
122 | !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) {
123 | return false;
124 | }
125 | } catch ( e ) {
126 | // IE8,9 Will throw exceptions on certain host objects #9897
127 | return false;
128 | }
129 |
130 | // Own properties are enumerated firstly, so to speed up,
131 | // if last one is own, then all properties are own.
132 |
133 | var key;
134 | for ( key in obj ) {}
135 |
136 | return key === undefined || hasOwn.call( obj, key );
137 | };
138 |
139 | utils.isEmptyObject = function( obj ) {
140 | var name;
141 | for ( name in obj ) {
142 | return false;
143 | }
144 | return true;
145 | };
146 |
147 | /**
148 | * Based on jQuery.each
149 | */
150 | utils.each = function( obj, callback, args ) {
151 | if(typeof obj === 'undefined')
152 | return;
153 |
154 | var name,
155 | i = 0,
156 | length = obj.length,
157 | isObj = length === undefined || _isFunction( obj );
158 |
159 | if ( args ) {
160 | if ( isObj ) {
161 | for ( name in obj ) {
162 | if ( callback.apply( obj[ name ], args ) === false ) {
163 | break;
164 | }
165 | }
166 | } else {
167 | for ( ; i < length; ) {
168 | if ( callback.apply( obj[ i++ ], args ) === false ) {
169 | break;
170 | }
171 | }
172 | }
173 |
174 | // A special, fast, case for the most common use of each
175 | } else {
176 | if ( isObj ) {
177 | for ( name in obj ) {
178 | if ( callback.call( obj[ name ], name, obj[ name ] ) === false ) {
179 | break;
180 | }
181 | }
182 | } else {
183 | for ( ; i < length; ) {
184 | if ( callback.call( obj[ i ], i, obj[ i++ ] ) === false ) {
185 | break;
186 | }
187 | }
188 | }
189 | }
190 |
191 | return obj;
192 | };
193 |
194 | utils.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) {
195 | _class2type[ "[object " + name + "]" ] = name.toLowerCase();
196 | });
197 |
198 |
199 | utils.map = function(obj,fn) {
200 | var result = [];
201 | utils.each(obj, function(i,v) {
202 | result[i] = fn(v);
203 | });
204 | return result;
205 | };
206 |
207 | /**
208 | * Based on jQuery.extend
209 | */
210 | utils.extend = function() {
211 | var options, name, src, copy, copyIsArray, clone,
212 | target = arguments[0] || {},
213 | i = 1,
214 | length = arguments.length,
215 | deep = false;
216 |
217 | // Handle a deep copy situation
218 | if ( typeof target === "boolean" ) {
219 | deep = target;
220 | target = arguments[1] || {};
221 | // skip the boolean and the target
222 | i = 2;
223 | }
224 |
225 | // Handle case when target is a string or something (possible in deep copy)
226 | if ( typeof target !== "object" && !_isFunction(target) ) {
227 | // target = {};
228 | }
229 |
230 | if ( length === i ) {
231 | target = this;
232 | --i;
233 | }
234 |
235 | for ( ; i < length; i++ ) {
236 | // Only deal with non-null/undefined values
237 | if ( (options = arguments[ i ]) != null ) {
238 | // Extend the base object
239 | for ( name in options ) {
240 | src = target[ name ];
241 | copy = options[ name ];
242 |
243 | // Prevent never-ending loop
244 | if ( target === copy ) {
245 | continue;
246 | }
247 |
248 | // Recurse if we're merging plain objects or arrays
249 | if ( deep && copy && ( _isPlainObject(copy) || (copyIsArray = _isArray(copy)) ) ) {
250 | if ( copyIsArray ) {
251 | copyIsArray = false;
252 | clone = src && _isArray(src) ? src : [];
253 |
254 | } else {
255 | clone = src && _isPlainObject(src) ? src : {};
256 | }
257 |
258 | // Never move original objects, clone them
259 | target[ name ] = utils.extend( deep, clone, copy );
260 |
261 | // Don't bring in undefined values
262 | } else if ( copy !== undefined ) {
263 | target[ name ] = copy;
264 | }
265 | }
266 | }
267 | }
268 |
269 | // Return the modified object
270 | return target;
271 | };
272 |
273 | utils.domify = require('domify');
274 |
275 | // TODO: Replace all uses with ReactiveURI
276 | utils.urlParams = function(params) {
277 | var str = [];
278 | for(var p in params) {
279 | if(params[p] !== undefined) {
280 | str.push(encodeURIComponent(p) + "=" + encodeURIComponent(params[p]));
281 | }
282 | }
283 | return str.join("&");
284 | }
285 |
--------------------------------------------------------------------------------
/lib/object.js:
--------------------------------------------------------------------------------
1 | module.exports = ReactiveObject;
2 |
3 | var Events = require('./events')
4 | , utils = require('./utils');
5 |
6 | /**
7 | * Common model constructor
8 | * This gets called directly by more specific models
9 | */
10 | function ReactiveObject(obj) {
11 | this.set(obj);
12 | };
13 |
14 | ReactiveObject.prototype = {
15 | /**
16 | * Set a single attribute on the model
17 | * or an object of key-value pairs which gets
18 | * deep merged.
19 | *
20 | * Setting always triggers a "set:{key}" event
21 | * and a "changed" event with the key as an argument
22 | * against the nearest emitter
23 | * after each property is assigned.
24 | */
25 | set: function(k,v) {
26 | var self = this;
27 |
28 | if(typeof k === 'undefined') return;
29 |
30 | function setAttr(target,k,newV) {
31 | // if(typeof newV === 'string') {
32 | // newV = target.substituteString(newV);
33 | // }
34 |
35 | var queue = [];
36 | var changed = false;
37 |
38 | utils.traverse(target, k, function(deepObj, deepAttr,
39 | nearestEmitter, shortestPath, remainingPath) {
40 |
41 | if(remainingPath && (!deepObj.hasOwnProperty(deepAttr)
42 | || typeof deepObj[deepAttr] === 'undefined')) {
43 | deepObj[deepAttr] = {};
44 | changed = true;
45 | }
46 |
47 | var preV = deepObj[deepAttr];
48 |
49 | if(!remainingPath) {
50 | deepObj[deepAttr] = newV;
51 |
52 | if(newV !== preV) {
53 | changed = true;
54 | }
55 |
56 | self._lastChangedProperty = k;
57 | self._lastChangedValue = preV;
58 | }
59 |
60 | queue.push([nearestEmitter, shortestPath, deepObj[deepAttr], preV]);
61 | });
62 |
63 | // Bubble set and changed events for every node
64 | // in the path if the leaf actually changed.
65 | var e;
66 | if(changed) {
67 | while(e = queue.pop()) {
68 | e[0].trigger('set:'+e[1],e[2],e[3]);
69 | e[0].trigger('changed',e[1],e[2],e[3]);
70 | }
71 | }
72 | }
73 |
74 | if(typeof k === 'object') {
75 | function recurse(target,src) {
76 | for(var k in src) {
77 | var srcv = src[k];
78 | if(src.hasOwnProperty(k)) {
79 | if(typeof srcv === 'object' && target.hasOwnProperty(k)) {
80 | recurse(target[k], srcv);
81 | }
82 | else {
83 | setAttr(target,k,srcv);
84 | }
85 | }
86 | }
87 | }
88 |
89 |
90 | recurse(this,k);
91 | }
92 | else {
93 | k = ''+k;
94 | var split = k.split(' ');
95 |
96 | if(split.length === 1) {
97 | setAttr(this, k, v);
98 | }
99 | else {
100 | for(var i=0; i < split.length; ++i) {
101 | setAttr(this, split[i], v[i]);
102 | }
103 | }
104 | }
105 |
106 | // this.trigger('changed', k, v);
107 | }
108 |
109 | /**
110 | * Get a single attribute after triggering a get:{key}
111 | * event which gives handlers the opportunity to change
112 | * the property's value.
113 | */
114 | , get: function(keypath, callback, allowDefault) {
115 |
116 | var result = utils.traverse(this, keypath, function(obj,key,
117 | nearestEmitter, shortestPath) {
118 | if(nearestEmitter) {
119 | nearestEmitter.trigger('get:'+shortestPath);
120 | }
121 | });
122 |
123 | if(callback) {
124 | if(result !== undefined) {
125 | callback.call(this, result);
126 | }
127 | else {
128 | this.watch(keypath, function() {
129 | var got = callback.apply(this.root, arguments);
130 | this.stop();
131 | });
132 | }
133 | }
134 |
135 | return result;
136 | }
137 |
138 | , debug: function(attr) {
139 | this.watch(attr, function(newV, preV) {
140 | console.log(attr, preV, '->', newV);
141 | });
142 | }
143 |
144 | /**
145 | * Monitors a space-separated list of attributes for changes,
146 | * calling a function after any one has changed.
147 | * The values of the source attributes are passed to the
148 | * callback in the same order they were given.
149 | */
150 | , watch: function(attr, fn) {
151 | var self = this;
152 |
153 | // Substitutions
154 | var interpolated = (typeof attr === 'string') && attr.match(/\{.+?\}/g);
155 | if(interpolated) {
156 | interpolated = utils.map(interpolated, function(a) {
157 | return a.slice(1, a.length-1);
158 | });
159 |
160 | return this.watch(interpolated.join(' '), function() {
161 | var result = attr;
162 | for(var i=0; i < arguments.length; ++i) {
163 | var v = arguments[i];
164 | result = result.replace('{'+interpolated[i]+'}', v);
165 | }
166 | fn.call(this, result);
167 | });
168 | }
169 |
170 | // this.load && this.load();
171 |
172 | // If only a function, we monitor any direct change
173 | // to the object attributes via the "changed" event
174 | if(arguments.length === 1 && typeof attr === 'function') {
175 | fn = attr;
176 | attr = '*';
177 | }
178 |
179 | if(attr === '*') {
180 | var event = this.on('changed', fn);
181 | fn.call(this); // Initial update
182 | // this.load && this.load();
183 | return event;
184 | }
185 |
186 | if(attr instanceof RegExp) {
187 | var event = this.on('changed', function(k,newV,preV) {
188 | if(attr.exec(k) !== null) {
189 | fn.call(this,k,newV,preV);
190 | }
191 | });
192 |
193 | for(var k in this) {
194 | if(attr.exec(k) !== null) {
195 | var v = this.get(k);
196 | fn.call(this,k,v);
197 | }
198 | }
199 | // fn.call(this); // Initial update
200 | // this.load && this.load();
201 |
202 | return;
203 | }
204 |
205 | // If no callback, just return gracefully and do nothing
206 | if(!fn) return;
207 |
208 |
209 | // Split out multiple input attributes
210 | var multi = attr.split(' ');
211 |
212 | // We encapsulate all of our events
213 | // under an outside container that only has one
214 | // attribute point to self
215 | // This allows us to unbind every event by simply
216 | // setting root to an eventless object, such as null.
217 | // This container is returned.
218 | var container = new ReactiveObject({
219 | root: self
220 | });
221 |
222 | var stopping = false;
223 | container.stop = function() {
224 | stopping = true;
225 | this.set('root', null);
226 | stopping = false;
227 | }
228 |
229 |
230 | var optional = [];
231 |
232 | // Prefix the attributes with root
233 | multi = utils.map(multi, function(attr) {
234 |
235 | if(attr.slice(-1) === '!') {
236 | return attr;
237 | }
238 |
239 | if(attr.slice(-1) === '?') {
240 | attr = attr.slice(0,-1)
241 | optional.push('root.'+attr);
242 | }
243 |
244 | return 'root.'+attr;
245 | });
246 |
247 | // Updater function we call when any invalidation occurs
248 | // Builds a list of arguments from the input attrs,
249 | // and envokes the callback with those values.
250 | function update(preV) {
251 | if(stopping) return;
252 |
253 | if(fn.norecurse) {
254 | // console.log('stopped recursion');
255 | // return;
256 | }
257 |
258 | var allDefined = true;
259 | var args = utils.map(multi, function(attr) {
260 | var val;
261 | if(attr.slice(-1) === '!') {
262 | val = attr.slice(0,-1);
263 | }
264 | else {
265 | val = container.get(attr);
266 | }
267 | if(val === undefined && utils.inArray(attr,optional) === -1) {
268 | allDefined = false;
269 | }
270 | return val;
271 | });
272 |
273 | if(!allDefined)
274 | return;
275 |
276 | args = args.concat(
277 | Array.prototype.slice.call(arguments,0)
278 | );
279 |
280 | fn.norecurse = true;
281 | fn.apply(container, args);
282 | fn.norecurse = false;
283 | }
284 |
285 | utils.each(multi, function(i,attr) {
286 |
287 | if(attr.slice(-1) === '!') {
288 | return;
289 | }
290 |
291 | utils.traverse(container, attr, function(deepObj, deepAttr,
292 | nearestEmitter, shortestPath, remainingPath) {
293 |
294 | if(nearestEmitter) {
295 | nearestEmitter.on('set:'+shortestPath, updateAttr);
296 | }
297 |
298 | // if(deepObj && deepObj.load) {
299 | // deepObj.load();
300 | // }
301 |
302 | function updateAttr(newV,preV) {
303 | if(newV !== preV) {
304 | utils.traverse(newV, remainingPath, function(deepObj, deepAttr,
305 | nearestEmitter, shortestPath) {
306 | if(nearestEmitter) {
307 | nearestEmitter.on('set:'+shortestPath, updateAttr);
308 | }
309 |
310 | // if(deepObj && deepObj.load) {
311 | // deepObj.load();
312 | // }
313 | });
314 |
315 | utils.traverse(preV, remainingPath, function(deepObj, deepAttr,
316 | nearestEmitter, shortestPath) {
317 | if(nearestEmitter)
318 | nearestEmitter.unbind('set:'+shortestPath, updateAttr);
319 | });
320 | }
321 |
322 | update(preV, attr);
323 | }
324 |
325 | });
326 | });
327 |
328 | update();
329 |
330 | return container;
331 | }
332 |
333 | // TODO: Do these better...
334 | // they might not actually be called promises
335 | // but a special case of something like Watchers
336 | // This gets the job done for now.
337 | , promise: function(fn) {
338 | var obj = {
339 | then: function(thenf) {
340 | if(this.result) {
341 | thenf(this.result);
342 | return;
343 | }
344 |
345 | this.waiting = thenf;
346 | fn.call(this, done);
347 |
348 | }
349 | , waiting: null
350 | , result: null
351 | , done: done
352 | };
353 |
354 | function done(result) {
355 | if(obj.waiting) {
356 | obj.waiting(result);
357 | return;
358 | }
359 |
360 | obj.result = result;
361 | }
362 |
363 |
364 | return obj;
365 | }
366 |
367 | /**
368 | * Use watch parameters to set a single result attribute
369 | */
370 | , synth: function(output,input,fwdFn,revFn) {
371 | function straight() {
372 | return arguments[0];
373 | };
374 |
375 | if(!fwdFn) {
376 | // We can only default reverse
377 | // if forward is default, so
378 | // we default them together.
379 | fwdFn = straight;
380 |
381 | var isReversible = typeof input === 'string' &&
382 | input.indexOf('{') === -1;
383 |
384 | if(isReversible)
385 | revFn = straight;
386 | }
387 |
388 | var self = this;
389 | self.watch(input, function() {
390 | var result = fwdFn.apply(self, arguments);
391 |
392 | // if(result && result.then) {
393 | // result.then(function(result) {
394 | // self.set(output, result);
395 | // });
396 | // }
397 | // else {
398 | self.set(output, result);
399 | // }
400 | });
401 |
402 | if(revFn) {
403 | this.synth(input, output, revFn);
404 | }
405 |
406 | return this;
407 | }
408 | , filter: function(extra) {
409 | var copy = new this.constructor();
410 | copy.set(this);
411 |
412 | if(arguments.length > 0) {
413 | copy.set.apply(copy, arguments);
414 | }
415 |
416 | return copy;
417 | }
418 | , substituteString: function(str) {
419 | var self = this;
420 | return str.replace(/\{.+?\}/g, function(m) {
421 | return self.get(m);
422 | });
423 | }
424 | };
425 |
426 | /**
427 | * Make an event emmitters
428 | */
429 | Events.mixin(ReactiveObject);
430 |
--------------------------------------------------------------------------------