vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

AngularとSpringBootでWebページを作ってみたい その1

はじめに

ひょんなことから試しにWebサイトを作ってみることにしました。

フロントはAngular、バックはSpring bootで、以下の機能を持たせてみます。

  • ページは以下の3ページ
    • 1.トップ
    • 2.メニュー一覧
    • 3.お問い合わせ
  • 1.トップでは 1-1. バナー画像、1-2. ニュース を表示
  • 1-1.バナー画像は複数画像(アイテム数に合わせて動的に設定)を矢印キーまたはタイマーで切り替え
  • 1-2. ニュースはDBに登録されている内容を非同期で取得して表示する
  • 各画面へはヘッダーにボタンを設置し、それをクリックして遷移する

機能や 2.メニュー一覧 の子どものページなど追加したい部分はありますが、
まずは上記をやってみることにします。

ヘッダーを作る

※2017.06.23更新
ページ遷移の方法を、「location.href=“/”;」のようにするのではなく、
「< button routerLink=“/” class=“header-button” >Home< /button >」とするよう変更しました。

切り替えのたびにページ全体がリロードしてしまうこと、
ng buildでビルド後、Spring bootなどで動かした時にルーティングが効かなくなってしまうためです。

global-header.component.html

< header id="global-header" >
  < img width="300" src="https://pbs.twimg.com/media/DClPJ4qUIAAROmQ.jpg" id="header-logo" >
  < button routerLink="/" class="header-button">Home< /button>
  < button routerLink="./menulist" class="header-button">Menu< /button>
  < button routerLink="./contact" class="header-button">お問い合わせ< /button>

global-header.component.css

#global-header{
    background-color: #2980B9;
    width: 100%;
    min-width: 1200px;
    height: 90px;
}
#header-logo{
    margin: 10px;
    width: 70px;
    height: 70px;
    float: left;
}
.header-button{
    background-color: #2980B9;
    border-style: none;
    color: #FFFFFF;
    font-size: 18px;
    width: 160px;
    height: 60px;
    margin: 30px 20px 0px 0px;
    float: left;
}
.header-button:hover{
    background-color: #FFF;
    color: #000;
    font-weight: bold;
}

global-header.component.ts

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

@Component({
  selector: 'app-global-header',
  templateUrl: './global-header.component.html',
  styleUrls: ['./global-header.component.css']
})
export class GlobalHeaderComponent implements OnInit {
  constructor() { }
  ngOnInit() {
  }
}

ページ遷移はHTML側で行うため、Typescriptでは何もしていません。

ルーティング

(開発環境では)localhost:4200以下の各URLにアクセスした時に、
該当のページを表示するため、ルーティングを行います。

まずはsrc/app以下にapp.routing.tsを作成します
(これはangular.cliを使うのではなく、エディタから作成するようです)。

app.routing.ts

import { ModuleWithProviders }  from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { MainPageComponent } from './main-page/main-page.component';
import { ContactPageComponent } from './contact-page/contact-page.component';

const appRoutes: Routes = [
    {
        path: '',
        component: MainPageComponent
    },
    {
        path: 'contact',
        component: ContactPageComponent
    }
];
export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);
  • まだ2.メニュー一覧は追加されていません。

これをapp.module.tsに登録して、
ルーティング処理を有効にします。

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { SlideAnimeComponent } from './slide-anime/slide-anime.component';
import { FormsModule } from '@angular/forms';
import { GlobalHeaderComponent } from './global-header/global-header.component';
import { ContactPageComponent } from './contact-page/contact-page.component';
import { routing } from './app.routing';
import { MainPageComponent } from './main-page/main-page.component';
import { TopBannerComponent } from './top-banner/top-banner.component';
import { TopNewsComponent } from './top-news/top-news.component';

@NgModule({
  declarations: [
    AppComponent,
    SlideAnimeComponent,
    GlobalHeaderComponent,
    ContactPageComponent,
    MainPageComponent,
    TopBannerComponent,
    TopNewsComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    FormsModule,
    routing
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

バナーを作る

トップページに、バナーを表示してみます。

top-banner.component.html

< section id="top-banner-area" >
  < img [src]="[imageSrcPaths[activeImageNum]]" class="top-banner" [@bannerState]="topBannerState" (@bannerState.done)="onBannerChanged()" />
< /section >
  • [@bannerState]と(@bannerState.done)は次の項目で触れます。
  • imgのsrcとして配列(今回は固定)に渡したURLのうち一つを渡し、表示しています。

top-banner.component.css

#top-banner-area{
    background-color: #000;
    padding-left:300px;
    width: 900px;
    height: 600px;
}
.top-banner{
    width: 600px;
    height: 600px;
}
  • 例によってサイズは適当です。

アニメーションをつける

アニメーションで画像を切り替えてみます。

基本的には以前やったものと同じような内容です。

top-banner.component.ts

import { Component, OnInit, HostListener } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';

@Component({
  selector: 'app-top-banner',
  templateUrl: './top-banner.component.html',
  styleUrls: ['./top-banner.component.css'],
  animations: [
    trigger('bannerState',
      [
        state('active', style({
          transform: 'scale(1.0)',
          filter: 'brightness(100%)'
        })),
        state('inactive', style({
          transform: 'scale(0.6)',
          filter: 'brightness(20%)'
        })),
        transition('active => inactive', animate('400ms ease-in')),
        transition('inactive => active', animate('10ms ease-in'))
      ]
    )]
})
export class TopBannerComponent implements OnInit {
  private imageSrcPaths: string[];
  private activeImageNum: number;
  private nextImageNum: number;
  private topBannerState: string;

  constructor() { }
  ngOnInit() {
    this.imageSrcPaths = [
      "https://pbs.twimg.com/media/DClPJ4qUIAAROmQ.jpg",
      "https://pbs.twimg.com/media/DCH75wfU0AAievH.jpg"
    ];
    this.activeImageNum = - 1;
    this.nextImageNum = 0;
    this.topBannerState = 'inactive';
    this.onBannerChanged();
  }
〜省略〜
  onBannerChanged(){
    // アニメーション完了後に次の画像に切り替え.
    // active->inactive、inactive->activeの両方で呼ばれてしまうので後者を無視.
    if(this.activeImageNum === this.nextImageNum){
      return;
    }
    this.activeImageNum = this.nextImageNum;
    this.topBannerState = 'active';
  }
  • あとはtopBannerStateを「active」「inactive」に切り替えてあげればOKです。
  • アニメーションでは画像のサイズを変える他、「filter: brightness」で明るさも変更しています。

注意点

注意すべき点として、Angular4からanimationが@angular/coreから分離されています。

ただ、まだ@angular/coreから削除はされていないため state、style、transition、trigger のimportができてしまうのですが、
@angular/animationsからimportしないとエラーが発生するようです。

また、app.module.tsにBrowserAnimationsModuleを追加する必要があります。

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { SlideAnimeComponent } from './slide-anime/slide-anime.component';
import { FormsModule } from '@angular/forms';
import { GlobalHeaderComponent } from './global-header/global-header.component';
import { ContactPageComponent } from './contact-page/contact-page.component';
import { routing } from './app.routing';
import { MainPageComponent } from './main-page/main-page.component';
import { TopBannerComponent } from './top-banner/top-banner.component';
import { TopNewsComponent } from './top-news/top-news.component';

@NgModule({
  declarations: [
    AppComponent,
    SlideAnimeComponent,
    GlobalHeaderComponent,
    ContactPageComponent,
    MainPageComponent,
    TopBannerComponent,
    TopNewsComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    FormsModule,
    routing
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

キーイベントを取る

さて、次はバナー画像の表示切り替えを、左右の矢印キーを押したときに行うようにしてみます。

キー入力のイベントは、例えばinputタグであれば下記のように書くことができます。

< input (keyup)="onArrowKeyUp($event)" >

これをTypeScript側でevent.keyCodeのようにすれば、押されたキーを判別できます。

top-banner.component.ts

〜省略〜
onArrowKeyUp(event: any){
    if(event.keyCode === 39){ // RightArrow.
        // 右矢印キーが押されたときの処理.
    }
    else if(event.keyCode === 37){ // LeftArrow
        // 左矢印キーが押されたときの処理.
    }
}
〜省略〜

が、inputではなくdivタグなどではうまくイベントを取ることができません。

document.onkeyupに、ngOnInit()などでメソッドを追加することはできるものの、
そのままではクラス内で定義している変数などにアクセスできない問題が起こります。

でどうするかというと、「HostListener」を使います。

top-banner.component.ts

import { Component, OnInit, HostListener } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';

〜省略〜

  @HostListener('document:keyup', ['$event'])
  onArrowKeyUp(event: any){
    if(event.keyCode === 39){ // RightArrow.
      // 右矢印キーが押されたときの処理.
    }
    else if(event.keyCode === 37){ // LeftArrow
      // 左矢印キーが押されたときの処理.
    }
  }
〜省略〜

Javaアノテーションのような感じで、取得したいイベント(document.onkeyup)とその引数($event)をそれぞれ指定すると、
直下(または直後?)のメソッドが、該当イベントの発生時に呼ばれるようになります。

top-banner.component.tsのコード

最後に、ここまでのtop-banner.component.tsのコードを載せておきます。

top-banner.component.ts

import { Component, OnInit, HostListener } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';

@Component({
  selector: 'app-top-banner',
  templateUrl: './top-banner.component.html',
  styleUrls: ['./top-banner.component.css'],
  animations: [
    trigger('bannerState',
      [
        state('active', style({
          transform: 'scale(1.0)',
          filter: 'brightness(100%)'
        })),
        state('inactive', style({
          transform: 'scale(0.6)',
          filter: 'brightness(20%)'
        })),
        transition('active => inactive', animate('400ms ease-in')),
        transition('inactive => active', animate('10ms ease-in'))
      ]
    )]
})
export class TopBannerComponent implements OnInit {
  private imageSrcPaths: string[];
  private activeImageNum: number;
  private nextImageNum: number;
  private topBannerState: string;

  constructor() { }
  ngOnInit() {
    this.imageSrcPaths = [
      "https://pbs.twimg.com/media/DClPJ4qUIAAROmQ.jpg",
      "https://pbs.twimg.com/media/DCH75wfU0AAievH.jpg"
    ];
    this.activeImageNum = - 1;
    this.nextImageNum = 0;
    this.topBannerState = 'inactive';
    this.onBannerChanged();
  }
  @HostListener('document:keyup', ['$event'])
  onArrowKeyUp(event: any){
    if(event.keyCode === 39){ // RightArrow.
      this.nextImageNum = (this.activeImageNum < this.imageSrcPaths.length - 1)? this.activeImageNum + 1 : 0;
      this.topBannerState = 'inactive';
    }
    else if(event.keyCode === 37){ // LeftArrow
      this.nextImageNum = (this.activeImageNum > 0)? this.activeImageNum - 1: this.imageSrcPaths.length - 1;
      this.topBannerState = 'inactive';
    }
  }
  onBannerChanged(){
    if(this.activeImageNum === this.nextImageNum){
      return;
    }
    this.activeImageNum = this.nextImageNum;
    this.topBannerState = 'active';
  }
}

おわりに

次はバナー画像をタイマーで切り替えるようにしたり、
非同期でニュースのデータを取得して表示できるようにしたいと思います。

参照

Angular

Animation

KeyEvent

CSS