SpringBot add related entities to Data table list view
Context
Model
For this article we will be using our LMS entity model from our Learning Management System (LMS) - Example project as our example. Specifically our Course
entity.

Specifically, our ‘Course’ entity and our ‘Course Category’ entity.
Task
For this article we will be adding the name column from our Course Category
entity to our Course
Data table list to show what category a course has been assigned.
Article Data table list
Currently, the course Data table only describes direct attributes of the Course
entity. This is available at http://localhost:4200/course
.
To add a column displaying the CourseCategory
relation of each course, we will need to add some custom code.
The primary file we will be working with for this task will be the course-crud-list.component.ts
which can be found at clientside/src/app/components/crud/course/list/course-crud-list.component.ts
.
Expands
The first thing we need to update is our expands. Expands allow us to select what data we fetch from our related entities.
-
Imports: Find the protected region in the
course-crud-list.component.ts
file calledAdd any additional imports here
, activate it and add the following:// % protected region % [Add any additional imports here] on begin import { getCourseCategoryModelWithId } from 'src/app/models/courseCategory/course_category.model.selector'; import { map } from 'rxjs/operators'; import {ModelPropertyType} from 'src/app/lib/models/abstract.model'; // % protected region % [Add any additional imports here] end
- In the same file, find the protected region called
Change your default expands if required here
and activate it. This is where we dictate which related data to return to the Data table list. By default we expand on no entities to improve performance. -
Add the following to the protected region activated above:
// % protected region % [Change your default expands if required here] on begin /** * Default references to expand * In CRUD data table, default to expand all the references */ private get defaultExpands(): Expand[] { return [{ name: 'courseCategory', fields: ['id', 'name'] }]; } // % protected region % [Change your default expands if required here] end
Given we wish to display the name, we need to include it in our query. Additionally, for the purposes of retrieving the CourseCategory entity from our store, we also must have its ID. Items that can be used in expands are found within the getRelations()
method with our model. In this case the clientside/src/app/models/course/course.model.ts
file.
For example:
/**
* The relations of the entity
*/
static getRelations(): { [name: string]: ModelRelation } {
return {
...super.getRelations(),
courseLessons: {
type: ModelRelationType.MANY,
name: 'courseLessonsIds',
// % protected region % [Customise your 1-1 or 1-M label for Course Lessons here] off begin
label: 'Course Lessons',
// % protected region % [Customise your 1-1 or 1-M label for Course Lessons here] end
// % protected region % [Customise your display name for Course Lessons here] off begin
displayName: 'order',
// % protected region % [Customise your display name for Course Lessons here] end
validators: [
// % protected region % [Add other validators for Course Lessons here] off begin
// % protected region % [Add other validators for Course Lessons here] end
],
// % protected region % [Add any additional field for relation Course Lessons here] off begin
// % protected region % [Add any additional field for relation Course Lessons here] end
},
courseCategory: {
type: ModelRelationType.ONE,
name: 'courseCategoryId',
// % protected region % [Customise your label for Course Category here] off begin
label: 'Course Category',
// % protected region % [Customise your label for Course Category here] end
// % protected region % [Customise your display name for Course Category here] off begin
// TODO change implementation to use OrderBy or create new metamodel property DisplayBy
displayName: 'name',
// % protected region % [Customise your display name for Course Category here] end
validators: [
// % protected region % [Add other validators for Course Category here] off begin
// % protected region % [Add other validators for Course Category here] end
],
// % protected region % [Add any additional field for relation Course Category here] off begin
// % protected region % [Add any additional field for relation Course Category here] end
},
};
}
We can expand on both courseCategory
and courseLessons
from our Course entity Data table. Fields which can be filtered are the model properties on the each of the Course and Course Categories models.
Header Options
-
In
course-crud-list.component.ts
create a new method calledsortedHeaderOptions
. Find the protected region calledAdd any additional class methods here
and add our method stub as follows:private sortedHeaderOptions(): HeaderOption[] { const headerOpts = this.modelProperties.map(prop => { return { ...prop, sortable: true, sourceDirectFromModel: true, valueSource: prop.name } as HeaderOption; }).filter(opt => opt.name !== 'id' && !opt.doHide); return headerOpts; }
At this stage, we have just reproduced the default behaviour, however, we have also completed the ground work required to customise it further. For example, you may notice the applied filter. This can be used to remove any attribute from the list view by matching the attribute name as it appears in the respective model. In this case we would be referring to
clientside/src/app/models/course/course.model.ts
. -
Now to update our header options to use this new method; find the protected region called
Change your header options required here
, activate it and replace its contents with the following:// % protected region % [Change your header options required here] on begin readonly headerOptions: HeaderOption[] = this.sortedHeaderOptions(); // % protected region % [Change your header options required here] end
Our new method now controls our header options.
-
We now need to customise it by adding our own custom column. We will be adding the following before our return statement:
headerOpts.push({ name: 'courseCategory', displayName: 'Course Category', sortable: true, sourceDirectFromModel: false, type: ModelPropertyType.OBSERVABLE, valueFunction: (model) => { let courseModel = model as CourseModel; if (courseModel.courseCategoryId){ return this.store.select(getCourseCategoryModelWithId, courseModel.courseCategoryId).pipe( map(res => res.name) ); } else { return ''; } } } as HeaderOption);
A couple of important things to notice with addition:
-
displayName
This defines what appears in the column header -
sourceDirectFromModel
We set this to false, as Course Category name is not part of the Course model -
type
This is set toModelPropertyType.OBSERVABLE
as we need to fetch the value from our store. -
valueFunction
Allows us to define how we retrieve the value which as you can see, is simply a retrieval from our store.
Now we have added our custom column, our method now looks as follows:
// % protected region % [Add any additional class methods here] on begin
private sortedHeaderOptions(): HeaderOption[] {
const headerOpts = this.modelProperties.map(prop => {
return {
...prop,
sortable: true,
sourceDirectFromModel: true,
valueSource: prop.name
} as HeaderOption;
}).filter(opt => opt.name !== 'id' && !opt.doHide);
headerOpts.push({
name: 'courseCategory',
displayName: 'Course Category',
sortable: true,
sourceDirectFromModel: false,
type: ModelPropertyType.OBSERVABLE,
valueFunction: (model) => {
let courseModel = model as CourseModel;
if (courseModel.courseCategoryId){
return this.store.select(getCourseCategoryModelWithId, courseModel.courseCategoryId).pipe(
map(res => res.name)
);
} else {
return '';
}
}
} as HeaderOption);
return headerOpts;
}
// % protected region % [Add any additional class methods here] end
We can now see our Course Category name in our Data table.

One to many reference
The previous example demonstrated how to add a reference if the target is a single entity. What if the target is many entities?
To handle this we will need to make some adjustments to the above.
For this example, we will be using the Book entity as it has an outgoing many-to-one relationship with the Article entity. What we will do is provide a count of the number of articles which are associated with a given book.
Primary file we will be working with is the book-tile-crud-list.component.ts
found in clientside/src/app/components/crud/book/list/book-crud-list.component.ts
.
Default expands
-
Find the protected region called
Change your default expands if required here
, activate it and add the following:// % protected region % [Change your default expands if required here] on begin /** * Default references to expand * In CRUD tile, default to expand all the references */ private get defaultExpands(): Expand[] { return [{ name: 'articles', fields: ['id'] }]; } // % protected region % [Change your default expands if required here] end
Again referring to the model, which for this example can be found in clientside/src/app/models/book/book.model.ts
.
Header options
-
Same as shown in the previous example, create a new method called
sortedHeaderOptions
within the protected region calledAdd any additional class methods here
as shown here:// % protected region % [Add any additional class methods here] on begin private sortedHeaderOptions(): HeaderOption[] { const headerOpts = this.modelProperties.map(prop => { return { ...prop, sortable: true, sourceDirectFromModel: true, valueSource: prop.name } as HeaderOption; }).filter(opt => opt.name !== 'id' && !opt.doHide); return headerOpts; } // % protected region % [Add any additional class methods here] end
-
Update the header options attribute to use this method by updating the protected region called
Change your header options required here
as follows:// % protected region % [Change your header options required here] on begin readonly headerOptions: HeaderOption[] = this.sortedHeaderOptions(); // % protected region % [Change your header options required here] end
Again this is as shown in the previous example.
-
Now we add our custom column within our method
sortedHeaderOptions
as follows:headerOpts.push({ name: 'article', displayName: 'Articles', sortable: true, sourceDirectFromModel: false, type: ModelPropertyType.NUMBER, valueFunction: (model) => { return model.articlesIds.length; } } as HeaderOption);
The key differences here are we now refer to our
articlesIds
and set the length, and ourModelPropertyType
is now a number as we no longer fetch from our store. -
The entire method should appear as follows:
// % protected region % [Add any additional class methods here] on begin private sortedHeaderOptions(): HeaderOption[] { const headerOpts = this.modelProperties.map(prop => { return { ...prop, sortable: true, sourceDirectFromModel: true, valueSource: prop.name } as HeaderOption; }).filter(opt => opt.name !== 'id' && !opt.doHide); headerOpts.push({ name: 'article', displayName: 'Articles', sortable: true, sourceDirectFromModel: false, type: ModelPropertyType.NUMBER, valueFunction: (model) => { return model.articlesIds.length; } } as HeaderOption); return headerOpts; } // % protected region % [Add any additional class methods here] end
-
Finally, update our imports:
// % protected region % \[Add any additional imports here\] on begin import { ModelPropertyType } from 'src/app/lib/models/abstract.model'; import { getArticleModels } from 'src/app/models/article/article.model.selector'; import { map } from 'rxjs/operators'; // % protected region % \[Add any additional imports here\] end
We will now see our count for our articles in the Article column as shown here:
-
Now, if we want to show the actual names in the Article column, we have to make some more changes. Firstly we want to update our
defaultExpands
method to include the Article names as follows:// % protected region % [Change your default expands if required here] on begin /** * Default references to expand * In CRUD tile, default to expand all the references */ private get defaultExpands(): Expand[] { return [{ name: 'articles', fields: ['id', 'title'] }]; } // % protected region % [Change your default expands if required here] end -->
-
Now we need to update our custom column, namely updating the type and the value function as follows:
// % protected region % [Add any additional class methods here] on begin private sortedHeaderOptions(): HeaderOption[] { const headerOpts = this.modelProperties.map(prop => { return { ...prop, sortable: true, sourceDirectFromModel: true, valueSource: prop.name } as HeaderOption; }).filter(opt => opt.name !== 'id' && !opt.doHide); headerOpts.push({ name: 'article', displayName: 'Articles', sortable: true, sourceDirectFromModel: false, type: ModelPropertyType.OBSERVABLE, valueFunction: (model) => { let bookModel = model as BookModel; return this.store.select(getArticleModels).pipe( map((res => { return res .filter(article => bookModel.articlesIds.indexOf(article.id) > -1) .map(article => article.title).join(", "); })) ) } } as HeaderOption); return headerOpts; } // % protected region % [Add any additional class methods here] end
As can be seen above, we have updated our type to be an observable, as we are now fetching from the store again. Additionally, our value function fetches all articles from the store and filters them based on their association with the current book. This method will show all associated articles so it may be good for practical implementations to place a limit to avoid excessive overflow.
Our Book Data table now looks like the following.
Solution
Have a look at the add-related-entities branch to see the solution code.
Was this article helpful?