Skip to main content

Angular Expanders 7.0.0

· 13 min read
Jan Hardy
Jan Hardy
R&D Engineer

Changes and improvements

Upgrade to Angular 21 and Angular Material 21

For this release we updated all the code to Angular 21, in order to stay up to date with the releases and best practices.

Added DataProjection support

A mechanism has been implemented to facilitate fetching of different DataProjections if the used control layer allows this (std-api). The defaults are set to details for getSingle calls and info for getList calls. This implementation also allows the use of calculated fields with the info projection now.

tip

Double check in your application if the same table columns are still shown. The option cruds.table.hide is now also taken into account.

For the moment, the possible projections are set by a ModelLoadingListener that defaults to the info ($DataConnector$-table.model.ts) and details ($DataConnector$.model.ts) projections. In the future it will be possible to manually define other projections as well.

Added Finder support

We have added a FinderConnector and QueryConnector that implements the new abstract FilterConnector. This allows to support finders as well as querysearch at the same time (depending on what control layer you are using).

The option hasSearchBar has also been ported. Like in KO this will expand a searchbar in the header of the list page connected to a finder defined by the specified value of the option.

Related to the addition of a universal Filter feature, previously only one querysearch was supported to be used as a filter. This has been changed to any amount of querysearches and finders.

Added Authorization support

We have added AuthorizationRights to make certain parts of the application not accessible (used in guards, directives, services). These rights can be accessed through the ACTION_AUTHORIZATION_SOURCE InjectionToken, the interface can be provided by a control layer specific implementation, or you can provide your own. Some directives have been added to handle authorization on the most common used components

  • ButtonAuthorizationDirective
  • TableRowActionMenuAuthorizationDirective

These directives handle disabling components as well as showing tooltips on why it is disabled. You can look at these as examples when wanting to use the authorization feature somewhere else.

Added support for swapping out default add/edit with DataOperations

Using the following tags you can now conditionally turn of some expanders:

  • #angular.data-view.action.add.custom: disable default add page.
  • #angular.data-view.action.edit.custom: disable default edit page.
  • #angular.data-view.action.delete.custom: disable default delete dialog.
  • #angular.data-view.routing.add.disable: disable the add route.
  • #angular.data-view.routing.edit.disable: disable the edit route.
  • Added feature anchors to remaining DataView actions (add, edit, delete, details, list).

Refactored use of HttpClient

We have added an API specific HttpClient. This allows for easy changing of authentication headers, error handling etc. per different API implemented. The implicit behavior of the http interceptors is also now removed. To facilitate the removal of the use of interceptors a lot of operator functions that handled the logic of the interceptors have been created.

  • handleDataAccessError
  • handleAlertError
  • handleDataAccessFormError
  • notifyDataConnectorSuccessEvent
  • notifyDataConnectorFailureEvent
Important

The new client is still based on the angular HttpClient and all functionality of this client can still be used.

Metamodel changes and additions

Metamodel additions of abstract Connector and View. This allows for easier extension of different types of views (e.g. ListView, InstanceView, FilterView) or connectors (e.g. DataProjectionConnector, DataOperationConnector, ...).

For more added features check out the changelog
  • Allowed file type validation
  • Dynamic sidebar support
  • New functions folder in structure
  • Resolved filename encoding issues
  • Added error handling for std-api

Migration guide

Upgrade to Angular 21

Normally you shouldn't have any issues, you can look at the migration guide to see what changes in custom code need to be done.

Some notable changes are:

  • We removed @angular/animations, this is being deprecated. However, if necessary it can be re-added as custom code in the package.json5 file and provided in the app.config.ts.
  • Control flow syntax migration, Angular is planning on removing the ngIf, ngFor, ... directives in Angular 22. They deprecated them a while ago, so it is best to start switching to the control flow syntax.
  • Lastly, we also upgraded to version 17.0.0 of ngx-translate. They deprecated some methodes and the direct calling of variables in their services. You can look at their migration guide for the preferred changes.
Stricter type inference

Type checking in templates have become stricter, and you will potentially have to explicitly define some types in your TypeScript files.

Added DataProjection support

A ListView element is automatically linked to a DataProjectionConnector for the info projection. If you want to change this back to the previous default of details, you can do this by using the option angular.legacy.table. useModelProjection. This option will stay in place until we move away for automatically defining the model with ModelLoadingListeners and start defining the model explicitly.

Option
angular.legacy.table.useModelProjection DataConnectorFeatureModuleAngularAppCascading

Instead of the new info projection, use the previously used details projection.

<options>
<angular.legacy.table.useModelProjection/>
</options>

While upgrading it is possible that the newly used info projection does not contain some of the fields being called in custom code. You can either (1) add these fields to the info projection or (2) go back to the details projection. Often times you can rely on duck typing to handle it correctly, e.g. when needing only the id.

The DataAccess interface has undergone some changes. The defaults allow for no breaking changes in the custom code. However, the %dataConnector%ListModel has been changed with a generic PaginatedList model from the ns-core runtime library. The expander for this model has also been removed.

Before
getSingle({ id }: { id: string }): Observable<%dataConnector%Model>;
getList({ segmentParameters }: { segmentParameters?: %dataConnector%SegmentParameters }): Observable<%dataConnector%ListModel>;
After
getSingle<T extends keyof %dataConnector%Projection = '%dataConnector%'>({ id, projection }: { id: string, projection?: T }): Observable<%dataConnector%Projection[T]>;
getList<T extends keyof %dataConnector%Projection = '%dataConnector%'>({ segmentParameters, projection }: { segmentParameters?: %dataConnector%SegmentParameters, projection?: T }): Observable<PaginatedList<%dataConnector%Projection[T]>>;

The option angular.isTableColumn has been removed together with transmuter adding this to all the fields having the isInfoField attribute. The option is not necessary anymore because this is now handled by the info projection. A new transmuter has been added that removes this option everywhere and sets theisInfoField attribute to true.

Transmutation
TransmuteIsTableColumnOptionApplication

Refactors all angular.isTableColumn options to info projection. If the option is not present isInfoField will be set false.

To make the DataSource default to a certain projection, the InjectionToken for the DataSource needed to get a type. For this reason we moved the DataView related tokens to a different file DataView-view.tokens. Namely, a file expanded by DataView instead of DataConnector. If you don't want change the imports in custom code right away, you can get the tokens back in the original place by using the option angular.legacy.tokens.

When you have a custom token defined, you should add the type now. This is necessary otherwise the DataSource cannot be used with multiple projections.

Option
angular.legacy.tokens DataConnectorFeatureModuleAngularAppCascading

Make the moved 'view.tokens' available in original '.tokens' file.

<options>
<angular.legacy.tokens/>
</options>

Lastly, when using a *-data-access.service directly, the defaults of the used projection is not respected. This means that custom code will break, because now you need to define a projection. However, when using the correct *_DATA_ACCESS InjectionToken this is handled correctly. This will also allow to easily swap implementations later on.

Future best practice

We have noticed that having a separate tokens file is actually not necessary. We will start combining the InjectionToken with the interface in the same file.

Added Finder support

To make the finders work together with querysearch, some things had to change.

  1. The previous standard querysearch model (now filter) has been changed from an interface to a class. Any places in the code where object instancing is used to create a filter object needs to be changed to a new class instance using:
  const filter = new FooBarFilter();
filter.foo = this.barService.getBarValue();

or wrapping the object in the fromObject() function expanded in the new class:

const filter = FooBarFilter.fromObject({obj: {foo: this.barService.getBarValue()}});
  1. The token injecting the InfiniteDataSource used for populating the dropdowns in the forms has been changed from <...>FILTER_DATA_SOURCE to <...>FINDER_FILTER_DATA_SOURCE and <...>QUERY_FILTER_DATA_SOURCE for finders and queryssearches respectively.
  2. When upgrading from a previous version your previously defined querysearch model file used will have been renamed from <...>Filters to <...>Filter resulting in possible breaking code in custom anchors. You can safely assume that changing <...>Filters to <...>Filter wil fix most issues in custom code and custom imports.
  3. Some imports could have been removed/added from the expanded files resulting in missing/duplicate imports.
  4. It could be possible that when a DataChild is defined in the model, and you do not have a valid field in the filter of the child pointing to the parent that code will be expanded that will not compile. If you run into this problem, add a field to your filter pointing to the parent.
  5. The FilterOptions have been moved from being calculated in their FilterOptionResolver to being calculated directly in their respective FilterFormComponent(s). If you have any custom code on the list page interacting with filterOptions this will have to be moved to the FilterFormComponent(s)
Legacy view

We understand that some project do not want to immediately change from how filtering previously looked. For this we added a #angular.filters.legacy root tag. When this is added in your profile, it will attempt to keep the code and visuals as close as possible to the previous querysearch filtering implementation. However, the new classes and names for the filter models will still be used resulting in possible breaking changes as explained above.

Refactored use of HttpClient

The use of angular interceptors have been removed and moved to the specific clients. When you use the generic angular httpClient the following will no longer occur

  1. Error mapping (check if error from svc-api and map it to internal error structure)
  2. Error handling (directly act on an error, e.g. 404)
  3. File download error handling
  4. Authentication header addition Either change to the new expanded api specific HttpClient (preferred) or add these interceptors again as custom code while awaiting the refactor.
Migration Example: Handling errors explicitly
Before
return this.httpClient
.get<X>({url: url})
.pipe(
map((document) => this.xModelMapper.map(document))
);
After
return this.httpClient
.get<X>({url: url})
.pipe(
map((document) => this.xProjectionMapper.map({document})),
handleDataAccessError({ injector: this.injector }),
handleAlertError({ injector: this.injector })
);
Migration Example: New interface declaration
Before
.get(url, {headers: headers, params: params})
After
get({url: url, options: options})

Some imports are no longer default expanded. These should be added custom if needed in *-data-access.service.ts.

  • catchError
  • AlertErrorModel
  • throwError
  • EMPTY
  • tap
  • filter
Migration Example: Using new HttpClient methods
Before
return this.httpClient.get(url, {
params: params,
observe: 'response',
responseType: 'blob',
});
}
After
return this.httpClient.downloadFile({
url,
options
}).pipe(
filter(response => response.type == HttpEventType.Response),
handleDataAccessError({ injector: this.injector }),
handleAlertError({ injector: this.injector })
);
}
Migration Example: Using new Event Operators
Before
create({ model }: { model: DemoElementModel }): Observable<{ id: string }> {
let headers = new HttpHeaders();
let data = this.demoElementCreateDocumentMapper.map({ model });
// anchor:custom-create:start
// anchor:custom-create:end
return this.httpClient.post<{ id: string }>(url, data, {headers: headers}).pipe(
map(response => ({id: response.id})),
tap(response => {
this.demoElementEventService.updateEvents({
event: {
objectIdentifier: response.id,
type: 'create'
}
})
}),
catchError(error => {
this.demoElementEventService.updateEvents({
event: {
objectIdentifier: '',
type: 'create-failed'
}
});
// anchor:custom-create-error:start
// anchor:custom-create-error:end
if (error instanceof AlertErrorModel) {
this.alertErrorHandler.handleError({error: error});
return EMPTY;
}
return throwError(() => error);
})
);
}

After
create({ model }: { model: DemoElementModel }): Observable<{ id: string }> {
const url = '/v1/demoelements';
const options: HttpClientOptions = {};
let data = this.demoElementCreateDocumentMapper.map({ model });
// anchor:custom-create:start
// anchor:custom-create:end
return this.httpClient.post<{ id: string }>({
url: url,
body: data,
options: options
}).pipe(
map(response => ({id: response.id})),
// @anchor:create-operators:start
notifyDemoElementSuccessEvent({
injector: this.injector,
event: {
type: 'create'
}
}),
notifyDemoElementFailureEvent({
injector: this.injector,
event: {
type: 'create-failed'
}
}),
handleDataAccessFormError({ injector: this.injector }),
handleDataAccessError({ injector: this.injector }),
handleAlertError({ injector: this.injector })
// @anchor:create-operators:end
);
}

A breaking change has been introduced in the event.model.ts interface where the objectIdentifier attribute has been made optional. This potentially results in some custom code changes to handle the possible undefined nature.

Modeling endpoints

Try to use DataOperations instead of custom endpoint calls. This will make migration in the future a lot easier, and you will have all the endpoints defined in your model.

Module-metamodel absorbed in prime-core

If your project started using the module-metamodel to declare dependencies between specific FeatureModules, you should refactor how these dependencies are defined. The module-metamodel has been absorbed in prime-core and received a new way of declaration.

  1. First of, you can remove the angularAppModules.xml file.
  2. In your angular-app.xml you should change the <featureModules> attribute with <dependencies>.
  3. Add a type to your featureModule.xml.
Before
<angularApp xmlns="https://schemas.normalizedsystems.org/xsd/angularProjects/7/0/0">
<name>space-app</name>
<featureModules>
<featureModule>demo</featureModule>
</featureModules>
</angularApp>

<featureModule xmlns="https://schemas.normalizedsystems.org/xsd/angularProjects/7/0/0">
<name>demo</name>
</featureModule>

After
<angularApp xmlns="https://schemas.normalizedsystems.org/xsd/angularProjects/7/0/0">
<name>space-app</name>
<dependencies>
<dependency>
<to>local::angular/featureModules/demo/model/demo</to>
<scope>expansion</scope>
</dependency>
</dependencies>
</angularApp>

<featureModule type="angularProjects::FeatureModule" xmlns="https://schemas.normalizedsystems.org/xsd/angularProjects/7/0/0">
<name>demo</name>
</featureModule>

Folder structure
project
├── conf
├── applications
├── angular
. ├── angularApps
. ├── space-app
. . ├── model
. . └── space-app.xml
. .
. └── angularAppModules.xml (to be removed)
├── featureModules
. ├── demo
. . ├── model
. . └── demo.xml