×
Back to book

SpringBot Add Related Entities to CRUD List View

By default the CRUD tiles list view shows the attributes for the entity that it is attached to. Sometimes you may with to add context by showing information from related entities.

Model

For this article we will be using our Fishnatics entity model as our example. Specifically our 'Fish' entity and our 'Tank' entity.

Fishnatics Entity Model

Task

For this article we will be adding the Species column to our Fish CRUD list to show what species a fish is.

Fish CRUD List

This is available at http://localhost:4200/fish/fish-wrapping-tile.

The primary file we will be working with for this task will be the fish-tile-crud-list.component.ts which can be found at clientside/src/app/tiles/crud/fish/list/fish-tile-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.

  1. Imports: Find the protected region in the fish-tile-crud-list.component.ts file called Add any additional imports here, activate it and add the following:
// % protected region % [Add any additional imports here] on begin

import  { getSpeciesModelWithId }  from  'src/app/models/species/species.model.selector';
import  { map }  from  'rxjs/operators';
import  { ModelPropertyType }  from  '../../../../lib/models/abstract.model';

// % protected region % [Add any additional imports here] end
  1. In the same file, find the protected region called Change your default expands if required here and activate it. This is where we dictate what related data to return to the CRUD list. By default we expand on no entities to improve performance.

  2. 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 tile, default to expand all the references
 */
private get defaultExpands(): Expand[] {
    return [{ name: 'species', 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 species entity from our store, we also must have it's ID. Items that can be used in expands are found within the getRelationships() method with our model. In this case theclientside/src/app/models/fish/fish.model.tsfile.

For example:

static getRelations(): { [name: string]: ModelRelation } {
    return {
        ...super.getRelations(),
        species: {
            type: ModelRelationType.ONE,
            name: 'speciesId',
            // % protected region % [Customise your label for Species here] off begin
            label: 'Species',
            // % protected region % [Customise your label for Species here] end
            // % protected region % [Customise your display name for Species here] off begin
            // TODO change implementation to use OrderBy or create new metamodel property DisplayBy
            displayName: 'name',
            // % protected region % [Customise your display name for Species here] end
            validators: [
                // % protected region % [Add other validators for Species here] off begin
                // % protected region % [Add other validators for Species here] end
            ],
            // % protected region % [Add any additional field for relation Species here] off begin
            // % protected region % [Add any additional field for relation Species here] end
        },
        tank: {
            type: ModelRelationType.ONE,
            name: 'tankId',
            // % protected region % [Customise your label for Tank here] off begin
            label: 'Tank',
            // % protected region % [Cust. We now need to customise it.
            // % protected region % [Add any additional field for relation Tank here] off begin
            // % protected region % [Add any additional field for relation Tank here] end
        },
    };
}

We can expand on both species and tank from our Fish entity CRUD tile. Fields that can be filtered are the model properties on the Species and Tank models respectively.

Header Options

  1. Create a new method called sortedHeaderOptions. Find the protected region called Add 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 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/fish/fish.model.ts.

  1. 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.

  1. 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: 'species',
    displayName: 'Species',
    sortable: true,
    sourceDirectFromModel: false,
    type: ModelPropertyType.OBSERVABLE,
    valueFunction: (model) => {
        let castModel = model as FishModel;
        return this.store.select(getSpeciesModelWithId, castModel.speciesId).pipe(
            map(res => res.name)
        );
    }
} 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 species name is not part of the Fish model
  • type This is set to ModelPropertyType.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 that we have added our custom column, Our method now looks like the following:

// % 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: 'species',
        displayName: 'Species',
        sortable: true,
        sourceDirectFromModel: false,
        type: ModelPropertyType.OBSERVABLE,
        valueFunction: (model) => {
            let castModel = model as FishModel;
            return this.store.select(getSpeciesModelWithId, castModel.speciesId).pipe(
                map(res => res.name)
            );
        }
    } as HeaderOption);

    return headerOpts;
}
// % protected region % [Add any additional class methods here] end

We can now see our Species name in our CRUD list.

X 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 Species entity as it has an outgoing many-to-one relationship with the Fish entity. What we will do is provide a count of the number of fish which are associated with a given species.

Primary file we will be working with is the species-tile-crud-list.component.ts found in clientside/src/app/tiles/crud/species/list/species-tile-crud-list.component.ts.

Default Expands

  1. 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: 'fishs', 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/species/species.model.ts.

Header Options

  1. Same as shown in the previous example, create a new method called sortedHeaderOptions within the protected region called Add 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
  1. 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.

  1. Now we add our custom column within our method sortedHeaderOptions as follows:
headerOpts.push({
    name: 'fish',
    displayName: 'Fish',
    sortable: true,
    sourceDirectFromModel: false,
    type: ModelPropertyType.NUMBER,
    valueFunction: (model) => {
        return model.fishsIds.length;
    }
} as HeaderOption);

The key differences here are we now refer to our fishsIds and set the length, and our ModelPropertyType is now a number as we no longer fetch from our store.

  1. 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: 'fish',
        displayName: 'Fish',
        sortable: true,
        sourceDirectFromModel: false,
        type: ModelPropertyType.NUMBER,
        valueFunction: (model) => {
            return model.fishsIds.length;
        }
    } as HeaderOption);

    return headerOpts;
}
// % protected region % [Add any additional class methods here] end
  1. Finally, update our imports:
// % protected region % \[Add any additional imports here\] on begin
import { ModelPropertyType } from '../../../../lib/models/abstract.model';
import { getFishModels } from 'src/app/models/fish/fish.model.selector';
import { map } from 'rxjs/operators';
// % protected region % \[Add any additional imports here\] end

We will now see our count for our fish in the Fish column as shown here:

75d50cd80aeb8dce776b7cb952d3711b

  1. Now, if we want to show the actual names in Fish column, we have to make some more changes. Firstly we want to update our defaultExpands method to include the Fish name 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: 'fishs', fields: ['id', 'name'] }];
}
// % protected region % [Change your default expands if required here] end -->
  1. 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: 'fish',
        displayName: 'Fish',
        sortable: true,
        sourceDirectFromModel: false,
        type: ModelPropertyType.OBSERVABLE,
        valueFunction: (model) => {
            let castModel = model as SpeciesModel;
            return this.store.select(getFishModels).pipe(
                map((res => {
                    return res
                        .filter(fish => castModel.fishsIds.indexOf(fish.id) > -1)
                        .map(fish => fish.name).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 fish from the store and filters them based on their association with the current species. This method will show all associated fish so it may be good for practical implementations to place a limit to avoid excessive overflow.

Our Species CRUD tile now looks like the following.