ダッシュボードにカスタムウィジェットを追加する

バージョン: 1017.0.23 | パッケージ: @c8y/cli, @c8y/apps, @c8y/ngx-components

Things Cloudで提供しているウィジェットが要件を満たさない場合、カスタムウィジェットを作成しダッシュボードに追加できます。

典型的なダッシュボードは以下のようになり、さまざまなウィジェットが表示されます。

ダッシュボード

ここでは HOOK_COMPONENTS を用いてダッシュボードにカスタムウィジェットを追加する方法を示しています。

1. サンプルアプリケーションを初期化する

まずはじめに、ダッシュボードを表示するアプリケーションが必要です。このために、c8ycli を使用して新しいコックピットのアプリケーションを作成します。

c8ycli new my-cockpit cockpit -a @c8y/apps@1017.0.23

次に、依存関係をインストールします。新しく作成されたフォルダに移動し、npm install を実行してください。

備考
c8ycli newコマンドには、スキャフォールディングに使用するパッケージを定義する-aフラグがあります。このようにして、スキャフォールディングとするアプリケーションのバージョンを定義することも可能です。例えば、以下の通りです。

c8ycli new my-cockpit cockpit -a @c8y/apps@1017.0.23 は、アプリケーションのバージョン 10.17.0.23 をスキャフォールディングします。

2. ウィジェットコンポーネントを作成する

ウィジェットは通常2つの部分で構成されます。

そのため2つのコンポーネントを作成する必要があります。

はじめに、demo-widget.component.ts を作成します。

import { Component, Input } from '@angular/core';

@Component({
  selector: 'c8y-widget-demo',
  template: `<p class="text">{{config?.text || 'No text'}}</p>`,
  styles: [ `.text { transform: scaleX(-1); font-size: 3em ;}` ]
})
export class WidgetDemo {
  @Input() config;
}

このコンポーネントは、CSSを介して、垂直に鏡写しされたテキストが表示します。他のAngularコンポーネントでできることは何でも行うことができます。

次のように定義されている demo-widget-config.component.ts から設定を渡すために config の入力が必要です。

import { Component, Input } from '@angular/core';

@Component({
  selector: 'c8y-widget-config-demo',
  template: `<div class="form-group">
    <c8y-form-group>
      <label translate>Text</label>
      <textarea name="text" [(ngModel)]="config.text" style="width:100%"></textarea>
    </c8y-form-group>
  </div>`
})
export class WidgetConfigDemo {
  @Input() config: any = {};
}

再び、ウィジェットに渡したいシリアル化可能な設定で埋めることのできる config オブジェクトを追加する必要があります。

ウィジェット設定の検証を有効にするには、@Componentデコレータに次のオプションを追加する必要があります。

import { ControlContainer, NgForm } from "@angular/forms";

@Component({
    ...
    viewProviders: [{ provide: ControlContainer, useExisting: NgForm }]
})

上記の例と組み合わせると、設定検証を有効にしたdemo-widget-config.component.tsコンポーネントは、次のようになります。

import { Component, Input } from '@angular/core';
import { ControlContainer, NgForm } from "@angular/forms";

@Component({
  selector: 'c8y-widget-config-demo',
  template: `<div class="form-group">
    <c8y-form-group>
      <label translate>Text</label>
      <textarea name="text" [(ngModel)]="config.text" style="width:100%"></textarea>
    </c8y-form-group>
  </div>`,
  viewProviders: [{ provide: ControlContainer, useExisting: NgForm }]
})
export class WidgetConfigDemo {
  @Input() config: any = {};
}

3. アプリケーションにウィジェットを追加する

ウィジェットを追加するには、HOOK_COMPONENTSを使用しentryComponents として作成したコンポーネントを定義する必要があります。

そのためには、app.module.ts に以下を追加します。

import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as NgRouterModule } from '@angular/router';
import { UpgradeModule as NgUpgradeModule } from '@angular/upgrade/static';

// --- 8< 変更箇所 ----
import { CoreModule, RouterModule, HOOK_COMPONENTS } from '@c8y/ngx-components';
// --- >8 ----

import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES } from '@c8y/ngx-components/upgrade'; 
import { SubAssetsModule } from '@c8y/ngx-components/sub-assets'; 
import { ChildDevicesModule } from '@c8y/ngx-components/child-devices';
import { CockpitDashboardModule,ReportDashboardModule } from '@c8y/ngx-components/context-dashboard';
import { ReportsModule } from '@c8y/ngx-components/reports';
import { SensorPhoneModule } from '@c8y/ngx-components/sensor-phone';
import { BinaryFileDownloadModule } from '@c8y/ngx-components/binary-file-download';
import { SearchModule } from '@c8y/ngx-components/search';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { CockpitConfigModule } from '@c8y/ngx-components/cockpit-config';
import { DatapointLibraryModule } from '@c8y/ngx-components/datapoint-library';
import { WidgetsModule } from '@c8y/ngx-components/widgets';
import { PluginSetupStepperModule } from '@c8y/ngx-components/ecosystem/plugin-setup-stepper';

// --- 8< 追加箇所 ----
import { WidgetDemo } from './demo-widget.component';
import { WidgetConfigDemo } from './demo-widget-config.component';
// --- >8 ----

@NgModule({
  imports: [
    // Upgrade module must be the first
    UpgradeModule,
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    NgRouterModule.forRoot([...UPGRADE_ROUTES], { enableTracing: false, useHash: true }),
    CoreModule.forRoot(),
    ReportsModule,
    NgUpgradeModule,
    AssetsNavigatorModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    ReportDashboardModule,
    BinaryFileDownloadModule, 
 		SearchModule, 
 		SubAssetsModule, 
 		ChildDevicesModule, 
 		CockpitConfigModule, 
 		DatapointLibraryModule.forRoot(), 
 		WidgetsModule, 
 		PluginSetupStepperModule
  ],

  // --- 8< 追加箇所 ----
  declarations: [WidgetDemo, WidgetConfigDemo],      // 1.
  entryComponents: [WidgetDemo, WidgetConfigDemo],
  providers: [{
    provide: HOOK_COMPONENTS,                         // 2.
    multi: true,
    useValue: [
      {
        id: 'acme.text.widget',                        // 3.
        label: 'Text widget',
        description: 'Can display a text',
        component: WidgetDemo,                         // 4.
        configComponent: WidgetConfigDemo,
      }
    ]
  }],
  // --- >8 ----

})
export class AppModule extends HybridAppModule {
  constructor(protected upgrade: NgUpgradeModule) {
    super();
  }
}

上記の数字の説明は以下の通りです。

  1. エントリーコンポーネントとしてコンポーネントを定義し、このモジュールによってアクセスできるように宣言します。
  2. HOOK_COMPONENTS と共にマルチプロバイダーフックを追加します。このフックはアプリケーションによって収集され、設定した値に基づいてウィジェットに追加します。
  3. インベントリに保存されているデータを特定するため、IDは一意である必要があります。ラベルと詳細記述はタイトルとウィジェットのドロップダウンとして表示されます。
  4. この部分は、すでに定義してあるコンポーネントとウィジェットに関連付けるようフックに指示します。

npm start でアプリケーションを開始すると、ダッシュボードにカスタムウィジェットが表示されているはずです。

ダッシュボードに追加されると、ウィジェットは以下のように表示されます。

カスタムウィジェット

Jestベースのユニットテストを追加する

バージョン: 1016.274.0 | パッケージ: @c8y/cli, @c8y/apps, @c8y/ngx-components

ユニットテストはすべての開発プロセスにおいて不可欠な要素です。 バージョン 10.13.0.0 以降、すべての新しい c8ycli スキャフォールディングアプリケーションには、ユニットテストフレームワーク Jest がデフォルトで含まれています。 このチュートリアルでは、最初のユニットテストを書いて検証する方法を紹介します。

1. サンプルアプリケーションを初期化する

例として、空のデフォルトのアプリケーションが必要です。

c8ycli new my-app application -a @c8y/apps@1016.274.0 

ただし、どのようなアプリケーションでも、同じようにユニットテストをサポートします。次に、すべての依存関係をインストールする必要があります。

備考
  • c8ycli new コマンドには、スキャフォールディングに使用するパッケージを定義する-aフラグがあります。このようにして、スキャフォールディングとするアプリケーションのバージョンを定義することも可能です。例えば、以下の通りです。
  • c8cycli my-app application -a @c8y/apps@1016.274.0 はバージョン 10.16.274.0 のアプリケーションをスキャフォールディングします。

2. コンポーネントを追加する

何かをテストするためには、まず検証可能なコンポーネントが必要です。そのため、test.component.ts という名前の新しいファイルを追加します。

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-test',
  template: `<h1>Hello world</h1>`
})
export class TestComponent implements OnInit {
  constructor() { }

  ngOnInit(): void { }
}

新しく作成したコンポーネントを app.module.ts の宣言文に追加します。

import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as ngRouterModule } from '@angular/router';
import { CoreModule, BootstrapComponent, RouterModule } from '@c8y/ngx-components';

// --- 8< 追加箇所 ----
import { TestComponent } from "./test.component"; 
// ---- >8 ---- 
 		 
@NgModule({ 
 	imports: [ 
    BrowserAnimationsModule, 
    RouterModule.forRoot(), 
    ngRouterModule.forRoot([], { enableTracing: false, useHash: true }),
    CoreModule.forRoot()
  ],
  bootstrap: [BootstrapComponent],

  // --- 8< 追加箇所----
  declarations: [
    TestComponent
  ]
  // --- >8 ----
})
export class AppModule {}

サンプルコンポーネントをモジュールに追加した後、コンポーネントをテストする準備ができました。

2. テストコンポーネント用のユニットテストを追加する

テストファイルの拡張子は .spec.ts です。 リポジトリには、app.module.spec.tsというサンプルのspecファイルがあります。 このspecファイル名を test.component.spec.ts に変更して、内容を次のように調整してください。

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AppModule } from './app.module';
import { TestComponent } from './test.component';

describe('Test component test', () => {
  let fixture: ComponentFixture<TestComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [AppModule]
    });
    fixture = TestBed.createComponent(TestComponent);
  });

  test('should be defined', () => {
    expect(fixture).toBeDefined();
  });
});

これは最初のテストファイルです。 Angularのテストモジュールを設定し、TestComponentが定義できるかどうかをチェックします。 Angularのテストサポートについては、Angular webサイトで詳細を確認できます。

テストを開始するには、コマンドラインから npm test を実行します。 これにより、package.jsonにある定義済みのスクリプトが実行され、Jestが起動します。 以下のようなテスト結果が表示されるはずです。

 PASS  ./test.component.spec.ts (32.071 s)
  Test component test
    ✓ should be defined (123 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        32.858 s

テストが「PASS」と表示されれば、すべてがうまくいき、最初のコンポーネントテストは成功したことになります。 これで、より詳細なテストケースを追加してコンポーネントが意図した通りに動作することを確認することができます。

3. スナップショットテストを使用して、コンポーネントテンプレートを検証する

このセクションでは、コンポーネントテンプレートを検証するための他の方法についての追加情報を提供します。

Karmaの代わりにJestを使用するのは、いわゆるスナップショットテストを使用するためのオプションが付属しているためです。 スナップショットテストは、すべての結果を定義することなく、テストの結果を検証することができます。 Jestの関数 toMatchSnapshot() は、初回実行時のテストのスナップショットを含むファイルを作成します。 test.component.spec.tsファイルに以下を追加して、スナップショットテストを使用する別のテストを作成し、 TestComponentのテンプレートを検証します。

test("should show a title tag", () => {
  expect(fixture.debugElement.nativeElement).toMatchSnapshot();
});

npm testを実行します。その結果、スナップショットが書き込まれたことが表示されます。

PASS  ./test.component.spec.ts
  Test component test
    ✓ should be defined (94 ms)
    ✓ should show a title tag (29 ms)

 › 1 snapshot written.
Snapshot Summary
 › 1 snapshot written from 1 test suite.

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   1 written, 1 total
Time:        5.154 s

このスナップショットは新しく作成されたフォルダ ./__snapshot__ にあり、確認することができます。 テンプレートが変更されると、テストは失敗するので、npm test -- -u でテストを上書きする必要があります。 この動作は test.component.ts ファイルでテンプレートを変更することでテストできます。

備考
このスナップショットをコードと一緒にコミットするのが一般的なやり方です。

まとめ

このチュートリアルでは、c8cycliコマンドを使用して新しくスキャフォールディングされたアプリケーションに、テストを追加する方法を紹介しました。 高度なスナップショットテストには、テンプレートをすばやく検証するためのオプションがあります。

コンテキストルートを使用して詳細ビューにタブを追加する

バージョン: 1017.0.23 | パッケージ: @c8y/cli, @c8y/apps, @c8y/ngx-components

デバイスやグループなどの詳細画面でユーザーに追加の情報を表示したい、というのは一般的なユースケースです。

このレシピでは、デバイス詳細画面に新しいタブを追加する方法を説明します。

カスタムタブでのデバイス情報

Angular向けWeb SDKでは、特定のコンテキストに対してのビューを提供するため、このような画面は ViewContext と呼ばれます。コンテキストビューにはいくつか種類があります(例えば、DeviceGroupUserApplicationTenant など)。ハッシュナビゲーションで特定の Route でナビゲートすることで、コンテキストビューにアクセスできます。例えば、apps/cockpit/#/device/1234 へアクセスした場合、アプリケーションは 1234 のIDを持つデバイスを解決しようとします。

詳細ビューは/info と呼ばれる別のルートから参照されている上記のスクリーンショットの情報タブにあるように、通常いくつかの Tabs を表示していますが、情報を表示するためにデバイスのコンテキストを再利用しています。

以下では、apps/cockpit/#/device/:id/hello からアクセスできるこのビューに新しいタブを追加する方法を紹介します。

1. サンプルアプリケーションを初期化する

はじめに、コンテキストルートをサポートするアプリケーションが必要です。 このために、c8cycliを使用して新しいコックピットアプリケーションを作成します。

c8ycli new my-cockpit cockpit  -a @c8y/apps@1017.0.23 

次に、すべての依存関係をインストールする必要があります。新しいフォルダに移動して、npm installを実行します。

備考
c8y cli newコマンドには、スキャフォールディングに使用するパッケージを定義する-aフラグがあります。このようにして、スキャフォールディングとするアプリケーションのバージョンを定義することも可能です。例えば、以下の通りです。

  • c8ycli new my-cockpit cockpit -a @c8y/apps@1017.0.23 は、アプリケーションのバージョン10.17.0.23をスキャフォールディングにします。

2. 新しいHOOK_ROUTEを追加する

フックの概念により既存のコードに追加できます。この例では、device/:id という既存のルートにいわゆるChildRoute(Angularによる)を追加します。

これを達成するために、app.module.ts に以下のコードを追加します。

import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as NgRouterModule } from '@angular/router';
import { UpgradeModule as NgUpgradeModule } from '@angular/upgrade/static';
// ---- 8< 変更箇所 ----
import { CoreModule, RouterModule, HOOK_ROUTE, ViewContext } from '@c8y/ngx-components';
// ---- >8 ----
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES } from '@c8y/ngx-components/upgrade';
import { CockpitDashboardModule, ReportDashboardModule } from '@c8y/ngx-components/context-dashboard';
import { ReportsModule } from '@c8y/ngx-components/reports';
import { SensorPhoneModule } from '@c8y/ngx-components/sensor-phone';
import { BinaryFileDownloadModule } from '@c8y/ngx-components/binary-file-download';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';

@NgModule({
    imports: [
      // Upgrade module must be the first
      UpgradeModule,
      BrowserAnimationsModule,
      RouterModule.forRoot(),
      NgRouterModule.forRoot([...UPGRADE_ROUTES], { enableTracing: false, useHash: true }),
      CoreModule.forRoot(),
      AssetsNavigatorModule,
      ReportsModule,
      NgUpgradeModule,
      DashboardUpgradeModule,
      CockpitDashboardModule,
      SensorPhoneModule,
      ReportDashboardModule,
      BinaryFileDownloadModule
    ],

    // ---- 8< 追加箇所 ----
    providers: [{
      provide: HOOK_ROUTE,          // 1.
      useValue: [{                       // 2.
        context: ViewContext.Device,     // 3.
        path: 'hello',                   // 4.
        component: HelloComponent,       // 5.
        label: 'hello',                  // 6.
        priority: 100,
        icon: 'rocket'
      }],
      multi: true
    }]
    // ---- >8 ----

})
export class AppModule extends HybridAppModule {
    constructor(protected upgrade: NgUpgradeModule) {
      super();
    }
}

上記の数字の説明は以下の通りです。

  1. マルチプロバイダーフックの HOOK_ROUTE を提供します。これは現在のルートの設定を拡張するためにアプリケーションに指示します。
  2. ルートフックの定義に値を使用することを指定します。例えば、ルートを非同期に解決したい場合、クラスを使用することもできます。
  3. ルートのコンテキストを定義します。定義するには ViewContext enum を使用する必要があります。この例では、デバイスのコンテキストを拡張します。
  4. 表示されるパスです。コンテキストパスに追加されます。この例では、完全パスは以下になります。device/:id/hello
  5. パスがユーザーによって入力されたらどのコンポーネントを表示するかを定義します。
  6. タブがどのように表示されるかを定義するのは label かつ icon プロパティです。priority はどの位置に表示するべきかを定義します。
備考
HOOK_ROUTE はAngular Routeの型を継承します。すべてのプロパティはここで再利用されます。

この一連の設定の後、ルートは登録されますが HelloComponent はまだ存在しないため、アプリケーションはコンパイルに失敗します。次のセクションで HelloComponent を作成します。

3. コンテキストデータを表示するコンポーネントを追加する

HelloComponentは、デバイスの詳細を表示したい場合があるでしょう。 これを行うには、それが開かれたコンテキストに関する情報が必要です。 コンテキストルートは、デバイスを前もって解決してくれます。 また、親ルートで直接アクセスすることができます。

hello.component.ts という名前のファイルを作成します。

import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-hello',
  template: `
  <c8y-title>world</c8y-title>
  <pre>
    <code>
      {{route.snapshot.parent.data.contextData | json}}
    </code>
  </pre>
  `
})
export class HelloComponent {
  constructor(public route: ActivatedRoute) {}
}

このコンポーネントは ActivatedRoute を注入し、その親データにアクセスします。 ここがキーとなるポイントです。親コンテキストルートはすでにデバイスのデータを解決しているので、このコンポーネントは常に現在のデバイスの詳細データを表示します。

これを app.module.tsentryComponents に追加することでアプリケーションをコンパイルすることができます。


import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as NgRouterModule } from '@angular/router';
import { UpgradeModule as NgUpgradeModule } from '@angular/upgrade/static';
import { CoreModule, RouterModule, HOOK_ROUTE, ViewContext } from '@c8y/ngx-components';
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES } from '@c8y/ngx-components/upgrade';
import { CockpitDashboardModule, ReportDashboardModule } from '@c8y/ngx-components/context-dashboard';
import { ReportsModule } from '@c8y/ngx-components/reports';
import { SensorPhoneModule } from '@c8y/ngx-components/sensor-phone';
import { BinaryFileDownloadModule } from '@c8y/ngx-components/binary-file-download';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
// ---- 8< 追加箇所 ----
import { HelloComponent } from './hello.component';
// ---- >8 ----

@NgModule({
  imports: [
    // Upgrade module must be the first
    UpgradeModule,
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    NgRouterModule.forRoot([...UPGRADE_ROUTES], { enableTracing: false, useHash: true }),
    CoreModule.forRoot(),
    AssetsNavigatorModule,
    ReportsModule,
    NgUpgradeModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    ReportDashboardModule,
    BinaryFileDownloadModule
  ],

  // ---- 8< 追加箇所 ----
  declarations: [HelloComponent],
  entryComponents: [HelloComponent],
  // ---- >8 ----

  providers: [{
    provide: HOOK_ROUTE,
    useValue: [{
      context: ViewContext.Device,
      path: 'hello',
      component: HelloComponent,
      label: 'hello',
      priority: 100,
      icon: 'rocket'
    }],
    multi: true
  }]
})
export class AppModule extends HybridAppModule {
  constructor(protected upgrade: NgUpgradeModule) {
    super();
  }
}

npm start でアプリケーションを開始する場合、デバイスの詳細ビューへアクセスすると以下のように表示されます。

カスタムタブ付きのデバイス情報

これで、デバイスにタブが追加されました。 テナント、ユーザー、アプリケーションの詳細ビューでも同じことができます。

次は条件が満たされた場合のみ、このタブを表示する方法を学びましょう。

(付録)4.条件を満たした場合のみ、タブを表示する

場合によっては、特定の条件を満たした場合にのみ追加の情報を利用できることがあります。例えば、デバイスに位置情報のフラグメントが関連付けられている場合にのみ位置情報を表示することは意味があります。そのような条件を追加するには、コンテキストルートは Angularのガード概念 を継承します。

ガードを追加するには、単純にルート定義に canActivate プロパティを追加するだけです。

import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as NgRouterModule } from '@angular/router';
import { UpgradeModule as NgUpgradeModule } from '@angular/upgrade/static';
import { CoreModule, RouterModule, HOOK_ROUTE, ViewContext } from '@c8y/ngx-components';
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES } from '@c8y/ngx-components/upgrade';
import { SubAssetsModule } from '@c8y/ngx-components/sub-assets'; 
import { ChildDevicesModule } from '@c8y/ngx-components/child-devices';
import { CockpitDashboardModule, ReportDashboardModule } from '@c8y/ngx-components/context-dashboard';
import { ReportsModule } from '@c8y/ngx-components/reports';
import { SensorPhoneModule } from '@c8y/ngx-components/sensor-phone';
import { BinaryFileDownloadModule } from '@c8y/ngx-components/binary-file-download';
import { SearchModule } from '@c8y/ngx-components/search';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { CockpitConfigModule } from '@c8y/ngx-components/cockpit-config';
import { DatapointLibraryModule } from '@c8y/ngx-components/datapoint-library';
import { WidgetsModule } from '@c8y/ngx-components/widgets';
import { PluginSetupStepperModule } from '@c8y/ngx-components/ecosystem/plugin-setup-stepper';
import { HelloComponent } from './hello.component';

// ---- 8< 追加箇所 ----
import { HelloGuard } from './hello.guard';
// ---- >8 ----

@NgModule({
  imports: [
    // Upgrade module must be the first
    UpgradeModule,
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    NgRouterModule.forRoot([...UPGRADE_ROUTES], { enableTracing: false, useHash: true }),
    CoreModule.forRoot(),
    AssetsNavigatorModule,
    ReportsModule,
    NgUpgradeModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    ReportDashboardModule,
    BinaryFileDownloadModule
  ],
  declarations: [HelloComponent],
  entryComponents: [HelloComponent],
  providers: [

    // ---- 8< 追加箇所 ----
    HelloGuard,
    // ---- >8 ----

    {
    provide: HOOK_ROUTE,
    useValue: [{
      context: ViewContext.Device,
      path: 'hello',
      component: HelloComponent,
      label: 'hello',
      priority: 100,
      icon: 'rocket',

      // ---- 8< 追加箇所 ----
      canActivate: [HelloGuard]
      // ---- >8 ----

    }],
    multi: true
  }]
})
export class AppModule extends HybridAppModule {
  constructor(protected upgrade: NgUpgradeModule) {
    super();
  }
}

これで、特定の条件をチェックするガードを作成できます。trueに解決されると、タブが表示され、それ以外の場合は表示されません。

デバイスの特定のフラグメントをチェックするガードはこの hello.guard.ts のようになります。

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot } from '@angular/router';
import { Observable } from 'rxjs';

@Injectable()
export class HelloGuard implements CanActivate {

  canActivate(route: ActivatedRouteSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    const contextData = route.data.contextData || route.parent.data.contextData;          // 1.
    const { 'acme_HelloWorld': helloWorldFragment } = contextData;                        // 2.
    return !!helloWorldFragment;
  }
}

上記の数字は以下のように説明できます。

  1. これは、Angularルーターと整合していない唯一の部分です。コンテキストルートでは、CanActivate が2回呼び出されます。1回は親ルートがアクティブになり、1回は子ルートがアクティブになります。最初の呼び出しは、タブを表示する必要があるかどうかを確認し、2番目の呼び出しは、ユーザーがそのタブに移動できるかどうかを確認します。したがって、 ActivatedRouteSnapshotは両方の呼び出しで異なり、2番目のケースでは親から contextDataを解決する必要があります。
  2. acme_HelloWorld フラグメントがコンテキストに設定されているかを確認します。

APIに acme_HelloWorld フラグメントを付与してデバイスをポストすると、他のデバイスではなく作成したデバイスのみにHelloタブが表示されるでしょう。

まとめ

コンテキストルートは、既存のルートをさらに詳細に拡張するのに役立ちます。

同時に、コンテキストは一度のみ解決され、コンテキストが見つからない場合は親によって処理されるため、この概念によりアプリケーションの一貫性がが保たれます。

しかし、コンテキストルートの概念を抽象化し、独自に実装するデフォルトの方法は現時点では存在しません。 このコンセプトはAngularのルーティングに大きく基づいているので、自分で概念を実装することができます。

既存のアプリケーションを拡張しフックを使用する

バージョン: 1017.0.23 | パッケージ: @c8y/cli, @c8y/apps, @c8y/ngx-components

コックピットやデバイス管理のような既存のアプリケーションを拡張することは、一般的なユースケースです。

カスタムルートでコックピットを拡張する方法、ナビゲータにこのルートを追加する方法を順番に説明していきます。 まず、何をハイブリッド アプリケーションと呼んでいるのか、@c8y/apps npmパッケージが何を含んでいるのか、という背景を紹介します。

背景

デフォルトのアプリケーションはThings Cloudに同梱されている3つのアプリケーションで構成されています。これらのアプリケーションは何年も開発が続けられた結果、コードは主にAngularJSベースとなっています。現在、すべての組み込みアプリケーションではAngularを使用しているため、双方のフレームワークを提供する方法が必要でした。 @c8y/cli により、2つのフレームワークを共存させるデフォルトアプリケーションをスキャフォールドすることが可能になりました。これは、AngularとAngularJSのアプリケーションを同時に提供するためにAngularのアップグレード機能を用いています。これにより、新機能はすべてのAngular JS プラグインを統合しながら、Angular で新しい機能を開発できます。これを私たちは ハイブリッド モードと呼んでいます。

しかし、ハイブリッドモードにはいくつか制限があります。このレシピの後半で説明します。これらの制限により、純粋で 空のスターターAngularアプリケーションを提供することにしました。これはAngularJSのプラグインを統合する可能性がありません。このAngularのみのアプリケーションは以下の2つを含みます。

Web SDKを使い始めるには、全部で3つの可能性があります。

どれを選択するかは、ビルドするアプリケーションに大きく依存します。 例えば、プラットフォームのルックアンドフィールに従ったアプリケーションが必要であるが、マテリアル フレームワークなど特定のシナリオに特別な依存関係を使用したい場合は、純粋な Angular CLI ソリューションを使用するのが最適です。

最も一般的なユースケースは、ハイブリッドアプリケーションの拡張で、このレシピではこれを取り上げます。 まず、このアプローチの限界を見て、なぜこの概念がそのように設計されているのかを理解しましょう。

ハイブリッドモードの制限

ハイブリッドアプリケーションを実行する時、AngularとAngularJSは並行して実行されるようにする必要があるため、いくつかの制限があります。

制限について理解したところで最初のアプリケーションを拡張し、拡張用フックを開発することができます。これを行うには、ハイブリッドアプリケーションをスキャフォールディングにしておく必要があります。 c8y/appsは、デフォルトのアプリケーションとその最小限のセットアップを含むパッケージです。 c8yclinew コマンドでアプリケーションを初期化するたびにこのパッケージを使用します。 次のセクションでは、スキャフォールディングプロセスとハイブリッドアプリケーションを拡張する方法について順を追って説明します。

1. サンプルアプリケーションを初期化する

まずはじめに、アプリケーションが必要です。c8ycli を利用して新しくコックピットアプリケーションを作成します。

c8ycli new my-cockpit cockpit -a @c8y/apps@1017.0.23

次に、依存関係をインストールします。新しく作成されたフォルダに移動し npm install を実行してください。

備考
c8y cli newコマンドには、スキャフォールディングに使用するパッケージを定義する-aフラグがあります。このようにして、スキャフォールディングするアプリケーションのバージョンを定義することも可能です。例えば、以下の通りです。

  • c8ycli new my-cockpit cockpit -a @c8y/apps@1017.0.23 は、アプリケーションのバージョン10.17.0.23 をスキャフォールディングします。

2. ルートにカスタムコンポーネントをバインドする

ルートはAngularと同じ方法で追加することができます。 唯一の違いは、ハイブリッドの制限のため、UPGRADE_ROUTESの前に定義する必要があることです。 プロジェクト内に以下の内容で hello.component.ts ファイルを作成します。

import { Component } from "@angular/core";

@Component({
  selector: "app-hello",
  template: `
    <c8y-title>Hello</c8y-title>
    World
  `,
})
export class HelloComponent {
  constructor() {}
}

これは、基本的なコンポーネントです。 テンプレートだけは、タイトルを表示するために「コンテンツ投影」という特別な機能を使用しています。 コンテンツ投影とは、コンテンツを定義されている場所以外の場所に表示するために使用されるAngularの概念です。 どのコンポーネントがコンテンツ投影をサポートしているかについては、コンポーネントライブラリ(ngx) ドキュメントをご覧ください。

以下の方法で app.module.ts を変更することによりルートにカスタムコンポーネントをバインドできるようになります。

import { NgModule } from `@angular/core`;
importBrowserAnimationsModule } from '@angular/platform-browser/animations';
importRouterModule as NgRouterModule } from '@angular/router';
importUpgradeModule as NgUpgradeModule } from '@angular/upgrade/static';
importCoreModule, RouterModule } from '@c8y/ngx-components';
importDashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES } from '@c8y/ngx-components/upgrade';
importSubAssetsModule } from '@c8y/ngx-components/sub-assets'import { ChildDevicesModule } from '@c8y/ngx-components/child-devices';
importCockpitDashboardModule, ReportDashboardModule } from '@c8y/ngx-components/context-dashboard';
importReportsModule } from '@c8y/ngx-components/reports';
importSensorPhoneModule } from '@c8y/ngx-components/sensor-phone';
importBinaryFileDownloadModule } from '@c8y/ngx-components/binary-file-download';
import { SearchModule } from '@c8y/ngx-components/search'import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator'import { CockpitConfigModule } from '@c8y/ngx-components/cockpit-config'import { DatapointLibraryModule } from '@c8y/ngx-components/datapoint-library'import { WidgetsModule } from '@c8y/ngx-components/widgets'import { PluginSetupStepperModule } from '@c8y/ngx-components/ecosystem/plugin-setup-stepper'// --- 8< 追加箇所----
import { HelloComponent } from './hello.component';    // 1
// --- >8 ----

@NgModule({

  // --- 8< 追加箇所----
  declarations: [HelloComponent],                      // 2
  // --- >8 ----

  imports: [
    // Upgrade module must be the first
    UpgradeModule,
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    // --- 8< 変更箇所----
    [{ path: "hello", component: HelloComponent }, ...UPGRADE_ROUTES], 
      { enableTracing: false, useHash: true } 
    ),
    // --- >8 ----
    CoreModule.forRoot(),
    ReportsModule,
    NgUpgradeModule,
    AssetsNavigatorModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    ReportDashboardModule,
    BinaryFileDownloadModule,
    SearchModule, 
    SubAssetsModule, 
    ChildDevicesModule, 
    CockpitConfigModule, 
    DatapointLibraryModule.forRoot(), 
    WidgetsModule, 
    PluginSetupStepperModule 
  ], 
})
export class AppModule extends HybridAppModule {
  constructor(protected upgrade: NgUpgradeModule) {
    super();
  }
}

ここでの変更は単純です。まずはじめに、(1)コンポーネントをインポートします。(2)次に、そのコンポーネントを宣言部へ追加します。(3)最後にこの場合では hello というパスへバインドする必要があります。c8ycli serverコマンドでアプリケーションを起動し、正しいハッシュを追加して URL(http://localhost:9000/apps/cockpit/#/hello)に移動すると、カスタム コンポーネントが表示されます。次のステップでは、左ナビゲータのコンポーネントを追加します。

3. ナビゲーターノードをフックする

新しく作成した hello.component.ts にユーザーが移動できるようにするために、左側のナビゲーターにナビゲーションを追加します。 そのためには、いわゆるフックを使用します。

フックは特定のインジェクション トークンにバインドされている単なるプロバイダーです。 複数のプロバイダーを追加できるようにするには、Angularのマルチプロバイダーの概念を使います。 詳しく説明することは、このチュートリアルの範囲を超えています。 angular.ioドキュメントをご覧ください。

インジェクショントークンは @c8y/ngx-components パッケージをインポートすることで受け取ることができます。 これらはすべて HOOK_ で始まり、その後に何に使われるかが続きます。 例えば、ナビゲータノードを追加するには app.module.tsHOOK_NAVIGATOR_NODE を次のように使用します。

{ 
    provide: HOOK_NAVIGATOR_NODES, 
    useValue: [{ 
      label: 'Hello',   
      path: 'hello', 
      icon: 'rocket', 
      priority: 1000 
    }] as NavigatorNode[],         // 1 
    multi: true 
} 

(1)にあるように、型付けは自分で行う必要があります。 これを避けるために、hookX関数を使うこともできます。 次の例では hookRoutehookNavigatorNode を使ってナビゲーターノードを追加しています。

importNgModule } from "@angular/core";
importBrowserAnimationsModule } from "@angular/platform-browser/animations";
importRouterModule as NgRouterModule } from "@angular/router";
importUpgradeModule as NgUpgradeModule } from "@angular/upgrade/static";
// --- 8< 変更箇所----
import { CoreModule, RouterModule, hookNavigator, hookRoute } from "@c8y/ngx-components";
// --- >8 ----
importDashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES } from "@c8y/ngx-components/upgrade";
importSubAssetsModule } from "@c8y/ngx-components/sub-assets"import { ChildDevicesModule } from "@c8y/ngx-components/child-devices";
importCockpitDashboardModule, ReportDashboardModule } from "@c8y/ngx-components/context-dashboard";
importReportsModule } from "@c8y/ngx-components/reports";
importSensorPhoneModule } from "@c8y/ngx-components/sensor-phone";
importBinaryFileDownloadModule } from "@c8y/ngx-components/binary-file-download";
import { SearchModule } from "@c8y/ngx-components/search"import { AssetsNavigatorModule } from "@c8y/ngx-components/assets-navigator"import { CockpitConfigModule } from "@c8y/ngx-components/cockpit-config"import { DatapointLibraryModule } from "@c8y/ngx-components/datapoint-library"import { WidgetsModule } from "@c8y/ngx-components/widgets"import { PluginSetupStepperModule } from "@c8y/ngx-components/ecosystem/plugin-setup-stepper"import { HelloComponent } from "./hello.component";


@NgModule({
  declarations: [HelloComponent],

  imports: [
    // Upgrade module must be the first
    UpgradeModule,
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    NgRouterModule.forRoot(
      [{ path: "hello", component: HelloComponent }, ...UPGRADE_ROUTES],
      { enableTracing: false, useHash: true }
    ),
    CoreModule.forRoot(),
    ReportsModule,
    NgUpgradeModule,
    AssetsNavigatorModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    ReportDashboardModule,
    BinaryFileDownloadModule,
    SearchModule,
    SubAssetsModule,
    ChildDevicesModule,
    CockpitConfigModule,
    DatapointLibraryModule.forRoot(),
    WidgetsModule,
    PluginSetupStepperModule,
  ],

  // --- 8< 変更箇所----
  providers: [
    hookRoute({                     // 1 
      path: "hello", 
      component: HelloComponent, 
    }), 
    hookNavigator({                 // 1, 2
      priority: 1000, 
      path: "/hello",               // 3 
      icon: "rocket",
      label: "Hello",               // 4
    }), 
  ], 
  // --- >8 ----

})
export class AppModule extends HybridAppModule {
  constructor(protected upgrade: NgUpgradeModule) {
    super();
  }
}

上記の数字の説明は以下の通りです。

  1. hookRoutehookNavigator を提供します。
  2. 特定のvalueを使います。複雑なケースでは useClassget() を定義することもできます。
  3. アプリケーションへのパスを指定します。必ず/ で始めてください。
  4. ナビゲーター・ノードがどのように見えるかを定義します。

この拡張フックを実装すると、ナビゲータに次のような新しいエントリーが表示されます。

拡張したコックピットアプリケーション

NavigatorNodeインターフェースのプロパティ priority は、ノードの表示順を定義することに注意してください。。

これで hello.component.ts は、コックピットアプリケーション内の空白のキャンバスのようになりました。 コックピットの所定の機能に影響を与えることなく、必要な機能をすべて実装することができます。

まとめ

ハイブリッドアプリケーションは、Angular JSとAngularの統合による制限があります。 しかし、フックの概念とカスタムルートによって、既存のハイブリッドアプリケーションに追加できるようになります。 これらは、組み込みアプリケーションを拡張するための強力なツールです。 追加機能が必要な場合は、純粋なAngularアプリケーションの方が適していることもあります。 これはユースケースによって異なります。

ログインページと認証の削除

バージョン: 1016.274.0 | パッケージ: @c8y/cli, @c8y/apps, @c8y/ngx-components

備考
この手法では、ユーザー名とパスワードが公開されます。このユーザーが重要なデータにアクセスできないことを確認してください。

デフォルトのアプリケーションでは、ページにアクセスする前に必ずログインページに移動して認証を行います。 このレシピでは、ログイン認証を解除してアプリケーションを直接使用する方法を説明します。

背景

すべての認証を削除することはできません。 これを回避するためには、アプリケーションがリクエスト時に読み取るデフォルトの認証情報を渡す必要があります。目標は、アプリケーションが認証されていないためにログインページをリクエストする前に、デフォルトの認証情報を使ってログインをトリガーすることです。

ログイン機能は @c8y/ngx-components パッケージの CoreModule に含まれており、Angular がアプリケーションをブートストラップする際に読み込まれます。その前に、デフォルトの認証情報をAPIに渡す必要があります。その結果、Angularが最初のページを読み込むとき、ユーザーはすでに認証されており、ログインページはスキップされることになります。

1. 新しいアプリケーションを初期化する

出発点として、アプリケーションが必要です。そのために、c8cycliを使用して新しいアプリケーションを作成します。

c8ycli new my-cockpit cockpit -a @c8y/apps@1016.274.0

これにより、コックピットアプリケーションの完全なコピーである新しいアプリケーションが作成されます。 次に、すべての依存関係をインストールする必要があります。 新しいフォルダに移動して、npm installを実行します。

備考
c8ycli new コマンドには -a フラグがあり、スキャフォールディングするパッケージを定義することができます。このようにして、スキャフォールディングするアプリケーションのバージョンを定義することも可能です。例えば、以下の通りです。

  • c8cycli new my-cockpit cockpit -a @c8y/apps@1016.274.0 は、アプリケーションのバージョン10.16.274.0 をスキャフォールディングにします。

2. デフォルト認証のロジックを追加する

まず、Angularがカスタムアプリケーションをブートストラップする前に、デフォルトの認証を追加する必要があります。 そのため、新しく作成したカスタムコックピットアプリケーションの app.module.ts に、ログイン前にトリガーされる新しいプロバイダーを追加する必要があります。 そのためには、Angularのインジェクション トークンAPP_INITIALIZERを使用します。 このトークンによって、新機能が実行されるまでアプリケーションが初期化されないようになります。

providers: [
  {
    provide: APP_INITIALIZER,
    useFactory: initApp,
    multi: true,
    deps: [LoginService],
  },
];

ファクトリー関数 initApp を使用し、ここでデフォルトの認証を定義して送信します。 APIに認証情報を送信するには、@c8y/ngx-componentsの一部である LoginService (http://resources.cumulocity.com/documentation/websdk/ngx-components/injectables/LoginService.html) に依存関係を追加してください。

export function initApp(loginService: LoginService) {
  return () => {
    const credentials = {
      tenant: "tenantName",
      user: "admin",
      password: "C8Yadmin",
    };
    const basicAuth = loginService.useBasicAuth(credentials);

    return loginService.login(basicAuth, credentials);
  };
}

デフォルトの認証情報でログインするには、サービスからログイン機能を呼び出して、認証方法とデフォルトの認証情報を渡す必要があります。

これでレシピは完成し、認証はバックグラウンドで行われるようになります。

// --- 8< 変更箇所 ----
import { APP_INITIALIZER, NgModule } from '@angular/core';
// --- >8 ----
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as NgRouterModule } from '@angular/router';
import { UpgradeModule as NgUpgradeModule } from '@angular/upgrade/static';
// --- 8< 変更箇所 ----
import { CoreModule, LoginService, RouterModule } from '@c8y/ngx-components';
// --- >8 ----
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES } from '@c8y/ngx-components/upgrade';
import { SubAssetsModule } from '@c8y/ngx-components/sub-assets'; 
import { ChildDevicesModule } from '@c8y/ngx-components/child-devices';
import { CockpitDashboardModule, ReportDashboardModule } from '@c8y/ngx-components/context-dashboard';
import { ReportsModule } from '@c8y/ngx-components/reports';
import { SensorPhoneModule } from '@c8y/ngx-components/sensor-phone';
import { BinaryFileDownloadModule } from '@c8y/ngx-components/binary-file-download';
import { SearchModule } from '@c8y/ngx-components/search';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { CockpitConfigModule } from '@c8y/ngx-components/cockpit-config';
import { DatapointLibraryModule } from '@c8y/ngx-components/datapoint-library';
import { WidgetsModule } from '@c8y/ngx-components/widgets';
import { PluginSetupStepperModule } from '@c8y/ngx-components/ecosystem/plugin-setup-stepper';

@NgModule({
  imports: [
    // Upgrade module must be the first
    UpgradeModule,
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    NgRouterModule.forRoot([...UPGRADE_ROUTES], { enableTracing: false, useHash: true }),
    CoreModule.forRoot(),
    ReportsModule,
    NgUpgradeModule,
    AssetsNavigatorModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    ReportDashboardModule,
    BinaryFileDownloadModule, 
    SearchModule, 
    SubAssetsModule, 
    ChildDevicesModule, 
    CockpitConfigModule, 
    DatapointLibraryModule.forRoot(), 
    WidgetsModule, 
    PluginSetupStepperModule 
  ],
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: initApp,
      multi: true,
      deps: [LoginService],
    },
  ]
})
export class AppModule extends HybridAppModule {
  constructor(protected upgrade: NgUpgradeModule) {
    super();
  }
}

export function initApp(loginService: LoginService) {
  return () => {
    const credentials = {
      tenant: "tenantName",
      user: "admin",
      password: "C8Yadmin",
    };
    const basicAuth = loginService.useBasicAuth(credentials);

    return loginService.login(basicAuth, credentials);
  };
}

まとめ

このチュートリアルでは、カスタムアプリケーションを開発する際に、認証を解除する方法を説明しています。 この種の手法は、アプリケーションが機密情報を持っていない場合に使用することができます。 データ保護が必要な場合は、この手法を避ける必要があります。

国際化の実施

バージョン: 1016.0.321 | パッケージ: @c8y/cli, @c8y/apps and @c8y/ngx-components

イントロダクション

Things Cloudは、コンテンツを翻訳できる統合ツールを提供します。このツールは ngx-translate ライブラリに基づいています。CoreModule はこのツールの設定済みのインスタンスをエクスポートし、Things Cloudは既に統合されています。詳細はngx-translate Github pageをご覧ください。

このチュートリアルでは、最小限の構成で新しいアプリケーションを作成します。

新しいアプリケーションのセットアップ

まず、バージョン1016.0.321以上で、基本プロジェクトapplicationをベースにした新しいアプリケーションを作成します。

以下のコマンドを実行します。

c8ycli new my-app-i18n
cd my-app-i18n
npm install

アプリケーションがセットアップされたら、コンテンツを追加して翻訳するために使用できる新しい基本モジュールを作成します。

以下のファイルを作成してください。

アプリケーションのモジュールに TranslationsModule をインポートします。

これでアプリケーションを実行することができます。 最初に、アプリケーションは1つのTranslationsメニュー項目を表示し、テキストが書かれた空白のページをレンダリングします。(例:Index

デフォルト翻訳の拡張

Things Cloudには、すでに多言語に翻訳された幅広いコンテンツが付属しています。 これらの翻訳は、言語用のカスタム *.po ファイルを追加することで拡張できます。これにより、新しい翻訳を追加したり、既存の翻訳を修正したりすることができます。

以下の手順で、既存の文字列の1つ、例えば「ユーザー設定」を上書きして、デフォルトの「ユーザー設定」の代わりに「ユーザー設定 (de)」を表示させることができます。

  1. 新しいファイル translations/locales/de.po を作成する。

    msgid ""
    msgstr ""
    "Project-Id-Version: c8yui.core\n"
    "Report-Msgid-Bugs-To: \n"
    "POT-Creation-Date: \n"
    "PO-Revision-Date: \n"
    "Last-Translator: \n"
    "Language: de\n"
    "Language-Team: \n"
    "Content-Type: text/plain; charset=UTF-8\n"
    "Content-Transfer-Encoding: 8bit\n"
    "Plural-Forms: nplurals=2; plural=(n != 1);\n"
    
    
    msgid "User settings"
    msgstr "User settings (de)"
    
  2. 以下のように、index.tsファイルを開き、新しく作成したファイルをインポートする。

    (...)
    import { AppModule } from './app.module';
    
    import './translations/locales/de.po';
    
    declare const __MODE__: string;
    (...)
    
  3. サーバーとアプリケーションを再起動します。これでドイツ語を選択できるようになり、ユーザー設定のラベルは、de.poファイルで定義されているように、ユーザー設定(de)に変更されます。

備考
node_modules/@c8y/ngx-components/localesの下にデフォルトの翻訳を含む*.poファイルがあります。これらのファイルを上書きするには、それらを locales ディレクトリにコピーし、 index.ts に上記の de.po のような import 文を追加します。

新しい言語の追加

デフォルトではサポートされていない新しい言語を定義するには、以下の例に従ってください。イタリア語の翻訳が追加されます。

  1. 新しい翻訳ファイルtranslations/locales/it.poを作成します。

    msgid ""
    msgstr ""
    "Project-Id-Version: c8yui.core\n"
    "Report-Msgid-Bugs-To: \n"
    "POT-Creation-Date: \n"
    "PO-Revision-Date: \n"
    "Last-Translator: \n"
    "Language: it\n"
    "Language-Team: \n"
    "Content-Type: text/plain; charset=UTF-8\n"
    "Content-Transfer-Encoding: 8bit\n"
    "Plural-Forms: nplurals=2; plural=(n != 1);\n"
    
    
    msgid "User settings"
    msgstr "User settings (it)"
    
  2. package.jsonファイルを開き、c8y.applicationオブジェクトを以下のように修正する。

    {
      (...)
      "c8y": {
        "application": {
          "name": "my-app-i18n",
          "contextPath": "my-app-i18n",
          "key": "my-app-i18n-application-key",
          "dynamicOptionsUrl": "/apps/public/public-options/options.json",
          "languages": {
            "it": {
              "name": "Italian",
              "nativeName": "Italiano"
            }
          }
        },
        "cli": {}
      }
    }
    
  3. 新しいit.poファイルをindex.ts内にインポートする。

    (...)
    import { AppModule } from './app.module';
    
    import './translations/locales/de.po';
    import './translations/locales/it.po'; // <--
    
    declare const __MODE__: string;
    (...)
    
  4. サーバーとアプリケーションを再起動する。

これでイタリア語を選択できるようになり、ユーザー設定のラベルは、it.poファイルで定義されているように、ユーザー設定(it)に変更されます。

基本的なテキスト翻訳

コンテンツを翻訳する方法は複数あります。最も一般的なのは translate パイプとディレクティブで、次のセクションで説明します。

翻訳パイプ

translateパイプはHTMLビューのコンテンツを翻訳する最も一般的な方法です。次の例は、前のセクションで説明したように、カスタムit.poファイルを追加したと仮定して動作します。

translations/text-translation.component.htmlファイルに、次のように追加してください。

<div>{{ 'User settings' | translate }}</div>

言語がイタリア語に設定されている場合、アプリケーションをリロードするとコンテンツは ユーザー設定(it) としてレンダリングされます。

translateパイプを使うと、翻訳された文字列にパラメータを含めることができます。

結果: Sig. Smithは40歳です。

備考

{{ ... }}で囲まれたテキストを直接テンプレートに入れる場合は、テキストの一部である中括弧をエスケープする必要があります。

例: <div>{{ 'Mr. Smith is \{\{ age \}\} years old' | translate:{ age: 40 } }}</div>

これによりコンパイル時の問題を回避できます。文字列抽出ツールは現在のところこのようなケースをサポートしていないので、自分で *.po ファイルにこのような文字列を記述する必要があります。

ディレクティブの翻訳

コンテンツを翻訳するもう一つの方法は、translations/text-translation.component.htmlの例にあるように、translate属性を使うことです。

<div class="card">
  <div class="card-header separator">
    <h4 class="card-title">Translate directive example</h4>
  </div>
  <div class="card-block">
    This phrase will be translated:
    <span class="m-r-4" translate>User settings</span>
  </div>
</div>

translateパイプを使った例と同様に、spanの内容はユーザー設定(it)に翻訳されます。

パラメータは translate ディレクティブと一緒に以下のように使うことができます。

上の例では、translateディレクティブに加えて、AngularのngNonBindableディレクティブを使用する必要があります。これにより、Angularは中括弧を無視し、翻訳サービスに処理を任せることができます。

さらに、以下の例に示すように、HTMLコードブロック全体を翻訳することもできます。

重要
通常、HTMLブロックを翻訳する際にはngNonBindableを指定することをお勧めします。

変数の内容の翻訳

TypeScriptでは、コンテンツを文字列変数として配置することが可能です。 以下の例のように、このような変数を翻訳することも可能です。

備考
このような文字列を gettext 関数でラップします。これにより、locales/locales.pot ファイルへの文字列の自動抽出が可能になります。これはまた、そのような文字列が翻訳されることを意図していることを示します。 locale-extractツールを使った翻訳用の文字列の抽出をご覧ください。

TypeScript コードの手動翻訳

TypeScript コード内で文字列を手動で翻訳することも可能です。 そのためには、TranslateService をコンポーネントに注入し、その instant メソッドを使用してコンテンツを翻訳します。

instantメソッドを使ってコンテンツを翻訳するのは1回限りの操作なので、ユーザーが言語を変更してアプリケーションをリロードしないと決めた場合、翻訳は更新されません。 このような場合に翻訳を更新したい場合は、代わりに stream メソッドを使用することをお勧めします。

this.textStream = this.translateService.stream(this.variableWithText);
{{ textStream | async }}

あるいは、onLangChangeイベントエミッターを購読して、明示的にテキストを再翻訳することもできます。

this.translateService.onLangChange.subscribe(() => {
  this.translatedVariableWithText = this.translateService.instant(this.variableWithText);
});
重要
メモリリークを防ぐために、すべての登録を解除する必要があります。これは、observablesでAngularの async パイプを使うことで回避できます。

locale-extractツールを使った翻訳用の文字列の抽出

c8cycliのlocale-extractコマンドを使って文字列を取り出すことができます。

このコマンドを実行すると、新しいディレクトリ ./locales が作成されます。 その中には以下が含まれます。

日付の翻訳

現在のロケール設定に従って日付を表示するには、以下の例のようにAngularの date パイプを使用します。

あるいは、c8yDate パイプを使用して medium フォーマットの日付を返します。これは ECMAScript がサポートする範囲外の値でも動作します。