How to Build your First Angular UI Plugin

This tutorial will show you how to build a plugin UI with Angular for the plentymarkets 7 backend. We will create this project based on a template we created, the plugin Terra-Basic.

Tutorial Content

  • Angular basics. How to set up a basic Angular UI.
  • How to use REST calls addressing our plentymarkets database.
  • How to use the Terra-Components.

1. Installing Node.js

Install the LTS version of Node.js.

Open your command line and run it.
→ For Mac and Linux, it's terminal; for Windows, it's cmd.

node -v

If the output looks like the following line, node.js is installed.

v8.9.4

2. Setting up an IDE

We recommend to use Atom or Visual Code as IDE.

3. Download template

We created the plugin Terra-Basic which includes a basic Angular application.

This template also includes all packages used in the tutorial. The Terra-Components are already installed.

4. Setting up a local test environment

  1. Open your IDE.
  2. Go to File→Open.
  3. Select the downloaded 'Plugin-Terra-Basic'.
  4. Use the command line to navigate to the 'Plugin-Terra-Basic' directory.

    cd /your-dir/plugin-terra-basic

  5. Run the following command to install all required packages. This may take some time.

    npm install

  6. Start your local test server.

    npm start

  7. The project is now compiled and will be shown in your browser on localhost:3002.

    → You can change the port in the file /plugin-terra-basic/config/webpack.dev.js .

Terra Components

We developed the Terra-Components for the Terra back end of plentymarkets 7.

The Terra-Components are in constant development and we improve and enhance them every day.

Terra-Components documentation:

If you have any issues with our Terra-Components, the fastest way to get a response from our Terra dev team is to create an issue in our github repository .

5. Creating a plugin view

If you are new to Angular, we recommend checking out the angular.io tutorial: Tour of Heroes .

  1. Open your IDE file browser and navigate to the views folder.
  2. Open the folder, create a new directory and name it stats-view.
  3. In the new directory, create the following three files.
    • stats-view.component.html
      → This file is used to display your UI.
    • stats-view.component.scss
      → This file is used to style your UI.
    • stats-view.component.ts
      → This file is used to handle all your component logic.

When you're done the folder structure should look like this.

plugin-terra-basic/
    └──src/
        └── app/
            └── views/
                └── stats-view/
                    ├── stats-view.component.html
                    ├── stats-view.component.scss
                    └── stats-view.component.ts

Creating the component

To create the stats-view component, start with the typescript file.

src/app/views/stats-view.component.ts
import {
    Component,
    OnInit
} from '@angular/core';

@Component({
               selector: 'stats-view',
               template: require('./stats-view.component.html'),
               styles:   [require('./stats-view.component.scss')]
           })
export class StatsViewComponent implements OnInit
{
    constructor()
    {
    }

    public ngOnInit():void
    {
    }
}

Usage of the typescript file

  • The typescript file and its class are the main part of your component.
  • In the class, you connect the html add scss file, name the selector and write your logic.

NgModule

In this step, you will link the stats-view component to the NgModule.
→ NgModule: Defines a module that contains components, directives, pipes, and providers.

  1. Navigate to the plugin-terra-basic.module.ts
  2. Import the StatsViewComponent, line 13.
  3. Add the StatsViewComponent to the NgModule declarations, line 26.
src/app/plugin-terra-basic.module.ts
import {
    APP_INITIALIZER,
    NgModule
} from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { PluginTerraBasicComponent } from './plugin-terra-basic.component';
import { TerraComponentsModule } from '@plentymarkets/terra-components/app/terra-components.module';
import { HttpModule } from '@angular/http';
import { TranslationModule } from 'angular-l10n';
import { FormsModule } from '@angular/forms';
import { LocalizationConfig } from './core/localization/terra-localization.config';
import { StartComponent } from './views/start/start.component';
import { StatsViewComponent } from './views/stats-view/stats-view.component'; // <--- Import the component

@NgModule({
    imports:      [
        BrowserModule,
        HttpModule,
        FormsModule,
        TranslationModule.forRoot(),
        TerraComponentsModule.forRoot()
    ],
    declarations: [
        PluginTerraBasicComponent,
        StartComponent,
        StatsViewComponent           // <--- declare the component, so that you can use it in your project.
    ],
    providers:    [
        LocalizationConfig,
        {
            provide:    APP_INITIALIZER,
            useFactory: initLocalization,
            deps:       [LocalizationConfig],
            multi:      true
        }
    ],
    bootstrap:    [
        PluginTerraBasicComponent
    ]
})
export class PluginTerraBasicModule
{
}

export function initLocalization(localizationConfig:LocalizationConfig):Function
{
    return () => localizationConfig.load();
}

Displaying the component

Open the plugin-terra-basic.component.html

Replace <start></start> with <stats-view></stats-view> .

src/app/plugin-terra-basic.component.html
<stats-view></stats-view>

Creating the UI

Open the stats-view.component.html.

In the next step, you will create a basic Angular UI with Terra-Components .

Terra uses the bootstrap grid system to form the UI.

src/app/views/stats-view.component.html
<div class="container">
    <div class="row">
        <div class="col-sm-12">
            <terra-base-toolbar>
                <terra-button inputIcon="icon-refresh"
                              inputTooltipText="fetch data"
                              inputTooltipPlacement="right">
                </terra-button>
            </terra-base-toolbar>
        </div>
    </div>
    <div class="row">
        <div class="col-sm-12">
            <terra-portlet inputPortletHeader="User information">
                 <div class="center">
                    <terra-loading-spinner></terra-loading-spinner>
                 </div>
            </terra-portlet>
        </div>
    </div>
    <div class="row">
        <div class="col-sm-6">
            <terra-portlet inputPortletHeader="Installed webstores">
               <p>
                    Lorem ipsum dolor sit amet, consetetur sadipscing elitr,
                    sed diam nonumy eirmod tempor invidunt ut labore et
                    dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et
               </p>
            </terra-portlet>
        </div>
        <div class="col-sm-6">
            <terra-portlet inputPortletHeader="Installed plugins" [inputIsCollapsable]="true">
               <p>
                    Lorem ipsum dolor sit amet, consetetur sadipscing elitr,
                    sed diam nonumy eirmod tempor invidunt ut labore et
                    dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et
               </p>
            </terra-portlet>
        </div>
    </div>
</div>

Styling the UI

To style the UI, we will use scss which basically is css with more features. learn more

src/app/views/stats-view.component.scss
.center
{
    display: flex;
    justify-content: center;
    align-items: center;
}
.user-information-container
{
    padding:10px;
}
.content
{
    min-height: 15vh;
    td
    {
        padding: 5px;
    }
    th
    {
        padding: 5px;
    }
}

6. How to use REST API

In this step, you will use REST calls addressing our plenty database.

In the first step, you create an Angular service which handles the REST logic.

Go to your stats-view directory and add a new typescript file.

The new directory structure should now look like this.

plugin-terra-basic/
    └──src/
        └── app/
            └── views/
                └── stats-view/
                    ├── stats-view.component.html
                    ├── stats-view.component.scss
                    ├── stats-view.component.ts
                    └── stats-view.service.ts

1. Creating a service

src/app/views/stats-view.service.ts
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { Observable } from 'rxjs';
import {
    TerraBaseService,
    TerraLoadingSpinnerService
} from '@plentymarkets/terra-components';


@Injectable()
export class StatsDataService extends TerraBaseService
{
    public bearer:string;
    private _basePathUrl:string;
    constructor(private _loadingSpinnerService:TerraLoadingSpinnerService,
                private _http:Http)
    {
        super(_loadingSpinnerService, _http, '/rest/');
        if(process.env.ENV !== 'production')
        {
            // tslint:disable-next-line:max-line-length
            this.bearer = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6IjFlNTllODUxNjY1ZjNiZmMwY2JjZjQxY2ZmYWIzMmI1ZTYxYjQ0NTFjMzEwMTRlMTE4NGE0YTBhZjA2MzAwMDAxZDI0YzJiMzZmNWI5MTZkIn0.eyJhdWQiOiIxIiwianRpIjoiMWU1OWU4NTE2NjVmM2JmYzBjYmNmNDFjZmZhYjMyYjVlNjFiNDQ1MWMzMTAxNGUxMTg0YTRhMGFmMDYzMDAwMDFkMjRjMmIzNmY1YjkxNmQiLCJpYXQiOjE1MjA5MzMzNzksIm5iZiI6MTUyMDkzMzM3OSwiZXhwIjoxNTIxMDE5Nzc5LCJzdWIiOiIzIiwic2NvcGVzIjpbIioiXX0.sThmLl-Nfj4rbDp08EKKA187tgaC6AwyJG3yycco6wFI1wj-uMsmy5ycSFL10eDATDhex5jpAI-sJGctipRMJGrtLj4tnDj0nrJtTLV5AEXhdGvKKOhjy02osYCU6Bd8zIxE7I5m1J76LILs8ag8-u4OPFs4dOIxPW2CB4VRhTUc-HteBtP87rzRyhMLreIx8z72dICBIRSQ6jpx3r9TtlkY8T0RdAd_rb76QVu_Al5c9Bky4DAjLXR7NkQBN__tmu3e2Nd8RRFqf3UBHgxhw2MTgoswTpUXEnrxyoy_0UuYax6tHmZp1dkaYAvuyXSnLERlfiMnhfcbGe5XjG-Ou0htvDYWw2IgIgQvD1klqW7yT9RIUdJCtHiAMIe2PeCzf1-y5zjui93BMZ8RgPuLWz-YsA32V4Aw_F_jZSkku912bAhe5YigP5Qw_XSerXEU7S1NR0AJFJZB5C07j06rijubuoTViIsoNE_Ex_0QzdYiXKhqVo5NgWFVdxRV9fkIatd4zT6ppcL6Lq3bHohee0CZzl4N1k0jfm2o3Vo3EC1Exe9TReuGwroy47Q86sxmsiPx1h5XHlMSzVeK7jJMLWtMmX8teH2PdjVMVOJ7ystnHhzs-br_7hr6j02NBTv8KLXXtQOzVyrcwM6exHgiY8HmbXrbKUMtVzZojnMau6Q';
            this._basePathUrl = 'http://master.login.plentymarkets.com';
            this.url = this._basePathUrl + this.url;
        }
        this.setHeader();
    }

    public getRestCallData(restRoute:string):Observable <Array<any>>
    {
        this.setAuthorization();
        let url:string;
        url = this._basePathUrl + restRoute;

        return this.mapRequest(
            this.http.get(url, {
                headers: this.headers,
                body:    ''
            })
        );
    }

    private setHeader():void
    {
        if(this.bearer !== null && this.bearer.length > 0)
        {
            this.headers.set('Authorization', 'Bearer ' + this.bearer);
        }
    }
}

To use the service and be able to send REST calls, you need to get an authentication token.

  1. Log in to your plentymarkets test system.
  2. Open your browser dev tools right click >> inspect .
  3. Go to Application .
  4. Open Local Storage .
  5. Click on your connection example: http://master.login.plentymarkets.com.
  6. This will open a list where you can copy the accessToken .
  7. Go to your stats-view.service.ts to line 22 and paste the copied token into the bearer variable.
  8. Change the _basePathUrl variable in line 23 and replace it with your plentymarkets test system url.

You are ready to use the service. Unfortunately, the token will change every time your login session expires.

So you may have to repeat this step several times.

Linking your service to the module

In order to use the service at your stats-view component, you need to add the service to the NgModule.

Import the service as shown in line 14.

Add the service to providers as shown in line 37.

src/app/plugin-terra-basic.module.ts
import {
    APP_INITIALIZER,
    NgModule
} from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { PluginTerraBasicComponent } from './plugin-terra-basic.component';
import { TerraComponentsModule } from '@plentymarkets/terra-components/app/terra-components.module';
import { HttpModule } from '@angular/http';
import { TranslationModule } from 'angular-l10n';
import { FormsModule } from '@angular/forms';
import { LocalizationConfig } from './core/localization/terra-localization.config';
import { StartComponent } from './views/start/start.component';
import { StatsViewComponent } from './views/stats-view/stats-view.component';
import { StatsDataService } from './views/stats-view/stats-view.service'; // <--- imported the service

@NgModule({
    imports:      [
        BrowserModule,
        HttpModule,
        FormsModule,
        TranslationModule.forRoot(),
        TerraComponentsModule.forRoot()
    ],
    declarations: [
        PluginTerraBasicComponent,
        StartComponent,
        StatsViewComponent
    ],
    providers:    [
        LocalizationConfig,
        {
            provide:    APP_INITIALIZER,
            useFactory: initLocalization,
            deps:       [LocalizationConfig],
            multi:      true
        },
        StatsDataService // <--- added the service to providers
    ],
    bootstrap:    [
        PluginTerraBasicComponent
    ]
})
export class PluginTerraBasicModule
{
}

export function initLocalization(localizationConfig:LocalizationConfig):Function
{
    return () => localizationConfig.load();
}

2. Handling REST calls

In this step, you learn how to get data from our database and how to handle the result.

You will find all REST routes in the REST menu.

  • Create three interfaces to provide a template for the REST call data.
  • Create three variables which store the received data.
  • Add the StatsDataService to the constructor.
  • Create three methods to fire a REST call to the database and handle the response.
  • The updateData method triggers all REST call functions and is also called at the OnInit function.
  • Add the Terra-Alert component to display alerts.
src/app/views/stats-view.component.ts
import {
    Component,
    OnInit
} from '@angular/core';
import { StatsDataService } from './stats-view.service';
import { TerraAlertComponent } from '@plentymarkets/terra-components';

interface PluginInterface
{
    name?:string;
    id?:number;
    created_at?:string;
}
interface UserInterface
{
    username?:string;
    email?:string;
}
interface WebStoreInterface
{
    id?:number;
    name?:string;
    type?:string;
}

@Component({
    selector: 'stats-view',
    template: require('./stats-view.component.html'),
    styles:   [require('./stats-view.component.scss')]
})
export class StatsViewComponent implements OnInit
{
    public plugins:Array<PluginInterface>;
    public user:UserInterface;
    public webStores:Array<WebStoreInterface>;

    private _alert:TerraAlertComponent;

    constructor(private _statsDataService:StatsDataService)
    {
        this._alert = TerraAlertComponent.getInstance();
    }

    public ngOnInit():void
    {
       this.updateData();
    }

    public updateData():void
    {
        this.createPluginData();
        this.createUserData();
        this.createWebStoreData();
        this._alert.addAlert(
            {
                msg:'Fetching data',
                type:'info',
                dismissOnTimeout:3000,
                identifier: 'info'
            });
    }

    private createPluginData():void
    {
        this.plugins = [];
        this._statsDataService.getRestCallData('/rest/plugins').subscribe((response:Array<any>) =>
        {
            for(let plugin of response)
            {
                this.plugins.push(
                    {
                        name: plugin.name,
                        id: plugin.id,
                        created_at: plugin.created_at
                    });
            }
        });
    }

    private createWebStoreData():void
    {
        this.webStores = [];
        this._statsDataService.getRestCallData('/rest/webstores').subscribe((response:Array<any>) =>
        {
            for(let store of response)
            {
                this.webStores.push(
                    {
                        id: store.id,
                        name: store.name,
                        type: store.type
                    });
            }
        });
    }

    private createUserData():void
    {
        this.user = {};
        this._statsDataService.getRestCallData('/rest/user').subscribe((response:any) =>
        {
            this.user =
                {
                    username: response.user,
                    email: response.user_email
                };
        });
    }
}

3. Displaying data at UI

In this step, you will print out the REST results from our component.

Use the Terra-Components to display your results at a back end view.

src/app/views/stats-view.component.html
<terra-alert-panel></terra-alert-panel>
<div class="container">
    <div class="row">
        <div class="col-sm-12">
            <terra-base-toolbar>
                <terra-button inputIcon="icon-refresh"
                              inputTooltipText="fetch data"
                              inputTooltipPlacement="right"
                              (outputClicked)="updateData()">
                </terra-button>
            </terra-base-toolbar>
        </div>
    </div>
    <div class="row">
        <div class="col-sm-12">
            <terra-portlet inputPortletHeader="User information">
                <div class="row content">
                    <div class="col-xs-2">
                        <div class="user-information-container">
                            <strong>Username:</strong>
                            <br>
                            <strong>Email:</strong>
                            <br>
                            <strong>Plugins:</strong>
                        </div>
                    </div>
                    <div class="col-xs-3">
                        <div *ngIf="(user | json) != '{}'" class="user-information-container">
                            <span>{{user?.username}}</span>
                            <br>
                            <span>{{user?.email}}</span>
                            <br>
                            <span>{{plugins?.length}}</span>
                        </div>
                    </div>
                </div>
            </terra-portlet>
        </div>
    </div>
    <div class="row">
        <div class="col-sm-6">
            <terra-portlet inputPortletHeader="Installed webstores">
                <div class="content">
                    <table>
                        <tr>
                            <th>Id</th>
                            <th>Name</th>
                            <th>Type</th>
                        </tr>
                        <tr *ngFor="let store of webStores">
                            <td>{{store.id}}</td>
                            <td>{{store.name}}</td>
                            <td>{{store.type}}</td>
                        </tr>
                    </table>
                </div>
            </terra-portlet>
        </div>
        <div class="col-sm-6">
            <terra-portlet inputPortletHeader="Installed plugins" [inputIsCollapsable]="true">
                <div class="content">
                    <table>
                        <tr>
                            <th>Id</th>
                            <th>Name</th>
                            <th>Created at</th>
                        </tr>
                        <tr *ngFor="let plugin of plugins">
                            <td>{{plugin.id}}</td>
                            <td>{{plugin.name}}</td>
                            <td>{{plugin.created_at}}</td>
                        </tr>
                    </table>
                </div>
            </terra-portlet>
        </div>
    </div>
</div>

At this point of the tutorial, your Angular UI should look like this:

7. Adding UI to Plugin

You should have a working Angular application.

To combine your Angular UI with the plugin, follow the Combine Angular UI with plugin .

We will continue to improve this tutorial and add more features to the shown plugin.

Further steps:

Download or clone the complete plugin tutorial at Github .

Public repository with a plugin written in Angular .