ダッシュボードにカスタムウィジェットを追加する
バージョン: 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つの部分で構成されます。
設定(Configuration): ユーザーがダッシュボードにウィジェットを追加する際に表示されるコンポーネント
ウィジェット(Widget): ダッシュボードに追加された時に表示されるコンポーネント
そのため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 ();
}
}
上記の数字の説明は以下の通りです。
エントリーコンポーネントとしてコンポーネントを定義し、このモジュールによってアクセスできるように宣言します。
HOOK_COMPONENTS
と共にマルチプロバイダーフックを追加します。このフックはアプリケーションによって収集され、設定した値に基づいてウィジェットに追加します。
インベントリに保存されているデータを特定するため、IDは一意である必要があります。ラベルと詳細記述はタイトルとウィジェットのドロップダウンとして表示されます。
この部分は、すでに定義してあるコンポーネントとウィジェットに関連付けるようフックに指示します。
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
と呼ばれます。コンテキストビューにはいくつか種類があります(例えば、Device
、 Group
、 User
、 Application
、Tenant
など)。ハッシュナビゲーションで特定の 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 ();
}
}
上記の数字の説明は以下の通りです。
マルチプロバイダーフックの HOOK_ROUTE
を提供します。これは現在のルートの設定を拡張するためにアプリケーションに指示します。
ルートフックの定義に値を使用することを指定します。例えば、ルートを非同期に解決したい場合、クラスを使用することもできます。
ルートのコンテキストを定義します。定義するには ViewContext
enum を使用する必要があります。この例では、デバイスのコンテキストを拡張します。
表示されるパスです。コンテキストパスに追加されます。この例では、完全パスは以下になります。device/:id/hello
パスがユーザーによって入力されたらどのコンポーネントを表示するかを定義します。
タブがどのように表示されるかを定義するのは 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.ts の entryComponents
に追加することでアプリケーションをコンパイルすることができます。
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 ;
}
}
上記の数字は以下のように説明できます。
これは、Angularルーターと整合していない唯一の部分です。コンテキストルートでは、CanActivate
が2回呼び出されます。1回は親ルートがアクティブになり、1回は子ルートがアクティブになります。最初の呼び出しは、タブを表示する必要があるかどうかを確認し、2番目の呼び出しは、ユーザーがそのタブに移動できるかどうかを確認します。したがって、 ActivatedRouteSnapshot
は両方の呼び出しで異なり、2番目のケースでは親から contextData
を解決する必要があります。
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つを含みます。
Angular CLI : Angular CLIを使用すると全Angularエコシステムの恩恵を受けることができるため、Angular CLIの多くのツールを再利用できます(テストなど)。
@c8y/cli :私たちのツールとうまく統合する事前に用意されている方法ですが、特別なケースは許可しない可能性が高いです。
Web SDKを使い始めるには、全部で3つの可能性があります。
既存のハイブリッドアプリケーションを拡張する。
Angular CLIで純粋なAngularアプリケーションをビルドする。
@c8y/cli
でビルドする。
どれを選択するかは、ビルドするアプリケーションに大きく依存します。
例えば、プラットフォームのルックアンドフィールに従ったアプリケーションが必要であるが、マテリアル フレームワークなど特定のシナリオに特別な依存関係を使用したい場合は、純粋な Angular CLI ソリューションを使用するのが最適です。
最も一般的なユースケースは、ハイブリッドアプリケーションの拡張で、このレシピではこれを取り上げます。
まず、このアプローチの限界を見て、なぜこの概念がそのように設計されているのかを理解しましょう。
ハイブリッドモードの制限
ハイブリッドアプリケーションを実行する時、AngularとAngularJSは並行して実行されるようにする必要があるため、いくつかの制限があります。
index.html にアクセスできない: ブートストラップの処理全体はThings Cloudが処理し、AngularとAngularJSの必要な要素がすべて揃っていることを確認する必要があります。ブートストラップ テンプレートを変更することはできず、ルートを追加することのみ可能です。
サービスは最初に読み込まれる必要があるため、ルートアプリモジュールにサービスを注入することもできません。サービスの宣言部分で、ルートもしくは providedIn: root
として提供する必要があります。
ルーターのルートは UPGRADED_ROUTES
の前で定義されていなければなりません。この理由は、Angularのルーターは UPGRADED_ROUTES
で定義されているすべてのAngularJSのルートに合致する **
というパスを持っているためです。UPGRADED_ROUTES
の前で定義する場合、定義したルートの前に **
パスが合致します。
すべての拡張はフックを通して行う必要があります。これはハイブリッドアプリケーションではAngularとAngularJSが必要であり、双方でフックが使われる可能性があるためです。
スタイリングはグローバルのスタイル変更に制限されています。つまり、カスタムブランディングを適用するか、インラインスタイルを使用することのみよって拡張可能になります。このバージョンでは styleUrls
はサポートされていません。
制限について理解したところで最初のアプリケーションを拡張し、拡張用フックを開発することができます。これを行うには、ハイブリッドアプリケーションをスキャフォールディングにしておく必要があります。
c8y/apps
は、デフォルトのアプリケーションとその最小限のセットアップを含むパッケージです。
c8ycli
は new
コマンドでアプリケーションを初期化するたびにこのパッケージを使用します。
次のセクションでは、スキャフォールディングプロセスとハイブリッドアプリケーションを拡張する方法について順を追って説明します。
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 ` ;
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 } 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' ;
// --- 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.ts のHOOK_NAVIGATOR_NODE
を次のように使用します。
{
provide : HOOK_NAVIGATOR_NODES ,
useValue : [{
label : 'Hello' ,
path : 'hello' ,
icon : 'rocket' ,
priority : 1000
}] as NavigatorNode [], // 1
multi : true
}
(1)にあるように、型付けは自分で行う必要があります。
これを避けるために、hookX
関数を使うこともできます。
次の例では hookRoute
と hookNavigatorNode
を使ってナビゲーターノードを追加しています。
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 , hookNavigator , hookRoute } 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" ;
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 ();
}
}
上記の数字の説明は以下の通りです。
hookRoute
と hookNavigator
を提供します。
特定のvalueを使います。複雑なケースでは useClass
や get()
を定義することもできます。
アプリケーションへのパスを指定します。必ず/
で始めてください。
ナビゲーター・ノードがどのように見えるかを定義します。
この拡張フックを実装すると、ナビゲータに次のような新しいエントリーが表示されます。
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
アプリケーションがセットアップされたら、コンテンツを追加して翻訳するために使用できる新しい基本モジュールを作成します。
以下のファイルを作成してください。
translations/translations.module.ts :
import { NgModule } from '@angular/core' ;
import { RouterModule , Routes } from '@angular/router' ;
import {
CoreModule ,
NavigatorNode ,
gettext ,
HOOK_NAVIGATOR_NODES
} from '@c8y/ngx-components' ;
import { TextTranslationComponent } from './text-translation.component' ;
const routes : Routes = [
{
path : 'translations' ,
component : TextTranslationComponent
},
];
const translationsNode = new NavigatorNode ({
label : gettext ('Translations' ),
icon : 'star' ,
path : '/translations' ,
routerLinkExact : false
});
export const navigatorNodes = {
provide : HOOK_NAVIGATOR_NODES ,
useValue : { get : () = > translationsNode },
multi : true
};
@NgModule ({
declarations : [TextTranslationComponent ],
imports : [RouterModule .forChild (routes ), CoreModule ],
providers : [navigatorNodes ]
})
export class TranslationsModule {}
translations/text-translation.component.ts :
import { Component } from '@angular/core' ;
@Component ({
selector : 'text-translation' ,
templateUrl : './text-translation.component.html'
})
export class TextTranslationComponent {}
translations/text-translation.component.html :
アプリケーションのモジュールに TranslationsModule
をインポートします。
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' ;
import { TranslationsModule } from './translations/translations.module' ; // <--
@NgModule ({
imports : [
BrowserAnimationsModule ,
RouterModule .forRoot (),
ngRouterModule .forRoot ([], { enableTracing : false , useHash : true }),
CoreModule .forRoot (),
TranslationsModule // <--
],
bootstrap : [BootstrapComponent ]
})
export class AppModule {}
これでアプリケーションを実行することができます。
最初に、アプリケーションは1つのTranslations メニュー項目を表示し、テキストが書かれた空白のページをレンダリングします。(例:Index
)
デフォルト翻訳の拡張
Things Cloudには、すでに多言語に翻訳された幅広いコンテンツが付属しています。 これらの翻訳は、言語用のカスタム *.po ファイルを追加することで拡張できます。これにより、新しい翻訳を追加したり、既存の翻訳を修正したりすることができます。
例
以下の手順で、既存の文字列の1つ、例えば「ユーザー設定」を上書きして、デフォルトの「ユーザー設定」の代わりに「ユーザー設定 (de)」を表示させることができます。
新しいファイル 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)"
以下のように、index.ts ファイルを開き、新しく作成したファイルをインポートする。
(...)
import { AppModule } from './app.module' ;
import './translations/locales/de.po' ;
declare const __MODE__ : string ;
(...)
サーバーとアプリケーションを再起動します。これでドイツ語を選択できるようになり、ユーザー設定 のラベルは、de.po ファイルで定義されているように、ユーザー設定 (de)に変更されます。
備考
node_modules/@c8y/ngx-components/locales の下にデフォルトの翻訳を含む*.po
ファイルがあります。これらのファイルを上書きするには、それらを locales ディレクトリにコピーし、 index.ts に上記の de.po のような import 文を追加します。
新しい言語の追加
デフォルトではサポートされていない新しい言語を定義するには、以下の例に従ってください。イタリア語の翻訳が追加されます。
新しい翻訳ファイル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)"
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" : {}
}
}
新しいit.po ファイルをindex.ts 内にインポートする。
(...)
import { AppModule } from './app.module' ;
import './translations/locales/de.po' ;
import './translations/locales/it.po' ; // <--
declare const __MODE__ : string ;
(...)
サーバーとアプリケーションを再起動する。
これでイタリア語を選択できるようになり、ユーザー設定 のラベルは、it.po ファイルで定義されているように、ユーザー設定 (it)に変更されます。
基本的なテキスト翻訳
コンテンツを翻訳する方法は複数あります。最も一般的なのは translate
パイプとディレクティブで、次のセクションで説明します。
翻訳パイプ
translate
パイプはHTMLビューのコンテンツを翻訳する最も一般的な方法です。次の例は、前のセクションで説明したように、カスタムit.po ファイルを追加したと仮定して動作します。
translations/text-translation.component.html ファイルに、次のように追加してください。
<div >{{ 'User settings' | translate }}</div >
言語がイタリア語に設定されている場合、アプリケーションをリロードするとコンテンツは ユーザー設定 (it) としてレンダリングされます。
translate
パイプを使うと、翻訳された文字列にパラメータを含めることができます。
新しいファイルtranslations/locales/it.po を作成する。:
msgid "Mr. Smith is {{ age }} years old"
msgstr "Sig. Smith ha {{ age }} anni"
translations/text-translation.component.ts :
export class TextTranslationComponent {
textWithParam = gettext ('Mr. Smith is {{ age }} years old' );
}
translations/text-translation.component.html :
<div >{{ textWithParam | translate:{ age: 40 } }}</div >
結果: 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
ディレクティブと一緒に以下のように使うことができます。
translations/locales/it.po :
msgid "{{ filteredItemsCount }} of {{ allItemsCount }} items."
msgstr "{{ filteredItemsCount }} of {{ allItemsCount }} items. (it)"
translations/text-translation.component.html :
<div class = "card" >
<div class = "card-header separator" >
<h4 class = "card-title" >Translate directive with parameters example</h4 >
</div >
<div class = "card-block" >
This sentence will be translated:
<span class = "m-r-4" ngNonBindable translate [ translateParams ] = " { filteredItemsCount: 10 , allItemsCount: 100 } " >
{{ filteredItemsCount }} of {{ allItemsCount }} items.
</span >
</div >
</div >
上の例では、translate
ディレクティブに加えて、AngularのngNonBindable
ディレクティブを使用する必要があります。これにより、Angularは中括弧を無視し、翻訳サービスに処理を任せることができます。
さらに、以下の例に示すように、HTMLコードブロック全体を翻訳することもできます。
translations/locales/it.po :
msgid "Read about your current language in <a href=\"#guide\">our guide</a>"
msgstr "Read about your Italian language in <a href=\"#italian-guide\">our Italian guide</a>"
translations/text-translation.component.html :
<div class = "card" >
<div class = "card-header separator" >
<h4 class = "card-title" >Translate directive used on html code</h4 >
</div >
<div class = "card-block" >
<span class = "m-r-4" translate ngNonBindable >
Read about your current language in <a href = "#guide" >our guide</a >
</span >
</div >
</div >
重要
通常、HTMLブロックを翻訳する際にはngNonBindable
を指定することをお勧めします。
変数の内容の翻訳
TypeScriptでは、コンテンツを文字列変数として配置することが可能です。
以下の例のように、このような変数を翻訳することも可能です。
備考
このような文字列を
gettext
関数でラップします。これにより、
locales/locales.pot ファイルへの文字列の自動抽出が可能になります。これはまた、そのような文字列が翻訳されることを意図していることを示します。
locale-extractツールを使った翻訳用の文字列の抽出 をご覧ください。
TypeScript コードの手動翻訳
TypeScript コード内で文字列を手動で翻訳することも可能です。
そのためには、TranslateService
をコンポーネントに注入し、その instant
メソッドを使用してコンテンツを翻訳します。
instant
メソッドを使ってコンテンツを翻訳するのは1回限りの操作なので、ユーザーが言語を変更してアプリケーションをリロードしないと決めた場合、翻訳は更新されません。
このような場合に翻訳を更新したい場合は、代わりに stream
メソッドを使用することをお勧めします。
translations/text-translation.component.ts :
this .textStream = this .translateService .stream (this .variableWithText );
translations/text-translation.component.html :
あるいは、onLangChange
イベントエミッターを購読して、明示的にテキストを再翻訳することもできます。
translations/text-translation.component.ts :
this .translateService .onLangChange .subscribe (() = > {
this .translatedVariableWithText = this .translateService .instant (this .variableWithText );
});
重要
メモリリークを防ぐために、すべての登録を解除する必要があります。これは、observablesでAngularの async
パイプを使うことで回避できます。
c8cycliのlocale-extract
コマンドを使って文字列を取り出すことができます。
node_modules/@c8y/locales - すべてのデフォルトの文字列と翻訳が含まれています。
. - カスタムモジュール、コンポーネント、テンプレート、サービスなどが含まれています。
このコマンドを実行すると、新しいディレクトリ ./locales が作成されます。
その中には以下が含まれます。
すべての利用可能な言語のための拡張子.po を持つファイル。これらのファイルを ./translations/locales ディレクトリにコピーし、必要なインポートを追加し、必要に応じて翻訳を編集することができます。
locales.pot ファイルには、 translate
パイプ、 translate
ディレクティブ、または gettext
メソッドでマークされた文字列がすべて含まれています。つまり、node_modules/@c8y/locales のデフォルト文字列は含まれていません。これらの値をカスタムの *.po
ファイルに追加して翻訳することができます。
日付の翻訳
現在のロケール設定に従って日付を表示するには、以下の例のようにAngularの date
パイプを使用します。
あるいは、c8yDate
パイプを使用して medium
フォーマットの日付を返します。これは ECMAScript がサポートする範囲外の値でも動作します。
translations/text-translation.component.html :
<div class = "card" >
<div class = "card-header separator" >
<h4 class = "card-title" >Cumulocity date pipe example</h4 >
</div >
<div class = "card-block" >
<div >This date will be translated: {{ currentDate | c8yDate }}.</div >
<div >
This date exceeding the range supported by ECMAScript will be translated:
{{ 8640000000000000 + 1 | c8yDate }}.
</div >
</div >
</div >