Skip to content

Commit

Permalink
Implemented SortableComponent behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
tommyminds committed Oct 8, 2015
1 parent cfe741a commit 142e1ac
Show file tree
Hide file tree
Showing 6 changed files with 244 additions and 3 deletions.
1 change: 1 addition & 0 deletions src/behaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export * from './behaviors/behavior';
export * from './behaviors/event-emitter';
export * from './behaviors/entity-store';
export * from './behaviors/sortable-store';
export * from './behaviors/sortable-component';
export * from './behaviors/transformable';
60 changes: 60 additions & 0 deletions src/behaviors/sortable-component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import angular from 'angular';
import {Behavior} from './behavior';
import {addBehavior} from '../utils';

export class SortableComponentBehavior extends Behavior {
constructor(instance, {actions, initial} = {}) {
super(...arguments);

this.actions = actions;
if (!this.actionsRef) {
const className = Reflect.getPrototypeOf(instance).constructor.name;
throw new Error(`SortableComponentBehavior: '${actions}' not found on ${className}`);
}

if (initial) {
this.sortExpression = initial;
}
}

get actionsRef() {
return this.instance[this.actions];
}

set sortExpression(sortExpression) {
this._sortExpression = sortExpression;
if (sortExpression === null) {
this.actionsRef.sortClear();
} else {
this.actionsRef.sortChange(sortExpression);
}
}

get sortExpression() {
return this._sortExpression || null;
}
}

const ENTITY_REGEX = /^([A-Z][a-z]*)/;

export function SortableComponent(config = {}) {
return cls => {
let preparedConfig = config;
if (angular.isString(config)) {
preparedConfig = {actions: config};
}

if (!preparedConfig.entity) {
preparedConfig.entity = ENTITY_REGEX.exec(cls.name)[0];
}
if (!preparedConfig.actions) {
preparedConfig.actions = `${preparedConfig.entity.toLowerCase()}Actions`;
}

addBehavior(cls, SortableComponentBehavior, {
property: 'sortableComponent',
config: preparedConfig,
proxy: ['sortExpression']
});
};
}
165 changes: 165 additions & 0 deletions src/behaviors/sortable-component.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*eslint-env node, jasmine*//*global module, inject*/
/*eslint-disable max-statements, max-params*/
import angular from 'angular';
import 'angular-mocks';

import {
Annotations,
Component,
SortableComponent,
SortableComponentBehavior
} from 'anglue/anglue';

describe('SortableComponent', () => {
// Clear the AnnotationCache for unit tests to ensure we create new annotations for each class.
beforeEach(() => {
Annotations.clear();
});

describe('SortableComponentBehavior', () => {
class MockComponent {
fooActions = {
sortChange: changeSpy,
sortClear: clearSpy
};
}

let mockInstance, behavior, changeSpy, clearSpy;
beforeEach(() => {
changeSpy = jasmine.createSpy('changeSpy');
clearSpy = jasmine.createSpy('clearSpy');
mockInstance = new MockComponent();
behavior = new SortableComponentBehavior(mockInstance, {
actions: 'fooActions'
});
});

it('should use the actions config to return the actions instance ref', () => {
expect(behavior.actionsRef).toBe(mockInstance.fooActions);
});

it('should throw an error if it cant find the actionsRef', () => {
const invalid = () => {
behavior = new SortableComponentBehavior(mockInstance, {
actions: 'barActions'
});
};
expect(invalid).toThrowError(
`SortableComponentBehavior: 'barActions' not found on MockComponent`);
});

it('should be possible to get the current sortExpression', () => {
behavior.sortExpression = 'foo';
expect(behavior.sortExpression).toEqual('foo');
});

it('should call sortChange method on the actionsRef when sortExpression is changed', () => {
behavior.sortExpression = 'foo';
expect(changeSpy).toHaveBeenCalledWith('foo');
});

it('should call sortChange method on the actionsRef when sortExpression set to null', () => {
behavior.sortExpression = null;
expect(clearSpy).toHaveBeenCalled();
});

it('should set dispatch for an initial sort expression', () => {
behavior = new SortableComponentBehavior(mockInstance, {
actions: 'fooActions',
initial: 'foo'
});
expect(changeSpy).toHaveBeenCalledWith('foo');
});
});

describe('@SortableComponent() decorator', () => {
@Component() @SortableComponent()
class SortableTestComponent {
sortableActions = {};
}

@Component() @SortableComponent({
entity: 'foo',
initial: '+name'
})
class SortableComplexComponent {
fooActions = {
sortChange() {}
};
}

@Component() @SortableComponent({actions: 'customActions'})
class CustomActionsComponent {
customActions = {};
}

@Component() @SortableComponent('customActions')
class CustomActionsStringComponent {
customActions = {};
}

angular.module('sortableComponents', [
SortableTestComponent.annotation.module.name,
CustomActionsComponent.annotation.module.name,
CustomActionsStringComponent.annotation.module.name,
SortableComplexComponent.annotation.module.name
]);

let $compile, $rootScope;
let testComponent, customActionsComponent, customActionsStringComponent, complexComponent;
beforeEach(module('sortableComponents'));
beforeEach(inject((_$compile_, _$rootScope_) => {
$compile = _$compile_;
$rootScope = _$rootScope_;

testComponent = compileTemplate('<sortable-test></sortable-test>', $compile, $rootScope)
.controller('sortableTest');
customActionsComponent = compileTemplate(
'<custom-actions></custom-actions>', $compile, $rootScope)
.controller('customActions');
customActionsStringComponent = compileTemplate(
'<custom-actions-string></custom-actions-string>', $compile, $rootScope)
.controller('customActionsString');
complexComponent = compileTemplate('<sortable-complex></sortable-complex>', $compile, $rootScope)
.controller('sortableComplex');
}));

it('should define the SortableComponent API on the component', () => {
[
'sortableComponent',
'sortExpression'
].forEach(api => expect(testComponent[api]).toBeDefined());
});

it('should have an instance of SortableComponentBehavior as the behavior property', () => {
expect(testComponent.sortableComponent).toEqual(jasmine.any(SortableComponentBehavior));
});

it('should use the first uppercased part of class name to determine the actions', () => {
expect(testComponent.sortableComponent.actions).toEqual('sortableActions');
});

it('should be possible to pass the actions in the configuration', () => {
expect(customActionsComponent.sortableComponent.actions).toEqual('customActions');
});

it('should be possible to pass the actions property as a string', () => {
expect(customActionsStringComponent.sortableComponent.actions).toEqual('customActions');
});

it('should be possible to configure an entity to determine the actions', () => {
expect(complexComponent.sortableComponent.actions).toEqual('fooActions');
});

it('should be possible to configure an initial sort expression', () => {
expect(complexComponent.sortExpression).toEqual('+name');
});
});
});

function compileTemplate(template, $compile, $rootScope) {
const el = angular.element(template.trim());
$compile(el)($rootScope.$new());
$rootScope.$digest();
return el;
}
4 changes: 2 additions & 2 deletions src/component.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,14 @@ describe('Components', () => {
onDestroy = jasmine.createSpy('onDestroy');
}

angular.module('componentApp', [
angular.module('componentsApp', [
ComplexComponent.annotation.module.name,
TemplateUrlComponent.annotation.module.name,
ReplaceComponent.annotation.module.name
]);

let $compile, $rootScope, $timeout;
beforeEach(module('componentApp'));
beforeEach(module('componentsApp'));
beforeEach(inject((_$compile_, _$rootScope_, _$timeout_) => {
$compile = _$compile_;
$rootScope = _$rootScope_;
Expand Down
4 changes: 4 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ export function addProxies(cls, BehaviorCls, property, proxies) {
/*eslint-disable no-loop-func */
get() {
return this[property][externalName];
},

set(value) {
this[property][externalName] = value;
}

/*eslint-disable no-loop-func */
Expand Down
13 changes: 12 additions & 1 deletion src/utils.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,11 @@ describe('Utils', () => {
});

describe('addBehavior()', () => {
let cls, TestCls, BehaviorCls, methodSpy;
let cls, TestCls, BehaviorCls, methodSpy, setterSpy;
beforeEach(() => {
methodSpy = jasmine.createSpy('method');
setterSpy = jasmine.createSpy('setter');

class BehaviorClass extends Behavior {
bar() {
methodSpy();
Expand All @@ -216,6 +218,9 @@ describe('Utils', () => {
get getter() {
return 'foo';
}
set getter(value) {
setterSpy(value);
}
}
class TestClass {
custom() {
Expand Down Expand Up @@ -252,6 +257,12 @@ describe('Utils', () => {
expect(cls.getter).toEqual('foo');
});

it('should create working proxy setters on the prototype of the target cls', () => {
addBehavior(TestCls, BehaviorCls, {property: 'foo', proxy: ['getter']});
cls.getter = 'foo';
expect(setterSpy).toHaveBeenCalledWith('foo');
});

it('should create working proxy properties on the prototype of the target cls', () => {
addBehavior(TestCls, BehaviorCls, {
property: 'foo',
Expand Down

0 comments on commit 142e1ac

Please sign in to comment.