Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Two-way sync between rows and model #167

Merged
merged 10 commits into from
Sep 13, 2016
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ before_install:
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
- npm config set spin false
- npm install -g bower
- bower --version
- npm install -g npm@^3
- npm install -g bower
- npm install -g codeclimate-test-reporter
- bower --version

install:
- npm install
Expand Down
11 changes: 11 additions & 0 deletions addon/-private/global-options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Ember from 'ember';
import config from 'ember-get-config';

const assign = Ember.assign || Ember.merge;
const globalOptions = config['ember-light-table'] || {};

export default globalOptions;

export function mergeOptionsWithGlobals(options) {
return assign(assign({}, globalOptions), options);
}
89 changes: 89 additions & 0 deletions addon/-private/sync-array-proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import Ember from 'ember';

const {
assert,
isArray
} = Ember;

export default Ember.ArrayProxy.extend({
/**
* The model that will be synchronized to the content of this proxy
* @property syncArray
* @type {Array}
*/
syncArray: null,

/**
* @property syncEnabled
* @type {Boolean}
*/
syncEnabled: true,

init() {
this._super(...arguments);

let syncArray = this.get('syncArray');

assert('[ember-light-table] enableSync requires the passed array to be an instance of Ember.A', isArray(syncArray) && typeof syncArray.addArrayObserver === 'function');

syncArray.addArrayObserver(this, {
willChange: 'syncArrayWillChange',
didChange: 'syncArrayDidChange'
});
},

destroy() {
this.get('syncArray').removeArrayObserver(this, {
willChange: 'syncArrayWillChange',
didChange: 'syncArrayDidChange'
});

this.setProperties({ syncArray: null, content: null });
},

/**
* Serialize objects before they are inserted into the content array
* @method serializeContentObjects
* @param {Array} objects
* @return {Array}
*/
serializeContentObjects(objects) {
return objects;
},

/**
* Serialize objects before they are inserted into the sync array
* @method serializeSyncArrayObjects
* @param {Array} objects
* @return {Array}
*/
serializeSyncArrayObjects(objects) {
return objects;
},

syncArrayWillChange() { /* Not needed */},

syncArrayDidChange(syncArray, start, removeCount, addCount) {
let content = this.get('content');

if(!this.get('syncEnabled')) {
return;
}

if(addCount > 0) {
content.replace(start, 0, this.serializeContentObjects(syncArray.slice(start, start + addCount)));
} else if(removeCount > 0) {
content.replace(start, removeCount, []);
}
},

replaceContent(start, removeCount, objectsToAdd) {
let syncArray = this.get('syncArray');

if(!this.get('syncEnabled')) {
return this._super(...arguments);
}

syncArray.replace(start, removeCount, this.serializeSyncArrayObjects(objectsToAdd));
}
});
60 changes: 55 additions & 5 deletions addon/classes/Table.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
import Ember from 'ember';
import Row from 'ember-light-table/classes/Row';
import Column from 'ember-light-table/classes/Column';
import SyncArrayProxy from 'ember-light-table/-private/sync-array-proxy';
import { mergeOptionsWithGlobals } from 'ember-light-table/-private/global-options';

const {
get,
computed,
isNone,
isEmpty,
A: emberArray
} = Ember;

const RowSyncArrayProxy = SyncArrayProxy.extend({
serializeContentObjects(objects) {
return Table.createRows(objects);
},

serializeSyncArrayObjects(objects) {
return objects.map(o => get(o, 'content'));
}
});

/**
* @module Table
* @private
Expand Down Expand Up @@ -129,13 +142,31 @@ export default class Table extends Ember.Object.extend({
* @constructor
* @param {Array} columns
* @param {Array} rows
* @param {Object} options
*
*/
constructor(columns = [], rows = []) {
constructor(columns = [], rows = [], options = {}) {
super();
this.setProperties({
rows: emberArray(Table.createRows(rows)),
columns: emberArray(Table.createColumns(columns)),
});

let _columns = emberArray(Table.createColumns(columns));
let _rows = emberArray(Table.createRows(rows));
let _options = mergeOptionsWithGlobals(options);

if(_options.enableSync) {
_rows = RowSyncArrayProxy.create({ syncArray: rows, content: _rows });
}

this.setProperties({ columns: _columns, rows: _rows });
}

destroy() {
this._super(...arguments);

let rows = this.get('rows');

if(rows instanceof RowSyncArrayProxy) {
rows.destroy();
}
}

// Rows
Expand Down Expand Up @@ -237,6 +268,16 @@ export default class Table extends Ember.Object.extend({
rows.forEach(r => this.removeRow(r));
}


/**
* Remove a row at the specified index
* @method removeRowAt
* @param {Number} index
*/
removeRowAt(index) {
this.get('rows').removeAt(index);
}

// Columns

/**
Expand Down Expand Up @@ -322,6 +363,15 @@ export default class Table extends Ember.Object.extend({
return this.get('columns').removeObjects(columns);
}

/**
* Remove a column at the specified index
* @method removeColumnAt
* @param {Number} index
*/
removeColumnAt(index) {
this.get('columns').removeAt(index);
}

/**
* Create a Row object with the given content
* @method createRow
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"dependencies": {
"ember-cli-babel": "^5.1.6",
"ember-cli-htmlbars": "^1.0.11",
"ember-get-config": "0.1.7",
"ember-in-viewport": "2.1.0",
"ember-scrollable": "0.3.2",
"ember-truth-helpers": "1.2.0",
Expand Down
2 changes: 1 addition & 1 deletion tests/dummy/app/adapters/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import DS from 'ember-data';
import ENV from '../config/environment';

export default DS.RESTAdapter.extend({
namespace: ENV.baseURL + 'api'
namespace: ENV.rootURL + 'api'
});
15 changes: 10 additions & 5 deletions tests/dummy/app/controllers/table.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,33 @@ import Ember from 'ember';
import Table from 'ember-light-table';

const {
isEmpty
isEmpty,
computed
} = Ember;

export default Ember.Controller.extend({
columns: null,
table: null,
sort: null,
page: 1,
limit: 10,
dir: 'asc',
isLoading: false,
canLoadMore: true,
model: null,

init() {
this._super(...arguments);
this.set('table', new Table(this.get('columns')));
},

table: computed('model', function() {
return new Table(this.get('columns'), this.get('model'), { enableSync: true });
}),

fetchRecords() {
this.set('isLoading', true);
this.store.query('user', this.getProperties(['page', 'limit', 'sort', 'dir'])).then(records => {
this.get('table').addRows(records);
// this.get('table').addRows(records);
this.get('model').pushObjects(records.toArray());
this.set('isLoading', false);
this.set('canLoadMore', !isEmpty(records));
});
Expand All @@ -44,7 +49,7 @@ export default Ember.Controller.extend({
sort: column.get('valuePath'),
page: 1
});
this.get('table').setRows([]);
this.get('model').setObjects([]);
this.fetchRecords();
}
}
Expand Down
4 changes: 2 additions & 2 deletions tests/dummy/app/routes/table-route.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import Ember from 'ember';

export default Ember.Route.extend({
model() {
return this.store.query('user', {page: 1, limit: 10});
return this.store.query('user', { page: 1, limit: 10 });
},

setupController(controller, model) {
controller.get('table').setRows(model.toArray());
controller.set('model', model.toArray());
},

resetController: function(controller, isExiting) {
Expand Down
4 changes: 2 additions & 2 deletions tests/dummy/config/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ module.exports = function(environment) {

if (environment === 'test') {
// Testem prefers this...
ENV.baseURL = '/';
ENV.rootURL = '/';
ENV.locationType = 'none';

// keep test console output quieter
Expand All @@ -41,7 +41,7 @@ module.exports = function(environment) {

if (environment === 'production') {
ENV.locationType = 'hash';
ENV.baseURL = '/ember-light-table/';
ENV.rootURL = '/ember-light-table/';
ENV['ember-cli-mirage'] = {
enabled: true
};
Expand Down
2 changes: 1 addition & 1 deletion tests/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
{{content-for "body"}}
{{content-for "test-body"}}

<script src="testem.js" integrity=""></script>
<script src="{{rootURL}}testem.js" integrity=""></script>
<script src="{{rootURL}}assets/vendor.js"></script>
<script src="{{rootURL}}assets/test-support.js"></script>
<script src="{{rootURL}}assets/dummy.js"></script>
Expand Down
65 changes: 65 additions & 0 deletions tests/unit/classes/table-test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import Ember from 'ember';
import { Table, Column, Row } from 'ember-light-table';
import { module, test } from 'qunit';

const {
A: emberArray
} = Ember;

module('Unit | Classes | Table');

test('create table - default options', function(assert) {
Expand Down Expand Up @@ -408,3 +413,63 @@ test('static table method - createColumns', function(assert) {
assert.equal(cols.length, 2);
assert.ok(cols[0] instanceof Column);
});

test('table modifications with sync enabled - simple', function(assert) {
let rows = emberArray([]);
let table = new Table([], rows, { enableSync: true });

table.addRow({ firstName: 'Offir' });

assert.equal(table.get('rows.length'), 1);
assert.equal(table.get('rows.length'), rows.get('length'));

rows.pushObject({ firstName: 'Taras' });

assert.equal(rows.get('length'), 2);
assert.equal(table.get('rows.length'), rows.get('length'));

assert.deepEqual(table.get('rows').getEach('firstName'), rows.getEach('firstName'));

table.get('rows').clear();

assert.equal(table.get('rows.length'), 0);
assert.equal(table.get('rows.length'), rows.get('length'));
});

test('table modifications with sync enabled - stress', function(assert) {
let rows = emberArray([]);
let table = new Table([], rows, { enableSync: true });

for(let i = 0; i < 100; i++) {
table.addRow({ position: i });
}

assert.equal(table.get('rows.length'), rows.get('length'));
assert.deepEqual(table.get('rows').getEach('position'), rows.getEach('position'));

for(let i = 100; i < 200; i++) {
rows.pushObject({ position: i });
}

assert.equal(table.get('rows.length'), rows.get('length'));
assert.deepEqual(table.get('rows').getEach('position'), rows.getEach('position'));

table.removeRowAt(5);
table.removeRowAt(10);
table.removeRowAt(125);

assert.equal(table.get('rows.length'), rows.get('length'));
assert.deepEqual(table.get('rows').getEach('position'), rows.getEach('position'));

rows.removeAt(10);
rows.removeAt(20);
rows.removeAt(150);

assert.equal(table.get('rows.length'), rows.get('length'));
assert.deepEqual(table.get('rows').getEach('position'), rows.getEach('position'));

table.get('rows').clear();

assert.equal(table.get('rows.length'), 0);
assert.equal(table.get('rows.length'), rows.get('length'));
});