Skip to content

Commit

Permalink
[FEATURE] Two-way sync between rows and model (#167)
Browse files Browse the repository at this point in the history
  • Loading branch information
offirgolan authored Sep 13, 2016
1 parent df72ead commit 0ec6c36
Show file tree
Hide file tree
Showing 11 changed files with 239 additions and 18 deletions.
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'));
});

0 comments on commit 0ec6c36

Please sign in to comment.