diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..2873e2f --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,13 @@ +module.exports = { + root: true, + parserOptions: { + ecmaVersion: 2017, + sourceType: 'module' + }, + extends: 'eslint:recommended', + env: { + browser: true + }, + rules: { + } +}; diff --git a/.gitignore b/.gitignore index 5ad14dd..fb4bfaa 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ # dependencies /node_modules /bower_components +.node_modules* # misc /.sass-cache @@ -15,3 +16,6 @@ /libpeerconnection.log npm-debug.log* testem.log +/docs/* +package.json.ember-try +bower.json.ember-try diff --git a/.npmignore b/.npmignore index fa8b147..889b2bf 100644 --- a/.npmignore +++ b/.npmignore @@ -8,7 +8,7 @@ .editorconfig .ember-cli .gitignore -.jshintrc +.eslintrc.js .watchmanconfig .travis.yml bower.json diff --git a/.travis.yml b/.travis.yml index b3be07e..72f90c9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,13 @@ --- language: node_js node_js: - - "4" + - "6" sudo: false cache: directories: - $HOME/.npm - - $HOME/.cache # includes bowers cache env: # we recommend testing LTS's and latest stable release (bonus points to beta/canary) @@ -17,6 +16,7 @@ env: - EMBER_TRY_SCENARIO=ember-release - EMBER_TRY_SCENARIO=ember-beta - EMBER_TRY_SCENARIO=ember-canary + - EMBER_TRY_SCENARIO=ember-default matrix: fast_finish: true @@ -25,16 +25,13 @@ matrix: before_install: - npm config set spin false - - npm install -g bower - - bower --version - - npm install phantomjs-prebuilt - - node_modules/phantomjs-prebuilt/bin/phantomjs --version + - npm install -g phantomjs-prebuilt + - phantomjs --version install: - npm install - - bower install script: # Usually, it's ok to finish the test scenario without reverting # to the addon's original dependency state, skipping "cleanup". - - ember try:one $EMBER_TRY_SCENARIO test --skip-cleanup + - node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO test --skip-cleanup diff --git a/CHANGELOG.md b/CHANGELOG.md index 704b832..668cc30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [0.2.0] - 2017-05-17 - Ember v2.X.X +### Added +- Adds ability to set column filter/sort independently. +- Add support for YUIDocs for better API documentation. + +### Changed +- Upgrades ember-power-select to 1.4.3, may require refactoring of `class` to `triggerClass` if applicable. +- Upgrade ember and ember-cli to 2.10.0 + +### Fixed + ## [0.1.0] - 2016-08-31 - Ember pre v1.13.10 ### Added - Initial base version built to support JSON API v1.0. @@ -14,14 +25,11 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Add subcomponent to change the table limit/count. - Setup `autoHide` computed property to conditionally hide the limit dropdown if the results are smaller than the smallest pagination limit. - Adds ability to persist data across transitions, setting `persistFiltering: true` and sharing the `filter`/`sort` properties with a controller/service. -- Adds ability to set column filter/sort independently. ### Changed - Update legacy name references in README.md. - Removed ember-canary from ember-try testing scenarios. - Removed ember-beta from ember-try testing scenarios. -- Upgrades ember-power-select to 1.4.3, may require refactoring of `class` to `triggerClass` if applicable. -- Upgrade ember and ember-cli to 2.10.0 ### Fixed - Fix erring ember try in ember-canary, properly remove component element wrapper. diff --git a/README.md b/README.md index e79089f..76d6873 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ You have full control over your table's `tbody` content. We are setting this to {{row.emailAddress}} {{row.firstName}} {{row.lastName}} + {{row.updatedAt}} {{#link-to "index" class="btn btn-xs" role="button"}} Edit @@ -75,10 +76,15 @@ export default Ember.Controller.extend({ label: 'Last Updated', type: 'date', }, + { + label: 'Actions', + }, ], }); ``` +* Reference the [API specs/documentation](/docs/index.html) for more information and for advanced usage. + ### Request Format Ember Tabular sticks very closely to jsonapi spec, a few examples of requests: @@ -90,93 +96,6 @@ Ember Tabular sticks very closely to jsonapi spec, a few examples of requests: * `sort` - Sort based on jsonapi's recommended sorting: http://jsonapi.org/format/#fetching-sorting * Ascending unless prefixed with `-` for descending. -## Advanced Usage -### Template -```hbs -{{#ember-tabular - columns=columns - modelName="user" - record=users - class="table-default" - tableClass="table-bordered table-hover table-striped" - staticParams=staticParams - as |section|}} - ... -{{/ember-tabular}} -``` -* `makeRequest` - boolean/string - Default: true - * If `true`: Ember Tabular will make request based on `modelName`. - * If `false`: Typically you'd bind the route's model to `record`. -* `class` - string - * Wraps the entire component. -* `tableClass` - string - Default: "table-bordered table-hover" - * Wraps only the `` and replaces defaults if provided. -* `staticParams` - object - Default: null - * Object to pass in static query-params that will not change based on any filter/sort criteria, ex. additional table-wide filters that need to be applied in all requests `?filter[is-open]=1`. - - ```js - // app/controllers/location.js - - export default Ember.Controller.extend({ - staticParams: Ember.computed('model', function() { - return { - 'filter[is-open]': '1', - 'include': 'hours', - }; - }), - ... - }); - ``` -* `tableLoadedMessage` - string - Default: "No Data." - * In some cases when the API response is loaded but does not contain any data "No Data." will not apply, on a case by case basis you can override this. For example, if you'd like to prompt the user to do some kind of action. "No data, select a different product". - -### Controller -```js -export default Ember.Controller.extend({ - users: null, - columns: [ - { - property: 'username', - label: 'Username', - defaultSort: 'username', - type: 'text', - }, - { - property: 'emailAddress', - label: 'Email', - type: 'text', - }, - { - property: 'firstName', - label: 'First Name', - type: 'text', - }, - { - property: 'lastName', - label: 'Last Name', - type: 'text', - }, - { - property: 'updatedAt', - label: 'Last Updated', - type: 'date', - }, - ], -}); -``` -* `columns.property` - string - * Required for column filtering/sorting - * Properties should be in camelCase format -* `columns.label` - string - * Required in all use-cases -* `columns.type` - string - Default: text - * Sets the filter `` -* `columns.sort` - boolean - Default: true - * Required for column sorting -* `columns.defaultSort` - string - * Initial sort value for API request - * Will be overridden with any sorting changes - ### Template - Yields ```hbs {{#ember-tabular columns=columns record=users as |section|}} @@ -206,26 +125,6 @@ Typically the global filter component would be rendered into the `{{yield header filterProperty="username" filterPlaceholder="Search by Username"}} ``` -* `filter` - object - Default: null - * Required - * Must also expose the `filter` property on the parent `ember-tabular` component to be able to pass the `filter` object back and forth between parent and child components. -* `query` - object - Default: `this.get('query') || this.get('parentView.query')` - * Pass the query object from the parent component if it is different or if used outside of the context of the component, otherwise query is optional and it component will attempt to grab within the context of the parent component. -* `filterProperty` - string - Default: null - * Required - * Used with the "Global Filter Sub-Component". - * Pass the property name in camelCase format. -* `filterPlaceholder` - string - Default: null - * Optional - * Placeholder to be used for the global-filter. -* `label` - string - Default: null - * Optional - * Set a label on the global-filter. -* `inputClass` - string - Default: null - * Optional - * Wraps the input field in a div. -* `labelClass` - string - Default: null - * Optional #### Date Filter Date filter changes `input type="date"` to take advantage of a browser's HTML5 date widget. Typically the date filter component would be rendered into the `{{yield header}}` of the main table component using the yield conditional `{{#if section.isHeader}} ...`. However, it can be used outside of the context of the main component if the proper properties are shared between the main component and sub-component. @@ -237,26 +136,51 @@ Date filter changes `input type="date"` to take advantage of a browser's HTML5 d filterProperty="updatedAt" label="Last Updated"}} ``` -* `filter` - object - Default: null - * Required - * Must also expose the `filter` property on the parent `ember-tabular` component to be able to pass the `filter` object back and forth between parent and child components. -* `query` - object - Default: `this.get('query') || this.get('parentView.query')` - * Pass the query object from the parent component if it is different or if used outside of the context of the component, otherwise query is optional and it component will attempt to grab within the context of the parent component. -* `filterProperty` - string - Default: null - * Required - * Used with the "Global Filter Sub-Component". - * Pass the property name in camelCase format. -* `dateFilter` - string - Default: null - * Optional - * Sets the input value. -* `label` - string - Default: null - * Optional - * Set a label on the global-filter. -* `inputClass` - string - Default: null - * Optional - * Wraps the input field in a div. -* `labelClass` - string - Default: null - * Optional + +#### Dropdown Filter +Use the dropdown filter globally. One way to do this is by setting up a computed property that returns an array of label/value objects. + +```js + +export default Ember.Controller.extend({ + users: null, + actions: { + setIsAdminFilter(object) { + if (object) { + this.set('isAdminFilter', object.value); + } else { + this.set('isAdminFilter', null); + } + }, + }, + adminContent: Ember.computed(function() { + return [ + { + label: 'Yes', + value: true, + }, + { + label: 'No', + value: false, + }, + ]; + }), +}); +``` +```hbs +{{#ember-tabular-dropdown-filter filter=filter filterProperty="isAdmin" label="Is Admin" searchFilter=isAdminFilter}} + {{#power-select + options=adminContent + selected=(find-by adminContent 'value' isAdminFilter) + searchField="label" + searchEnabled=false + placeholder="Select to filter" + onchange=(action "setIsAdminFilter") + as |option|}} + {{option.label}} + {{/power-select}} +{{/ember-tabular-dropdown-filter}} +``` ## Note * This component adheres to jsonapi spec: http://jsonapi.org/ @@ -274,44 +198,7 @@ If you are using Ember Data, then you can lean on your application's custom adap * Filtering * This addon expects a `filter` object with nested property/value pairs. -If you are not using Ember Data then you can extend this addon's component and override a set of serialize and normalized methods: -```js -import EmberTabular from 'ember-tabular/components/ember-tabular'; - -export default EmberTabular.extend({ - serializePagination(params) { - // override default pagination ?page[offset]= and ?[page]limit= - // offset and limit will be sent as ?offset= and ?limit= - params.offset = (params.page * params.limit) - params.limit; - if (isNaN(params.offset)) { - params.offset = null; - } - - return params; - }, -}); -``` -```js -import EmberTabular from 'ember-tabular/components/ember-tabular'; - -export default EmberTabular.extend({ - serializeProperty(property) { - // Override to convert all properties sent in requests to camelize instead of the default dasherized - // ?filter[lastName]&sort=isAdmin - // (pseudo code) - if (property) { - return Ember.String.camelize(property); - } - - return null; - }, -}); -``` -Check add-on source for full list of serialized/normalized methods available for extension. -Note: - -* On success you must set the `record` with the array of table data - +If you are not using Ember Data or following json:api then you can extend this component and override a set of serialize and normalized methods, checkout the [API specs/documentation](/docs/index.html) for more details/examples. # Contributing to this addon ## Installation diff --git a/addon/components/ember-tabular-alert.js b/addon/components/ember-tabular-alert.js index 25b3be3..9f70d56 100644 --- a/addon/components/ember-tabular-alert.js +++ b/addon/components/ember-tabular-alert.js @@ -1,8 +1,28 @@ import Ember from 'ember'; +/** +* Any errors returned from the request(s) are displayed in an alert box. +* +* @class EmberTabularAlert +*/ export default Ember.Component.extend({ + /** + * @property tagName + * @type String + * @default 'div' + */ tagName: 'div', + /** + * @property type + * @type String + * @default 'info' + */ type: 'info', + /** + * @property typeClass + * @type String + * @default 'alert-[type]' + */ typeClass: Ember.computed('type', function () { return `alert-${this.get('type')}`; }), diff --git a/addon/components/ember-tabular-date-filter.js b/addon/components/ember-tabular-date-filter.js index 82aded6..49a57eb 100644 --- a/addon/components/ember-tabular-date-filter.js +++ b/addon/components/ember-tabular-date-filter.js @@ -1,3 +1,18 @@ import EmberTabularGlobalFilter from './ember-tabular-global-filter'; +/** +* ## Date Filter +* Date filter changes `input type="date"` to take advantage of a browser's HTML5 date widget. Typically the date filter component would be rendered into the `{{yield header}}` of the main table component using the yield conditional `{{#if section.isHeader}} ...`. +* +* However, it can be used outside of the context of the main component if the proper properties are shared between the main component and sub-component. +* +* - Sent in request as: `?filter[filterProperty]=dateFilter`, e.g. `?filter[updated-at]=2015-06-29` +```hbs +{{ember-tabular-date-filter + filter=filter + filterProperty="updatedAt" + label="Last Updated"}} +``` +* @class EmberTabularDateFilter +*/ export default EmberTabularGlobalFilter.extend(); diff --git a/addon/components/ember-tabular-dropdown-filter.js b/addon/components/ember-tabular-dropdown-filter.js index 82aded6..de7bf7a 100644 --- a/addon/components/ember-tabular-dropdown-filter.js +++ b/addon/components/ember-tabular-dropdown-filter.js @@ -1,3 +1,50 @@ import EmberTabularGlobalFilter from './ember-tabular-global-filter'; +/** +* ## Dropdown Filter +* Use the dropdown filter globally. One way to do this is by setting up a computed property that returns an array of label/value objects. +```js +export default Ember.Controller.extend({ + users: null, + actions: { + setIsAdminFilter(object) { + if (object) { + this.set('isAdminFilter', object.value); + } else { + this.set('isAdminFilter', null); + } + }, + }, + adminContent: Ember.computed(function() { + return [ + { + label: 'Yes', + value: true, + }, + { + label: 'No', + value: false, + } + ]; + }), +}); +``` +```hbs +{{#ember-tabular-dropdown-filter filter=filter filterProperty="isAdmin" label="Is Admin" searchFilter=isAdminFilter}} + {{#power-select + options=adminContent + selected=(find-by adminContent 'value' isAdminFilter) + searchField="label" + searchEnabled=false + placeholder="Select to filter" + onchange=(action "setIsAdminFilter") + as |option|}} + {{option.label}} + {{/power-select}} +{{/ember-tabular-dropdown-filter}} +``` +* +* @class EmberTabularDropdownFilter +* @extends EmberTabularGlobalFilter +*/ export default EmberTabularGlobalFilter.extend(); diff --git a/addon/components/ember-tabular-dropdown-limit.js b/addon/components/ember-tabular-dropdown-limit.js index e69f03a..77b4df3 100644 --- a/addon/components/ember-tabular-dropdown-limit.js +++ b/addon/components/ember-tabular-dropdown-limit.js @@ -1,12 +1,34 @@ import Ember from 'ember'; +/** +* Sets up component for changing the table row count/limit. +* +* @class EmberTabularDropdownLimit +*/ export default Ember.Component.extend({ + /** + * @property tagName + * @type String + * @default 'div' + */ tagName: 'div', classNames: ['ember-tabular-dropdown-limit'], - // populates limit dropdown + /** + * @property limits + * @type Array + * @default [10, 25, 50, 100, 500] + */ limits: [10, 25, 50, 100, 500], + /** + * Computed Property to determine if the result set is large enough to display the dropdown limit component. + * + * @property autoHide + * @param record + * @param count + * @return Boolean + */ autoHide: Ember.computed('record', 'count', function() { let record = this.get('record'); let count = this.get('count'); diff --git a/addon/components/ember-tabular-filter.js b/addon/components/ember-tabular-filter.js index 718273a..f34dcbb 100644 --- a/addon/components/ember-tabular-filter.js +++ b/addon/components/ember-tabular-filter.js @@ -1,11 +1,42 @@ import Ember from 'ember'; +/** +* Filtering on a column by column basis within the component's `
`. +* +* @class EmberTabularFilter +*/ export default Ember.Component.extend({ + /** + * @property tagName + * @type String + * @default 'th' + */ tagName: 'th', action: null, + /** + * Value of filter. + * + * @property headerFilter + * @type String + * @default '' + */ headerFilter: '', + /** + * Pass the `query` object from the parent component if it is different or if used outside of the context of the component, otherwise `query` is optional and the component will attempt to grab within the context of the parent component. + * + * @property query + * @type Object + * @default null + */ query: null, + /** + * Must expose the `filter` property on the parent ember-tabular component to be able to pass the filter object back and forth between parent and child components. + * + * @property filter + * @type Object + * @default null + */ filter: null, actions: { @@ -35,10 +66,18 @@ export default Ember.Component.extend({ // to avoid multiple requests for properties that are set on init this.addObserver('headerFilter', this.filterBy); }), - // observable property is set during init + /** + * Debounce the `filterName` method. + * observable property is set during init + * + * @method filterBy + */ filterBy: Ember.observer(function () { Ember.run.debounce(this, 'filterName', 750); }), + /** + * @property isClearable + */ isClearable: Ember.computed('headerFilter', function () { if (this.get('headerFilter')) { return true; @@ -52,6 +91,11 @@ export default Ember.Component.extend({ return 'YYYY-MM-DD'; } }), + /** + * Constructs and sets the `filter` Object. + * + * @method filterName + */ filterName() { const query = this.get('query'); const property = this.get('property'); diff --git a/addon/components/ember-tabular-global-filter.js b/addon/components/ember-tabular-global-filter.js index bcc5371..975805e 100644 --- a/addon/components/ember-tabular-global-filter.js +++ b/addon/components/ember-tabular-global-filter.js @@ -1,12 +1,54 @@ import Ember from 'ember'; +/** +* ## Global Filter +* Typically the global filter component would be rendered into the `{{yield header}}` of the main table component using the yield conditional `{{#if section.isHeader}} ...`. +* +* However, it can be used outside of the context of the main component if the proper properties are shared between the main component and sub-component. + +* - Sent in request as: `?filter[filterProperty]=searchFilter`, e.g. `?filter[username]=John.Doe2` +```hbs +{{ember-tabular-global-filter + filter=filter + filterProperty="username" + filterPlaceholder="Search by Username"}} +``` +* +* @class EmberTabularGlobalFilter +*/ export default Ember.Component.extend({ + /** + * @property tagName + * @type String + * @default 'div' + */ tagName: 'div', classNames: ['table-filter'], action: null, + /** + * Property to be filtered upon. + * + * @property filterProperty + * @type String + * @default null + */ filterProperty: null, + /** + * Value of filter. + * + * @property searchFilter + * @type String + * @default '' + */ searchFilter: '', + /** + * Pass the `query` object from the parent component if it is different or if used outside of the context of the component, otherwise `query` is optional and the component will attempt to grab within the context of the parent component. + * + * @property query + * @type Object + * @default null + */ query: null, filter: null, @@ -15,15 +57,29 @@ export default Ember.Component.extend({ this.set('searchFilter', ''); }, }, + /** + * Debounce the `filterName` method. + * + * @method filterTable + */ filterTable: Ember.observer('searchFilter', function () { Ember.run.debounce(this, 'filterName', 750); }), + /** + * @property isClearable + * @default false + */ isClearable: Ember.computed('searchFilter', function () { if (this.get('searchFilter')) { return true; } return false; }), + /** + * Constructs and sets the `filter` Object. + * + * @method filterName + */ filterName() { // Reference parent component query obj const query = this.get('query') || this.get('parentView.query'); diff --git a/addon/components/ember-tabular.js b/addon/components/ember-tabular.js index 0e46c05..26774f8 100644 --- a/addon/components/ember-tabular.js +++ b/addon/components/ember-tabular.js @@ -1,15 +1,135 @@ import Ember from 'ember'; +/** +* ## Basic Usage +* - `columns` - Controller array to setup the table headers/columns (detailed below). + - `modelName` - for the component to make the proper request when filtering/sorting, you must pass the model type matching your Ember model structure. e.g. brand/diagram, product. + - `record` - this is bound to the controller and is used to iterate over the table's model data. +* ### Template + ```hbs + {{! app/templates/my-route.hbs }} + + {{#ember-tabular columns=columns modelName="user" record=users as |section|}} + {{#if section.isBody}} + {{#each users as |row|}} + + + + + + + + {{/each}} + {{/if}} + {{/ember-tabular}} + ``` +* ### Controller +* Setup the columns array, which is how the table headers are constructed, `label` is required in all cases. + ```js + // app/controllers/my-route.js + + export default Ember.Controller.extend({ + users: null, + columns: [ + { + property: 'username', + label: 'Username', + defaultSort: 'username', + }, + { + property: 'emailAddress', + label: 'Email', + }, + { + property: 'firstName', + label: 'First Name', + }, + { + property: 'lastName', + label: 'Last Name', + }, + { + property: 'updatedAt', + label: 'Last Updated', + type: 'date', + }, + ], + }); + ``` +* +* @class EmberTabular +*/ export default Ember.Component.extend({ store: Ember.inject.service('store'), action: null, classNames: ['ember-tabular'], + /** + * Component will attempt to make a request to fetch the data. + * + * @property makeRequest + * @type Boolean + * @default true + */ makeRequest: true, + /** + * Used to toggle the filter row bar. + * + * @property showFilterRow + * @type Boolean + * @default false + */ showFilterRow: false, - // requires sharing the `filter`/`sort` property with the controller/service/etc to persist + /** + * Requires sharing the `filter`/`sort` property with the controller/service/etc to persist filter data + * + * @property persistFiltering + * @type Boolean + * @default false + */ persistFiltering: false, + /** + * @property sortableClass + * @type String + * @default 'sortable' + */ sortableClass: 'sortable', + /** + * @property tableWrapperClass + * @type String + * @default '' + */ + tableWrapperClass: '', + /** + * @property tableClass + * @type String + * @default 'table-bordered table-hover' + */ + tableClass: 'table-bordered table-hover', + /** + * @property paginationWrapperClass + * @type String + * @default '' + */ + paginationWrapperClass: '', + /** + * Once the `isRecordLoaded` is determined if true and no data exists then this is displayed. + * + * @property tableLoadedMessage + * @type String + * @default 'No Data.' + */ tableLoadedMessage: 'No Data.', + /** + * Computed Property to determine the column length dependent upon `columns`. + * + * @property columnLength + * @param columns {Array} + * @return {Number} + */ columnLength: Ember.computed('columns', function () { return this.get('columns').length; }), @@ -25,40 +145,220 @@ export default Ember.Component.extend({ isFooter: true, }, - // Model to be requested + /** + * Model to be requested using `makeRequest: true`. + * + * @property modelName + * @type String + * @default null + */ modelName: null, - // Bind variable for table data + /** + * This is typically bound to the controller and is used to iterate over the table's model data. + * + * @property record + * @type Object + * @default null + */ record: null, + /** + * This is typically setup on the controller and passed into the component, and is used to construct the table headers/filtering. + * + ```js + export default Ember.Controller.extend({ + users: null, + columns: [ + { + property: 'username', + label: 'Username', + defaultSort: 'username', + }, + { + property: 'emailAddress', + label: 'Email', + }, + { + property: 'firstName', + label: 'First Name', + sort: false, + }, + { + property: 'isAdmin', + label: 'Is Admin', + list: [ + { + label: 'Yes', + value: true, + }, + { + label: 'No', + value: false, + } + ], + }, + { + property: 'updatedAt', + label: 'Last Updated', + type: 'date', + }, + { + label: 'Actions', + }, + ], + }); + ``` + * + - `columns.property` - {String} + - Required for column filtering/sorting + - Properties should be in camelCase format + - `columns.label` - {String} + - Required in all use-cases + - `columns.type` - {String} - Default: text + - Sets the filter `` + - `columns.sort` - {Boolean} - Default: `true` + - Required for column sorting + - `columns.list` - {Array} - Default: `null` - Filtering the column based on a dropdown list. + - `list.label` - Displayed to the user for selection. + - `list.value` - Value that is sent in the request. + - `columns.defaultSort` - {String} + - Initial sort value for API request + - Will be overridden with any sorting changes + * + * @property columns + * @type Array + * @default null + */ columns: null, isDropdownLimit: true, // pagination defaults + /** + * @property page + * @type Number + * @default 1 + */ page: 1, + /** + * Used in request to construct pagination. + * + * @property limit + * @type Number + * @default 10 + */ limit: 10, + /** + * Number passed to the pagination add-on. + * + * @property pageLimit + * @type Number + * @default 0 + */ pageLimit: 0, + /** + * Used in request to construct pagination. + * + * @property offset + * @type Number + * @default 0 + */ offset: 0, + /** + * @property sort + * @type String + * @default null + */ sort: null, + /** + * @property filter + * @type Object + * @default null + */ filter: null, - // If additional static params are required in requests - // expects Object {} + /** + * Object to pass in static query-params that will not change based on any filter/sort criteria. + * Additional table-wide filters that need to be applied in all requests. Typically bound to the controller. + ```js + // app/controllers/location.js + + export default Ember.Controller.extend({ + staticParams: Ember.computed('model', function() { + return { + 'filter[is-open]': '1', + include: 'hours', + }; + }), + ... + }); + ``` + + ```hbs + {{! app/templates/my-route.hbs }} + + {{#ember-tabular columns=columns modelName="user" record=users staticParams=staticParams as |section|}} + ... + {{/ember-tabular}} + ``` + * + * @property staticParams + * @type Object + * @default null + */ staticParams: null, // State flags + /** + * @property isSuccess + * @type Boolean + * @default false + */ isSuccess: false, + /** + * @property isFailure + * @type Boolean + * @default false + */ isFailure: false, + /** + * @property isLoading + * @type Boolean + * @default false + */ isLoading: false, + /** + * @property defaultSuccessMessage + * @type String + * @default 'Success!' + */ defaultSuccessMessage: 'Success!', + /** + * @property defaultFailureMessage + * @type String + * @default 'There was an issue. Please check below for errors.' + */ defaultFailureMessage: 'There was an issue. Please check below for errors.', // Messages successMessage: Ember.get(Ember.Component, 'defaultSuccessMessage'), failureMessage: Ember.get(Ember.Component, 'defaultFailureMessage'), - // For pushing any per field errors + /** + * Conforms to json:api spec: http://jsonapi.org/format/#errors + * + * @property errors + * @type Array + * @default null + */ errors: null, + /** + * Used to serialize the parameters within `request`. + * + * @method serialize + * @param params {Object} An object of query parameters. + * @return params {Object} The serialized query parameters. + */ serialize(params) { // Serialize Pagination params = this.serializePagination(params); @@ -70,6 +370,36 @@ export default Ember.Component.extend({ return params; }, + /** + * Transform params related to pagination into API expected format. + * Follows json:api spec by default: http://jsonapi.org/format/#fetching-pagination. + * + * `offset` => `?page[offset]`. + * + * `limit` => `?page[limit]`. + * + * If you are not using Ember Data then you can extend this addon's component and override a set of serialize and normalized methods: + ```js + import EmberTabular from 'ember-tabular/components/ember-tabular'; + + export default EmberTabular.extend({ + serializePagination(params) { + // override default pagination ?page[offset]= and ?[page]limit= + // offset and limit will be sent as ?offset= and ?limit= + params.offset = (params.page * params.limit) - params.limit; + if (isNaN(params.offset)) { + params.offset = null; + } + + return params; + }, + }); + ``` + * + * @method serializePagination + * @param params {Object} An object of query parameters. + * @return params {Object} The serialized pagination query parameters. + */ serializePagination(params) { // Override to set dynamic offset based on page and limit params.offset = (params.page * params.limit) - params.limit; @@ -87,6 +417,15 @@ export default Ember.Component.extend({ return params; }, + /** + * Transform params related to filtering into API expected format. + * Follows json:api spec by default: http://jsonapi.org/recommendations/#filtering. + * `?filter[lastName]` => `?filter[last-name]`. + * + * @method serializeFilter + * @param params {Object} An object of query parameters. + * @return params {Object} The serialized filter query parameters. + */ serializeFilter(params) { // serialize filter query params const filter = params.filter; @@ -107,13 +446,47 @@ export default Ember.Component.extend({ return params; }, + /** + * Transform params related to sorting into API expected format. + * Follows json:api spec by default: http://jsonapi.org/format/#fetching-sorting. + * `?sort=lastName` => `?sort=last-name`. + * + * @method serializeSort + * @param params {Object} An object of query parameters. + * @return params {Object} The serialized sort query parameters. + */ serializeSort(params) { params.sort = this.serializeProperty(params.sort); return params; }, + /** + * Follows json:api dasherized naming. + * `lastName` => `last-name`. + * + * If you are not supporting json:api's dasherized properties this can be extended to support other conventions: + ```js + import EmberTabular from 'ember-tabular/components/ember-tabular'; + + export default EmberTabular.extend({ + serializeProperty(property) { + // Override to convert all properties sent in requests to camelize instead of the default dasherized + // ?filter[lastName]&sort=isAdmin + // (pseudo code) + if (property) { + return Ember.String.camelize(property); + } + return null; + }, + }); + ``` + * + * @method serializeProperty + * @param property {String} + * @return property {String} + */ serializeProperty(property) { if (property) { return Ember.String.dasherize(property); @@ -122,6 +495,14 @@ export default Ember.Component.extend({ return null; }, + /** + * Used to normalize query parameters returned from `request` to components expected format. + * + * @method normalize + * @param data {Object} Data object returned from request. + * @param params {Object} The returned object of query parameters. + * @return data {Object} + */ normalize(data, params) { // Normalize Pagination data = this.normalizePagination(data, params); @@ -133,6 +514,16 @@ export default Ember.Component.extend({ return data; }, + /** + * Used to normalize pagination related query parameters returned from `request` to components expected format. + * `?page[offset]` => `offset`. + * `?page[limit]` => `limit`. + * + * @method normalizePagination + * @param data {Object} Data object returned from request. + * @param params {Object} The returned object of query parameters. + * @return data {Object} + */ normalizePagination(data, params) { // pagination - return number of pages const pageLimit = Math.ceil(data.meta.total / params.page.limit); @@ -146,6 +537,15 @@ export default Ember.Component.extend({ return data; }, + /** + * Used to normalize filter related query parameters returned from `request` to components expected format. + * `?filter[last-name]` => `filter[lastName]`. + * `?filter[user.first-name]` => `filter[user.firstName]`. + * + * @method normalizeFilter + * @param query {Object} The returned object of query parameters. + * @return query {Object} + */ normalizeFilter(query) { // normalize filter[property-key] // into filter[propertyKey] @@ -176,10 +576,26 @@ export default Ember.Component.extend({ return query; }, + /** + * Used to normalize sort related query parameters returned from `request` to components expected format. + * Expects json:api by default. + * + * @method normalizeSort + * @param query {Object} The returned object of query parameters. + * @return query {Object} + */ normalizeSort(query) { return query; }, + /** + * Used to normalize properties to components expected format. + * By default this will camelize the property. + * + * @method normalizeProperty + * @param property {String} + * @return property {String} + */ normalizeProperty(property) { if (property) { return Ember.String.camelize(property); @@ -188,12 +604,23 @@ export default Ember.Component.extend({ return null; }, + /** + * @method segmentProperty + * @param property {String} + * @return segments {Array} + */ segmentProperty(property) { let segments = property.split('.'); return segments; }, + /** + * Determine if `record` is loaded using a number of different property checks. + * + * @property isRecordLoaded + * @type Function + */ isRecordLoaded: Ember.computed('errors', 'record', 'record.isFulfilled', 'record.isLoaded', 'modelName', function () { // If record array isLoaded but empty @@ -220,6 +647,14 @@ export default Ember.Component.extend({ return false; }), + /** + * Used in templates to determine if table header will allow filtering. + * + * @property isColumnFilters + * @type Boolean + * @return {Boolean} + * @default false + */ isColumnFilters: Ember.computed('columns', function () { const columns = this.get('columns'); @@ -232,6 +667,11 @@ export default Ember.Component.extend({ return false; }), + /** + * Runs on init to setup the table header default columns. + * + * @method setColumnDefaults + */ setColumnDefaults: Ember.on('init', function () { this.get('columns').map(function (column) { // if column does not have a sort property defined set to true @@ -249,6 +689,11 @@ export default Ember.Component.extend({ }); }), + /** + * Runs on init to set the default sort param. + * + * @method defaultSort + */ defaultSort: Ember.on('init', function () { const sort = this.get('sort'); if (!sort) { @@ -262,13 +707,20 @@ export default Ember.Component.extend({ willDestroy() { this._super(...arguments); - // clear any filters if we are persisting filtering + // clear any filters if we are not persisting filtering const persistFiltering = this.get('persistFiltering'); if (!persistFiltering) { this.set('filter', null); } }, + /** + * Constructs the query object to be used in `request`. + * + * @property query + * @type Object + * @return {Object} + */ query: Ember.computed('page', 'limit', 'offset', 'sort', 'filter.@each.value', 'staticParams', function () { let query = {}; @@ -289,6 +741,13 @@ export default Ember.Component.extend({ return query; }), + /** + * Make request to API for data. + * + * @method request + * @param params {Object} Serialized query parameters. + * @param modelName {String} + */ request(params, modelName) { params = this.serialize(params); @@ -308,6 +767,11 @@ export default Ember.Component.extend({ ); }, + /** + * Sets the `record` after the `request` is resolved. + * + * @method setModel + */ setModel: Ember.on('init', Ember.observer('query', 'makeRequest', function () { Ember.run.once(this, function () { // If makeRequest is false do not make request and setModel @@ -332,6 +796,12 @@ export default Ember.Component.extend({ }, }, + /** + * Sets the active sort property. + * + * @method setSort + * @param sortProperty {String} + */ setSort: Ember.on('didInsertElement', function (sortProperty) { if (this.get('sort') || sortProperty) { let property; @@ -354,6 +824,12 @@ export default Ember.Component.extend({ } }), + /** + * Sets the proper classes on table headers when sorting. + * + * @method updateSortUI + * @param sortProperty {String} + */ updateSortUI: Ember.on('didInsertElement', function (sortProperty) { if (this.get('sort') || sortProperty) { const _this = this; @@ -385,6 +861,10 @@ export default Ember.Component.extend({ } }), + /** + * @method failure + * @param response {Object} + */ failure(response) { this.reset(); this.setProperties({ @@ -398,6 +878,11 @@ export default Ember.Component.extend({ } }, + /** + * Resets all state specific properties. + * + * @method reset + */ reset() { this.setProperties({ isLoading: false, diff --git a/bower.json b/bower.json index 58f6155..d7896be 100644 --- a/bower.json +++ b/bower.json @@ -1,19 +1,8 @@ { "name": "ember-tabular", "dependencies": { - "ember": "~2.10.0", - "ember-cli-shims": "0.1.3", - "ember-cli-test-loader": "0.2.2", - "ember-qunit-notifications": "0.1.0", - "pretender": "~0.12.0", - "lodash": "3.7.0", "Faker": "~3.0.0", "bootstrap": "3.3.5", - "font-awesome": "4.6.2", - "DOM-shim": "Raynos/DOM-shim" - }, - "resolutions": { - "FakeXMLHttpRequest": "1.3.0", - "ember-qunit-notifications": "0.1.0" + "font-awesome": "4.6.2" } } diff --git a/config/ember-try.js b/config/ember-try.js index c0126c4..a070124 100644 --- a/config/ember-try.js +++ b/config/ember-try.js @@ -1,6 +1,22 @@ -/*jshint node:true*/ +/* eslint-env node */ module.exports = { scenarios: [ + { + name: 'ember-lts-2.4', + bower: { + dependencies: { + 'ember': 'components/ember#lts-2-4' + }, + resolutions: { + 'ember': 'lts-2-4' + } + }, + npm: { + devDependencies: { + 'ember-source': null + } + } + }, { name: 'ember-lts-2.8', bower: { @@ -10,6 +26,11 @@ module.exports = { resolutions: { 'ember': 'lts-2-8' } + }, + npm: { + devDependencies: { + 'ember-source': null + } } }, { @@ -21,6 +42,11 @@ module.exports = { resolutions: { 'ember': 'release' } + }, + npm: { + devDependencies: { + 'ember-source': null + } } }, { @@ -32,6 +58,11 @@ module.exports = { resolutions: { 'ember': 'beta' } + }, + npm: { + devDependencies: { + 'ember-source': null + } } }, { @@ -43,6 +74,17 @@ module.exports = { resolutions: { 'ember': 'canary' } + }, + npm: { + devDependencies: { + 'ember-source': null + } + } + }, + { + name: 'ember-default', + npm: { + devDependencies: {} } } ] diff --git a/config/environment.js b/config/environment.js index 28a787b..012a412 100644 --- a/config/environment.js +++ b/config/environment.js @@ -1,4 +1,4 @@ -/*jshint node:true*/ +/* eslint-env node */ 'use strict'; module.exports = function(/* environment, appConfig */) { diff --git a/ember-cli-build.js b/ember-cli-build.js index 26ac22c..0bec360 100644 --- a/ember-cli-build.js +++ b/ember-cli-build.js @@ -1,7 +1,6 @@ -/* jshint node:true */ -/* global require, module */ +/* eslint-env node */ var EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); -var pickFiles = require('broccoli-static-compiler'); +var Funnel = require('broccoli-funnel'); var mergeTrees = require('broccoli-merge-trees'); module.exports = function(defaults) { @@ -25,9 +24,9 @@ module.exports = function(defaults) { behave. You most likely want to be modifying `./index.js` or app's build file */ // Copy non-compiled bootstrap dependency - var vendor = pickFiles('bower_components/bootstrap/dist/css/', { + var vendor = new Funnel('bower_components/bootstrap/dist/css/', { srcDir: '/', - files: ['bootstrap.css.map'], + include: ['bootstrap.css.map'], destDir: '/assets' }); diff --git a/index.js b/index.js index eaa4865..7ae4f81 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,4 @@ -/* jshint node: true */ +/* eslint-env node */ 'use strict'; var Funnel = require('broccoli-funnel'); var mergeTrees = require('broccoli-merge-trees'); diff --git a/package.json b/package.json index e1214f2..1279517 100644 --- a/package.json +++ b/package.json @@ -13,39 +13,36 @@ }, "repository": "", "engines": { - "node": ">= 0.12.0" + "node": ">= 4" }, "author": "", "license": "Apache-2.0", "devDependencies": { - "broccoli-asset-rev": "^2.4.5", - "broccoli-funnel": "^1.0.0", - "broccoli-merge-trees": "^0.2.3", - "broccoli-static-compiler": "^0.2.2", - "ember-ajax": "^2.4.1", - "ember-cli": "2.10.0", - "ember-cli-app-version": "^2.0.0", - "ember-cli-dependency-checker": "^1.3.0", - "ember-cli-htmlbars": "^1.0.10", - "ember-cli-htmlbars-inline-precompile": "^0.3.3", + "broccoli-asset-rev": "2.5.0", + "broccoli-funnel": "1.2.0", + "broccoli-merge-trees": "2.0.0", + "ember-ajax": "2.5.2", + "ember-cli": "^2.13.2", + "ember-cli-app-version": "3.0.0", + "ember-cli-dependency-checker": "1.3.0", + "ember-cli-htmlbars": "1.1.1", + "ember-cli-htmlbars-inline-precompile": "0.4.3", "ember-cli-inject-live-reload": "^1.4.1", - "ember-cli-jshint": "^2.0.1", "ember-cli-mirage": "0.2.4", "ember-cli-moment-shim": "3.1.0", - "ember-cli-qunit": "^3.0.1", - "ember-cli-release": "^0.2.9", - "ember-cli-sri": "^2.1.0", - "ember-cli-test-loader": "^1.1.0", + "ember-cli-qunit": "4.0.0", + "ember-cli-shims": "1.0.2", + "ember-cli-sri": "2.1.1", "ember-cli-uglify": "^1.2.0", - "ember-data": "^2.10.0", + "ember-data": "2.13.1", "ember-disable-prototype-extensions": "^1.1.0", - "ember-disable-proxy-controllers": "1.0.1", - "ember-export-application-global": "^1.0.5", - "ember-load-initializers": "^0.5.1", + "ember-export-application-global": "^2.0.0", + "ember-load-initializers": "^1.0.0", "ember-moment": "7.3.0", "ember-power-select": "1.4.3", - "ember-resolver": "^2.0.3", - "loader.js": "4.0.9" + "ember-resolver": "^4.0.0", + "ember-source": "~2.13.0", + "loader.js": "^4.2.3" }, "keywords": [ "ember-addon", @@ -55,14 +52,14 @@ "json api" ], "dependencies": { - "ember-cli-babel": "^5.1.7", + "ember-cli-babel": "^6.0.0", "pagination-pager": "2.4.1", "ember-truth-helpers": "1.2.0", "ember-power-select": "1.4.3", - "broccoli-merge-trees": "^0.2.3", - "broccoli-funnel": "^1.0.0" + "broccoli-merge-trees": "2.0.0", + "broccoli-funnel": "1.2.0" }, "ember-addon": { "configPath": "tests/dummy/config" } -} +} \ No newline at end of file diff --git a/testem.js b/testem.js index 26044b2..b234048 100644 --- a/testem.js +++ b/testem.js @@ -1,6 +1,5 @@ -/*jshint node:true*/ +/* eslint-env node */ module.exports = { - "framework": "qunit", "test_page": "tests/index.html?hidepassed", "disable_watching": true, "launch_in_ci": [ diff --git a/testem.json b/testem.json deleted file mode 100644 index 0f35392..0000000 --- a/testem.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "framework": "qunit", - "test_page": "tests/index.html?hidepassed", - "disable_watching": true, - "launch_in_ci": [ - "PhantomJS" - ], - "launch_in_dev": [ - "PhantomJS", - "Chrome" - ] -} diff --git a/tests/.eslintrc.js b/tests/.eslintrc.js new file mode 100644 index 0000000..fbf2555 --- /dev/null +++ b/tests/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = { + env: { + embertest: true + } +}; diff --git a/tests/dummy/config/environment.js b/tests/dummy/config/environment.js index 703c472..6a378d3 100644 --- a/tests/dummy/config/environment.js +++ b/tests/dummy/config/environment.js @@ -1,4 +1,4 @@ -/* jshint node: true */ +/* eslint-env node */ module.exports = function(environment) { var ENV = { diff --git a/tests/dummy/config/targets.js b/tests/dummy/config/targets.js new file mode 100644 index 0000000..2bebfac --- /dev/null +++ b/tests/dummy/config/targets.js @@ -0,0 +1,10 @@ +/* eslint-env node */ + +module.exports = { + browsers: [ + 'ie 9', + 'last 1 Chrome versions', + 'last 1 Firefox versions', + 'last 1 Safari versions' + ] +}; diff --git a/tests/helpers/start-app.js b/tests/helpers/start-app.js index f3e7082..1794e77 100644 --- a/tests/helpers/start-app.js +++ b/tests/helpers/start-app.js @@ -9,16 +9,13 @@ util(); registerPowerSelectHelpers(); export default function startApp(attrs) { - let application; + let attributes = Ember.merge({}, config.APP); + attributes = Ember.merge(attributes, attrs); // use defaults, but you can override; - // use defaults, but you can override - let attributes = Ember.assign({}, config.APP, attrs); - - Ember.run(() => { - application = Application.create(attributes); + return Ember.run(() => { + let application = Application.create(attributes); application.setupForTesting(); application.injectTestHelpers(); + return application; }); - - return application; } diff --git a/tests/test-helper.js b/tests/test-helper.js index e6cfb70..f219659 100644 --- a/tests/test-helper.js +++ b/tests/test-helper.js @@ -2,5 +2,7 @@ import resolver from './helpers/resolver'; import { setResolver } from 'ember-qunit'; +import { start } from 'ember-cli-qunit'; setResolver(resolver); +start(); diff --git a/yuidoc.json b/yuidoc.json new file mode 100644 index 0000000..11dd498 --- /dev/null +++ b/yuidoc.json @@ -0,0 +1,19 @@ +{ + "name": "ember-tabular", + "description": "The default blueprint for ember-cli addons.", + "version": "0.2.0", + "options": { + "paths": [ + "addon" + ], + "enabledEnvironments": ["development", "production"], + "exclude": "vendor", + "outdir": "docs", + "linkNatives": true, + "quiet": true, + "parseOnly": false, + "lint": false, + "themedir" : "node_modules/yuidoc-bootstrap-theme", + "helpers" : [ "node_modules/yuidoc-bootstrap-theme/helpers/helpers.js" ] + } +} \ No newline at end of file
{{row.username}}{{row.emailAddress}}{{row.firstName}}{{row.lastName}} + {{#link-to "index" class="btn btn-xs" role="button"}} + Edit + {{/link-to}} +