Extension Points

The Kinvey Studio app directory structure contains a number of folders and files, not all of which are user-editable. The ones that are editable are referred to as extension points. The extension points represent different means by which you can further customize the appearance and behavior of the generated application.

The options for customization include handling of events, extracting common logic in monolithic structures (such as your components and views), tweaking the layout and appearance, importing third-party libraries, and so on.

Styling and theming extension points are discussed in App Appearence.

Directory Structure Overview

Kinvey Studio projects are NativeScript with Angular code-sharing projects but have added files and folder specific to Kinvey Studio. Some of the files are user-editable, as explained in the remainder of the document, while some are auto-generated. Editing the auto-generated files is not recommenced because doing so means that you will most probably break the app or lose your edits.

The following figure is a representation of the directory tree. In the interest of brevity, it omits some deeper tree levels and less important files.

  • App_Resources—Resources for building native mobile application with NativeScript. Created on project creation. You are free to change the contents of this folder as long as you comply with the respective requirements for Android and iOS development.
  • artifacts—This folder contains extensibility files that you can use to add to or override the main files provided by Kinvey Studio. Examples include package.json and angular.json.
  • hooks—This folder contains NativeScript hooks. These are executable pieces of code or Node.js scripts that customize the execution of NativeScript commands. You can add your own hooks, but modifying existing hooks is not recommended.
  • meta—This folder contains app metadata stored by the Kinvey Studio designer mode. This includes data describing modules, views, the selected theme, and so on. Kinvey Studio combines this metadata with data from artifacts to generate the source code for all your app. You can look at this folder as a persistent layer of Kinvey Studio for your project.
  • node_modules—Standard location for NPM modules, including modules that come standard with Kinvey Studio and modules installed by you.
  • platforms—This folder is used and managed by the NativeScript CLI. You should exclude it from source control. Making direct changes to it is not recommended.
  • src—This folder contains the application's source code as generated by Kinvey Studio. It follows the recommended Angular application file structure.
    • src/app/core—Holds files for the System Module which is imported in the app's root AppModule. It contains all Angular services used in the app for authentication, Kinvey services, navigation, and notification.
    • src/app/data—Contains classes modeling each connected Kinvey collection.
    • src/app/modules—Holds folders for each module you create. All modules share the same file structure and use the same extension points.
    • src/app/modules/<module-name>—Holds files for the respective view. For every view inside a Kinvey Studio module you get an Angular module. All views share the same file structure and extension points.
    • src/app/modules/shared—Holds the Shared module, which contains Angular directives/components and pipes. These are reusable UI parts of the app, different than the services in the System module. Note that your custom modules import the Shared module, but the root AppModule does not.
  • templates—This folder contains user-defined templates, including templates that you export from Blank views. Also see Custom Web Component Templates.
  • angular.json—The root Angular workspace configuration file. Learn how to extend it in Configuring the Angular Workspace.
  • nsconfig.json—Configuration file for NativeScript CLI. You can extend it using artifacts/nsconfig.json.
  • package.json—The root NPM packages file. Learn how to extend it in Installing NPM Packages.
  • tsconfig.json—Configuration file for the TypeScript Angular compiler on the web side of the project. You can extend it using artifacts/tsconfig.json.
  • tslint.json—Configuration file for TSLint. You can extend it using artifacts/tslint.json.
  • webpack.config.js—Configuration file for Webpack. Manually modifying the contents of this folder is not recommended.

Extending TypeScript Code

The extension points for extending the auto-generated code are represented by specialized files. These files stay intact during app generation to ensure that your custom code is preserved.

The app assets that you can extend include views and components.

To allow you to make changes in a safe and consistent way, Kinvey Studio uses the class extension paradigm. In every custom-code file, you will find one or more classes that extend the auto-generated base classes. The base classes are designed in such a way as to allow maximum extensibility through their public members.

View Level

Kinvey Studio allows you to quickly extend the view code by switching from the Design to the Code tab. However, you will need to know the associated files locations if you prefer to work in another IDE or editor.

All view files are located under the following base path: <app base folder>/src/app/modules/<module name>/<view name>/.

The code extension points under this path include:

  • <view name>.component.<extension>—Extend the base class, access component properties.
  • <view name>.config.ts—(Web views only) A place to register new Angular components on view level.

Where:

  • <app base folder> is the folder where you created the Kinvey Studio application
  • <module name> is the name of the module where you created the view
  • <view name> is the development name (not the display name) of the view
  • <extension> is tns.ts for mobile views and ts for web views

Using the public members of the base class, you can also extend components laid out on the view.

The next example shows a basic even handling scenario that you can add to a web view file like vehicles-web.component.ts. The custom code here is the onRowSelect() function. Don't forget to set the onRowSelect handler name in the view's property inspector, on the Events tab.

import { Inject, Injector } from '@angular/core';
import { VehiclesMobileViewBaseComponent } from '@src/app/modules/main/vehicles-mobile/vehicles-mobile.base.component';

export class VehiclesMobileViewComponent extends VehiclesMobileViewBaseComponent {
    constructor(@Inject(Injector) injector: Injector) {
        super(injector);
    }
    // Handle the grid's onRowSelect event
    onRowSelect(e) {
        console.log(e);
    }
}

If this next example, suppose you want to extend the model of a component with a property that didn't originally exist, but you rather retrieve it from elsewhere, for example the router parameters. Access to the component is accomplished through this.$config inside the onBeforeSubmit event handler.

import { Inject, Injector } from '@angular/core';
import { ApprovedVisitViewBaseComponent } from '@src/app/modules/main/approved-visit/approved-visit.base.component';

export class ApprovedVisitViewComponent extends ApprovedVisitViewBaseComponent {
    constructor(@Inject(Injector) injector: Injector) {
        super(injector);
    }
    // Handle mobile-form’s onBeforeSubmit event
    this.$config.mobileform0.onBeforeSubmit = ({ form }) => {
            form.additionalFields.clientId = this.$activatedRoute.snapshot.queryParams.ClientId;
    }
}

Module Level

On module level, each module that you create offers a couple of extension points on its own, located under <app base folder>/src/app/modules/<module name>/:

  • <module-name>.config.<extension>—A place to register new Angular components, providers, and so on.
  • <module-name>-routing.config.<extension>—Allows you to add custom Angular routes.

Where:

  • <app base folder> is the folder where you created the Kinvey Studio application
  • <module name> is the name of the module
  • <extension> is tns.ts for mobile settings and ts for web settings

Application Level

On application level, the app-routing.config file located under <app base folder>/src/app/ allows you to make application-wide Angular router configuration.

For example, to change the router strategy to hash, use the following code:

import { Routes, ExtraOptions } from '@angular/router';

export const config: { routes: Routes, extraOptions?: ExtraOptions } = {
    routes: [],
    extraOptions: {
        useHash: true
    }
};

export function transformRoutes(routes: Routes): void {}

This next snippet enables router tracing, which makes it easy to track down problems in routing. Tracing output appears in the web browser's or mobile device's console.

import { Routes, ExtraOptions } from '@angular/router';

export const config: { routes: Routes, extraOptions?: ExtraOptions } = {
    routes: [],
    extraOptions: {
        enableTracing: true
    }
};

export function transformRoutes(routes: Routes): void {}

Writing Services

Services are an essential part of your app. This is where you typically do most of the heavy lifting and business logic that feed data to the view controller.

You can write your services as full-fledged Angular services or as utility functions.

As Angular Services

Writing full-fledged services in a Kinvey Studio application does not differ from writing an Angular service.

Place service files in <app base folder>/src/app/core/custom-services/ in .service.ts files. For example, the following IdGeneratorService.service.ts file implements an UUID generator.

import { Injectable } from '@angular/core';

@Injectable()
export class IdGeneratorService {
    constructor() {}

    uuid() {
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
            var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
            return v.toString(16);
        });
    }
}

After implementing the service, you can register it in the core module or in any other module by adding it in the providers array. The following example adds to the core module's files at the following locations:

  • Mobile: <app base folder>/src/app/core/core.config.tns.ts
  • Web: <app base folder>/src/app/core/core.config.ts

You can do the same inside any module's <module-name>.config.tns.ts or <module-name>.config.ts files under <app base folder>/src/app/modules/<module-name>/.

import { NgModule } from '@angular/core';
import { IdGeneratorService } from './id-generator.service';

export const config: NgModule = {
    providers: [
        IdGeneratorService
    ],
    declarations: [],
    imports: [],
    exports: [],
    entryComponents: [],
    bootstrap: [],
    schemas: []
};

export function transformConfig(configMeta: NgModule): void {}

As Utility Functions

Utility functions allow you to write short extensions to your code that don’t logically belong to any class. Place such utilities in <app base folder>/src/app/core/utils.service.ts for web or utils.service.tns.ts for mobile. These files are generated only once at app creation and are not overwritten on app regeneration. They are imported in the core module, meaning you can use your utilities across the web or mobile app respectively.

import { Injectable } from '@angular/core';

@Injectable()
export class UtilsService {

    safeCallFn(context, fnName, args, fallbackFn?: (...args: any) => void) {
        fallbackFn = fallbackFn || (() => { console.error(`${fnName} is not defined!`); });
        return (context[fnName] || fallbackFn).apply(context, args);
    }
}

Overriding Built-in Services

Kinvey Studio implements a number of built-in features as Angular services. You can reimplement a built-in service using a couple of basic steps. The example below reimplements ErrorHandlingService—a service that determines the look and content of error messages.

  1. In <app base folder>/src/app/core/core.config.ts, use the transformConfig function to remove the default ErrorHandlingService from providers and then add a new service to providers.
import { NgModule } from '@angular/core';
import { IdGeneratorService } from './id-generator.service';
import { ErrorHandlingService } from './error-handling.service';
import { CustomErrorHandlingService } from './custom-error-handling.service';

export const config: NgModule = {
    …
};

export function transformConfig(configMeta: NgModule): void {
    configMeta.providers = configMeta.providers.filter(p => p !== ErrorHandlingService);
    configMeta.providers.push({ provide: ErrorHandlingService, useClass: CustomErrorHandlingService });
}
  1. Create a file for the new service (e.g. custom-error-handling.service.ts). In it, extend ErrorHandlingService and add your custom logic.
import { Notification } from '@src/app/core/notification/notification';
import { NotificationService } from '@src/app/core/notification/notification.service';
import { ErrorHandlingService } from './error-handling.service';

@Injectable()
export class CustomErrorHandlingService extends ErrorHandlingService {
    constructor(protected notificationService: NotificationService) {
       super(notificationService);
    }

    protected createErrorNotification(err: any) {
        switch (err) {
            case err.status === 403:
                return new Notification('You are not authorized for that action', 'error', 10000);
            default:
                const message = (err.error && (err.error.message || err.error.error)) || err.message;
                return new Notification(`<ul><li>${message}<li></ul>`, 'error', 10000);
        }
    }
}

Custom Web Component Templates

Kinvey Studio supports templates for implementing custom components. It utilizes the EJS templating engine and expects .ejs files.

In addition to HTML, you can write directives coming from Kendo UI, Kinvey Studio, or third-party components.

The following figure represents the expected tree structure. Add your custom templates in the templates folder at the root application level. The <component_name> placeholder represent the component name.

templates/
└── components/
    └── <component_name>/
        ├── angular/                    -- runtime template
        |   ├── config.json.ejs
        |   └── template.html.ejs
        ├── design-time/                -- design-time template
        |   ├── <component_name>.png
        |   ├── options.json.ejs
        |   └── template.html.ejs
        └── <component_name>.json       -- component schema definition

The component extension points allow you to encapsulate and reuse common logic in the form of a custom component template.

Similar to the built-in components, the custom components can also have properties.

Custom component templates can be beneficial in the following common scenarios:

  • If you need a component that does not exist in the toolbox, you can implement your own or use a third-party component.
  • If you want to augment the functionality or adjust the appearance of a built-in component, you can wrap it in a custom component.

Each custom component template includes the following parts. The examples included in these represent a custom calendar that utilizes built-in Kinvey Studio components.

Schema Definition

The <component_name>.json file represents the schema file that is used by Kinvey Studio to display all available properties and to generate the runtime code. Kinvey Studio utilizes the JSON schema version 4 as a standard.

The name, category, and description properties are optional. The category is used by the toolbox to group the components in a convenient visual manner and accepts any string.

The properties that you define here appear in the properties inspector in Kinvey Studio. Some of the properties descriptor fields are:

  • type—(Required) Renders the proper editor for that property. Possible values and corresponding editors: string (TextBox), boolean (CheckBox), number (Numeric Text Box), integer (Integer Text Box), array (Drop Down List), and object (renders nested, expandable levels in the property grid). Kinvey Studio also supports composite types—for example, [ "integer", "null" ]. Using the correct type provides template users with a hint of what value type is expected.
  • title—(Required) Represents the name of the property that will be displayed in the property grid.
  • description—(Optional) Represents a hint in the code that is also displayed as hint in the property inspector.
  • default—(Optional) Auto-populates the editor too. The generated code will have a default value too.
  • order—(Optional) Defines the order in the property grid for that property.

This properties field description is compliant with the JSON schema version 4. You can also use any other field that is defined in the standard.

After you define properties, you can optionally lay them out using the layout <component_name>.json property. It takes precedence over the properties' order fields.

{
    "$schema": "http://json-schema.org/draft-04/schema#",
    "id": "custom-calendar",
    "type": "object",
    "name": "Calendar",
    "description": "Custom Calendar",
    "category": "Scheduling",
    "properties": {
        "id": {
            "allOf": [
                {
                    "$ref": "definitions/valid-component-id.json"
                },
                {
                    "type": "string",
                    "title": "Id",
                    "description": "The Id of the component",
                    "default": "",
                    "minLength": 1
                }
            ],
            "order": 1
        },
        "title": {
            "type": "string",
            "title": "Component Title",
            "description": "The title of the component",
            "default": "Calendar",
            "order": 1
        },
        "minDate": {
            "type": "string",
            "title": "Min Date",
            "default": "1899-12-31T22:00:00.000Z",
            "format": "date-time",
            "editorType": "date",
            "order": 2
        },
        "maxDate": {
            "type": "string",
            "title": "Max Date",
            "default": "2099-12-30T22:00:00.000Z",
            "format": "date-time",
            "editorType": "date",
            "order": 3
        },
        "events": {
            "type": "object",
            "title": "Events",
            "editorRowType": "events",
            "hideTitle": true,
            "order": 6,
            "properties": {
                "onChange": {
                    "type": "string",
                    "title": "Change Event Function",
                    "description": "Fires when the selected date is changed.",
                    "default": ""
                }
            }
        }
    },
    "layout": {
        "groups": [
            {
                "name": "Properties",
                "properties": [
                    { "name": "id" },
                    { "name": "title" },
                    { "name": "minDate" },
                    { "name": "maxDate" }                
                ]
            },
            {
                "name": "Events",
                "properties": [
                    { "name": "events" }
                ]
            }
        ]
    }
}

Design-Time Template

The design-time template defines the component's look and feel when dropped on the canvas in Design mode. The design-time template files are located under ./design-time.

The template consists of several files some of which are optional. However, template.html.ejs is required and represents the template that appears on the Design mode canvas. It can display anything—for example, a Hello World string or a very complex HTML structure, but its purpose is to display sufficiently close visual representation of the generated application version. It is recommended to keep the design-time template simple, as it does not allow for direct interaction. Instead, it reflects any changes you make to its properties using the Property Inspector.

A simple design-time/template.html.ejs could look like this:

<div>Custom Calendar</div>

To add styles to the HTML template, append a <div></div> section at the end of the file and "namespace" the styles with a prefixed class.

<div class="my-custom-calendar">custom calendar</div>

.my-custom-calendar .date-cell {
    color: blue
}

The wrapper HTML element has the my-custom-calendar class with which the .date-cell selector is namespaced. The namespacing class itself has the my- prefix in this case, which is done to minimize the risk of accidental style overrides in the canvas.

  • options.json.ejs—(Required) Defines the template properties that are later used to extend the initial template model. This approach is suitable when you want to provide the template with more dynamic behavior.

    If the template is simple enough, provide an empty object {} so that the file can be JSON validated.

  • generator/index.js—(Optional) Used to augment the initial model of the template and, in this way, provide additional dynamic behavior when designing the application. For example, you can data-bind some components that are inside the view and display sample data. Otherwise, they will be empty and not representative to other developers. Another option for you is to show or hide certain parts of the template based on the selected properties. For example, if the edit property is true, you display a form. In this file you have full access to the meta model. If the template is simple enough, skip it.

  • <component_name>.png—(Optional) Represents the component in the Components pane. If not provided, a default image is displayed.

Runtime Template

The runtime template is an Angular template located under ./angular and consisting of the following files:

  • template.html.ejs—(Required) Represents the Angular component that will be rendered directly in the view when the user adds it. The definition of the component and controller are provided separately. For example, to create a custom calendar component named custom-calendar, the template will look similar to the following example:

      <custom-calendar
         [config]="$config.components.<%- id %>"
         [id]="'<%- id %>'"
         <% if (meta.events.onChange) { %>
         (modelChange)="<%- meta.events.onChange %>($event)"
         <% } %>
         >
      </custom-calendar>
  • config.json.ejs—(Required) Provides a way to pass a subset or calculated set of properties from the meta definition to runtime. The properties become accessible through the $config object as shown above. Under the hood, the generator constructs this object and exposes it as a public member from the base view:

      {
          min: new Date('<%- meta.minDate %>'),
          max: new Date('<%- meta.maxDate %>'),
          title: '<%- meta.title %>'
      }

Component Definition

Kinvey Studio uses the custom component template to generate code and render it inside the application each time the user drags it from the toolbox to the canvas. At this point, Angular does not relate with the rendered <custom-calendar> component and you have to define its template and controller. Otherwise, an exception is thrown.

To define the template and controller:

  1. Create a folder inside <app base folder>/src/app/shared next to the components/ folder and name it, for example, custom-components.
  2. Inside this folder, create a folder for each custom component, and in it create the following files:

    • custom-calendar.component.html:

        <div>
            <h2></h2>
            <kendo-calendar
                #calendar
                    [min]="config.min"
                    [max]="config.max"
                    (valueChange)="valueChange($event)"
                    [formControlName]="id">
            </kendo-calendar>
        </div>
    • custom-calendar.component.ts:

        import { Component, Input, Output, EventEmitter } from '@angular/core';
      
        @Component({
           selector: 'custom-calendar',
           templateUrl: './custom-calendar.component.html'
        })
        export class CustomCalendarComponent {
           @Input() public config;
           @Input() public id;
           @Output() public modelChange: EventEmitter<Date> = new EventEmitter();
      
           _modelChange(e) {
              this.modelChange.emit(e);
           }
        }
    • custom-calendar.component.css—use to write component-specific styles

  3. Handle the calendar event in the parent view:

     public calendarChange(e) {
         console.log(e);
     }

    The name calendarChange comes from the Kinvey Studio property inspector. It can be any valid function name.

    The generated calendar code will look as follows:

     <custom-calendar [config]="$config.components.customcalendar0" [id]="'customcalendar0'" (modelChange)="calendarChange($event)">
     </custom-calendar>

Custom Markup

There are a couple of ways to insert custom markup into your views. One includes using the purpose-built Custom Xml/Html components available for mobile and web views. The other is limited to web views and relies on predefined sections inside the built-in view templates.

The Custom Component

When laying out a view, you can add the Custom Xml (on mobile) or Custom Html (on view) component to inject arbitrary markup. You can even inject Angular components as long as they are part of the application.

After you add the component, click the Edit XML/HTML link in the property inspector to see a text editor where to add the code.

For example, you can use the Bootstrap 4 Alert component in a web view if you have the framework installed. To do that, add the following code to the Custom Html component:

<div class="alert alert-success" role="alert">
  A simple success alert-check it out!
</div>

The result should look like this:

Bootstrap alert

The next example shows how to add a built-in Kinvey Studio component to the Custom XML component. In this case, this is the NativeScript Label component.

<Label text="Example" textWrap="true" class="medium-text"></Label>

Web View Sections

All built-in web views feature predefined sections that you can edit to inject custom HTML code. For example, the Data Grid view features sections called top-section, middle-section, and bottom-section, each of which provides an Edit in Code Mode link that allow you to write your own code to supplement the respective parts of the view.

If you want to edit the section outside of Kinvey Studio, use the following file locations under <app base folder>/src/app/modules/<module name>/<view name>/:

  • Top section: top-section.component.html
  • Middle section: middle-section.component.html
  • Bottom section: bottom-section.component.html

Configuring the Angular Workspace

The angular.json file provides configuration defaults for build and development tools provided by the Angular CLI.

To extend angular.json:

  1. Add an empty angular.json file to the artifacts folder on the root application level.
  2. Add the necessary settings to angular.json.

    The following example changes the root folder of the app:

     {
         "projects": {
             "MyAppName": {
                 "root": "rootFolder"
             }
         }
     }
  3. Restart the development web server if running:

    1. From the toolbar in the upper-right corner, click Stop Dev Server.
    2. After the server stops, click the Start Dev Server button.

Installing NPM Packages

Kinvey Studio enables you to extend the existing package.json file by adding your own packages. You can add packages either from inside Kinvey Studio or by directly editing the file. The later option is suitable for advanced NPM tasks like creating scripts.

To install NPM packages using Kinvey Studio:

  1. In the Kinvey Studio main menu, click Window > Kinvey Studio Mobile Sidekick.
  2. In the left-hand side navigation, click the plug icon.
  3. Click the Available list.
  4. In the search box, enter the name of the package that you want to install.
  5. When the package name appears in the list, select it and click Install in the right-hand pane.

    You can also choose to install the package as a development dependency.

Go to the Installed tab to manage installed packages. Keep in mind that uninstalling packages that have beed preinstalled by Kinvey Studio will most probably cause Kinvey Studio or your app to malfunction.

To extend package.json manually:

  1. Add an empty package.json file to the artifacts folder on the root application level.
  2. Add the necessary dependencies and other changes to package.json.

     {
         "dependencies": {
             "nativescript-push-notifications": "1.1.6"
         },
         "scripts": {
             "hmr": "ng serve --hmr"
         }
     }
  3. Click Generate.

    As a result, the custom package.json file is merged with the automatically-generated one.

Related Resources