diff --git a/.gitignore b/.gitignore index 86fceae..2591bb1 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ /libpeerconnection.log npm-debug.log testem.log +/docs/* diff --git a/CHANGELOG.md b/CHANGELOG.md index 4058b92..c04cccc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Add additional configurable wrapper classes for additional styling. - 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. +- Add support for YuiDocs for better API documentation. +- Setup deployment to gh-pages for demo URL. ### Changed - Update legacy name references in README.md. diff --git a/README.md b/README.md index e79089f..b50ea26 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,92 +96,7 @@ 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 +## Misc ### Template - Yields ```hbs @@ -206,26 +127,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 +138,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 +200,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-filter.js b/addon/components/ember-tabular-filter.js index 666f010..f380290 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: { @@ -20,15 +51,26 @@ export default Ember.Component.extend({ } }, }, + /** + * Debounce the `filterName` method. + * + * @method filterBy + */ filterBy: Ember.observer('headerFilter', function () { Ember.run.debounce(this, 'filterName', 750); }), + /** + * @property isClearable + */ isClearable: Ember.computed('headerFilter', function () { if (this.get('headerFilter')) { return true; } return false; }), + /** + * @property inputPlaceholder + */ inputPlaceholder: Ember.computed('header.type', function () { const type = this.get('header.type'); @@ -36,6 +78,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..b828ca4 100644 --- a/addon/components/ember-tabular-global-filter.js +++ b/addon/components/ember-tabular-global-filter.js @@ -1,13 +1,62 @@ 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, + /** + * 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: { @@ -15,15 +64,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 32a6385..abac666 100644 --- a/addon/components/ember-tabular.js +++ b/addon/components/ember-tabular.js @@ -1,13 +1,127 @@ 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, + /** + * @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; }), @@ -23,31 +137,198 @@ 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 @@ -55,8 +336,22 @@ export default Ember.Component.extend({ 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); @@ -68,6 +363,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; @@ -85,6 +410,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; @@ -105,13 +439,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); @@ -120,6 +488,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); @@ -131,6 +507,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); @@ -144,6 +530,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] @@ -173,10 +568,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); @@ -185,12 +596,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 @@ -217,6 +639,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'); @@ -229,6 +659,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 @@ -242,6 +677,11 @@ export default Ember.Component.extend({ }); }), + /** + * Runs on init to set the default sort param. + * + * @method defaultSort + */ defaultSort: Ember.on('init', function () { this.get('columns').map(function (el) { if (el.hasOwnProperty('defaultSort')) { @@ -250,6 +690,13 @@ export default Ember.Component.extend({ }.bind(this)); }), + /** + * 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 = {}; @@ -270,6 +717,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); @@ -289,6 +743,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 @@ -313,6 +772,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; @@ -335,6 +800,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; @@ -366,6 +837,10 @@ export default Ember.Component.extend({ } }), + /** + * @method failure + * @param response {Object} + */ failure(response) { this.reset(); this.setProperties({ @@ -379,6 +854,11 @@ export default Ember.Component.extend({ } }, + /** + * Resets all state specific properties. + * + * @method reset + */ reset() { this.setProperties({ isLoading: false, diff --git a/package.json b/package.json index 52af321..dc4bc00 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "ember-cli": "2.4.2", "ember-cli-app-version": "^1.0.0", "ember-cli-dependency-checker": "^1.2.0", + "ember-cli-github-pages": "0.1.0", "ember-cli-htmlbars": "^1.0.1", "ember-cli-htmlbars-inline-precompile": "^0.3.1", "ember-cli-inject-live-reload": "^1.3.1", @@ -35,6 +36,7 @@ "ember-cli-release": "0.2.8", "ember-cli-sri": "^2.1.0", "ember-cli-uglify": "^1.2.0", + "ember-cli-yuidoc": "0.8.4", "ember-data": "2.4.3", "ember-disable-prototype-extensions": "^1.1.0", "ember-disable-proxy-controllers": "^1.0.1", @@ -56,14 +58,16 @@ "json api" ], "dependencies": { + "broccoli-funnel": "^1.0.0", + "broccoli-merge-trees": "^0.2.3", "ember-cli-babel": "^5.1.5", - "pagination-pager": "2.4.1", - "ember-truth-helpers": "1.2.0", "ember-power-select": "0.10.10", - "broccoli-merge-trees": "^0.2.3", - "broccoli-funnel": "^1.0.0" + "ember-truth-helpers": "1.2.0", + "pagination-pager": "2.4.1", + "yuidoc-bootstrap-theme": "^1.0.6" }, "ember-addon": { - "configPath": "tests/dummy/config" + "configPath": "tests/dummy/config", + "demoURL": "http://caxiam.github.io/ember-tabular/" } } diff --git a/tests/dummy/config/environment.js b/tests/dummy/config/environment.js index c59bcd5..6b2d9a0 100644 --- a/tests/dummy/config/environment.js +++ b/tests/dummy/config/environment.js @@ -40,7 +40,12 @@ module.exports = function(environment) { } if (environment === 'production') { + ENV.locationType = 'hash'; + ENV.baseURL = '/ember-tabular/'; + ENV['ember-cli-mirage'] = { + enabled: true + }; } return ENV; diff --git a/tests/dummy/mirage/config.js b/tests/dummy/mirage/config.js index 6360d25..a126050 100644 --- a/tests/dummy/mirage/config.js +++ b/tests/dummy/mirage/config.js @@ -17,6 +17,7 @@ function filterObj(where, collection, query) { export default function() { this.namespace = ''; + this.urlPrefix = '/'; /* Routes diff --git a/yuidoc.json b/yuidoc.json new file mode 100644 index 0000000..09ab1e9 --- /dev/null +++ b/yuidoc.json @@ -0,0 +1,19 @@ +{ + "name": "ember-tabular", + "description": "The default blueprint for ember-cli addons.", + "version": "0.1.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}} +