diff --git a/.gitignore b/.gitignore index 677a6fc..b37e8ba 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .build* +.idea \ No newline at end of file diff --git a/CHILD_TABLE_README.md b/CHILD_TABLE_README.md new file mode 100644 index 0000000..d878570 --- /dev/null +++ b/CHILD_TABLE_README.md @@ -0,0 +1,212 @@ + +# Child (Sub) Tables BETA + +This guide shows how to add child/sub tables, this simply inserts a new row when expanded. +You can control the expand/collapse control and render an array on your data structure or +simply pass in a template for the sub table and handle it yourself. + + +**Requirements:** + +- Your data must have a unique `_id` field, if you're using a Collection this shouldn't be an issue, +otherwise add the field and you can use `Meteor.uuid()` for example. + + +--- + +### Expand Button Setup + +There are 2 implementations of the expand button + + +#### Simple Prepended Icon + +On any field you can add the setting `expandButton` + +Set `useFontAwesome = true` in the settings for a better icon +[https://github.com/aslagle/reactive-table#settings](https://github.com/aslagle/reactive-table#settings) + + + + fields: [ + + { + key: 'store_name', + label: 'Store Name', + expandButton: true + }, + ... + ] + +By itself this will prepend an expand/collapse icon to the cell. + +This respects the virtual column's `fn` function, if you use this it will prepend the icon to the result. + + + +#### Custom Expand Button Template + +First `expandButton` must be set to true, and if you are using a Template, +we will automatically pass in to the template data two helper functions, please note the underscore prefix +to avoid collisions. In fact the *\_.extend* I use to add these helpers prioritizes your data first, so you +can override these completely. + +- `_expandChildren` - show the child / sub table +- `_collapseChildren` - hide the above + +For example I can pass in my own template: + + fields: [ + + { + key: 'store_name', + label: 'Store Name', + expandButton: true, + tmpl: Template.StoreExpandCell + }, + ... + ] + +And then in my template I can create an event, where functions `this._expandChildren` and `this._collapseChildren` +are accessible. You can save your state however you wish, in my case I am using the DOM. + + + Template.StoreExpandCell.events({ + + 'click .my-expand-control': function( ev, t ){ + + var divContainer = $(ev.currentTarget); + var iconExpand = divContainer.find('.expand-icon'); + + if (!!divContainer.data('expanded')) { + iconExpand.removeClass( 'fa-minus-square-o' ).addClass( 'fa-plus-square-o' ); + divContainer.data('expanded', false); + this._collapseChildren(); + } + else { + iconExpand.removeClass( 'fa-plus-square-o' ).addClass( 'fa-minus-square-o' ); + divContainer.data('expanded', true); + this._expandChildren(); + } + + } + + }); + + +--- + + +### Child/Sub Table Setup + +You have two options for the contents of the child table, either a simple table rendered with the default reactive-table fields options +or a custom template + +The child table settings are added with the reactive-table setting `children` + + +#### Child Table + +For a child table you need two settings in `children`, these are `dataField` and `fields` + +`dataField` - this must be a field referencing an array of structs, for example if I have the following data, +you may notice that `store_locations` is actually an array, I don't reference it in my parent table `fields` +setting, and plan to use it to iterate over and generate my child table. + + [ + { + _id: 1, + store_name: 'ABC Store', + store_locations: [ + { + _id: 1000, + city: 'San Francisco' + }, + ... + ] + }, + { + _id: 2, + store_name: 'Tom\'s Hardware' + store_locations: [ + { + _id: 2000 + city: 'Chicago' + }, + { + _id: 2001, + city: 'New York' + }, + { + _id: 2002, + city: 'Los Angeles' + } + ... + ] + } + ... + ] + + + +`fields` - similar to the `fields` setting on the parent table, all the same functionality is retained such as +virtual columns, templates and styling options. It will iterate over the array referenced by the key `dataField` +and generate a row for each iteration with the `children.fields` specification. + +`expandIcon` - Make expand icon customizable.
+`collapseIcon` - Make collapse icon customizable. + + fields: [ + + { + key: 'store_name', + label: 'Store Name', + expandButton: true, + tmpl: Template.StoreExpandCell + }, + ... + ], + + children: { + dataField: 'store_locations', + fields: [ + { + key: 'city', + label: 'Location' + } + ... + ], + expandIcon: 'fa fa-angle-down', + collapseIcon: 'fa fa-angle-up', + } + + + +#### Custom Template + +Simply add the template to a `children` field, and the entire data object will be passed into your specified template +for you to reference + + fields: [ + + { + key: 'store_name', + label: 'Store Name', + expandButton: true, + tmpl: Template.StoreExpandCell + }, + ... + ], + + children: { + tmpl: Template.StoreLocations + } + + +### Nested Reactive Tables + +You can definitely nest entire reactive tables now with the `tmpl` option, in the above example Template.StoreLocations could +simply insert another reactive table + +*However if you use the `expandButton` option for controls, you need to a specify a new setting `childrenExpandIconClass` on the settings object +you pass into reactive table, this ensures the expand icons don't conflict, I'm using a DOM $.find CSS to toggle these for now* diff --git a/README.md b/README.md index e478c37..b599db3 100644 --- a/README.md +++ b/README.md @@ -211,6 +211,11 @@ You can specify a template to use to render cells in a column, by adding `tmpl` The template's context will be the full object, so it will have access to all fields. + +It's useful to note that irregardless of what's displayed by tmpl, the `fn` column field is what's used for sorting. +This allows you to handle situations where you want a custom value to sort on (`sortByValue` doesn't work) and still +want the additional formatting provided by a template. + #### Virtual columns You can also compute a function on the attribute's value to display in the table, by adding `fn` to the field. diff --git a/lib/reactive_table.css b/lib/reactive_table.css index 6e40f2d..a299de9 100644 --- a/lib/reactive_table.css +++ b/lib/reactive_table.css @@ -1,47 +1,47 @@ .reactive-table-options { - padding-right: 0px; - margin-right: -5px; + padding-right: 0px; + margin-right: -5px; } .reactive-table-filter { - float: right; + float: right; } .reactive-table-columns-dropdown { - float: right; + float: right; } .reactive-table-columns-dropdown button { - float: right; + float: right; } -.reactive-table .sortable, .reactive-table-add-column { - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; +.reactive-table.sortable, .reactive-table-add-column { + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } .table > thead > tr > th.reactive-table-add-column { - border-bottom: none; + border-bottom: none; } .reactive-table-navigation { - display: inline-block; - width: 100%; + display: inline-block; + width: 100%; } .reactive-table-navigation .form-inline input { - width: 45px; + width: 45px; } .reactive-table-navigation .rows-per-page { - float: left; + float: left; } .reactive-table-navigation .page-number { - float: right; + float: right; } .reactive-table-navigation .previous-page, @@ -53,20 +53,36 @@ .reactive-table-navigation .previous-page.fa, .reactive-table-navigation .next-page.fa { - vertical-align: middle; + vertical-align: middle; } .reactive-table-navigation .previous-page:hover, .reactive-table-navigation .next-page:hover { - cursor: pointer; + cursor: pointer; } .reactive-table .fa-sort-asc { - position: relative; - top: -2px; + position: relative; + top: -2px; } .reactive-table .fa-sort-desc { - position: relative; - top: 2px; + position: relative; + top: 2px; } + +.reactive-table .reactive-table-children-expand { + cursor: pointer; +} + +.reactive-table .reactive-table-children-container .error { + color: #d9534f; +} + +.reactive-table .reactive-table-children-container td { + padding: 0; +} + +.reactive-table-children { + width: 100%; +} \ No newline at end of file diff --git a/lib/reactive_table.html b/lib/reactive_table.html index 8f205ab..ed3a553 100644 --- a/lib/reactive_table.html +++ b/lib/reactive_table.html @@ -69,13 +69,86 @@ {{#each sortedRows}} - + {{#each ../fields}} {{#if isVisible}} - {{#if tmpl}}{{#with ..}}{{> ../tmpl}}{{/with}}{{else}}{{getField ..}}{{/if}} + + {{#if tmpl}} + {{#with ..}} + {{#if ../expandButton}} + {{#with extendExpandHelpers}} + {{> ../../tmpl}} + {{/with}} + {{else}} + {{> ../tmpl}} + {{/if}} + {{/with}} + {{else}} + {{#if expandButton}} + {{#if ../../useFontAwesome}} + {{#if ../../children}} + {{#if isVisibleChild ../_id}} + + {{else}} + + {{/if}} + {{/if}} + {{else}} + + {{/if}} + {{/if}} + {{getField ..}} + {{/if}} + {{/if}} {{/each}} + {{#if ../children}} + {{#if isVisibleChild}} + + + {{#if ../children/fields}} + + + + {{#each ../children.fields}} + + {{/each}} + + + + {{#each getChildrenRows}} + + {{#each ../../children.fields}} + + {{/each}} + + {{/each}} + +
+ {{label}} +
+ {{#if tmpl}} + {{#with ..}} + {{> ../tmpl}} + {{/with}} + {{else}} + {{getField ..}} + {{/if}} +
+ {{else}} + {{#if ../children/tmpl}} + {{#with extendExpandHelpers}} + {{> ../../children/tmpl}} + {{/with}} + {{else}} + if children field exists, you must have defined either tmpl or fields + {{/if}} + {{/if}} + + + {{/if}} + {{/if}} {{/each}} diff --git a/lib/reactive_table.js b/lib/reactive_table.js index ad46c0b..8267a97 100644 --- a/lib/reactive_table.js +++ b/lib/reactive_table.js @@ -107,6 +107,7 @@ var setup = function () { context.templateData = this.data; this.data.settings = this.data.settings || {}; var collection = this.data.collection || this.data.settings.collection || this.data; + var hasChildren = false; if (!(collection instanceof Mongo.Collection)) { if (_.isArray(collection)) { @@ -214,9 +215,17 @@ var setup = function () { var visibleFields = []; _.each(fields, function (field, i) { visibleFields.push({fieldId:field.fieldId, isVisible: getDefaultFieldVisibility(field)}); + if (!!field.expandButton && context.templateData.settings.children) { + hasChildren = true; + } }); context.visibleFields = (!_.isUndefined(oldContext.visibleFields) && !_.isEmpty(oldContext.visibleFields)) ? oldContext.visibleFields : new ReactiveVar(visibleFields); + if (hasChildren) { + context.visibleChildren = new ReactiveVar([]); + context.childrenExpandIconClass = this.data.settings.childrenExpandIconClass || 'reactive-table-children-expand'; + context.children = this.data.settings.children; + } var rowClass = this.data.rowClass || this.data.settings.rowClass || function() {return '';}; if (typeof rowClass === 'string') { @@ -518,6 +527,10 @@ Template.reactiveTable.helpers({ } }, + 'getChildrenRows': function() { + var childrenRows = this[ Template.parentData().children.dataField ]; + return childrenRows instanceof Array ? childrenRows : []; + }, 'noData': function () { var pageCount = getPageCount.call(this); return (pageCount === 0) && this.noDataTmpl; @@ -548,7 +561,70 @@ Template.reactiveTable.helpers({ if (this.showNavigation === 'never') return false; return getPageCount.call(this) > 1; }, - 'getRowCount': getRowCount + 'getRowCount': getRowCount, + + 'getChildrenTmpl': function () { + return Template.instance().context.children != null && + Template.instance().context.children.tmpl + }, + 'extendExpandHelpers': function () { + + var template = Template.instance(); + + return _.extend({ + + '_expandChildren': function () { + if (template.context.visibleChildren) { + var visibleChildren = template.context.visibleChildren.get(); + visibleChildren.push(this._id); + template.context.visibleChildren.set(visibleChildren); + } + }, + '_collapseChildren': function() { + if (template.context.visibleChildren) { + var visibleChildren = template.context.visibleChildren.get(); + visibleChildren = _.without( visibleChildren, this._id ); + template.context.visibleChildren.set(visibleChildren); + } + } + + }, this) + }, + 'isVisibleChild': function (id) { + if (Template.instance().context.visibleChildren) { + var visibleChildren = Template.instance().context.visibleChildren.get(); + if (id) { + return _.indexOf(visibleChildren, id) >= 0; + } + return _.indexOf(visibleChildren, this._id) >= 0; + } + }, + 'getExpandIcon': function() { + var instance = Template.instance(); + var useFontAwesome = instance.context.useFontAwesome; + if (!useFontAwesome) { + return; + } + var children = instance.context.children; + if (children && children.expandIcon) { + return children.expandIcon; + } + + return 'fa fa-plus-square-o'; + }, + 'getCollapseIcon': function() { + var instance = Template.instance(); + var useFontAwesome = instance.context.useFontAwesome; + if (!useFontAwesome) { + return; + } + var children = instance.context.children; + if (children && children.collapseIcon) { + return children.collapseIcon; + } + + return 'fa fa-minus-square-o'; + }, }); Template.reactiveTable.events({ @@ -614,5 +690,58 @@ Template.reactiveTable.events({ var currentPage = template.context.currentPage.get(); template.context.currentPage.set(currentPage + 1); template.updateHandle(template.context); + }, + + 'click .reactive-table > tbody > tr > td': function (event) { + var $target = $(event.target); + var iconExpand; + var template = Template.instance(); + if (!template.context.visibleChildren) { + return; + } + + // TODO: there is a bug if you call collapseChildren elsewhere, the icons need to update + if (this.tmpl) { + if (!$target.is('td')) { + return; + } + // Find iconExpand + const $parentRow = $target.parent('tr'); + iconExpand = $parentRow.find('.' + template.context.childrenExpandIconClass); + + } else { + iconExpand = $(event.currentTarget).find('.' + template.context.childrenExpandIconClass); + if (!iconExpand.length) { + if (!$target.is('td')) { + return; + } + + // Find iconExpand + const $parentRow = $target.parent('tr'); + iconExpand = $parentRow.find('.' + template.context.childrenExpandIconClass); + } + } + + + var dataId = iconExpand.data('id'); + var visibleChildren = template.context.visibleChildren.get(); + + if (!template.context.useFontAwesome){ + if (iconExpand.html() === '➕'){ + iconExpand.html('➖'); + } + else if (iconExpand.html() === '➖'){ + iconExpand.html('➕'); + } + } + + if (_.indexOf(visibleChildren, dataId) < 0){ + visibleChildren.push(dataId); + } + else { + visibleChildren = _.without(visibleChildren, dataId); + } + + template.context.visibleChildren.set(visibleChildren); } });