add tree config with tests

This commit is contained in:
Andrew
2015-03-20 20:00:06 -06:00
parent dc0eb872f0
commit 9103918abd
13 changed files with 482 additions and 167 deletions

View File

@ -1,4 +1,31 @@
<ion-aside-parent> <ion-aside-parent>
<ion-aside side="right">
Hello ...
<p>...</p>
<p>...</p>
<p>...</p>
<p>...</p>
<p>...</p>
Side menu!
</ion-aside>
<ion-aside side="bottom">
Hello ...
<p>...</p>
<p>...</p>
<p>...</p>
<p>...</p>
<p>...</p>
Side menu!
</ion-aside>
<ion-aside side="top">
Hello ...
<p>...</p>
<p>...</p>
<p>...</p>
<p>...</p>
<p>...</p>
Side menu!
</ion-aside>
<ion-aside side="left"> <ion-aside side="left">
Hello ... Hello ...
<p>...</p> <p>...</p>
@ -13,20 +40,20 @@
<ion-tabbar view-title="Tabs 2"></ion-tabbar> <ion-tabbar view-title="Tabs 2"></ion-tabbar>
<ion-tabbar view-title="Tabs 4"></ion-tabbar> <ion-tabbar view-title="Tabs 4"></ion-tabbar>
<button (click)="showModal()">Show Modal</button>
<ion-switch></ion-switch>
<!--
<ion-modal>
<h2>I'm a modal!</h2>
</ion-modal>
-->
<button (click)="showModal()">Show Modal</button> <button (click)="showModal()">Show Modal</button>
<ion-switch></ion-switch>
<!-- <!--
<ion-modal> <ion-modal>
<h2>I'm a modal!</h2> <h2>I'm a modal!</h2>
</ion-modal> </ion-modal>
--> -->
</div> <button (click)="showModal()">Show Modal</button>
<!--
<ion-modal>
<h2>I'm a modal!</h2>
</ion-modal>
-->
</div>
</ion-aside-parent> </ion-aside-parent>

View File

@ -2,8 +2,6 @@ import {bootstrap} from 'angular2/core';
import {Component, Template} from 'angular2/angular2'; import {Component, Template} from 'angular2/angular2';
import {Aside, AsideParent} from 'ionic/components'; import {Aside, AsideParent} from 'ionic/components';
// import 'ionic/components/tabbar/mixins/android/android-tabbar';
@Component({ selector: '[playground-main]' }) @Component({ selector: '[playground-main]' })
@Template({ @Template({
url: 'main.html', url: 'main.html',

View File

@ -1,2 +1,2 @@
export * from './components/sidemenu/sidemenu'; export * from './components/aside/aside'
import './components/sidemenu/behaviors/direction/direction'; import './components/aside/behaviors/direction/direction'

View File

@ -4,10 +4,10 @@ import {IonConfig} from '../../config';
import {DragGesture} from '../../core/gestures/drag-gesture'; import {DragGesture} from '../../core/gestures/drag-gesture';
import * as util from '../../util'; import * as util from '../../util';
export var asideConfig = new IonConfig('sidemenu') export var asideConfig = new IonConfig('aside');
// TODO defaults or bindings? // TODO defaults or bindings?
asideConfig.defaults({ asideConfig.set({
side: 'left', side: 'left',
dragThreshold: 50 dragThreshold: 50
}); });
@ -15,7 +15,7 @@ asideConfig.defaults({
@Component({ @Component({
selector: 'ion-aside', selector: 'ion-aside',
bind: { bind: {
edge: 'side', side: 'side',
dragThreshold: 'dragThreshold' dragThreshold: 'dragThreshold'
}, },
}) })
@ -38,7 +38,8 @@ export class Aside extends Ion {
}); });
this.dragMethods = { this.dragMethods = {
getMenuStart() { return 0; }, getMenuStart() { return 0; },
getEventPos(ev) { return ev.center.x; } getEventPos(ev) { return ev.center.x; },
canStart() { return true; }
}; };
this.gesture.listen(); this.gesture.listen();
@ -46,7 +47,10 @@ export class Aside extends Ion {
this.setChanging(false); this.setChanging(false);
}) })
asideConfig(this); // TODO: remove this. setTimeout has to be done so the bindings can be applied
setTimeout(() => {
asideConfig.invoke(this);
});
} }
onDragStart(ev) { onDragStart(ev) {
if (!this.dragMethods.canStart(ev)) { if (!this.dragMethods.canStart(ev)) {

View File

@ -1,14 +1,18 @@
import {asideConfig} from '../../sidemenu'; import {asideConfig} from '../../aside';
import * as util from '../../../../util'; import * as util from '../../../../util';
asideConfig.when(instance => instance.side === 'bottom') asideConfig
.mixin(function() { .behavior(function() {
if (this.side !== 'bottom') return;
this.gesture.options({ this.gesture.options({
direction: Hammer.DIRECTION_VERTICAL direction: Hammer.DIRECTION_VERTICAL
}); });
this.domElement.classList.add('bottom'); this.domElement.classList.add('bottom');
util.extend(this.dragMethods, { util.extend(this.dragMethods, {
canStart: ev => {
return this.isOpen || ev.center.y > window.innerHeight - this.dragThreshold;
},
getMenuStart: (drag, ev) => { getMenuStart: (drag, ev) => {
return this.isOpen ? -drag.height : 0; return this.isOpen ? -drag.height : 0;
}, },
@ -27,13 +31,14 @@ asideConfig.when(instance => instance.side === 'bottom')
return ev.center.y; return ev.center.y;
} }
}); });
})
}); .behavior(function() {
if (this.side !== 'left') return;
asideConfig.when(instance => instance.side === 'left')
.mixin(function() {
this.domElement.classList.add('left'); this.domElement.classList.add('left');
this.gesture.options({
direction: Hammer.DIRECTION_HORIZONTAL
});
util.extend(this.dragMethods, { util.extend(this.dragMethods, {
canStart: (ev) => { canStart: (ev) => {
return this.isOpen || ev.center.x < this.dragThreshold; return this.isOpen || ev.center.x < this.dragThreshold;
@ -51,21 +56,26 @@ asideConfig.when(instance => instance.side === 'left')
this.setOpen(drag.pos > drag.width / 2); this.setOpen(drag.pos > drag.width / 2);
this.domElement.style.transform = ''; this.domElement.style.transform = '';
} }
}); })
}); })
.behavior(function() {
asideConfig.when(instance => instance.side === 'right') if (this.side !== 'right') return;
.mixin(function() {
this.domElement.classList.add('right'); this.domElement.classList.add('right');
this.gesture.options({
direction: Hammer.DIRECTION_HORIZONTAL
});
util.extend(this.dragMethods, { util.extend(this.dragMethods, {
canStart: ev => {
return this.isOpen || ev.center.x > window.innerWidth - this.dragThreshold;
},
getMenuStart: (drag, ev) => { getMenuStart: (drag, ev) => {
return this.isOpen ? -drag.width : 0; return this.isOpen ? -drag.width : 0;
}, },
onDrag: (drag, ev) => { onDrag: (drag, ev) => {
drag.pos = util.clamp( drag.pos = util.clamp(
0, -drag.menuStart + drag.pointerStart - ev.center.x, drag.height 0, -drag.menuStart + drag.pointerStart - ev.center.x, drag.width
); );
this.domElement.style.transform = 'translate3d(' + this.domElement.style.transform = 'translate3d(' +
(drag.width - drag.pos) + 'px,0,0)'; (drag.width - drag.pos) + 'px,0,0)';
@ -76,13 +86,15 @@ asideConfig.when(instance => instance.side === 'right')
} }
}); });
}); })
.behavior(function() {
asideConfig.when(instance => instance.side === 'top') if (this.side !== 'top') return;
.mixin(function() {
this.domElement.classList.add('top'); this.domElement.classList.add('top');
util.extend(this.dragMethods, { util.extend(this.dragMethods, {
canStart: ev => {
return this.isOpen || ev.center.y < this.dragThreshold * 5;
},
getMenuStart: (drag, ev) => { getMenuStart: (drag, ev) => {
return this.isOpen ? drag.height : 0; return this.isOpen ? drag.height : 0;
}, },

View File

@ -1,55 +1,177 @@
import * as Platform from './platform'; import * as Platform from './platform';
import * as util from './util'; import * as util from './util';
export function IonConfig(name) { /*
config
.set({ side: 'left' })
.set('threshold', 50)
.platform('ios')
.set('side', 'top')
.unset('threshold')
.media('lg')
.set('side', 'right')
// TODO automatically add platform class config.platform('ios')
// TODO do bindings/defaults have to be written twice? .behavior(function() {
// TODO maybe add config to IonicComponent annotation do something
// TODO map options to config })
function Config(instance, element) { .defaults({
var platformName = Platform.getPlatform(); side: 'right'
(element.domElement || element).classList.add(`${name}-${platformName}`); })
config.platform('ios').media('tablet')
.defaults({
side: 'bottom'
});
*/
util.defaults(instance, Config._defaults || {});
var conditions = Config._conditions; /*
for (var i = 0, ii = conditions.length; i < ii; i++) { User wants to remove the default behavior for sidemenu, but that's stuck under `.platform('ios').`
if (conditions[i]._callback(instance)) {
for (var j = 0, jj = conditions[i]._mixins.length; j < jj; j++) { config.platform('ios').media('tablet') === config.media('tablet').platform('ios')
conditions[i]._mixins[j].call(instance); */
} var QUERIES = {
} sm: true,
md: true,
lg: true
};
var PLATFORMS = {
ios: true,
android: true
};
function isPlatform(key = '') {
return key.toLowerCase() in PLATFORMS;
}
function isMedia(key = '') {
return key.toLowerCase() in QUERIES;
}
class ConfigCase {
constructor({ root, parent, path }) {
this._root = root;
this._parent = parent;
this._path = path || [];
this._values = {};
this.behaviors = [];
}
platform(key = '') {
if (isPlatform(key)) return this._root._addCase(key, this);
return this;
}
media(key = '') {
if (isMedia(key)) return this._root._addCase(key, this);
return this;
}
when(condition = '') {
if (isPlatform(condition) || isMedia(condition)) {
return this._root._addCase(condition, this);
} }
}
Config._conditions = [];
Config.defaults = function(defaults) {
Config._defaults = defaults;
};
Config.when = function when(callback) {
var condition = new ConfigCondition(callback);
Config._conditions.push(condition);
return condition;
};
Config.platform = function platform(name) {
return Config.when(() => Platform.getPlatform() === name);
};
return Config;
}
class ConfigCondition {
constructor(callback, mixins = [], template = '') {
this._callback = callback;
this._mixins = mixins;
this._template = template;
}
mixin(mixinFn) {
this._mixins.push(mixinFn);
return this; return this;
} }
template(url) { behavior(fn) {
this._template = url; this.behaviors.push(fn);
return this; return this;
} }
set(a, b) {
if (util.isString(a)) {
this._values[a] = b;
} else {
util.extend(this._values, a || {});
}
return this;
}
unset(key) {
delete this._values[key];
}
get(key) {
return util.isDefined(this._values[key]) ?
this._values[key] :
(this._parent ? this._parent.get(key) : undefined);
}
}
export class IonConfig extends ConfigCase {
constructor() {
this._root = this;
this._cases = {};
super({
root: this,
parent: null,
path: ''
});
}
invoke(instance) {
return invokeConfig(this, instance);
}
_addCase(key, baseCase) {
var path = baseCase._path.slice();
path.push(key);
// Remove empties & duplicates
path = path
.filter((value, index) => {
return value && path.indexOf(value) === index;
})
.sort();
if (path.join(' ') === baseCase._path.join(' ')) {
return baseCase;
}
return this._createCase(path);
}
_createCase(path) {
if (!path.length) return this;
var pathStr = path.join(' ');
var configCase = this._cases[pathStr];
if (!configCase) {
var parentPath = path.slice(0, path.length - 1);
configCase = this._cases[pathStr] = new ConfigCase({
root: this,
parent: this._createCase(parentPath),
path: path
});
}
return configCase;
}
}
export function invokeConfig(config, object, opts = {}) {
util.defaults(opts, { media: 'lg', platform: 'ios' });
var { platform, media } = opts;
var passedCases = [config].concat(
Object.keys(config._cases)
.map(name => config._cases[name])
.filter(configCasePasses)
.sort(function(a,b) {
return a._path.length < b._path.length ? -1 : 1;
})
);
// Extend the given object with the values of all the passed cases, starting with the
// most specific.
var defaults = [object];
var behaviors = [];
for (let i = 0, ii = passedCases.length; i < ii; i++) {
defaults.push(passedCases[i]._values);
// Avoid allocating a new array for each passed case's array of behaviors
behaviors.push.apply(behaviors, passedCases[i].behaviors);
}
util.defaults.apply(null, defaults);
for (let i = 0, ii = behaviors.length; i < ii; i++) {
behaviors[i].call(object, object);
}
function configCasePasses(configCase) {
var path = configCase._path;
var key;
for (let i = 0, ii = path.length; i < ii; i++) {
if (!(media === path[i] || platform === path[i])) return false;
}
return true;
}
} }

137
src/config_spec.js Normal file
View File

@ -0,0 +1,137 @@
import {IonConfig} from './config';
export function main() {
var rootConfig;
beforeEach(() => {
rootConfig = new IonConfig();
});
it('should create a config one level down', () => {
var sub = rootConfig.platform('ios');
expect(sub._parent).toBe(rootConfig);
expect(sub._path).toEqual(['ios']);
expect(rootConfig._cases.ios).toBe(sub);
});
it('should create a config two levels down', () => {
var sub1 = rootConfig.platform('ios');
var sub2 = sub1.media('lg');
expect(sub2._parent).toBe(sub1);
expect(sub1._parent).toBe(rootConfig);
expect(rootConfig._cases['ios lg']).toBe(sub2);
expect(rootConfig._cases.ios).toBe(sub1);
});
it('set should be chainable', () => {
expect(rootConfig.set()).toBe(rootConfig);
});
it('should set values on the root', () => {
rootConfig.set({
letter: 'a'
});
expect(rootConfig.get('letter')).toBe('a');
});
it('should always return the same object for the same key', () => {
expect(rootConfig.platform('android')).toBe(rootConfig.platform('android'));
expect(rootConfig.platform('ios')).toBe(rootConfig.platform('ios'));
expect(rootConfig.media('lg')).toBe(rootConfig.media('lg'));
});
it('should return the same object when nesting in different order', () => {
var sub1 = rootConfig.platform('ios').media('sm');
var sub2 = rootConfig.media('sm').platform('ios');
expect(sub1).toBe(sub2);
});
it('should return the same object when nesting in different order for huge queries', () => {
var sub1 = rootConfig.platform('ios').media('sm').platform('android').media('lg');
var sub2 = rootConfig.media('sm').media('lg').platform('android').platform('ios');
expect(sub1).toBe(sub2);
});
it('should set values one level down and be chainable', () => {
rootConfig.set({ letter: 'a' });
var sub1 = rootConfig.platform('ios');
expect(sub1.get('letter')).toBe('a');
expect( sub1.set({ letter: 'b' }) ).toBe(sub1);
expect(sub1.get('letter')).toBe('b');
});
it('should set values two levels down and be chainable', () => {
rootConfig.set({ letter: 'a' });
var sub1 = rootConfig.platform('ios');
sub1.set({ letter: 'b' });
var sub2 = sub1.media('lg');
expect(sub2.get('letter')).toBe('b');
expect( sub2.set({ letter: 'c' }) ).toBe(sub2);
expect(sub2.get('letter')).toBe('c');
});
it('should use parent\'s value if its later set to undefined', () => {
rootConfig.set({ letter: 'a' });
var sub1 = rootConfig.platform('ios');
sub1.set({ letter: 'b' });
expect(sub1.get('letter')).toBe('b');
sub1.unset('letter');
expect(sub1.get('letter')).toBe('a');
});
it('when() as alias for media()', () => {
expect(rootConfig.when('lg')).toBe(rootConfig.media('lg'));
expect(rootConfig.when('bad')).toBe(rootConfig);
expect(rootConfig.when('lg')).not.toBe(rootConfig.when('ios'));
});
it('when() as alias for platform()', () => {
expect(rootConfig.platform('ios')).toBe(rootConfig.when('ios'));
expect(rootConfig.when('bad')).toBe(rootConfig);
});
describe('invokeConfig', function() {
it('should invoke defaults', () => {
var obj = {};
rootConfig.set('foo', 'bar');
rootConfig.invoke(obj);
});
it('should invoke defaults in nested whens', () => {
var obj = {};
rootConfig.set({ a: 'root', b: 'root' });
rootConfig.when('ios').set({b: 'ios', c: 'ios'});
rootConfig.when('ios').when('lg').set({ c: 'ios-lg', d: 'ios-lg' });
rootConfig.invoke(obj);
expect(obj).toEqual({
a: 'root',
b: 'ios',
c: 'ios-lg',
d: 'ios-lg'
});
});
it('should run behaviors', () => {
var obj = {};
rootConfig.behavior(instance => {
instance.foo = 'bar';
});
rootConfig.invoke(obj);
expect(obj.foo).toBe('bar');
});
it('should invoke behaviors in nested whens', () => {
var obj = {};
rootConfig.when('ios')
.behavior(o => o.ios = true)
.when('lg')
.behavior(o => o.lg = true)
rootConfig.invoke(obj);
expect(obj).toEqual({
ios: true,
lg: true
});
});
});
}

View File

@ -1,54 +0,0 @@
import * as Platform from '../../platform';
import * as util from '../../util';
export function IonConfig() {
// TODO automatically add platform class
// TODO do bindings/defaults have to be written twice?
// TODO maybe add config to IonicComponent annotation
// TODO map options to config
function Config(instance, element) {
(element.domElement || element).classList.add('platform-' + Platform.getPlatform());
util.defaults(instance, Config._defaults || {});
var conditions = Config._conditions;
for (var i = 0, ii = conditions.length; i < ii; i++) {
if (conditions[i]._callback(instance)) {
for (var j = 0, jj = conditions[i]._mixins.length; j < jj; j++) {
conditions[i]._mixins[j].call(instance);
}
}
}
}
Config._conditions = [];
Config.defaults = function(defaults) {
Config._defaults = defaults;
};
Config.when = function when(callback) {
var condition = new ConfigCondition(callback);
Config._conditions.push(condition);
return condition;
};
Config.platform = function platform(name) {
return Config.when(() => Platform.getPlatform() === name);
};
return Config;
}
class ConfigCondition {
constructor(callback, mixins = [], template = '') {
this._callback = callback;
this._mixins = mixins;
this._template = template;
}
mixin(mixinFn) {
this._mixins.push(mixinFn);
return this;
}
template(url) {
this._template = url;
return this;
}
}

View File

@ -1,4 +0,0 @@
import {Config} from './config';
export function main() {
}

View File

@ -2,6 +2,8 @@ import * as util from '../../util';
export class Gesture { export class Gesture {
constructor(element, opts = {}) { constructor(element, opts = {}) {
util.defaults(opts, {
});
this.element = element; this.element = element;
this._options = opts; this._options = opts;
} }

View File

@ -16,11 +16,38 @@ export function clamp(min, n, max) {
return Math.max(min, Math.min(n, max)); return Math.max(min, Math.min(n, max));
} }
export function defaults(obj, src) { export function defaults(dest) {
for (var key in src) { let extendObj = {};
if (src.hasOwnProperty(key) && !obj.hasOwnProperty(key)) { for (let i = arguments.length - 1; i >= 1; i--) {
obj[key] = src[key]; let source = arguments[i] || {};
for (let key in source) {
if (!dest.hasOwnProperty(key) && !extendObj.hasOwnProperty(key)) {
extendObj[key] = source[key];
}
} }
} }
return obj; for (let key in extendObj) {
dest[key] = extendObj[key];
}
return dest;
} }
export function isString(val) {
return typeof val === 'string';
}
export function isFunction(val) {
return typeof val === 'function';
}
export function isDefined(val) {
return typeof val !== 'undefined';
}
export var array = {
unique(array) {
return array.filter(function(value, index) {
return array.indexOf(value) === index;
});
}
};

52
src/util_spec.js Normal file
View File

@ -0,0 +1,52 @@
import * as util from './util';
export function main() {
describe('extend', function() {
it('should extend simple', () => {
var obj = { a: '0', c: '0' };
expect( util.extend(obj, { a: '1', b: '2' }) ).toBe(obj);
expect(obj).toEqual({ a: '1', b: '2', c: '0' });
});
it('should extend complex', () => {
expect(util.extend(
{ a: '0', b: '0' },
{ b: '1', c: '1' },
{ c: '2', d: '2' }
)).toEqual({
a: '0',
b: '1',
c: '2',
d: '2'
});
});
});
describe('defaults', function() {
it('should simple defaults', () => {
var obj = { a: '1' };
expect(util.defaults(obj, { a: '2', b: '2' })).toBe(obj);
expect(obj).toEqual({
a: '1', b: '2'
});
});
it('should complex defaults', () => {
expect(util.defaults(
{ a: '0', b: '0' },
{ b: '1', c: '1', e: '1' },
{ c: '2', d: '2' }
)).toEqual({
a: '0',
b: '0',
c: '2',
d: '2',
e: '1'
});
});
});
}

View File

@ -1,33 +1,25 @@
import * as util from '../util'; import * as util from '../util';
import {ViewHistory} from './view-history'; import {ViewHistory} from './view-history';
// import {Children} from 'angular2/angular2';
export class View extends ViewSwitcher { export class View {
constructor(el) { constructor(
// @Children() views: View
) {
super(el); super(el);
this.children = [];
// A linear history of views that this switcher has gone
// through.
this.history = new ViewHistory(); this.history = new ViewHistory();
this.currentChild = null;
} }
setChild(view, options = {}) { setSelected(isSelected) {
var direction = options.direction || 'forward'; this.isSelected = isSelected;
var viewIndex = this.history.indexOf(view);
if (viewIndex !== -1) {
direction = 'back';
this.history.popTo(viewIndex);
} else {
this.history.push(view);
}
if (this.currentView) {
this.element.removeChild(this.currentView.element);
}
this.element.appendChild(view.element);
this.currentView = view.element;
} }
selectChild(child) {
if (this.selectedChild) {
this.selectedChild.setSelected(false);
}
child.setSelected(true);
this.selectedChild = child;
}
} }