Developer Docs

Custom Tiles in SpringBot

It is easy to add custom tiles should you require more fine grain control over your UI elements.
For this project, we will be using the Codebots LMS Project. You can follow along with the project by downloading the latest version from the public git page.

User Story: as a content consumer I would like to see some information on the amount and type of library content available so I can plan my learning goals.

Task: Create a custom tile with a chart to display a simple graph of article, lesson and course counts. Here we will use ChartJS for charts and graphs.

If you havent already installed ChartJS, check out our article Custom JavaScript Library with SpringBot to do so.

In springbot we use the NgRx libraries which are inspired by the Redux pattern to handle state management in your application.

Creating the Tile

On the platform we have created a page callled “Stats” with a custom area for our custom tile to go.

Stats Page with Custom Tile

This will create a custom protected region for our refrence code as can be seen by navigating to clientside/src/app/pages/stats/stats.page.component.html

Library Content Graph

If we were creating a tile that didnt need replication, we would simply add our custom code with this region.

However for this case, we would like to create a tile that can be reused throughout our project. As such we will be creating a new tile and then linking it to our stats page through this protected region. For this instance we will be using a group of libraries called NgRx which are inspired by the redux pattern

Implementation

To start, you will need a folder which contains all the relevent files you need to create your custom tile.

  1. Create a new folder in clientside/src/app/tiles called stats.
  2. Inside the project, create the following files:
File Name Description
stats.tile.component.html HTML file where we define our new chart with graphs
stats.tile.component.ts TypeScript file that accompanies the HTML file, allowing us to define functionality
stats.tile.component.scss SCSS file that contains styling for our new chart
stats.tile.module.ts TypeScript file to include our component as a single module
  1. Insert the following code into stats.tile.component.ts. Here we are creating our custom tile.
import {Component} from '@angular/core';

import {ElementRef, ViewChild} from '@angular/core';
import {Chart} from 'chart.js';
import {Store} from '@ngrx/store';
import {
    getLessonCollectionModels
} from 'src/app/models/lesson/lesson.model.selector';
import {
    getArticleCollectionModels,

} from 'src/app/models/article/article.model.selector';
import {
    getCourseCollectionModels,
} from 'src/app/models/course/course.model.selector';
import {Observable} from 'rxjs';
import {QueryOperation} from '../../services/http/interfaces'; 

import {ArticleModel} from 'src/app/models/article/article.model';
import * as articleModelAction from 'src/app/models/article/article.model.action';
import {ArticleModelState} from 'src/app/models/article/article.model.state';

import {LessonModel} from 'src/app/models/lesson/lesson.model';
import * as lessonModelAction from 'src/app/models/lesson/lesson.model.action';
import {LessonModelState} from 'src/app/models/lesson/lesson.model.state';

import {CourseModel} from 'src/app/models/course/course.model';
import * as courseModelAction from 'src/app/models/course/course.model.action';
import {CourseModelState} from 'src/app/models/course/course.model.state';

/**
 * Page Component named Stats.
 *
 * The values of any form controls added to this tile can be accessed by implementing a constructor and subscribing to them in that method.
 *
 * Any class fields which can be updated or functions which can be implemented to impact the tiles structure will have an associated
 * protected region which can be utilised for that purpose without having to rebuild with the bot
 *
 * Selectors for all of the html elements present in the HTML structure can be found in 'stats.component.scss', so styling of this
 * tile can be completed in that file
 *
 */
@Component({
    selector: 'cb-stats-tile-component',
    templateUrl: './stats.tile.component.html',
    styleUrls: ['./stats.tile.component.scss']
})
export class StatsTileComponent {
    // initialising our observable collections
    articles: Observable<ArticleModel[]>;
    articleCount: number = 0;
    articlesId = 'articles';

    lessons: Observable<LessonModel[]>;
    lessonCount: number = 0;
    lessonsId = 'lessons';

    courses: Observable<CourseModel[]>;
    courseCount: number = 0;
    coursesId = 'courses';

    // Setting up database interaction 
    constructor(
        private readonly storeArticles: Store<{ model: ArticleModelState }>,
        private readonly storeLessons: Store<{ model: LessonModelState }>,
        private readonly storeCourses: Store<{ model: CourseModelState }>,
    ) {

    // Initialises a data structure to store our collected article models once they are fetched from the database.  
    this.storeArticles.dispatch(new articleModelAction.ArticleAction(
        articleModelAction.ArticleModelActionTypes.INITIALISE_ARTICLE_COLLECTION_STATE,
        {
            collectionId: this.articlesId
        }));

    // Populating the collection with data
    this.storeArticles.dispatch(new articleModelAction.ArticleAction(
        articleModelAction.ArticleModelActionTypes.FETCH_ALL_ARTICLE,
        {
            collectionId: this.articlesId
        }
    ));

     // Accessing the collected articles 
    this.articles = this.storeArticles.select(getArticleCollectionModels, this.articlesId);

    // Subscribing to changes in the collection of articles
    // Whenever the models stored in this.articles change, the code in this block will be executed
    this.articles.subscribe(articles => {
        this.articleCount = articles.length;
        console.log(this.articleCount);
        this.updateChart();
    });

    this.storeLessons.dispatch(new lessonModelAction.LessonAction(
        lessonModelAction.LessonModelActionTypes.INITIALISE_LESSON_COLLECTION_STATE,
        {
            collectionId: this.lessonsId
        }));

    this.storeLessons.dispatch(new lessonModelAction.LessonAction(
        lessonModelAction.LessonModelActionTypes.FETCH_ALL_LESSON,
        {
            collectionId: this.lessonsId
        }
    ));

    this.lessons = this.storeLessons.select(getLessonCollectionModels, this.lessonsId);

    this.lessons.subscribe(lessons => {
        this.lessonCount = lessons.length;
        console.log(this.lessonCount);
        this.updateChart();
    });


    this.storeCourses.dispatch(new courseModelAction.CourseAction(
        courseModelAction.CourseModelActionTypes.INITIALISE_COURSE_COLLECTION_STATE,
        {
            collectionId: this.coursesId
        }));

    this.storeCourses.dispatch(new courseModelAction.CourseAction(
        courseModelAction.CourseModelActionTypes.FETCH_ALL_COURSE,
        {
            collectionId: this.coursesId
        }
    ));

    this.courses = this.storeCourses.select(getCourseCollectionModels, this.coursesId);

    this.courses.subscribe(courses => {
        this.courseCount = courses.length;
        console.log(this.courseCount);
        this.updateChart();
    });

    }

    // Updates the chart with the current collected datasets
    updateChart() {
        if (this.chart) {
            this.chart.data.datasets[0].data = [
                this.articleCount, this.lessonCount, this.courseCount,
            ];
            this.chart.update();
        }
    }

    // Initialising our chart
    @ViewChild('canvas')
    canvasEl: ElementRef;

    chart: Chart;

    ngAfterViewInit() {
        let ctx = (document.getElementById('myChart') as HTMLCanvasElement).getContext('2d');

        Chart.defaults.global.legend.display = false;

        this.chart = new Chart(ctx, {
            // The type of chart we want to create
            type: 'bar',

            // The data for our dataset
            data: {
                labels: ['Articles', 'Lessons', 'Courses'],
                datasets: [{
                    backgroundColor: ['rgba(204, 229, 255)', 'rgba(204, 255, 204)', 'rgba(255, 204, 229)'],
                    borderColor: 'black',
                    data: [this.articleCount, this.lessonCount, this.courseCount]
                }]
            },
            // Configuration options go here
            options: {
                title: {
                    text: 'Library Content',
                    display: true
                },
                scales: {
                    xAxes: [{
                        display: true
                    }],
                    yAxes: [{
                        display: true,
                        ticks: {
                            beginAtZero: true
                        }
                    }],
                }
            }
        });
    }
}
  1. Next we will create our module class. Insert the following code into stats.tile.module.ts
import { CommonModule } from '@angular/common';
import {NgModule} from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { StatsTileComponent } from './stats.tile.component';
import { CommonComponentModule } from 'src/app/lib/components/common.component.module';

@NgModule({
    declarations: [
        StatsTileComponent,
    ],
    imports: [
        CommonModule,
        CommonComponentModule,
        FormsModule,
        ReactiveFormsModule,
    ],
    exports: [
        StatsTileComponent,
    ],
    providers: [
    ],
})
export class StatsTileModule {
}
  1. Finally we will link it all together by inserting the following code into stats.tile.component.html
<div>
    <h1>Key stats</h1>
</div>
<div>
    <canvas id="myChart"></canvas>
</div>

For the purposes of this lesson ‘stats.tile.component.scss` has been left blank (default styling) however you can easily add custom styling to this as you see fit.

Adding the custom tile to a page

The next step is to add our custom stats tile to the stats page. To do this we must import the StatsTileModule and the add the html refrence.

  1. Navigate to the stats page under clientside/src/app/pages/stats/stats.page.component.html
  2. Locate the custom protected region, signalled by [Add code for (Custom Region Id) here]:
<cb-stats-tile-component></cb-stats-tile-component>
  1. In the same folder, navigate to the stats page module stats.page.module.ts
  2. Locate the protected region Add any additional imports here turn it on and place the following code:
import { StatsTileModule } from '../../lib/tiles/stats/stats.tile.module';
  1. Import the StatsTileModule by locating the protected region Add any additional module imports here and adding it in so it looks like the following:
// % protected region % [Add any additional module imports here] on begin
        StatsTileModule, 
// % protected region % [Add any additional module imports here] end
  1. Run your application and navigate to the Stats..... page. You should see the following graph created (populated with your created articles, lessons and courses).
Custom Graph

Reusing Components

To use this stats tile on other pages, simply follow the process above by importing the StatsTileModule into the pages module file and adding the html reference in the custom protected region on the page itself.

Once the application is reloaded, the stats tile should appear on the home page in the same format as above.

For more information on reusing components or tiles in springbot checkout our Reusing Components in SpringBot Article.

Solution

Have a look at the custom-tile branch to see the code solution.