diff --git a/CHANGELOG.md b/CHANGELOG.md index d7d3d2323eb..2268ee0eef7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,42 @@ All notable changes for each version of this project will be documented in this - `IgxIconModule`: - **Breaking change** `igxIconService` is now provided in root (providedIn: 'root') and `IgxIconModule.forRoot()` method is deprecated. - **Breaking change** `glyphName` property of the `igxIconComponent` is deprecated. +- `IgxMask`: + - `placeholder` input property is added to allow developers to specify the placeholder attribute of the host input element that the `igxMask` is applied on; + - `displayValuePipe` input property is provided that allows developers to additionally transform the value on blur; + - `focusedValuePipe` input property is provided that allows developers to additionally transform the value on focus; - `IgxTreeGrid`: + - Batch editing - an injectable transaction provider accumulates pending changes, which are not directly applied to the grid's data source. Those can later be inspected, manipulated and submitted at once. Changes are collected for individual cells or rows, depending on editing mode, and accumulated per data row/record. - You can now export the tree grid both to CSV and Excel. - The hierarchy and the records' expanded states would be reflected in the exported Excel worksheet. +## 7.0.3 +### Bug fixes + +- ng add igniteui-angular adds igniteui-cli package to both dependencies and devDependencies ([#3254](https://github.com/IgniteUI/igniteui-angular/issues/3254)) +- Group column header is not styled correctly when moving that column ([#3072](https://github.com/IgniteUI/igniteui-angular/issues/3072)) +- igx-grid: Filter row remains after disabling filtering feature ([#3255](https://github.com/IgniteUI/igniteui-angular/issues/3255)) +- [igxGrid] Keyboard navigation between cells and filtering row with MCH ([#3179](https://github.com/IgniteUI/igniteui-angular/issues/3179)) +- Argument $color of red($color) must be a color ([#3190](https://github.com/IgniteUI/igniteui-angular/issues/3190)) +- Shell strings localization ([#3237](https://github.com/IgniteUI/igniteui-angular/issues/3237)) +- Tabbing out of the combo search input not possible ([#3200](https://github.com/IgniteUI/igniteui-angular/issues/3200)) +- Localization (i18n) not available for inputs/buttons on the grid filtering dialog ([#2517](https://github.com/IgniteUI/igniteui-angular/issues/2517)) +- When in the tree grid are pinned columns and scroll horizontal the cells text is over the pinned text #3163 +- Request for update of shell strings in Japanese ([#3163](https://github.com/IgniteUI/igniteui-angular/issues/3163)) +- Refactor(themes): remove get-function calls ([#3327](https://github.com/IgniteUI/igniteui-angular/issues/3327)) +- Fix(grid): recalculate grid body size when changing allowFiltering dynamically ([#3321](https://github.com/IgniteUI/igniteui-angular/issues/3321)) +- Fix - Combo - Hide Search input when !filterable && !allowCustomValues - 7.0.x ([#3314](https://github.com/IgniteUI/igniteui-angular/issues/3314)) +- Fixing column chooser column updating - 7.0.x ([#3235](https://github.com/IgniteUI/igniteui-angular/issues/3235)) +- Disable combo checkbox animations on scroll ([#3303](https://github.com/IgniteUI/igniteui-angular/issues/3303)) +- Added validation if last column collides with grid's scroll. ([#3028](https://github.com/IgniteUI/igniteui-angular/issues/3028)) ([#3100](https://github.com/IgniteUI/igniteui-angular/issues/3100)) +- Use value instead of ngModel to update editValue for checkbox and calendar in igxCell ([#3225](https://github.com/IgniteUI/igniteui-angular/issues/3225)) +- Add @inheritdoc, create ScrollStrategy abstract class and fix method signatures 7.0.x ([#3222](https://github.com/IgniteUI/igniteui-angular/issues/3222)) +- When scroll with the mouse wheel the value in datePicker editor for edited cell is empty ([#2958](https://github.com/IgniteUI/igniteui-angular/issues/2958)) +- igxToolbar should have the option to add custom template ([#2983](https://github.com/IgniteUI/igniteui-angular/issues/2983)) +- fix(grid): mark grid for check inside NgZone when resizing ([#2792](https://github.com/IgniteUI/igniteui-angular/issues/2792)) ([#3277](https://github.com/IgniteUI/igniteui-angular/issues/3277)) +- IgxGridHeaderGroupComponent should have preset min width ([#3071](https://github.com/IgniteUI/igniteui-angular/issues/3071)) +- Tree grid selection ([#3334](https://github.com/IgniteUI/igniteui-angular/issues/3334)) + ## 7.0.2 ### Features - `ng add igniteui-angular` support :tada: diff --git a/README.md b/README.md index 6c87fb4422d..217640b95f5 100644 --- a/README.md +++ b/README.md @@ -16,39 +16,43 @@ You can find source files under the [`src`](https://github.com/IgniteUI/igniteui ![](https://dl.infragistics.com/tools/extensions/angular-tooltips/tooltip_preview.gif) **IMPORTANT** The repository has been renamed from `igniteui-js-blocks` to `igniteui-angular`. Read more on our new [naming convention](https://www.infragistics.com/community/blogs/b/infragistics/posts/ignite-ui-github-repo-name-changes). - + Current list of controls include: | *Components* | Status | | | | *Directives* | Status | | | | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | | **avatar** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/avatar/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/avatar.html) | | **button** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/directives/button/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/button.html) | -| **badge** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/badge/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/badge.html) | | **dragdrop** | InProgress | [Readme](https://github.com/IgniteUI/igniteui-angular/tree/master/projects/igniteui-angular/src/lib/directives/dragdrop/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/dragdrop.html) | -| **buttonGroup** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/buttonGroup/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/buttongroup.html) | | **filter** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/directives/filter/README-FILTER.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/list.html) | -| **calendar** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/calendar/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/calendar.html) | | **forOf** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/directives/for-of/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/for_of.html) | +| **badge** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/badge/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/badge.html) | | **dragdrop** | InProgress | [Readme](https://github.com/IgniteUI/igniteui-angular/tree/master/projects/igniteui-angular/src/lib/directives/dragdrop/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/drag_drop.html) | +| **banner** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/banner/README.md) | | | **filter** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/directives/filter/README-FILTER.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/list.html) | +| **buttonGroup** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/buttonGroup/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/buttongroup.html) | | **focus** | Available | | | +| **calendar** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/calendar/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/calendar.html) | | **forOf** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/directives/for-of/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/for_of.html) | | **card** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/card/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/card.html) | | **hint** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/tree/master/projects/igniteui-angular/src/lib/input-group/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/input_group.html) | | **carousel** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/carousel/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/carousel.html) | | **input** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/directives/input/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/input_group.html) | | **checkbox** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/checkbox/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/checkbox.html) | | **label** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/directives/label/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/input_group.html) | -| **chips** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/chips/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/chip.html) | | **layout** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/directives/layout/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/layout.html) | +| **chips** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/chips/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/chip.html) | | **layout** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/directives/layout/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/layout.html) | | **circular progress** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/progressbar/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/circular_progress.html) | | **mask** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/directives/mask/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/mask.html) | -| **combo** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/combo/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/combo.html) | | **prefix** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/input-group/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/input_group.html) | -| **datePicker** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/date-picker/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/date_picker.html) | | **ripple** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/directives/ripple/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/button.html) | -| **dialog** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/dialog/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/dialog.html) | | **suffix** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/tree/master/projects/igniteui-angular/src/lib/input-group/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/input_group.html) | -| **drop down** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/drop-down/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/drop_down.html) | | **text-highlight** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/tree/master/projects/igniteui-angular/src/lib/directives/text-highlight/README.md) | | -| **grid** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/grid/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/grid.html) | | **text-selection** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/tree/master/projects/igniteui-angular/src/lib/directives/text-selection/README.md) | | -| **icon** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/icon/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/icon.html) | | **toggle** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/directives/toggle/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/toggle.html) | -| **input group** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/input-group/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/input_group.html) | | *Others* | Status | Docs | | -| **list** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/list/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/list.html) | | **Animations** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/animations/README.md) | | -| **navbar** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/navbar/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/navbar.html) | | **dataUtil** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/data-operations/README-DATAUTIL.md) | | -| **navigation drawer** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/navigation-drawer/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/navdrawer.html) | | **dataContainer** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/data-operations/README-DATACONTAINER.md) | | -| **radio** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/radio/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/radio_button.html) | | | | | | -| **slider** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/slider/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/slider.html) | | | | | | +| **combo** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/combo/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/combo.html) | | **prefix** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/input-group/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/input_group.html) | +| **datePicker** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/date-picker/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/date_picker.html) | | **radio-group** | Available | | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/radio_button.html#radio-group)| +| **dialog** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/dialog/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/dialog.html) | | **ripple** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/directives/ripple/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/button.html) | +| **drop down** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/drop-down/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/drop_down.html) | | **scroll-inertia** | Available | | | +| **expansion-panel** | Available | | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/expansion_panel.html) | | **suffix** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/tree/master/projects/igniteui-angular/src/lib/input-group/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/input_group.html) | +| **grid** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/grids/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/grid.html) | | **template-outlet** | Available | | | +| **icon** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/icon/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/icon.html) | | **text-highlight** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/tree/master/projects/igniteui-angular/src/lib/directives/text-highlight/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/texthighlight.html) | +| **input group** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/input-group/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/input_group.html) | | **text-selection** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/tree/master/projects/igniteui-angular/src/lib/directives/text-selection/README.md) | | | +| **linear progress** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/tree/master/projects/igniteui-angular/src/lib/progressbar) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/linear_progress.html) | | **toggle** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/directives/toggle/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/toggle.html) | +| **list** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/list/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/list.html) | | **tooltip** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/directives/tooltip/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/tooltip.html) | +| **navbar** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/navbar/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/navbar.html) | | *Others* | Status | Docs | | +| **navigation drawer** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/navigation-drawer/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/navdrawer.html) | | **Animations** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/animations/README.md) | | +| **radio** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/radio/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/radio_button.html) | | **dataUtil** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/data-operations/README-DATAUTIL.md) | | +| **slider** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/slider/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/slider.html) | | **dataContainer** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/data-operations/README-DATACONTAINER.md) | | | **snackbar** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/snackbar/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/snackbar.html) | | | | | | | **switch** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/switch/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/switch.html) | | | | | | | **tabbar** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/tabbar/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/tabbar.html) | | | | | | | **tabs** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/tabs/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/tabs.html) | | | | | | | **time picker** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/time-picker/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/time_picker.html) | | | | | | | **toast** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/toast/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/toast.html) | | | | | | +| **tree grid** | Available | [Readme](https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/grids/tree-grid/README.md) | [Docs](https://www.infragistics.com/products/ignite-ui-angular/angular/components/treegrid.html) | | | | | | ## Setup @@ -58,7 +62,7 @@ From the root folder run: npm install ``` -## Getting Started with CLI +## Create new Project with Ignite UI CLI To get started with the Ignite UI CLI and Ignite UI for Angular: ``` @@ -69,6 +73,33 @@ ig add grid ig start ``` +## Adding IgniteUI for Angular to Existing Project + +Including the `igniteui-angular` and `igniteui-cli` packages to your project: + +``` +ng add igniteui-angular +``` + +After this operation you can use the Ignite UI CLI commands in your project, such as `ig` and `ig add`. +[Learn more](https://github.com/IgniteUI/igniteui-cli#usage) + +## Updating Existing Project + +Analyze your project for possible migrations: + +``` +ng update +``` + +If there are new versions available, update your packages: + +``` +ng update igniteui-angular +... +ng update igniteui-cli +``` + ## Building the Library ``` // build the code @@ -126,18 +157,11 @@ You can include Ignite UI for Angular in your project as a dependency using the [General Naming Guidelines](../../wiki//General-Naming-Guidelines-for-Ignite-UI-for-Angular) - -## Quickstart App -[Ignite UI for Angular Quickstart app](https://github.com/IgniteUI/igniteui-angular-quickstart) -This repository is a fork of the Angular QuickStart Source and has been updated to demonstrate how to include and use components from Ignite UI for Angular. It basically follows the shortest path to bootstrap writing the application with Ignite UI for Angular: - -- Fork the [the angular quickstart](https://github.com/angular/quickstart) -- Install Ignite UI for Angular from npm using `npm install igniteui-angular --save-dev` -- Update the views with sample Ignite UI for Angular controls. - ## Demo Apps & Documentation The [Warehouse Picklist App](https://github.com/IgniteUI/warehouse-js-blocks) demonstrates using several Ignite UI for Angular widgets together to build a modern, mobile app. +The [Crypto Portfolio App](https://igniteui.github.io/crypto-portfolio-app/#/home) is web and mobile application, developed with Ignite UI for Angular most solid components and styled with one of a kind theming engine. + To get started with the Data Grid, use the steps in the [grid walk-through](https://www.infragistics.com/angular-samples/components/grid.html). All help, related API documents and walk-throughs can be found for each control [here](https://www.infragistics.com/angular-samples/components/grid.html). diff --git a/projects/igniteui-angular/src/lib/core/selection.ts b/projects/igniteui-angular/src/lib/core/selection.ts index 1835316806c..41ce9496d79 100644 --- a/projects/igniteui-angular/src/lib/core/selection.ts +++ b/projects/igniteui-angular/src/lib/core/selection.ts @@ -208,7 +208,7 @@ export class IgxSelectionAPIService { * @returns If all items are selected. */ public are_all_selected(componentID: string, dataCount: number): boolean { - return this.size(componentID) === dataCount; + return dataCount > 0 && dataCount === this.size(componentID); } /** diff --git a/projects/igniteui-angular/src/lib/core/styles/components/grid-summary/_grid-summary-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/grid-summary/_grid-summary-theme.scss index 3b77824b955..92d64117bc7 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/grid-summary/_grid-summary-theme.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/grid-summary/_grid-summary-theme.scss @@ -104,14 +104,9 @@ display: flex; flex-direction: column; flex: 1 1 0%; - border-left: 1px solid --var($theme, 'border-color'); padding: map-get($summary-padding, 'comfortable'); background: --var($theme, 'background-color'); overflow: hidden; - - &:first-of-type { - border-left: 0; - } } %igx-grid-summary--cosy { @@ -135,10 +130,6 @@ border-right: map-get($cell-pin, 'style') map-get($cell-pin, 'color'); } - %igx-grid-summary--empty { - border-left: 1px solid --var($theme, 'border-color'); - } - %igx-grid-summary__item { display: flex; align-items: center; diff --git a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss index 04a78dc229b..38b3ed23736 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss @@ -86,6 +86,15 @@ } } + @include e(summaries, $m: 'body') { + @extend %grid-summaries !optional; + @extend %grid-summaries--body !optional; + + igx-display-container { + @extend %grid-display-container-tr !optional; + } + } + @include e(summaries-patch) { @extend %grid-summaries-patch !optional; } diff --git a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss index 34618b2ca40..3650444f161 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss @@ -61,6 +61,11 @@ /// @param {Color} $filtering-row-background [null] - The background color of the filtering row. /// @param {Color} $filtering-row-text-color [null] - The text-color color of the filtering row. /// +/// @param {Color} $body-summaries-background [null] - The background color of the summary groups located the body. +/// @param {Color} $body-summaries-text-color [null] - The text color of the summary results located the body. +/// @param {Color} $root-summaries-background [null] - The background color of the summary groups located the footer. +/// @param {Color} $root-summaries-text-color [null] - The text color of the summary results located the footer. +/// /// @requires $default-palette /// @requires $light-schema /// @requires apply-palette @@ -139,7 +144,12 @@ $filtering-header-text-color: null, $filtering-row-background: null, $filtering-row-text-color: null, - $tree-filtered-text-color: null + $tree-filtered-text-color: null, + + $body-summaries-background: null, + $body-summaries-text-color: null, + $root-summaries-background: null, + $root-summaries-text-color: null ) { $name: 'igx-grid'; $theme: apply-palette(map-get($schema, $name), $palette); @@ -283,6 +293,14 @@ $filtering-row-text-color: text-contrast(hexrgba($filtering-row-background)); } + @if not($body-summaries-text-color) and $body-summaries-background { + $body-summaries-text-color: text-contrast($body-summaries-background); + } + + @if not($root-summaries-text-color) and $root-summaries-background { + $root-summaries-text-color: text-contrast($root-summaries-background); + } + @return extend($theme, ( name: $name, palette: $palette, @@ -358,7 +376,12 @@ tree-filtered-text-color: $tree-filtered-text-color, tree-selected-filtered-row-text-color: $tree-selected-filtered-row-text-color, - tree-selected-filtered-cell-text-color: $tree-selected-filtered-cell-text-color + tree-selected-filtered-cell-text-color: $tree-selected-filtered-cell-text-color, + + body-summaries-background: $body-summaries-background, + body-summaries-text-color: $body-summaries-text-color, + root-summaries-background: $root-summaries-background, + root-summaries-text-color: $root-summaries-text-color )); } @@ -614,7 +637,7 @@ } %grid-scroll-start { - background: igx-color($palette, 'grays', 200); + background: --var($theme, 'header-background'); } %grid-scroll-main { @@ -1023,13 +1046,33 @@ %grid-summaries { display: flex; - background: inherit; + background: --var($theme, 'root-summaries-background'); + + // %igx-grid-summary__label, + %igx-grid-summary__result { + color: --var($theme, 'root-summaries-text-color'); + } + } + + %grid-summaries--body { + background: --var($theme, 'body-summaries-background'); + border-bottom: 1px dashed --var($theme, 'row-border-color'); + + &:last-of-type { + border-bottom: none; + } + + // %igx-grid-summary__label, + %igx-grid-summary__result { + color: --var($theme, 'body-summaries-text-color'); + } } %grid-summaries-patch { background: inherit; position: relative; z-index: 1; + border-right: 1px solid --var($theme, 'header-border-color'); } // Column moving diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_grid.scss b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_grid.scss index 8f09cb6ad82..885aab7aa8b 100644 --- a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_grid.scss +++ b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/dark/_grid.scss @@ -22,6 +22,10 @@ /// @prop {Map} group-row-selected-background [igx-color: ('grays', 200), hexrgba: #222] - The drop area background on drop color. /// @prop {Color} filtering-header-background [#222] - The background color of the filtered column header. /// @prop {Color} filtering-row-background [#222] - The background color of the filtering row. +/// @prop {Map} body-summaries-background [igx-color: ('grays', 300), hexrgba: #222] - The background color of the summary groups located the body. +/// @prop {Map} body-summaries-text-color [igx-color: ('grays', 300), hexrgba: #222, text-contrast: ()] - The text color of the summary groups located the body. +/// @prop {Map} root-summaries-background [igx-color: ('grays', 100), hexrgba: #222] - The background color of the summary groups located the footer. +/// @prop {Map} root-summaries-text-color [igx-color: ('grays', 100), hexrgba: #222, text-contrast: ()] - The text color of the summary groups located the footer. /// @requires extend /// @requires $_light-grid /// @see $default-palette @@ -75,5 +79,27 @@ $_dark-grid: extend($_light-grid, ( filtering-row-background: #222, - cell-selected-text-color: #fff + cell-selected-text-color: #fff, + + body-summaries-background: ( + igx-color: ('grays', 100), + hexrgba: #222 + ), + + body-summaries-text-color: ( + igx-color: ('grays', 100), + hexrgba: #222, + text-contrast: () + ), + + root-summaries-background: ( + igx-color: ('grays', 300), + hexrgba: #222 + ), + + root-summaries-text-color: ( + igx-color: ('grays', 300), + hexrgba: #222, + text-contrast: () + ) )); diff --git a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_grid.scss b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_grid.scss index 507796d315b..bd82267be24 100644 --- a/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_grid.scss +++ b/projects/igniteui-angular/src/lib/core/styles/themes/schemas/light/_grid.scss @@ -54,6 +54,10 @@ /// @prop {Map} filtering-header-text-color [igx-color: ('grays', 800)] - The text color color of the filtered column header. /// @prop {Color} filtering-row-background [#fff] - The background color of the filtering row. /// @prop {Map} filtering-row-text-color [igx-color: ('grays', 800)] - The text-color color of the filtering row. +/// @prop {Map} body-summaries-background [igx-color: ('grays', 300), hexrgba: #fff] - The background color of the summary groups located the body. +/// @prop {Map} body-summaries-text-color [igx-color: ('grays', 300), hexrgba: #fff, text-contrast: ()] - The text color of the summary groups located the body. +/// @prop {Map} root-summaries-background [igx-color: ('grays', 100), hexrgba: #fff] - The background color of the summary groups located the footer. +/// @prop {Map} root-summaries-text-color [igx--color: ('grays', 100), hexrgba: #fff, text-contrast: ()] - The text color of the summary groups located the footer. /// @see $default-palette $_light-grid: ( header-background: ( @@ -239,5 +243,27 @@ $_light-grid: ( tree-filtered-text-color: ( igx-color: ('grays', 500) + ), + + body-summaries-background: ( + igx-color: ('grays', 100), + hexrgba: #fff + ), + + body-summaries-text-color: ( + igx-color: ('grays', 100), + hexrgba: #fff, + text-contrast: () + ), + + root-summaries-background: ( + igx-color: ('grays', 300), + hexrgba: #fff + ), + + root-summaries-text-color: ( + igx-color: ('grays', 300), + hexrgba: #fff, + text-contrast: () ) ); diff --git a/projects/igniteui-angular/src/lib/core/utils.ts b/projects/igniteui-angular/src/lib/core/utils.ts index c6209cde679..cb22db31cdb 100644 --- a/projects/igniteui-angular/src/lib/core/utils.ts +++ b/projects/igniteui-angular/src/lib/core/utils.ts @@ -24,13 +24,11 @@ export function cloneHierarchicalArray(array: any[], childDataKey: any): any[] { } for (const item of array) { + const clonedItem = cloneValue(item); if (Array.isArray(item[childDataKey])) { - const clonedItem = cloneValue(item); clonedItem[childDataKey] = cloneHierarchicalArray(clonedItem[childDataKey], childDataKey); - result.push(clonedItem); - } else { - result.push(item); } + result.push(clonedItem); } return result; } diff --git a/projects/igniteui-angular/src/lib/data-operations/data-util.spec.ts b/projects/igniteui-angular/src/lib/data-operations/data-util.spec.ts index 02d32089e2b..4e8559f0273 100644 --- a/projects/igniteui-angular/src/lib/data-operations/data-util.spec.ts +++ b/projects/igniteui-angular/src/lib/data-operations/data-util.spec.ts @@ -14,11 +14,15 @@ import { FilteringStrategy } from './filtering-strategy'; import { IFilteringExpressionsTree, FilteringExpressionsTree } from './filtering-expressions-tree'; import { IFilteringState } from './filtering-state.interface'; import { FilteringLogic } from './filtering-expression.interface'; -import { IgxNumberFilteringOperand, +import { + IgxNumberFilteringOperand, IgxStringFilteringOperand, IgxDateFilteringOperand, - IgxBooleanFilteringOperand } from './filtering-condition'; + IgxBooleanFilteringOperand +} from './filtering-condition'; import { IPagingState, PagingError } from './paging-state.interface'; +import { SampleTestData } from '../test-utils/sample-test-data.spec'; +import { Transaction, TransactionType, HierarchicalTransaction } from '../services'; /* Test sorting */ function testSort() { @@ -266,6 +270,7 @@ function testGroupBy() { }); } /* //Test sorting */ + /* Test filtering */ class CustomFilteringStrategy extends FilteringStrategy { public filter(data: T[], expressionsTree: IFilteringExpressionsTree): T[] { @@ -387,6 +392,7 @@ function testFilter() { }); } /* //Test filtering */ + /* Test paging */ function testPage() { const dataGenerator: DataGenerator = new DataGenerator(); @@ -426,9 +432,144 @@ function testPage() { }); } /* //Test paging */ + +/* Test merging */ +function testMerging() { + describe('Test merging', () => { + it('Should merge add transactions correctly', () => { + const data = SampleTestData.personIDNameData(); + const addRow4 = { ID: 4, Name: 'Peter' }; + const addRow5 = { ID: 5, Name: 'Mimi' }; + const addRow6 = { ID: 6, Name: 'Pedro' }; + const transactions: Transaction[] = [ + { id: addRow4.ID, newValue: addRow4, type: TransactionType.ADD }, + { id: addRow5.ID, newValue: addRow5, type: TransactionType.ADD }, + { id: addRow6.ID, newValue: addRow6, type: TransactionType.ADD }, + ]; + + DataUtil.mergeTransactions(data, transactions, 'ID'); + expect(data.length).toBe(6); + expect(data[3]).toBe(addRow4); + expect(data[4]).toBe(addRow5); + expect(data[5]).toBe(addRow6); + }); + + it('Should merge update transactions correctly', () => { + const data = SampleTestData.personIDNameData(); + const transactions: Transaction[] = [ + { id: 1, newValue: { Name: 'Peter' }, type: TransactionType.UPDATE }, + { id: 3, newValue: { Name: 'Mimi' }, type: TransactionType.UPDATE }, + ]; + + DataUtil.mergeTransactions(data, transactions, 'ID'); + expect(data.length).toBe(3); + expect(data[0].Name).toBe('Peter'); + expect(data[2].Name).toBe('Mimi'); + }); + + it('Should merge delete transactions correctly', () => { + const data = SampleTestData.personIDNameData(); + const secondRow = data[1]; + const transactions: Transaction[] = [ + { id: 1, newValue: null, type: TransactionType.DELETE }, + { id: 3, newValue: null, type: TransactionType.DELETE }, + ]; + + DataUtil.mergeTransactions(data, transactions, 'ID', true); + expect(data.length).toBe(1); + expect(data[0]).toEqual(secondRow); + }); + + it('Should merge add hierarchical transactions correctly', () => { + const data = SampleTestData.employeeSmallTreeData(); + const addRootRow = { ID: 1000, Name: 'Pit Peter', HireDate: new Date(2008, 3, 20), Age: 55 }; + const addChildRow1 = { ID: 1001, Name: 'Marry May', HireDate: new Date(2018, 4, 1), Age: 102 }; + const addChildRow2 = { ID: 1002, Name: 'April Alison', HireDate: new Date(2021, 5, 10), Age: 4 }; + const transactions: HierarchicalTransaction[] = [ + { id: addRootRow.ID, newValue: addRootRow, type: TransactionType.ADD, path: [] }, + { id: addChildRow1.ID, newValue: addChildRow1, type: TransactionType.ADD, path: [data[0].ID, data[0].Employees[1].ID] }, + { id: addChildRow2.ID, newValue: addChildRow2, type: TransactionType.ADD, path: [addRootRow.ID] }, + ]; + + DataUtil.mergeHierarchicalTransactions(data, transactions, 'Employees', 'ID', false); + expect(data.length).toBe(4); + + expect(data[3].Age).toBe(addRootRow.Age); + expect(data[3].Employees.length).toBe(1); + expect(data[3].HireDate).toBe(addRootRow.HireDate); + expect(data[3].ID).toBe(addRootRow.ID); + expect(data[3].Name).toBe(addRootRow.Name); + + expect((data[0].Employees[1] as any).Employees.length).toBe(1); + expect((data[0].Employees[1] as any).Employees[0]).toBe(addChildRow1); + + expect(data[3].Employees[0]).toBe(addChildRow2); + }); + + it('Should merge update hierarchical transactions correctly', () => { + const data = SampleTestData.employeeSmallTreeData(); + const updateRootRow = { Name: 'May Peter', Age: 13 }; + const updateChildRow1 = { HireDate: new Date(2100, 1, 12), Age: 1300 }; + const updateChildRow2 = { HireDate: new Date(2100, 1, 12), Name: 'Santa Claus' }; + + const transactions: HierarchicalTransaction[] = [ + { + id: data[1].ID, + newValue: updateRootRow, + type: TransactionType.UPDATE, + path: [] + }, + { + id: data[2].Employees[0].ID, + newValue: updateChildRow1, + type: TransactionType.UPDATE, + path: [data[2].ID] + }, + { + id: (data[0].Employees[2] as any).Employees[0].ID, + newValue: updateChildRow2, + type: TransactionType.UPDATE, + path: [data[0].ID, data[0].Employees[2].ID] + }, + ]; + + DataUtil.mergeHierarchicalTransactions(data, transactions, 'Employees', 'ID', false); + expect(data[1].Name).toBe(updateRootRow.Name); + expect(data[1].Age).toBe(updateRootRow.Age); + + expect(data[2].Employees[0].HireDate.getTime()).toBe(updateChildRow1.HireDate.getTime()); + expect(data[2].Employees[0].Age).toBe(updateChildRow1.Age); + + expect((data[0].Employees[2] as any).Employees[0].Name).toBe(updateChildRow2.Name); + expect((data[0].Employees[2] as any).Employees[0].HireDate.getTime()).toBe(updateChildRow2.HireDate.getTime()); + }); + + it('Should merge delete hierarchical transactions correctly', () => { + const data = SampleTestData.employeeSmallTreeData(); + const transactions: HierarchicalTransaction[] = [ + // root row with no children + { id: data[1].ID, newValue: null, type: TransactionType.DELETE, path: [] }, + // root row with children + { id: data[2].ID, newValue: null, type: TransactionType.DELETE, path: [] }, + // child row with no children + { id: data[0].Employees[0].ID, newValue: null, type: TransactionType.DELETE, path: [data[0].ID] }, + // child row with children + { id: data[0].Employees[2].ID, newValue: null, type: TransactionType.DELETE, path: [data[0].ID] } + ]; + + DataUtil.mergeHierarchicalTransactions(data, transactions, 'Employees', 'ID', true); + + expect(data.length).toBe(1); + expect(data[0].Employees.length).toBe(1); + }); + }); +} +/* //Test merging */ + describe('DataUtil', () => { testSort(); testGroupBy(); testFilter(); testPage(); + testMerging(); }); diff --git a/projects/igniteui-angular/src/lib/data-operations/data-util.ts b/projects/igniteui-angular/src/lib/data-operations/data-util.ts index 8d987bb2aac..05659c75cee 100644 --- a/projects/igniteui-angular/src/lib/data-operations/data-util.ts +++ b/projects/igniteui-angular/src/lib/data-operations/data-util.ts @@ -29,13 +29,13 @@ export enum DataType { * @hidden */ export class DataUtil { - public static sort(data: T[], expressions: ISortingExpression [], sorting: IgxSorting = new IgxSorting()): T[] { + public static sort(data: T[], expressions: ISortingExpression[], sorting: IgxSorting = new IgxSorting()): T[] { return sorting.sort(data, expressions); } public static treeGridSort(hierarchicalData: ITreeGridRecord[], - expressions: ISortingExpression [], - parent?: ITreeGridRecord): ITreeGridRecord[] { + expressions: ISortingExpression[], + parent?: ITreeGridRecord): ITreeGridRecord[] { let res: ITreeGridRecord[] = []; hierarchicalData.forEach((hr: ITreeGridRecord) => { const rec: ITreeGridRecord = DataUtil.cloneTreeGridRecord(hr); @@ -58,8 +58,7 @@ export class DataUtil { children: hierarchicalRecord.children, isFilteredOutParent: hierarchicalRecord.isFilteredOutParent, level: hierarchicalRecord.level, - expanded: hierarchicalRecord.expanded, - path: [...hierarchicalRecord.path] + expanded: hierarchicalRecord.expanded }; return rec; } @@ -189,60 +188,100 @@ export class DataUtil { * @param data Collection to merge * @param transactions Transactions to merge into data * @param primaryKey Primary key of the collection, if any + * @param deleteRows Should delete rows with DELETE transaction type from data + * @returns Provided data collections updated with all provided transactions */ - public static mergeTransactions(data: T[], transactions: Transaction[], primaryKey?: any): T[] { + public static mergeTransactions(data: T[], transactions: Transaction[], primaryKey?: any, deleteRows: boolean = false): T[] { data.forEach((item: any, index: number) => { const rowId = primaryKey ? item[primaryKey] : item; const transaction = transactions.find(t => t.id === rowId); - if (Array.isArray(item.children)) { - this.mergeTransactions(item.children, transactions, primaryKey); - } if (transaction && transaction.type === TransactionType.UPDATE) { data[index] = transaction.newValue; } }); + if (deleteRows) { + transactions + .filter(t => t.type === TransactionType.DELETE) + .forEach(t => { + const index = primaryKey ? data.findIndex(d => d[primaryKey] === t.id) : data.findIndex(d => d === t.id); + if (0 <= index && index < data.length) { + data.splice(index, 1); + } + }); + } + data.push(...transactions .filter(t => t.type === TransactionType.ADD) .map(t => t.newValue)); + return data; } - // TODO: optimize addition of added rows. Should not filter transaction in each recursion!!! - /** @experimental @hidden */ + /** + * Merges all changes from provided transactions into provided hierarchical data collection + * @param data Collection to merge + * @param transactions Transactions to merge into data + * @param childDataKey Data key of child collections + * @param primaryKey Primary key of the collection, if any + * @param deleteRows Should delete rows with DELETE transaction type from data + * @returns Provided data collections updated with all provided transactions + */ public static mergeHierarchicalTransactions( data: any[], transactions: HierarchicalTransaction[], childDataKey: any, primaryKey?: any, - parentKey?: any): any[] { - - for (let index = 0; index < data.length; index++) { - const dataItem = data[index]; - const rowId = primaryKey ? dataItem[primaryKey] : dataItem; - const updateTransaction = transactions.filter(t => t.type === TransactionType.UPDATE).find(t => t.id === rowId); - const addedTransactions = transactions.filter(t => t.type === TransactionType.ADD).filter(t => t.parentId === rowId); - if (updateTransaction || addedTransactions.length > 0) { - data[index] = mergeObjects(cloneValue(dataItem), updateTransaction && updateTransaction.newValue); - } - if (addedTransactions.length > 0) { - if (!data[index][childDataKey]) { - data[index][childDataKey] = []; - } - for (const addedTransaction of addedTransactions) { - data[index][childDataKey].push(addedTransaction.newValue); + deleteRows: boolean = false): any[] { + + for (const transaction of transactions) { + if (transaction.path) { + const parent = this.findParentFromPath(data, primaryKey, childDataKey, transaction.path); + let collection: any[] = parent ? parent[childDataKey] : data; + switch (transaction.type) { + case TransactionType.ADD: + // if there is no parent this is ADD row at root level + if (parent && !parent[childDataKey]) { + parent[childDataKey] = collection = []; + } + collection.push(transaction.newValue); + break; + case TransactionType.UPDATE: + const updateIndex = collection.findIndex(x => x[primaryKey] === transaction.id); + if (updateIndex !== -1) { + collection[updateIndex] = mergeObjects(cloneValue(collection[updateIndex]), transaction.newValue); + } + break; + case TransactionType.DELETE: + if (deleteRows) { + const deleteIndex = collection.findIndex(r => r[primaryKey] === transaction.id); + if (deleteIndex !== -1) { + collection.splice(deleteIndex, 1); + } + } + break; } - } - if (data[index][childDataKey]) { - data[index][childDataKey] = this.mergeHierarchicalTransactions( - data[index][childDataKey], - transactions, - childDataKey, - primaryKey, - rowId - ); + } else { + // if there is no path this is ADD row in root. Push the newValue to data + data.push(transaction.newValue); } } return data; } + + private static findParentFromPath(data: any[], primaryKey: any, childDataKey: any, path: any[]): any { + let collection: any[] = data; + let result: any; + + for (const id of path) { + result = collection && collection.find(x => x[primaryKey] === id); + if (!result) { + break; + } + + collection = result[childDataKey]; + } + + return result; + } } diff --git a/projects/igniteui-angular/src/lib/directives/mask/README.md b/projects/igniteui-angular/src/lib/directives/mask/README.md index aced6b6745c..bae586552b1 100644 --- a/projects/igniteui-angular/src/lib/directives/mask/README.md +++ b/projects/igniteui-angular/src/lib/directives/mask/README.md @@ -50,6 +50,29 @@ handleValueChange(event) { ``` +Use the `placeholder` input property to specify the placeholder attribute of the host input element that the `igxMask` is applied on. +```typescript +placeholder = 'hello'; +``` +```html + +``` + +Use the `focusedValuePipe` and `displayValuePipe` input properties to additionally transform the value on focus and blur. +```typescript +@Pipe({ name: "displayFormat" }) +export class DisplayFormatPipe implements PipeTransform { + transform(value: any): string { + return value.toLowerCase(); + } +} + +displayFormat = new DisplayFormatPipe(); +``` +```html + +``` + ### API ### Inputs @@ -58,6 +81,9 @@ handleValueChange(event) { | `mask`| `String` | Represents the current mask. | | `promptChar`| `String` | Character representing a fillable spot in the mask. | | `includeLiterals`| `Boolean` | Include or exclude literals in the raw value. | +| `placeholder`| `string` | Specifies a short hint that describes the expected value. | +| `displayValuePipe`| `PipeTransform` | A pipe to transform the input value on blur. | +| `focusedValuePipe`| `PipeTransform` | A pipe to transform the input value on focus. | ### Outputs | Name | Return Type | Description | diff --git a/projects/igniteui-angular/src/lib/directives/mask/mask-helper.ts b/projects/igniteui-angular/src/lib/directives/mask/mask-helper.ts index 8462ca73013..babb30b81c2 100644 --- a/projects/igniteui-angular/src/lib/directives/mask/mask-helper.ts +++ b/projects/igniteui-angular/src/lib/directives/mask/mask-helper.ts @@ -87,6 +87,22 @@ export class MaskHelper { return inputValue; } + public parseMask(maskOptions): string { + let outputVal = ''; + const mask: string = maskOptions.format; + const literals: Map = this.getMaskLiterals(mask); + + for (const maskSym of mask) { + outputVal += maskOptions.promptChar; + } + + literals.forEach((val: string, key: number) => { + outputVal = this.replaceCharAt(outputVal, key, val); + }); + + return outputVal; + } + public parseValueByMaskOnInit(inputVal, maskOptions): string { let outputVal = ''; let value = ''; diff --git a/projects/igniteui-angular/src/lib/directives/mask/mask.directive.spec.ts b/projects/igniteui-angular/src/lib/directives/mask/mask.directive.spec.ts index 32958009981..245deeb6128 100644 --- a/projects/igniteui-angular/src/lib/directives/mask/mask.directive.spec.ts +++ b/projects/igniteui-angular/src/lib/directives/mask/mask.directive.spec.ts @@ -1,4 +1,4 @@ -import { Component, Input, ViewChild } from '@angular/core'; +import { Component, Input, ViewChild, OnInit, ElementRef, Pipe, PipeTransform } from '@angular/core'; import { async, fakeAsync, @@ -6,7 +6,6 @@ import { tick } from '@angular/core/testing'; import { FormsModule } from '@angular/forms'; -import { By } from '@angular/platform-browser'; import { IgxInputGroupModule } from '../../input-group/input-group.component'; import { IgxMaskModule } from './mask.directive'; @@ -26,7 +25,9 @@ describe('igxMask', () => { IncludeLiteralsComponent, LetterSpaceMaskComponent, MaskComponent, - OneWayBindComponent + OneWayBindComponent, + PipesMaskComponent, + PlaceholderMaskComponent ], imports: [ FormsModule, @@ -41,21 +42,21 @@ describe('igxMask', () => { const fixture = TestBed.createComponent(DefMaskComponent); fixture.detectChanges(); - const input = fixture.debugElement.query(By.css('input')); + const input = fixture.componentInstance.input; - expect(input.nativeElement.value).toEqual('__________'); + expect(input.nativeElement.value).toEqual(''); + expect(input.nativeElement.getAttribute('placeholder')).toEqual('CCCCCCCCCC'); - input.triggerEventHandler('click', {}); + input.nativeElement.dispatchEvent(new Event('click')); tick(); input.nativeElement.value = '@#$YUA123'; input.nativeElement.dispatchEvent(new Event('input')); tick(); - input.triggerEventHandler('focus', {}); + input.nativeElement.dispatchEvent(new Event('focus')); tick(); - fixture.detectChanges(); expect(input.nativeElement.value).toEqual('@#$YUA123_'); })); @@ -63,9 +64,9 @@ describe('igxMask', () => { const fixture = TestBed.createComponent(DigitSpaceMaskComponent); fixture.detectChanges(); - const input = fixture.debugElement.query(By.css('input')); + const input = fixture.componentInstance.input; - input.triggerEventHandler('focus', {}); + input.nativeElement.dispatchEvent(new Event('focus')); tick(); expect(input.nativeElement.value).toEqual('555 55'); @@ -76,9 +77,9 @@ describe('igxMask', () => { const fixture = TestBed.createComponent(DigitPlusMinusMaskComponent); fixture.detectChanges(); - const input = fixture.debugElement.query(By.css('input')); + const input = fixture.componentInstance.input; - input.triggerEventHandler('focus', {}); + input.nativeElement.dispatchEvent(new Event('focus')); tick(); expect(input.nativeElement.value).toEqual('+359-884 19 08 54'); @@ -88,9 +89,9 @@ describe('igxMask', () => { const fixture = TestBed.createComponent(LetterSpaceMaskComponent); fixture.detectChanges(); - const input = fixture.debugElement.query(By.css('input')); + const input = fixture.componentInstance.input; - input.triggerEventHandler('focus', {}); + input.nativeElement.dispatchEvent(new Event('focus')); tick(); expect(input.nativeElement.value).toEqual('AB _CD E'); @@ -100,9 +101,9 @@ describe('igxMask', () => { const fixture = TestBed.createComponent(AlphanumSpaceMaskComponent); fixture.detectChanges(); - const input = fixture.debugElement.query(By.css('input')); + const input = fixture.componentInstance.input; - input.triggerEventHandler('focus', {}); + input.nativeElement.dispatchEvent(new Event('focus')); tick(); expect(input.nativeElement.value).toEqual('7c_ 8u'); @@ -112,9 +113,9 @@ describe('igxMask', () => { const fixture = TestBed.createComponent(AnyCharMaskComponent); fixture.detectChanges(); - const input = fixture.debugElement.query(By.css('input')); + const input = fixture.componentInstance.input; - input.triggerEventHandler('focus', {}); + input.nativeElement.dispatchEvent(new Event('focus')); tick(); expect(input.nativeElement.value).toEqual('_=%. p]'); @@ -125,58 +126,52 @@ describe('igxMask', () => { fixture.detectChanges(); const comp = fixture.componentInstance; - const input = fixture.debugElement.query(By.css('input')); + const input = comp.input; - input.triggerEventHandler('focus', {}); + input.nativeElement.dispatchEvent(new Event('input')); tick(); - fixture.detectChanges(); expect(input.nativeElement.value).toEqual('(123) 4567-890'); expect(comp.value).toEqual('1234567890'); - input.nativeElement.value = '7777'; - input.nativeElement.dispatchEvent(new Event('input')); - tick(); + comp.value = '7777'; + fixture.detectChanges(); - input.triggerEventHandler('focus', {}); + input.nativeElement.dispatchEvent(new Event('input')); tick(); - fixture.detectChanges(); expect(input.nativeElement.value).toEqual('(777) 7___-___'); expect(comp.value).toEqual('7777'); - })); it('Enter incorrect value with a preset mask', fakeAsync(() => { const fixture = TestBed.createComponent(MaskComponent); fixture.detectChanges(); - const input = fixture.debugElement.query(By.css('input[type=text]')); + const input = fixture.componentInstance.input; - input.triggerEventHandler('focus', {}); + input.nativeElement.dispatchEvent(new Event('focus')); tick(); input.nativeElement.value = 'abc4569d12'; input.nativeElement.dispatchEvent(new Event('input')); tick(); - input.triggerEventHandler('focus', {}); + input.nativeElement.dispatchEvent(new Event('focus')); tick(); - fixture.detectChanges(); expect(input.nativeElement.value).toEqual('(___) 4569-_12'); - input.triggerEventHandler('focus', {}); + input.nativeElement.dispatchEvent(new Event('focus')); tick(); input.nativeElement.value = '1111111111111111111'; input.nativeElement.dispatchEvent(new Event('input')); tick(); - input.triggerEventHandler('focus', {}); + input.nativeElement.dispatchEvent(new Event('focus')); tick(); - fixture.detectChanges(); expect(input.nativeElement.value).toEqual('(111) 1111-111'); })); @@ -184,34 +179,31 @@ describe('igxMask', () => { const fixture = TestBed.createComponent(IncludeLiteralsComponent); fixture.detectChanges(); - const comp = fixture.componentInstance; - const inputs = fixture.debugElement.queryAll(By.css('input')); + const input = fixture.componentInstance.input; - inputs[0].triggerEventHandler('focus', {}); + input.nativeElement.dispatchEvent(new Event('focus')); tick(); - expect(inputs[0].nativeElement.value).toEqual('(555) 55__-___'); + expect(input.nativeElement.value).toEqual('(555) 55__-___'); })); it('Correct event firing', fakeAsync(() => { const fixture = TestBed.createComponent(EventFiringComponent); fixture.detectChanges(); - const input = fixture.debugElement.query(By.css('input[type=text]')); + const input = fixture.componentInstance.input; - input.triggerEventHandler('focus', {}); + input.nativeElement.dispatchEvent(new Event('focus')); tick(); input.nativeElement.value = '123'; input.nativeElement.dispatchEvent(new Event('input')); tick(); - input.triggerEventHandler('focus', {}); + input.nativeElement.dispatchEvent(new Event('focus')); tick(); - fixture.detectChanges(); expect(input.nativeElement.value).toEqual('(123) ____-___'); - expect(fixture.componentInstance.raw).toEqual('123'); })); @@ -220,15 +212,13 @@ describe('igxMask', () => { fixture.detectChanges(); const comp = fixture.componentInstance; - const input = fixture.debugElement.query(By.css('input')); + const input = comp.input; expect(input.nativeElement.value).toEqual('3456'); - input.triggerEventHandler('focus', null); + input.nativeElement.dispatchEvent(new Event('focus')); tick(); - fixture.detectChanges(); - expect(input.nativeElement.value).toEqual('3456****'); expect(comp.value).toEqual(3456); @@ -236,7 +226,7 @@ describe('igxMask', () => { input.nativeElement.dispatchEvent(new Event('input')); tick(); - input.triggerEventHandler('focus', null); + input.nativeElement.dispatchEvent(new Event('focus')); tick(); expect(input.nativeElement.value).toEqual('A*******'); @@ -246,7 +236,7 @@ describe('igxMask', () => { const fixture = TestBed.createComponent(MaskComponent); fixture.detectChanges(); - const input = fixture.debugElement.query(By.css('input')); + const input = fixture.componentInstance.input; input.nativeElement.focus(); tick(); @@ -258,8 +248,6 @@ describe('igxMask', () => { input.nativeElement.dispatchEvent(keyEvent); tick(); - fixture.detectChanges(); - input.nativeElement.value = ''; input.nativeElement.dispatchEvent(new Event('input')); tick(); @@ -267,17 +255,14 @@ describe('igxMask', () => { input.nativeElement.dispatchEvent(new Event('focus')); tick(); - fixture.detectChanges(); - expect(input.nativeElement.value).toEqual('(___) ____-___'); - })); it('Enter value over literal', fakeAsync(() => { const fixture = TestBed.createComponent(MaskComponent); fixture.detectChanges(); - const input = fixture.debugElement.query(By.css('input')); + const input = fixture.componentInstance.input; input.nativeElement.focus(); tick(); @@ -289,8 +274,6 @@ describe('igxMask', () => { input.nativeElement.dispatchEvent(keyEvent); tick(); - fixture.detectChanges(); - input.nativeElement.value = ''; input.nativeElement.dispatchEvent(new Event('input')); tick(); @@ -298,91 +281,159 @@ describe('igxMask', () => { input.nativeElement.dispatchEvent(new Event('focus')); tick(); - fixture.detectChanges(); - expect(input.nativeElement.value).toEqual('(___) ____-___'); input.nativeElement.value = '6666'; input.nativeElement.dispatchEvent(new Event('input')); tick(); - input.triggerEventHandler('focus', {}); + input.nativeElement.dispatchEvent(new Event('focus')); tick(); - fixture.detectChanges(); expect(input.nativeElement.value).toEqual('(666) 6___-___'); })); + + it('Apply display and input pipes on blur and focus.', fakeAsync(() => { + const fixture = TestBed.createComponent(PipesMaskComponent); + fixture.detectChanges(); + + const input = fixture.componentInstance.input; + + input.nativeElement.focus(); + tick(); + + expect(input.nativeElement.value).toEqual('SSS'); + + input.nativeElement.dispatchEvent(new Event('blur')); + tick(); + + expect(input.nativeElement.value).toEqual('sss'); + + input.nativeElement.dispatchEvent(new Event('focus')); + tick(); + + expect(input.nativeElement.value).toEqual('SSS'); + })); + + it('Apply placehodler when value is not defined.', fakeAsync(() => { + const fixture = TestBed.createComponent(PlaceholderMaskComponent); + fixture.detectChanges(); + + const input = fixture.componentInstance.input; + + expect(input.nativeElement.value).toEqual(''); + expect(input.nativeElement.placeholder).toEqual('hello'); + + input.nativeElement.dispatchEvent(new Event('focus')); + tick(); + + expect(input.nativeElement.value).toEqual('(__) (__)'); + expect(input.nativeElement.placeholder).toEqual('hello'); + + input.nativeElement.dispatchEvent(new Event('blur')); + tick(); + + expect(input.nativeElement.value).toEqual(''); + expect(input.nativeElement.placeholder).toEqual('hello'); + })); }); @Component({ template: ` - + ` }) class DefMaskComponent { public mask; public value; + + @ViewChild('input') + public input: ElementRef; } @Component({ template: ` - + ` }) class MaskComponent { - public mask = '(000) 0000-000'; - public value = '1234567890'; + mask = '(000) 0000-000'; + value = '1234567890'; + + @ViewChild('input') + public input: ElementRef; } @Component({ template: ` - + - + ` }) class IncludeLiteralsComponent { public mask = '(000) 0000-000'; @Input() public value = '55555'; + + @ViewChild('input') + public input: ElementRef; + + @ViewChild('input1') + public input1: ElementRef; } @Component({ template: ` - + ` }) class DigitSpaceMaskComponent { public mask = '999999'; public value = '555 555'; + + @ViewChild('input') + public input: ElementRef; } @Component({ template: ` - + ` }) class DigitPlusMinusMaskComponent { public mask = '####-### ## ## ##'; public value = '+359884190854'; + + @ViewChild('input') + public input: ElementRef; } @Component({ template: ` - + ` }) class LetterSpaceMaskComponent { public mask = 'LL??LL??'; public value = 'AB 2CD E'; + + @ViewChild('input') + public input: ElementRef; } @Component({ template: ` - + ` }) class AlphanumSpaceMaskComponent { public mask = 'AAAaaa'; public value = '7c 8u'; + + @ViewChild('input') + public input: ElementRef; } @Component({ template: ` - + ` }) class AnyCharMaskComponent { public mask = '&&&.CCC'; public value = ' =% p]'; + + @ViewChild('input') + public input: ElementRef; } @Component({ template: ` - ` }) class EventFiringComponent { @@ -391,6 +442,9 @@ class EventFiringComponent { raw: string; formatted: string; + @ViewChild('input') + public input: ElementRef; + handleValueChange(event) { this.raw = event.rawValue; this.formatted = event.formattedValue; @@ -398,9 +452,62 @@ class EventFiringComponent { } @Component({ template: ` - + ` }) class OneWayBindComponent { myMask = 'AAAAAAAA'; value = 3456; + + @ViewChild('input') + public input: ElementRef; +} + +@Component({ template: ` + + ` }) +class PlaceholderMaskComponent { + public mask = '(00) (00)'; + public value = null; + + @ViewChild('input') + public input: ElementRef; +} + +@Component({ template: ` + + ` }) +class PipesMaskComponent { + public mask = 'CCC'; + public value = 'SSS'; + + public displayFormat = new DisplayFormatPipe(); + public inputFormat = new InputFormatPipe(); + + @ViewChild('input') + public input: ElementRef; +} + +@Pipe({ name: 'inputFormat' }) +export class InputFormatPipe implements PipeTransform { + transform(value: any): string { + return value.toUpperCase(); + } +} + +@Pipe({ name: 'displayFormat' }) +export class DisplayFormatPipe implements PipeTransform { + transform(value: any): string { + return value.toLowerCase(); + } } diff --git a/projects/igniteui-angular/src/lib/directives/mask/mask.directive.ts b/projects/igniteui-angular/src/lib/directives/mask/mask.directive.ts index 1c1fa4913ba..d8fd233fc8e 100644 --- a/projects/igniteui-angular/src/lib/directives/mask/mask.directive.ts +++ b/projects/igniteui-angular/src/lib/directives/mask/mask.directive.ts @@ -7,7 +7,8 @@ import { Input, NgModule, OnInit, - Output + Output, + PipeTransform } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { KEYS, MaskHelper } from './mask-helper'; @@ -28,6 +29,7 @@ export class IgxMaskDirective implements OnInit, ControlValueAccessor { */ @Input('igxMask') public mask: string; + /** * Sets the character representing a fillable spot in the input mask. * Default value is "'_'". @@ -38,6 +40,7 @@ export class IgxMaskDirective implements OnInit, ControlValueAccessor { */ @Input() public promptChar: string; + /** * Specifies if the bound value includes the formatting symbols. * ```html @@ -47,11 +50,43 @@ export class IgxMaskDirective implements OnInit, ControlValueAccessor { */ @Input() public includeLiterals: boolean; + + /** + * Specifies a placeholder. + * ```html + * + * ``` + * @memberof IgxMaskDirective + */ + @Input() + public placeholder: string; + + /** + * Specifies a pipe to be used on blur. + * ```html + * + * ``` + * @memberof IgxMaskDirective + */ + @Input() + public displayValuePipe: PipeTransform; + + /** + * Specifies a pipe to be used on focus. + * ```html + * + * ``` + * @memberof IgxMaskDirective + */ + @Input() + public focusedValuePipe: PipeTransform; + /** *@hidden */ @Input() private dataValue: string; + /** * Emits an event each time the value changes. * Provides `rawValue: string` and `formattedValue: string` as event arguments. @@ -61,52 +96,57 @@ export class IgxMaskDirective implements OnInit, ControlValueAccessor { */ @Output() public onValueChange = new EventEmitter(); + /** *@hidden */ private get value() { return this.nativeElement.value; } + /** *@hidden */ private set value(val) { this.nativeElement.value = val; } + /** *@hidden */ private get nativeElement() { return this.elementRef.nativeElement; } + /** *@hidden */ private get selectionStart() { return this.nativeElement.selectionStart; } + /** *@hidden */ private get selectionEnd() { return this.nativeElement.selectionEnd; } + /** *@hidden */ private _ctrlDown: boolean; - /** - *@hidden - */ - private _cachedVal: string; + /** *@hidden */ private _paste: boolean; + /** *@hidden */ private _selection: number; + /** *@hidden */ @@ -114,26 +154,32 @@ export class IgxMaskDirective implements OnInit, ControlValueAccessor { format: '', promptChar: '' }; + /** *@hidden */ private _key; + /** *@hidden */ private _cursorOnPaste; + /** *@hidden */ private _valOnPaste; + /** *@hidden */ private maskHelper: MaskHelper; + /** *@hidden */ private _onTouchedCallback: () => void = noop; + /** *@hidden */ @@ -142,6 +188,7 @@ export class IgxMaskDirective implements OnInit, ControlValueAccessor { constructor(private elementRef: ElementRef) { this.maskHelper = new MaskHelper(); } + /** *@hidden */ @@ -152,8 +199,9 @@ export class IgxMaskDirective implements OnInit, ControlValueAccessor { this._maskOptions.format = this.mask ? this.mask : 'CCCCCCCCCC'; this._maskOptions.promptChar = this.promptChar ? this.promptChar : '_'; - this.nativeElement.setAttribute('placeholder', this.mask); + this.nativeElement.setAttribute('placeholder', this.placeholder ? this.placeholder : this._maskOptions.format); } + /** *@hidden */ @@ -172,6 +220,7 @@ export class IgxMaskDirective implements OnInit, ControlValueAccessor { this._key = key; this._selection = Math.abs(this.selectionEnd - this.selectionStart); } + /** *@hidden */ @@ -183,6 +232,7 @@ export class IgxMaskDirective implements OnInit, ControlValueAccessor { this._ctrlDown = false; } } + /** *@hidden */ @@ -193,6 +243,7 @@ export class IgxMaskDirective implements OnInit, ControlValueAccessor { this._valOnPaste = this.value; this._cursorOnPaste = this.getCursorPosition(); } + /** *@hidden */ @@ -225,25 +276,45 @@ export class IgxMaskDirective implements OnInit, ControlValueAccessor { this.onValueChange.emit({ rawValue: rawVal, formattedValue: this.value }); } + + /** + *@hidden + */ + @HostListener('focus', ['$event.target.value']) + public onFocus(value) { + if (this.focusedValuePipe) { + this.value = this.focusedValuePipe.transform(value); + } else { + this.value = this.maskHelper.parseValueByMaskOnInit(this.value, this._maskOptions); + } + } + /** *@hidden */ - @HostListener('focus', ['$event']) - public onFocus(event) { - this.value = this.maskHelper.parseValueByMaskOnInit(this.value, this._maskOptions); + @HostListener('blur', ['$event.target.value']) + public onBlur(value) { + if (this.displayValuePipe) { + this.value = this.displayValuePipe.transform(value); + } else if (value === this.maskHelper.parseMask(this._maskOptions)) { + this.value = ''; + } } + /** *@hidden */ private getCursorPosition(): number { return this.nativeElement.selectionStart; } + /** *@hidden */ private setCursorPosition(start: number, end: number = start): void { this.nativeElement.setSelectionRange(start, end); } + /** *@hidden */ @@ -252,26 +323,30 @@ export class IgxMaskDirective implements OnInit, ControlValueAccessor { this._maskOptions.promptChar = this.promptChar.substring(0, 1); } - this.value = this.maskHelper.parseValueByMaskOnInit(value, this._maskOptions); + if (value) { + this.value = this.maskHelper.parseValueByMaskOnInit(value, this._maskOptions); + } this.dataValue = this.includeLiterals ? this.value : value; this._onChangeCallback(this.dataValue); this.onValueChange.emit({ rawValue: value, formattedValue: this.value }); } + /** *@hidden */ public registerOnChange(fn: (_: any) => void) { this._onChangeCallback = fn; } + /** *@hidden */ public registerOnTouched(fn: () => void) { this._onTouchedCallback = fn; } } + /** * The IgxMaskModule provides the {@link IgxMaskDirective} inside your application. */ - export interface IMaskEventArgs { rawValue: string; formattedValue: string; diff --git a/projects/igniteui-angular/src/lib/directives/template-outlet/template_outlet.directive.ts b/projects/igniteui-angular/src/lib/directives/template-outlet/template_outlet.directive.ts index a1f4b4b6635..0384de3dfb4 100644 --- a/projects/igniteui-angular/src/lib/directives/template-outlet/template_outlet.directive.ts +++ b/projects/igniteui-angular/src/lib/directives/template-outlet/template_outlet.directive.ts @@ -58,7 +58,7 @@ export class IgxTemplateOutletDirective implements OnChanges { private _recreateView() { // remove and recreate if (this._viewRef) { - this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._viewRef)); + this._viewContainerRef.detach(this._viewContainerRef.indexOf(this._viewRef)); } if (this.igxTemplateOutlet) { this._viewRef = this._viewContainerRef.createEmbeddedView( @@ -70,10 +70,7 @@ export class IgxTemplateOutletDirective implements OnChanges { // Note: Views in detached state do not appear in the DOM, however they remain stored in memory. const res = this._embeddedViewsMap.get(this.igxTemplateOutletContext['templateID']); if (!res) { - let emptyView = this._viewContainerRef.createEmbeddedView( - this.igxTemplateOutlet, {}); - emptyView = this._viewContainerRef.detach(this._viewContainerRef.indexOf(emptyView)) as EmbeddedViewRef; - this._embeddedViewsMap.set(this.igxTemplateOutletContext['templateID'], emptyView); + this._embeddedViewsMap.set(this.igxTemplateOutletContext['templateID'], this._viewRef); } } } diff --git a/projects/igniteui-angular/src/lib/grids/api.service.ts b/projects/igniteui-angular/src/lib/grids/api.service.ts index c0c6fa98fde..19da0f5a345 100644 --- a/projects/igniteui-angular/src/lib/grids/api.service.ts +++ b/projects/igniteui-angular/src/lib/grids/api.service.ts @@ -11,7 +11,6 @@ import { IgxRowComponent } from './row.component'; import { IFilteringOperation } from '../data-operations/filtering-condition'; import { IFilteringExpressionsTree, FilteringExpressionsTree } from '../data-operations/filtering-expressions-tree'; import { Transaction, TransactionType } from '../services/index'; -import { ISortingStrategy } from '../data-operations/sorting-strategy'; /** *@hidden */ @@ -22,7 +21,6 @@ export class GridBaseAPIService { protected state: Map = new Map(); protected editCellState: Map = new Map(); protected editRowState: Map = new Map(); - protected summaryCacheMap: Map> = new Map>(); protected destroyMap: Map> = new Map>(); public register(grid: T) { @@ -40,7 +38,6 @@ export class GridBaseAPIService { public unset(id: string) { this.state.delete(id); - this.summaryCacheMap.delete(id); this.editCellState.delete(id); this.editRowState.delete(id); this.destroyMap.delete(id); @@ -48,7 +45,6 @@ export class GridBaseAPIService { public reset(oldId: string, newId: string) { const destroy = this.destroyMap.get(oldId); - const summary = this.summaryCacheMap.get(oldId); const editCellState = this.editCellState.get(oldId); const editRowState = this.editRowState.get(oldId); const grid = this.get(oldId); @@ -63,10 +59,6 @@ export class GridBaseAPIService { this.destroyMap.set(newId, destroy); } - if (summary) { - this.summaryCacheMap.set(newId, summary); - } - if (editCellState) { this.editCellState.set(newId, editCellState); } @@ -80,11 +72,7 @@ export class GridBaseAPIService { return this.get(id).columnList.find((col) => col.field === name); } - public set_summary_by_column_name(id: string, name: string) { - if (!this.summaryCacheMap.get(id)) { - this.summaryCacheMap.set(id, new Map()); - } - const column = this.get_column_by_name(id, name); + public get_summary_data(id) { const grid = this.get(id); let data = grid.filteredData; if (!data) { @@ -94,28 +82,19 @@ export class GridBaseAPIService { grid.transactions.getAggregatedChanges(true), grid.primaryKey ); + const deletedRows = grid.transactions.getTransactionLog().filter(t => t.type === TransactionType.DELETE).map(t => t.id); + deletedRows.forEach(rowID => { + const tempData = grid.primaryKey ? data.map(rec => rec[grid.primaryKey]) : data; + const index = tempData.indexOf(rowID); + if (index !== -1) { + data.splice(index, 1); + } + }); } else { data = grid.data; } } - if (data) { - const columnValues = data.map((rec) => rec[column.field]); - this.calculateSummaries(id, column, columnValues); - } - } - - public get_summaries(id: string) { - return this.summaryCacheMap.get(id); - } - - public remove_summary(id: string, name?: string) { - if (this.summaryCacheMap.has(id)) { - if (!name) { - this.summaryCacheMap.delete(id); - } else { - this.summaryCacheMap.get(id).delete(name); - } - } + return data; } public set_cell_inEditMode(gridId: string, cell: IgxGridCellComponent) { @@ -187,7 +166,7 @@ export class GridBaseAPIService { if (!grid) { return -1; } - const data = this.get_all_data(id); + const data = this.get_all_data(id, grid.transactions.enabled); return grid.primaryKey ? data.findIndex(record => record[grid.primaryKey] === rowID) : data.indexOf(rowID); } @@ -276,7 +255,7 @@ export class GridBaseAPIService { rowData: any } { const grid = this.get(id); - const data = this.get_all_data(id); + const data = this.get_all_data(id, grid.transactions.enabled); const isRowSelected = grid.selection.is_item_selected(id, rowID); const editableCell = this.get_cell_inEditMode(id); const column = grid.columnList.toArray()[columnID]; @@ -313,9 +292,9 @@ export class GridBaseAPIService { } const args = { rowID, - oldValue: oldValue, - newValue: editValue, - cancel: false + oldValue: oldValue, + newValue: editValue, + cancel: false }; if (cellObj) { Object.assign(args, { @@ -337,7 +316,7 @@ export class GridBaseAPIService { rowData: any }): void { const grid = this.get(id); - const data = this.get_all_data(id); + // const data = this.get_all_data(id, grid.transactions.enabled); const currentGridEditState = gridEditState || this.create_grid_edit_args(id, rowID, columnID, editValue); const emittedArgs = currentGridEditState.args; const column = grid.columnList.toArray()[columnID]; @@ -357,32 +336,52 @@ export class GridBaseAPIService { // if edit (new) value is same as old value do nothing here if (emittedArgs.oldValue !== undefined && isEqual(emittedArgs.oldValue, emittedArgs.newValue)) { return; } - const transaction: Transaction = { - id: rowID, type: TransactionType.UPDATE, newValue: { [column.field]: emittedArgs.newValue } - }; - if (grid.transactions.enabled) { - grid.transactions.add(transaction, currentGridEditState.rowData); - } else { - const rowValue = this.get_all_data(id)[rowIndex]; - mergeObjects(rowValue, {[column.field]: emittedArgs.newValue }); - } - if (grid.primaryKey === column.field && currentGridEditState.isRowSelected) { - grid.selection.deselect_item(id, rowID); - grid.selection.select_item(id, emittedArgs.newValue); + const rowValue = this.get_all_data(id, grid.transactions.enabled)[rowIndex]; + this.updateData(grid, rowID, rowValue, currentGridEditState.rowData, { [column.field]: emittedArgs.newValue }); + if (grid.primaryKey === column.field) { + if (currentGridEditState.isRowSelected) { + grid.selection.deselect_item(id, rowID); + grid.selection.select_item(id, emittedArgs.newValue); + } + if (grid.hasSummarizedColumns) { + grid.summaryService.removeSummaries(rowID); + } } - if (!grid.rowEditable || !grid.rowInEditMode || grid.rowInEditMode.rowID !== rowID) { + grid.summaryService.clearSummaryCache(emittedArgs); + if (!grid.rowEditable || !grid.rowInEditMode || grid.rowInEditMode.rowID !== rowID || !grid.transactions.enabled) { (grid as any)._pipeTrigger++; } } } + /** + * Updates related row of provided grid's data source with provided new row value + * @param grid Grid to update data for + * @param rowID ID of the row to update + * @param rowValueInDataSource Initial value of the row as it is in data source + * @param rowCurrentValue Current value of the row as it is with applied previous transactions + * @param rowNewValue New value of the row + */ + protected updateData(grid, rowID, rowValueInDataSource: any, rowCurrentValue: any, rowNewValue: {[x: string]: any}) { + if (grid.transactions.enabled) { + const transaction: Transaction = { + id: rowID, + type: TransactionType.UPDATE, + newValue: rowNewValue + }; + grid.transactions.add(transaction, rowCurrentValue); + } else { + mergeObjects(rowValueInDataSource, rowNewValue); + } + } + public update_row(value: any, id: string, rowID: any, gridState?: { args: IGridEditEventArgs, isRowSelected: boolean, rowData: any }): void { const grid = this.get(id); - const data = this.get_all_data(id); + const data = this.get_all_data(id, grid.transactions.enabled); const currentGridState = gridState ? gridState : this.create_grid_edit_args(id, rowID, null, value); const emitArgs = currentGridState.args; const index = this.get_row_index_in_data(id, rowID); @@ -406,16 +405,15 @@ export class GridBaseAPIService { if (currentRowInEditMode) { grid.transactions.endPending(false); } - if (grid.transactions.enabled && emitArgs.newValue !== null) { - grid.transactions.add({id: rowID, newValue: emitArgs.newValue, type: TransactionType.UPDATE}, emitArgs.oldValue); - } else if (emitArgs.newValue !== null && emitArgs.newValue !== undefined) { - Object.assign(data[index], emitArgs.newValue); - } + this.updateData(grid, rowID, data[index], emitArgs.oldValue, emitArgs.newValue); if (currentGridState.isRowSelected) { grid.selection.deselect_item(id, rowID); const newRowID = (grid.primaryKey) ? emitArgs.newValue[grid.primaryKey] : emitArgs.newValue; grid.selection.select_item(id, newRowID); } + if (grid.hasSummarizedColumns) { + grid.summaryService.removeSummaries(rowID); + } (grid as any)._pipeTrigger++; } } @@ -474,8 +472,6 @@ export class GridBaseAPIService { } filteringTree.filteringOperands = []; - this.remove_summary(id); - if (condition) { for (const column of grid.columns) { this.prepare_filtering_expression(filteringTree, column.field, term, @@ -500,23 +496,14 @@ export class GridBaseAPIService { if (index > -1) { filteringState.filteringOperands.splice(index, 1); - this.remove_summary(id, fieldName); } else { filteringState.filteringOperands = []; - this.remove_summary(id); } grid.filteredData = null; grid.filteringExpressionsTree = filteringState; } - protected calculateSummaries(id: string, column, data) { - if (!this.summaryCacheMap.get(id).get(column.field)) { - this.summaryCacheMap.get(id).set(column.field, - column.summaries.operate(data)); - } - } - public clear_sort(id, fieldName) { const sortingState = this.get(id).sortingExpressions; const index = sortingState.findIndex((expr) => expr.fieldName === fieldName); @@ -594,9 +581,9 @@ export class GridBaseAPIService { return column.dataType === DataType.Number; } - public get_all_data(id: string, transactions?: boolean): any[] { + public get_all_data(id: string, includeTransactions = false): any[] { const grid = this.get(id); - const data = transactions ? grid.dataWithAddedInTransactionRows : grid.data; + const data = includeTransactions ? grid.dataWithAddedInTransactionRows : grid.data; return data ? data : []; } @@ -604,4 +591,9 @@ export class GridBaseAPIService { return this.get_column_by_name(this.get(id).id, fieldName) ? this.get_column_by_name(id, fieldName).sortStrategy : undefined; } + + public get_row_id(id: string, rowData) { + const grid = this.get(id); + return grid.primaryKey ? rowData[grid.primaryKey] : rowData; + } } diff --git a/projects/igniteui-angular/src/lib/grids/column.component.ts b/projects/igniteui-angular/src/lib/grids/column.component.ts index 245dd28a7a8..4fd20253d85 100644 --- a/projects/igniteui-angular/src/lib/grids/column.component.ts +++ b/projects/igniteui-angular/src/lib/grids/column.component.ts @@ -14,7 +14,7 @@ import { DataType } from '../data-operations/data-util'; import { IgxTextHighlightDirective } from '../directives/text-highlight/text-highlight.directive'; import { GridBaseAPIService } from './api.service'; import { IgxGridCellComponent } from './cell.component'; -import { IgxDateSummaryOperand, IgxNumberSummaryOperand, IgxSummaryOperand } from './grid-summary'; +import { IgxDateSummaryOperand, IgxNumberSummaryOperand, IgxSummaryOperand } from './summaries/grid-summary'; import { IgxRowComponent } from './row.component'; import { IgxCellEditorTemplateDirective, @@ -139,18 +139,31 @@ export class IgxColumnComponent implements AfterContentInit { @Input() public resizable = false; /** - * Enables/disables summary for the column. - * Default value is `false`. + * Gets a value indicating whether the summary for the column is enabled. * ```typescript * let hasSummary = this.column.hasSummary; * ``` + * @memberof IgxColumnComponent + */ + @Input() + get hasSummary() { + return this._hasSummary; + } + /** + * Sets a value indicating whether the summary for the column is enabled. + * Default value is `false`. * ```html * * ``` * @memberof IgxColumnComponent */ - @Input() - public hasSummary = false; + set hasSummary(value) { + this._hasSummary = value; + + if (this.grid) { + this.grid.recalculateSummaries(); + } + } /** * Gets whether the column is hidden. * ```typescript @@ -198,10 +211,7 @@ export class IgxColumnComponent implements AfterContentInit { this.grid.refreshSearch(); } } - if (this.hasSummary) { - this.grid.summariesHeight = 0; - } - + this.grid.summaryService.resetSummaryHeight(); this.grid.reflow(); this.grid.filteringService.refreshExpressions(); } @@ -453,6 +463,12 @@ export class IgxColumnComponent implements AfterContentInit { */ public set summaries(classRef: any) { this._summaries = new classRef(); + + if (this.grid) { + this.grid.summaryService.removeSummariesCachePerColumn(this.field); + (this.grid as any)._summaryPipeTrigger++; + this.grid.recalculateSummaries(); + } } /** * Sets/gets whether the column is `searchable`. @@ -724,6 +740,16 @@ export class IgxColumnComponent implements AfterContentInit { return lvl; } + get isLastPinned(): boolean { + const pinnedCols = this.grid.pinnedColumns; + + if (pinnedCols.length === 0) { + return false; + } + + return pinnedCols.indexOf(this) === pinnedCols.length - 1; + } + /** * hidden */ @@ -822,6 +848,10 @@ export class IgxColumnComponent implements AfterContentInit { *@hidden */ protected _defaultMinWidth = '64'; + /** + *@hidden + */ + protected _hasSummary = false; /** *@hidden */ diff --git a/projects/igniteui-angular/src/lib/grids/grid-base.component.ts b/projects/igniteui-angular/src/lib/grids/grid-base.component.ts index 23764883d4d..adab3e7c187 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.component.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.component.ts @@ -41,7 +41,7 @@ import { GridBaseAPIService } from './api.service'; import { IgxGridCellComponent } from './cell.component'; import { IColumnVisibilityChangedEventArgs } from './column-hiding-item.directive'; import { IgxColumnComponent } from './column.component'; -import { ISummaryExpression } from './grid-summary'; +import { ISummaryExpression } from './summaries/grid-summary'; import { DropPosition, ContainerPositioningStrategy } from './grid.common'; import { IgxGridToolbarComponent } from './grid-toolbar.component'; import { IgxRowComponent } from './row.component'; @@ -65,6 +65,8 @@ import { IgxGridHeaderGroupComponent } from './grid-header-group.component'; import { IgxGridToolbarCustomContentDirective } from './grid-toolbar.component'; import { IGridResourceStrings } from '../core/i18n/grid-resources'; import { CurrentResourceStrings } from '../core/i18n/resources'; +import { IgxGridSummaryService } from './summaries/grid-summary.service'; +import { IgxSummaryRowComponent } from './summaries/summary-row.component'; const MINIMUM_COLUMN_WIDTH = 136; const FILTER_ROW_HEIGHT = 50; @@ -150,6 +152,17 @@ export interface IFocusChangeEventArgs { cancel: boolean; } +export enum GridSummaryPosition { + top = 'top', + bottom = 'bottom' +} + +export enum GridSummaryCalculationMode { + rootLevelOnly = 'rootLevelOnly', + childLevelsOnly = 'childLevelsOnly', + rootAndChildLevels = 'rootAndChildLevels' +} + export abstract class IgxGridBaseComponent extends DisplayDensityBase implements OnInit, OnDestroy, AfterContentInit, AfterViewInit { /** @@ -174,12 +187,12 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements this._resourceStrings = Object.assign({}, this._resourceStrings, value); } - /** - * An accessor that returns the resource strings. - */ - get resourceStrings(): IGridResourceStrings { - return this._resourceStrings; - } + /** + * An accessor that returns the resource strings. + */ + get resourceStrings(): IGridResourceStrings { + return this._resourceStrings; + } /** * An @Input property that autogenerates the `IgxGridComponent` columns. @@ -489,7 +502,9 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements */ set rowEditable(val: boolean) { this._rowEditable = val; - this.refreshGridState(); + if (this.gridAPI.get(this.id)) { + this.refreshGridState(); + } } /** @@ -729,7 +744,7 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements * ```typescript * let filtering = this.grid.allowFiltering; * ``` - * @memberof IgxGridComponent + * @memberof IgxGridBaseComponent */ @Input() get allowFiltering() { @@ -742,7 +757,7 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements * ```html * * ``` - * @memberof IgxGridComponent + * @memberof IgxGridBaseComponent */ set allowFiltering(value) { if (this._allowFiltering !== value) { @@ -766,6 +781,63 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements } } + /** + * Returns the summary position. + * ```typescript + * let summaryPosition = this.grid.summaryPosition; + * ``` + * @memberof IgxGridBaseComponent + */ + @Input() + get summaryPosition() { + return this._summaryPosition; + } + + /** + * Sets summary position. + * By default it is bottom. + * ```html + * + * ``` + * @memberof IgxGridBaseComponent + */ + set summaryPosition(value) { + this._summaryPosition = value; + if (this.gridAPI.get(this.id)) { + this.markForCheck(); + } + } + + /** + * Returns the summary calculation mode. + * ```typescript + * let summaryCalculationMode = this.grid.summaryCalculationMode; + * ``` + * @memberof IgxGridBaseComponent + */ + @Input() + get summaryCalculationMode() { + return this._summaryCalculationMode; + } + + /** + * Sets summary calculation mode. + * By default it is rootAndChildLevels which means the summaries are calculated for the root level and each child level. + * ```html + * + * ``` + * @memberof IgxGridBaseComponent + */ + set summaryCalculationMode(value) { + this._summaryCalculationMode = value; + if (this.gridAPI.get(this.id)) { + this.summaryService.summaryHeight = 0; + this.endEdit(true); + this.calculateGridHeight(); + this.cdr.markForCheck(); + } + } + /** * Emitted when `IgxGridCellComponent` is clicked. Returns the `IgxGridCellComponent`. * ```html @@ -1246,6 +1318,22 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements @ViewChildren('row') private _rowList: QueryList; + @ViewChildren('summaryRow', { read: IgxSummaryRowComponent }) + protected _summaryRowList: QueryList; + + + public get summariesRowList() { + const res = new QueryList(); + if (!this._summaryRowList) { + return res; + } + const sumList = this._summaryRowList.filter((item) => { + return item.element.nativeElement.parentElement !== null; + }); + res.reset(sumList); + return res; + } + /** * A list of `IgxGridRowComponent`. * ```typescript @@ -1329,9 +1417,6 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements @ViewChild('verticalScrollContainer', { read: IgxGridForOfDirective }) public verticalScrollContainer: IgxGridForOfDirective; - @ViewChild('summaryContainer', { read: IgxGridForOfDirective }) - protected summaryContainer: IgxGridForOfDirective; - /** * @hidden */ @@ -1386,11 +1471,6 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements @ViewChild('tfoot') public tfoot: ElementRef; - /** - * @hidden - */ - @ViewChild('summaries') - public summaries: ElementRef; /** * @hidden @@ -1509,6 +1589,13 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements return this._pipeTrigger; } + /** + * @hidden + */ + get summaryPipeTrigger(): number { + return this._summaryPipeTrigger; + } + /** * Returns the sorting state of the `IgxGridComponent`. * ```typescript @@ -2001,6 +2088,10 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements * @hidden */ protected _pipeTrigger = 0; + /** + * @hidden + */ + protected _summaryPipeTrigger = 0; /** * @hidden */ @@ -2066,6 +2157,9 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements private _defaultTargetRecordNumber = 10; + private _summaryPosition = GridSummaryPosition.bottom; + private _summaryCalculationMode = GridSummaryCalculationMode.rootAndChildLevels; + private rowEditPositioningStrategy = new ContainerPositioningStrategy({ horizontalDirection: HorizontalAlignment.Left, verticalDirection: VerticalAlignment.Bottom, @@ -2100,9 +2194,6 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements this.headerContainer.onHScroll(scrollLeft); this._horizontalForOfs.forEach(vfor => vfor.onHScroll(scrollLeft)); - if (this.summaryContainer) { - this.summaryContainer.onHScroll(scrollLeft); - } this.zone.run(() => { this.cdr.detectChanges(); this.parentVirtDir.onChunkLoad.emit(this.headerContainer.state); @@ -2136,6 +2227,7 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements private viewRef: ViewContainerRef, private navigation: IgxGridNavigationService, public filteringService: IgxFilteringService, + public summaryService: IgxGridSummaryService, @Optional() @Inject(DisplayDensityToken) protected _displayDensityOptions: IDisplayDensityOptions) { super(_displayDensityOptions); this.resizeHandler = () => { @@ -2151,16 +2243,15 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements this.gridAPI.register(this); this.navigation.grid = this; this.filteringService.gridId = this.id; + this.summaryService.grid = this; this.columnListDiffer = this.differs.find([]).create(null); this.calcWidth = this._width && this._width.indexOf('%') === -1 ? parseInt(this._width, 10) : 0; this.calcHeight = 0; this.calcRowCheckboxWidth = 0; - this.onRowAdded.pipe(takeUntil(this.destroy$)).subscribe(() => this.refreshGridState()); - this.onRowDeleted.pipe(takeUntil(this.destroy$)).subscribe(() => this.clearSummaryCache()); - this.onFilteringDone.pipe(takeUntil(this.destroy$)).subscribe(() => this.refreshGridState()); - this.onCellEdit.pipe(takeUntil(this.destroy$)).subscribe((editCell) => this.clearSummaryCache(editCell)); - this.onRowEdit.pipe(takeUntil(this.destroy$)).subscribe(() => this.clearSummaryCache()); + this.onRowAdded.pipe(takeUntil(this.destroy$)).subscribe((args) => this.refreshGridState(args)); + this.onRowDeleted.pipe(takeUntil(this.destroy$)).subscribe((args) => this.clearSummaryCache(args)); + this.onFilteringDone.pipe(takeUntil(this.destroy$)).subscribe(() => this.endEdit(true)); this.onColumnMoving.pipe(takeUntil(this.destroy$)).subscribe(() => { this.endEdit(true); }); @@ -2185,8 +2276,6 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements this.initColumns(this.columnList, (col: IgxColumnComponent) => this.onColumnInit.emit(col)); this.columnListDiffer.diff(this.columnList); - this.clearSummaryCache(); - this.summariesHeight = this.calcMaxSummaryHeight(); this._derivePossibleHeight(); this.markForCheck(); @@ -2234,7 +2323,7 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements this.calculateGridSizes(); this.onDensityChanged.pipe(takeUntil(this.destroy$)).subscribe(() => { requestAnimationFrame(() => { - this.summariesHeight = 0; + this.summaryService.summaryHeight = 0; this.reflow(); this.verticalScrollContainer.recalcUpdateSizes(); }); @@ -2267,10 +2356,12 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements } this._dataRowList.changes.pipe(takeUntil(this.destroy$)).subscribe(list => - this._horizontalForOfs = list.toArray() - .filter(item => item.element.nativeElement.parentElement !== null) - .map(row => row.virtDirRow) + this._horizontalForOfs = this.combineForOfCollections(list.toArray() + .filter(item => item.element.nativeElement.parentElement !== null), this._summaryRowList) ); + this._summaryRowList.changes.pipe(takeUntil(this.destroy$)).subscribe(summaryList => + this._horizontalForOfs - this.combineForOfCollections(this._dataRowList, summaryList.toArray() + .filter(item => item.element.nativeElement.parentElement !== null))); this.zone.runOutsideAngular(() => { this._vScrollListener = this.verticalScrollHandler.bind(this); @@ -2281,11 +2372,16 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements this._hScrollListener = this.horizontalScrollHandler.bind(this); this.parentVirtDir.getHorizontalScroll().addEventListener('scroll', this._hScrollListener); }); - this._horizontalForOfs = this._dataRowList.map(row => row.virtDirRow); + this._horizontalForOfs = this.combineForOfCollections(this._dataRowList, this._summaryRowList); const vertScrDC = this.verticalScrollContainer.dc.instance._viewContainer.element.nativeElement; vertScrDC.addEventListener('scroll', (evt) => { this.scrollHandler(evt); }); } + private combineForOfCollections(dataList, summaryList) { + return dataList.map(row => row.virtDirRow).concat(summaryList.map(row => row.virtDirRow)); + + } + /** * @hidden */ @@ -2916,10 +3012,7 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements } } - /** - * @hidden - * @param - */ + /** @hidden */ public deleteRowById(rowId: any) { let index: number; const data = this.gridAPI.get_all_data(this.id); @@ -3118,8 +3211,6 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements } else { this._summaries(rest[0], true, rest[1]); } - this.summariesHeight = 0; - this.markForCheck(); this.calculateGridHeight(); this.cdr.detectChanges(); } @@ -3142,8 +3233,6 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements } else { this._summaries(rest[0], false); } - this.summariesHeight = 0; - this.markForCheck(); this.calculateGridHeight(); this.cdr.detectChanges(); } @@ -3191,20 +3280,16 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements /** * @hidden */ - public clearSummaryCache(editCell?) { - if (editCell && editCell.cell) { - this.gridAPI.remove_summary(this.id, editCell.cell.column.filed); - } else { - this.gridAPI.remove_summary(this.id); - } + public clearSummaryCache(args?) { + this.summaryService.clearSummaryCache(args); } /** * @hidden */ - public refreshGridState(editCell?) { - this.endEdit(true); - this.clearSummaryCache(editCell); + public refreshGridState(args?) { + this.endEdit(true); + this.clearSummaryCache(args); } // TODO: We have return values here. Move them to event args ?? @@ -3250,16 +3335,12 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements } /** - * Recalculates grid summary area. - * Should be run for example when enabling or disabling summaries for a column. - * ```typescript - * this.grid.recalculateSummaries(); - * ``` - * @memberof IgxGridBaseComponent + * @hidden */ public recalculateSummaries() { - this.summariesHeight = 0; - requestAnimationFrame(() => this.calculateGridSizes()); + this.summaryService.resetSummaryHeight(); + this.calculateGridHeight(); + this.cdr.detectChanges(); } /** @@ -3389,10 +3470,15 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements * @memberof IgxGridBaseComponent */ get hasSummarizedColumns(): boolean { - const summarizedColumns = this.columnList.filter(col => col.hasSummary); - return summarizedColumns.length > 0 && summarizedColumns.some(col => !col.hidden); + return this.summaryService.hasSummarizedColumns; } + /** + * @hidden + */ + get rootSummariesEnabled(): boolean { + return this.summaryCalculationMode !== GridSummaryCalculationMode.childLevelsOnly; + } /** * Returns if the `IgxGridComponent` has moveable columns. * ```typescript @@ -3490,12 +3576,11 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements this.theadRow.nativeElement.style.height = `${(this.maxLevelHeaderDepth + 1) * this.defaultRowHeight + (this.allowFiltering ? FILTER_ROW_HEIGHT : 0) + 1}px`; } - + this.summariesHeight = 0; if (!this._height) { this.calcHeight = null; - if (this.hasSummarizedColumns && !this.summariesHeight) { - this.summariesHeight = this.summaries ? - this.calcMaxSummaryHeight() : 0; + if (this.hasSummarizedColumns && this.rootSummariesEnabled) { + this.summariesHeight = this.summaryService.calcMaxSummaryHeight(); } return; } @@ -3512,11 +3597,9 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements this.paginator.nativeElement.offsetHeight : 0; } - if (!this.summariesHeight) { - this.summariesHeight = this.summaries ? - this.calcMaxSummaryHeight() : 0; + if (this.hasSummarizedColumns && this.rootSummariesEnabled) { + this.summariesHeight = this.summaryService.calcMaxSummaryHeight(); } - const groupAreaHeight = this.getGroupAreaHeight(); if (this._height && this._height.indexOf('%') !== -1) { @@ -3604,23 +3687,6 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements this._derivePossibleWidth(); } - /** - * @hidden - */ - protected calcMaxSummaryHeight() { - let maxSummaryLength = 0; - this.columnList.filter((col) => col.hasSummary && !col.hidden).forEach((column) => { - this.gridAPI.set_summary_by_column_name(this.id, column.field); - const getCurrentSummaryColumn = this.gridAPI.get_summaries(this.id).get(column.field); - if (getCurrentSummaryColumn) { - if (maxSummaryLength < getCurrentSummaryColumn.length) { - maxSummaryLength = getCurrentSummaryColumn.length; - } - } - }); - return maxSummaryLength * this.defaultRowHeight; - } - /** * @hidden */ @@ -3628,12 +3694,15 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements this.calculateGridWidth(); this.cdr.detectChanges(); this.calculateGridHeight(); + if (this.showRowCheckboxes) { - this.calcRowCheckboxWidth = this.headerCheckboxContainer.nativeElement.clientWidth; + this.calcRowCheckboxWidth = this.headerCheckboxContainer.nativeElement.getBoundingClientRect().width; } + if (this.rowEditable) { this.repositionRowEditingOverlay(this.rowInEditMode); } + this.cdr.detectChanges(); } @@ -3678,9 +3747,12 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements */ protected _summaries(fieldName: string, hasSummary: boolean, summaryOperand?: any) { const column = this.gridAPI.get_column_by_name(this.id, fieldName); - column.hasSummary = hasSummary; - if (summaryOperand) { - column.summaries = summaryOperand; + if (column) { + column.hasSummary = hasSummary; + if (summaryOperand) { + if (this.rootSummariesEnabled) {this.summaryService.retriggerRootPipe = !this.summaryService.retriggerRootPipe; } + column.summaries = summaryOperand; + } } } @@ -3695,8 +3767,10 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements /** * @hidden */ - protected _disableMultipleSummaries(expressions: string[]) { - expressions.forEach((column) => { this._summaries(column, false); }); + protected _disableMultipleSummaries(expressions) { + expressions.forEach((column) => { + const columnName = column && column.fieldName ? column.fieldName : column; + this._summaries(columnName, false); }); } /** @@ -4495,9 +4569,6 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements return rowChanges ? Object.keys(rowChanges).length : 0; } - protected writeToData(rowIndex: number, value: any) { - mergeObjects(this.data[rowIndex], value); - } /** * TODO: Refactor * @hidden @@ -4528,6 +4599,7 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements }); if (!commit) { this.onRowEditCancel.emit(emitArgs); + this.transactions.endPending(commit); } else { this.gridAPI.update_row(emitArgs.newValue, this.id, rowID, currentGridState); } @@ -4535,7 +4607,6 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements this.transactions.startPending(); return; } - this.transactions.endPending(commit); this.closeRowEditingOverlay(); } @@ -4610,4 +4681,12 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements protected getExportCsv(): boolean { return this._exportCsv; } + + /** + * @hidden + */ + public isSummaryRow(rowData): boolean { + return rowData.summaries && (rowData.summaries instanceof Map); + } + } diff --git a/projects/igniteui-angular/src/lib/grids/grid-common.module.ts b/projects/igniteui-angular/src/lib/grids/grid-common.module.ts index 1dcbc2ba3d3..ce038c86735 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-common.module.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-common.module.ts @@ -20,7 +20,6 @@ import { IgxGridCellComponent } from './cell.component'; import { IgxColumnComponent, IgxColumnGroupComponent } from './column.component'; import { IgxColumnHidingModule } from './column-hiding.component'; import { IgxGridHeaderComponent } from './grid-header.component'; -import { IgxGridSummaryComponent } from './grid-summary.component'; import { IgxGridToolbarComponent } from './grid-toolbar.component'; import { IgxGridFilteringCellComponent } from './filtering/grid-filtering-cell.component'; import { IgxGridFilteringRowComponent } from './filtering/grid-filtering-row.component'; @@ -58,6 +57,9 @@ import { IgxGridNavigationService } from './grid-navigation.service'; import { IgxGridHeaderGroupComponent } from './grid-header-group.component'; import { IgxColumnResizingService } from './grid-column-resizing.service'; import { IgxGridToolbarCustomContentDirective } from './grid-toolbar.component'; +import { IgxSummaryRowComponent } from './summaries/summary-row.component'; +import { IgxSummaryCellComponent } from './summaries/summary-cell.component'; +import { IgxSummaryDataPipe } from './summaries/grid-root-summary.pipe'; @NgModule({ declarations: [ @@ -65,7 +67,6 @@ import { IgxGridToolbarCustomContentDirective } from './grid-toolbar.component'; IgxColumnComponent, IgxColumnGroupComponent, IgxGridHeaderComponent, - IgxGridSummaryComponent, IgxGridToolbarComponent, IgxGridToolbarCustomContentDirective, IgxCellFooterTemplateDirective, @@ -85,8 +86,11 @@ import { IgxGridToolbarCustomContentDirective } from './grid-toolbar.component'; IgxGridFilteringRowComponent, IgxDatePipeComponent, IgxDecimalPipeComponent, + IgxSummaryDataPipe, IgxRowComponent, - IgxGridHeaderGroupComponent + IgxGridHeaderGroupComponent, + IgxSummaryRowComponent, + IgxSummaryCellComponent ], entryComponents: [ IgxColumnComponent, @@ -97,7 +101,6 @@ import { IgxGridToolbarCustomContentDirective } from './grid-toolbar.component'; IgxColumnComponent, IgxColumnGroupComponent, IgxGridHeaderComponent, - IgxGridSummaryComponent, IgxGridToolbarComponent, IgxGridToolbarCustomContentDirective, IgxCellFooterTemplateDirective, @@ -114,6 +117,7 @@ import { IgxGridToolbarCustomContentDirective } from './grid-toolbar.component'; IgxRowComponent, IgxGridFilterConditionPipe, IgxGridTransactionPipe, + IgxSummaryDataPipe, IgxDatePipeComponent, IgxDecimalPipeComponent, IgxButtonModule, @@ -137,7 +141,9 @@ import { IgxGridToolbarCustomContentDirective } from './grid-toolbar.component'; IgxColumnPinningModule, IgxGridFilteringCellComponent, IgxGridFilteringRowComponent, - IgxGridHeaderGroupComponent + IgxGridHeaderGroupComponent, + IgxSummaryRowComponent, + IgxSummaryCellComponent ], imports: [ CommonModule, diff --git a/projects/igniteui-angular/src/lib/grids/grid-navigation.service.ts b/projects/igniteui-angular/src/lib/grids/grid-navigation.service.ts index d593eaa0a3c..a4e6b098a98 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-navigation.service.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-navigation.service.ts @@ -27,7 +27,11 @@ export class IgxGridNavigationService { } public horizontalScroll(rowIndex) { - return this.grid.dataRowList.find((row) => row.index === rowIndex).virtDirRow; + let rowComp = this.grid.dataRowList.find((row) => row.index === rowIndex); + if (!rowComp) { + rowComp = this.grid.summariesRowList.find((row) => row.index === rowIndex); + } + return rowComp.virtDirRow; } public getColumnUnpinnedIndex(visibleColumnIndex: number) { @@ -90,7 +94,11 @@ export class IgxGridNavigationService { } } - public getCellElementByVisibleIndex(rowIndex, visibleColumnIndex) { + public getCellElementByVisibleIndex(rowIndex, visibleColumnIndex, isSummary = false) { + if (isSummary) { + return this.grid.nativeElement.querySelector( + `igx-grid-summary-cell[data-rowindex="${rowIndex}"][data-visibleIndex="${visibleColumnIndex}"]`); + } if (this.isTreeGrid && visibleColumnIndex === 0) { return this.grid.nativeElement.querySelector( `igx-tree-grid-cell[data-rowindex="${rowIndex}"][data-visibleIndex="${visibleColumnIndex}"]`); @@ -99,12 +107,12 @@ export class IgxGridNavigationService { `igx-grid-cell[data-rowindex="${rowIndex}"][data-visibleIndex="${visibleColumnIndex}"]`); } - public onKeydownArrowRight(element, rowIndex, visibleColumnIndex) { + public onKeydownArrowRight(element, rowIndex, visibleColumnIndex, isSummary = false) { if (this.grid.unpinnedColumns[this.grid.unpinnedColumns.length - 1].visibleIndex === visibleColumnIndex) { return; } if (this.isColumnFullyVisible(visibleColumnIndex + 1)) { // if next column is fully visible or is pinned - if (element.classList.contains('igx-grid__th--pinned-last')) { + if (element.classList.contains('igx-grid__th--pinned-last') || element.classList.contains('igx-grid-summary--pinned-last')) { if (this.isColumnLeftFullyVisible(visibleColumnIndex + 1)) { element.nextElementSibling.firstElementChild.focus(); } else { @@ -121,11 +129,11 @@ export class IgxGridNavigationService { } } else { this.grid.nativeElement.focus({preventScroll: true}); - this.performHorizontalScrollToCell(rowIndex, visibleColumnIndex + 1); + this.performHorizontalScrollToCell(rowIndex, visibleColumnIndex + 1, isSummary); } } - public onKeydownArrowLeft(element, rowIndex, visibleColumnIndex) { + public onKeydownArrowLeft(element, rowIndex, visibleColumnIndex, isSummary = false) { if (visibleColumnIndex === 0) { return; } @@ -134,7 +142,7 @@ export class IgxGridNavigationService { element.parentNode.previousElementSibling.focus(); } else if (!this.isColumnLeftFullyVisible(visibleColumnIndex - 1)) { this.grid.nativeElement.focus({preventScroll: true}); - this.performHorizontalScrollToCell(rowIndex, visibleColumnIndex - 1); + this.performHorizontalScrollToCell(rowIndex, visibleColumnIndex - 1, isSummary); } else { element.previousElementSibling.focus(); } @@ -188,11 +196,16 @@ export class IgxGridNavigationService { this.performHorizontalScrollToCell(rowIndex, editableIndex); } } - public onKeydownHome(rowIndex) { - const rowElement = this.grid.dataRowList.find((row) => row.index === rowIndex).nativeElement; - let firstCell = this.isTreeGrid ? - rowElement.querySelector('igx-tree-grid-cell') : - rowElement.querySelector('igx-grid-cell'); + public onKeydownHome(rowIndex, isSummary = false) { + let rowElement = this.grid.dataRowList.find((row) => row.index === rowIndex); + let cellTag = this.isTreeGrid ? 'igx-tree-grid-cell' : 'igx-grid-cell'; + if (isSummary) { + rowElement = this.grid.summariesRowList.find((row) => row.index === rowIndex); + cellTag = 'igx-grid-summary-cell'; + } + if (!rowElement) { return; } + rowElement = rowElement.nativeElement; + let firstCell = rowElement.querySelector(`${cellTag}`); if (this.grid.pinnedColumns.length || this.displayContainerScrollLeft === 0) { firstCell.focus(); } else { @@ -200,18 +213,24 @@ export class IgxGridNavigationService { .pipe(first()) .subscribe(() => { this.grid.nativeElement.focus({preventScroll: true}); - firstCell = this.isTreeGrid ? rowElement.querySelector('igx-tree-grid-cell') : - rowElement.querySelector('igx-grid-cell'); + firstCell = rowElement.querySelector(`${cellTag}`); firstCell.focus(); }); this.horizontalScroll(rowIndex).scrollTo(0); } } - public onKeydownEnd(rowIndex) { + public onKeydownEnd(rowIndex, isSummary = false) { const index = this.grid.unpinnedColumns[this.grid.unpinnedColumns.length - 1].visibleIndex; - const rowElement = this.grid.dataRowList.find((row) => row.index === rowIndex).nativeElement; - const allCells = rowElement.querySelectorAll('igx-grid-cell'); + let rowElement = this.grid.dataRowList.find((row) => row.index === rowIndex); + let cellTag = 'igx-grid-cell'; + if (isSummary) { + rowElement = this.grid.summariesRowList.find((row) => row.index === rowIndex); + cellTag = 'igx-grid-summary-cell'; + } + if (!rowElement) { return; } + rowElement = rowElement.nativeElement; + const allCells = rowElement.querySelectorAll(`${cellTag}`); const lastCell = allCells[allCells.length - 1]; if (this.isColumnFullyVisible(index)) { lastCell.focus(); @@ -277,8 +296,8 @@ export class IgxGridNavigationService { .pipe(first()) .subscribe(() => { const tag = rowElement.tagName.toLowerCase(); - if (tag === 'igx-grid-row' || tag === 'igx-tree-grid-row') { - rowElement = this.getRowByIndex(currentRowIndex); + if (tag === 'igx-grid-row' || tag === 'igx-tree-grid-row' || tag === 'igx-grid-summary-row') { + rowElement = this.getRowByIndex(currentRowIndex, tag); } else { rowElement = this.grid.nativeElement.querySelector( `igx-grid-groupby-row[data-rowindex="${currentRowIndex}"]`); @@ -294,7 +313,13 @@ export class IgxGridNavigationService { if (currentRowEl.previousElementSibling.tagName.toLowerCase() === 'igx-grid-groupby-row') { currentRowEl.previousElementSibling.focus(); } else { + const isSummaryRow = currentRowEl.previousElementSibling.tagName.toLowerCase() === 'igx-grid-summary-row'; if (this.isColumnFullyVisible(visibleColumnIndex) && this.isColumnLeftFullyVisible(visibleColumnIndex)) { + if (isSummaryRow) { + currentRowEl.previousElementSibling. + querySelector(`igx-grid-summary-cell[data-visibleIndex="${visibleColumnIndex}"]`).focus(); + return; + } const cell = this.isTreeGrid && visibleColumnIndex === 0 ? currentRowEl.previousElementSibling.querySelector(`igx-tree-grid-cell[data-visibleIndex="${visibleColumnIndex}"]`) : currentRowEl.previousElementSibling.querySelector(`igx-grid-cell[data-visibleIndex="${visibleColumnIndex}"]`); @@ -303,7 +328,7 @@ export class IgxGridNavigationService { } this.grid.nativeElement.focus({preventScroll: true}); this.performHorizontalScrollToCell(parseInt( - currentRowEl.previousElementSibling.getAttribute('data-rowindex'), 10), visibleColumnIndex); + currentRowEl.previousElementSibling.getAttribute('data-rowindex'), 10), visibleColumnIndex, isSummaryRow); } } @@ -323,8 +348,8 @@ export class IgxGridNavigationService { .pipe(first()) .subscribe(() => { const tag = rowElement.tagName.toLowerCase(); - if (tag === 'igx-grid-row' || tag === 'igx-tree-grid-row') { - rowElement = this.getRowByIndex(currentRowIndex); + if (tag === 'igx-grid-row' || tag === 'igx-tree-grid-row' || tag === 'igx-grid-summary-row') { + rowElement = this.getRowByIndex(currentRowIndex, tag); } else { rowElement = this.grid.nativeElement.querySelector( `igx-grid-groupby-row[data-rowindex="${currentRowIndex}"]`); @@ -340,7 +365,12 @@ export class IgxGridNavigationService { if (rowElement.nextElementSibling.tagName.toLowerCase() === 'igx-grid-groupby-row') { rowElement.nextElementSibling.focus(); } else { + const isSummaryRow = rowElement.nextElementSibling.tagName.toLowerCase() === 'igx-grid-summary-row'; if (this.isColumnFullyVisible(visibleColumnIndex) && this.isColumnLeftFullyVisible(visibleColumnIndex)) { + if (isSummaryRow) { + rowElement.nextElementSibling.querySelector(`igx-grid-summary-cell[data-visibleIndex="${visibleColumnIndex}"]`).focus(); + return; + } const cell = this.isTreeGrid && visibleColumnIndex === 0 ? rowElement.nextElementSibling.querySelector(`igx-tree-grid-cell[data-visibleIndex="${visibleColumnIndex}"]`) : rowElement.nextElementSibling.querySelector(`igx-grid-cell[data-visibleIndex="${visibleColumnIndex}"]`); @@ -348,7 +378,7 @@ export class IgxGridNavigationService { return; } this.performHorizontalScrollToCell(parseInt( - rowElement.nextElementSibling.getAttribute('data-rowindex'), 10), visibleColumnIndex); + rowElement.nextElementSibling.getAttribute('data-rowindex'), 10), visibleColumnIndex, isSummaryRow); } } @@ -394,23 +424,26 @@ export class IgxGridNavigationService { } } - public performTab(currentRowEl, rowIndex, visibleColumnIndex) { + public performTab(currentRowEl, rowIndex, visibleColumnIndex, isSummaryRow = false) { if (this.grid.unpinnedColumns[this.grid.unpinnedColumns.length - 1].visibleIndex === visibleColumnIndex) { if (this.isRowInEditMode(rowIndex)) { this.grid.rowEditTabs.first.element.nativeElement.focus(); return; } - if (this.grid.rowList.find(row => row.index === rowIndex + 1)) { + const rowEl = this.grid.rowList.find(row => row.index === rowIndex + 1) ? + this.grid.rowList.find(row => row.index === rowIndex + 1) : + this.grid.summariesRowList.find(row => row.index === rowIndex + 1); + if (rowEl) { this.navigateDown(currentRowEl, rowIndex, 0); } } else { - const cell = this.getCellElementByVisibleIndex(rowIndex, visibleColumnIndex); + const cell = this.getCellElementByVisibleIndex(rowIndex, visibleColumnIndex, isSummaryRow); if (cell) { if (this.grid.rowEditable && this.isRowInEditMode(rowIndex)) { this.moveNextEditable(cell, rowIndex, visibleColumnIndex); return; } - this.onKeydownArrowRight(cell, rowIndex, visibleColumnIndex); + this.onKeydownArrowRight(cell, rowIndex, visibleColumnIndex, isSummaryRow); } } } @@ -425,7 +458,7 @@ export class IgxGridNavigationService { } } - public performShiftTabKey(currentRowEl, rowIndex, visibleColumnIndex) { + public performShiftTabKey(currentRowEl, rowIndex, visibleColumnIndex, isSummary = false) { if (visibleColumnIndex === 0) { if (this.isRowInEditMode(rowIndex)) { this.grid.rowEditTabs.last.element.nativeElement.focus(); @@ -438,31 +471,30 @@ export class IgxGridNavigationService { this.grid.unpinnedColumns[this.grid.unpinnedColumns.length - 1].visibleIndex); } } else { - const cell = currentRowEl.querySelector(`igx-grid-cell[data-visibleIndex="${visibleColumnIndex}"]`); + const cell = this.getCellElementByVisibleIndex(rowIndex, visibleColumnIndex, isSummary); if (cell) { if (this.grid.rowEditable && this.isRowInEditMode(rowIndex)) { this.movePreviousEditable( rowIndex, visibleColumnIndex); return; } - this.onKeydownArrowLeft(cell, rowIndex, visibleColumnIndex); + this.onKeydownArrowLeft(cell, rowIndex, visibleColumnIndex, isSummary); } } } - private performHorizontalScrollToCell(rowIndex, visibleColumnIndex) { + private performHorizontalScrollToCell(rowIndex, visibleColumnIndex, isSummary = false) { const unpinnedIndex = this.getColumnUnpinnedIndex(visibleColumnIndex); this.grid.parentVirtDir.onChunkLoad .pipe(first()) .subscribe(() => { - this.getCellElementByVisibleIndex(rowIndex, visibleColumnIndex).focus(); + this.getCellElementByVisibleIndex(rowIndex, visibleColumnIndex, isSummary).focus(); }); this.horizontalScroll(rowIndex).scrollTo(unpinnedIndex); } - private getRowByIndex(index) { - return this.isTreeGrid ? this.grid.nativeElement.querySelector( - `igx-tree-grid-row[data-rowindex="${index}"]`) : - this.grid.nativeElement.querySelector( - `igx-grid-row[data-rowindex="${index}"]`); + + private getRowByIndex(index, tag) { + return this.grid.nativeElement.querySelector( + `${tag}[data-rowindex="${index}"]`); } private getAllRows() { diff --git a/projects/igniteui-angular/src/lib/grids/grid-summary.component.html b/projects/igniteui-angular/src/lib/grids/grid-summary.component.html deleted file mode 100644 index d0da05407b7..00000000000 --- a/projects/igniteui-angular/src/lib/grids/grid-summary.component.html +++ /dev/null @@ -1,10 +0,0 @@ - - -
- {{ translateSummary(summary) }} - - {{ column.dataType === 'number' ? (summary.summaryResult | igxdecimal) : column.dataType === 'date' ? (summary.summaryResult | igxdate) : (summary.summaryResult) }} - -
-
-
diff --git a/projects/igniteui-angular/src/lib/grids/grid-summary.component.ts b/projects/igniteui-angular/src/lib/grids/grid-summary.component.ts deleted file mode 100644 index 136c9da80c8..00000000000 --- a/projects/igniteui-angular/src/lib/grids/grid-summary.component.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { - ChangeDetectionStrategy, ChangeDetectorRef, - Component, DoCheck, HostBinding, Input -} from '@angular/core'; -import { DataType } from '../data-operations/data-util'; -import { GridBaseAPIService } from './api.service'; -import { IgxColumnComponent } from './column.component'; -import { IgxGridBaseComponent } from './grid-base.component'; -import { IgxSummaryResult } from './grid-summary'; -import { DisplayDensity } from '../core/displayDensity'; -/** - *@hidden - */ -@Component({ - changeDetection: ChangeDetectionStrategy.OnPush, - preserveWhitespaces: false, - selector: 'igx-grid-summary', - templateUrl: './grid-summary.component.html' -}) -export class IgxGridSummaryComponent implements DoCheck { - - fieldName: string; - - @Input() - public column: IgxColumnComponent; - - @Input() - public gridID: string; - - @HostBinding('class.igx-grid-summary--fw') - get widthPersistenceClass(): boolean { - return this.column.width !== null; - } - - @HostBinding('class.igx-grid-summary--pinned') - get isPinned() { - return this.column.pinned; - } - - @HostBinding('class.igx-grid-summary--pinned-last') - get isLastPinned() { - const pinnedCols = this.gridAPI.get(this.gridID).pinnedColumns; - if (pinnedCols.length === 0) { - return false; - } else { - return pinnedCols.indexOf(this.column) === pinnedCols.length - 1; - } - } - - @HostBinding('class.igx-grid-summary--empty') - get emptyClass(): boolean { - return !this.column.hasSummary; - } - - @HostBinding('style.min-width') - @HostBinding('style.flex-basis') - get width() { - return this.column.width; - } - - @HostBinding('class.igx-grid-summary--compact') - get compactCSS() { - return this.gridAPI.get(this.gridID).displayDensity === DisplayDensity.compact; - } - - @HostBinding('class.igx-grid-summary--cosy') - get cosyCSS() { - return this.gridAPI.get(this.gridID).displayDensity === DisplayDensity.cosy; - } - - @HostBinding('class.igx-grid-summary') - get defaultCSS() { - return this.gridAPI.get(this.gridID).displayDensity === DisplayDensity.comfortable; - } - - get dataType(): DataType { - return this.column.dataType; - } - public summaryItemHeight; - public itemClass = 'igx-grid-summary__item'; - - constructor(public gridAPI: GridBaseAPIService, public cdr: ChangeDetectorRef) { } - - ngDoCheck() { - this.summaryItemHeight = this.gridAPI.get(this.gridID).defaultRowHeight; - this.cdr.detectChanges(); - } - - public translateSummary(summary: IgxSummaryResult): string { - return this.gridAPI.get(this.gridID).resourceStrings[`igx_grid_summary_${summary.key}`] || summary.label; - } - - get resolveSummaries(): any[] { - if (this.fieldName) { - const field = this.fieldName; - this.fieldName = null; - this.gridAPI.set_summary_by_column_name(this.gridID, field); - if (this.column.field === field) { - return this.gridAPI.get_summaries(this.gridID).get(field); - } else { - return this.gridAPI.get_summaries(this.gridID).get(this.column.field); - } - } else { - this.gridAPI.set_summary_by_column_name(this.gridID, this.column.field); - return this.gridAPI.get_summaries(this.gridID).get(this.column.field); - } - } -} diff --git a/projects/igniteui-angular/src/lib/grids/grid.common.ts b/projects/igniteui-angular/src/lib/grids/grid.common.ts index 6f65ad68b32..3c5ca200cd7 100644 --- a/projects/igniteui-angular/src/lib/grids/grid.common.ts +++ b/projects/igniteui-angular/src/lib/grids/grid.common.ts @@ -590,10 +590,16 @@ export class IgxDecimalPipeComponent extends DecimalPipe implements PipeTransfor } } +/** + * @hidden + */ export interface ContainerPositionSettings extends PositionSettings { container?: HTMLElement; } +/** + * @hidden + */ export class ContainerPositioningStrategy extends ConnectedPositioningStrategy { isTop = false; isTopInitialPosition = null; diff --git a/projects/igniteui-angular/src/lib/grids/grid/column-group.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/column-group.spec.ts index 47e648ab9e1..9d13000f372 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/column-group.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/column-group.spec.ts @@ -1207,36 +1207,6 @@ describe('IgxGrid - multi-column headers', () => { testGroupsAndColumns(18, 11); })); - it('summaries - verify summaries when there are grouped columns', (async () => { - const fixture = TestBed.createComponent(ColumnGroupFourLevelTestComponent); - fixture.detectChanges(); - const grid = fixture.componentInstance.grid; - - // Verify columns and groups - testGroupsAndColumns(18, 11); - - const allColumns = grid.columnList; - allColumns.forEach((col) => { - if (!col.columnGroup) { - col.hasSummary = true; - } - }); - await wait(); - fixture.detectChanges(); - - const summaries = fixture.debugElement.queryAll(By.css('igx-grid-summary')); - expect(summaries.length).toBe(7); - let index = 0; - grid.visibleColumns.forEach((col) => { - if (!col.columnGroup && index < 7) { - expect(col.hasSummary).toBeTruthy(); - const labels = summaries[index].queryAll(By.css('.igx-grid-summary__label')); - expect(labels.length).toBe(1); - expect(labels[0].nativeElement.innerText).toBe('Count'); - index++; - } - }); - })); it('grouping - verify grouping when there are grouped columns', () => { const fixture = TestBed.createComponent(ColumnGroupGroupingTestComponent); diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-api.service.ts b/projects/igniteui-angular/src/lib/grids/grid/grid-api.service.ts index 69bfde99546..750c39b43ce 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-api.service.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-api.service.ts @@ -137,4 +137,21 @@ export class IgxGridAPIService extends GridBaseAPIService { }); } + public get_groupBy_record_id(gRow: IGroupByRecord): string { + let recordId = '{ '; + const hierrarchy = DataUtil.getHierarchy(gRow); + + for (let i = 0; i < hierrarchy.length; i++) { + const groupByKey = hierrarchy[i]; + recordId += `'${groupByKey.fieldName}': '${groupByKey.value}'`; + + if (i < hierrarchy.length - 1) { + recordId += ', '; + } + } + recordId += ' }'; + + return recordId; + } + } diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-filtering.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid-filtering.spec.ts index 8691c133d0f..3b636ea814f 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-filtering.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-filtering.spec.ts @@ -10,6 +10,7 @@ import { IgxStringFilteringOperand, IgxNumberFilteringOperand, IgxBooleanFilteringOperand, IgxDateFilteringOperand, IgxFilteringOperand, FilteringExpressionsTree } from '../../../public_api'; import { configureTestSuite } from '../../test-utils/configure-suite'; import { IgxChipComponent } from '../../chips'; +import { HelperUtils } from '../../test-utils/helper-utils.spec'; const FILTERING_TOGGLE_CLASS = 'igx-filtering__toggle'; const FILTERING_TOGGLE_FILTERED_CLASS = 'igx-filtering__toggle--filtered'; @@ -574,10 +575,8 @@ describe('IgxGrid - Filtering actions', () => { expect(grid.rowList.length).toEqual(1); - const summariesReleaseDate = fix.debugElement.queryAll(By.css('.igx-grid-summary'))[0]; - const count = summariesReleaseDate.query(By.css('[title=\'Count\']')).nativeElement.nextSibling.innerText; - - expect(count).toBe('1'); + const summaryRow = fix.debugElement.query(By.css('igx-grid-summary-row')); + HelperUtils.verifyColumnSummaries(summaryRow, 0, ['Count'], ['1']); })); }); diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-summary.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid-summary.spec.ts index b84738bdf3c..a6423f65eec 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-summary.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-summary.spec.ts @@ -10,632 +10,839 @@ import { UIInteractions, wait } from '../../test-utils/ui-interactions.spec'; import { GridFunctions } from '../../test-utils/grid-functions.spec'; import { configureTestSuite } from '../../test-utils/configure-suite'; +import { + ProductsComponent, + VirtualSummaryColumnComponent, + SummaryColumnComponent, + FilteringComponent, + SummarieGroupByComponent +} from '../../test-utils/grid-samples.spec'; +import { HelperUtils } from '../../test-utils/helper-utils.spec'; +import { SampleTestData } from '../../test-utils/sample-test-data.spec'; +import { IgxNumberFilteringOperand, SortingDirection } from 'igniteui-angular'; +import { ColumnGroupFourLevelTestComponent } from './column-group.spec'; describe('IgxGrid - Summaries', () => { configureTestSuite(); const SUMMARY_CLASS = '.igx-grid-summary'; - const SUMMARY_LABEL_CLASS = '.igx-grid-summary__label'; - const SUMMARY_VALUE_CLASS = '.igx-grid-summary__result'; const ITEM_CLASS = 'igx-grid-summary__item'; - const HIDDEN_ITEM_CLASS = 'igx-grid-summary__item--inactive'; + const SUMMARY_ROW = 'igx-grid-summary-row'; + const SUMARRY_CELL = 'igx-grid-summary-cell'; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ - NoActiveSummariesComponent, + ProductsComponent, SummaryColumnComponent, CustomSummariesComponent, VirtualSummaryColumnComponent, - SummaryColumnsWithIdenticalWidthsComponent + SummaryColumnsWithIdenticalWidthsComponent, + FilteringComponent, + ColumnGroupFourLevelTestComponent, + SummarieGroupByComponent ], imports: [BrowserAnimationsModule, IgxGridModule.forRoot(), NoopAnimationsModule] }) .compileComponents(); })); - it('should not have summary if no summary is active ', () => { - const fixture = TestBed.createComponent(NoActiveSummariesComponent); - fixture.detectChanges(); - expect(fixture.debugElement.query(By.css(SUMMARY_CLASS))).toBeNull(); - }); - it('should enableSummaries through grid API ', () => { - const fixture = TestBed.createComponent(NoActiveSummariesComponent); - fixture.detectChanges(); - - const grid = fixture.componentInstance.grid1; - expect(grid.hasSummarizedColumns).toBe(false); - let tFoot = fixture.debugElement.query(By.css('.igx-grid__tfoot')).nativeElement.getBoundingClientRect().height; - expect(tFoot < grid.defaultRowHeight).toBe(true); - - grid.enableSummaries([{ fieldName: 'ProductName' }, { fieldName: 'ProductID' }]); - fixture.detectChanges(); - - const summaries = fixture.debugElement.queryAll(By.css('igx-grid-summary')); - let summaryLength = 0; - summaries.forEach((summary) => { - if (summary.children.length > 0) { - summaryLength++; - } + describe('Base tests: ', () => { + it('should not have summary if no summary is active ', () => { + const fixture = TestBed.createComponent(ProductsComponent); + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css(SUMMARY_CLASS))).toBeNull(); }); - expect(grid.hasSummarizedColumns).toBe(true); - - tFoot = fixture.debugElement.query(By.css('.igx-grid__tfoot')).nativeElement.getBoundingClientRect().height; - expect(summaryLength).toBe(2); - expect(grid.getColumnByName('ProductID').hasSummary).toBe(true); - expect(grid.getColumnByName('ProductName').hasSummary).toBe(true); - expect(grid.getColumnByName('OrderDate').hasSummary).toBe(false); - - const expectedLength = GridFunctions.calcMaxSummaryHeight(grid.columnList, summaries, grid.defaultRowHeight); - expect(tFoot >= expectedLength).toBe(true); - }); + it('should enableSummaries through grid API ', () => { + const fixture = TestBed.createComponent(ProductsComponent); + fixture.detectChanges(); - it('should disableSummaries through grid API ', () => { - const fixture = TestBed.createComponent(SummaryColumnComponent); - fixture.detectChanges(); + const grid = fixture.componentInstance.grid; + expect(grid.hasSummarizedColumns).toBe(false); + let tFoot = fixture.debugElement.query(By.css('.igx-grid__tfoot')).nativeElement.getBoundingClientRect().height; + expect(tFoot < grid.defaultRowHeight).toBe(true); - const grid = fixture.componentInstance.grid1; - const summariedColumns = []; - grid.columnList.forEach((col) => { - if (col.hasSummary) { - summariedColumns.push(col.field); - } - }); - grid.disableSummaries(summariedColumns); - fixture.detectChanges(); + grid.enableSummaries([{ fieldName: 'ProductName' }, { fieldName: 'ProductID' }]); + fixture.detectChanges(); - expect(fixture.debugElement.query(By.css(SUMMARY_CLASS))).toBeNull(); - expect(grid.hasSummarizedColumns).toBe(false); - }); - it('should have summary per each column that \'hasSummary\'= true', () => { - const fixture = TestBed.createComponent(SummaryColumnComponent); - fixture.detectChanges(); + const summaryRow = fixture.debugElement.query(By.css(SUMMARY_ROW)); + HelperUtils.verifyColumnSummaries(summaryRow, 0, ['Count'], ['10']); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['10']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 3, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 4, [], []); - expect(fixture.debugElement.query(By.css(SUMMARY_CLASS))).toBeDefined(); + expect(grid.getColumnByName('ProductID').hasSummary).toBe(true); + expect(grid.getColumnByName('ProductName').hasSummary).toBe(true); + expect(grid.getColumnByName('OrderDate').hasSummary).toBe(false); - let summaries = 0; - const summariedColumns = fixture.componentInstance.grid1.columnList.filter((col) => col.hasSummary === true).length; - summaries = fixture.debugElement.queryAll(By.css(SUMMARY_CLASS)).filter((summary) => summary.children.length > 0).length; - expect(summaries).toBe(summariedColumns); - }); - it('should have count summary for string and boolean data types', () => { - const fixture = TestBed.createComponent(SummaryColumnComponent); - fixture.detectChanges(); - - const sum = fixture.componentInstance.grid1; - const summaries = fixture.debugElement.queryAll(By.css('igx-grid-summary')); - - let index = 0; - sum.columnList.forEach((col) => { - if (col.hasSummary && (col.dataType === 'string' || col.dataType === 'boolean')) { - const labels = summaries[index].queryAll(By.css(SUMMARY_LABEL_CLASS)); - expect(labels.length).toBe(1); - expect(labels[0].nativeElement.innerText).toBe('Count'); - } - index++; - }); - }); - it('should have count, min, max, avg and sum summary for numeric data types', () => { - const fixture = TestBed.createComponent(SummaryColumnComponent); - fixture.detectChanges(); - - const sum = fixture.componentInstance.grid1; - const summaries = fixture.debugElement.queryAll(By.css('igx-grid-summary')); - - let index = 0; - sum.columnList.forEach((col) => { - if (col.hasSummary && (col.dataType === 'number')) { - const labels = summaries[index].queryAll(By.css(SUMMARY_LABEL_CLASS)); - expect(labels.length).toBe(5); - expect(labels[0].nativeElement.innerText).toBe('Count'); - expect(labels[1].nativeElement.innerText).toBe('Min'); - expect(labels[2].nativeElement.innerText).toBe('Max'); - expect(labels[3].nativeElement.innerText).toBe('Sum'); - expect(labels[4].nativeElement.innerText).toBe('Avg'); - } - index++; - }); - }); - it('should have count, earliest and latest summary for \'date\' data types', () => { - const fixture = TestBed.createComponent(SummaryColumnComponent); - fixture.detectChanges(); - - const sum = fixture.componentInstance.grid1; - const summaries = fixture.debugElement.queryAll(By.css('igx-grid-summary')); - - let index = 0; - sum.columnList.forEach((col) => { - if (col.hasSummary && (col.dataType === 'date')) { - const labels = summaries[index].queryAll(By.css(SUMMARY_LABEL_CLASS)); - expect(labels.length).toBe(3); - expect(labels[0].nativeElement.innerText).toBe('Count'); - expect(labels[1].nativeElement.innerText).toBe('Earliest'); - expect(labels[2].nativeElement.innerText).toBe('Latest'); - } - index++; - }); - }); - it('should summary function stay active when is clicked on it\'s label', () => { - const fixture = TestBed.createComponent(SummaryColumnComponent); - fixture.detectChanges(); - - const summary = fixture.debugElement.queryAll(By.css('igx-grid-summary'))[3]; - const min: DebugElement = summary.query(By.css('[title=\'Min\']')); - - expect(min.parent.nativeElement.classList.contains(ITEM_CLASS)).toBeTruthy(); - min.triggerEventHandler('click', null); - fixture.detectChanges(); - - expect(min.parent.nativeElement.classList.contains(ITEM_CLASS)).toBeTruthy(); - expect(summary.query(By.css('[title=\'Count\']')).parent.nativeElement.classList.contains(ITEM_CLASS)).toBeTruthy(); - expect(summary.query(By.css('[title=\'Max\']')).parent.nativeElement.classList.contains(ITEM_CLASS)).toBeTruthy(); - expect(summary.query(By.css('[title=\'Sum\']')).parent.nativeElement.classList.contains(ITEM_CLASS)).toBeTruthy(); - expect(summary.query(By.css('[title=\'Avg\']')).parent.nativeElement.classList.contains(ITEM_CLASS)).toBeTruthy(); - }); - it('should recalculate summary functions onRowAdded', () => { - const fixture = TestBed.createComponent(SummaryColumnComponent); - fixture.detectChanges(); - - const grid = fixture.componentInstance.grid1; - const summaries = fixture.debugElement.queryAll(By.css(SUMMARY_CLASS)); - - let countValue; - summaries.forEach((summary) => { - const countLabel = summary.query(By.css('[title=\'Count\']')); - if (countLabel) { - countValue = countLabel.nativeElement.nextSibling.innerText; - expect(+countValue).toBe(grid.rowList.length); - } + tFoot = fixture.debugElement.query(By.css('.igx-grid__tfoot')).nativeElement.getBoundingClientRect().height; + expect(tFoot).toEqual(grid.defaultRowHeight + 1); }); - grid.addRow({ - ProductID: 11, ProductName: 'Belgian Chocolate', InStock: true, UnitsInStock: 99000, OrderDate: new Date('2018-03-01') - }); - fixture.detectChanges(); - - let updatedValue; - summaries.forEach((summary) => { - const countLabel = summary.query(By.css('[title=\'Count\']')); - if (countLabel) { - updatedValue = countLabel.nativeElement.nextSibling.innerText; - expect(+updatedValue).toBe(grid.rowList.length); - } - }); - }); - it('should recalculate summary functions onRowDeleted', () => { - const fixture = TestBed.createComponent(SummaryColumnComponent); - fixture.detectChanges(); - - const grid = fixture.componentInstance.grid1; - const summaries = fixture.debugElement.queryAll(By.css(SUMMARY_CLASS)); - - let countValue; - summaries.forEach((summary) => { - const countLabel = summary.query(By.css('[title=\'Count\']')); - if (countLabel) { - countValue = countLabel.nativeElement.nextSibling.innerText; - expect(+countValue).toBe(grid.rowList.length); - } + it(`should recalculate grid sizes correctly when the column is outside of the viewport`, (async () => { + const fixture = TestBed.createComponent(ProductsComponent); + fixture.detectChanges(); + + const grid = fixture.componentInstance.grid; + grid.width = '300px'; + await wait(100); + fixture.detectChanges(); + + grid.getColumnByName('UnitsInStock').hasSummary = true; + await wait(30); + fixture.detectChanges(); + + const tFoot = fixture.debugElement.query(By.css('.igx-grid__tfoot')).nativeElement.getBoundingClientRect().height; + expect(tFoot).toEqual(5 * grid.defaultRowHeight + 1); + expect(fixture.debugElement.query(By.css(SUMMARY_CLASS))).toBeDefined(); + })); + + xit('should have correct summaries when there are null and undefined values', fakeAsync(() => { + const fixture = TestBed.createComponent(FilteringComponent); + fixture.detectChanges(); + + const grid = fixture.componentInstance.grid; + grid.getColumnByName('ProductName').hasSummary = true; + grid.getColumnByName('Downloads').hasSummary = true; + grid.getColumnByName('Released').hasSummary = true; + grid.getColumnByName('ReleaseDate').hasSummary = true; + tick(100); + fixture.detectChanges(); + + const summaryRow = fixture.debugElement.query(By.css(SUMMARY_ROW)); + HelperUtils.verifyColumnSummaries(summaryRow, 0, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['8']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['8', '1', '1,000', '2,204', '275.5']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count'], ['8']); + const options = { year: 'numeric', month: 'short', day: 'numeric' }; + const earliest = SampleTestData.timeGenerator.timedelta(SampleTestData.today, 'month', -1).toLocaleString('us', options); + const latest = SampleTestData.timeGenerator.timedelta(SampleTestData.today, 'month', 1).toLocaleString('us', options); + HelperUtils.verifyColumnSummaries(summaryRow, 4, ['Count', 'Earliest', 'Latest'], ['8', earliest, latest]); + })); + + it('should properly render custom summaries', () => { + const fixture = TestBed.createComponent(CustomSummariesComponent); + const gridComp = fixture.componentInstance.grid1; + fixture.detectChanges(); + + const summaryRow = fixture.debugElement.query(By.css(SUMMARY_ROW)); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Sum', 'Avg'], ['10', '39,004', '3,900.4']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, ['Earliest'], ['5/17/1990']); + + gridComp.filter('UnitsInStock', '0', IgxNumberFilteringOperand.instance().condition('lessThan'), true); + fixture.detectChanges(); + + const filterResult = gridComp.rowList.length; + expect(filterResult).toEqual(0); + + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Sum', 'Avg'], ['0', '', '']); }); - grid.deleteRow(1); - fixture.detectChanges(); + it(`Should update summary section when the column is outside of the + viewport and have identical width with others`, (async () => { + const fixture = TestBed.createComponent(ProductsComponent); + fixture.detectChanges(); + + const grid = fixture.componentInstance.grid; + grid.getColumnByName('UnitsInStock').hasSummary = true; + grid.width = '300px'; + await wait(100); + fixture.detectChanges(); + + grid.addRow({ + ProductID: 11, ProductName: 'Belgian Chocolate', InStock: true, UnitsInStock: 99000, OrderDate: new Date('2018-03-01') + }); + await wait(30); + fixture.detectChanges(); + GridFunctions.scrollLeft(grid, 600); + await wait(30); + fixture.detectChanges(); + + const summaryRow = fixture.debugElement.query(By.css(SUMMARY_ROW)); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Min', 'Max', 'Sum', 'Avg'], + ['11', '0', '99,000', '138,004', '12,545.818']); + })); + + xit('When we have data which is undefined and enable summary per defined column, error should not be thrown', () => { + const fixture = TestBed.createComponent(ProductsComponent); + fixture.detectChanges(); + + const grid = fixture.componentInstance.grid; + const idColumn = grid.getColumnByName('ProductID'); + expect(grid.data.length > 0).toEqual(true); + + fixture.componentInstance.data = undefined; + fixture.detectChanges(); + + expect(grid.data).toEqual(undefined); + expect(() => { + grid.enableSummaries(idColumn.field); + fixture.detectChanges(); + }).not.toThrow(); + }); - let updatedValue; - summaries.forEach((summary) => { - const countLabel = summary.query(By.css('[title=\'Count\']')); - if (countLabel) { - updatedValue = countLabel.nativeElement.nextSibling.innerText; - expect(+updatedValue).toBe(grid.rowList.length); + xit('should change custom summaries at runtime', fakeAsync(() => { + const fixture = TestBed.createComponent(CustomSummariesComponent); + const grid = fixture.componentInstance.grid1; + fixture.detectChanges(); + + const summaryRow = fixture.debugElement.query(By.css(SUMMARY_ROW)); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Sum', 'Avg'], ['10', '39,004', '3,900.4']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, ['Earliest'], ['5/17/1990']); + grid.getColumnByName('UnitsInStock').summaries = fixture.componentInstance.dealsSummaryMinMax; + tick(100); + fixture.detectChanges(); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Min', 'Max'], ['0', '20,000']); + })); + + it('should render correct data after hiding one bigger and then one smaller summary when scrolled to the bottom', (async () => { + const fixture = TestBed.createComponent(VirtualSummaryColumnComponent); + fixture.detectChanges(); + + const grid = fixture.componentInstance.grid; + let rowsRendered; + let tbody; + let expectedRowLenght; + let firstCellsText; + + fixture.componentInstance.scrollTop(10000); + await wait(100); + fixture.detectChanges(); + + rowsRendered = fixture.nativeElement.querySelectorAll('igx-grid-row'); + tbody = grid.nativeElement.querySelector('.igx-grid__tbody').getBoundingClientRect().height; + expectedRowLenght = Math.ceil(parseFloat(tbody) / grid.defaultRowHeight) + 1; + expect(rowsRendered.length).toEqual(expectedRowLenght); + + grid.disableSummaries(['ProductName', 'InStock', 'UnitsInStock']); + await wait(50); + fixture.detectChanges(); + + rowsRendered = Array.from(fixture.nativeElement.querySelectorAll('igx-grid-row')); + tbody = grid.nativeElement.querySelector('.igx-grid__tbody').getBoundingClientRect().height; + expectedRowLenght = Math.ceil(parseFloat(tbody) / grid.defaultRowHeight) + 1; + + firstCellsText = rowsRendered.map((item) => { + return item.querySelectorAll('igx-grid-cell')[0].textContent.trim(); + }); + expect(rowsRendered.length).toEqual(expectedRowLenght); + let expectedFirstCellNum = grid.data.length - expectedRowLenght + 1; + + for (let i = 0; i < rowsRendered.length - 1; i++) { + expect(firstCellsText[i]).toEqual((expectedFirstCellNum + i).toString()); } - }); - }); - it('should recalculate summary functions on updateRow', () => { - const fixture = TestBed.createComponent(SummaryColumnComponent); - fixture.detectChanges(); - - const grid = fixture.componentInstance.grid1; - const summaries = fixture.debugElement.queryAll(By.css(SUMMARY_CLASS)); - - const productNameCell = grid.getCellByColumn(0, 'ProductName'); - const unitsInStockCell = grid.getCellByColumn(0, 'UnitsInStock'); - let countValue; - summaries.forEach((summary) => { - const countLabel = summary.query(By.css('[title=\'Count\']')); - if (countLabel) { - countValue = countLabel.nativeElement.nextSibling.innerText; - expect(+countValue).toBe(grid.rowList.length); + + grid.disableSummaries(['OrderDate']); + await wait(50); + fixture.detectChanges(); + + rowsRendered = Array.from(fixture.nativeElement.querySelectorAll('igx-grid-row')); + tbody = grid.nativeElement.querySelector('.igx-grid__tbody').getBoundingClientRect().height; + expectedRowLenght = Math.ceil(parseFloat(tbody) / grid.defaultRowHeight) + 1; + + firstCellsText = rowsRendered.map((item) => { + return item.querySelectorAll('igx-grid-cell')[0].textContent.trim(); + }); + expect(rowsRendered.length).toEqual(expectedRowLenght); + expectedFirstCellNum = grid.data.length - expectedRowLenght + 1; + for (let i = 0; i < rowsRendered.length - 1; i++) { + expect(firstCellsText[i]).toEqual((expectedFirstCellNum + i).toString()); } + })); + + describe('', () => { + let fix; + let grid: IgxGridComponent; + beforeEach(() => { + fix = TestBed.createComponent(SummaryColumnComponent); + fix.detectChanges(); + grid = fix.componentInstance.grid; + }); + + it('should disableSummaries through grid API ', () => { + const summariedColumns = []; + grid.columnList.forEach((col) => { + if (col.hasSummary) { + summariedColumns.push(col.field); + } + }); + grid.disableSummaries(summariedColumns); + fix.detectChanges(); + + expect(fix.debugElement.query(By.css(SUMMARY_CLASS))).toBeNull(); + expect(grid.hasSummarizedColumns).toBe(false); + }); + + it('should change summary operand through grid API ', (async () => { + grid.enableSummaries([{ fieldName: 'UnitsInStock', customSummary: fix.componentInstance.dealsSummaryMinMax }]); + // grid.recalculateSummaries(); + await wait(200); + fix.detectChanges(); + + const summaryRow = fix.debugElement.query(By.css(SUMMARY_ROW)); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Min', 'Max'], ['0', '20,000']); + })); + + it('should have summary per each column that \'hasSummary\'= true', () => { + const summaryRow = fix.debugElement.query(By.css(SUMMARY_ROW)); + HelperUtils.verifyColumnSummaries(summaryRow, 0, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], []); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count'], []); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Min', 'Max', 'Sum', 'Avg'], []); + HelperUtils.verifyColumnSummaries(summaryRow, 4, ['Count', 'Earliest', 'Latest'], []); + }); + + it('should have count summary for string and boolean data types', () => { + const summaryRow = fix.debugElement.query(By.css(SUMMARY_ROW)); + grid.columnList.forEach((col) => { + if (col.hasSummary && (col.dataType === 'string' || col.dataType === 'boolean')) { + HelperUtils.verifyColumnSummaries(summaryRow, col.visibleIndex, ['Count'], []); + } + }); + }); + + it('should have count, min, max, avg and sum summary for numeric data types', () => { + const summaryRow = fix.debugElement.query(By.css(SUMMARY_ROW)); + grid.columnList.forEach((col) => { + if (col.hasSummary && (col.dataType === 'number')) { + HelperUtils.verifyColumnSummaries(summaryRow, col.visibleIndex, ['Count', 'Min', 'Max', 'Sum', 'Avg'], []); + } + }); + }); + + it('should have count, earliest and latest summary for \'date\' data types', () => { + const summaryRow = fix.debugElement.query(By.css(SUMMARY_ROW)); + grid.columnList.forEach((col) => { + if (col.hasSummary && (col.dataType === 'date')) { + HelperUtils.verifyColumnSummaries(summaryRow, col.visibleIndex, ['Count', 'Earliest', 'Latest'], []); + } + }); + }); + + it('should summary function stay active when is clicked on it\'s label', () => { + const summary = fix.debugElement.queryAll(By.css('igx-grid-summary-cell'))[3]; + const min: DebugElement = summary.query(By.css('[title=\'Min\']')); + + expect(min.parent.nativeElement.classList.contains(ITEM_CLASS)).toBeTruthy(); + min.triggerEventHandler('click', null); + fix.detectChanges(); + + expect(min.parent.nativeElement.classList.contains(ITEM_CLASS)).toBeTruthy(); + expect(summary.query(By.css('[title=\'Count\']')).parent.nativeElement.classList.contains(ITEM_CLASS)).toBeTruthy(); + expect(summary.query(By.css('[title=\'Max\']')).parent.nativeElement.classList.contains(ITEM_CLASS)).toBeTruthy(); + expect(summary.query(By.css('[title=\'Sum\']')).parent.nativeElement.classList.contains(ITEM_CLASS)).toBeTruthy(); + expect(summary.query(By.css('[title=\'Avg\']')).parent.nativeElement.classList.contains(ITEM_CLASS)).toBeTruthy(); + }); + + it('should calculate summaries for \'number\' dataType or return if no data is provided', () => { + const summaryClass = fix.componentInstance.numberSummary; + + const summaries = summaryClass.operate(fix.componentInstance.data.map((x) => x['UnitsInStock'])); + expect(summaries[0].summaryResult).toBe(10); + expect(summaries[1].summaryResult).toBe(0); + expect(summaries[2].summaryResult).toBe(20000); + expect(summaries[3].summaryResult).toBe(39004); + expect(summaries[4].summaryResult).toBe(3900.4); + + const emptySummaries = summaryClass.operate(); + expect(emptySummaries[0].summaryResult).toBe(0); + expect(typeof emptySummaries[1].summaryResult).not.toEqual(undefined); + expect(typeof emptySummaries[2].summaryResult).not.toEqual(undefined); + expect(typeof emptySummaries[3].summaryResult).not.toEqual(undefined); + expect(typeof emptySummaries[4].summaryResult).not.toEqual(undefined); + + expect(typeof emptySummaries[1].summaryResult).not.toEqual(null); + expect(typeof emptySummaries[2].summaryResult).not.toEqual(null); + expect(typeof emptySummaries[3].summaryResult).not.toEqual(null); + expect(typeof emptySummaries[4].summaryResult).not.toEqual(null); + + expect(emptySummaries[1].summaryResult.length === 0).toBeTruthy(); + expect(emptySummaries[2].summaryResult.length === 0).toBeTruthy(); + expect(emptySummaries[3].summaryResult.length === 0).toBeTruthy(); + expect(emptySummaries[4].summaryResult.length === 0).toBeTruthy(); + }); + + + it('should calculate summaries for \'date\' dataType or return if no data is provided', () => { + const summaryClass = fix.componentInstance.dateSummary; + + const summaries = summaryClass.operate(fix.componentInstance.data.map((x) => x['OrderDate'])); + expect(summaries[0].summaryResult).toBe(10); + expect(summaries[1].summaryResult.toLocaleDateString()).toBe('5/17/1990'); + expect(summaries[2].summaryResult.toLocaleDateString()).toBe('12/25/2025'); + + const emptySummaries = summaryClass.operate([]); + expect(emptySummaries[0].summaryResult).toBe(0); + expect(emptySummaries[1].summaryResult).toBe(undefined); + expect(emptySummaries[2].summaryResult).toBe(undefined); + }); + + it('should calc tfoot height according number of summary functions', () => { + const summaries = fix.debugElement.queryAll(By.css('igx-grid-summary-cell')); + const footerRow = fix.debugElement.query(By.css('.igx-grid__tfoot')).query(By.css('.igx-grid__summaries')) + .nativeElement.getBoundingClientRect().height; + const tfootSize = +footerRow; + + const expectedHeight = GridFunctions.calcMaxSummaryHeight(grid.columnList, summaries, grid.defaultRowHeight); + + expect(tfootSize).toBe(expectedHeight); + }); + + it('should be able to change \'hasSummary\' property runtime and to recalculate grid sizes correctly', fakeAsync(() => { + grid.columnList.forEach((col) => { + if (col.field !== 'ProductID') { + expect(grid.getColumnByName(col.field).hasSummary).toBe(true); + } + }); + grid.getColumnByName('UnitsInStock').hasSummary = false; + tick(100); + fix.detectChanges(); + + expect(grid.getColumnByName('UnitsInStock').hasSummary).toBe(false); + + const summaries = fix.debugElement.queryAll(By.css(SUMARRY_CELL)).filter((el) => + el.nativeElement.classList.contains('igx-grid-summary--empty') === false); + const tfootSize = +fix.debugElement.query(By.css('.igx-grid__summaries')) + .nativeElement.getBoundingClientRect().height; + const expectedHeight = GridFunctions.calcMaxSummaryHeight(grid.columnList, summaries, grid.defaultRowHeight); + expect(tfootSize).toBe(expectedHeight); + + grid.getColumnByName('ProductName').hasSummary = false; + grid.getColumnByName('InStock').hasSummary = false; + grid.getColumnByName('OrderDate').hasSummary = false; + fix.detectChanges(); + tick(100); + expect(fix.debugElement.query(By.css('.igx-grid__summaries'))).toBeNull(); + expect(grid.hasSummarizedColumns).toBe(false); + })); }); - - expect(productNameCell.value).toBe('Chai'); - expect(unitsInStockCell.value).toBe(2760); - - grid.updateRow({ - ProductID: 1, ProductName: 'Spearmint', InStock: true, UnitsInStock: 1, OrderDate: new Date('2005-03-21') - }, 1); - fixture.detectChanges(); - - expect(+countValue).toBe(grid.rowList.length); - expect(productNameCell.value).toBe('Spearmint'); - expect(unitsInStockCell.value).toBe(1); }); - it('should recalculate summary functions on cell update', () => { - const fixture = TestBed.createComponent(SummaryColumnComponent); - fixture.detectChanges(); - const oldMaxValue = '20,000'; - const newMaxValue = '99,000'; - const grid = fixture.componentInstance.grid1; - const summariesUnitOfStock = fixture.debugElement.queryAll(By.css(SUMMARY_CLASS))[3]; - const unitsInStockCell = grid.getCellByColumn(0, 'UnitsInStock'); + describe('Integration Scenarious: ', () => { + describe('', () => { + let fix; + let grid: IgxGridComponent; + beforeEach(() => { + fix = TestBed.createComponent(SummaryColumnComponent); + fix.detectChanges(); + grid = fix.componentInstance.grid; + }); + + it('Filtering: should calculate summaries only over filteredData', () => { + grid.filter('UnitsInStock', 0, IgxNumberFilteringOperand.instance().condition('equals'), true); + fix.detectChanges(); + + let filterResult = grid.rowList.length; + expect(filterResult).toEqual(3); + + let summaryRow = fix.debugElement.query(By.css(SUMMARY_ROW)); + HelperUtils.verifyColumnSummaries(summaryRow, 0, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['3']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count'], ['3']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['3', '0', '0', '0', '0']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, ['Count', 'Earliest', 'Latest'], ['3', 'Jul 27, 2001', 'Oct 11, 2007']); + + grid.filter('ProductID', 0, IgxNumberFilteringOperand.instance().condition('equals'), true); + fix.detectChanges(); + + filterResult = grid.rowList.length; + expect(filterResult).toEqual(0); + + summaryRow = fix.debugElement.query(By.css(SUMMARY_ROW)); + HelperUtils.verifyColumnSummaries(summaryRow, 0, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['0']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count'], ['0']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['0', '', '', '', '']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, ['Count', 'Earliest', 'Latest'], ['0', '', '']); + + grid.clearFilter(); + fix.detectChanges(); + + filterResult = grid.rowList.length; + expect(filterResult).toEqual(10); + + summaryRow = fix.debugElement.query(By.css(SUMMARY_ROW)); + HelperUtils.verifyColumnSummaries(summaryRow, 0, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['10']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count'], ['10']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, + ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['10', '0', '20,000', '39,004', '3,900.4']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, + ['Count', 'Earliest', 'Latest'], ['10', 'May 17, 1990', 'Dec 25, 2025']); + }); + + it('Moving: should move summaries when move colomn', () => { + const colUnitsInStock = grid.getColumnByName('UnitsInStock'); + const colProductID = grid.getColumnByName('ProductID'); + colUnitsInStock.movable = true; + fix.detectChanges(); + + grid.moveColumn(colUnitsInStock, colProductID); + fix.detectChanges(); + + const summaryRow = fix.debugElement.query(By.css(SUMMARY_ROW)); + HelperUtils.verifyColumnSummaries(summaryRow, 0, + ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['10', '0', '20,000', '39,004', '3,900.4']); + HelperUtils.verifyColumnSummaries(summaryRow, 1, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count'], ['10']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count'], ['10']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, + ['Count', 'Earliest', 'Latest'], ['10', 'May 17, 1990', 'Dec 25, 2025']); + }); + + it('Hiding: should hide summary row when a column which has summary is hidded', fakeAsync(() => { + grid.getColumnByName('ProductName').hasSummary = false; + grid.getColumnByName('InStock').hasSummary = false; + grid.getColumnByName('OrderDate').hasSummary = false; + // grid.recalculateSummaries(); + fix.detectChanges(); + tick(100); + + let summaryRow = fix.debugElement.query(By.css(SUMMARY_ROW)); + HelperUtils.verifyColumnSummaries(summaryRow, 0, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 1, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 2, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 3, + ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['10', '0', '20,000', '39,004', '3,900.4']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, [], []); + + grid.getColumnByName('UnitsInStock').hidden = true; + tick(); + fix.detectChanges(); + + + let summaryArea = fix.debugElement.query(By.css('.igx-grid__summaries')); + expect(summaryArea).toBeNull(); + expect(grid.hasSummarizedColumns).toBe(false); + + grid.getColumnByName('UnitsInStock').hidden = false; + tick(); + fix.detectChanges(); + summaryArea = fix.debugElement.query(By.css('.igx-grid__summaries')); + expect(summaryArea).toBeDefined(); + expect(grid.hasSummarizedColumns).toBe(true); + summaryRow = fix.debugElement.query(By.css(SUMMARY_ROW)); + HelperUtils.verifyColumnSummaries(summaryRow, 0, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 1, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 2, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 3, + ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['10', '0', '20,000', '39,004', '3,900.4']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, [], []); + })); + + it('Hiding: should recalculate summary area after column with enabled summary is hidden', fakeAsync(() => { + let tFoot = fix.debugElement.query(By.css('.igx-grid__tfoot')).nativeElement.getBoundingClientRect().height; + expect(tFoot).toEqual(5 * grid.defaultRowHeight + 1); + + grid.getColumnByName('UnitsInStock').hidden = true; + tick(); + fix.detectChanges(); + + tFoot = fix.debugElement.query(By.css('.igx-grid__tfoot')).nativeElement.getBoundingClientRect().height; + expect(tFoot).toEqual(3 * grid.defaultRowHeight + 1); + expect(grid.hasSummarizedColumns).toBe(true); + + let summaryRow = fix.debugElement.query(By.css(SUMMARY_ROW)); + HelperUtils.verifyColumnSummaries(summaryRow, 0, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['10']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count'], ['10']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Earliest', 'Latest'], ['10', 'May 17, 1990', 'Dec 25, 2025']); + + grid.getColumnByName('UnitsInStock').hidden = false; + tick(); + fix.detectChanges(); + + expect(grid.hasSummarizedColumns).toBe(true); + tFoot = fix.debugElement.query(By.css('.igx-grid__tfoot')).nativeElement.getBoundingClientRect().height; + expect(tFoot).toEqual(5 * grid.defaultRowHeight + 1); + summaryRow = fix.debugElement.query(By.css(SUMMARY_ROW)); + HelperUtils.verifyColumnSummaries(summaryRow, 0, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['10']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count'], ['10']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, + ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['10', '0', '20,000', '39,004', '3,900.4']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, + ['Count', 'Earliest', 'Latest'], ['10', 'May 17, 1990', 'Dec 25, 2025']); + })); + + it('CRUD: should recalculate summary functions onRowAdded', () => { + grid.addRow({ + ProductID: 11, ProductName: 'Belgian Chocolate', InStock: true, UnitsInStock: 99000, OrderDate: new Date('2018-03-01') + }); + fix.detectChanges(); + + const summaryRow = fix.debugElement.query(By.css(SUMMARY_ROW)); + HelperUtils.verifyColumnSummaries(summaryRow, 0, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['11']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count'], ['11']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, + ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['11', '0', '99,000', '138,004', '12,545.818']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, + ['Count', 'Earliest', 'Latest'], ['11', 'May 17, 1990', 'Dec 25, 2025']); + }); + + it('CRUD: should recalculate summary functions onRowDeleted', () => { + grid.deleteRow(9); + fix.detectChanges(); + + const summaryRow = fix.debugElement.query(By.css(SUMMARY_ROW)); + HelperUtils.verifyColumnSummaries(summaryRow, 0, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['9']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count'], ['9']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, + ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['9', '0', '20,000', '32,006', '3,556.222']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, + ['Count', 'Earliest', 'Latest'], ['9', 'May 17, 1990', 'Mar 1, 2018']); + }); + + it('CRUD: should recalculate summary functions on updateRow', () => { + const productNameCell = grid.getCellByColumn(0, 'ProductName'); + const unitsInStockCell = grid.getCellByColumn(0, 'UnitsInStock'); + + expect(productNameCell.value).toBe('Chai'); + expect(unitsInStockCell.value).toBe(2760); + + grid.updateRow({ + ProductID: 1, ProductName: 'Spearmint', InStock: true, UnitsInStock: 510000, OrderDate: new Date('1984-03-21') + }, 1); + fix.detectChanges(); + + expect(productNameCell.value).toBe('Spearmint'); + expect(unitsInStockCell.value).toBe(510000); + + const summaryRow = fix.debugElement.query(By.css(SUMMARY_ROW)); + HelperUtils.verifyColumnSummaries(summaryRow, 0, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['10']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count'], ['10']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, + ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['10', '0', '510,000', '546,244', '54,624.4']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, + ['Count', 'Earliest', 'Latest'], ['10', 'Mar 21, 1984', 'Dec 25, 2025']); + }); + + it('CRUD: should recalculate summary functions on cell update', () => { + const unitsInStockCell = grid.getCellByColumn(0, 'UnitsInStock'); + unitsInStockCell.update(99000); + fix.detectChanges(); + + let summaryRow = fix.debugElement.query(By.css(SUMMARY_ROW)); + HelperUtils.verifyColumnSummaries(summaryRow, 3, + ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['10', '0', '99,000', '135,244', '13,524.4']); + + unitsInStockCell.update(-12); + fix.detectChanges(); + summaryRow = fix.debugElement.query(By.css(SUMMARY_ROW)); + HelperUtils.verifyColumnSummaries(summaryRow, 3, + ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['10', '-12', '20,000', '36,232', '3,623.2']); + }); + + it('Pinning: should display all active summaries after column pinning', () => { + grid.pinColumn('UnitsInStock'); + grid.pinColumn('ProductID'); + fix.detectChanges(); + + const summaryRow = fix.debugElement.query(By.css(SUMMARY_ROW)); + HelperUtils.verifyColumnSummaries(summaryRow, 0, + ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['10', '0', '20,000', '39,004', '3,900.4']); + HelperUtils.verifyColumnSummaries(summaryRow, 1, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count'], ['10']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count'], ['10']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, + ['Count', 'Earliest', 'Latest'], ['10', 'May 17, 1990', 'Dec 25, 2025']); + }); + }); - let maxValue = summariesUnitOfStock.query(By.css('[title=\'Max\']')).nativeElement.nextSibling.innerText; - expect(maxValue).toBe(oldMaxValue); - unitsInStockCell.update(99000); - fixture.detectChanges(); + it('MCH - verify summaries when there are grouped columns', (async () => { + const fixture = TestBed.createComponent(ColumnGroupFourLevelTestComponent); + fixture.detectChanges(); + const grid = fixture.componentInstance.grid; - maxValue = summariesUnitOfStock.query(By.css('[title=\'Max\']')).nativeElement.nextSibling.innerText; - expect(maxValue).toBe(newMaxValue); + // Verify columns and groups + expect(document.querySelectorAll('igx-grid-header-group').length).toEqual(18); + expect(document.querySelectorAll('.igx-grid__th').length).toEqual(11); + const allColumns = grid.columnList; + allColumns.forEach((col) => { + if (!col.columnGroup) { + col.hasSummary = true; + } + }); + await wait(); + fixture.detectChanges(); + + const summaryRow = fixture.debugElement.query(By.css(SUMMARY_ROW)); + HelperUtils.verifyColumnSummaries(summaryRow, 0, ['Count'], ['27']); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['27']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count'], ['27']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count'], ['27']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, ['Count'], ['27']); + HelperUtils.verifyColumnSummaries(summaryRow, 5, ['Count'], ['27']); + HelperUtils.verifyColumnSummaries(summaryRow, 6, ['Count'], ['27']); + })); }); - it('should display all active summaries after column pinning', () => { - const fixture = TestBed.createComponent(SummaryColumnComponent); - fixture.detectChanges(); - const grid = fixture.componentInstance.grid1; - const summariedColumns = grid.columnList.filter((col) => col.hasSummary === true).length; - let displayedSummaries = fixture.debugElement.queryAll(By.css(SUMMARY_CLASS)) - .filter((summary) => summary.children.length > 0).length; - expect(displayedSummaries).toBe(summariedColumns); - - grid.pinColumn('UnitsInStock'); - grid.pinColumn('ProductID'); - fixture.detectChanges(); - - displayedSummaries = fixture.debugElement.queryAll(By.css(SUMMARY_CLASS)).filter((summary) => summary.children.length > 0).length; - expect(displayedSummaries).toBe(summariedColumns); + describe('Grouping tests: ', () => { + let fix; + let grid; + beforeEach(() => { + fix = TestBed.createComponent(SummarieGroupByComponent); + fix.detectChanges(); + grid = fix.componentInstance.grid; + grid.groupBy({ + fieldName: 'ParentID', dir: SortingDirection.Asc, ignoreCase: false + }); - }); - it('should calc tfoot height according number of summary functions', () => { - const fixture = TestBed.createComponent(SummaryColumnComponent); - fixture.detectChanges(); - const grid = fixture.componentInstance.grid1; - const summaries = fixture.debugElement.queryAll(By.css('igx-grid-summary')); - const footerRow = fixture.debugElement.query(By.css('.igx-grid__tfoot')).query(By.css('.igx-grid__summaries')) - .nativeElement.getBoundingClientRect().height; - const tfootSize = +footerRow; - - const expectedHeight = GridFunctions.calcMaxSummaryHeight(grid.columnList, summaries, grid.defaultRowHeight); - - expect(tfootSize).toBe(expectedHeight); - }); - it('should calculate summaries for \'number\' dataType or return if no data is provided', () => { - const fixture = TestBed.createComponent(SummaryColumnComponent); - fixture.detectChanges(); - - const grid = fixture.componentInstance.grid1; - const summaryClass = fixture.componentInstance.numberSummary; - - const summaries = summaryClass.operate(fixture.componentInstance.data.map((x) => x['UnitsInStock'])); - expect(summaries[0].summaryResult).toBe(10); - expect(summaries[1].summaryResult).toBe(0); - expect(summaries[2].summaryResult).toBe(20000); - expect(summaries[3].summaryResult).toBe(39004); - expect(summaries[4].summaryResult).toBe(3900.4); - - const emptySummaries = summaryClass.operate(); - expect(emptySummaries[0].summaryResult).toBe(0); - expect(typeof emptySummaries[1].summaryResult).not.toEqual(undefined); - expect(typeof emptySummaries[2].summaryResult).not.toEqual(undefined); - expect(typeof emptySummaries[3].summaryResult).not.toEqual(undefined); - expect(typeof emptySummaries[4].summaryResult).not.toEqual(undefined); - - expect(typeof emptySummaries[1].summaryResult).not.toEqual(null); - expect(typeof emptySummaries[2].summaryResult).not.toEqual(null); - expect(typeof emptySummaries[3].summaryResult).not.toEqual(null); - expect(typeof emptySummaries[4].summaryResult).not.toEqual(null); - - expect(emptySummaries[1].summaryResult.length === 0).toBeTruthy(); - expect(emptySummaries[2].summaryResult.length === 0).toBeTruthy(); - expect(emptySummaries[3].summaryResult.length === 0).toBeTruthy(); - expect(emptySummaries[4].summaryResult.length === 0).toBeTruthy(); - }); - it('should calculate summaries for \'date\' dataType or return if no data is provided', () => { - const fixture = TestBed.createComponent(SummaryColumnComponent); - fixture.detectChanges(); - - const grid = fixture.componentInstance.grid1; - const summaryClass = fixture.componentInstance.dateSummary; - - const summaries = summaryClass.operate(fixture.componentInstance.data.map((x) => x['OrderDate'])); - expect(summaries[0].summaryResult).toBe(10); - expect(summaries[1].summaryResult.toLocaleDateString()).toBe('5/17/1990'); - expect(summaries[2].summaryResult.toLocaleDateString()).toBe('12/25/2025'); - - const emptySummaries = summaryClass.operate([]); - expect(emptySummaries[0].summaryResult).toBe(0); - expect(emptySummaries[1].summaryResult).toBe(undefined); - expect(emptySummaries[2].summaryResult).toBe(undefined); - }); - it('should calculate summaries only over filteredData', fakeAsync(() => { - const fixture = TestBed.createComponent(SummaryColumnComponent); - fixture.detectChanges(); - - const grid = fixture.componentInstance.grid1; - const summaries = fixture.debugElement.queryAll(By.css('igx-grid-summary')); - const colChips = GridFunctions.getFilterChipsForColumn('UnitsInStock', fixture); - - colChips[0].nativeElement.click(); - fixture.detectChanges(); - - GridFunctions.filterBy('Equals', '0', fixture); - fixture.detectChanges(); - - GridFunctions.closeFilterRow(fixture); - fixture.detectChanges(); - - const filterResult = grid.rowList.length; - expect(filterResult).toEqual(3); - let index = 0; - grid.columnList.forEach((col) => { - if (col.hasSummary) { - const values = summaries[index].queryAll(By.css(SUMMARY_VALUE_CLASS)); - expect(+values[0].nativeElement.innerText).toBe(filterResult); - if (col.field === 'UnitsInStock') { - expect(values[1].nativeElement.innerText).toBe('0'); - expect(values[2].nativeElement.innerText).toBe('0'); - } - } - index++; + fix.detectChanges(); }); - })); - - it('When we have data which is undefined and enable summary per defined column, error should not be thrown', () => { - const fix = TestBed.createComponent(NoActiveSummariesComponent); - fix.detectChanges(); - - const grid = fix.componentInstance.grid1; - const idColumn = grid.getColumnByName('ProductID'); - expect(grid.data.length > 0).toEqual(true); - - fix.componentInstance.data = undefined; - fix.detectChanges(); - - expect(grid.data).toEqual(undefined); - expect(() => { - grid.enableSummaries(idColumn.field); + it('should render correct summaries when there is grouped colomn', () => { + verifyBaseSummaries(fix); + verifySummariesForParentID17(fix, 3); + const groupRows = grid.groupsRowList.toArray(); + groupRows[0].toggle(); fix.detectChanges(); - }).not.toThrow(); - }); - - it('should render correct data after hiding all summaries when scrolled to the bottom', (async () => { - const fixture = TestBed.createComponent(VirtualSummaryColumnComponent); - fixture.detectChanges(); - - const grid = fixture.componentInstance.grid1; - const summariedColumns = ['ProductName', 'InStock', 'UnitsInStock', 'OrderDate']; - let rowsRendered; - let tbody; - let expectedRowLenght; - - fixture.componentInstance.scrollTop(10000); - await wait(200); - fixture.detectChanges(); - - rowsRendered = fixture.nativeElement.querySelectorAll('igx-grid-row'); - tbody = grid.nativeElement.querySelector('.igx-grid__tbody').getBoundingClientRect().height; - expectedRowLenght = Math.ceil(parseFloat(tbody) / grid.defaultRowHeight) + 1; - - expect(rowsRendered.length).toEqual(expectedRowLenght); - grid.disableSummaries(summariedColumns); - fixture.detectChanges(); - await wait(50); - fixture.detectChanges(); - - rowsRendered = Array.from(fixture.nativeElement.querySelectorAll('igx-grid-row')); - tbody = grid.nativeElement.querySelector('.igx-grid__tbody').getBoundingClientRect().height; - expectedRowLenght = Math.ceil(parseFloat(tbody) / grid.defaultRowHeight) + 1; - - fixture.detectChanges(); - const firstCells = rowsRendered.map((item) => { - return item.querySelectorAll('igx-grid-cell')[0]; + verifyBaseSummaries(fix); + verifySummariesForParentID19(fix, 3); }); - expect(rowsRendered.length).toEqual(expectedRowLenght); - const expectedFirstCellNum = grid.data.length - expectedRowLenght + 1; - for (let i = 0; i < rowsRendered.length - 1; i++) { - expect(firstCells[i].textContent.trim()).toEqual((expectedFirstCellNum + i).toString()); - } - })); - - it('should render correct data after hiding one bigger and then one smaller summary when scrolled to the bottom', (async () => { - const fixture = TestBed.createComponent(VirtualSummaryColumnComponent); - fixture.detectChanges(); - - const grid = fixture.componentInstance.grid1; - const summariedColumns = ['ProductName', 'InStock', 'UnitsInStock', 'OrderDate']; - let rowsRendered; - let tbody; - let expectedRowLenght; - let firstCellsText; - - fixture.componentInstance.scrollTop(10000); - await wait(100); - fixture.detectChanges(); - - rowsRendered = fixture.nativeElement.querySelectorAll('igx-grid-row'); - tbody = grid.nativeElement.querySelector('.igx-grid__tbody').getBoundingClientRect().height; - expectedRowLenght = Math.ceil(parseFloat(tbody) / grid.defaultRowHeight) + 1; - expect(rowsRendered.length).toEqual(expectedRowLenght); + it('should be able to enable/disable summaries at runtime', () => { + grid.getColumnByName('Age').hasSummary = false; + grid.getColumnByName('ParentID').hasSummary = false; + fix.detectChanges(); - grid.disableSummaries(['ProductName', 'InStock', 'UnitsInStock']); - await wait(50); - fixture.detectChanges(); + HelperUtils.verifyVisbleSummariesHeight(fix, 3, grid.defaultRowHeight ); + + let summaries = HelperUtils.getAllVisbleSummaries(fix); + summaries.forEach(summary => { + HelperUtils.verifyColumnSummaries(summary, 0, [], []); + HelperUtils.verifyColumnSummaries(summary, 1, [], []); + HelperUtils.verifyColumnSummaries(summary, 2, ['Count'], []); + HelperUtils.verifyColumnSummaries(summary, 3, ['Count', 'Earliest', 'Latest'], []); + HelperUtils.verifyColumnSummaries(summary, 4, [], []); + HelperUtils.verifyColumnSummaries(summary, 5, ['Count'], []); + }); + + // Disable all summaries + grid.getColumnByName('Name').hasSummary = false; + grid.getColumnByName('HireDate').hasSummary = false; + grid.getColumnByName('OnPTO').hasSummary = false; + fix.detectChanges(); + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(0); - rowsRendered = Array.from(fixture.nativeElement.querySelectorAll('igx-grid-row')); - tbody = grid.nativeElement.querySelector('.igx-grid__tbody').getBoundingClientRect().height; - expectedRowLenght = Math.ceil(parseFloat(tbody) / grid.defaultRowHeight) + 1; + grid.getColumnByName('Name').hasSummary = true; + fix.detectChanges(); - firstCellsText = rowsRendered.map((item) => { - return item.querySelectorAll('igx-grid-cell')[0].textContent.trim(); + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(5); + HelperUtils.verifyVisbleSummariesHeight(fix, 1, grid.defaultRowHeight ); + summaries = HelperUtils.getAllVisbleSummaries(fix); + summaries.forEach(summary => { + HelperUtils.verifyColumnSummaries(summary, 0, [], []); + HelperUtils.verifyColumnSummaries(summary, 1, [], []); + HelperUtils.verifyColumnSummaries(summary, 2, ['Count'], []); + HelperUtils.verifyColumnSummaries(summary, 3, [], []); + HelperUtils.verifyColumnSummaries(summary, 4, [], []); + HelperUtils.verifyColumnSummaries(summary, 5, [], []); + }); }); - expect(rowsRendered.length).toEqual(expectedRowLenght); - let expectedFirstCellNum = grid.data.length - expectedRowLenght + 1; - for (let i = 0; i < rowsRendered.length - 1; i++) { - expect(firstCellsText[i]).toEqual((expectedFirstCellNum + i).toString()); - } - - grid.disableSummaries(['OrderDate']); - await wait(50); - fixture.detectChanges(); - - rowsRendered = Array.from(fixture.nativeElement.querySelectorAll('igx-grid-row')); - tbody = grid.nativeElement.querySelector('.igx-grid__tbody').getBoundingClientRect().height; - expectedRowLenght = Math.ceil(parseFloat(tbody) / grid.defaultRowHeight) + 1; + it('should show/hide summaries when expand/collapse group row', () => { + grid.disableSummaries([{ fieldName: 'Age' }, { fieldName: 'ParentID' }, { fieldName: 'HireDate' }]); + fix.detectChanges(); + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(5); - firstCellsText = rowsRendered.map((item) => { - return item.querySelectorAll('igx-grid-cell')[0].textContent.trim(); - }); - expect(rowsRendered.length).toEqual(expectedRowLenght); - expectedFirstCellNum = grid.data.length - expectedRowLenght + 1; - for (let i = 0; i < rowsRendered.length - 1; i++) { - expect(firstCellsText[i]).toEqual((expectedFirstCellNum + i).toString()); - } - })); + const groupRows = grid.groupsRowList.toArray(); + groupRows[0].toggle(); + fix.detectChanges(); - it(`Should update summary section when the column is outside of the - viewport and have identical width with others`, (async () => { - const fix = TestBed.createComponent(SummaryColumnsWithIdenticalWidthsComponent); - fix.detectChanges(); + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(4); - const grid = fix.componentInstance.grid1; - const gridApi = (grid as any)._gridAPI; - let summaries = gridApi.get_summaries(grid.id); + grid.toggleAllGroupRows(); + fix.detectChanges(); + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(1); - let getCountResSummary = summaries.get('UnitsInStock').find((k) => k.key === 'count').summaryResult; - expect(getCountResSummary).toEqual(fix.componentInstance.data.length); + groupRows[0].toggle(); + fix.detectChanges(); - grid.addRow({ - ProductID: 11, ProductName: 'Belgian Chocolate', InStock: true, UnitsInStock: 99000, OrderDate: new Date('2018-03-01') - }); - fix.detectChanges(); - GridFunctions.scrollLeft(grid, 400); - await wait(100); - fix.detectChanges(); - - summaries = gridApi.get_summaries(grid.id); - getCountResSummary = summaries.get('UnitsInStock').find((k) => k.key === 'count').summaryResult; - expect(getCountResSummary).toEqual(fix.componentInstance.data.length); - })); + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(2); - it('should be able to change \'hasSummary\' property runtime and to recalculate grid sizes correctly', fakeAsync(() => { - const fixture = TestBed.createComponent(SummaryColumnComponent); - fixture.detectChanges(); - const grid = fixture.componentInstance.grid1; - grid.columnList.forEach((col) => { - if (col.field !== 'ProductID') { - expect(grid.getColumnByName(col.field).hasSummary).toBe(true); - } + grid.toggleAllGroupRows(); + fix.detectChanges(); + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(5); }); - grid.getColumnByName('UnitsInStock').hasSummary = false; - grid.recalculateSummaries(); - tick(100); - fixture.detectChanges(); - expect(grid.getColumnByName('UnitsInStock').hasSummary).toBe(false); - const summaries = fixture.debugElement.queryAll(By.css('igx-grid-summary')).filter((el) => - el.nativeElement.classList.contains('igx-grid-summary--empty') === false); - const tfootSize = +fixture.debugElement.query(By.css('.igx-grid__summaries')) - .nativeElement.getBoundingClientRect().height; - const expectedHeight = GridFunctions.calcMaxSummaryHeight(grid.columnList, summaries, grid.defaultRowHeight); - expect(tfootSize).toBe(expectedHeight); - - grid.getColumnByName('ProductName').hasSummary = false; - grid.getColumnByName('InStock').hasSummary = false; - fixture.componentInstance.hasSummary = false; - grid.recalculateSummaries(); - fixture.detectChanges(); - tick(100); - expect(fixture.debugElement.query(By.css('.igx-grid__summaries'))).toBeNull(); - expect(grid.hasSummarizedColumns).toBe(false); - })); - it('should recalculate summary area after column with enabled summary is hidden', fakeAsync(() => { - const fixture = TestBed.createComponent(SummaryColumnComponent); - fixture.detectChanges(); - const grid = fixture.componentInstance.grid1; - - grid.getColumnByName('ProductName').hasSummary = false; - grid.getColumnByName('InStock').hasSummary = false; - fixture.componentInstance.hasSummary = false; - grid.recalculateSummaries(); - fixture.detectChanges(); - tick(100); - const summaries = fixture.debugElement.queryAll(By.css('igx-grid-summary')).filter((el) => - el.nativeElement.classList.contains('igx-grid-summary--empty') === false); - const tfootSize = +fixture.debugElement.query(By.css('.igx-grid__summaries')) - .nativeElement.getBoundingClientRect().height; - const expectedHeight = GridFunctions.calcMaxSummaryHeight(grid.columnList, summaries, grid.defaultRowHeight); - expect(tfootSize).toBe(expectedHeight); - expect(grid.hasSummarizedColumns).toBe(true); - - grid.getColumnByName('UnitsInStock').hidden = true; - tick(); - fixture.detectChanges(); - - let summaryArea = fixture.debugElement.query(By.css('.igx-grid__summaries')); - expect(summaryArea).toBeNull(); - expect(grid.hasSummarizedColumns).toBe(false); - - grid.enableSummaries('InStock'); - fixture.detectChanges(); - summaryArea = fixture.debugElement.query(By.css('.igx-grid__summaries')); - expect(grid.hasSummarizedColumns).toBe(true); - expect(summaryArea).toBeDefined(); - - grid.disableSummaries('InStock'); - fixture.detectChanges(); - summaryArea = fixture.debugElement.query(By.css('.igx-grid__summaries')); - expect(grid.hasSummarizedColumns).toBe(false); - expect(summaryArea).toBeNull(); - - grid.getColumnByName('UnitsInStock').hidden = false; - tick(); - fixture.detectChanges(); - summaryArea = fixture.debugElement.query(By.css('.igx-grid__summaries')); - expect(summaryArea).toBeDefined(); - expect(grid.hasSummarizedColumns).toBe(true); - })); + it('should be able to enable/disable summaries at runtime', () => { + grid.getColumnByName('Age').hasSummary = false; + grid.getColumnByName('ParentID').hasSummary = false; + fix.detectChanges(); - it('should properly render custom summaries', fakeAsync(() => { - const fixture = TestBed.createComponent(CustomSummariesComponent); - fixture.detectChanges(); + HelperUtils.verifyVisbleSummariesHeight(fix, 3, grid.defaultRowHeight ); + + let summaries = HelperUtils.getAllVisbleSummaries(fix); + summaries.forEach(summary => { + HelperUtils.verifyColumnSummaries(summary, 0, [], []); + HelperUtils.verifyColumnSummaries(summary, 1, [], []); + HelperUtils.verifyColumnSummaries(summary, 2, ['Count'], []); + HelperUtils.verifyColumnSummaries(summary, 3, ['Count', 'Earliest', 'Latest'], []); + HelperUtils.verifyColumnSummaries(summary, 4, [], []); + HelperUtils.verifyColumnSummaries(summary, 5, ['Count'], []); + }); + + // Disable all summaries + grid.getColumnByName('Name').hasSummary = false; + grid.getColumnByName('HireDate').hasSummary = false; + grid.getColumnByName('OnPTO').hasSummary = false; + fix.detectChanges(); + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(0); - const grid = fixture.componentInstance.grid1; - const summariesUnitOfStock = fixture.debugElement.queryAll(By.css(SUMMARY_CLASS))[3]; - const summariesOrderDate = fixture.debugElement.queryAll(By.css(SUMMARY_CLASS))[4]; + grid.getColumnByName('Name').hasSummary = true; + fix.detectChanges(); - const maxValue = summariesUnitOfStock.query(By.css('[title=\'Sum\']')).nativeElement.nextSibling.innerText; - const earliest = summariesOrderDate.query(By.css('[title=\'Earliest\']')).nativeElement.nextSibling.innerText; - expect(earliest).toBe('5/17/1990'); - expect(maxValue).toBe('39,004'); + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(5); + HelperUtils.verifyVisbleSummariesHeight(fix, 1, grid.defaultRowHeight ); + summaries = HelperUtils.getAllVisbleSummaries(fix); + summaries.forEach(summary => { + HelperUtils.verifyColumnSummaries(summary, 0, [], []); + HelperUtils.verifyColumnSummaries(summary, 1, [], []); + HelperUtils.verifyColumnSummaries(summary, 2, ['Count'], []); + HelperUtils.verifyColumnSummaries(summary, 3, [], []); + HelperUtils.verifyColumnSummaries(summary, 4, [], []); + HelperUtils.verifyColumnSummaries(summary, 5, [], []); + }); + }); - const colChips = GridFunctions.getFilterChipsForColumn('UnitsInStock', fixture); - colChips[0].nativeElement.click(); - fixture.detectChanges(); - GridFunctions.filterBy('Less Than', '0', fixture); - GridFunctions.closeFilterRow(fixture); + }); - const filterResult = grid.rowList.length; - expect(filterResult).toEqual(0); + function verifyBaseSummaries(fixture) { + const summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fixture, 0); + HelperUtils.verifyColumnSummaries(summaryRow, 0, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['8', '17', '847', '2,188', '273.5']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count'], ['8']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Earliest', 'Latest'], ['8', 'Dec 18, 2007', 'Dec 9, 2017']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['8', '25', '50', '293', '36.625']); + HelperUtils.verifyColumnSummaries(summaryRow, 5, ['Count'], ['8']); + } - const countValue = summariesUnitOfStock.query(By.css('[title=\'Count\']')).nativeElement.nextSibling.innerText; - expect(countValue).toBe('0'); - })); + function verifySummariesForParentID19(fixture, vissibleIndex) { + const summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fixture, vissibleIndex); + HelperUtils.verifyColumnSummaries(summaryRow, 0, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['1', '19', '19', '19', '19']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count'], ['1']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Earliest', 'Latest'], ['1', 'May 4, 2014', 'May 4, 2014']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['1', '44', '44', '44', '44']); + HelperUtils.verifyColumnSummaries(summaryRow, 5, ['Count'], ['1']); + } + function verifySummariesForParentID17(fixture, vissibleIndex) { + const summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fixture, vissibleIndex); + HelperUtils.verifyColumnSummaries(summaryRow, 0, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['2', '17', '17', '34', '17']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count'], ['2']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Earliest', 'Latest'], ['2', 'Dec 18, 2007', 'Mar 19, 2016']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['2', '27', '50', '77', '38.5']); + HelperUtils.verifyColumnSummaries(summaryRow, 5, ['Count'], ['2']); + } }); @Component({ @@ -654,152 +861,12 @@ describe('IgxGrid - Summaries', () => { ` }) -export class SummaryColumnsWithIdenticalWidthsComponent { - - @ViewChild('grid1', { read: IgxGridComponent }) - public grid1: IgxGridComponent; - - public data = [ - { ProductID: 1, ProductName: 'Chai', InStock: true, UnitsInStock: 2760, OrderDate: '2005-03-21' }, - { ProductID: 2, ProductName: 'Aniseed Syrup', InStock: false, UnitsInStock: 198, OrderDate: '2008-01-15' }, - { ProductID: 3, ProductName: 'Chef Antons Cajun Seasoning', InStock: true, UnitsInStock: 52, OrderDate: '2010-11-20' }, - { ProductID: 4, ProductName: 'Grandmas Boysenberry Spread', InStock: false, UnitsInStock: 0, OrderDate: '2007-10-11' }, - { ProductID: 5, ProductName: 'Uncle Bobs Dried Pears', InStock: false, UnitsInStock: 0, OrderDate: '2001-07-27' }, - { ProductID: 6, ProductName: 'Northwoods Cranberry Sauce', InStock: true, UnitsInStock: 1098, OrderDate: '1990-05-17' }, - { ProductID: 7, ProductName: 'Queso Cabrales', InStock: false, UnitsInStock: 0, OrderDate: '2005-03-03' }, - { ProductID: 8, ProductName: 'Tofu', InStock: true, UnitsInStock: 7898, OrderDate: '2017-09-09' }, - { ProductID: 9, ProductName: 'Teatime Chocolate Biscuits', InStock: true, UnitsInStock: 6998, OrderDate: '2025-12-25' }, - { ProductID: 10, ProductName: 'Chocolate', InStock: true, UnitsInStock: 20000, OrderDate: '2018-03-01' } - ]; -} - - -@Component({ - template: ` - - - - - - - - - - - - - ` -}) -export class NoActiveSummariesComponent { +export class SummaryColumnsWithIdenticalWidthsComponent { @ViewChild('grid1', { read: IgxGridComponent }) public grid1: IgxGridComponent; - public data = [ - { ProductID: 1, ProductName: 'Chai', InStock: true, UnitsInStock: 2760, OrderDate: '2005-03-21' }, - { ProductID: 2, ProductName: 'Aniseed Syrup', InStock: false, UnitsInStock: 198, OrderDate: '2008-01-15' }, - { ProductID: 3, ProductName: 'Chef Antons Cajun Seasoning', InStock: true, UnitsInStock: 52, OrderDate: '2010-11-20' }, - { ProductID: 4, ProductName: 'Grandmas Boysenberry Spread', InStock: false, UnitsInStock: 0, OrderDate: '2007-10-11' }, - { ProductID: 5, ProductName: 'Uncle Bobs Dried Pears', InStock: false, UnitsInStock: 0, OrderDate: '2001-07-27' }, - { ProductID: 6, ProductName: 'Northwoods Cranberry Sauce', InStock: true, UnitsInStock: 1098, OrderDate: '1990-05-17' }, - { ProductID: 7, ProductName: 'Queso Cabrales', InStock: false, UnitsInStock: 0, OrderDate: '2005-03-03' }, - { ProductID: 8, ProductName: 'Tofu', InStock: true, UnitsInStock: 7898, OrderDate: '2017-09-09' }, - { ProductID: 9, ProductName: 'Teatime Chocolate Biscuits', InStock: true, UnitsInStock: 6998, OrderDate: '2025-12-25' }, - { ProductID: 10, ProductName: 'Chocolate', InStock: true, UnitsInStock: 20000, OrderDate: '2018-03-01' } - ]; -} - -@Component({ - template: ` - - - - - - - - - - - - - ` -}) -export class SummaryColumnComponent { - - public data = [ - { ProductID: 1, ProductName: 'Chai', InStock: true, UnitsInStock: 2760, OrderDate: new Date('2005-03-21') }, - { ProductID: 2, ProductName: 'Aniseed Syrup', InStock: false, UnitsInStock: 198, OrderDate: new Date('2008-01-15') }, - { ProductID: 3, ProductName: 'Chef Antons Cajun Seasoning', InStock: true, UnitsInStock: 52, OrderDate: new Date('2010-11-20') }, - { ProductID: 4, ProductName: 'Grandmas Boysenberry Spread', InStock: false, UnitsInStock: 0, OrderDate: new Date('2007-10-11') }, - { ProductID: 5, ProductName: 'Uncle Bobs Dried Pears', InStock: false, UnitsInStock: 0, OrderDate: new Date('2001-07-27') }, - { ProductID: 6, ProductName: 'Northwoods Cranberry Sauce', InStock: true, UnitsInStock: 1098, OrderDate: new Date('1990-05-17') }, - { ProductID: 7, ProductName: 'Queso Cabrales', InStock: false, UnitsInStock: 0, OrderDate: new Date('2005-03-03') }, - { ProductID: 8, ProductName: 'Tofu', InStock: true, UnitsInStock: 7898, OrderDate: new Date('2017-09-09') }, - { ProductID: 9, ProductName: 'Teatime Chocolate Biscuits', InStock: true, UnitsInStock: 6998, OrderDate: new Date('2025-12-25') }, - { ProductID: 10, ProductName: 'Chocolate', InStock: true, UnitsInStock: 20000, OrderDate: new Date('2018-03-01') } - ]; - @ViewChild('grid1', { read: IgxGridComponent }) - public grid1: IgxGridComponent; - public hasSummary = true; - - public numberSummary = new IgxNumberSummaryOperand(); - public dateSummary = new IgxDateSummaryOperand(); -} - -@Component({ - template: ` - - - - - - - - - - - - - ` -}) -export class VirtualSummaryColumnComponent { - - public data = [ - { ProductID: 1, ProductName: 'Chai', InStock: true, UnitsInStock: 2760, OrderDate: new Date('2005-03-21') }, - { ProductID: 2, ProductName: 'Aniseed Syrup', InStock: false, UnitsInStock: 198, OrderDate: new Date('2008-01-15') }, - { ProductID: 3, ProductName: 'Chef Antons Cajun Seasoning', InStock: true, UnitsInStock: 52, OrderDate: new Date('2010-11-20') }, - { ProductID: 4, ProductName: 'Grandmas Boysenberry Spread', InStock: false, UnitsInStock: 0, OrderDate: new Date('2007-10-11') }, - { ProductID: 5, ProductName: 'Uncle Bobs Dried Pears', InStock: false, UnitsInStock: 0, OrderDate: new Date('2001-07-27') }, - { ProductID: 6, ProductName: 'Northwoods Cranberry Sauce', InStock: true, UnitsInStock: 1098, OrderDate: new Date('1990-05-17') }, - { ProductID: 7, ProductName: 'Queso Cabrales', InStock: false, UnitsInStock: 0, OrderDate: new Date('2005-03-03') }, - { ProductID: 8, ProductName: 'Tofu', InStock: true, UnitsInStock: 7898, OrderDate: new Date('2017-09-09') }, - { ProductID: 9, ProductName: 'Teatime Chocolate Biscuits', InStock: true, UnitsInStock: 6998, OrderDate: new Date('2025-12-25') }, - { ProductID: 10, ProductName: 'Pie', InStock: true, UnitsInStock: 1000, OrderDate: new Date('2017-05-07') }, - { ProductID: 11, ProductName: 'Pasta', InStock: false, UnitsInStock: 198, OrderDate: new Date('2001-02-15') }, - { ProductID: 12, ProductName: 'Krusty krab\'s burger', InStock: true, UnitsInStock: 52, OrderDate: new Date('2012-09-25') }, - { ProductID: 13, ProductName: 'Lasagna', InStock: false, UnitsInStock: 0, OrderDate: new Date('2015-02-09') }, - { ProductID: 14, ProductName: 'Uncle Bobs Dried Pears', InStock: false, UnitsInStock: 0, OrderDate: new Date('2008-03-17') }, - { ProductID: 15, ProductName: 'Cheese', InStock: true, UnitsInStock: 1098, OrderDate: new Date('1990-11-27') }, - { ProductID: 16, ProductName: 'Devil\'s Hot Chilli Sauce', InStock: false, UnitsInStock: 0, OrderDate: new Date('2012-08-14') }, - { ProductID: 17, ProductName: 'Parmesan', InStock: true, UnitsInStock: 4898, OrderDate: new Date('2017-09-09') }, - { ProductID: 18, ProductName: 'Steaks', InStock: true, UnitsInStock: 3098, OrderDate: new Date('2025-12-25') }, - { ProductID: 19, ProductName: 'Biscuits', InStock: true, UnitsInStock: 10570, OrderDate: new Date('2018-03-01') } - ]; - - @ViewChild('grid1', { read: IgxGridComponent }) - public grid1: IgxGridComponent; - - public width = '800px'; - public height = '600px'; - - public numberSummary = new IgxNumberSummaryOperand(); - public dateSummary = new IgxDateSummaryOperand(); - - public scrollTop(newTop: number) { - const vScrollbar = this.grid1.verticalScrollContainer.getVerticalScroll(); - vScrollbar.scrollTop = newTop; - } + public data = SampleTestData.foodProductData(); } class DealsSummary extends IgxNumberSummaryOperand { @@ -813,7 +880,7 @@ class DealsSummary extends IgxNumberSummaryOperand { const summaryResult = obj.summaryResult; // apply formatting to float numbers if (Number(summaryResult) === summaryResult) { - obj.summaryResult = summaryResult.toLocaleString('en-us', {maximumFractionDigits: 2}); + obj.summaryResult = summaryResult.toLocaleString('en-us', { maximumFractionDigits: 2 }); } return obj; } @@ -822,6 +889,23 @@ class DealsSummary extends IgxNumberSummaryOperand { } } +class DealsSummaryMinMax extends IgxNumberSummaryOperand { + constructor() { + super(); + } + + public operate(summaries?: any[]): IgxSummaryResult[] { + const result = super.operate(summaries); + result.push({ + key: 'test', + label: 'Test', + summaryResult: summaries.filter(rec => rec > 10 && rec < 30).length + }); + + return result; + } +} + class EarliestSummary extends IgxDateSummaryOperand { constructor() { super(); @@ -855,22 +939,12 @@ class EarliestSummary extends IgxDateSummaryOperand { ` }) -export class CustomSummariesComponent { - public data = [ - { ProductID: 1, ProductName: 'Chai', InStock: true, UnitsInStock: 2760, OrderDate: new Date('2005-03-21') }, - { ProductID: 2, ProductName: 'Aniseed Syrup', InStock: false, UnitsInStock: 198, OrderDate: new Date('2008-01-15') }, - { ProductID: 3, ProductName: 'Chef Antons Cajun Seasoning', InStock: true, UnitsInStock: 52, OrderDate: new Date('2010-11-20') }, - { ProductID: 4, ProductName: 'Grandmas Boysenberry Spread', InStock: false, UnitsInStock: 0, OrderDate: new Date('2007-10-11') }, - { ProductID: 5, ProductName: 'Uncle Bobs Dried Pears', InStock: false, UnitsInStock: 0, OrderDate: new Date('2001-07-27') }, - { ProductID: 6, ProductName: 'Northwoods Cranberry Sauce', InStock: true, UnitsInStock: 1098, OrderDate: new Date('1990-05-17') }, - { ProductID: 7, ProductName: 'Queso Cabrales', InStock: false, UnitsInStock: 0, OrderDate: new Date('2005-03-03') }, - { ProductID: 8, ProductName: 'Tofu', InStock: true, UnitsInStock: 7898, OrderDate: new Date('2017-09-09') }, - { ProductID: 9, ProductName: 'Teatime Chocolate Biscuits', InStock: true, UnitsInStock: 6998, OrderDate: new Date('2025-12-25') }, - { ProductID: 10, ProductName: 'Chocolate', InStock: true, UnitsInStock: 20000, OrderDate: new Date('2018-03-01') } - ]; +export class CustomSummariesComponent { + public data = SampleTestData.foodProductData(); @ViewChild('grid1', { read: IgxGridComponent }) public grid1: IgxGridComponent; public dealsSummary = DealsSummary; + public dealsSummaryMinMax = DealsSummaryMinMax; public earliest = EarliestSummary; } diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.component.html b/projects/igniteui-angular/src/lib/grids/grid/grid.component.html index 296827b24f6..3a65273b1c7 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.html @@ -74,7 +74,8 @@ | gridSort:sortingExpressions:pipeTrigger | gridPreGroupBy:groupingExpressions:groupingExpansionState:groupsExpanded:id:pipeTrigger | gridPaging:page:perPage:id:pipeTrigger - | gridPostGroupBy:groupingExpressions:groupingExpansionState:groupsExpanded:id:groupsRecords:pipeTrigger" + | gridPostGroupBy:groupingExpressions:groupingExpansionState:groupsExpanded:id:groupsRecords:pipeTrigger + | gridSummary:hasSummarizedColumns:summaryCalculationMode:summaryPosition:id:summaryPipeTrigger" let-rowIndex="index" [igxForScrollOrientation]="'vertical'" [igxForContainerSize]='calcHeight' [igxForItemSize]="rowHeight" #verticalScrollContainer (onChunkPreload)="dataLoading($event)"> @@ -85,7 +86,11 @@ - + + + + + @@ -94,24 +99,8 @@
-
- -
-
- -
-
- - - - - - -
+ +
@@ -145,13 +134,16 @@
+ You have {{ rowChangesCount }} changes in this row + +
diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.component.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.component.spec.ts index 8b9fb8df079..6138ce8ba4f 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.spec.ts @@ -2247,7 +2247,7 @@ describe('IgxGrid Component Tests', () => { grid.recalculateSummaries(); // get the summaries for a particular column - const summaries = targetCell.gridAPI.get_summaries(targetCell.gridID); + const summaries = targetCell.gridAPI.get_summary_data(targetCell.gridID); const earliestDate = summaries.get('OrderDate')[1].summaryResult.toLocaleDateString(); expect(earliestDate).toEqual(newDate); diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts index 15faaacf76a..57ae8b2cea5 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.component.ts @@ -25,6 +25,7 @@ import { takeUntil } from 'rxjs/operators'; import { IgxFilteringService } from '../filtering/grid-filtering.service'; import { IGroupingExpression } from '../../data-operations/grouping-expression.interface'; import { IgxColumnResizingService } from '../grid-column-resizing.service'; +import { IgxGridSummaryService } from '../summaries/grid-summary.service'; let NEXT_ID = 0; @@ -56,7 +57,7 @@ export interface IGroupingDoneEventArgs { @Component({ changeDetection: ChangeDetectionStrategy.OnPush, preserveWhitespaces: false, - providers: [IgxGridNavigationService, + providers: [IgxGridNavigationService, IgxGridSummaryService, { provide: GridBaseAPIService, useClass: IgxGridAPIService }, { provide: IgxGridBaseComponent, useExisting: forwardRef(() => IgxGridComponent) }, IgxFilteringService, IgxColumnResizingService @@ -124,9 +125,10 @@ export class IgxGridComponent extends IgxGridBaseComponent implements OnInit, Do viewRef: ViewContainerRef, navigation: IgxGridNavigationService, filteringService: IgxFilteringService, + summaryService: IgxGridSummaryService, @Optional() @Inject(DisplayDensityToken) protected _displayDensityOptions: IDisplayDensityOptions) { super(gridAPI, selection, _transactions, elementRef, zone, document, cdr, resolver, differs, viewRef, navigation, - filteringService, _displayDensityOptions); + filteringService, summaryService, _displayDensityOptions); this._gridAPI = gridAPI; } @@ -622,7 +624,7 @@ export class IgxGridComponent extends IgxGridBaseComponent implements OnInit, Do public getContext(rowData): any { return { $implicit: rowData, - templateID: this.isGroupByRecord(rowData) ? 'groupRow' : 'dataRow' + templateID: this.isGroupByRecord(rowData) ? 'groupRow' : this.isSummaryRow(rowData) ? 'summaryRow' : 'dataRow' }; } @@ -837,7 +839,10 @@ export class IgxGridComponent extends IgxGridBaseComponent implements OnInit, Do public ngOnInit() { super.ngOnInit(); - this.onGroupingDone.pipe(takeUntil(this.destroy$)).subscribe(() => this.endEdit(true)); + this.onGroupingDone.pipe(takeUntil(this.destroy$)).subscribe((args) => { + this.endEdit(true); + this.summaryService.updateSummaryCache(args); + }); } public ngDoCheck(): void { diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.groupby.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.groupby.spec.ts index 7d65ea9fefb..7960c56a467 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.groupby.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.groupby.spec.ts @@ -1541,35 +1541,6 @@ describe('IgxGrid - GroupBy', () => { } })); - // GroupBy + Summaries - it('should take into account only the data records when calculating summaries.', fakeAsync(() => { - const fix = TestBed.createComponent(DefaultGridComponent); - const grid = fix.componentInstance.instance; - fix.componentInstance.width = '1200px'; - tick(); - grid.columnWidth = '200px'; - tick(); - fix.detectChanges(); - grid.groupBy({ - fieldName: 'ProductName', dir: SortingDirection.Desc, ignoreCase: false - }); - fix.detectChanges(); - - grid.enableSummaries([{ fieldName: 'ProductName' }]); - tick(); - fix.detectChanges(); - - expect(grid.hasSummarizedColumns).toBe(true); - - const summaries = fix.debugElement.queryAll(By.css('igx-grid-summary')); - const labels = summaries[2].queryAll(By.css(SUMMARY_LABEL_CLASS)); - const values = summaries[2].queryAll(By.css(SUMMARY_VALUE_CLASS)); - expect(labels.length).toBe(1); - expect(labels[0].nativeElement.innerText).toBe('Count'); - expect(values.length).toBe(1); - expect(values[0].nativeElement.innerText).toBe('8'); - })); - // GroupBy + Hiding it('should retain same size for group row after a column is hidden.', fakeAsync(() => { const fix = TestBed.createComponent(DefaultGridComponent); diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.module.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.module.ts index 616cc7d2e57..50d43cb168c 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.module.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.module.ts @@ -18,6 +18,7 @@ import { IgxGridRowComponent } from './grid-row.component'; import { IgxChipsModule } from '../../chips/chips.module'; import { IgxGridCommonModule } from '../grid-common.module'; import { DeprecateMethod } from '../../core/deprecateDecorators'; +import { IgxGridSummaryPipe } from './grid.summary.pipe'; /** * @hidden @@ -33,7 +34,8 @@ import { DeprecateMethod } from '../../core/deprecateDecorators'; IgxGridPostGroupingPipe, IgxGridPagingPipe, IgxGridSortingPipe, - IgxGridFilteringPipe + IgxGridFilteringPipe, + IgxGridSummaryPipe ], exports: [ IgxGridComponent, diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.search.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.search.spec.ts index cc2ca6f15cd..f62fe23d019 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.search.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.search.spec.ts @@ -887,7 +887,7 @@ describe('IgxGrid - search API', () => { expect(highlight).toBe(spans[4]); }); - it('Should be able to react to changes in grouping', async () => { + xit('Should be able to react to changes in grouping', async () => { grid.groupBy({ fieldName: 'JobTitle', dir: SortingDirection.Asc, @@ -943,7 +943,7 @@ describe('IgxGrid - search API', () => { expect(highlight !== null).toBeTruthy(); }); - it('Should be able to navigate through highlights with grouping and paging enabled', async () => { + xit('Should be able to navigate through highlights with grouping and paging enabled', async () => { grid.groupBy({ fieldName: 'JobTitle', dir: SortingDirection.Asc, @@ -988,7 +988,7 @@ describe('IgxGrid - search API', () => { expect(grid.page).toBe(1); }); - it('Should be able to properly handle perPage changes with gouping and paging', async () => { + xit('Should be able to properly handle perPage changes with gouping and paging', async () => { grid.groupBy({ fieldName: 'JobTitle', dir: SortingDirection.Asc, @@ -1069,7 +1069,7 @@ describe('IgxGrid - search API', () => { expect(grid.isExpandedGroup(grid.groupsRecords[0])).toBeTruthy(); }); - it('Should be able to properly handle navigating through collapsed rows with paging', async () => { + xit('Should be able to properly handle navigating through collapsed rows with paging', async () => { grid.groupBy({ fieldName: 'JobTitle', dir: SortingDirection.Asc, diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.summary.pipe.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.summary.pipe.ts new file mode 100644 index 00000000000..2e3b5dd87a1 --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.summary.pipe.ts @@ -0,0 +1,103 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { IgxGridAPIService } from './grid-api.service'; +import { GridBaseAPIService } from '../api.service'; +import { IgxGridBaseComponent, GridSummaryPosition, GridSummaryCalculationMode } from '../grid-base.component'; +import { IgxGridComponent } from './grid.component'; +import { IgxSummaryResult, ISummaryRecord } from '../summaries/grid-summary'; +import { IGroupByRecord } from '../../data-operations/groupby-record.interface'; +import { DataUtil } from '../../data-operations/data-util'; + +/** @hidden */ +@Pipe({ + name: 'gridSummary', + pure: true +}) +export class IgxGridSummaryPipe implements PipeTransform { + private gridAPI: IgxGridAPIService; + + constructor(gridAPI: GridBaseAPIService) { + this.gridAPI = gridAPI; + } + + public transform(flatData: any[], + hasSummary: boolean, + summaryCalculationMode: GridSummaryCalculationMode, + summaryPosition: GridSummaryPosition, + id: string, pipeTrigger: number): any[] { + + if (!flatData || !hasSummary || summaryCalculationMode === GridSummaryCalculationMode.rootLevelOnly) { + return flatData; + } + + return this.addSummaryRows(id, flatData, summaryPosition); + } + + private addSummaryRows(gridId: string, collection: any[], summaryPosition: GridSummaryPosition): any[] { + const recordsWithSummary = []; + const lastChildMap = new Map(); + const grid: IgxGridComponent = this.gridAPI.get(gridId); + + for (let i = 0; i < collection.length; i++) { + const record = collection[i]; + recordsWithSummary.push(record); + + let recordId; + let groupByRecord: IGroupByRecord = null; + + if (grid.isGroupByRecord(record)) { + groupByRecord = record as IGroupByRecord; + recordId = this.gridAPI.get_groupBy_record_id(groupByRecord); + } else { + recordId = this.gridAPI.get_row_id(gridId, record); + } + + if (summaryPosition === GridSummaryPosition.bottom && lastChildMap.has(recordId)) { + const groupRecords = lastChildMap.get(recordId); + + for (let j = 0; j < groupRecords.length; j++) { + const groupRecord = groupRecords[j]; + const groupRecordId = this.gridAPI.get_groupBy_record_id(groupRecord); + const summaries = grid.summaryService.calculateSummaries(groupRecordId, groupRecord.records); + const summaryRecord: ISummaryRecord = { + summaries: summaries + }; + recordsWithSummary.push(summaryRecord); + } + } + + if (groupByRecord === null || !grid.isExpandedGroup(groupByRecord)) { + continue; + } + + if (summaryPosition === GridSummaryPosition.top) { + const summaries = grid.summaryService.calculateSummaries(recordId, groupByRecord.records); + const summaryRecord: ISummaryRecord = { + summaries: summaries + }; + recordsWithSummary.push(summaryRecord); + } else if (summaryPosition === GridSummaryPosition.bottom) { + let lastChild = groupByRecord; + + while (lastChild.groups && lastChild.groups.length > 0 && grid.isExpandedGroup(lastChild)) { + lastChild = lastChild.groups[lastChild.groups.length - 1]; + } + + let lastChildId; + if (grid.isExpandedGroup(lastChild)) { + lastChildId = this.gridAPI.get_row_id(gridId, lastChild.records[lastChild.records.length - 1]); + } else { + lastChildId = this.gridAPI.get_groupBy_record_id(lastChild); + } + + let groupRecords = lastChildMap.get(lastChildId); + if (!groupRecords) { + groupRecords = []; + lastChildMap.set(lastChildId, groupRecords); + } + groupRecords.unshift(groupByRecord); + } + } + + return recordsWithSummary; + } +} diff --git a/projects/igniteui-angular/src/lib/grids/index.ts b/projects/igniteui-angular/src/lib/grids/index.ts index 3233741910d..01bdff3a80d 100644 --- a/projects/igniteui-angular/src/lib/grids/index.ts +++ b/projects/igniteui-angular/src/lib/grids/index.ts @@ -4,7 +4,7 @@ export * from './row.component'; export * from './column.component'; export * from './grid-base.component'; export * from './grid.common'; -export * from './grid-summary'; +export * from './summaries/grid-summary'; export * from './grid-common.module'; export { ColumnDisplayOrder } from './column-chooser-base'; export { IColumnVisibilityChangedEventArgs } from './column-hiding-item.directive'; diff --git a/projects/igniteui-angular/src/lib/grids/summaries/grid-root-summary.pipe.ts b/projects/igniteui-angular/src/lib/grids/summaries/grid-root-summary.pipe.ts new file mode 100644 index 00000000000..97cd20fb6bb --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/summaries/grid-root-summary.pipe.ts @@ -0,0 +1,20 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { GridBaseAPIService } from '../api.service'; +import { IgxGridBaseComponent } from '../grid-base.component'; + +@Pipe({ + name: 'igxGridSummaryDataPipe', + pure: true +}) +export class IgxSummaryDataPipe implements PipeTransform { + + constructor(private gridAPI: GridBaseAPIService) { } + + transform(id: string, trigger: boolean = false) { + const summaryService = this.gridAPI.get(id).summaryService; + return summaryService.calculateSummaries( + summaryService.rootSummaryID, + this.gridAPI.get_summary_data(id) + ); + } +} diff --git a/projects/igniteui-angular/src/lib/grids/summaries/grid-summary.service.ts b/projects/igniteui-angular/src/lib/grids/summaries/grid-summary.service.ts new file mode 100644 index 00000000000..bcab4d211eb --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/summaries/grid-summary.service.ts @@ -0,0 +1,188 @@ +import { Injectable} from '@angular/core'; +import { IgxSummaryResult } from './grid-summary'; + +/** @hidden */ +@Injectable() +export class IgxGridSummaryService { + protected summaryCacheMap: Map> = new Map>(); + public grid; + public rootSummaryID = 'igxGridRootSummary'; + public summaryHeight = 0; + public maxSummariesLenght = 0; + public groupingExpressions = []; + public retriggerRootPipe = false; + + public clearSummaryCache(args?) { + if (!args) { + this.summaryCacheMap.clear(); + if (this.grid.rootSummariesEnabled) { + this.retriggerRootPipe = !this.retriggerRootPipe; + } + return; + } + if (args.data) { + let rowID = args.rowID; + if (!args.rowID) { + rowID = this.grid.primaryKey ? args.data[this.grid.primaryKey] : args.data; + } + this.removeSummaries(rowID); + } + if (args.rowID) { + const columnName = args.cellID ? this.grid.columnList.find(col => col.index === args.cellID.columnID).field : undefined; + this.removeSummaries(args.rowID, columnName); + } + } + + public removeSummaries(rowID, columnName?) { + if (this.summaryCacheMap.size === 0) { return; } + this.deleteSummaryCache(this.rootSummaryID, columnName); + if (this.summaryCacheMap.size === 1 && this.summaryCacheMap.has(this.rootSummaryID)) { return; } + if (this.isTreeGrid) { + this.removeAllTreeGridSummaries(rowID, columnName); + } else { + const summaryIds = this.getSummaryID(rowID, this.grid.groupingExpressions); + summaryIds.forEach(id => { + this.deleteSummaryCache(id, columnName); + }); + } + } + + public removeSummariesCachePerColumn(columnName) { + this.summaryCacheMap.forEach((cache) => { + if (cache.get(columnName)) { + cache.delete(columnName); + } + }); + if (this.grid.rootSummariesEnabled) { this.retriggerRootPipe = !this.retriggerRootPipe; } + } + + public calcMaxSummaryHeight() { + if (this.summaryHeight) { + return this.summaryHeight; + } + if (!this.grid.data) {return this.summaryHeight = 0; } + let maxSummaryLength = 0; + this.grid.columnList.filter((col) => col.hasSummary && !col.hidden).forEach((column) => { + const getCurrentSummaryColumn = column.summaries.operate([]).length; + if (getCurrentSummaryColumn) { + if (maxSummaryLength < getCurrentSummaryColumn) { + maxSummaryLength = getCurrentSummaryColumn; + } + } + }); + this.maxSummariesLenght = maxSummaryLength; + this.summaryHeight = maxSummaryLength * this.grid.defaultRowHeight; + return this.summaryHeight; + } + + public calculateSummaries(rowID, data) { + let rowSummaries = this.summaryCacheMap.get(rowID); + if (!rowSummaries) { + rowSummaries = new Map(); + this.summaryCacheMap.set(rowID, rowSummaries); + } + if (!this.hasSummarizedColumns || !data) {return rowSummaries; } + this.grid.columnList.filter(col => col.hasSummary).forEach((column) => { + if (!rowSummaries.get(column.field)) { + const columnValues = data.map(record => record[column.field]); + rowSummaries.set(column.field, + column.summaries.operate(columnValues)); + } + }); + return rowSummaries; + } + + public resetSummaryHeight() { + this.summaryHeight = 0; + if (this.grid.rootSummariesEnabled) { + this.retriggerRootPipe = !this.retriggerRootPipe; + } + } + + public updateSummaryCache(groupingArgs) { + if (this.summaryCacheMap.size === 0 || !this.hasSummarizedColumns) { return; } + if (this.groupingExpressions.length === 0) { + this.groupingExpressions = groupingArgs.expressions.map(record => record.fieldName); + return; + } + if (groupingArgs.length === 0) { + this.groupingExpressions = []; + this.clearSummaryCache(); + return; + } + this.compareGroupingExpressions(this.groupingExpressions, groupingArgs); + this.groupingExpressions = groupingArgs.expressions.map(record => record.fieldName); + } + + public get hasSummarizedColumns(): boolean { + const summarizedColumns = this.grid.columnList.filter(col => col.hasSummary && !col.hidden); + return summarizedColumns.length > 0; + } + + private deleteSummaryCache(id, columnName) { + if (this.summaryCacheMap.get(id)) { + if (columnName && this.summaryCacheMap.get(id).get(columnName)) { + this.summaryCacheMap.get(id).delete(columnName); + } else { + this.summaryCacheMap.delete(id); + } + if (id === this.rootSummaryID && this.grid.rootSummariesEnabled) { + this.retriggerRootPipe = !this.retriggerRootPipe; + } + } + } + + private getSummaryID(rowID, groupingExpressions) { + if (groupingExpressions.length === 0) { return []; } + const summaryIDs = []; + const rowData = this.grid.primaryKey ? this.grid.getRowByKey(rowID).rowData : rowID; + let id = '{ '; + groupingExpressions.forEach(expr => { + id += `'${expr.fieldName}': '${rowData[expr.fieldName]}'`; + summaryIDs.push(id.concat(' }')); + id += ', '; + }); + return summaryIDs; + } + + private removeAllTreeGridSummaries(rowID, columnName?) { + let row = this.grid.records.get(rowID); + if (!row) { return; } + row = row.children ? row : row.parent; + while (row) { + rowID = row.rowID; + this.deleteSummaryCache(rowID, columnName); + row = row.parent; + } + } + + private compareGroupingExpressions(current, groupingArgs) { + const newExpressions = groupingArgs.expressions.map(record => record.fieldName); + const removedCols = groupingArgs.ungroupedColumns; + if (current.length <= newExpressions.length) { + const newExpr = newExpressions.slice(0, current.length).toString(); + if (current.toString() !== newExpr) { + this.clearSummaryCache(); + } + return; + } + if (current.length > newExpressions.length) { + const currExpr = current.slice(0, newExpressions.length).toString(); + if (currExpr !== newExpressions.toString()) { + this.clearSummaryCache(); + return; + } + removedCols.map(col => col.field).forEach(colName => { + this.summaryCacheMap.forEach((cache, id) => { + if (id.indexOf(colName) !== -1) { + this.summaryCacheMap.delete(id); + }}); + }); + } + } + + private get isTreeGrid() { + return this.grid.nativeElement.tagName.toLowerCase() === 'igx-tree-grid'; + } + +} diff --git a/projects/igniteui-angular/src/lib/grids/grid-summary.ts b/projects/igniteui-angular/src/lib/grids/summaries/grid-summary.ts similarity index 97% rename from projects/igniteui-angular/src/lib/grids/grid-summary.ts rename to projects/igniteui-angular/src/lib/grids/summaries/grid-summary.ts index 7fe74d6adfc..9275a78e038 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-summary.ts +++ b/projects/igniteui-angular/src/lib/grids/summaries/grid-summary.ts @@ -1,5 +1,3 @@ -import { DataUtil } from '../data-operations/data-util'; -import { ISortingExpression, SortingDirection } from '../data-operations/sorting-expression.interface'; export interface ISummaryExpression { fieldName: string; customSummary?: any; @@ -10,6 +8,11 @@ export interface IgxSummaryResult { summaryResult: any; } +export interface ISummaryRecord { + summaries: Map; + cellIndentation?: number; +} + export class IgxSummaryOperand { /** * Counts all the records in the data source. diff --git a/projects/igniteui-angular/src/lib/grids/summaries/summary-cell.component.html b/projects/igniteui-angular/src/lib/grids/summaries/summary-cell.component.html new file mode 100644 index 00000000000..6b429e970ff --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/summaries/summary-cell.component.html @@ -0,0 +1,19 @@ + + +
+ + +
+ +
+ chevron_right +
+
+ + {{ summary.label }} + + {{ columnDatatype === 'number' ? (summary.summaryResult | igxdecimal) : columnDatatype === 'date' ? (summary.summaryResult | igxdate) : (summary.summaryResult) }} + +
+
+
\ No newline at end of file diff --git a/projects/igniteui-angular/src/lib/grids/summaries/summary-cell.component.ts b/projects/igniteui-angular/src/lib/grids/summaries/summary-cell.component.ts new file mode 100644 index 00000000000..7ab49a61fdd --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/summaries/summary-cell.component.ts @@ -0,0 +1,174 @@ +import { Component, Input, HostBinding, HostListener, ChangeDetectionStrategy, ElementRef } from '@angular/core'; +import { IgxSummaryResult } from './grid-summary'; +import { IgxColumnComponent } from '../column.component'; +import { DisplayDensity } from '../../core/density'; +import { DataType } from '../../data-operations/data-util'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + preserveWhitespaces: false, + selector: 'igx-grid-summary-cell', + templateUrl: './summary-cell.component.html' +}) +export class IgxSummaryCellComponent { + + @Input() + public summaryResults: IgxSummaryResult[]; + + @Input() + public column: IgxColumnComponent; + + @Input() + public firstCellIndentation = 0; + + @Input() + public hasSummary = false; + + @Input() + public density; + + constructor(private element: ElementRef) { + } + + @HostBinding('class') + get styleClasses(): string { + const defaultClasses = ['igx-grid-summary--cell']; + const classList = { + 'igx-grid-summary': this.density === DisplayDensity.comfortable, + 'igx-grid-summary--fw': this.column.width !== null, + 'igx-grid-summary--empty': !this.column.hasSummary, + 'igx-grid-summary--compact': this.density === DisplayDensity.compact, + 'igx-grid-summary--cosy': this.density === DisplayDensity.cosy, + 'igx-grid-summary--pinned': this.column.pinned, + 'igx-grid-summary--pinned-last': this.column.isLastPinned + }; + Object.entries(classList).forEach(([className, value]) => { + if (value) { + defaultClasses.push(className); + } + }); + return defaultClasses.join(' '); + } + + @Input() + @HostBinding('attr.data-rowIndex') + public rowIndex: number; + + @HostBinding('attr.data-visibleIndex') + get visibleColumnIndex(): number { + return this.column.visibleIndex; + } + + @HostBinding('attr.tabindex') + public tabindex = 0; + + @HostBinding('attr.aria-describedby') + public get describeby() { + return `Summary_${this.column.field}`; + } + + get nativeElement(): any { + return this.element.nativeElement; + } + + @HostListener('keydown', ['$event']) + dispatchEvent(event: KeyboardEvent) { + const key = event.key.toLowerCase(); + if (!this.isKeySupportedInCell(key)) { return; } + const shift = event.shiftKey; + const ctrl = event.ctrlKey; + event.preventDefault(); + event.stopPropagation(); + if (this.rowIndex === 0 && + this.grid.unpinnedColumns[this.grid.unpinnedColumns.length - 1].visibleIndex === this.visibleColumnIndex) { + return; + + } + if (ctrl && (key === 'arrowup' || key === 'up' || key === 'down' || key === 'arrowdown')) { return; } + const row = this.getRowElementByIndex(this.rowIndex); + switch (key) { + case 'tab': + if (shift) { + this.grid.navigation.performShiftTabKey(row, this.rowIndex, this.visibleColumnIndex, true); + break; + } + this.grid.navigation.performTab(row, this.rowIndex, this.visibleColumnIndex, true); + break; + case 'arrowleft': + case 'home': + case 'left': + if (ctrl || key === 'home') { + this.grid.navigation.onKeydownHome(this.rowIndex, true); + break; + } + this.grid.navigation.onKeydownArrowLeft(this.nativeElement, this.rowIndex, this.visibleColumnIndex, true); + break; + case 'end': + case 'arrowright': + case 'right': + if (ctrl || key === 'end') { + this.grid.navigation.onKeydownEnd(this.rowIndex, true); + break; + } + this.grid.navigation.onKeydownArrowRight(this.nativeElement, this.rowIndex, this.visibleColumnIndex, true); + break; + case 'arrowup': + case 'up': + this.grid.navigation.navigateUp(row, this.rowIndex, this.visibleColumnIndex); + break; + case 'arrowdown': + case 'down': + this.grid.navigation.navigateDown(row, this.rowIndex, this.visibleColumnIndex); + break; + } + } + + @HostBinding('style.min-width') + @HostBinding('style.max-width') + @HostBinding('style.flex-basis') + get width() { + const hasVerticalScroll = !this.grid.verticalScrollContainer.dc.instance.notVirtual; + const colWidth = this.column.width; + const isPercentageWidth = colWidth && typeof colWidth === 'string' && colWidth.indexOf('%') !== -1; + + if (colWidth && !isPercentageWidth) { + let cellWidth = this.isLastUnpinned && hasVerticalScroll ? + parseInt(colWidth, 10) - 18 + '' : colWidth; + + if (typeof cellWidth !== 'string' || cellWidth.endsWith('px') === false) { + cellWidth += 'px'; + } + + return cellWidth; + } else { + return colWidth; + } + } + + get isLastUnpinned() { + const unpinnedColumns = this.grid.unpinnedColumns; + return unpinnedColumns[unpinnedColumns.length - 1] === this.column; + } + + get columnDatatype(): DataType { + return this.column.dataType; + } + + get itemHeight() { + return this.column.grid.defaultRowHeight; + } + + private get grid() { + return (this.column.grid as any); + } + + private getRowElementByIndex(rowIndex) { + return this.grid.nativeElement.querySelector(`igx-grid-summary-row[data-rowindex="${rowIndex}"]`); + } + + private isKeySupportedInCell(key) { + return ['down', 'up', 'left', 'right', 'arrowdown', 'arrowup', 'arrowleft', 'arrowright', + 'home', 'end', 'tab'].indexOf(key) !== -1; + + } +} diff --git a/projects/igniteui-angular/src/lib/grids/summaries/summary-row.component.html b/projects/igniteui-angular/src/lib/grids/summaries/summary-row.component.html new file mode 100644 index 00000000000..6081768bf16 --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/summaries/summary-row.component.html @@ -0,0 +1,18 @@ + + +
+
+ +
+
+ + + + + + +
diff --git a/projects/igniteui-angular/src/lib/grids/summaries/summary-row.component.ts b/projects/igniteui-angular/src/lib/grids/summaries/summary-row.component.ts new file mode 100644 index 00000000000..a8d49aa99aa --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/summaries/summary-row.component.ts @@ -0,0 +1,111 @@ +import { Component, Input, + ViewChildren, QueryList, + HostBinding, ViewChild, + ElementRef, + ChangeDetectionStrategy, + ChangeDetectorRef, + DoCheck} from '@angular/core'; +import { IgxSummaryResult } from './grid-summary'; +import { IgxSummaryCellComponent } from './summary-cell.component'; +import { IgxGridForOfDirective } from '../../directives/for-of/for_of.directive'; +import { GridBaseAPIService } from '../api.service'; +import { IgxGridBaseComponent } from '../grid-base.component'; +import { IgxColumnComponent } from '../column.component'; +import { DisplayDensity } from '../../core/density'; + + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + preserveWhitespaces: false, + selector: 'igx-grid-summary-row', + templateUrl: './summary-row.component.html' +}) +export class IgxSummaryRowComponent implements DoCheck { + + @Input() + public summaries: Map; + + @Input() + public gridID; + + @Input() + public index: number; + + @Input() + public indentation = 0; + + @Input() + public firstCellIndentation = -1; + + @HostBinding('attr.data-rowIndex') + get dataRowIndex() { + return this.index; + } + + get minHeight() { + return this.grid.summaryService.calcMaxSummaryHeight(); + } + + @ViewChildren(IgxSummaryCellComponent, { read: IgxSummaryCellComponent }) + public summaryCells: QueryList; + + /** + * @hidden + */ + @ViewChild('igxDirRef', { read: IgxGridForOfDirective }) + public virtDirRow: IgxGridForOfDirective; + + constructor(public gridAPI: GridBaseAPIService, + public element: ElementRef, + public cdr: ChangeDetectorRef) {} + + public ngDoCheck() { + this.cdr.detectChanges(); + } + + public get grid() { + return this.gridAPI.get(this.gridID); + } + public get nativeElement() { + return this.element.nativeElement; + } + + // TO DO: to be refactored when displayDensity refactoring is merged + get gridDensity(): string { + if (this.grid.isCosy()) { + return DisplayDensity.cosy; + } else if (this.grid.isCompact()) { + return DisplayDensity.compact; + } else { + return DisplayDensity.comfortable; + } + } + + public getColumnSummaries(columnName) { + if (!this.summaries.get(columnName)) { + return []; + } + return this.summaries.get(columnName); + + } + /** + * @hidden + */ + public notGroups(columns) { + return columns.filter(c => !c.columnGroup); + } + + /** + * @hidden + */ + public get pinnedColumns(): IgxColumnComponent[] { + return this.grid.pinnedColumns; + } + + /** + * @hidden + */ + public get unpinnedColumns(): IgxColumnComponent[] { + return this.grid.unpinnedColumns; + } +} diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-api.service.ts b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-api.service.ts index d7c56066018..5d40a660714 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-api.service.ts +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-api.service.ts @@ -5,6 +5,8 @@ import { ITreeGridRecord } from './tree-grid.interfaces'; import { IRowToggleEventArgs } from './tree-grid.interfaces'; import { IgxColumnComponent } from '../column.component'; import { first } from 'rxjs/operators'; +import { HierarchicalTransaction, TransactionType } from '../../services'; +import { mergeObjects } from '../../core/utils'; export class IgxTreeGridAPIService extends GridBaseAPIService { public get_all_data(id: string, transactions?: boolean): any[] { @@ -13,6 +15,13 @@ export class IgxTreeGridAPIService extends GridBaseAPIService row.isFilteredOutParent === undefined || row.isFilteredOutParent === false) + .map(rec => rec.data); + return data; + } + public expand_row(id: string, rowID: any) { const grid = this.get(id); const expandedStates = grid.expansionStates; @@ -123,4 +132,45 @@ export class IgxTreeGridAPIService extends GridBaseAPIService { configureTestSuite(); @@ -32,9 +39,14 @@ describe('IgxTreeGrid - Integration', () => { IgxTreeGridDateTreeColumnComponent, IgxTreeGridBooleanTreeColumnComponent, IgxTreeGridRowEditingComponent, - IgxTreeGridMultiColHeadersComponent + IgxTreeGridMultiColHeadersComponent, + IgxTreeGridRowEditingTransactionComponent, + IgxTreeGridRowEditingHierarchicalDSTransactionComponent ], - imports: [NoopAnimationsModule, IgxToggleModule, IgxTreeGridModule] + imports: [NoopAnimationsModule, IgxToggleModule, IgxTreeGridModule], + providers: [ + { provide: IgxGridTransaction, useClass: IgxHierarchicalTransactionService } + ] }) .compileComponents(); })); @@ -295,12 +307,7 @@ describe('IgxTreeGrid - Integration', () => { treeGrid = fix.componentInstance.treeGrid; }); - it('banner has no indentation when editing a parent node.', fakeAsync(() => { - // TODO - // Verify the overlay has the same width as the row that is edited - })); - - it('shows the banner below the edited parent node', fakeAsync(() => { + it('should show the banner below the edited parent node', () => { // Collapsed state const grid = fix.componentInstance.treeGrid as IgxTreeGridComponent; grid.collapseAll(); @@ -315,7 +322,6 @@ describe('IgxTreeGrid - Integration', () => { function verifyBannerPositioning(columnIndex: number) { const cell = grid.getCellByColumn(columnIndex, 'Name'); cell.inEditMode = true; - tick(); fix.detectChanges(); const editRow = cell.row.nativeElement; @@ -329,15 +335,15 @@ describe('IgxTreeGrid - Integration', () => { // No much space between the row and the banner expect(bannerTop - editRowBottom).toBeLessThan(2); } - })); + }); - it('shows the banner below the edited child node', fakeAsync(() => { + it('should show the banner below the edited child node', () => { const grid = fix.componentInstance.treeGrid as IgxTreeGridComponent; grid.expandAll(); fix.detectChanges(); + const cell = grid.getCellByColumn(1, 'Name'); cell.inEditMode = true; - tick(); fix.detectChanges(); const editRow = cell.row.nativeElement; @@ -350,15 +356,17 @@ describe('IgxTreeGrid - Integration', () => { expect(bannerTop).toBeGreaterThanOrEqual(editRowBottom); // No much space between the row and the banner expect(bannerTop - editRowBottom).toBeLessThan(2); - })); + }); - it('shows the banner above the edited parent node if it is the last one', fakeAsync(() => { + it('should show the banner above the last parent node when in edit mode', fakeAsync(() => { const grid = fix.componentInstance.treeGrid as IgxTreeGridComponent; grid.height = '200px'; tick(16); // height animationFrame fix.detectChanges(); + grid.collapseAll(); fix.detectChanges(); + const cell = grid.getCellByColumn(2, 'Name'); cell.inEditMode = true; tick(); @@ -376,13 +384,13 @@ describe('IgxTreeGrid - Integration', () => { expect(editRowTop - bannerBottom).toBeLessThan(2); })); - it('shows the banner above the edited child node if it is the last one', fakeAsync(() => { + it('should show the banner above the last child node when in edit mode', () => { const grid = fix.componentInstance.treeGrid as IgxTreeGridComponent; grid.expandAll(); fix.detectChanges(); - const cell = grid.getCellByColumn(9, 'Name'); + + const cell = grid.getCellByColumn(grid.rowList.length - 1, 'Name'); cell.inEditMode = true; - tick(); fix.detectChanges(); const editRow = cell.row.nativeElement; @@ -395,69 +403,130 @@ describe('IgxTreeGrid - Integration', () => { expect(bannerBottom).toBeLessThanOrEqual(editRowTop); // No much space between the row and the banner expect(editRowTop - bannerBottom).toBeLessThan(2); - })); + }); - it('banner hides when you expand/collapse the edited row', fakeAsync(() => { + it('should hide banner when edited parent row is being expanded/collapsed', () => { const grid = fix.componentInstance.treeGrid as IgxTreeGridComponent; grid.collapseAll(); fix.detectChanges(); + // Edit parent row cell const cell = grid.getCellByColumn(0, 'Name'); cell.inEditMode = true; - tick(); fix.detectChanges(); - const banner = document.getElementsByClassName(CSS_CLASS_BANNER)[0]; - console.log(banner.attributes); + let banner = fix.debugElement.query(By.css('.' + CSS_CLASS_BANNER)); + expect(banner.parent.attributes['aria-hidden']).toEqual('false'); - // let banner = fix.debugElement.query(By.css('.' + CSS_CLASS_BANNER)); - // expect(banner.parent.attributes['aria-hidden']).toEqual('false'); + // Expand parent row + grid.expandRow(cell.row.rowID); + fix.detectChanges(); - // const row = cell.row as IgxTreeGridRowComponent; - // grid.expandRow(row.rowID); - // tick(); - // fix.detectChanges(); + banner = fix.debugElement.query(By.css('.' + CSS_CLASS_BANNER)); + expect(cell.inEditMode).toBeFalsy(); + expect(banner.parent.attributes['aria-hidden']).toEqual('true'); - // banner = fix.debugElement.query(By.css('.' + CSS_CLASS_BANNER)); - // expect(cell.inEditMode).toBeFalsy(); - // // expect(banner.parent.attributes['aria-hidden']).toEqual('true'); + // Edit parent row cell + cell.inEditMode = true; + fix.detectChanges(); - // cell = grid.getCellByColumn(0, 'Name'); - // cell.inEditMode = true; - // tick(); - // fix.detectChanges(); + banner = fix.debugElement.query(By.css('.' + CSS_CLASS_BANNER)); + expect(banner.parent.attributes['aria-hidden']).toEqual('false'); - // banner = fix.debugElement.query(By.css('.' + CSS_CLASS_BANNER)); - // expect(banner.parent.attributes['aria-hidden']).toEqual('false'); + // Collapse parent row + grid.collapseRow(cell.row.rowID); + fix.detectChanges(); - // grid.collapseRow(row.rowID); - // tick(); - // fix.detectChanges(); + banner = fix.debugElement.query(By.css('.' + CSS_CLASS_BANNER)); + expect(cell.inEditMode).toBeFalsy(); + expect(banner.parent.attributes['aria-hidden']).toEqual('true'); + }); - // banner = fix.debugElement.query(By.css('.igx-overlay__content')); - // // console.log(banner); - // expect(cell.inEditMode).toBeFalsy(); - // expect(banner.parent.attributes['aria-hidden']).toEqual('true'); + it('should hide banner when edited child row is being expanded/collapsed', () => { + const grid = fix.componentInstance.treeGrid as IgxTreeGridComponent; + grid.expandAll(); + fix.detectChanges(); + // Edit child row child cell + const childCell = grid.getCellByColumn(4, 'Name'); + childCell.inEditMode = true; + fix.detectChanges(); + let banner = fix.debugElement.query(By.css('.' + CSS_CLASS_BANNER)); + expect(banner.parent.attributes['aria-hidden']).toEqual('false'); - // TODO - // Verify the changes are preserved - // 1.) Expand a parent row while editing it - // 2.) Collapse an expanded parent row while editing it - // 3.) Collapse an expanded parent row while editing a child (test with more than 2 levels) - })); + // Collapse parent child row + let parentRow = grid.getRowByIndex(3); + grid.collapseRow(parentRow.rowID); + fix.detectChanges(); - it('TAB navigation cannot leave the edited row and the banner.', fakeAsync(() => { + banner = fix.debugElement.query(By.css('.' + CSS_CLASS_BANNER)); + expect(childCell.inEditMode).toBeFalsy(); + expect(banner.parent.attributes['aria-hidden']).toEqual('true'); + + // Edit child row cell + const parentCell = grid.getCellByColumn(3, 'Name'); + parentCell.inEditMode = true; + fix.detectChanges(); + + banner = fix.debugElement.query(By.css('.' + CSS_CLASS_BANNER)); + expect(banner.parent.attributes['aria-hidden']).toEqual('false'); + + // Collapse parent row + parentRow = grid.getRowByIndex(0); + grid.collapseRow(parentRow.rowID); + fix.detectChanges(); + + banner = fix.debugElement.query(By.css('.' + CSS_CLASS_BANNER)); + expect(parentCell.inEditMode).toBeFalsy(); + expect(banner.parent.attributes['aria-hidden']).toEqual('true'); + }); + + it('TAB navigation should not leave the edited row and the banner.', async () => { const grid = fix.componentInstance.treeGrid as IgxTreeGridComponent; - const cell = grid.getCellByColumn(2, 'Name'); - cell.inEditMode = true; - tick(); + const row = grid.getRowByIndex(2); + const dateCell = grid.getCellByColumn(2, 'HireDate'); + const nameCell = grid.getCellByColumn(2, 'Name'); + const idCell = grid.getCellByColumn(2, 'ID'); + const ageCell = grid.getCellByColumn(2, 'Age'); + dateCell.inEditMode = true; + await wait(30); fix.detectChanges(); - // TODO - // Verify the focus do not go to the next row - // Verify non-editable columns are skipped while navigating - })); + + await TreeGridFunctions.moveGridCellWithTab(fix, dateCell); + expect(dateCell.inEditMode).toBeFalsy(); + expect(nameCell.inEditMode).toBeTruthy(); + + await TreeGridFunctions.moveGridCellWithTab(fix, nameCell); + expect(nameCell.inEditMode).toBeFalsy(); + expect(idCell.inEditMode).toBeFalsy(); + expect(ageCell.inEditMode).toBeTruthy(); + + const cancelBtn = fix.debugElement.queryAll(By.css('.igx-button--flat'))[0] as DebugElement; + const doneBtn = fix.debugElement.queryAll(By.css('.igx-button--flat'))[1]; + spyOn(cancelBtn.nativeElement, 'focus').and.callThrough(); + spyOn(grid.rowEditTabs.first, 'move').and.callThrough(); + spyOn(grid.rowEditTabs.last, 'move').and.callThrough(); + + await TreeGridFunctions.moveGridCellWithTab(fix, ageCell); + expect(cancelBtn.nativeElement.focus).toHaveBeenCalled(); + + const mockObj = jasmine.createSpyObj('mockObj', ['stopPropagation', 'preventDefault']); + cancelBtn.triggerEventHandler('keydown.Tab', mockObj); + await wait(30); + fix.detectChanges(); + expect((grid.rowEditTabs.first).move).not.toHaveBeenCalled(); + expect(mockObj.preventDefault).not.toHaveBeenCalled(); + expect(mockObj.stopPropagation).toHaveBeenCalled(); + + doneBtn.triggerEventHandler('keydown.Tab', mockObj); + await wait(30); + fix.detectChanges(); + expect(dateCell.inEditMode).toBeTruthy(); + expect((grid.rowEditTabs.last).move).toHaveBeenCalled(); + expect(mockObj.preventDefault).toHaveBeenCalled(); + expect(mockObj.stopPropagation).toHaveBeenCalled(); + }); it('should preserve updates after removing Filtering', () => { const grid = fix.componentInstance.treeGrid as IgxTreeGridComponent; @@ -493,10 +562,9 @@ describe('IgxTreeGrid - Integration', () => { const childCell = grid.getCellByColumn(0, 'Age'); const childRowID = childCell.row.rowID; + childCell.update(14); const parentCell = grid.getCellByColumn(1, 'Age'); const parentRowID = parentCell.row.rowID; - - childCell.update(14); parentCell.update(80); fix.detectChanges(); @@ -513,6 +581,526 @@ describe('IgxTreeGrid - Integration', () => { }); }); + describe('Batch Editing', () => { + it('Children are transformed into parent nodes after their parent is deleted', fakeAsync(() => { + fix = TestBed.createComponent(IgxTreeGridRowEditingTransactionComponent); + fix.detectChanges(); + treeGrid = fix.componentInstance.treeGrid as IgxTreeGridComponent; + + const row: HTMLElement = treeGrid.getRowByIndex(0).nativeElement; + treeGrid.cascadeOnDelete = false; + const trans = treeGrid.transactions; + + treeGrid.deleteRowById(1); + fix.detectChanges(); + tick(); + + expect(row.classList).toContain('igx-grid__tr--deleted'); + expect(treeGrid.getRowByKey(1).index).toBe(0); + expect(treeGrid.getRowByKey(2).index).toBe(1); + expect(treeGrid.getRowByKey(3).index).toBe(2); + trans.commit(treeGrid.data); + tick(); + + expect(row.classList).not.toContain('igx-grid__tr--deleted'); + expect(treeGrid.getRowByKey(2).index).toBe(0); + expect(treeGrid.getRowByKey(3).index).toBe(1); + expect(trans.canUndo).toBe(false); + })); + + it('Children are deleted along with their parent', fakeAsync(() => { + fix = TestBed.createComponent(IgxTreeGridRowEditingTransactionComponent); + fix.detectChanges(); + treeGrid = fix.componentInstance.treeGrid as IgxTreeGridComponent; + treeGrid.cascadeOnDelete = true; + const trans = treeGrid.transactions; + + treeGrid.deleteRowById(1); + fix.detectChanges(); + tick(); + + for (let i = 0; i < 5; i++) { + const curRow: HTMLElement = treeGrid.getRowByIndex(i).nativeElement; + expect(curRow.classList).toContain('igx-grid__tr--deleted'); + } + expect(treeGrid.getRowByKey(1).index).toBe(0); + expect(treeGrid.getRowByKey(2).index).toBe(1); + expect(treeGrid.getRowByKey(3).index).toBe(2); + expect(treeGrid.getRowByKey(7).index).toBe(3); + expect(treeGrid.getRowByKey(4).index).toBe(4); + + trans.commit(treeGrid.data); + tick(); + + expect(treeGrid.getRowByKey(1)).toBeUndefined(); + expect(treeGrid.getRowByKey(2)).toBeUndefined(); + expect(treeGrid.getRowByKey(3)).toBeUndefined(); + expect(treeGrid.getRowByKey(7)).toBeUndefined(); + expect(treeGrid.getRowByKey(4)).toBeUndefined(); + + expect(treeGrid.getRowByKey(6).index).toBe(0); + expect(treeGrid.getRowByKey(10).index).toBe(1); + expect(trans.canUndo).toBe(false); + })); + + it('Editing a cell is possible with Hierarchical DS', fakeAsync(() => { + fix = TestBed.createComponent(IgxTreeGridRowEditingHierarchicalDSTransactionComponent); + fix.detectChanges(); + treeGrid = fix.componentInstance.treeGrid as IgxTreeGridComponent; + const trans = treeGrid.transactions; + + const targetCell = treeGrid.getCellByColumn(3, 'Age'); + targetCell.inEditMode = true; + targetCell.update('333'); + fix.detectChanges(); + tick(); + + // ged DONE button and click it + const rowEditingBannerElement = fix.debugElement.query(By.css('.igx-banner')); + const buttonElements = rowEditingBannerElement.queryAll(By.css('.igx-button--flat')); + const doneButtonElement = buttonElements.find(el => el.nativeElement.innerText === 'Done'); + doneButtonElement.nativeElement.click(); + tick(); + + // Verify the value is updated and the correct style is applied before committing + expect(targetCell.inEditMode).toBeFalsy(); + expect(targetCell.value).toBe('333'); + expect(targetCell.nativeElement.classList).toContain('igx-grid__td--edited'); + + // Commit + trans.commit(treeGrid.data, treeGrid.primaryKey, treeGrid.childDataKey); + tick(); + + // Verify the correct value is set + expect(targetCell.value).toBe('333'); + + // Add new root lv row + treeGrid.addRow({ ID: 11, ParentID: -1, Name: 'Dan Kolov', JobTitle: 'wrestler', Age: 32, OnPTO: true }); + tick(); + + // Edit a cell value and check it is correctly updated + const newTargetCell = treeGrid.getCellByColumn(10, 'Age'); + newTargetCell.inEditMode = true; + newTargetCell.update('666'); + fix.detectChanges(); + tick(); + + expect(newTargetCell.value).toBe('666'); + expect(newTargetCell.nativeElement.classList).toContain('igx-grid__td--edited'); + })); + + it('Undo/Redo keeps the correct number of steps with Hierarchical DS', () => { + // TODO: + // 1. Update a cell in three different rows + // 2. Execute "Undo" three times + // 3. Verify the initial state is shown + // 4. Execute "Redo" three times + // 5. Verify all the updates are shown with correct styles + // 6. Press "Commit" + // 7. Verify the changes are comitted + fix = TestBed.createComponent(IgxTreeGridRowEditingHierarchicalDSTransactionComponent); + fix.detectChanges(); + treeGrid = fix.componentInstance.treeGrid; + const trans = treeGrid.transactions; + const treeGridData = treeGrid.data; + // Get initial data + const rowData = { + 147: Object.assign({}, treeGrid.getRowByKey(147).rowData), + 475: Object.assign({}, treeGrid.getRowByKey(475).rowData), + 19: Object.assign({}, treeGrid.getRowByKey(19).rowData) + }; + const initialData = treeGrid.data.map(e => { + return Object.assign({}, e); + }); + let targetCell: IgxGridCellComponent; + // Get 147 row + targetCell = treeGrid.getCellByKey(147, 'Name'); + expect(targetCell.value).toEqual('John Winchester'); + // Edit 'Name' + targetCell.update('Testy Testington'); + // Get 475 row (1st child of 147) + targetCell = treeGrid.getCellByKey(475, 'Age'); + expect(targetCell.value).toEqual(30); + // Edit Age + targetCell.update(42); + // Get 19 row + targetCell = treeGrid.getCellByKey(19, 'Name'); + // Edit Name + expect(targetCell.value).toEqual('Yang Wang'); + targetCell.update('Old Richard'); + expect(rowData[147].Name).not.toEqual(treeGrid.getRowByKey(147).rowData.Name); + expect(rowData[475].Age).not.toEqual(treeGrid.getRowByKey(475).rowData.Age); + expect(rowData[19].Name).not.toEqual(treeGrid.getRowByKey(19).rowData.Name); + expect(treeGridData[0].Employees[475]).toEqual(initialData[0].Employees[475]); + expect(trans.canUndo).toBeTruthy(); + expect(trans.canRedo).toBeFalsy(); + trans.undo(); + trans.undo(); + trans.undo(); + expect(rowData[147].Name).toEqual(treeGrid.getRowByKey(147).rowData.Name); + expect(rowData[475].Age).toEqual(treeGrid.getRowByKey(475).rowData.Age); + expect(rowData[19].Name).toEqual(treeGrid.getRowByKey(19).rowData.Name); + expect(trans.canUndo).toBeFalsy(); + expect(trans.canRedo).toBeTruthy(); + trans.redo(); + trans.redo(); + trans.redo(); + expect(rowData[147].Name).not.toEqual(treeGrid.getRowByKey(147).rowData.Name); + expect(rowData[475].Age).not.toEqual(treeGrid.getRowByKey(475).rowData.Age); + expect(rowData[19].Name).not.toEqual(treeGrid.getRowByKey(19).rowData.Name); + expect(treeGridData[0].Employees[475]).toEqual(initialData[0].Employees[475]); + trans.commit(treeGridData, treeGrid.primaryKey, treeGrid.childDataKey); + expect(treeGridData[0].Name).toEqual('Testy Testington'); + expect(treeGridData[0].Employees[0].Age).toEqual(42); + expect(treeGridData[1].Name).toEqual('Old Richard'); + }); + + it('Add parent node to a Flat DS tree grid', fakeAsync(() => { + fix = TestBed.createComponent(IgxTreeGridRowEditingTransactionComponent); + fix.detectChanges(); + treeGrid = fix.componentInstance.treeGrid as IgxTreeGridComponent; + const trans = treeGrid.transactions; + + treeGrid.addRow({ ID: 11, ParentID: -1, Name: 'Dan Kolov', JobTitle: 'wrestler', Age: 32 }); + fix.detectChanges(); + tick(); + + expect(trans.canUndo).toBe(true); + expect(treeGrid.getRowByKey(11).nativeElement.classList).toContain(CSS_CLASS_ROW_EDITED); + + trans.commit(treeGrid.data); + tick(); + + expect(treeGrid.getRowByKey(11).nativeElement.classList).not.toContain(CSS_CLASS_ROW_EDITED); + expect(trans.canUndo).toBe(false); + + treeGrid.addRow({ ID: 12, ParentID: -1, Name: 'Kubrat Pulev', JobTitle: 'Boxer', Age: 33 }); + fix.detectChanges(); + tick(); + + expect(trans.canUndo).toBe(true); + expect(treeGrid.getRowByKey(12).nativeElement.classList).toContain(CSS_CLASS_ROW_EDITED); + })); + + it('Add parent node to a Hierarchical DS tree grid', () => { + fix = TestBed.createComponent(IgxTreeGridRowEditingHierarchicalDSTransactionComponent); + fix.detectChanges(); + treeGrid = fix.componentInstance.treeGrid; + const initialDataLength = treeGrid.data.length; + const trans = treeGrid.transactions; + spyOn(trans, 'add').and.callThrough(); + + const addedRowId_1 = treeGrid.rowList.length; + const newRow = { + ID: addedRowId_1, + Name: 'John Dow', + HireDate: new Date(2018, 10, 20), + Age: 22, + OnPTO: false, + Employees: [] + }; + + treeGrid.addRow(newRow); + fix.detectChanges(); + + expect(trans.getTransactionLog().length).toEqual(1); + expect(trans.add).toHaveBeenCalled(); + expect(trans.add).toHaveBeenCalledTimes(1); + const transParams = { id: addedRowId_1, type: 'add', newValue: newRow }; + expect(trans.add).toHaveBeenCalledWith(transParams); + + expect(treeGrid.records.get(addedRowId_1).level).toBe(0); + expect(treeGrid.getRowByKey(addedRowId_1).nativeElement.classList).toContain(CSS_CLASS_ROW_EDITED); + + trans.commit(treeGrid.data); + fix.detectChanges(); + + expect(treeGrid.data.length).toEqual(initialDataLength + 1); + expect(treeGrid.data[initialDataLength]).toEqual(newRow); + expect(treeGrid.records.get(addedRowId_1).level).toBe(0); + expect(treeGrid.getRowByKey(addedRowId_1).nativeElement.classList).not.toContain(CSS_CLASS_ROW_EDITED); + expect(trans.getTransactionLog().length).toEqual(0); + expect(trans.canUndo).toBeFalsy(); + + const addedRowId_2 = treeGrid.rowList.length; + const newParentRow = { + ID: addedRowId_2, + Name: 'Brad Pitt', + HireDate: new Date(2016, 8, 14), + Age: 54, + OnPTO: false + }; + + treeGrid.addRow(newParentRow); + fix.detectChanges(); + + expect(treeGrid.records.get(addedRowId_2).level).toBe(0); + expect(treeGrid.getRowByKey(addedRowId_2).nativeElement.classList).toContain(CSS_CLASS_ROW_EDITED); + expect(treeGrid.getRowByKey(addedRowId_1).nativeElement.classList).not.toContain(CSS_CLASS_ROW_EDITED); + }); + + it('Add a child node to a previously added parent node - Flat DS', () => { + fix = TestBed.createComponent(IgxTreeGridRowEditingTransactionComponent); + fix.detectChanges(); + treeGrid = fix.componentInstance.treeGrid as IgxTreeGridComponent; + const rootRow = { ID: 11, ParentID: -1, Name: 'Kubrat Pulev', JobTitle: 'wrestler', Age: 32 }; + const childRow = { ID: 12, ParentID: 11, Name: 'Tervel Pulev', JobTitle: 'wrestler', Age: 30 }; + const grandChildRow = { ID: 13, ParentID: 12, Name: 'Asparuh Pulev', JobTitle: 'wrestler', Age: 14 }; + const trans = treeGrid.transactions; + + treeGrid.addRow(rootRow, 0); + fix.detectChanges(); + + treeGrid.addRow(childRow, 11); + fix.detectChanges(); + + expect(treeGrid.getRowByKey(11).nativeElement.classList).toContain(CSS_CLASS_ROW_EDITED); + expect(treeGrid.getRowByKey(12).nativeElement.classList).toContain(CSS_CLASS_ROW_EDITED); + + trans.commit(treeGrid.data); + + expect(treeGrid.getRowByKey(11).nativeElement.classList).not.toContain(CSS_CLASS_ROW_EDITED); + expect(treeGrid.getRowByKey(12).nativeElement.classList).not.toContain(CSS_CLASS_ROW_EDITED); + + treeGrid.addRow(grandChildRow, 12); + fix.detectChanges(); + + expect(treeGrid.getRowByKey(11).nativeElement.classList).not.toContain(CSS_CLASS_ROW_EDITED); + expect(treeGrid.getRowByKey(12).nativeElement.classList).not.toContain(CSS_CLASS_ROW_EDITED); + expect(treeGrid.getRowByKey(13).nativeElement.classList).toContain(CSS_CLASS_ROW_EDITED); + }); + + it('Add a child node to a previously added parent node - Hierarchical DS', () => { + fix = TestBed.createComponent(IgxTreeGridRowEditingTransactionComponent); + fix.detectChanges(); + treeGrid = fix.componentInstance.treeGrid as IgxTreeGridComponent; + const rowData = { + parent: { ID: 13, Name: 'Dr. Evil', JobTitle: 'Doctor of Evilness', Age: 52 }, + child: { ID: 133, Name: 'Scott', JobTitle: `Annoying Teen, Dr. Evil's son`, Age: 17 }, + grandChild: { ID: 1337, Name: 'Mr. Bigglesworth', JobTitle: 'Evil Cat', Age: 13 } + }; + // 1. Add a row at level 0 to the grid + treeGrid.addRow(rowData.parent); + // 2. Add a child row to that parent + treeGrid.addRow(rowData.child, rowData.parent.ID); + // 3. Verify the new rows are pending with the correct styles + expect(treeGrid.getRowByKey(13).nativeElement.classList).toContain(CSS_CLASS_ROW_EDITED); + expect(treeGrid.getRowByKey(133).nativeElement.classList).toContain(CSS_CLASS_ROW_EDITED); + expect(treeGrid.data.findIndex(e => e.ID === rowData.parent.ID)).toEqual(-1); + expect(treeGrid.data.findIndex(e => e.ID === rowData.child.ID)).toEqual(-1); + expect(treeGrid.transactions.getAggregatedChanges(true).length).toEqual(2); + // 4. Commit + treeGrid.transactions.commit(treeGrid.data, treeGrid.primaryKey, treeGrid.childDataKey); + // 5. verify the rows are committed, the styles are OK + expect(treeGrid.data.findIndex(e => e.ID === rowData.parent.ID)).not.toEqual(-1); + expect(treeGrid.data.findIndex(e => e.ID === rowData.child.ID)).not.toEqual(-1); + expect(treeGrid.getRowByKey(13).nativeElement.classList).not.toContain(CSS_CLASS_ROW_EDITED); + expect(treeGrid.getRowByKey(133).nativeElement.classList).not.toContain(CSS_CLASS_ROW_EDITED); + expect(treeGrid.transactions.getAggregatedChanges(true).length).toEqual(0); + // 6. Add another child row at level 2 (grand-child of the first row) + treeGrid.addRow(rowData.grandChild, rowData.child.ID); + // 7. verify the pending styles is applied only to the newly added row + // and not to the previously added rows + expect(treeGrid.getRowByKey(13).nativeElement.classList).not.toContain(CSS_CLASS_ROW_EDITED); + expect(treeGrid.getRowByKey(133).nativeElement.classList).not.toContain(CSS_CLASS_ROW_EDITED); + expect(treeGrid.getRowByKey(1337).nativeElement.classList).toContain(CSS_CLASS_ROW_EDITED); + expect(treeGrid.transactions.getAggregatedChanges(true).length).toEqual(1); + }); + + it('Delete a pending parent node - Flat DS', () => { + fix = TestBed.createComponent(IgxTreeGridPrimaryForeignKeyComponent); + fix.detectChanges(); + treeGrid = fix.componentInstance.treeGrid; + const trans = treeGrid.transactions; + spyOn(trans, 'add').and.callThrough(); + treeGrid.foreignKey = 'ParentID'; + + const addedRowId = treeGrid.data.length; + const newRow = { + ID: addedRowId, + ParentID: 1, + Name: 'John Dow', + JobTitle: 'Copywriter', + Age: 22 + }; + treeGrid.addRow(newRow, 0); + fix.detectChanges(); + + const addedRow = treeGrid.rowList.filter(r => r.rowID === addedRowId)[0] as IgxTreeGridRowComponent; + treeGrid.selectRows([treeGrid.getRowByIndex(addedRow.index).rowID], true); + fix.detectChanges(); + expect(treeGrid.transactions.getTransactionLog().length).toEqual(1); + expect(trans.add).toHaveBeenCalled(); + expect(trans.add).toHaveBeenCalledTimes(1); + const transParams = { id: addedRowId, type: 'add', newValue: newRow }; + expect(trans.add).toHaveBeenCalledWith(transParams); + + treeGrid.deleteRowById(treeGrid.selectedRows()[0]); + fix.detectChanges(); + expect(treeGrid.rowList.filter(r => r.rowID === addedRowId).length).toEqual(0); + expect(treeGrid.transactions.getTransactionLog().length).toEqual(2); + expect(treeGrid.transactions.getTransactionLog()[1].id).toEqual(addedRowId); + expect(treeGrid.transactions.getTransactionLog()[1].type).toEqual('delete'); + expect(treeGrid.transactions.getTransactionLog()[1].newValue).toBeNull(); + + treeGrid.transactions.undo(); + fix.detectChanges(); + expect(treeGrid.rowList.filter(r => r.rowID === addedRowId).length).toEqual(1); + expect(treeGrid.transactions.getTransactionLog().length).toEqual(1); + expect(trans.add).toHaveBeenCalled(); + expect(trans.add).toHaveBeenCalledTimes(2); + expect(trans.add).toHaveBeenCalledWith(transParams); + }); + + it('Delete a pending parent node - Hierarchical DS', fakeAsync(() => { + fix = TestBed.createComponent(IgxTreeGridRowEditingHierarchicalDSTransactionComponent); + fix.detectChanges(); + treeGrid = fix.componentInstance.treeGrid; + const trans = treeGrid.transactions; + spyOn(trans, 'add').and.callThrough(); + + const parentRow = treeGrid.getRowByIndex(0) as IgxTreeGridRowComponent; + const addedRowId = treeGrid.rowList.length; + const newRow = { + ID: addedRowId, + Name: 'John Dow', + HireDate: new Date(2018, 10, 20), + Age: 22, + OnPTO: false, + Employees: [] + }; + + treeGrid.addRow(newRow, parentRow.rowID); + fix.detectChanges(); + + const addedRow = treeGrid.rowList.filter(r => r.rowID === addedRowId)[0] as IgxTreeGridRowComponent; + treeGrid.selectRows([treeGrid.getRowByIndex(addedRow.index).rowID], true); + tick(20); + fix.detectChanges(); + expect(treeGrid.transactions.getTransactionLog().length).toEqual(1); + expect(trans.add).toHaveBeenCalled(); + expect(trans.add).toHaveBeenCalledTimes(1); + const transParams = { id: addedRowId, path: [parentRow.rowID], newValue: newRow, type: 'add' }; + expect(trans.add).toHaveBeenCalledWith(transParams, null); + + treeGrid.deleteRowById(treeGrid.selectedRows()[0]); + tick(); + fix.detectChanges(); + expect(treeGrid.rowList.filter(r => r.rowID === addedRowId).length).toEqual(0); + expect(treeGrid.transactions.getTransactionLog().length).toEqual(2); + expect(treeGrid.transactions.getTransactionLog()[1].id).toEqual(addedRowId); + expect(treeGrid.transactions.getTransactionLog()[1].type).toEqual('delete'); + expect(treeGrid.transactions.getTransactionLog()[1].newValue).toBeNull(); + + treeGrid.transactions.undo(); + fix.detectChanges(); + expect(treeGrid.rowList.filter(r => r.rowID === addedRowId).length).toEqual(1); + expect(treeGrid.transactions.getTransactionLog().length).toEqual(1); + expect(trans.add).toHaveBeenCalled(); + expect(trans.add).toHaveBeenCalledTimes(2); + expect(trans.add).toHaveBeenCalledWith(transParams, null); + })); + + it('Delete a pending child node - Flat DS', () => { + fix = TestBed.createComponent(IgxTreeGridPrimaryForeignKeyComponent); + fix.detectChanges(); + treeGrid = fix.componentInstance.treeGrid; + const trans = treeGrid.transactions; + spyOn(trans, 'add').and.callThrough(); + treeGrid.foreignKey = 'ParentID'; + + const addedRowId = treeGrid.data.length; + const newRow = { + ID: addedRowId, + ParentID: 1, + Name: 'John Dow', + JobTitle: 'Copywriter', + Age: 22 + }; + treeGrid.addRow(newRow, 1); + fix.detectChanges(); + + const addedRow = treeGrid.rowList.filter(r => r.rowID === addedRowId)[0] as IgxTreeGridRowComponent; + treeGrid.selectRows([treeGrid.getRowByIndex(addedRow.index).rowID], true); + fix.detectChanges(); + expect(treeGrid.transactions.getTransactionLog().length).toEqual(1); + expect(trans.add).toHaveBeenCalled(); + expect(trans.add).toHaveBeenCalledTimes(1); + const transParams = { id: addedRowId, type: 'add', newValue: newRow }; + expect(trans.add).toHaveBeenCalledWith(transParams); + + treeGrid.deleteRowById(treeGrid.selectedRows()[0]); + fix.detectChanges(); + expect(treeGrid.rowList.filter(r => r.rowID === addedRowId).length).toEqual(0); + expect(treeGrid.transactions.getTransactionLog().length).toEqual(2); + expect(treeGrid.transactions.getTransactionLog()[1].id).toEqual(addedRowId); + expect(treeGrid.transactions.getTransactionLog()[1].type).toEqual('delete'); + expect(treeGrid.transactions.getTransactionLog()[1].newValue).toBeNull(); + + treeGrid.transactions.undo(); + fix.detectChanges(); + expect(treeGrid.rowList.filter(r => r.rowID === addedRowId).length).toEqual(1); + expect(treeGrid.transactions.getTransactionLog().length).toEqual(1); + expect(trans.add).toHaveBeenCalled(); + expect(trans.add).toHaveBeenCalledTimes(2); + expect(trans.add).toHaveBeenCalledWith(transParams); + }); + + it('Delete a pending child node - Hierarchical DS', fakeAsync(() => { + fix = TestBed.createComponent(IgxTreeGridRowEditingHierarchicalDSTransactionComponent); + fix.detectChanges(); + treeGrid = fix.componentInstance.treeGrid; + const trans = treeGrid.transactions; + spyOn(trans, 'add').and.callThrough(); + + const parentRow = treeGrid.getRowByIndex(1) as IgxTreeGridRowComponent; + const addedRowId = treeGrid.rowList.length; + const newRow = { + ID: addedRowId, + Name: 'John Dow', + HireDate: new Date(2018, 10, 20), + Age: 22, + OnPTO: false, + Employees: [] + }; + + treeGrid.addRow(newRow, parentRow.rowID); + fix.detectChanges(); + + const addedRow = treeGrid.rowList.filter(r => r.rowID === addedRowId)[0] as IgxTreeGridRowComponent; + treeGrid.selectRows([treeGrid.getRowByIndex(addedRow.index).rowID], true); + tick(20); + fix.detectChanges(); + expect(treeGrid.transactions.getTransactionLog().length).toEqual(1); + expect(trans.add).toHaveBeenCalled(); + expect(trans.add).toHaveBeenCalledTimes(1); + const transPasrams = { + id: addedRowId, + path: [treeGrid.getRowByIndex(0).rowID, parentRow.rowID], + newValue: newRow, + type: 'add' + }; + expect(trans.add).toHaveBeenCalledWith(transPasrams, null); + + treeGrid.deleteRowById(treeGrid.selectedRows()[0]); + tick(); + fix.detectChanges(); + expect(treeGrid.rowList.filter(r => r.rowID === addedRowId).length).toEqual(0); + expect(treeGrid.transactions.getTransactionLog().length).toEqual(2); + expect(treeGrid.transactions.getTransactionLog()[1].id).toEqual(addedRowId); + expect(treeGrid.transactions.getTransactionLog()[1].type).toEqual('delete'); + expect(treeGrid.transactions.getTransactionLog()[1].newValue).toBeNull(); + + treeGrid.transactions.undo(); + fix.detectChanges(); + expect(treeGrid.rowList.filter(r => r.rowID === addedRowId).length).toEqual(1); + expect(treeGrid.transactions.getTransactionLog().length).toEqual(1); + expect(trans.add).toHaveBeenCalled(); + expect(trans.add).toHaveBeenCalledTimes(2); + expect(trans.add).toHaveBeenCalledWith(transPasrams, null); + })); + }); + describe('Multi-column header', () => { beforeEach(() => { fix = TestBed.createComponent(IgxTreeGridMultiColHeadersComponent); @@ -616,5 +1204,90 @@ describe('IgxTreeGrid - Integration', () => { TreeGridFunctions.verifyTreeColumnInMultiColHeaders(fix, 'HireDate', 4); })); + + it('Add rows to empty grid - Hierarchical DS', () => { + fix = TestBed.createComponent(IgxTreeGridRowEditingHierarchicalDSTransactionComponent); + fix.detectChanges(); + treeGrid = fix.componentInstance.treeGrid as IgxTreeGridComponent; + // set empty data + treeGrid.data = []; + + const trans = treeGrid.transactions; + const rootRow = { + ID: 11, + Name: 'Kubrat Pulev', + HireDate: new Date(2018, 10, 20), + Age: 32, + OnPTO: false, + Employees: [] + }; + const childRow = { + ID: 12, + Name: 'Tervel Pulev', + HireDate: new Date(2018, 10, 10), + Age: 30, + OnPTO: true, + Employees: [] + }; + const grandChildRow = { + ID: 13, + Name: 'Asparuh Pulev', + HireDate: new Date(2017, 10, 10), + Age: 14, + OnPTO: true, + Employees: [] + }; + treeGrid.addRow(rootRow); + treeGrid.addRow(childRow, 11); + expect(treeGrid.getRowByKey(11).nativeElement.classList).toContain('igx-grid__tr--edited'); + expect(treeGrid.getRowByKey(12).nativeElement.classList).toContain('igx-grid__tr--edited'); + trans.commit(treeGrid.data, treeGrid.primaryKey, treeGrid.childDataKey); + expect(treeGrid.getRowByKey(11).nativeElement.classList).not.toContain('igx-grid__tr--edited'); + expect(treeGrid.getRowByKey(12).nativeElement.classList).not.toContain('igx-grid__tr--edited'); + treeGrid.addRow(grandChildRow, 12); + expect(treeGrid.getRowByKey(11).nativeElement.classList).not.toContain('igx-grid__tr--edited'); + expect(treeGrid.getRowByKey(12).nativeElement.classList).not.toContain('igx-grid__tr--edited'); + expect(treeGrid.getRowByKey(13).nativeElement.classList).toContain('igx-grid__tr--edited'); + expect(treeGrid.records.get(11).level).toBe(0); + expect(treeGrid.records.get(12).level).toBe(1); + expect(treeGrid.records.get(13).level).toBe(2); + }); + + it('Add rows to empty grid - Flat DS', () => { + fix = TestBed.createComponent(IgxTreeGridRowEditingTransactionComponent); + fix.detectChanges(); + treeGrid = fix.componentInstance.treeGrid as IgxTreeGridComponent; + // set empty data + treeGrid.data = []; + + const rootRow = { ID: 11, ParentID: -1, Name: 'Kubrat Pulev', JobTitle: 'wrestler', Age: 32 }; + const childRow = { ID: 12, ParentID: 11, Name: 'Tervel Pulev', JobTitle: 'wrestler', Age: 30 }; + const grandChildRow = { ID: 13, ParentID: 12, Name: 'Asparuh Pulev', JobTitle: 'wrestler', Age: 14 }; + const trans = treeGrid.transactions; + + treeGrid.addRow(rootRow, 0); + fix.detectChanges(); + + treeGrid.addRow(childRow, 11); + fix.detectChanges(); + + expect(treeGrid.getRowByKey(11).nativeElement.classList).toContain('igx-grid__tr--edited'); + expect(treeGrid.getRowByKey(12).nativeElement.classList).toContain('igx-grid__tr--edited'); + + trans.commit(treeGrid.data); + + expect(treeGrid.getRowByKey(11).nativeElement.classList).not.toContain('igx-grid__tr--edited'); + expect(treeGrid.getRowByKey(12).nativeElement.classList).not.toContain('igx-grid__tr--edited'); + + treeGrid.addRow(grandChildRow, 12); + fix.detectChanges(); + + expect(treeGrid.getRowByKey(11).nativeElement.classList).not.toContain('igx-grid__tr--edited'); + expect(treeGrid.getRowByKey(12).nativeElement.classList).not.toContain('igx-grid__tr--edited'); + expect(treeGrid.getRowByKey(13).nativeElement.classList).toContain('igx-grid__tr--edited'); + expect(treeGrid.records.get(11).level).toBe(0); + expect(treeGrid.records.get(12).level).toBe(1); + expect(treeGrid.records.get(13).level).toBe(2); + }); }); }); diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.ts b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.ts index 6b73256892c..f96f54714ad 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.ts +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-row.component.ts @@ -89,22 +89,24 @@ export class IgxTreeGridRowComponent extends IgxRowComponent { TreeGridFunctions.verifyHeaderCheckboxSelection(fix, false); }); + it('when all items are selected and then some of the selected rows are deleted, still all the items should be selected', () => { + treeGrid.selectAllRows(); + fix.detectChanges(); + TreeGridFunctions.verifyHeaderCheckboxSelection(fix, true); + + treeGrid.deleteRowById(treeGrid.selectedRows()[0]); + fix.detectChanges(); + TreeGridFunctions.verifyHeaderCheckboxSelection(fix, true); + + treeGrid.deleteRowById(treeGrid.selectedRows()[0]); + fix.detectChanges(); + TreeGridFunctions.verifyHeaderCheckboxSelection(fix, true); + + treeGrid.deleteRowById(treeGrid.selectedRows()[0]); + fix.detectChanges(); + // When deleting the last selected row, header checkbox will be unchecked. + TreeGridFunctions.verifyHeaderCheckboxSelection(fix, false); + }); + it('should be able to select row of any level', () => { treeGrid.selectRows([treeGrid.getRowByIndex(0).rowID], true); fix.detectChanges(); diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-summaries.spec.ts b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-summaries.spec.ts new file mode 100644 index 00000000000..ab2f39b42ec --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-summaries.spec.ts @@ -0,0 +1,740 @@ +import { async, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { IgxTreeGridModule } from './index'; +import { + IgxTreeGridSummariesComponent, + IgxTreeGridSummariesKeyComponent, + IgxTreeGridCustomSummariesComponent +} from '../../test-utils/tree-grid-components.spec'; +import { configureTestSuite } from '../../test-utils/configure-suite'; +import { HelperUtils } from '../../test-utils/helper-utils.spec'; +import { wait } from '../../test-utils/ui-interactions.spec'; +import { IgxNumberFilteringOperand } from 'igniteui-angular'; + +describe('IgxTreeGrid - Summaries', () => { + configureTestSuite(); + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + IgxTreeGridSummariesComponent, + IgxTreeGridSummariesKeyComponent, + IgxTreeGridCustomSummariesComponent + ], + imports: [ + BrowserAnimationsModule, + IgxTreeGridModule] + }) + .compileComponents(); + })); + + describe('', () => { + let fix; + let treeGrid; + beforeEach(() => { + fix = TestBed.createComponent(IgxTreeGridSummariesKeyComponent); + fix.detectChanges(); + treeGrid = fix.componentInstance.treeGrid; + }); + + it('should render summaries for all the rows when have parentKey', () => { + verifyTreeBaseSummaries(fix); + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(1); + // Expand second row and verify summaries + treeGrid.toggleRow(treeGrid.getRowByIndex(1).rowID); + fix.detectChanges(); + + verifyTreeBaseSummaries(fix); + verifySummaryForRow847(fix, 4); + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(2); + + // Expand child row and verify summaries + treeGrid.toggleRow(treeGrid.getRowByIndex(3).rowID); + fix.detectChanges(); + + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(3); + + verifyTreeBaseSummaries(fix); + verifySummaryForRow663(fix, 5); + verifySummaryForRow847(fix, 6); + }); + + it('should render summaries on top when position is top ', () => { + treeGrid.summaryPosition = 'top'; + fix.detectChanges(); + + verifyTreeBaseSummaries(fix); + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(1); + // Expand first row and verify summaries + treeGrid.toggleRow(treeGrid.getRowByIndex(0).rowID); + fix.detectChanges(); + + verifyTreeBaseSummaries(fix); + verifySummaryForRow147(fix, 1); + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(2); + + // Expand second row and verify summaries + treeGrid.toggleRow(treeGrid.getRowByIndex(5).rowID); + fix.detectChanges(); + + verifyTreeBaseSummaries(fix); + verifySummaryForRow847(fix, 6); + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(3); + + // Expand first row child and verify summaries + treeGrid.toggleRow(treeGrid.getRowByIndex(4).rowID); + fix.detectChanges(); + + verifySummaryForRow317(fix, 5); + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(4); + }); + + it('should be able to change summaryPosition at runtime', () => { + treeGrid.expandAll(); + fix.detectChanges(); + + verifyTreeBaseSummaries(fix); + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(5); + expect(HelperUtils.getAllVisbleSummariesRowIndexes(fix)).toEqual([0, 6, 7, 12, 13]); + + treeGrid.summaryPosition = 'top'; + fix.detectChanges(); + + verifyTreeBaseSummaries(fix); + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(5); + + expect(HelperUtils.getAllVisbleSummariesRowIndexes(fix)).toEqual([0, 1, 5, 9, 12]); + + treeGrid.summaryPosition = 'bottom'; + fix.detectChanges(); + + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(5); + + expect(HelperUtils.getAllVisbleSummariesRowIndexes(fix)).toEqual([0, 6, 7, 12, 13]); + }); + + it('should be able to change summaryCalculationMode at runtime', async () => { + treeGrid.expandAll(); + fix.detectChanges(); + + verifyTreeBaseSummaries(fix); + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(5); + + expect(HelperUtils.getAllVisbleSummariesRowIndexes(fix)).toEqual([0, 6, 7, 12, 13]); + + treeGrid.summaryCalculationMode = 'rootLevelOnly'; + fix.detectChanges(); + await wait(50); + + verifyTreeBaseSummaries(fix); + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(1); + + treeGrid.summaryCalculationMode = 'childLevelsOnly'; + fix.detectChanges(); + await wait(50); + + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(5); + expect(HelperUtils.getAllVisbleSummariesRowIndexes(fix)).toEqual([6, 7, 12, 13, 16]); + const summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 0); + expect(summaryRow).toBeNull(); + + treeGrid.summaryCalculationMode = 'rootAndChildLevels'; + fix.detectChanges(); + await wait(50); + + verifyTreeBaseSummaries(fix); + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(5); + expect(HelperUtils.getAllVisbleSummariesRowIndexes(fix)).toEqual([0, 6, 7, 12, 13]); + }); + + it('should be able to enable/disable summaries at runtime', () => { + treeGrid.expandAll(); + fix.detectChanges(); + + treeGrid.getColumnByName('Age').hasSummary = false; + fix.detectChanges(); + + HelperUtils.verifyVisbleSummariesHeight(fix, 3); + + let summaries = HelperUtils.getAllVisbleSummaries(fix); + summaries.forEach(summary => { + HelperUtils.verifyColumnSummaries(summary, 0, [], []); + HelperUtils.verifyColumnSummaries(summary, 1, ['Count'], []); + HelperUtils.verifyColumnSummaries(summary, 2, ['Count', 'Earliest', 'Latest'], []); + HelperUtils.verifyColumnSummaries(summary, 3, [], []); + HelperUtils.verifyColumnSummaries(summary, 4, ['Count'], []); + }); + + // Disable all summaries + treeGrid.getColumnByName('Name').hasSummary = false; + treeGrid.getColumnByName('HireDate').hasSummary = false; + treeGrid.getColumnByName('OnPTO').hasSummary = false; + fix.detectChanges(); + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(0); + + treeGrid.collapseAll(); + fix.detectChanges(); + + treeGrid.getColumnByName('Name').hasSummary = true; + fix.detectChanges(); + + treeGrid.toggleRow(treeGrid.getRowByIndex(0).rowID); + fix.detectChanges(); + + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(2); + summaries = HelperUtils.getAllVisbleSummaries(fix); + summaries.forEach(summary => { + HelperUtils.verifyColumnSummaries(summary, 0, [], []); + HelperUtils.verifyColumnSummaries(summary, 1, ['Count'], []); + HelperUtils.verifyColumnSummaries(summary, 2, [], []); + HelperUtils.verifyColumnSummaries(summary, 3, [], []); + HelperUtils.verifyColumnSummaries(summary, 4, [], []); + }); + HelperUtils.verifyVisbleSummariesHeight(fix, 1); + }); + + xit('should be able to enable/disable summaries with API', () => { + treeGrid.disableSummaries([{ fieldName: 'Age' }, { fieldName: 'HireDate' }]); + fix.detectChanges(); + + HelperUtils.verifyVisbleSummariesHeight(fix, 1); + + treeGrid.toggleRow(treeGrid.getRowByIndex(0).rowID); + fix.detectChanges(); + + let summaries = HelperUtils.getAllVisbleSummaries(fix); + summaries.forEach(summary => { + HelperUtils.verifyColumnSummaries(summary, 0, [], []); + HelperUtils.verifyColumnSummaries(summary, 1, ['Count'], []); + HelperUtils.verifyColumnSummaries(summary, 2, [], []); + HelperUtils.verifyColumnSummaries(summary, 3, [], []); + HelperUtils.verifyColumnSummaries(summary, 4, ['Count'], []); + }); + + HelperUtils.verifyVisbleSummariesHeight(fix, 1); + + treeGrid.disableSummaries('Name'); + treeGrid.disableSummaries('OnPTO'); + fix.detectChanges(); + + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(0); + + treeGrid.enableSummaries('HireDate'); + fix.detectChanges(); + + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(2); + + HelperUtils.verifyVisbleSummariesHeight(fix, 3); + + summaries = HelperUtils.getAllVisbleSummaries(fix); + summaries.forEach(summary => { + HelperUtils.verifyColumnSummaries(summary, 0, [], []); + HelperUtils.verifyColumnSummaries(summary, 1, [], []); + HelperUtils.verifyColumnSummaries(summary, 2, ['Count', 'Earliest', 'Latest'], []); + HelperUtils.verifyColumnSummaries(summary, 3, [], []); + HelperUtils.verifyColumnSummaries(summary, 4, [], []); + }); + + let summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 4); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count', 'Earliest', 'Latest'], ['3', 'Jul 19, 2009', 'Sep 18, 2014']); + + treeGrid.enableSummaries([{ fieldName: 'Age' }, { fieldName: 'ID' }]); + fix.detectChanges(); + + HelperUtils.verifyVisbleSummariesHeight(fix, 5); + + summaries = HelperUtils.getAllVisbleSummaries(fix); + summaries.forEach(summary => { + HelperUtils.verifyColumnSummaries(summary, 0, ['Count', 'Min', 'Max', 'Sum', 'Avg'], []); + HelperUtils.verifyColumnSummaries(summary, 1, [], []); + HelperUtils.verifyColumnSummaries(summary, 2, ['Count', 'Earliest', 'Latest'], []); + HelperUtils.verifyColumnSummaries(summary, 3, ['Count', 'Min', 'Max', 'Sum', 'Avg'], []); + HelperUtils.verifyColumnSummaries(summary, 4, [], []); + }); + + summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 4); + HelperUtils.verifyColumnSummaries(summaryRow, 0, ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['3', '29', '43', '103', '34.333']); + + summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 0); + HelperUtils.verifyColumnSummaries(summaryRow, 0, ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['4', '19', '847', '207', '51.75']); + }); + + xit('should be able to change summary operant at runtime', () => { + treeGrid.expandAll(); + fix.detectChanges(); + + HelperUtils.verifyVisbleSummariesHeight(fix, 5); + + treeGrid.getColumnByName('Age').summaries = fix.componentInstance.ageSummaryTest; + fix.detectChanges(); + + HelperUtils.verifyVisbleSummariesHeight(fix, 6); + + let summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 7); + HelperUtils.verifyColumnSummaries(summaryRow, 3, + ['Count', 'Min', 'Max', 'Sum', 'Avg', 'Test'], ['3', '29', '43', '103', '34.333', '2']); + + summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 6); + HelperUtils.verifyColumnSummaries(summaryRow, 3, + ['Count', 'Min', 'Max', 'Sum', 'Avg', 'Test'], ['2', '35', '44', '79', '39.5', '1']); + + summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 0); + HelperUtils.verifyColumnSummaries(summaryRow, 3, + ['Count', 'Min', 'Max', 'Sum', 'Avg', 'Test'], ['4', '42', '61', '207', '51.75', '0']); + }); + + it('should be able to change summary operant with API', () => { + treeGrid.expandAll(); + fix.detectChanges(); + + HelperUtils.verifyVisbleSummariesHeight(fix, 5); + + treeGrid.enableSummaries([{ fieldName: 'Age', customSummary: fix.componentInstance.ageSummary }]); + fix.detectChanges(); + + HelperUtils.verifyVisbleSummariesHeight(fix, 3); + + let summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 7); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Sum', 'Avg'], ['3', '103', '34.33']); + + summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 6); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Sum', 'Avg'], ['2', '79', '39.5']); + + summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 0); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Sum', 'Avg'], ['4', '207', '51.75']); + }); + + it('Hiding: should render correct summaries when show/hide a colomn', () => { + treeGrid.expandAll(); + fix.detectChanges(); + + treeGrid.getColumnByName('Age').hidden = true; + fix.detectChanges(); + + let summaries = HelperUtils.getAllVisbleSummaries(fix); + summaries.forEach(summary => { + HelperUtils.verifyColumnSummaries(summary, 0, [], []); + HelperUtils.verifyColumnSummaries(summary, 1, ['Count'], []); + HelperUtils.verifyColumnSummaries(summary, 2, ['Count', 'Earliest', 'Latest'], []); + HelperUtils.verifyColumnSummaries(summary, 3, ['Count'], []); + }); + + HelperUtils.verifyVisbleSummariesHeight(fix, 3); + + treeGrid.getColumnByName('Name').hidden = true; + treeGrid.getColumnByName('HireDate').hidden = true; + treeGrid.getColumnByName('OnPTO').hidden = true; + fix.detectChanges(); + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(0); + + treeGrid.getColumnByName('HireDate').hidden = false; + treeGrid.getColumnByName('OnPTO').hidden = false; + fix.detectChanges(); + + summaries = HelperUtils.getAllVisbleSummaries(fix); + summaries.forEach(summary => { + HelperUtils.verifyColumnSummaries(summary, 0, [], []); + HelperUtils.verifyColumnSummaries(summary, 1, ['Count', 'Earliest', 'Latest'], []); + HelperUtils.verifyColumnSummaries(summary, 2, ['Count'], []); + }); + + HelperUtils.verifyVisbleSummariesHeight(fix, 3); + + let summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 7); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count'], ['3']); + + summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 6); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count'], ['2']); + + summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 0); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count'], ['4']); + }); + + it('Filtering: should render correct summaries when filter and found only childs', () => { + treeGrid.filter('ID', 12, IgxNumberFilteringOperand.instance().condition('lessThanOrEqualTo')); + fix.detectChanges(); + + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(2); + let summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 2); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['1']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count', 'Earliest', 'Latest'], ['1', 'Dec 18, 2007', 'Dec 18, 2007']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['1', '50', '50', '50', '50']); + + summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 0); + verifySummaryIsEmpty(summaryRow); + }); + + it('Filtering: should render correct summaries when filter and no results are found', () => { + treeGrid.filter('ID', 0, IgxNumberFilteringOperand.instance().condition('lessThanOrEqualTo')); + fix.detectChanges(); + + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(1); + const summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 0); + verifySummaryIsEmpty(summaryRow); + }); + + it('Filtering: should render correct summaries when filter', () => { + treeGrid.filter('ID', 17, IgxNumberFilteringOperand.instance().condition('lessThanOrEqualTo')); + fix.detectChanges(); + + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(3); + + let summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 5); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['1']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count', 'Earliest', 'Latest'], ['1', 'Dec 18, 2007', 'Dec 18, 2007']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['1', '50', '50', '50', '50']); + + summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 2); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['1']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count', 'Earliest', 'Latest'], ['1', 'May 4, 2014', 'May 4, 2014']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['1', '44', '44', '44', '44']); + + summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 0); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['1']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['1', '61', '61', '61', '61']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count', 'Earliest', 'Latest'], ['1', 'Feb 1, 2010', 'Feb 1, 2010']); + }); + + it('Paging: should render correct summaries when paging is enable and position is buttom', () => { + treeGrid.paging = true; + treeGrid.perPage = 4; + fix.detectChanges(); + + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(1); + verifyTreeBaseSummaries(fix); + + treeGrid.toggleRow(treeGrid.getRowByIndex(0).rowID); + fix.detectChanges(); + + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(2); + verifyTreeBaseSummaries(fix); + verifySummaryForRow147(fix, 4); + + treeGrid.toggleRow(treeGrid.getRowByIndex(3).rowID); + fix.detectChanges(); + + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(1); + + treeGrid.page = 1; + fix.detectChanges(); + + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(3); + verifyTreeBaseSummaries(fix); + verifySummaryForRow147(fix, 3); + verifySummaryForRow317(fix, 2); + }); + + it('Paging: should render correct summaries when paging is enable and position is top', () => { + treeGrid.paging = true; + treeGrid.perPage = 4; + treeGrid.summaryPosition = 'top'; + fix.detectChanges(); + + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(1); + verifyTreeBaseSummaries(fix); + + treeGrid.toggleRow(treeGrid.getRowByIndex(0).rowID); + fix.detectChanges(); + + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(2); + verifyTreeBaseSummaries(fix); + verifySummaryForRow147(fix, 1); + + treeGrid.toggleRow(treeGrid.getRowByIndex(4).rowID); + fix.detectChanges(); + + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(3); + verifySummaryForRow317(fix, 5); + verifySummaryForRow147(fix, 1); + + treeGrid.page = 1; + fix.detectChanges(); + + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(1); + verifyTreeBaseSummaries(fix); + + treeGrid.toggleRow(treeGrid.getRowByIndex(2).rowID); + fix.detectChanges(); + + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(2); + verifySummaryForRow847(fix, 3); + }); + + it('CRUD: Add root node', () => { + treeGrid.expandAll(); + fix.detectChanges(); + + const newRow = { + ID: 777, + ParentID: -1, + Name: 'New Employee', + HireDate: new Date(2019, 3, 3), + Age: 19 + }; + treeGrid.addRow(newRow); + fix.detectChanges(); + + const summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 0); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['5']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count', 'Earliest', 'Latest'], ['5', 'Apr 20, 2008', 'Apr 3, 2019']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['5', '19', '61', '226', '45.2']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, ['Count'], ['5']); + + verifySummaryForRow147(fix, 7); + }); + + xit('CRUD: Add child node', () => { + treeGrid.expandAll(); + fix.detectChanges(); + + const newRow = { + ID: 777, + ParentID: 147, + Name: 'New Employee', + HireDate: new Date(2019, 3, 3), + Age: 19 + }; + treeGrid.addRow(newRow); + fix.detectChanges(); + + const summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 8); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['4']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count', 'Earliest', 'Latest'], ['4', 'Jul 19, 2009', 'Apr 3, 2019']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, ['Count'], ['4']); + + verifyTreeBaseSummaries(fix); + }); + + it('CRUD: delete root node', () => { + treeGrid.expandAll(); + fix.detectChanges(); + + treeGrid.deleteRow(treeGrid.getRowByIndex(0).rowID); + fix.detectChanges(); + + const summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 0); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['3']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count', 'Earliest', 'Latest'], ['3', 'Feb 1, 2010', 'Feb 22, 2014']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['3', '42', '61', '152', '50.667']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, ['Count'], ['3']); + + verifySummaryForRow847(fix, 5); + }); + + it('CRUD: delete all root nodes', () => { + treeGrid.toggleRow(treeGrid.getRowByIndex(0).rowID); + fix.detectChanges(); + + treeGrid.toggleRow(treeGrid.getRowByIndex(5).rowID); + fix.detectChanges(); + + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(3); + + treeGrid.deleteRow(treeGrid.getRowByIndex(5).rowID); + fix.detectChanges(); + + let summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 0); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['3']); + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(2); + + treeGrid.deleteRow(treeGrid.getRowByIndex(5).rowID); + fix.detectChanges(); + + summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 0); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['2']); + + treeGrid.deleteRow(treeGrid.getRowByIndex(5).rowID); + fix.detectChanges(); + + summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 0); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['1']); + + treeGrid.deleteRow(treeGrid.getRowByIndex(0).rowID); + fix.detectChanges(); + + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(1); + summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 0); + verifySummaryIsEmpty(summaryRow); + }); + + it('CRUD: delete child node', () => { + treeGrid.toggleRow(treeGrid.getRowByIndex(0).rowID); + fix.detectChanges(); + + treeGrid.toggleRow(treeGrid.getRowByIndex(3).rowID); + fix.detectChanges(); + + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(3); + + treeGrid.deleteRow(treeGrid.getRowByIndex(3).rowID); + fix.detectChanges(); + + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(2); + verifyTreeBaseSummaries(fix); + + const summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 3); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['2']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count', 'Earliest', 'Latest'], ['2', 'Jul 19, 2009', 'Jul 3, 2011']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['2', '29', '43', '72', '36']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, ['Count'], ['2']); + + treeGrid.deleteRow(treeGrid.getRowByIndex(2).rowID); + fix.detectChanges(); + + treeGrid.deleteRow(treeGrid.getRowByIndex(1).rowID); + fix.detectChanges(); + + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(1); + verifyTreeBaseSummaries(fix); + }); + + it('CRUD: Update root node', () => { + const newRow = { + ID: 147, + ParentID: -1, + Name: 'New Employee', + HireDate: new Date(2019, 3, 3), + Age: 19 + }; + treeGrid.getRowByKey(147).update(newRow); + fix.detectChanges(); + + const summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 0); + HelperUtils.verifyColumnSummaries(summaryRow, 0, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['4']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count', 'Earliest', 'Latest'], ['4', 'Feb 1, 2010', 'Apr 3, 2019']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['4', '19', '61', '171', '42.75']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, ['Count'], ['4']); + }); + + it('CRUD: Update child node', () => { + treeGrid.toggleRow(treeGrid.getRowByIndex(1).rowID); + fix.detectChanges(); + + treeGrid.toggleRow(treeGrid.getRowByIndex(3).rowID); + fix.detectChanges(); + + const newRow = { + ID: 663, + ParentID: 847, + Name: 'New Employee', + HireDate: new Date(2019, 3, 3), + Age: 19 + }; + treeGrid.getRowByKey(663).update(newRow); + fix.detectChanges(); + + verifyTreeBaseSummaries(fix); + + let summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 6); + HelperUtils.verifyColumnSummaries(summaryRow, 0, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['2']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count', 'Earliest', 'Latest'], ['2', 'May 4, 2014', 'Apr 3, 2019']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['2', '19', '44', '63', '31.5']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, ['Count'], ['2']); + + summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 5); + HelperUtils.verifyColumnSummaries(summaryRow, 0, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['1']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count', 'Earliest', 'Latest'], ['1', 'Apr 22, 2010', 'Apr 22, 2010']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['1', '39', '39', '39', '39']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, ['Count'], ['1']); + }); + }); + + it('should render correct custom summaries', () => { + const fix = TestBed.createComponent(IgxTreeGridCustomSummariesComponent); + fix.detectChanges(); + const treeGrid = fix.componentInstance.treeGrid; + treeGrid.expandAll(); + fix.detectChanges(); + + HelperUtils.verifyVisbleSummariesHeight(fix, 3); + + let summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 7); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Sum', 'Avg'], ['3', '103', '34.33']); + + summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 6); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Sum', 'Avg'], ['2', '79', '39.5']); + + summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fix, 0); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Sum', 'Avg'], ['4', '207', '51.75']); + }); + + it('should render summaries for all the rows', () => { + const fix = TestBed.createComponent(IgxTreeGridSummariesComponent); + fix.detectChanges(); + const treeGrid = fix.componentInstance.treeGrid; + + verifyTreeBaseSummaries(fix); + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(1); + + treeGrid.toggleRow(treeGrid.getRowByIndex(1).rowID); + fix.detectChanges(); + + verifyTreeBaseSummaries(fix); + verifySummaryForRow847(fix, 4); + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(2); + + treeGrid.toggleRow(treeGrid.getRowByIndex(3).rowID); + fix.detectChanges(); + + expect(HelperUtils.getAllVisbleSummariesLength(fix)).toEqual(3); + + verifyTreeBaseSummaries(fix); + verifySummaryForRow663(fix, 5); + verifySummaryForRow847(fix, 6); + }); + + function verifySummaryForRow147(fixture, vissibleIndex) { + const summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fixture, vissibleIndex); + HelperUtils.verifyColumnSummaries(summaryRow, 0, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['3']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count', 'Earliest', 'Latest'], ['3', 'Jul 19, 2009', 'Sep 18, 2014']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['3', '29', '43', '103', '34.333']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, ['Count'], ['3']); + } + + function verifySummaryForRow317(fixture, vissibleIndex) { + const summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fixture, vissibleIndex); + HelperUtils.verifyColumnSummaries(summaryRow, 0, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['2']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count', 'Earliest', 'Latest'], ['2', 'Nov 11, 2009', 'Oct 17, 2015']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['2', '35', '44', '79', '39.5']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, ['Count'], ['2']); + } + + function verifySummaryForRow847(fixture, vissibleIndex) { + const summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fixture, vissibleIndex); + HelperUtils.verifyColumnSummaries(summaryRow, 0, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['2']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count', 'Earliest', 'Latest'], ['2', 'May 4, 2014', 'Dec 9, 2017']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['2', '25', '44', '69', '34.5']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, ['Count'], ['2']); + } + + function verifySummaryForRow663(fixture, vissibleIndex) { + const summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fixture, vissibleIndex); + HelperUtils.verifyColumnSummaries(summaryRow, 0, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['1']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count', 'Earliest', 'Latest'], ['1', 'Apr 22, 2010', 'Apr 22, 2010']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['1', '39', '39', '39', '39']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, ['Count'], ['1']); + } + + function verifySummaryIsEmpty(summaryRow) { + HelperUtils.verifyColumnSummaries(summaryRow, 0, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['0']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count', 'Earliest', 'Latest'], ['0', '', '']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['0', '', '', '', '']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, ['Count'], ['0']); + } + + function verifyTreeBaseSummaries(fixture) { + const summaryRow = HelperUtils.getSummaryRowByDataRowIndex(fixture, 0); + HelperUtils.verifyColumnSummaries(summaryRow, 0, [], []); + HelperUtils.verifyColumnSummaries(summaryRow, 1, ['Count'], ['4']); + HelperUtils.verifyColumnSummaries(summaryRow, 2, ['Count', 'Earliest', 'Latest'], ['4', 'Apr 20, 2008', 'Feb 22, 2014']); + HelperUtils.verifyColumnSummaries(summaryRow, 3, ['Count', 'Min', 'Max', 'Sum', 'Avg'], ['4', '42', '61', '207', '51.75']); + HelperUtils.verifyColumnSummaries(summaryRow, 4, ['Count'], ['4']); + } +}); diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.html b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.html index db1a34a5f35..5e9165a74fc 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.html +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.html @@ -52,7 +52,8 @@ | treeGridFiltering:filteringExpressionsTree:id:pipeTrigger | treeGridSorting:sortingExpressions:id:pipeTrigger | treeGridFlattening:id:expansionDepth:expansionStates:pipeTrigger - | treeGridPaging:page:perPage:id:pipeTrigger" + | treeGridPaging:page:perPage:id:pipeTrigger + | treeGridSummary:hasSummarizedColumns:summaryCalculationMode:summaryPosition:id:summaryPipeTrigger" let-rowIndex="index" [igxForScrollOrientation]="'vertical'" [igxForContainerSize]='calcHeight' [igxForItemSize]="rowHeight" #verticalScrollContainer (onChunkPreload)="dataLoading($event)"> @@ -60,8 +61,12 @@ + + + + - + @@ -69,22 +74,8 @@
- - + +
diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.ts b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.ts index 4ce90347c9d..feda02f2e61 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.ts +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.ts @@ -28,6 +28,8 @@ import { IgxGridNavigationService } from '../grid-navigation.service'; import { mergeObjects } from '../../core/utils'; import { IgxHierarchicalTransactionService } from '../../services'; import { IgxFilteringService } from '../filtering/grid-filtering.service'; +import { IgxSummaryResult } from '../summaries/grid-summary'; +import { IgxGridSummaryService } from '../summaries/grid-summary.service'; let NEXT_ID = 0; @@ -52,7 +54,7 @@ let NEXT_ID = 0; preserveWhitespaces: false, selector: 'igx-tree-grid', templateUrl: 'tree-grid.component.html', - providers: [ IgxGridNavigationService, { provide: GridBaseAPIService, useClass: IgxTreeGridAPIService }, + providers: [IgxGridNavigationService, IgxGridSummaryService, { provide: GridBaseAPIService, useClass: IgxTreeGridAPIService }, { provide: IgxGridBaseComponent, useExisting: forwardRef(() => IgxTreeGridComponent) }, IgxFilteringService] }) export class IgxTreeGridComponent extends IgxGridBaseComponent { @@ -183,7 +185,7 @@ export class IgxTreeGridComponent extends IgxGridBaseComponent { this.cdr.markForCheck(); } - private _expansionStates: Map = new Map(); + private _expansionStates: Map = new Map(); /** * Returns a list of key-value pairs [row ID, expansion state]. Includes only states that differ from the default one. @@ -248,30 +250,20 @@ export class IgxTreeGridComponent extends IgxGridBaseComponent { viewRef: ViewContainerRef, navigation: IgxGridNavigationService, filteringService: IgxFilteringService, + summaryService: IgxGridSummaryService, @Optional() @Inject(DisplayDensityToken) protected _displayDensityOptions: IDisplayDensityOptions) { super(gridAPI, selection, _transactions, elementRef, zone, document, cdr, resolver, differs, viewRef, navigation, - filteringService, _displayDensityOptions); + filteringService, summaryService, _displayDensityOptions); this._gridAPI = gridAPI; } - /** - * @hidden - * Returns if the `IgxTreeGridComponent` has summarized columns. - * ```typescript - * const summarizedGrid = this.grid.hasSummarizedColumns; - * ``` - * @memberof IgxTreeGridComponent - */ - get hasSummarizedColumns(): boolean { - return false; - } - private cloneMap(mapIn: Map): Map { + private cloneMap(mapIn: Map): Map { const mapCloned: Map = new Map(); mapIn.forEach((value: boolean, key: any, mapObj: Map) => { - mapCloned.set(key, value); + mapCloned.set(key, value); }); return mapCloned; @@ -368,9 +360,13 @@ export class IgxTreeGridComponent extends IgxGridBaseComponent { const childKey = this.childDataKey; if (this.transactions.enabled) { const rowId = this.primaryKey ? data[this.primaryKey] : data; + const path: any[] = []; + path.push(parentRowID); + path.push(...this.generateRowPath(parentRowID)); + path.reverse(); this.transactions.add({ id: rowId, - parentId: parentRowID, + path: path, newValue: data, type: TransactionType.ADD } as HierarchicalTransaction, @@ -393,26 +389,31 @@ export class IgxTreeGridComponent extends IgxGridBaseComponent { } } - /** - * @hidden - */ + /** @hidden */ public deleteRowById(rowId: any) { - if (this.transactions.enabled && this.cascadeOnDelete) { + // if this is flat self-referencing data, and CascadeOnDelete is set to true + // and if we have transactions we should start pending transaction. This allows + // us in case of delete action to delete all child rows as single undo action + const flatDataWithCascadeOnDeleteAndTransactions = + this.primaryKey && + this.foreignKey && + this.cascadeOnDelete && + this.transactions.enabled; + + if (flatDataWithCascadeOnDeleteAndTransactions) { this.transactions.startPending(); } super.deleteRowById(rowId); - if (this.transactions.enabled && this.cascadeOnDelete) { + if (flatDataWithCascadeOnDeleteAndTransactions) { this.transactions.endPending(true); } } - /** - * @hidden - */ + /** @hidden */ protected deleteRowFromData(rowID: any, index: number) { - if (this.primaryKey && this.foreignKey) { + if (this.primaryKey && this.foreignKey) { super.deleteRowFromData(rowID, index); if (this.cascadeOnDelete) { @@ -424,30 +425,46 @@ export class IgxTreeGridComponent extends IgxGridBaseComponent { } } } - } else { + } else { const record = this.records.get(rowID); - const childData = record.parent ? record.parent.data[this.childDataKey] : this.data; - index = this.primaryKey ? childData.map(c => c[this.primaryKey]).indexOf(rowID) : - childData.indexOf(rowID); + const collection = record.parent ? record.parent.data[this.childDataKey] : this.data; + index = this.primaryKey ? + collection.map(c => c[this.primaryKey]).indexOf(rowID) : + collection.indexOf(rowID); + + const selectedChildren = []; + this._gridAPI.get_selected_children(this.id, record, selectedChildren); + if (selectedChildren.length > 0) { + this.deselectRows(selectedChildren); + } + if (this.transactions.enabled) { + const path = this.generateRowPath(rowID); this.transactions.add({ - id: rowID, - type: TransactionType.DELETE, - newValue: null, - parentId: record.parent ? record.parent.rowID : undefined - }, - this.data); + id: rowID, + type: TransactionType.DELETE, + newValue: null, + path: path + }, + collection[index] + ); } else { - childData.splice(index, 1); + collection.splice(index, 1); } } } - /** - * @hidden - */ - protected calcMaxSummaryHeight() { - return 0; + /** @hidden */ + public generateRowPath(rowId: any): any[] { + const path: any[] = []; + let record = this.records.get(rowId); + + while (record.parent) { + path.push(record.parent.rowID); + record = record.parent; + } + + return path; } /** @@ -486,14 +503,10 @@ export class IgxTreeGridComponent extends IgxGridBaseComponent { /** * @hidden */ - public getContext(rowData): any { + public getContext(rowData): any { return { $implicit: rowData, - templateID: 'dataRow' + templateID: this.isSummaryRow(rowData) ? 'summaryRow' : 'dataRow' }; } - - protected writeToData(rowIndex: number, value: any) { - mergeObjects(this.flatData[rowIndex], value); - } } diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.interfaces.ts b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.interfaces.ts index 3789aff836f..0ea55334161 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.interfaces.ts +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.interfaces.ts @@ -7,7 +7,6 @@ export interface ITreeGridRecord { level?: number; isFilteredOutParent?: boolean; expanded?: boolean; - path: any[]; } export interface IRowToggleEventArgs { diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.module.ts b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.module.ts index 45b4cd153f4..4dc6f8c60ac 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.module.ts +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.module.ts @@ -9,6 +9,7 @@ import { IgxTreeGridHierarchizingPipe } from './tree-grid.pipes'; import { IgxTreeGridFlatteningPipe, IgxTreeGridSortingPipe, IgxTreeGridPagingPipe, IgxTreeGridTransactionPipe } from './tree-grid.pipes'; import { IgxTreeGridCellComponent } from './tree-cell.component'; import { IgxTreeGridFilteringPipe } from './tree-grid.filtering.pipe'; +import { IgxTreeGridSummaryPipe } from './tree-grid.summary.pipe'; @NgModule({ declarations: [ @@ -20,7 +21,8 @@ import { IgxTreeGridFilteringPipe } from './tree-grid.filtering.pipe'; IgxTreeGridSortingPipe, IgxTreeGridFilteringPipe, IgxTreeGridPagingPipe, - IgxTreeGridTransactionPipe + IgxTreeGridTransactionPipe, + IgxTreeGridSummaryPipe ], exports: [ IgxTreeGridComponent, diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.pipes.ts b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.pipes.ts index 459b3f91f5b..cb5f044b0e5 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.pipes.ts +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.pipes.ts @@ -6,7 +6,7 @@ import { GridBaseAPIService } from '../api.service'; import { IgxTreeGridComponent } from './tree-grid.component'; import { ISortingExpression } from '../../../public_api'; import { ITreeGridRecord } from './tree-grid.interfaces'; -import { IgxGridBaseComponent } from '../grid'; +import { IgxGridBaseComponent, IgxSummaryResult } from '../grid'; /** *@hidden @@ -55,16 +55,11 @@ export class IgxTreeGridHierarchizingPipe implements PipeTransform { const record: ITreeGridRecord = { rowID: this.getRowID(primaryKey, row), data: row, - children: [], - path: [] + children: [] }; const parent = map.get(row[foreignKey]); if (parent) { record.parent = parent; - if (parent) { - record.path.push(...parent.path); - record.path.push(parent.rowID); - } parent.children.push(record); } else { missingParentRecords.push(record); @@ -110,13 +105,8 @@ export class IgxTreeGridHierarchizingPipe implements PipeTransform { rowID: this.getRowID(primaryKey, item), data: item, parent: parent, - level: indentationLevel, - path: [] + level: indentationLevel }; - if (parent) { - record.path.push(...parent.path); - record.path.push(parent.rowID); - } record.expanded = this.gridAPI.get_row_expansion_state(id, record.rowID, record.level); flatData.push(item); map.set(record.rowID, record); @@ -148,7 +138,7 @@ export class IgxTreeGridFlatteningPipe implements PipeTransform { expandedLevels: number, expandedStates: Map, pipeTrigger: number): any[] { const grid: IgxTreeGridComponent = this.gridAPI.get(id); - const data: ITreeGridRecord[] = []; + const data: any[] = []; grid.processedRootRecords = collection; grid.processedRecords = new Map(); @@ -158,12 +148,13 @@ export class IgxTreeGridFlatteningPipe implements PipeTransform { return data; } - private getFlatDataRecursive(collection: ITreeGridRecord[], data: ITreeGridRecord[] = [], + private getFlatDataRecursive(collection: ITreeGridRecord[], data: any[], expandedLevels: number, expandedStates: Map, gridID: string, parentExpanded: boolean) { if (!collection || !collection.length) { return; } + const grid: IgxTreeGridComponent = this.gridAPI.get(gridID); for (let i = 0; i < collection.length; i++) { const hierarchicalRecord = collection[i]; @@ -172,10 +163,9 @@ export class IgxTreeGridFlatteningPipe implements PipeTransform { data.push(hierarchicalRecord); } - const grid: IgxTreeGridComponent = this.gridAPI.get(gridID); - hierarchicalRecord.expanded = this.gridAPI.get_row_expansion_state(gridID, hierarchicalRecord.rowID, hierarchicalRecord.level); + this.updateNonProcessedRecordExpansion(grid, hierarchicalRecord); grid.processedRecords.set(hierarchicalRecord.rowID, hierarchicalRecord); @@ -234,7 +224,7 @@ export class IgxTreeGridPagingPipe implements PipeTransform { } public transform(collection: ITreeGridRecord[], page = 0, perPage = 15, id: string, pipeTrigger: number): ITreeGridRecord[] { - const grid: IgxTreeGridComponent = this.gridAPI.get(id) as IgxTreeGridComponent; + const grid = this.gridAPI.get(id); if (!grid.paging) { return collection; } @@ -270,26 +260,30 @@ export class IgxTreeGridTransactionPipe implements PipeTransform { transform(collection: any[], id: string, pipeTrigger: number): any[] { const grid: IgxTreeGridComponent = this.gridAPI.get(id); if (collection && grid.transactions.enabled) { - const primaryKey = grid.primaryKey; - if (!primaryKey) { - return collection; - } + const aggregatedChanges = grid.transactions.getAggregatedChanges(true); + if (aggregatedChanges.length > 0) { + const primaryKey = grid.primaryKey; + if (!primaryKey) { + return collection; + } - const foreignKey = grid.foreignKey; - const childDataKey = grid.childDataKey; - - if (foreignKey) { - return DataUtil.mergeTransactions( - cloneArray(collection), - grid.transactions.getAggregatedChanges(true), - grid.primaryKey); - } else if (childDataKey) { - return DataUtil.mergeHierarchicalTransactions( - cloneHierarchicalArray(collection, childDataKey), - grid.transactions.getAggregatedChanges(true), - childDataKey, - grid.primaryKey - ); + const foreignKey = grid.foreignKey; + const childDataKey = grid.childDataKey; + + if (foreignKey) { + const flatDataClone = cloneArray(collection); + return DataUtil.mergeTransactions( + flatDataClone, + aggregatedChanges, + grid.primaryKey); + } else if (childDataKey) { + const hierarchicalDataClone = cloneHierarchicalArray(collection, childDataKey); + return DataUtil.mergeHierarchicalTransactions( + hierarchicalDataClone, + aggregatedChanges, + childDataKey, + grid.primaryKey); + } } } diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.summary.pipe.ts b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.summary.pipe.ts new file mode 100644 index 00000000000..2007c18e4d9 --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.summary.pipe.ts @@ -0,0 +1,79 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { IgxTreeGridAPIService } from './tree-grid-api.service'; +import { GridBaseAPIService } from '../api.service'; +import { IgxGridBaseComponent, GridSummaryPosition, GridSummaryCalculationMode } from '../grid-base.component'; +import { ITreeGridRecord } from './tree-grid.interfaces'; +import { IgxTreeGridComponent } from './tree-grid.component'; +import { IgxSummaryResult, ISummaryRecord } from '../summaries/grid-summary'; + +/** @hidden */ +@Pipe({ + name: 'treeGridSummary', + pure: true +}) +export class IgxTreeGridSummaryPipe implements PipeTransform { + private gridAPI: IgxTreeGridAPIService; + + constructor(gridAPI: GridBaseAPIService) { + this.gridAPI = gridAPI; + } + + public transform(flatData: ITreeGridRecord[], + hasSummary: boolean, + summaryCalculationMode: GridSummaryCalculationMode, + summaryPosition: GridSummaryPosition, + id: string, pipeTrigger: number): any[] { + const grid: IgxTreeGridComponent = this.gridAPI.get(id); + + if (!flatData || !hasSummary || summaryCalculationMode === GridSummaryCalculationMode.rootLevelOnly) { + return flatData; + } + + return this.addSummaryRows(grid, flatData, summaryPosition); + } + + private addSummaryRows(grid: IgxTreeGridComponent, collection: ITreeGridRecord[], summaryPosition: GridSummaryPosition): any[] { + const recordsWithSummary = []; + + for (let i = 0; i < collection.length; i++) { + const record = collection[i]; + recordsWithSummary.push(record); + + const isExpanded = record.children && record.children.length > 0 && record.expanded; + + if (summaryPosition === GridSummaryPosition.bottom && !isExpanded) { + let childRecord = record; + let parent = record.parent; + + while (parent) { + const children = parent.children; + + if (children[children.length - 1] === childRecord ) { + const childData = children.filter(r => !r.isFilteredOutParent).map(r => r.data); + const summaries = grid.summaryService.calculateSummaries(parent.rowID, childData); + const summaryRecord: ISummaryRecord = { + summaries: summaries, + cellIndentation: parent.level + 1 + }; + recordsWithSummary.push(summaryRecord); + + childRecord = parent; + parent = childRecord.parent; + } else { + break; + } + } + } else if (summaryPosition === GridSummaryPosition.top && isExpanded) { + const childData = record.children.map(r => r.data); + const summaries = grid.summaryService.calculateSummaries(record.rowID, childData); + const summaryRecord: ISummaryRecord = { + summaries: summaries, + cellIndentation: record.level + 1 + }; + recordsWithSummary.push(summaryRecord); + } + } + return recordsWithSummary; + } + +} diff --git a/projects/igniteui-angular/src/lib/services/transaction/igx-hierarchical-transaction.ts b/projects/igniteui-angular/src/lib/services/transaction/igx-hierarchical-transaction.ts index 9fbd8f14ad6..2f722f5d4e3 100644 --- a/projects/igniteui-angular/src/lib/services/transaction/igx-hierarchical-transaction.ts +++ b/projects/igniteui-angular/src/lib/services/transaction/igx-hierarchical-transaction.ts @@ -1,6 +1,7 @@ -import { HierarchicalTransaction, HierarchicalState, TransactionType, HierarchicalTransactionNode } from './transaction'; +import { HierarchicalTransaction, HierarchicalState, TransactionType } from './transaction'; import { Injectable } from '@angular/core'; import { IgxTransactionService } from './igx-transaction'; +import { DataUtil } from '../../data-operations/data-util'; /** @experimental @hidden */ @Injectable() @@ -12,19 +13,52 @@ export class IgxHierarchicalTransactionService { const value = mergeChanges ? this.mergeValues(state.recordRef, state.value) : state.value; this.clearArraysFromObject(value); - result.push({ id: key, parentId: state.parentId, newValue: value, type: state.type } as T); + result.push({ id: key, path: state.path, newValue: value, type: state.type } as T); }); return result; } protected updateState(states: Map, transaction: T, recordRef?: any): void { super.updateState(states, transaction, recordRef); + + // if transaction has no path, e.g. flat data source, get out + if (!transaction.path) { + return; + } + const currentState = states.get(transaction.id); - if (currentState && transaction.type === TransactionType.ADD) { - currentState.parentId = transaction.parentId; + if (currentState) { + currentState.path = transaction.path; + } + + // if transaction has path, Hierarchical data source, and it is DELETE + // type transaction for all child rows remove ADD states and update + // transaction type and value of UPDATE states + if (transaction.type === TransactionType.DELETE) { + states.forEach((v: S, k: any) => { + if (v.path && v.path.indexOf(transaction.id) !== -1) { + switch (v.type) { + case TransactionType.ADD: + states.delete(k); + break; + case TransactionType.UPDATE: + states.get(k).type = TransactionType.DELETE; + states.get(k).value = null; + } + } + }); } } + public commit(data: any[], primaryKey?: any, childDataKey?: any): void { + if (childDataKey) { + DataUtil.mergeHierarchicalTransactions(data, this.getAggregatedChanges(true), childDataKey, primaryKey, true); + } else { + super.commit(data); + } + this.clear(); + } + // TODO: remove this method. Force cloning to strip child arrays when needed instead private clearArraysFromObject(obj: {}) { for (const prop of Object.keys(obj)) { @@ -34,3 +68,4 @@ export class IgxHierarchicalTransactionService { describe('IgxTransaction UNIT tests', () => { @@ -571,7 +572,11 @@ describe('IgxTransaction', () => { [ { id: 'Key1', - newValue: { key: 'Key1', value1: 10, value3: 30 }, + newValue: { key: 'Key1', value1: 10 }, + type: 'update' + }, { + id: 'Key1', + newValue: { key: 'Key1', value3: 30 }, type: 'update' } ]); @@ -608,5 +613,75 @@ describe('IgxTransaction', () => { expect(trans.getAggregatedChanges(true)).toEqual([]); }); }); + + describe('IgxHierarchicalTransaction UNIT Test', () => { + it('Should set path for each state when transaction is added in Hierarchical data source', () => { + const transaction = new IgxHierarchicalTransactionService(); + expect(transaction).toBeDefined(); + + const path: any[] = ['P1', 'P2']; + const addTransaction: HierarchicalTransaction = { id: 1, type: TransactionType.ADD, newValue: 'Add row', path }; + transaction.add(addTransaction); + expect(transaction.getState(1).path).toBeDefined(); + expect(transaction.getState(1).path.length).toBe(2); + expect(transaction.getState(1).path).toEqual(path); + + path.push('P3'); + const updateTransaction: HierarchicalTransaction = { id: 1, type: TransactionType.UPDATE, newValue: 'Updated row', path }; + transaction.add(updateTransaction, 'Update row'); + expect(transaction.getState(1).path.length).toBe(3); + expect(transaction.getState(1).path).toEqual(path); + }); + + it('Should remove added transaction from states when deleted in Hierarchical data source', () => { + const transaction = new IgxHierarchicalTransactionService(); + expect(transaction).toBeDefined(); + + const path: any[] = []; + let addTransaction: HierarchicalTransaction = { id: 1, type: TransactionType.ADD, newValue: 'Parent row', path }; + transaction.add(addTransaction); + expect(transaction.getState(1).path).toBeDefined(); + expect(transaction.getState(1).path.length).toBe(0); + expect(transaction.getState(1).path).toEqual(path); + + path.push(addTransaction.id); + addTransaction = { id: 2, type: TransactionType.ADD, newValue: 'Child row', path }; + transaction.add(addTransaction); + expect(transaction.getState(2).path).toBeDefined(); + expect(transaction.getState(2).path.length).toBe(1); + expect(transaction.getState(2).path).toEqual(path); + + const deleteTransaction: HierarchicalTransaction = { id: 1, type: TransactionType.DELETE, newValue: null, path: [] }; + transaction.add(deleteTransaction); + expect(transaction.getState(1)).toBeUndefined(); + expect(transaction.getState(2)).toBeUndefined(); + }); + + it('Should mark update transactions state as deleted type when deleted in Hierarchical data source', () => { + const transaction = new IgxHierarchicalTransactionService(); + expect(transaction).toBeDefined(); + + const path: any[] = []; + let updateTransaction: HierarchicalTransaction = { id: 1, type: TransactionType.UPDATE, newValue: 'Parent row', path }; + transaction.add(updateTransaction, 'Original value'); + expect(transaction.getState(1).path).toBeDefined(); + expect(transaction.getState(1).path.length).toBe(0); + expect(transaction.getState(1).path).toEqual(path); + + path.push(updateTransaction.id); + updateTransaction = { id: 2, type: TransactionType.UPDATE, newValue: 'Child row', path }; + transaction.add(updateTransaction, 'Original Value'); + expect(transaction.getState(2).path).toBeDefined(); + expect(transaction.getState(2).path.length).toBe(1); + expect(transaction.getState(2).path).toEqual(path); + + const deleteTransaction: HierarchicalTransaction = { id: 1, type: TransactionType.DELETE, newValue: null, path: [] }; + transaction.add(deleteTransaction); + expect(transaction.getState(1)).toBeDefined(); + expect(transaction.getState(1).type).toBe(TransactionType.DELETE); + expect(transaction.getState(2)).toBeDefined(); + expect(transaction.getState(2).type).toBe(TransactionType.DELETE); + }); + }); }); diff --git a/projects/igniteui-angular/src/lib/services/transaction/igx-transaction.ts b/projects/igniteui-angular/src/lib/services/transaction/igx-transaction.ts index 9363470f940..c8f6e433956 100644 --- a/projects/igniteui-angular/src/lib/services/transaction/igx-transaction.ts +++ b/projects/igniteui-angular/src/lib/services/transaction/igx-transaction.ts @@ -6,8 +6,8 @@ import { isObject, mergeObjects, cloneValue } from '../../core/utils'; @Injectable() export class IgxTransactionService extends IgxBaseTransactionService { protected _transactions: T[] = []; - protected _redoStack: { transaction: T, recordRef: any, useInUndo?: boolean }[] = []; - protected _undoStack: { transaction: T, recordRef: any, useInUndo?: boolean }[] = []; + protected _redoStack: { transaction: T, recordRef: any }[][] = []; + protected _undoStack: { transaction: T, recordRef: any }[][] = []; protected _states: Map = new Map(); get canUndo(): boolean { @@ -26,14 +26,14 @@ export class IgxTransactionService exten this.addTransaction(transaction, states, recordRef); } - private addTransaction(transaction: T, states: Map, recordRef?: any, useInUndo: boolean = true) { + protected addTransaction(transaction: T, states: Map, recordRef?: any) { this.updateState(states, transaction, recordRef); const transactions = this._isPending ? this._pendingTransactions : this._transactions; transactions.push(transaction); if (!this._isPending) { - this._undoStack.push({ transaction, recordRef, useInUndo }); + this._undoStack.push([{ transaction, recordRef }]); this._redoStack = []; this.onStateUpdate.emit(); } @@ -85,11 +85,16 @@ export class IgxTransactionService exten public endPending(commit: boolean): void { this._isPending = false; if (commit) { - let i = 0; - this._pendingStates.forEach((s: S, k: any) => { - this.addTransaction({ id: k, newValue: s.value, type: s.type } as T, this._states, s.recordRef, i === 0); - i++; - }); + const actions: { transaction: T, recordRef: any }[] = []; + for (const transaction of this._pendingTransactions) { + const pendingState = this._pendingStates.get(transaction.id); + this._transactions.push(transaction); + this.updateState(this._states, transaction, pendingState.recordRef); + actions.push({ transaction, recordRef: pendingState.recordRef }); + } + + this._undoStack.push(actions); + this._redoStack = []; } super.endPending(commit); } @@ -129,35 +134,30 @@ export class IgxTransactionService exten return; } - let action: { transaction: T, recordRef: any, useInUndo?: boolean }; - do { - action = this._undoStack.pop(); - this._transactions.pop(); - this._redoStack.push(action); - } while (!action.useInUndo); + const lastActions: { transaction: T, recordRef: any }[] = this._undoStack.pop(); + this._transactions.splice(this._transactions.length - lastActions.length); + this._redoStack.push(lastActions); this._states.clear(); - this._undoStack.map(a => this.updateState(this._states, a.transaction, a.recordRef)); + for (const currentActions of this._undoStack) { + for (const transaction of currentActions) { + this.updateState(this._states, transaction.transaction, transaction.recordRef); + } + } + this.onStateUpdate.emit(); } public redo(): void { if (this._redoStack.length > 0) { - // remove first item from redo stack (it should always has useInUndo === true) - // and then all next items until there are items and useInUndo === false. - // If there are no more items, or next item's useInUndo === true leave. - let undoItem: { transaction: T, recordRef: any, useInUndo?: boolean }; - undoItem = this._redoStack.pop(); - this.updateState(this._states, undoItem.transaction, undoItem.recordRef); - this._transactions.push(undoItem.transaction); - this._undoStack.push(undoItem); - - while (this._redoStack[this._redoStack.length - 1] && !this._redoStack[this._redoStack.length - 1].useInUndo) { - undoItem = this._redoStack.pop(); - this.updateState(this._states, undoItem.transaction, undoItem.recordRef); - this._transactions.push(undoItem.transaction); - this._undoStack.push(undoItem); + let actions: { transaction: T, recordRef: any, useInUndo?: boolean }[]; + actions = this._redoStack.pop(); + for (const action of actions) { + this.updateState(this._states, action.transaction, action.recordRef); + this._transactions.push(action.transaction); } + + this._undoStack.push(actions); this.onStateUpdate.emit(); } } diff --git a/projects/igniteui-angular/src/lib/services/transaction/transaction.ts b/projects/igniteui-angular/src/lib/services/transaction/transaction.ts index 22dba1606d6..5bc55a04357 100644 --- a/projects/igniteui-angular/src/lib/services/transaction/transaction.ts +++ b/projects/igniteui-angular/src/lib/services/transaction/transaction.ts @@ -14,7 +14,7 @@ export interface Transaction { /** @experimental @hidden */ export interface HierarchicalTransaction extends Transaction { - parentId: any; + path: any[]; } export interface State { @@ -25,14 +25,7 @@ export interface State { /** @experimental @hidden */ export interface HierarchicalState extends State { - parentId: any; -} - -/** @experimental @hidden */ -export interface HierarchicalTransactionNode { - id: any; - parentId?: any; - childNodes: HierarchicalTransactionNode[]; + path: any[]; } export interface TransactionService { diff --git a/projects/igniteui-angular/src/lib/test-utils/grid-samples.spec.ts b/projects/igniteui-angular/src/lib/test-utils/grid-samples.spec.ts index d217fc4d5dd..275b492aa74 100644 --- a/projects/igniteui-angular/src/lib/test-utils/grid-samples.spec.ts +++ b/projects/igniteui-angular/src/lib/test-utils/grid-samples.spec.ts @@ -1,6 +1,6 @@ import { Component, TemplateRef, ViewChild, Input } from '@angular/core'; import { IgxGridCellComponent } from '../grids/cell.component'; -import { IgxDateSummaryOperand, IgxNumberSummaryOperand } from '../grids/grid-summary'; +import { IgxDateSummaryOperand, IgxNumberSummaryOperand, IgxSummaryResult } from '../grids/summaries/grid-summary'; import { IGridCellEventArgs, IGridEditEventArgs } from '../grids/grid-base.component'; import { BasicGridComponent, BasicGridSearchComponent, GridAutoGenerateComponent, GridNxMComponent, GridWithSizeComponent, PagingComponent } from './grid-base-components.spec'; @@ -206,7 +206,39 @@ export class ScrollsComponent extends BasicGridComponent { export class SummariesComponent extends BasicGridComponent { data = SampleTestData.foodProductData(); } -/* Maybe add SummaryColumnComponent? */ + +class DealsSummaryMinMax extends IgxNumberSummaryOperand { + constructor() { + super(); + } + + public operate(summaries?: any[]): IgxSummaryResult[] { + const result = super.operate(summaries).filter((obj) => { + if (obj.key === 'min' || obj.key === 'max') { + const summaryResult = obj.summaryResult; + // apply formatting to float numbers + if (Number(summaryResult) === summaryResult) { + obj.summaryResult = summaryResult.toLocaleString('en-us', { maximumFractionDigits: 2 }); + } + return obj; + } + }); + return result; + } +} +@Component({ + template: GridTemplateStrings.declareGrid( + ` [primaryKey]="'ProductID'" [allowFiltering]="true"`, + '', ColumnDefinitions.productDefaultSummaries) +}) +export class SummaryColumnComponent extends BasicGridComponent { + data = SampleTestData.foodProductData(); + public hasSummary = true; + + public numberSummary = new IgxNumberSummaryOperand(); + public dateSummary = new IgxDateSummaryOperand(); + public dealsSummaryMinMax = DealsSummaryMinMax; +} @Component({ template: GridTemplateStrings.declareGrid( @@ -234,7 +266,6 @@ export class VirtualSummaryColumnComponent extends BasicGridComponent { } -/* NoActiveSummariesComponent */ @Component({ template: GridTemplateStrings.declareBasicGridWithColumns(ColumnDefinitions.productBasic) }) @@ -709,3 +740,52 @@ export class GridWithAvatarComponent extends GridWithSizeComponent { data = SampleTestData.personAvatarData(); height = '500px'; } + + +@Component({ + template: `${GridTemplateStrings.declareGrid(`height="1000px" width="900px" [primaryKey]="'ID'"`, '', + ColumnDefinitions.summariesGoupByColumns)}` +}) +export class SummarieGroupByComponent extends BasicGridComponent { + public data = SampleTestData.employeeGroupByData(); + public calculationMode = 'rootAndChildLevels'; + public ageSummary = AgeSummary; + public ageSummaryTest = AgeSummaryTest; +} + +class AgeSummary extends IgxNumberSummaryOperand { + constructor() { + super(); + } + + public operate(summaries?: any[]): IgxSummaryResult[] { + const result = super.operate(summaries).filter((obj) => { + if (obj.key === 'average' || obj.key === 'sum' || obj.key === 'count') { + const summaryResult = obj.summaryResult; + // apply formatting to float numbers + if (Number(summaryResult) === summaryResult) { + obj.summaryResult = summaryResult.toLocaleString('en-us', { maximumFractionDigits: 2 }); + } + return obj; + } + }); + return result; + } +} + +class AgeSummaryTest extends IgxNumberSummaryOperand { + constructor() { + super(); + } + + public operate(summaries?: any[]): IgxSummaryResult[] { + const result = super.operate(summaries); + result.push({ + key: 'test', + label: 'Test', + summaryResult: summaries.filter(rec => rec > 10 && rec < 40).length + }); + + return result; + } +} diff --git a/projects/igniteui-angular/src/lib/test-utils/helper-utils.spec.ts b/projects/igniteui-angular/src/lib/test-utils/helper-utils.spec.ts index fff6e5dd401..4d041aaf9d1 100644 --- a/projects/igniteui-angular/src/lib/test-utils/helper-utils.spec.ts +++ b/projects/igniteui-angular/src/lib/test-utils/helper-utils.spec.ts @@ -9,7 +9,7 @@ export class HelperUtils { public static getCheckboxElement(name: string, element: DebugElement, fix) { const checkboxElements = element.queryAll(By.css('igx-checkbox')); const chkElement = checkboxElements.find((el) => - (el.context as IgxCheckboxComponent).placeholderLabel.nativeElement.innerText === name); + (el.context as IgxCheckboxComponent).placeholderLabel.nativeElement.innerText === name); return chkElement; } @@ -38,7 +38,6 @@ export class HelperUtils { expect(chkInput.checked).toBe(isChecked); } - public static clearOverlay() { const overlays = document.getElementsByClassName('igx-overlay') as HTMLCollectionOf; Array.from(overlays).forEach(element => { @@ -52,46 +51,98 @@ export class HelperUtils { grid: IgxGridComponent, rowStartIndex: number, rowEndIndex: number, - colIndex?: number) => new Promise(async(resolve, reject) => { - const dir = rowStartIndex > rowEndIndex ? 'ArrowUp' : 'ArrowDown'; - const row = grid.getRowByIndex(rowStartIndex); - const cIndx = colIndex || 0; - const colKey = grid.columnList.toArray()[cIndx].field; - let nextRow = dir === 'ArrowUp' ? grid.getRowByIndex(rowStartIndex - 1) : grid.getRowByIndex(rowStartIndex + 1); - const elem = row instanceof IgxGridGroupByRowComponent ? - row : grid.getCellByColumn(row.index, colKey); - if (rowStartIndex === rowEndIndex) { - if (!elem.focused) { - elem.nativeElement.focus(); + colIndex?: number) => new Promise(async (resolve, reject) => { + const dir = rowStartIndex > rowEndIndex ? 'ArrowUp' : 'ArrowDown'; + const row = grid.getRowByIndex(rowStartIndex); + const cIndx = colIndex || 0; + const colKey = grid.columnList.toArray()[cIndx].field; + let nextRow = dir === 'ArrowUp' ? grid.getRowByIndex(rowStartIndex - 1) : grid.getRowByIndex(rowStartIndex + 1); + const elem = row instanceof IgxGridGroupByRowComponent ? + row : grid.getCellByColumn(row.index, colKey); + if (rowStartIndex === rowEndIndex) { + if (!elem.focused) { + elem.nativeElement.focus(); + } + resolve(); + return; } - resolve(); - return; - } - const keyboardEvent = new KeyboardEvent('keydown', { - code: dir, - key: dir - }); + const keyboardEvent = new KeyboardEvent('keydown', { + code: dir, + key: dir + }); - if (dir === 'ArrowDown') { - elem.nativeElement.dispatchEvent(keyboardEvent); - } else { - elem.nativeElement.dispatchEvent(keyboardEvent); - } + if (dir === 'ArrowDown') { + elem.nativeElement.dispatchEvent(keyboardEvent); + } else { + elem.nativeElement.dispatchEvent(keyboardEvent); + } - if (nextRow) { - await wait(10); - HelperUtils.navigateVerticallyToIndex(grid, nextRow.index, rowEndIndex, colIndex) - .then(() => { resolve(); }); - } else { - // else wait for chunk to load. - grid.verticalScrollContainer.onChunkLoad.pipe(take(1)).subscribe({ - next: async() => { - nextRow = dir === 'ArrowUp' ? grid.getRowByIndex(rowStartIndex - 1) : grid.getRowByIndex(rowStartIndex + 1); - HelperUtils.navigateVerticallyToIndex(grid, nextRow.index, rowEndIndex, colIndex) + if (nextRow) { + await wait(10); + HelperUtils.navigateVerticallyToIndex(grid, nextRow.index, rowEndIndex, colIndex) .then(() => { resolve(); }); + } else { + // else wait for chunk to load. + grid.verticalScrollContainer.onChunkLoad.pipe(take(1)).subscribe({ + next: async () => { + nextRow = dir === 'ArrowUp' ? grid.getRowByIndex(rowStartIndex - 1) : grid.getRowByIndex(rowStartIndex + 1); + HelperUtils.navigateVerticallyToIndex(grid, nextRow.index, rowEndIndex, colIndex) + .then(() => { resolve(); }); + } + }); + } + }) + + public static verifyColumnSummaries(summaryRow: DebugElement, summaryIndex: number, summaryLabels, summaryResults) { + const summary = summaryRow.query(By.css('igx-grid-summary-cell[data-visibleindex="' + summaryIndex + '"]')); + expect(summary).toBeDefined(); + const summaryItems = summary.queryAll(By.css('.igx-grid-summary__item')); + if (summaryLabels.length === 0) { + expect(summary.nativeElement.classList.contains('igx-grid-summary--empty')).toBeTruthy(); + expect(summaryItems.length).toBe(0); + } else { + expect(summary.nativeElement.classList.contains('igx-grid-summary--empty')).toBeFalsy(); + expect(summaryItems.length).toEqual(summaryLabels.length); + if (summaryItems.length === summaryLabels.length) { + for (let i = 0; i < summaryLabels.length; i++) { + const summaryItem = summaryItems[i]; + const summaryLabel = summaryItem.query(By.css('.igx-grid-summary__label')); + expect(summaryLabels[i]).toEqual(summaryLabel.nativeElement.textContent.trim()); + if (summaryResults.length > 0) { + const summaryResult = summaryItem.query(By.css('.igx-grid-summary__result')); + expect(summaryResults[i]).toEqual(summaryResult.nativeElement.textContent.trim()); + } } - }); + } } + } + + public static getSummaryRowByDataRowIndex(fix, rowIndex: number) { + return fix.debugElement.query(By.css('igx-grid-summary-row[data-rowindex="' + rowIndex + '"]')); + } - }) + public static getAllVisbleSummariesLength(fix) { + return HelperUtils.getAllVisbleSummaries(fix).length; + } + + public static getAllVisbleSummariesRowIndexes(fix) { + const summaries = HelperUtils.getAllVisbleSummaries(fix); + const rowIndexes = []; + summaries.forEach(summary => { + rowIndexes.push(Number(summary.attributes['data-rowIndex'])); + }); + return rowIndexes.sort((a: number, b: number) => a - b); + } + + public static getAllVisbleSummaries(fix) { + return fix.debugElement.queryAll(By.css('igx-grid-summary-row')); + } + + public static verifyVisbleSummariesHeight(fix, summariesRows, rowHeight = 50) { + const visibleSummaries = HelperUtils.getAllVisbleSummaries(fix); + visibleSummaries.forEach(summary => { + expect(summary.nativeElement.getBoundingClientRect().height).toBeGreaterThanOrEqual(summariesRows * rowHeight); + expect(summary.nativeElement.getBoundingClientRect().height).toBeLessThanOrEqual(summariesRows * rowHeight + 1); + }); + } } diff --git a/projects/igniteui-angular/src/lib/test-utils/sample-test-data.spec.ts b/projects/igniteui-angular/src/lib/test-utils/sample-test-data.spec.ts index a0d5d36a59f..6f83eb33420 100644 --- a/projects/igniteui-angular/src/lib/test-utils/sample-test-data.spec.ts +++ b/projects/igniteui-angular/src/lib/test-utils/sample-test-data.spec.ts @@ -5,8 +5,8 @@ import { ValueData } from '../services/excel/test-data.service.spec'; export class SampleTestData { - private static timeGenerator: Calendar = new Calendar(); - private static today: Date = new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate(), 0, 0, 0); + public static timeGenerator: Calendar = new Calendar(); + public static today: Date = new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate(), 0, 0, 0); // tslint:disable:quotemark public static stringArray = () => ([ @@ -46,14 +46,14 @@ export class SampleTestData { /* Fields: index: number, value: number; 2 items. */ public static numberDataTwoFields = () => ([ - { index: 1, value: 1}, - { index: 2, value: 2} + { index: 1, value: 1 }, + { index: 2, value: 2 } ]) /* Fields: index: number, value: number, other: number, another: number; 2 items. */ public static numberDataFourFields = () => ([ - { index: 1, value: 1, other: 1, another: 1}, - { index: 2, value: 2, other: 2, another: 2} + { index: 1, value: 1, other: 1, another: 1 }, + { index: 2, value: 2, other: 2, another: 2 } ]) /* Fields: Number: number, String: string, Boolean: boolean; Date: date; 3 items. */ @@ -152,7 +152,7 @@ export class SampleTestData { public static personIDNameRegionData = () => ([ { ID: 2, Name: "Jane", LastName: "Brown", Region: "AD" }, { ID: 1, Name: "Brad", LastName: "Williams", Region: "BD" }, - { ID: 6, Name: "Rick", LastName: "Jones", Region: "ACD"}, + { ID: 6, Name: "Rick", LastName: "Jones", Region: "ACD" }, { ID: 7, Name: "Rick", LastName: "BRown", Region: "DD" }, { ID: 5, Name: "ALex", LastName: "Smith", Region: "MlDs" }, { ID: 4, Name: "Alex", LastName: "Wilson", Region: "DC" }, @@ -446,7 +446,7 @@ export class SampleTestData { Released: null }, { - Downloads: 0, + Downloads: 1, ID: 7, ProductName: null, ReleaseDate: SampleTestData.timeGenerator.timedelta(SampleTestData.today, "month", 1), @@ -608,7 +608,7 @@ export class SampleTestData { const data = []; for (let i = 0; i < rowsCount; i++) { const obj = {}; - for (let j = 0; j < cols.length; j++) { + for (let j = 0; j < cols.length; j++) { const col = cols[j].field; obj[col] = 10 * i * j; } @@ -1095,33 +1095,258 @@ export class SampleTestData { } ]) - public static employeePrimaryForeignKeyTreeData = () => ([ + public static employeeTreeDataDisplayOrder = () => ([ { ID: 1, ParentID: -1, Name: 'Casey Houston', JobTitle: 'Vice President', Age: 32 }, { ID: 2, ParentID: 1, Name: 'Gilberto Todd', JobTitle: 'Director', Age: 41 }, { ID: 3, ParentID: 2, Name: 'Tanya Bennett', JobTitle: 'Director', Age: 29 }, + { ID: 7, ParentID: 2, Name: 'Debra Morton', JobTitle: 'Associate Software Developer', Age: 35 }, { ID: 4, ParentID: 1, Name: 'Jack Simon', JobTitle: 'Software Developer', Age: 33 }, { ID: 6, ParentID: -1, Name: 'Erma Walsh', JobTitle: 'CEO', Age: 52 }, - { ID: 7, ParentID: 2, Name: 'Debra Morton', JobTitle: 'Associate Software Developer', Age: 35 }, - { ID: 9, ParentID: 10, Name: 'Leslie Hansen', JobTitle: 'Associate Software Developer', Age: 44 }, - { ID: 10, ParentID: -1, Name: 'Eduardo Ramirez', JobTitle: 'Manager', Age: 53 } + { ID: 10, ParentID: -1, Name: 'Eduardo Ramirez', JobTitle: 'Manager', Age: 53 }, + { ID: 9, ParentID: 10, Name: 'Leslie Hansen', JobTitle: 'Associate Software Developer', Age: 44 } ]) - public static employeeTreeDataDisplayOrder = () => ([ + public static employeePrimaryForeignKeyTreeData = () => ([ { ID: 1, ParentID: -1, Name: 'Casey Houston', JobTitle: 'Vice President', Age: 32 }, { ID: 2, ParentID: 1, Name: 'Gilberto Todd', JobTitle: 'Director', Age: 41 }, { ID: 3, ParentID: 2, Name: 'Tanya Bennett', JobTitle: 'Director', Age: 29 }, - { ID: 7, ParentID: 2, Name: 'Debra Morton', JobTitle: 'Associate Software Developer', Age: 35 }, { ID: 4, ParentID: 1, Name: 'Jack Simon', JobTitle: 'Software Developer', Age: 33 }, { ID: 6, ParentID: -1, Name: 'Erma Walsh', JobTitle: 'CEO', Age: 52 }, - { ID: 10, ParentID: -1, Name: 'Eduardo Ramirez', JobTitle: 'Manager', Age: 53 }, - { ID: 9, ParentID: 10, Name: 'Leslie Hansen', JobTitle: 'Associate Software Developer', Age: 44 } + { ID: 7, ParentID: 2, Name: 'Debra Morton', JobTitle: 'Associate Software Developer', Age: 35 }, + { ID: 9, ParentID: 10, Name: 'Leslie Hansen', JobTitle: 'Associate Software Developer', Age: 44 }, + { ID: 10, ParentID: -1, Name: 'Eduardo Ramirez', JobTitle: 'Manager', Age: 53 } + ]) + + public static employeeTreeDataPrimaryForeignKey = () => ([ + { + ID: 147, + ParentID: -1, + Name: 'John Winchester', + HireDate: new Date(2008, 3, 20), + Age: 55, + OnPTO: false + }, + { + ID: 475, + ParentID: 147, + Name: 'Michael Langdon', + HireDate: new Date(2011, 6, 3), + Age: 43, + OnPTO: false, + Employees: null + }, + { + ID: 957, + ParentID: 147, + Name: 'Thomas Hardy', + HireDate: new Date(2009, 6, 19), + Age: 29, + OnPTO: true, + Employees: undefined + }, + { + ID: 317, + ParentID: 147, + Name: 'Monica Reyes', + HireDate: new Date(2014, 8, 18), + Age: 31, + OnPTO: false + }, + { + ID: 711, + ParentID: 317, + Name: 'Roland Mendel', + HireDate: new Date(2015, 9, 17), + Age: 35, + OnPTO: true, + }, + { + ID: 998, + ParentID: 317, + Name: 'Sven Ottlieb', + HireDate: new Date(2009, 10, 11), + Age: 44, + OnPTO: false, + }, + { + ID: 847, + ParentID: -1, + Name: 'Ana Sanders', + HireDate: new Date(2014, 1, 22), + Age: 42, + OnPTO: false + }, + { + ID: 225, + ParentID: 847, + Name: 'Laurence Johnson', + HireDate: new Date(2014, 4, 4), + OnPTO: true, + Age: 44, + }, + { + ID: 663, + ParentID: 847, + Name: 'Elizabeth Richards', + HireDate: new Date(2017, 11, 9), + Age: 25, + OnPTO: false + }, + + { + ID: 141, + ParentID: 663, + Name: 'Trevor Ashworth', + HireDate: new Date(2010, 3, 22), + OnPTO: false, + Age: 39 + }, + { + ID: 19, + ParentID: -1, + Name: 'Victoria Lincoln', + HireDate: new Date(2014, 1, 22), + Age: 49, + OnPTO: false + }, + { + ID: 15, + ParentID: 19, + Name: 'Antonio Moreno', + HireDate: new Date(2014, 4, 4), + Age: 44, + OnPTO: true, + Employees: [] + }, + { + ID: 17, + ParentID: -1, + Name: 'Yang Wang', + HireDate: new Date(2010, 1, 1), + Age: 61, + OnPTO: false + }, + { + ID: 12, + ParentID: 17, + Name: 'Pedro Afonso', + HireDate: new Date(2007, 11, 18), + Age: 50, + OnPTO: false + }, + { + ID: 101, + ParentID: 12, + Name: 'Patricio Simpson', + HireDate: new Date(2017, 11, 9), + Age: 25, + OnPTO: false, + Employees: [] + }, + { + ID: 99, + ParentID: 12, + Name: 'Francisco Chang', + HireDate: new Date(2010, 3, 22), + OnPTO: true, + Age: 39 + }, + { + ID: 299, + ParentID: 12, + Name: 'Peter Lewis', + HireDate: new Date(2018, 3, 18), + OnPTO: false, + Age: 25 + }, + { + ID: 101, + ParentID: 17, + Name: 'Casey Harper', + HireDate: new Date(2016, 2, 19), + OnPTO: false, + Age: 27 + } ]) + + public static employeeGroupByData = () => ([ + { + ID: 475, + ParentID: 147, + Name: 'Michael Langdon', + HireDate: new Date(2011, 6, 3), + Age: 43, + OnPTO: false, + Employees: null + }, + { + ID: 957, + ParentID: 147, + Name: 'Thomas Hardy', + HireDate: new Date(2009, 6, 19), + Age: 29, + OnPTO: true, + Employees: undefined + }, + { + ID: 317, + ParentID: 147, + Name: 'Monica Reyes', + HireDate: new Date(2014, 8, 18), + Age: 31, + OnPTO: false + }, + { + ID: 225, + ParentID: 847, + Name: 'Laurence Johnson', + HireDate: new Date(2014, 4, 4), + OnPTO: true, + Age: 44, + }, + { + ID: 663, + ParentID: 847, + Name: 'Elizabeth Richards', + HireDate: new Date(2017, 11, 9), + Age: 25, + OnPTO: false + }, + + { + ID: 15, + ParentID: 19, + Name: 'Antonio Moreno', + HireDate: new Date(2014, 4, 4), + Age: 44, + OnPTO: true, + Employees: [] + }, + { + ID: 12, + ParentID: 17, + Name: 'Pedro Afonso', + HireDate: new Date(2007, 11, 18), + Age: 50, + OnPTO: false + }, + { + ID: 101, + ParentID: 17, + Name: 'Casey Harper', + HireDate: new Date(2016, 2, 19), + OnPTO: false, + Age: 27 + } + ]) + + /** * Generates simple array of primitve values * @param rows Number of items to add to the array * @param type The type of the items */ - public static generateListOfPrimitiveValues(rows: number, type: Number|String|Boolean): any[] { + public static generateListOfPrimitiveValues(rows: number, type: Number | String | Boolean): any[] { const data: any[] = []; for (let row = 0; row < rows; row++) { if (type === 'Number') { @@ -1153,7 +1378,7 @@ export class SampleTestData { } } - // tslint:enable:quotemark +// tslint:enable:quotemark export class DataParent { public today: Date = new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate(), 0, 0, 0); diff --git a/projects/igniteui-angular/src/lib/test-utils/template-strings.spec.ts b/projects/igniteui-angular/src/lib/test-utils/template-strings.spec.ts index 4659d8b60c7..b8098c00029 100644 --- a/projects/igniteui-angular/src/lib/test-utils/template-strings.spec.ts +++ b/projects/igniteui-angular/src/lib/test-utils/template-strings.spec.ts @@ -390,6 +390,15 @@ export class ColumnDefinitions { `; + + public static summariesGoupByColumns = ` + + + + + + + `; } export class EventSubscriptions { diff --git a/projects/igniteui-angular/src/lib/test-utils/tree-grid-components.spec.ts b/projects/igniteui-angular/src/lib/test-utils/tree-grid-components.spec.ts index 78258e65cec..f74d1130cb5 100644 --- a/projects/igniteui-angular/src/lib/test-utils/tree-grid-components.spec.ts +++ b/projects/igniteui-angular/src/lib/test-utils/tree-grid-components.spec.ts @@ -1,6 +1,9 @@ import { Component, ViewChild } from '@angular/core'; import { IgxTreeGridComponent } from '../grids/tree-grid/tree-grid.component'; import { SampleTestData } from './sample-test-data.spec'; +import { IgxNumberSummaryOperand, IgxSummaryResult } from '../grids'; +import { IgxTransactionService, IgxHierarchicalTransactionService } from '../../public_api'; +import { IgxGridTransaction } from '../grids/grid-base.component'; @Component({ template: ` @@ -19,7 +22,7 @@ export class IgxTreeGridSortingComponent { @Component({ template: ` - + @@ -258,3 +261,131 @@ export class IgxTreeGridMultiColHeadersComponent { @ViewChild(IgxTreeGridComponent) public treeGrid: IgxTreeGridComponent; public data = SampleTestData.employeeSmallTreeData(); } + + +@Component({ + template: ` + + + + + + + + ` +}) +export class IgxTreeGridSummariesComponent { + @ViewChild(IgxTreeGridComponent) public treeGrid: IgxTreeGridComponent; + public data = SampleTestData.employeeTreeData(); + public ageSummary = AgeSummary; + public ageSummaryTest = AgeSummaryTest; +} + +@Component({ + template: ` + + + + + + + + ` +}) +export class IgxTreeGridSummariesKeyComponent { + @ViewChild(IgxTreeGridComponent) public treeGrid: IgxTreeGridComponent; + public data = SampleTestData.employeeTreeDataPrimaryForeignKey(); + public calculationMode = 'rootAndChildLevels'; + public ageSummary = AgeSummary; + public ageSummaryTest = AgeSummaryTest; +} + +class AgeSummary extends IgxNumberSummaryOperand { + constructor() { + super(); + } + + public operate(summaries?: any[]): IgxSummaryResult[] { + const result = super.operate(summaries).filter((obj) => { + if (obj.key === 'average' || obj.key === 'sum' || obj.key === 'count') { + const summaryResult = obj.summaryResult; + // apply formatting to float numbers + if (Number(summaryResult) === summaryResult) { + obj.summaryResult = summaryResult.toLocaleString('en-us', { maximumFractionDigits: 2 }); + } + return obj; + } + }); + return result; + } +} + +class AgeSummaryTest extends IgxNumberSummaryOperand { + constructor() { + super(); + } + + public operate(summaries?: any[]): IgxSummaryResult[] { + const result = super.operate(summaries); + result.push({ + key: 'test', + label: 'Test', + summaryResult: summaries.filter(rec => rec > 10 && rec < 40).length + }); + + return result; + } +} + +@Component({ + template: ` + + + + + + + ` + , providers: [{ provide: IgxGridTransaction, useClass: IgxTransactionService }], +}) +export class IgxTreeGridRowEditingTransactionComponent { + public data = SampleTestData.employeePrimaryForeignKeyTreeData(); + @ViewChild('treeGrid', { read: IgxTreeGridComponent }) public treeGrid: IgxTreeGridComponent; + public paging = false; +} + +@Component({ + template: ` + + + + + + + + ` +}) +export class IgxTreeGridCustomSummariesComponent { + @ViewChild(IgxTreeGridComponent) public treeGrid: IgxTreeGridComponent; + public data = SampleTestData.employeeTreeData(); + public ageSummary = AgeSummary; + public ageSummaryTest = AgeSummaryTest; +} + +@Component({ + template: ` + + + + + + ` + , providers: [{ provide: IgxGridTransaction, useClass: IgxHierarchicalTransactionService }], +}) +export class IgxTreeGridRowEditingHierarchicalDSTransactionComponent { + public data = SampleTestData.employeeAllTypesTreeData(); + @ViewChild('treeGrid', { read: IgxTreeGridComponent }) public treeGrid: IgxTreeGridComponent; + public paging = false; +} diff --git a/projects/igniteui-angular/src/lib/test-utils/tree-grid-functions.spec.ts b/projects/igniteui-angular/src/lib/test-utils/tree-grid-functions.spec.ts index f2c4512d4c8..707c7be05ab 100644 --- a/projects/igniteui-angular/src/lib/test-utils/tree-grid-functions.spec.ts +++ b/projects/igniteui-angular/src/lib/test-utils/tree-grid-functions.spec.ts @@ -493,4 +493,12 @@ export class TreeGridFunctions { expect(newCell.inEditMode).toBe(true); resolve(); }) + + public static moveGridCellWithTab = + (fix, cell: IgxGridCellComponent) => new Promise(async (resolve, reject) => { + UIInteractions.triggerKeyDownEvtUponElem('Tab', cell.nativeElement, true); + await wait(DEBOUNCETIME); + fix.detectChanges(); + resolve(); + }) } diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 5fb1cbf6bf2..d9516898be0 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -38,16 +38,16 @@ export class AppComponent implements OnInit { icon: 'error', name: 'Badge' }, - { - link: '/bottom-navigation', - icon: 'tab', - name: 'Bottom Navigation' - }, { link: '/banner', icon: 'banner', name: 'Banner' }, + { + link: '/bottom-navigation', + icon: 'tab', + name: 'Bottom Navigation' + }, { link: '/buttonGroup', icon: 'group_work', @@ -88,16 +88,16 @@ export class AppComponent implements OnInit { icon: 'all_out', name: 'Dialog' }, - { - link: '/dropDown', - icon: 'drop_down', - name: 'DropDown' - }, { link: '/drag-drop', icon: 'view_column', name: 'Drag and Drop' }, + { + link: '/dropDown', + icon: 'view_list', + name: 'DropDown' + }, { link: '/expansionPanel', icon: 'expand_more', @@ -118,11 +118,6 @@ export class AppComponent implements OnInit { icon: 'view_column', name: 'Grid Cell Editing' }, - { - link: '/gridConditionalCellStyling', - icon: 'view_column', - name: 'Grid Cell Styling' - }, { link: '/gridColumnGroups', icon: 'view_column', @@ -144,20 +139,25 @@ export class AppComponent implements OnInit { name: 'Grid Column Resizing' }, { - link: '/gridGroupBy', + link: '/gridConditionalCellStyling', icon: 'view_column', - name: 'Grid GroupBy' + name: 'Grid Cell Styling' }, { - link: '/gridPerformance', + link: '/gridGroupBy', icon: 'view_column', - name: 'Grid Performance' + name: 'Grid GroupBy' }, { link: '/gridPercentage', icon: 'view_column', name: 'Grid Percentage' }, + { + link: '/gridPerformance', + icon: 'view_column', + name: 'Grid Performance' + }, { link: '/gridRemoteVirtualization', icon: 'view_column', @@ -188,16 +188,6 @@ export class AppComponent implements OnInit { icon: 'view_column', name: 'Grid Toolbar Custom Content' }, - { - link: '/treeGrid', - icon: 'view_column', - name: 'Tree Grid' - }, - { - link: '/treeGridFlatData', - icon: 'view_column', - name: 'Tree Grid Flat Data' - }, { link: '/icon', icon: 'android', @@ -230,12 +220,12 @@ export class AppComponent implements OnInit { }, { link: '/overlay', - icon: 'overlay', + icon: 'flip_to_front', name: 'Overlay' }, { link: '/overlay-animation', - icon: 'overlay_animation', + icon: 'flip_to_front', name: 'Overlay Animation' }, { @@ -272,6 +262,16 @@ export class AppComponent implements OnInit { link: '/toast', icon: 'android', name: 'Toast' + }, + { + link: '/treeGrid', + icon: 'view_column', + name: 'Tree Grid' + }, + { + link: '/treeGridFlatData', + icon: 'view_column', + name: 'Tree Grid Flat Data' } ]; @@ -291,25 +291,25 @@ export class AppComponent implements OnInit { icon: 'view_quilt', name: 'Layout' }, - { - link: '/ripple', - icon: 'wifi_tethering', - name: 'Ripple' - }, - { - link: '/virtualForDirective', - icon: 'view_column', - name: 'Virtual-For Directive' - }, { link: '/mask', icon: 'view_column', name: 'Mask Directive' }, + { + link: '/ripple', + icon: 'wifi_tethering', + name: 'Ripple' + }, { link: '/tooltip', icon: 'info', name: 'Tooltip' + }, + { + link: '/virtualForDirective', + icon: 'view_column', + name: 'Virtual-For Directive' } ]; @@ -319,15 +319,15 @@ export class AppComponent implements OnInit { icon: 'color_lens', name: 'Colors' }, - { - link: '/typography', - icon: 'font_download', - name: 'Typography' - }, { link: '/shadows', icon: 'layers', name: 'Shadows' + }, + { + link: '/typography', + icon: 'font_download', + name: 'Typography' } ]; diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 5e6cecf79fc..567725a6da1 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -77,6 +77,7 @@ import { TreeGridSampleComponent } from './tree-grid/tree-grid.sample'; import { TreeGridFlatDataSampleComponent } from './tree-grid-flat-data/tree-grid-flat-data.sample'; import { GridColumnPercentageWidthsSampleComponent } from './grid-percentage-columns/grid-percantge-widths.sample'; import { BannerSampleComponent } from './banner/banner.sample'; +import { TreeGridWithTransactionsComponent } from './tree-grid/tree-grid-with-transactions.component'; const components = [ AppComponent, @@ -136,6 +137,7 @@ const components = [ GridWithTransactionsComponent, TreeGridSampleComponent, TreeGridFlatDataSampleComponent, + TreeGridWithTransactionsComponent, CustomContentComponent, ColorsSampleComponent, ShadowsSampleComponent, diff --git a/src/app/grid-cellEditing/grid-cellEditing.component.html b/src/app/grid-cellEditing/grid-cellEditing.component.html index 3e2387f965f..47c3b26215a 100644 --- a/src/app/grid-cellEditing/grid-cellEditing.component.html +++ b/src/app/grid-cellEditing/grid-cellEditing.component.html @@ -13,11 +13,11 @@

Grid with primary key ProductID

- + - + @@ -33,6 +33,7 @@

Grid with primary key ProductID

+

Grid without PK

diff --git a/src/app/grid-cellEditing/grid-cellEditing.component.ts b/src/app/grid-cellEditing/grid-cellEditing.component.ts index af07f1db291..e7ebb1fac0f 100644 --- a/src/app/grid-cellEditing/grid-cellEditing.component.ts +++ b/src/app/grid-cellEditing/grid-cellEditing.component.ts @@ -49,6 +49,14 @@ export class GridCellEditingComponent { }); } + enDisSummaries() { + if (this.gridWithPK.getColumnByName('ReorderLevel').hasSummary) { + this.gridWithPK.disableSummaries([{ fieldName: 'ReorderLevel' }]); + } else { + this.gridWithPK.enableSummaries([{ fieldName: 'ReorderLevel' }]); + } + } + public deleteRow(event, rowID) { event.stopPropagation(); const row = this.gridWithPK.getRowByKey(rowID); diff --git a/src/app/grid-column-moving/grid-column-moving.sample.html b/src/app/grid-column-moving/grid-column-moving.sample.html index 2b1ce8971cc..bd913fad11a 100644 --- a/src/app/grid-column-moving/grid-column-moving.sample.html +++ b/src/app/grid-column-moving/grid-column-moving.sample.html @@ -15,7 +15,7 @@ [rowSelectable]="true" [paging]="false" [width]="'900px'" - [height]="'500px'"> + [height]="'800px'"> diff --git a/src/app/grid-column-moving/grid-column-moving.sample.ts b/src/app/grid-column-moving/grid-column-moving.sample.ts index b7e23ff4e07..8b3b04dc2c0 100644 --- a/src/app/grid-column-moving/grid-column-moving.sample.ts +++ b/src/app/grid-column-moving/grid-column-moving.sample.ts @@ -18,7 +18,7 @@ export class GridColumnMovingSampleComponent implements OnInit { public ngOnInit(): void { this.columns = [ - { field: 'ID', width: 40, resizable: true, movable: true }, + { field: 'ID', width: 80, resizable: true, movable: true }, { field: 'CompanyName', width: 150, resizable: true, movable: true, type: 'string'}, { field: 'ContactName', width: 150, resizable: true, movable: true, type: 'string' }, { field: 'ContactTitle', width: 150, resizable: true, movable: true, type: 'string' }, diff --git a/src/app/grid-groupby/grid-groupby.sample.html b/src/app/grid-groupby/grid-groupby.sample.html index 309564abc22..a4baaf7012a 100644 --- a/src/app/grid-groupby/grid-groupby.sample.html +++ b/src/app/grid-groupby/grid-groupby.sample.html @@ -1,10 +1,13 @@
Toggle Hiding Of Grouped Columns
+
+ +
+ [height]="'700px'" [rowSelectable]='true' [summaryCalculationMode]="summaryMode"> + [hidden]='c.hidden' [sortable]='true' [groupable]='c.groupable' [movable]='true' [pinned]='!!c.pinned' [editable]="true" [filterable]="true" [hasSummary]="true" [dataType]='c.dataType'>