From 46ab55bb1705a57d42e6bca8013b9bbd8b0e0234 Mon Sep 17 00:00:00 2001 From: Damyan Petev Date: Wed, 29 Mar 2023 08:35:00 +0300 Subject: [PATCH 1/3] feat(elements): expose missing non-input props --- .../src/analyzer/component.ts | 41 +- .../src/analyzer/elements.config.ts | 479 ++++++++++++++++++ .../src/analyzer/printer.ts | 15 +- .../src/analyzer/types.ts | 4 +- .../src/analyzer/utils.ts | 10 +- .../src/app/component-config.ts | 2 + .../src/app/create-custom-element.ts | 26 + .../grids/columns/column-group.component.ts | 8 +- .../grids/columns/column-layout.component.ts | 4 +- .../hierarchical-grid-base.directive.ts | 8 +- .../grids/pivot-grid/pivot-grid.component.ts | 2 +- 11 files changed, 582 insertions(+), 17 deletions(-) diff --git a/projects/igniteui-angular-elements/src/analyzer/component.ts b/projects/igniteui-angular-elements/src/analyzer/component.ts index 87aadf47f47..b1a3c3ed78a 100644 --- a/projects/igniteui-angular-elements/src/analyzer/component.ts +++ b/projects/igniteui-angular-elements/src/analyzer/component.ts @@ -1,6 +1,6 @@ import * as ts from 'typescript'; import type { ComponentMetadata, ContentQuery } from './types'; -import { asString, first, getDecoratorName, getDecorators, getProvidedAs, getTypeExpressionIdentifier, isMethod, isProperty, isPublic } from './utils'; +import { asString, first, getDecoratorName, getDecorators, getProvidedAs, getTypeExpressionIdentifier, isMethod, isProperty, isPublic, isReadOnly } from './utils'; export class AnalyzerComponent { #checker: ts.TypeChecker; @@ -61,6 +61,7 @@ export class AnalyzerComponent { parents, contentQueries: this.#parseQueryProps(), methods: this.publicMethods.map(m => ({ name: m.name })), + additionalProperties: this.additionalProperties.map(p => ({ name: p.name, writable: !isReadOnly(p)})), booleanProperties: this.booleanProperties.map(asString), numericProperties: this.numericProperties.map(asString), templateProperties: this.templateProperties.map(asString), @@ -93,7 +94,7 @@ export class AnalyzerComponent { } /** - * Return all @Input properties of the underlying component. + * Return all `@Input` properties of the underlying component. * * @readonly * @memberof AnalyzerComponent @@ -105,7 +106,7 @@ export class AnalyzerComponent { } /** - * Return all @Output properties of the underlying component. + * Return all `@Output` properties of the underlying component. * * @readonly * @memberof AnalyzerComponent @@ -116,6 +117,22 @@ export class AnalyzerComponent { .filter(prop => getDecorators(first(prop.declarations as any))?.some(isOutput)); } + /** + * Return all leftover exposed properties (non-inputs) + */ + get additionalProperties() { + const isInputOutput = (dec: ts.Decorator) => ['Input', 'Output'].includes(getDecoratorName(dec)); + // TODO: Better handling of collisions with HTMLElement: + const forbiddenNames = ['children']; + + const additionalProperties = this.publicProperties + .filter(prop => !prop.declarations?.some(x => ts.canHaveDecorators(x) && getDecorators(x)?.some(isInputOutput))) + .filter(x => !forbiddenNames.includes(x.name)) + .filter(x => !this.isOverrideOfParentInput(x, this.#component)); + + return additionalProperties; + } + /** * Return all boolean @Input properties of the underlying component. * @@ -235,4 +252,22 @@ export class AnalyzerComponent { return queries; } + + private isOverrideOfParentInput(symbol: ts.Symbol, type: ts.InterfaceType): boolean { + const isInputOutput = (dec: ts.Decorator) => ['Input', 'Output'].includes(getDecoratorName(dec)); + if (ts.getCombinedModifierFlags(symbol.valueDeclaration!) & ts.ModifierFlags.Override) { + const baseTypes = type.getBaseTypes() || []; + // ignore overrides of inherited inputs: + for (const base of baseTypes) { + const baseProp = base.getProperty(symbol.escapedName.toString()); + if (baseProp.valueDeclaration === symbol.valueDeclaration) { + // also inherited + return this.isOverrideOfParentInput(symbol, base as ts.InterfaceType); + } + const isInput = baseProp?.declarations?.some(x => ts.canHaveDecorators(x) && getDecorators(x)?.some(isInputOutput)); + return isInput; + } + } + return false; + } } diff --git a/projects/igniteui-angular-elements/src/analyzer/elements.config.ts b/projects/igniteui-angular-elements/src/analyzer/elements.config.ts index abde82f7626..911612ceb6d 100644 --- a/projects/igniteui-angular-elements/src/analyzer/elements.config.ts +++ b/projects/igniteui-angular-elements/src/analyzer/elements.config.ts @@ -42,6 +42,17 @@ export var registerConfig = [ IgxPivotGridComponent, ], contentQueries: [], + additionalProperties: [ + { name: "totalPages", writable: true }, + { name: "classCosy" }, + { name: "classCompact" }, + { name: "classComfortable" }, + { name: "isLastPage" }, + { name: "isFirstPage" }, + { name: "isFirstPageDisabled" }, + { name: "isLastPageDisabled" }, + { name: "nativeElement" }, + ], methods: ["nextPage", "previousPage", "paginate", "ngDoCheck"], numericProps: ["page", "perPage", "totalRecords"], }, @@ -60,6 +71,7 @@ export var registerConfig = [ isQueryList: true, }, ], + additionalProperties: [{ name: "cdr", writable: true }], methods: ["show", "hide", "ngDoCheck"], boolProps: ["hidden"], }, @@ -67,6 +79,11 @@ export var registerConfig = [ component: IgxGridEditingActionsComponent, parents: [IgxActionStripComponent], contentQueries: [], + additionalProperties: [ + { name: "hasChildren" }, + { name: "buttons", writable: true }, + { name: "strip", writable: true }, + ], methods: ["startEdit", "deleteRowHandler", "addRowHandler"], boolProps: ["addRow", "editRow", "deleteRow", "addChild", "asMenuItems"], provideAs: IgxGridActionsBaseDirective, @@ -75,6 +92,10 @@ export var registerConfig = [ component: IgxGridPinningActionsComponent, parents: [IgxActionStripComponent], contentQueries: [], + additionalProperties: [ + { name: "buttons", writable: true }, + { name: "strip", writable: true }, + ], methods: ["pin", "unpin", "scrollToRow"], boolProps: ["asMenuItems"], provideAs: IgxGridActionsBaseDirective, @@ -91,6 +112,35 @@ export var registerConfig = [ IgxColumnLayoutComponent, ], contentQueries: [], + additionalProperties: [ + { name: "selected", writable: true }, + { name: "autoSize", writable: true }, + { name: "calcPixelWidth", writable: true }, + { name: "index" }, + { name: "defaultMinWidth" }, + { name: "cells" }, + { name: "visibleIndex" }, + { name: "columnGroup" }, + { name: "columnLayout" }, + { name: "columnLayoutChild" }, + { name: "allChildren" }, + { name: "level" }, + { name: "isLastPinned" }, + { name: "isFirstPinned" }, + { name: "rightPinnedOffset" }, + { name: "gridRowSpan" }, + { name: "gridColumnSpan" }, + { name: "defaultWidth", writable: true }, + { name: "widthSetByUser", writable: true }, + { name: "filteringExpressionsTree" }, + { name: "parent", writable: true }, + { name: "grid", writable: true }, + { name: "cdr", writable: true }, + { name: "topLevelParent" }, + { name: "headerCell" }, + { name: "filterCell" }, + { name: "headerGroup" }, + ], methods: [ "getInitialChildColumnSizes", "getFilledChildColumnSizes", @@ -145,6 +195,35 @@ export var registerConfig = [ isQueryList: true, }, ], + additionalProperties: [ + { name: "cells" }, + { name: "selected", writable: true }, + { name: "allChildren" }, + { name: "columnGroup" }, + { name: "columnLayout" }, + { name: "autoSize", writable: true }, + { name: "calcPixelWidth", writable: true }, + { name: "index" }, + { name: "defaultMinWidth" }, + { name: "visibleIndex" }, + { name: "columnLayoutChild" }, + { name: "level" }, + { name: "isLastPinned" }, + { name: "isFirstPinned" }, + { name: "rightPinnedOffset" }, + { name: "gridRowSpan" }, + { name: "gridColumnSpan" }, + { name: "defaultWidth", writable: true }, + { name: "widthSetByUser", writable: true }, + { name: "filteringExpressionsTree" }, + { name: "parent", writable: true }, + { name: "grid", writable: true }, + { name: "cdr", writable: true }, + { name: "topLevelParent" }, + { name: "headerCell" }, + { name: "filterCell" }, + { name: "headerGroup" }, + ], methods: [ "getInitialChildColumnSizes", "getFilledChildColumnSizes", @@ -188,12 +267,14 @@ export var registerConfig = [ component: IgxGridToolbarTitleComponent, parents: [IgxGridToolbarComponent], contentQueries: [], + additionalProperties: [], methods: [], }, { component: IgxGridToolbarActionsComponent, parents: [IgxGridToolbarComponent], contentQueries: [], + additionalProperties: [], methods: [], }, { @@ -207,6 +288,7 @@ export var registerConfig = [ contentQueries: [ { property: "hasActions", childType: IgxGridToolbarActionsComponent }, ], + additionalProperties: [{ name: "nativeElement" }], methods: ["ngDoCheck"], boolProps: ["showProgress"], provideAs: IgxToolbarToken, @@ -233,6 +315,81 @@ export var registerConfig = [ isQueryList: true, }, ], + additionalProperties: [ + { name: "groupsRecords", writable: true }, + { name: "filteredData", writable: true }, + { name: "totalItemCount", writable: true }, + { name: "groupsRowList" }, + { name: "groupAreaTemplate", writable: true }, + { name: "hasGroupableColumns" }, + { name: "dropAreaVisible" }, + { name: "selectedCells" }, + { name: "hasColumnsToAutosize" }, + { name: "headerGroups" }, + { name: "actionStrip", writable: true }, + { name: "emptyFilteredGridTemplate", writable: true }, + { name: "emptyGridDefaultTemplate", writable: true }, + { name: "headerContainer" }, + { name: "headerSelectorContainer" }, + { name: "headerDragContainer" }, + { name: "headerGroupContainer" }, + { name: "filteringRow" }, + { name: "rowExpandedIndicatorTemplate", writable: true }, + { name: "rowCollapsedIndicatorTemplate", writable: true }, + { name: "headerExpandIndicatorTemplate", writable: true }, + { name: "headerCollapseIndicatorTemplate", writable: true }, + { name: "excelStyleHeaderIconDirectiveTemplate", writable: true }, + { name: "sortDescendingHeaderIconDirectiveTemplate", writable: true }, + { name: "dragRowID", writable: true }, + { name: "headerWidth" }, + { name: "shouldGenerate", writable: true }, + { name: "headerGroupsList" }, + { name: "headerCellList" }, + { name: "filterCellList" }, + { name: "rowList" }, + { name: "dataRowList" }, + { name: "activeDescendant" }, + { name: "bannerClass" }, + { name: "hiddenColumnsCount" }, + { name: "pinnedColumnsCount" }, + { name: "transactions" }, + { name: "isPivot", writable: true }, + { name: "filteredSortedData" }, + { name: "validation", writable: true }, + { name: "selectionService", writable: true }, + { name: "colResizingService", writable: true }, + { name: "gridAPI", writable: true }, + { name: "document", writable: true }, + { name: "cdr", writable: true }, + { name: "navigation", writable: true }, + { name: "filteringService", writable: true }, + { name: "summaryService", writable: true }, + { name: "nativeElement" }, + { name: "defaultRowHeight" }, + { name: "defaultHeaderGroupMinWidth" }, + { name: "pinnedWidth" }, + { name: "unpinnedWidth" }, + { name: "pinnedColumns" }, + { name: "pinnedRows" }, + { name: "unpinnedColumns" }, + { name: "visibleColumns" }, + { name: "totalPages" }, + { name: "isFirstPage" }, + { name: "isLastPage" }, + { name: "totalWidth" }, + { name: "pinnedRowHeight" }, + { name: "totalHeight" }, + { name: "hasSortableColumns" }, + { name: "hasEditableColumns" }, + { name: "hasFilterableColumns" }, + { name: "hasSummarizedColumns" }, + { name: "hasMovableColumns" }, + { name: "hasColumnGroups" }, + { name: "hasColumnLayouts" }, + { name: "pinnedDataView" }, + { name: "unpinnedDataView" }, + { name: "dataView" }, + ], methods: [ "getCellByColumnVisibleIndex", "groupBy", @@ -380,6 +537,79 @@ export var registerConfig = [ }, { property: "actionStrip", childType: IgxActionStripComponent }, ], + additionalProperties: [ + { name: "islandToolbarTemplate", writable: true }, + { name: "islandPaginatorTemplate", writable: true }, + { name: "actionStrips", writable: true }, + { name: "data" }, + { name: "filteredData" }, + { name: "selectionService", writable: true }, + { name: "colResizingService", writable: true }, + { name: "document", writable: true }, + { name: "summaryService", writable: true }, + { name: "rowIslandAPI", writable: true }, + { name: "gridAPI", writable: true }, + { name: "hasColumnsToAutosize" }, + { name: "headerGroups" }, + { name: "actionStrip", writable: true }, + { name: "emptyFilteredGridTemplate", writable: true }, + { name: "emptyGridDefaultTemplate", writable: true }, + { name: "headerContainer" }, + { name: "headerSelectorContainer" }, + { name: "headerDragContainer" }, + { name: "headerGroupContainer" }, + { name: "filteringRow" }, + { name: "rowExpandedIndicatorTemplate", writable: true }, + { name: "rowCollapsedIndicatorTemplate", writable: true }, + { name: "headerExpandIndicatorTemplate", writable: true }, + { name: "headerCollapseIndicatorTemplate", writable: true }, + { name: "excelStyleHeaderIconDirectiveTemplate", writable: true }, + { name: "sortDescendingHeaderIconDirectiveTemplate", writable: true }, + { name: "dragRowID", writable: true }, + { name: "headerWidth" }, + { name: "shouldGenerate", writable: true }, + { name: "headerGroupsList" }, + { name: "headerCellList" }, + { name: "filterCellList" }, + { name: "rowList" }, + { name: "dataRowList" }, + { name: "activeDescendant" }, + { name: "bannerClass" }, + { name: "hiddenColumnsCount" }, + { name: "pinnedColumnsCount" }, + { name: "transactions" }, + { name: "isPivot", writable: true }, + { name: "filteredSortedData" }, + { name: "validation", writable: true }, + { name: "cdr", writable: true }, + { name: "navigation", writable: true }, + { name: "filteringService", writable: true }, + { name: "nativeElement" }, + { name: "defaultRowHeight" }, + { name: "defaultHeaderGroupMinWidth" }, + { name: "pinnedWidth" }, + { name: "unpinnedWidth" }, + { name: "pinnedColumns" }, + { name: "pinnedRows" }, + { name: "unpinnedColumns" }, + { name: "visibleColumns" }, + { name: "totalPages" }, + { name: "isFirstPage" }, + { name: "isLastPage" }, + { name: "totalWidth" }, + { name: "pinnedRowHeight" }, + { name: "totalHeight" }, + { name: "hasSortableColumns" }, + { name: "hasEditableColumns" }, + { name: "hasFilterableColumns" }, + { name: "hasSummarizedColumns" }, + { name: "hasMovableColumns" }, + { name: "hasColumnGroups" }, + { name: "hasColumnLayouts" }, + { name: "pinnedDataView" }, + { name: "unpinnedDataView" }, + { name: "dataView" }, + ], methods: [ "isRecordPinnedByIndex", "toggleColumnVisibility", @@ -519,6 +749,81 @@ export var registerConfig = [ isQueryList: true, }, ], + additionalProperties: [ + { name: "toolbarTemplate", writable: true }, + { name: "toolbarOutlet", writable: true }, + { name: "paginatorOutlet", writable: true }, + { name: "filteredData", writable: true }, + { name: "totalItemCount", writable: true }, + { name: "foreignKey" }, + { name: "outletDirective" }, + { name: "selectedCells" }, + { name: "selectionService", writable: true }, + { name: "colResizingService", writable: true }, + { name: "gridAPI", writable: true }, + { name: "document", writable: true }, + { name: "summaryService", writable: true }, + { name: "hasColumnsToAutosize" }, + { name: "headerGroups" }, + { name: "actionStrip", writable: true }, + { name: "emptyFilteredGridTemplate", writable: true }, + { name: "emptyGridDefaultTemplate", writable: true }, + { name: "headerContainer" }, + { name: "headerSelectorContainer" }, + { name: "headerDragContainer" }, + { name: "headerGroupContainer" }, + { name: "filteringRow" }, + { name: "rowExpandedIndicatorTemplate", writable: true }, + { name: "rowCollapsedIndicatorTemplate", writable: true }, + { name: "headerExpandIndicatorTemplate", writable: true }, + { name: "headerCollapseIndicatorTemplate", writable: true }, + { name: "excelStyleHeaderIconDirectiveTemplate", writable: true }, + { name: "sortDescendingHeaderIconDirectiveTemplate", writable: true }, + { name: "dragRowID", writable: true }, + { name: "headerWidth" }, + { name: "shouldGenerate", writable: true }, + { name: "headerGroupsList" }, + { name: "headerCellList" }, + { name: "filterCellList" }, + { name: "rowList" }, + { name: "dataRowList" }, + { name: "activeDescendant" }, + { name: "bannerClass" }, + { name: "hiddenColumnsCount" }, + { name: "pinnedColumnsCount" }, + { name: "transactions" }, + { name: "isPivot", writable: true }, + { name: "filteredSortedData" }, + { name: "validation", writable: true }, + { name: "cdr", writable: true }, + { name: "navigation", writable: true }, + { name: "filteringService", writable: true }, + { name: "nativeElement" }, + { name: "defaultRowHeight" }, + { name: "defaultHeaderGroupMinWidth" }, + { name: "pinnedWidth" }, + { name: "unpinnedWidth" }, + { name: "pinnedColumns" }, + { name: "pinnedRows" }, + { name: "unpinnedColumns" }, + { name: "visibleColumns" }, + { name: "totalPages" }, + { name: "isFirstPage" }, + { name: "isLastPage" }, + { name: "totalWidth" }, + { name: "pinnedRowHeight" }, + { name: "totalHeight" }, + { name: "hasSortableColumns" }, + { name: "hasEditableColumns" }, + { name: "hasFilterableColumns" }, + { name: "hasSummarizedColumns" }, + { name: "hasMovableColumns" }, + { name: "hasColumnGroups" }, + { name: "hasColumnLayouts" }, + { name: "pinnedDataView" }, + { name: "unpinnedDataView" }, + { name: "dataView" }, + ], methods: [ "getCellByColumnVisibleIndex", "getRowByIndex", @@ -649,6 +954,66 @@ export var registerConfig = [ isQueryList: true, }, ], + additionalProperties: [ + { name: "dimensionsSortingExpressions" }, + { name: "columnGroupStates", writable: true }, + { name: "dimensionDataColumns", writable: true }, + { name: "pivotKeys" }, + { name: "isPivot", writable: true }, + { name: "emptyRowDimension" }, + { name: "defaultRowHeight" }, + { name: "selectionService", writable: true }, + { name: "colResizingService", writable: true }, + { name: "document", writable: true }, + { name: "summaryService", writable: true }, + { name: "allDimensions" }, + { name: "filteredData", writable: true }, + { name: "pivotContentCalcWidth" }, + { name: "pivotPinnedWidth" }, + { name: "pivotUnpinnedWidth" }, + { name: "rowDimensions" }, + { name: "columnDimensions" }, + { name: "filterDimensions" }, + { name: "values" }, + { name: "hasColumnsToAutosize" }, + { name: "headerGroups" }, + { name: "emptyFilteredGridTemplate", writable: true }, + { name: "emptyGridDefaultTemplate", writable: true }, + { name: "headerContainer" }, + { name: "headerSelectorContainer" }, + { name: "headerDragContainer" }, + { name: "headerGroupContainer" }, + { name: "filteringRow" }, + { name: "rowExpandedIndicatorTemplate", writable: true }, + { name: "rowCollapsedIndicatorTemplate", writable: true }, + { name: "headerExpandIndicatorTemplate", writable: true }, + { name: "headerCollapseIndicatorTemplate", writable: true }, + { name: "excelStyleHeaderIconDirectiveTemplate", writable: true }, + { name: "sortDescendingHeaderIconDirectiveTemplate", writable: true }, + { name: "headerWidth" }, + { name: "headerGroupsList" }, + { name: "headerCellList" }, + { name: "filterCellList" }, + { name: "rowList" }, + { name: "dataRowList" }, + { name: "activeDescendant" }, + { name: "bannerClass" }, + { name: "filteredSortedData" }, + { name: "validation", writable: true }, + { name: "gridAPI", writable: true }, + { name: "cdr", writable: true }, + { name: "navigation", writable: true }, + { name: "filteringService", writable: true }, + { name: "nativeElement" }, + { name: "defaultHeaderGroupMinWidth" }, + { name: "visibleColumns" }, + { name: "totalWidth" }, + { name: "hasSortableColumns" }, + { name: "hasFilterableColumns" }, + { name: "hasColumnGroups" }, + { name: "hasColumnLayouts" }, + { name: "dataView" }, + ], methods: [ "notifyDimensionChange", "toggleColumn", @@ -728,6 +1093,39 @@ export var registerConfig = [ isQueryList: true, }, ], + additionalProperties: [ + { name: "childrenVisibleIndexes", writable: true }, + { name: "width", writable: true }, + { name: "columnLayout" }, + { name: "visibleIndex" }, + { name: "hasLastPinnedChildColumn" }, + { name: "hasFirstPinnedChildColumn" }, + { name: "cells" }, + { name: "selected", writable: true }, + { name: "allChildren" }, + { name: "columnGroup" }, + { name: "autoSize", writable: true }, + { name: "calcPixelWidth", writable: true }, + { name: "index" }, + { name: "defaultMinWidth" }, + { name: "columnLayoutChild" }, + { name: "level" }, + { name: "isLastPinned" }, + { name: "isFirstPinned" }, + { name: "rightPinnedOffset" }, + { name: "gridRowSpan" }, + { name: "gridColumnSpan" }, + { name: "defaultWidth", writable: true }, + { name: "widthSetByUser", writable: true }, + { name: "filteringExpressionsTree" }, + { name: "parent", writable: true }, + { name: "grid", writable: true }, + { name: "cdr", writable: true }, + { name: "topLevelParent" }, + { name: "headerCell" }, + { name: "filterCell" }, + { name: "headerGroup" }, + ], methods: [ "getInitialChildColumnSizes", "getFilledChildColumnSizes", @@ -771,12 +1169,17 @@ export var registerConfig = [ component: IgxGridToolbarAdvancedFilteringComponent, parents: [IgxGridToolbarComponent], contentQueries: [], + additionalProperties: [{ name: "grid" }], methods: [], }, { component: IgxGridToolbarExporterComponent, parents: [IgxGridToolbarComponent], contentQueries: [], + additionalProperties: [ + { name: "isExporting", writable: true }, + { name: "grid" }, + ], methods: ["export", "ngOnDestroy"], boolProps: ["exportCSV", "exportExcel"], }, @@ -784,6 +1187,7 @@ export var registerConfig = [ component: IgxGridToolbarHidingComponent, parents: [IgxGridToolbarComponent], contentQueries: [], + additionalProperties: [{ name: "grid" }], methods: ["checkAll", "uncheckAll", "ngOnDestroy"], numericProps: ["indentetion"], boolProps: ["hideFilter"], @@ -792,6 +1196,7 @@ export var registerConfig = [ component: IgxGridToolbarPinningComponent, parents: [IgxGridToolbarComponent], contentQueries: [], + additionalProperties: [{ name: "grid" }], methods: ["checkAll", "uncheckAll", "ngOnDestroy"], numericProps: ["indentetion"], boolProps: ["hideFilter"], @@ -818,6 +1223,79 @@ export var registerConfig = [ isQueryList: true, }, ], + additionalProperties: [ + { name: "rootRecords", writable: true }, + { name: "records", writable: true }, + { name: "processedRootRecords", writable: true }, + { name: "processedRecords", writable: true }, + { name: "filteredData", writable: true }, + { name: "selectionService", writable: true }, + { name: "colResizingService", writable: true }, + { name: "gridAPI", writable: true }, + { name: "document", writable: true }, + { name: "cdr", writable: true }, + { name: "navigation", writable: true }, + { name: "filteringService", writable: true }, + { name: "summaryService", writable: true }, + { name: "selectedCells" }, + { name: "hasGroupableColumns" }, + { name: "hasColumnsToAutosize" }, + { name: "headerGroups" }, + { name: "actionStrip", writable: true }, + { name: "emptyFilteredGridTemplate", writable: true }, + { name: "emptyGridDefaultTemplate", writable: true }, + { name: "headerContainer" }, + { name: "headerSelectorContainer" }, + { name: "headerDragContainer" }, + { name: "headerGroupContainer" }, + { name: "filteringRow" }, + { name: "rowExpandedIndicatorTemplate", writable: true }, + { name: "rowCollapsedIndicatorTemplate", writable: true }, + { name: "headerExpandIndicatorTemplate", writable: true }, + { name: "headerCollapseIndicatorTemplate", writable: true }, + { name: "excelStyleHeaderIconDirectiveTemplate", writable: true }, + { name: "sortDescendingHeaderIconDirectiveTemplate", writable: true }, + { name: "dragRowID", writable: true }, + { name: "headerWidth" }, + { name: "shouldGenerate", writable: true }, + { name: "headerGroupsList" }, + { name: "headerCellList" }, + { name: "filterCellList" }, + { name: "rowList" }, + { name: "dataRowList" }, + { name: "activeDescendant" }, + { name: "bannerClass" }, + { name: "hiddenColumnsCount" }, + { name: "pinnedColumnsCount" }, + { name: "isPivot", writable: true }, + { name: "filteredSortedData" }, + { name: "validation", writable: true }, + { name: "nativeElement" }, + { name: "defaultRowHeight" }, + { name: "defaultHeaderGroupMinWidth" }, + { name: "pinnedWidth" }, + { name: "unpinnedWidth" }, + { name: "pinnedColumns" }, + { name: "pinnedRows" }, + { name: "unpinnedColumns" }, + { name: "visibleColumns" }, + { name: "totalPages" }, + { name: "isFirstPage" }, + { name: "isLastPage" }, + { name: "totalWidth" }, + { name: "pinnedRowHeight" }, + { name: "totalHeight" }, + { name: "hasSortableColumns" }, + { name: "hasEditableColumns" }, + { name: "hasFilterableColumns" }, + { name: "hasSummarizedColumns" }, + { name: "hasMovableColumns" }, + { name: "hasColumnGroups" }, + { name: "hasColumnLayouts" }, + { name: "pinnedDataView" }, + { name: "unpinnedDataView" }, + { name: "dataView" }, + ], methods: [ "getCellByColumnVisibleIndex", "ngDoCheck", @@ -932,6 +1410,7 @@ export var registerConfig = [ component: IgxPivotDataSelectorComponent, parents: [], contentQueries: [], + additionalProperties: [{ name: "animationSettings", writable: true }], methods: [], boolProps: [ "columnsExpanded", diff --git a/projects/igniteui-angular-elements/src/analyzer/printer.ts b/projects/igniteui-angular-elements/src/analyzer/printer.ts index 921e0081c0e..e23a66220a8 100644 --- a/projects/igniteui-angular-elements/src/analyzer/printer.ts +++ b/projects/igniteui-angular-elements/src/analyzer/printer.ts @@ -2,7 +2,7 @@ import * as ts from 'typescript'; import * as path from 'node:path'; import * as fs from 'node:fs'; import { format } from 'prettier'; -import type { ComponentMetadata, ContentQuery } from './types'; +import type { ComponentMetadata, ContentQuery, PropertyInfo } from './types'; export class AnalyzerPrinter { @@ -53,11 +53,24 @@ export class AnalyzerPrinter { return ts.factory.createObjectLiteralExpression(properties); } + private createPropertyLiteral(prop: PropertyInfo) { + const properties = [ + ts.factory.createPropertyAssignment('name', ts.factory.createStringLiteral(prop.name)), + ]; + + if (prop.writable) { + properties.push(ts.factory.createPropertyAssignment('writable', ts.factory.createToken(ts.SyntaxKind.TrueKeyword))); + } + + return ts.factory.createObjectLiteralExpression(properties); + } + #createMetaLiteralObject([type, meta]: readonly [ts.InterfaceType, ComponentMetadata]) { const properties = [ ts.factory.createPropertyAssignment('component', ts.factory.createIdentifier(type.symbol.name)), ts.factory.createPropertyAssignment('parents', ts.factory.createArrayLiteralExpression(meta.parents.map(x => ts.factory.createIdentifier(x.symbol.name)))), ts.factory.createPropertyAssignment('contentQueries', ts.factory.createArrayLiteralExpression(meta.contentQueries.map(x => this.#createContentQueryLiteral(x)))), + ts.factory.createPropertyAssignment('additionalProperties', ts.factory.createArrayLiteralExpression(meta.additionalProperties.map(x => this.createPropertyLiteral(x)))), ts.factory.createPropertyAssignment('methods', ts.factory.createArrayLiteralExpression(meta.methods.map(x => ts.factory.createStringLiteral(x.name)))) ]; if (meta.templateProperties?.length) { diff --git a/projects/igniteui-angular-elements/src/analyzer/types.ts b/projects/igniteui-angular-elements/src/analyzer/types.ts index 1195ed6f483..9e04a2b6349 100644 --- a/projects/igniteui-angular-elements/src/analyzer/types.ts +++ b/projects/igniteui-angular-elements/src/analyzer/types.ts @@ -1,6 +1,7 @@ import * as ts from 'typescript'; -export type MethodInfo = { name: string } +export type MethodInfo = { name: string }; +export type PropertyInfo = { name: string, writable?: boolean }; export type ContentQuery = { property: string, @@ -13,6 +14,7 @@ export type ComponentMetadata = { parents: T[], contentQueries: ContentQuery[], methods: MethodInfo[], + additionalProperties: PropertyInfo[]; templateProperties?: string[], booleanProperties?: string[], numericProperties?: string[], diff --git a/projects/igniteui-angular-elements/src/analyzer/utils.ts b/projects/igniteui-angular-elements/src/analyzer/utils.ts index 2b25fc142f8..713bd74bd36 100644 --- a/projects/igniteui-angular-elements/src/analyzer/utils.ts +++ b/projects/igniteui-angular-elements/src/analyzer/utils.ts @@ -40,7 +40,7 @@ export function hasDecorators(node: ts.HasDecorators): boolean { * @param {(ts.ClassDeclaration | ts.PropertyDeclaration)} node the decorated node * @return {*} {readonly} */ -export function getDecorators(node: ts.ClassDeclaration | ts.PropertyDeclaration): readonly ts.Decorator[] { +export function getDecorators(node: ts.HasDecorators): readonly ts.Decorator[] { return hasDecorators(node) ? ts.getDecorators(node)! : []; } @@ -86,6 +86,14 @@ export function isPublic(symbol: ts.Symbol) { return false; } +/** returns if a symbol is either readonly or just a getter equivalent */ +export function isReadOnly(symbol: ts.Symbol) { + const isGetter = (symbol.flags & ts.SymbolFlags.GetAccessor) !== ts.SymbolFlags.None && + (symbol.flags & ts.SymbolFlags.SetAccessor) === ts.SymbolFlags.None; + const readonly = symbol.valueDeclaration && ts.getCombinedModifierFlags(symbol.valueDeclaration) & ts.ModifierFlags.Readonly; + return isGetter || readonly; +} + export function asString(x?: ts.Symbol) { return x ? x.escapedName.toString() : ''; } diff --git a/projects/igniteui-angular-elements/src/app/component-config.ts b/projects/igniteui-angular-elements/src/app/component-config.ts index 360b549f379..ef3d02d8d39 100644 --- a/projects/igniteui-angular-elements/src/app/component-config.ts +++ b/projects/igniteui-angular-elements/src/app/component-config.ts @@ -1,4 +1,5 @@ import { AbstractType, Type } from '@angular/core'; +import { PropertyInfo } from '../analyzer/types'; export interface ContentQueryMeta { property: string; @@ -11,6 +12,7 @@ export interface ComponentConfig { selector?: string; parents: Type[], contentQueries: ContentQueryMeta[]; + additionalProperties: PropertyInfo[]; methods: string[]; templateProps?: string[]; numericProps?: string[]; diff --git a/projects/igniteui-angular-elements/src/app/create-custom-element.ts b/projects/igniteui-angular-elements/src/app/create-custom-element.ts index 22f65b0a1f3..4ecd9f37038 100644 --- a/projects/igniteui-angular-elements/src/app/create-custom-element.ts +++ b/projects/igniteui-angular-elements/src/app/create-custom-element.ts @@ -24,6 +24,32 @@ export function createIgxCustomElement(component: Type, config: IgxNgEle } } + // Reuse `createCustomElement`'s approach for Inputs, should work for any prop too: + componentConfig?.additionalProperties.forEach((p) => { + let set: (v: any) => void | undefined; + + + if (p.name in elementCtor.prototype) { + throw new Error(`Potentially illegal property name ${p.name} defined`); + + } + + if (p.writable) { + set = function (newValue) { + this.ngElementStrategy.setInputValue(p.name, newValue); + } + } + + Object.defineProperty(elementCtor.prototype, p.name, { + get() { + return this.ngElementStrategy.getInputValue(p.name); + }, + set, + configurable: true, + enumerable: true, + }); + }); + // TODO: all 'template' props, setInput check for componentRef!, accumulated Props before init, object componentRef // let propName = 'sortHeaderIconTemplate'; // Object.defineProperty(elementCtor.prototype, propName, { diff --git a/projects/igniteui-angular/src/lib/grids/columns/column-group.component.ts b/projects/igniteui-angular/src/lib/grids/columns/column-group.component.ts index 3cfbb085665..9024febb1d7 100644 --- a/projects/igniteui-angular/src/lib/grids/columns/column-group.component.ts +++ b/projects/igniteui-angular/src/lib/grids/columns/column-group.component.ts @@ -150,11 +150,11 @@ export class IgxColumnGroupComponent extends IgxColumnComponent implements After * * @memberof IgxColumnGroupComponent */ - public get selectable(): boolean { + public override get selectable(): boolean { return this.children && this.children.some(child => child.selectable); } - public set selectable(value: boolean) {} + public override set selectable(value: boolean) {} /** * Returns a reference to the body template. @@ -357,7 +357,7 @@ export class IgxColumnGroupComponent extends IgxColumnComponent implements After * * @memberof IgxColumnGroupComponent */ - public get width() { + public override get width() { const width = `${this.children.reduce((acc, val) => { if (val.hidden) { return acc; @@ -367,7 +367,7 @@ export class IgxColumnGroupComponent extends IgxColumnComponent implements After return width + 'px'; } - public set width(val) { } + public override set width(val) { } /** * @hidden diff --git a/projects/igniteui-angular/src/lib/grids/columns/column-layout.component.ts b/projects/igniteui-angular/src/lib/grids/columns/column-layout.component.ts index 276f317c2ab..1dc9ad993bd 100644 --- a/projects/igniteui-angular/src/lib/grids/columns/column-layout.component.ts +++ b/projects/igniteui-angular/src/lib/grids/columns/column-layout.component.ts @@ -32,12 +32,12 @@ export class IgxColumnLayoutComponent extends IgxColumnGroupComponent implements * * @memberof IgxColumnGroupComponent */ - public get width(): any { + public override get width(): any { const width = this.getFilledChildColumnSizes(this.children).reduce((acc, val) => acc + parseInt(val, 10), 0); return width; } - public set width(val: any) { } + public override set width(val: any) { } public get columnLayout() { return true; diff --git a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid-base.directive.ts b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid-base.directive.ts index beb1e66f69d..d59d25c9e1c 100644 --- a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid-base.directive.ts @@ -112,25 +112,25 @@ export abstract class IgxHierarchicalGridBaseDirective extends IgxGridBaseDirect * @remark * If set, returns the outlet defined outside the grid. Otherwise returns the grid's internal outlet directive. */ - public get outlet() { + public override get outlet() { return this.rootGrid ? this.rootGrid.resolveOutlet() : this.resolveOutlet(); } /** * Sets the outlet used to attach the grid's overlays to. */ - public set outlet(val: any) { + public override set outlet(val: any) { this._userOutletDirective = val; } /** @hidden @internal */ public batchEditingChange: EventEmitter = new EventEmitter(); - public get batchEditing(): boolean { + public override get batchEditing(): boolean { return this._batchEditing; } - public set batchEditing(val: boolean) { + public override set batchEditing(val: boolean) { if (val !== this._batchEditing) { delete this._transactions; this.switchTransactionService(val); diff --git a/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid.component.ts b/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid.component.ts index 3129b315b66..f10a7cfd59d 100644 --- a/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid.component.ts +++ b/projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid.component.ts @@ -874,7 +874,7 @@ export class IgxPivotGridComponent extends IgxGridBaseDirective implements OnIni public set batchEditing(_val: boolean) { } - public get selectedRows(): any[] { + public override get selectedRows(): any[] { if (this.selectionService.getSelectedRows().length === 0) { return []; } From 533be257cfae23d54e194f201c55b1417bfa605b Mon Sep 17 00:00:00 2001 From: Damyan Petev Date: Mon, 3 Apr 2023 19:46:06 +0300 Subject: [PATCH 2/3] fix(elements): properly exclude additional props from deep inheritance --- .../src/analyzer/component.ts | 26 +++++++++---------- .../src/analyzer/elements.config.ts | 1 - .../src/analyzer/utils.ts | 7 ++++- .../src/app/create-custom-element.ts | 2 +- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/projects/igniteui-angular-elements/src/analyzer/component.ts b/projects/igniteui-angular-elements/src/analyzer/component.ts index b1a3c3ed78a..409381bcdb8 100644 --- a/projects/igniteui-angular-elements/src/analyzer/component.ts +++ b/projects/igniteui-angular-elements/src/analyzer/component.ts @@ -1,6 +1,11 @@ import * as ts from 'typescript'; import type { ComponentMetadata, ContentQuery } from './types'; -import { asString, first, getDecoratorName, getDecorators, getProvidedAs, getTypeExpressionIdentifier, isMethod, isProperty, isPublic, isReadOnly } from './utils'; +import { asString, first, getDecoratorName, getDecorators, getProvidedAs, getTypeExpressionIdentifier, isMethod, isOverride, isProperty, isPublic, isReadOnly } from './utils'; + + +const isInput = (dec: ts.Decorator) => getDecoratorName(dec).includes('Input'); +const isOutput = (dec: ts.Decorator) => getDecoratorName(dec).includes('Output'); +const isInputOutput = (dec: ts.Decorator) => ['Input', 'Output'].includes(getDecoratorName(dec)); export class AnalyzerComponent { #checker: ts.TypeChecker; @@ -100,7 +105,6 @@ export class AnalyzerComponent { * @memberof AnalyzerComponent */ get inputProperties() { - const isInput = (dec: ts.Decorator) => getDecoratorName(dec).includes('Input'); return this.publicProperties .filter(prop => getDecorators(first(prop.declarations as any))?.some(isInput)); } @@ -112,7 +116,6 @@ export class AnalyzerComponent { * @memberof AnalyzerComponent */ get outputProperties() { - const isOutput = (dec: ts.Decorator) => getDecoratorName(dec).includes('Output'); return this.publicProperties .filter(prop => getDecorators(first(prop.declarations as any))?.some(isOutput)); } @@ -121,7 +124,6 @@ export class AnalyzerComponent { * Return all leftover exposed properties (non-inputs) */ get additionalProperties() { - const isInputOutput = (dec: ts.Decorator) => ['Input', 'Output'].includes(getDecoratorName(dec)); // TODO: Better handling of collisions with HTMLElement: const forbiddenNames = ['children']; @@ -254,18 +256,16 @@ export class AnalyzerComponent { } private isOverrideOfParentInput(symbol: ts.Symbol, type: ts.InterfaceType): boolean { - const isInputOutput = (dec: ts.Decorator) => ['Input', 'Output'].includes(getDecoratorName(dec)); - if (ts.getCombinedModifierFlags(symbol.valueDeclaration!) & ts.ModifierFlags.Override) { - const baseTypes = type.getBaseTypes() || []; - // ignore overrides of inherited inputs: - for (const base of baseTypes) { + if (isOverride(symbol)) { + // should resolve a single base for classes + const base = first(type.getBaseTypes() || []); + if (base?.isClass()) { const baseProp = base.getProperty(symbol.escapedName.toString()); - if (baseProp.valueDeclaration === symbol.valueDeclaration) { + if (isOverride(baseProp)) { // also inherited - return this.isOverrideOfParentInput(symbol, base as ts.InterfaceType); + return this.isOverrideOfParentInput(baseProp, base); } - const isInput = baseProp?.declarations?.some(x => ts.canHaveDecorators(x) && getDecorators(x)?.some(isInputOutput)); - return isInput; + return baseProp?.declarations?.some(x => ts.canHaveDecorators(x) && getDecorators(x)?.some(isInputOutput)); } } return false; diff --git a/projects/igniteui-angular-elements/src/analyzer/elements.config.ts b/projects/igniteui-angular-elements/src/analyzer/elements.config.ts index 911612ceb6d..b4b2000703c 100644 --- a/projects/igniteui-angular-elements/src/analyzer/elements.config.ts +++ b/projects/igniteui-angular-elements/src/analyzer/elements.config.ts @@ -1095,7 +1095,6 @@ export var registerConfig = [ ], additionalProperties: [ { name: "childrenVisibleIndexes", writable: true }, - { name: "width", writable: true }, { name: "columnLayout" }, { name: "visibleIndex" }, { name: "hasLastPinnedChildColumn" }, diff --git a/projects/igniteui-angular-elements/src/analyzer/utils.ts b/projects/igniteui-angular-elements/src/analyzer/utils.ts index 713bd74bd36..47402202261 100644 --- a/projects/igniteui-angular-elements/src/analyzer/utils.ts +++ b/projects/igniteui-angular-elements/src/analyzer/utils.ts @@ -17,7 +17,7 @@ export function readTSConfig() { export function first(arr?: T[]) { - return arr!.at(0) as T; + return arr!.at(0); } /** @@ -94,6 +94,11 @@ export function isReadOnly(symbol: ts.Symbol) { return isGetter || readonly; } +/** returns if a symbol has an override modifier */ +export function isOverride(symbol: ts.Symbol) { + return (ts.getCombinedModifierFlags(symbol.valueDeclaration!) & ts.ModifierFlags.Override) !== ts.ModifierFlags.None; +} + export function asString(x?: ts.Symbol) { return x ? x.escapedName.toString() : ''; } diff --git a/projects/igniteui-angular-elements/src/app/create-custom-element.ts b/projects/igniteui-angular-elements/src/app/create-custom-element.ts index 4ecd9f37038..f47465d5704 100644 --- a/projects/igniteui-angular-elements/src/app/create-custom-element.ts +++ b/projects/igniteui-angular-elements/src/app/create-custom-element.ts @@ -30,7 +30,7 @@ export function createIgxCustomElement(component: Type, config: IgxNgEle if (p.name in elementCtor.prototype) { - throw new Error(`Potentially illegal property name ${p.name} defined`); + throw new Error(`Potentially illegal property name ${p.name} defined for ${component.name}`); } From f2a420563d7a656620734cd6cf6d42e1bb1298d1 Mon Sep 17 00:00:00 2001 From: Damyan Petev Date: Tue, 20 Jun 2023 09:42:35 +0300 Subject: [PATCH 3/3] chore(elements): regenerate config --- .../src/analyzer/elements.config.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/projects/igniteui-angular-elements/src/analyzer/elements.config.ts b/projects/igniteui-angular-elements/src/analyzer/elements.config.ts index 52067cf0bf6..3446202d870 100644 --- a/projects/igniteui-angular-elements/src/analyzer/elements.config.ts +++ b/projects/igniteui-angular-elements/src/analyzer/elements.config.ts @@ -279,6 +279,7 @@ export var registerConfig = [ { name: "gridAPI" }, { name: "cdr" }, { name: "navigation", writable: true }, + { name: "virtualizationState" }, { name: "nativeElement" }, { name: "defaultRowHeight" }, { name: "defaultHeaderGroupMinWidth" }, @@ -545,6 +546,7 @@ export var registerConfig = [ { name: "validation" }, { name: "cdr" }, { name: "navigation", writable: true }, + { name: "virtualizationState" }, { name: "nativeElement" }, { name: "defaultRowHeight" }, { name: "defaultHeaderGroupMinWidth" }, @@ -726,6 +728,7 @@ export var registerConfig = [ { name: "gridAPI" }, { name: "cdr" }, { name: "navigation", writable: true }, + { name: "virtualizationState" }, { name: "nativeElement" }, { name: "defaultHeaderGroupMinWidth" }, { name: "visibleColumns" }, @@ -849,6 +852,7 @@ export var registerConfig = [ { name: "validation" }, { name: "cdr" }, { name: "navigation", writable: true }, + { name: "virtualizationState" }, { name: "nativeElement" }, { name: "defaultRowHeight" }, { name: "defaultHeaderGroupMinWidth" }, @@ -996,6 +1000,7 @@ export var registerConfig = [ { name: "gridAPI" }, { name: "cdr" }, { name: "navigation", writable: true }, + { name: "virtualizationState" }, { name: "nativeElement" }, { name: "defaultRowHeight" }, { name: "defaultHeaderGroupMinWidth" },