vaguely

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

Angularのセキュリティ対策について調べてみる その2

はじめに

前回の続きです。

今回はContent security policy(CSP)、Offline template compilerについて追いかけてみました。

Content security policy(CSP)について

CSPは、画像やJavascriptなどのファイルを読み込むことのできるサイト(ドメイン)を指定するためのものです。

もしこれらのファイルを同一ドメインのみから読み込む場合、CSPで同一ドメインのみに制限をかけることで、
万一クロスサイトスクリプティング(XSS)の対応漏れがあったとしても、
(外部のスクリプトファイルが無効なため)危険を回避できる可能性が高まります。

で、このCSPを有効にするには、Webサーバーから Content-Security-Policy HTTP ヘッダを返す必要があります。

今回はSpring boot(Spring Framework)を使用するため、
Spring Securityで設定を行うことになります。

Spring Securityを使用する

※今回はあくまでCSPの確認を行うことができれば良い。という方針で設定しています。
Spring Securityの正しい使い方については公式ドキュメントや本記事の参照リンクなどを確認してください。

今回は以前作成したプロジェクトを流用することにします。

といってもやっているのはコントローラークラスのルーティング設定ぐらいですが。

ではまずSpring Securityを使えるようにしてみます。

build.gradle

buildscript {
    ext {
        springBootVersion = '1.5.4.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'

version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}
dependencies {
    compile('org.springframework.boot:spring-boot-starter-security')
    compile('org.springframework.boot:spring-boot-starter-web')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}
  • dependenciesにSpring Securityを追加するだけです。
    これを起動すると、Spring Securityが有効になってトップページからログインを求められるようになります。
    (ユーザー名: User、パスワードはSpring bootのプロジェクト起動中にログに出力されます)

Configクラスの作成

このままだとログインが必要ないページでも全てログインしないと表示できなくなるので問題です。
また、(今回は使用しませんが)ユーザーのパスワードが、ビルドごとに変更されるのも不便ですね。

このような設定を行うため、xmlファイルまたはclassを作成します。
(今回はclassを使用することにしました)

WebSecurityConfig.java

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 1. ユーザーの設定.
        auth
                .inMemoryAuthentication()
                .withUser("user")
                .password("1234")
                .roles("USER");
    }
    @Override
    public void configure(HttpSecurity http) throws Exception {
        // 2. ページの閲覧権限の設定.
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/menulist").authenticated();
    }
}
  • Spring SecurityのConfigクラスとして認識させるためには、 WebSecurityConfigurerAdapter の継承、
    @Configuration 、 @EnableWebSecurity の付与が必要です。
  • 1でユーザー名、パスワードを設定し、2でページ(トップ、/menulist)の閲覧権限を指定しています。
  • (上記では仮に /menulist をログイン中でしか表示できないようにしていますが)
    今回は全てのユーザーに表示して良いページしかないため、1、2は無くても問題ありません。

X-Frame-Options

実はこの状態で下記のような iframe を含んだHTMLを表示させようとしても、
iframe がブロックされ、表示されません。

main-page.component.html

< img src='https://pbs.twimg.com/media/DC_pK4OUMAAmy_B.jpg' />
< img src='assets/img/sake.jpg' />
< iframe srcdoc="< img src='https://pbs.twimg.com/media/DC_pK4OUMAAmy_B.jpg' />">< /iframe>

それは、Spring Securityではデフォルトで X-Frame-Options がDENYに設定され、iframeが無効になっているためです。

有効にするためにはConfigクラスの configure(HttpSecurity http) で設定を変更します。

WebSecurityConfig.java

〜省略〜
    @Override
    public void configure(HttpSecurity http) throws Exception {
        〜省略〜
        http
                .headers()
                .frameOptions()
                .sameOrigin();
    }
}
  • これで同一ドメインから追加された iframe は有効になり表示されるようになります。
  • ただし iframe が不要なのであればDENYのままの方が良いですね。

CSPの設定

さていよいよCSPです。
こちらはデフォルトでは有効にならないため、 configure(HttpSecurity http) で設定を行います。

WebSecurityConfig.java

〜省略〜
    @Override
    public void configure(HttpSecurity http) throws Exception {
    〜省略〜
        http
            .headers()
            .contentSecurityPolicy("script-src 'self' 'unsafe-eval'; img-src 'self';");
    }
}
  • スクリプト、画像を同一ドメインのみ有効にしています。
  • 各要素は ; (セミコロン)で区切られます。
  • AngularでCSPを使用する場合、'self'以外に'unsafe-eval'(文字列からコードを生成する eval() や類似メソッドの使用許可)を指定しておく必要があります。
    これが抜けていると、 vendor.bundle.js でエラーが発生し、ページが表示されません。
  • スクリプト、画像の他にドメインを制限したい場合は下記などを参照してください。
    Content Security Policy (CSP) - Web セキュリティ - MDN

Offline template compilerについて

【個人的理解に基づく意訳】

Offline template compilerを使うことで、Template Injectionと呼ばれる脆弱性(下記参照)の回避とパフォーマンスの向上が見込まれます。

プロダクトレベル、特にテンプレートがユーザーデータを含む場合、動的にテンプレートを生成するのではなく、
Offline template compilerを使ってください。

Angularではテンプレートを信頼しているため、動的に生成したテンプレートに含まれる問題に対して組み込みのセキュリティ機能が働かないためです

動的にフォームを生成する必要がある場合は、Dynamic Formsのガイドページを参照してください。

ということですので、Dynamic Formsについてもおいおい調べてみたいと思います(そんなのばっかりですね。。。)。

サーバー側のXSS対策について

【個人的理解に基づく意訳】

Angularで構築したWebアプリケーションに、サーバー側からテンプレート言語(Spring Frameworkだと JSP や Thymeleaf ?)を使ってテンプレートのコードを入れてしまうと、
XSSによってWebアプリケーションが自由にコントロールされてしまう危険があります。

これを防ぐため、サーバー側からテンプレート言語を使って渡す値をエスケープ処理し、 サーバー側でAngularのテンプレートを生成するのを避けてください。

フロント側はAngularに任せて、サーバー側からAngularCLIで生成したHTMLなどにあれこれするのはやめてね。ということでしょうか。

参照

Angularのセキュリティ対策について調べてみる その1

はじめに

フロントエンドで行うセキュリティ対策について、Angularではどのようにしているのかなぁ〜、と疑問に思ったので調べてみました。

まずはAngularの公式ドキュメントを元に、あれこれ試してみることにします。

https://angular.io/guide/security

雑な翻訳で進めているため、間違いなどありましたらご指摘のほどよろしくお願いいたしますm( )m

XSSについて

ドキュメントではまずXSSへの対応について書かれています。

Angularではプロパティやスタイルなどの値はTemplateから挿入され、それらの値は全てデフォルトでサニタイズされます。
そのため、仮にユーザーが入力した値をそのままページ上に表示したとしても、Javascriptなどのコードは実行されません。

Angularのセキュリティの文脈で扱われる要素は以下の4つです。

  • HTML
  • Style (CSS)
  • URL (例:< a href>)
  • Resource URL (例:< script src>)

untrustedなコードがサニタイズされた場合、ConsoleにはWarningが出力されます。

ということで、src/app/inner-html-binding.component.htmlを試してみました。

< h3>Binding innerHTML
< p>Bound value:< /p>
< p>{{htmlSnippet}}< /p>
< p>Result of binding to innerHTML:< /p>
< p [innerHTML]="htmlSnippet">< /p>
  • クラスは今回特に関係がないと考えたため、簡略化のため省いています。
  • {{htmlSnippet}}と[innerHTML]に後述のHTMLなどを含めた値を渡し、どのように反映されるかを確認します。
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-sample-from-docs',
  templateUrl: './sample-from-docs.component.html',
  styleUrls: ['./sample-from-docs.component.css']
})
export class SampleFromDocsComponent implements OnInit {

  
  htmlSnippet = 'Template < script>alert("0wned")< /script>< b>Syntax< /b>';
  

  constructor() { }
  ngOnInit() {
  }
}
  • htmlSnippetにHTMLタグを含めてどう表示されるかを確認しています。
    上記はブログ内でHTMLタグが反映されないように「<」の後にスペースを入れていますが、実際には削除してください。

結果

{{htmlSnippet}}

  • 入力されたHTMLタグは反映されず、記号も含めてそのまま表示される。

[innerHTML]

  • scriptタグは除去され、bタグは反映される。

色々試してみた

[innerHTML]の方はセキュリティ上問題ないとされたHTMLタグは反映されました。

では、他にどのようなものが反映されるか、または反映されないのかを調べてみました。

〜省略〜
export class SampleFromDocsComponent implements OnInit {

  
  htmlSnippet = '< div class="testclass" id="testid" style="color:red;">Test< br>Test2\nTest3< /div>< font color=red>Test4< /font>< ul>< li>t< /li>< li>e< /li>< /ul>< h1>H1< /h1>< img src="#" onerror=alert(0) alt="test" />< video src="#">< /video>< a href="/">Top< /a>< a href="javascript:alert(0)">script< /a>';
  

  constructor() { }
  ngOnInit() {
  }
}

結果

{{htmlSnippet}}

  • 先程と同じくタグは全て反映されず、記号も含めて入力された値がそのまま表示される。

[innerHTML]

反映されたもの
* divタグ
* class
* brタグ
* fontタグ
* ulタグ
* liタグ
* h1タグ
* imgタグ
* videoタグ
* aタグ
反映されなかったもの
* id
* style
* 改行文字(\n)
* onerror
* href="javascript:alert(0)"
* iframeタグ
  • 反映されなかったものの大部分はただ除去されただけですが、「href=“javascript:alert(0)"」は「href="unsafe:javascript:alert(0)"」に置き換えられ、
    リンクをクリックすると「xdg-openを開きますか?」と聞かれ、「xdg-openを開く」としてもJavascriptの実行はされませんでした。
  • classが反映されてidが除去されるのが謎ですね。。。何かidに関連する脆弱性があったのでしょうか。
  • classは反映されるものの、例えばCSSにこのクラスに対して何かを書いても反映はされませんでした。おそらくCSSと上記コードの実行順によるものだと思います。
  • aタグはそのまま反映されるということで、ユーザーの入力した値をそのまま反映する場合は、クッションのページなどを挟んだ方が良いかも知れません。

FormsModuleについて

ドキュメントからは外れますが、Formに入力されたデータも見てみました。

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { SampleFromDocsComponent } from './sample-from-docs/sample-from-docs.component';
import { SampleForFormComponent } from './sample-for-form/sample-for-form.component';

@NgModule({
  declarations: [
    AppComponent,
    SampleFromDocsComponent,
    SampleForFormComponent
  ],
  imports: [
    BrowserModule,
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
  • ngModelを使って2 way bindingを行うために、FormsModuleのインポートが必要です。

sample-for-form.component.ts

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

@Component({
  selector: 'app-sample-for-form',
  templateUrl: './sample-for-form.component.html',
  styleUrls: ['./sample-for-form.component.css']
})
export class SampleForFormComponent implements OnInit {
  private inputValue: string;
  constructor() { }

  ngOnInit() {
  }
  onSubmitButtonClicked(){
    console.log(this.inputValue);
  }
}
  • 入力された値を保持する変数と、ボタンをクリックした時に入力されたデータをログに出力するようにしました。

sample-for-form.component.html

< textarea [(ngModel)]='inputValue'>< /textarea>
< textarea [(ngModel)]='inputValue'>< /textarea>
< div>{{inputValue}}< /div>
< button (click)='onSubmitButtonClicked()'>Submit< /button>
  • 2つある内どちらかのTextareaに値を入力すると、もう一方にも反映される、という内容です。

確認内容

HTMLタグ、改行、絵文字を入力して、2 way bindingでの動作とボタン押下時のログを見てみます。

結果

HTMLタグ、絵文字はそのまま、改行も反映された状態で出力されました。

ということで、HTTPヘッダ、メールヘッダにユーザーが入力した値をそのまま反映する場合、
改行を取り除く必要があります(HTTPヘッダインジェクション、メールヘッダインジェクション防止のため)。

ま、そもそも値をそのまま反映するな、という話ではありますが。

暗号化

Angularに限ったことではありませんが、例えばログインを行う場合、
入力されたパスワードは暗号化してサーバー側に投げるのかな?と思っていました。

が、実際にはSSLで第三者に読み取られないようにした上で、サーバー側で必要に応じて暗号化を行う、というのが主流のようです。

ま、そりゃそうか。

ちなみにJavasciriptで暗号化とググるとCrypto.jsと、
Web Crypto APIが出てきます。

こちらもおいおい調べていきたいところ。

おわりに

結局ドキュメントからは話が逸れてしまいましたが、XSSについてはデフォルトでも対策がとられていることがわかりました。

自分でサニタイズをするかどうかは、AngularのTemplateだけを使って値を表示する場合は不要ですが、
そうでない場合はサニタイズしておくか、またはユーザーが入力した値がそのまま保存されていることを明示しておいた方が良さそうです。

次はドキュメントの続きを。。。

参照

AngularのAnimationsで画像をスライドさせたい

はじめに

AngularのAnimationsで画像をスライドさせてみたら色々大変でした、というお話です。

※2017.7.6 各コードの解説などを追記しました

今回実現したかったことは、Unity日本語サイトトップのバナーのような、
表示中の画像に、画面外から次の画像が重なってくるアニメーションです。

http://japan.unity3d.com/

結局うまく再現できなかったため、表示中の画像が画面外にスライドすると同時に次の画像がスライドしてくる、という内容にしました。

アニメーション1(失敗)

以前は表示中の画像をフェードアウトのアニメーションで非表示にしたあと、
次の画像を表示する、という内容にしていました。

ということは、次の画像を表示するアニメーションを画面外から移動してくるようにして、
フェードアウトと同時に実行すれば完成じゃね?

と思い、そして失敗しましたorz

main-page.component.html

< section id="top-banner-area" >
  < div *ngFor='let bannerImage of bannerImages' >
    < a href='bannerImage.transitionPath'>
      < img [src]="bannerImage.imagePath" class="top-banner" [@bannerState]="bannerImage.bannerState" (@bannerState.done)="onBannerChanged()" />
    < /a>
  < /div>
< /section>
〜省略〜

main-page.component.ts

import { Component, OnInit, HostListener } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { Title } from '@angular/platform-browser';
import { BannerImage } from '../banner-image' 

@Component({
  selector: 'app-main-page',
  templateUrl: './main-page.component.html',
  styleUrls: ['./main-page.component.css'],
  animations: [
    trigger('bannerState',
      [
        state('active', style({
          zindex: 1,
          display: 'block',
          transform: 'translateX(0%)',
          filter: 'brightness(100%)',
          
        })),
        state('inactive', style({
          zindex: 0,
          filter: 'brightness(20%)',
          display: 'none',
        })),
        state('beforeActive', style({
          transform: 'translateX(100%)',
          filter: 'brightness(100%)',
          zindex: 1,
        })),
        transition('active => inactive', animate('400ms ease-in')),
        transition('inactive => beforeActive', animate('1ms ease-in')),
        transition('beforeActive => active', animate('400ms ease-in'))
      ]
    )]
})
export class MainPageComponent implements OnInit {
  private bannerImages: BannerImage[];
  private activeImageNum: number;
  private nextImageNum: number;
  
  constructor(private titleService: Title) {}
  ngOnInit() {
    this.titleService.setTitle('メインページ');

    this.bannerImages = [
      {imagePath: "assets/img/banner01.jpg", transitionPath: '', bannerState: 'inactive'},
      {imagePath: "assets/img/sake.jpg", transitionPath: '', bannerState: 'inactive'}
    ];
    this.activeImageNum = -1;
    this.nextImageNum = 0;
    
    this.onBannerChanged();
  }
  @HostListener('document:keyup', ['$event'])
  onArrowKeyUp(event: any){
    if(event.keyCode === 39){ // RightArrow.
      this.nextImageNum = (this.activeImageNum < this.bannerImages.length - 1)? this.activeImageNum + 1 : 0;
      this.bannerImages[this.activeImageNum].bannerState = 'inactive';
      this.bannerImages[this.nextImageNum].bannerState = 'beforeActive';
    }
    else if(event.keyCode === 37){ // LeftArrow
      this.nextImageNum = (this.activeImageNum > 0)? this.activeImageNum - 1: this.bannerImages.length - 1;
      this.bannerImages[this.activeImageNum].bannerState = 'inactive';
      this.bannerImages[this.nextImageNum].bannerState = 'beforeActive';
    }
  }
  onBannerChanged(){
    if(this.activeImageNum === this.nextImageNum){
      return;
    }
    this.bannerImages[this.nextImageNum].bannerState = 'active';
    this.activeImageNum = this.nextImageNum;
  }
}

main-page.component.css

#top-banner-area{
    background-color: #000;
    width: 81%;
    min-width: 1200px;
    height: 600px;

}
.top-banner{
    height: 600px;
    position:absolute;
    margin-left: auto;
    margin-right: auto;
}

内容

  • active -> inactive -> beforeActive -> activeの繰り返し
  • inactiveで画像の明るさを落としたあと非表示
  • beforeActiveで画像をX軸方向に枠外まで移動 + z-indexを1にしてinactiveより前に表示
  • activeでbeforeActiveの位置から画面中央に移動

うまくいかなかったところ

  • z-indexが効かず、beforeActive -> activeで移動する画像が表示中の画像より前に表示されたり後ろに表示されたりする
  • 上記のコードで再確認したところ再現はしなかったのだが(stateが2つの場合?)アニメーションの実行完了速度が画像(配列の順番?)によって異なる現象が発生した

画像をスライドさせる

ということで、表示中の画像が画面外にスライドすると同時に次の画像がスライドしてくる、という内容のアニメーションを作ってみることにしました。

main-page.component.html

アニメーション1(失敗)と同じです。

main-page.component.ts

import { Component, OnInit, HostListener } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { Title } from '@angular/platform-browser';
import { BannerImage } from '../banner-image' 

@Component({
  selector: 'app-main-page',
  templateUrl: './main-page.component.html',
  styleUrls: ['./main-page.component.css'],
  animations: [
    trigger('bannerState',
      [
        state('active', style({
          display: 'block',
          transform: 'translateX(0%)',
          filter: 'brightness(100%)',
        })),
        state('moveLeft', style({
          transform: 'translateX(-100%)',
          filter: 'brightness(20%)',
          display: 'none'
        })),
        state('fromRight', style({
          transform: 'translateX(100%)',
        })),
        state('moveRight', style({
          transform: 'translateX(100%)',
          filter: 'brightness(20%)',
          display: 'none'
        })),
        state('fromLeft', style({
          transform: 'translateX(-100%)',
        })),
        transition('active => *', animate('400ms ease-in')),
        transition('moveLeft => fromRight', animate('1ms ease-in')),
        transition('fromRight => active', animate('400ms ease-in')),
        transition('moveRight => fromLeft', animate('1ms ease-in')),
        transition('fromLeft => active', animate('400ms ease-in'))
      ]
    )]
})
export class MainPageComponent implements OnInit {
  private bannerImages: BannerImage[];
  private activeImageNum: number;
  private nextImageNum: number;
〜省略〜
  ngOnInit() {
〜省略〜
    this.bannerImages = [
      {imagePath: "assets/img/banner01.jpg", transitionPath: '', bannerState: 'inactive'},
      {imagePath: "assets/img/sake.jpg", transitionPath: '', bannerState: 'inactive'}
    ];
    this.activeImageNum = -1;
    this.nextImageNum = 0;
    
    this.onBannerChanged();
  }
  @HostListener('document:keyup', ['$event'])
  onArrowKeyUp(event: any){
    if(event.keyCode === 39){ // RightArrow.
      this.nextImageNum = (this.activeImageNum < this.bannerImages.length - 1)? this.activeImageNum + 1 : 0;
      this.bannerImages[this.activeImageNum].bannerState = 'moveLeft';
      this.bannerImages[this.nextImageNum].bannerState = 'fromRight';
    }
    else if(event.keyCode === 37){ // LeftArrow
      this.nextImageNum = (this.activeImageNum > 0)? this.activeImageNum - 1: this.bannerImages.length - 1;
      this.bannerImages[this.activeImageNum].bannerState = 'moveRight';
      this.bannerImages[this.nextImageNum].bannerState = 'fromLeft';
    }
  }
  onBannerChanged(){
    if(this.activeImageNum === this.nextImageNum){
      return;
    }
    this.bannerImages[this.nextImageNum].bannerState = 'active';
    this.activeImageNum = this.nextImageNum;
  }
}

main-page.component.css

#top-banner-area{
    background-color: #000;
    width: 100%;
    min-width: 1200px;
    height: 600px;
    margin-left: auto;
    margin-right: auto;
    overflow: hidden;
    position: relative;
}
.top-banner{
    position: absolute;
    display: block;
    margin-left: auto;
    margin-right: auto;
    height: 600px;
}
  • class:top-banner-areaで「overflow: hidden;」を指定することで、
    アニメーション中に「translateX(100%)」を実行したときもブラウザの横方向のスライドバーが表示されるのを防ぎます。
  • 最初は横方向のスライドバーの表示を抑えるためclass:top-bannerのpositionを「fixed」にしていたのですが、
    画面に固定されてしまうため、positionは「absolute」に変更し、overflowを指定しました。
  • 「overflow: hidden;」は、一番外側の要素に設定しないと効果が無いようです。
  • スライドする画像を全て同じ位置に表示するためpositionを「absolute」にする必要があり、
    他の要素を並べるためにはheightを固定値で持つ必要があります(%指定だと正しく表示できない)。

背景画像をスライドさせる

画像の高さを固定にすると、画像の比率を保つためには幅も固定にする必要がでてきます。

この場合ここまで作成したようにimgタグを使用していると、
(少なくとも今の私の知識では)画面の幅が狭いデバイスで見たとき画像の左側だけが見える、という状態になります。

これを例えば画像の中央部分を表示するよう指定するためには、
imgタグで画像を表示せず、要素の背景画像として指定する、という方法があります。

main-page.component.html

< section id="top-banner-area" >
  < div *ngFor='let bannerImage of bannerImages' >
    < a href='bannerImage.transitionPath'>
      < div [style.background-image]="'url(' + bannerImage.imagePath + ')'" class="top-banner" [@bannerState]="bannerImage.bannerState" (@bannerState.done)="onBannerChanged()">
      < /div>
    < /a>
  < /div>
< /section>
〜省略〜
  • ngForを使って画像のURLを指定したかったため、HTMLでbackground-imageを設定しています
  • background-imageのURLを「"‘url(bannerImage.imagePath)’“」のようにしてしまうと、
    「bannerImage.imagePath」がngForで得られた内容ではなく、「bannerImage.imagePath」という文字列として扱われ、エラーになります。

main-page.component.ts

import { Component, OnInit, HostListener } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { Title } from '@angular/platform-browser';
import { BannerImage } from '../banner-image' 

@Component({
  selector: 'app-main-page',
  templateUrl: './main-page.component.html',
  styleUrls: ['./main-page.component.css'],
  animations: [
    trigger('bannerState',
      [
        state('active', style({
          zIndex: 1,
          display: 'block',
          transform: 'translateX(0%)',
          filter: 'brightness(100%)',
        })),
        state('moveLeft', style({
          zIndex: 0,
          transform: 'translateX(-100%)',
          filter: 'brightness(20%)',
          display: 'none'
        })),
        state('fromRight', style({
          transform: 'translateX(100%)',
        })),
        state('moveRight', style({
          zIndex: 0,
          transform: 'translateX(100%)',
          filter: 'brightness(20%)',
          display: 'none'
        })),
        state('fromLeft', style({
          transform: 'translateX(-100%)',
        })),
        state('start', style({
          transform: 'translateX(0%)',
          display: 'none'
        })),
        transition('active => *', animate('400ms ease-in')),
        transition('moveLeft => fromRight', animate('1ms ease-in')),
        transition('fromRight => active', animate('400ms ease-in')),
        transition('moveRight => fromLeft', animate('1ms ease-in')),
        transition('fromLeft => active', animate('400ms ease-in')),
        transition('* => start', animate('1ms ease-in')),
        transition('start => *', animate('1ms ease-in')),
      ]
    )]
})
export class MainPageComponent implements OnInit {
  private bannerImages: BannerImage[];
  private activeImageNum: number;
  private nextImageNum: number;
  
  constructor(private titleService: Title) {}
  ngOnInit() {
    this.titleService.setTitle('メインページ');

    this.bannerImages = [
      {imagePath: "assets/img/banner01.jpg", transitionPath: '', bannerState: 'start'},
      {imagePath: "assets/img/sake.jpg", transitionPath: '', bannerState: 'start'},
      {imagePath: 'assets/img/pic_1466247624676.jpg', transitionPath: '', bannerState: 'start'}
    ];
    this.activeImageNum = -1;
    this.nextImageNum = 0;
    
    this.onBannerChanged();
  }
  @HostListener('document:keyup', ['$event'])
  onArrowKeyUp(event: any){
    if(event.keyCode === 39){ // RightArrow.
      this.nextImageNum = (this.activeImageNum < this.bannerImages.length - 1)? this.activeImageNum + 1 : 0;
      this.bannerImages[this.activeImageNum].bannerState = 'moveLeft';
      this.bannerImages[this.nextImageNum].bannerState = 'fromRight';
    }
    else if(event.keyCode === 37){ // LeftArrow
      this.nextImageNum = (this.activeImageNum > 0)? this.activeImageNum - 1: this.bannerImages.length - 1;
      this.bannerImages[this.activeImageNum].bannerState = 'moveRight';
      this.bannerImages[this.nextImageNum].bannerState = 'fromLeft';
    }
  }
  onBannerChanged(){
    if(this.activeImageNum === this.nextImageNum){
      return;
    }
    this.bannerImages[this.nextImageNum].bannerState = 'active';
    this.activeImageNum = this.nextImageNum;
  }
}
  • 先程はうまく活躍できなかったz-indexですが、ここではactive状態の場合のみ1にして、他より前に表示されるようにしておくことで、
    移動時の画像などが表示されてしまうことによるチラツキを抑えることができます。

main-page.component.css

#top-banner-area{
    background-color: #000;
    width: 80%;
    height: 600px;
    display: block;
    margin-top: 90px;
    margin-left: auto;
    margin-right: auto;
    overflow: hidden;
    position: relative;
}
.top-banner{
    position: absolute;
    background-size: cover;
    background-position: center;
    display: block;
    width: 100%;
    height: 600px;
}

参照

Angularで引っかかったあれこれ

はじめに

相変わらず上の続きなのですが、Angular周りで細々したことに引っかかりまくったため、ここにまとめることとします。

静的なファイルの読み込み

たとえばヘッダーのロゴ画像など、静的にファイルを読み込みたい場合。

普通にHTMLで書くときと同じように、HTMLファイルと同じ階層に画像ファイルを置いても読み込むことはできません。

angular-cliを使っている場合、ファイルの置き場所は.angular-cli.jsonの中の、app > assets の中で指定されます。

.angular-cli.json

〜省略〜
  "apps": [
    {
〜省略〜
      "assets": [
        "assets",
        "favicon.ico"
      ],
      "index": "index.html",
      "main": "main.ts",
      "polyfills": "polyfills.ts",
      "test": "test.ts",
      "tsconfig": "tsconfig.app.json",
      "testTsconfig": "tsconfig.spec.json",
      "prefix": "app",
      "styles": [
        "styles.css"
      ],
      "scripts": [],
      "environmentSource": "environments/environment.ts",
      "environments": {
        "dev": "environments/environment.ts",
        "prod": "environments/environment.prod.ts"
      }
    }
  ],
  〜省略〜

デフォルトでは favicon.ico と src > assets が指定されており、
例えば src > assets > img > image01.jpg となるようフォルダと画像ファイルを追加した場合、
「< img src=“assets/img/image01.jpg”>」で表示できるようになります。

動的にページタイトルを設定する

ルーティングは以前やりましたが、ページが切り替わったらタイトルも変更したいところ。

ページタイトルを切り替えるには、対象のページのクラスにDIで「Title」クラスを注入し、
「setTitle()」を使って設定します。

main-page.component.ts

import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';

@Component({
  selector: 'app-main-page',
  templateUrl: './main-page.component.html',
  styleUrls: ['./main-page.component.css']
})
export class MainPageComponent implements OnInit {

  constructor(private titleService: Title) {}
  ngOnInit() {
    this.titleService.setTitle('メインページ');
  }

}

IE対応

気になってはいたのです。。。
IEでちゃんと開けるのかということに。

とはいえ、下記を見ればIE9以降であればサポートはされているようなので、
諸事情によりサポート切れのブラウザで表示する必要が。。。みたいな環境でなければ問題ないはずw

ところが、IE11で開くとなにやらエラーが発生してページが正しく動作しませんorz

IEに対応するためには、プロジェクト > src にあるpolyfills.tsの下記の部分をアンコメントする必要があります。

polyfills.ts

/** IE9, IE10 and IE11 requires all of the following polyfills. **/
// import 'core-js/es6/symbol';
// import 'core-js/es6/object';
// import 'core-js/es6/function';
// import 'core-js/es6/parse-int';
// import 'core-js/es6/parse-float';
// import 'core-js/es6/number';
// import 'core-js/es6/math';
// import 'core-js/es6/string';
// import 'core-js/es6/date';
// import 'core-js/es6/array';
// import 'core-js/es6/regexp';
// import 'core-js/es6/map';
// import 'core-js/es6/weak-map';
// import 'core-js/es6/set';

ちなみにここで登場するcore-jsは、ブラウザ間の差異を吸収してくれるもののようです。
(この辺もちゃんと調べたいと思いつつ何もできてないマンですはい)

デフォルトでこれがオンになってない理由は、余計なファイルを含めたくない、ということなのでしょうか。

Componentをどう分けるか

これまで1ページの中の要素でも、ひとかたまりごとにComponentに分けて作っていました。

ただ、この辺りのお話を見ていると、そうではなく複数の箇所から共通で使う部分だけを切り分ける、
とした方が良いように思えてきました。

今から思えば、たしかにハンズオンも、先にまとめて作ったあと切り分けていたなぁ、と。

これもバランスだとは思いますが、今後はComponentをもう少しまとめてみようと思います。

画像切り替え

以前は画像のパスをimgタグ渡して、動的に画像を切り替える、ということをしていましたが、
これには問題がありました。

最初にページを表示した時に、画像がキャッシュされていないので画像切り替え時に一瞬前の画像が見えたりしていたのです。

大量に画像を表示するなどの場合は別ですが、今回のように決まった数を表示する場合は、
画像を全てキャッシュしておいて、それを切り替えた方が良さそうです。

banner-image.ts

export interface BannerImage {
    imagePath: string;
    transitionPath: string;
    bannerState: string;
}

main-page.component.ts

import { Component, OnInit, HostListener } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { Title } from '@angular/platform-browser';
import { BannerImage } from '../banner-image' 

@Component({
  selector: 'app-main-page',
  templateUrl: './main-page.component.html',
  styleUrls: ['./main-page.component.css'],
  animations: [
    trigger('bannerState',
      [
        state('active', style({
          transform: 'scale(1.0)',
          filter: 'brightness(100%)',
          display: 'block'
        })),
        state('inactive', style({
          transform: 'scale(0.6)',
          filter: 'brightness(20%)',
          display: 'none'
        })),
        transition('active => inactive', animate('400ms ease-in')),
        transition('inactive => active', animate('10ms ease-in'))
      ]
    )]
})
export class MainPageComponent implements OnInit {
  private bannerImages: BannerImage[];
  private activeImageNum: number;
  private nextImageNum: number;
  
  constructor(private titleService: Title) {}
  ngOnInit() {
    this.titleService.setTitle('メインページ');

    this.bannerImages = [
      {imagePath: "assets/img/banner01.jpg", transitionPath: '', bannerState: 'inactive'},
      {imagePath: "assets/img/banner01.jpg", transitionPath: '', bannerState: 'inactive'},
      {imagePath: "assets/img/banner01.jpg", transitionPath: '', bannerState: 'inactive'},
    ];
    this.activeImageNum = - 1;
    this.nextImageNum = 0;
    this.onBannerChanged();
  }
  @HostListener('document:keyup', ['$event'])
  onArrowKeyUp(event: any){
    if(event.keyCode === 39){ // RightArrow.
      this.nextImageNum = (this.activeImageNum < this.bannerImages.length - 1)? this.activeImageNum + 1 : 0;
      this.bannerImages[this.activeImageNum].bannerState = 'inactive';
    }
    else if(event.keyCode === 37){ // LeftArrow
      this.nextImageNum = (this.activeImageNum > 0)? this.activeImageNum - 1: this.bannerImages.length - 1;
      this.bannerImages[this.activeImageNum].bannerState = 'inactive';
    }
  }
  onBannerChanged(){
    if(this.activeImageNum === this.nextImageNum){
      return;
    }
    this.bannerImages[this.nextImageNum].bannerState = 'active';
    this.activeImageNum = this.nextImageNum;
  }
}

main-page.component.html

< section id="top-banner-area" >
  < div *ngFor='let bannerImage of bannerImages' >
    < a href='bannerImage.transitionPath'>
      < img [src]="bannerImage.imagePath" class="top-banner" [@bannerState]="bannerImage.bannerState" (@bannerState.done)="onBannerChanged()" />
    < /a>
  < /div>
< /section>
< app-top-news>< /app-top-news>

参照

静的ファイルの読み込み

ページタイトル

ブラウザ対応

Component分割

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

はじめに

今回は、Spring boot側のルーティングと、特定の要素をマウスオーバーした時に、メニューを表示する方法について調べたことをまとめます。

ルーティング

以前Angular側ではルーティングを行ったわけですが、トップページから遷移したときは問題なくても、
「トップドメイン/contact」 など、トップから遷移したあとのURLに直接アクセスしようとするとエラーになってしまいます。

これを回避するためには、Spring boot側でトップドメイン以下のページのURLにアクセスしたときもAngularのindex.htmlを返すようにする必要があります。

MainController.java

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.InternalResourceView;

@Controller
public class MainController {
    @RequestMapping(value = {"/", "/menulist", "/contact"})
    public View redirect() {
        return new InternalResourceView("/index.html");
    }
}

htmlを表示するために使っているInternalResourceViewですが、下記などを見ると、JSPを表示する場合に使用されるもののようです。

これを使うと、ng buildでビルドしたファイルを全部 src/main/resources/static 以下に入れればOKなのですが、
これが正しい方法なのか、というところはちょっと不安が残っています。

アイテムがマウスオーバーされた時にメニューを表示する

ボタンをマウスオーバーした時に、メニューを表示してみます。

具体的には https://www.jetbrains.com/ の上部メニューの、「IDEs」「.NET & VISUAL STUDIO」などをマウスオーバーした時に表示されるメニュー(ウィンドウ)を真似したいということですw

ngIfなどを使ったり試行錯誤していたのですが、ぐぐってみるとCSSでシンプルに解決する方法が見つかりましたorz

global-header.component.html

< header id="global-header">
〜省略〜
  < div id="header-button-menulist-area">
    < button routerLink="./menulist" class="header-button">Menu< /button>
    < app-global-header-menulist-window id="global-header-menulist-parent">< /app-global-header-menulist-window>
  < /div>
〜省略〜
< /header>

global-header.component.css

#global-header{
    background-color: #2980B9;
    width: 100%;
    min-width: 1200px;
    height: 90px;
    position: fixed;
    top: 0;
    z-index: 1;
}
〜省略〜
.header-button{
    background-color: #2980B9;
    border-style: none;
    color: #FFFFFF;
    font-size: 18px;
    width: 160px;
    height: 60px;
}
.header-button:hover{
    background-color: #FFF;
    color: #000;
    font-weight: bold;
}
〜省略〜
#header-button-menulist-area{
    width: 160px;
    height: 60px;
    float: left;
    margin: 30px 20px 0px 0px;
}
〜省略〜

#header-button-menulist-area #global-header-menulist-parent{
    visibility: hidden;
}
#header-button-menulist-area:hover #global-header-menulist-parent{
    visibility: visible;   
}

global-header-menulist-window.component.html

< div id="global-header-menulist-window-frame">
  < button class="menulist-window-button">
    < div>Menu 1< /div>
    < div>1つめのメニューはカレーうどんについて。
いや〜、うまいっすね。
みなさんも白い服を着ているときはカレーうどんに決まり! < /div> < /button> < button class="menulist-window-button"> < div>Menu 2< /div> < div>2つめのメニューはかけうどん。
いいっすよ、かけうどん。
かけっこしながらかけうどん! < /div> < /button> < button class="menulist-window-button"> < div>Menu 3< /div> < div>3つめのメニューは月見うどん。
月が、きれいですね。 < /div> < /button> < button class="menulist-window-button"> < div>Menu 4< /div> < div>4つめのメニューはきつねうどん
場所によってはきつねそばもあるらしいです。 < /div> < /button> < /div>

global-header-menulist-window.component.css

#global-header-menulist-window-frame{
    width: 100%;
    height:300px;
    background-color: #FFF;
    position: absolute;
    left: 0;
    padding: 40px;
}
.menulist-window-button{
    border-style: none;
    width: 400px;
    height: 120px;
    margin-left: 60px;
    margin: 10px;
    float: left;
}
  • メニュー自体は適当にボタンを並べているだけです。
  • global-header-menulist-window.component.htmlが表示されている間は、
    マウスオーバーの領域が div id:header-button-menulist-area に加えて global-header-menulist-window.component.html に広がるため、メニュー内のボタンも選択可能、というわけですね。

おわりに

次は入力フォーム辺りを触ってみたいところ。

参照

KANJAVA PARTYに参加してきました

はじめに

6/24に行われたKANJAVA PARTYに参加してきました。

今回は関ジャバとしては初となる複数トラック制の、
参加人数も100名超と、普段より少し規模の大きい勉強会でした。

関ジャバとJava

www.slideshare.net

まずは関ジャバについてのお話から。

簡単な歴史や、関ジャバこと関西ジャバエンジニアの会は、あくまで関西中心のジャバエンジニアのための会であって、
必ずしもジャバだけを勉強する会ではない(のでフロントエンドなどの勉強会も行う)、といったお話をされていました。

私も過去に何度も参加させてもらいながら、歴史については知らずにいたためなるほど〜、と感心。

一点、もとの題名であった「すべらない話」を期待していたのは内緒ですw
(念の為書いておくと、滑ってはいませんでしたw)

Spring Securityにできること・できないこと

qiita.com

IPAが公開している安全なWebサイトの作り方で指摘されている脆弱性の内、
Spring Securityが対策してくれることとそうでないことを、脆弱性対策しているサイトとそうでないサイトのサンプルを用意して、比較しながら説明されていました。

Spring Security対策してくれるところ、そうでないところでも保険的なフォローをしてくれるところといった内容を、
デモを交えて説明してくれたのがとてもわかりやすかったです。

あと実践で使用するにあたっては、そのフレームワークや技術がどのように動いているのか、
ということをしっかりと理解できていなければいけない、と改めて感じました。

どうも自分は表面的なところだけで次に進んでしまいがちなので、
セキュリティはもちろん、それ以外も深く追っていかねばと思いました。

安全なWebサイトの作り方も早いうちに読んでおきたいと思います。

JUnit5の味見

speakerdeck.com

ここも相当迷いましたがJUit5に。

このセッションではJUnit5のユーザーガイドをベースに、
かみくだいた内容から気になるところをピックアップする、という方法で進められていました。

ようやくJUnit4を触りだした私としては、話についていけたとは言い難いのですが、
話されていない内容も含めてガッツリ資料にまとめてくれていると、あとから追えるので大変助かります^^;
(翌日次のバージョンが出る、という悲しいオチもありましたが。。。)

Java8に対応したり、便利になっているということはわかった(気がする)ので、
特にプライベートでは積極的にトライしていきたいと思います。

ストリーム処理ことはじめ 〜Akka StreamとRxJava

speakerdeck.com

Java Stream APIの基礎的な説明から、forではなくストリーム処理が何故良いのか、というお話へ。
さらに、Stream APIの抱える課題点と、Akka Streamがどうそれを解決するか、といった流れで説明されていました。

mapなどの処理の中身をStreamから分離する、というのは処理が複雑になったり、
複数ヶ所で同じ処理を行う時に力を発揮しそうですし、これはC#Linqを使うときなんかにも使えるのでは、と感じました。

処理が進んで値が流れていく様子がスライドで表現されていたのも良かったと思いました。

ちなみにAkka Streamを調べるとScalaだけでなくJavaもあるのですが、
Java版も同じように使えるのでしょうか。

その辺りも試してみたいところです。

Kafkaを使ってイベントを中心にしたアプリケーションを作ってみる。Spring Cloudにのせて

bufferings.hatenablog.com

スライドがなく、マインドマップ?をベースにやってみたこと、
困ったことや考えなどを話されていて、ある意味異色のセッションでしたw

Kafkaについての前知識が全くない状態で聴いていたため、
本当にぼんやりとこんな感じかなぁ。。。?と思っているくらいですが、とにかく楽しいセッションでした。

まぁKafkaについても近い内触ってみる、ということで。

あと今回に限らないのですが、bufferingsさんの資料やデモに、
娘さんが描かれたという絵が載せられていて、うらやましいです。

うちの子がもう少し大きくなって、絵を掻けるようになったら真似したいな〜と思っているのですが、いつになるやらw

関西Java女子部ショートセッション大会

関西Java女子部の方々によるショートセッション × 3 + 1

まずは関西Java女子部の活動の説明から、システムに対して理解が薄い顧客に対して自分たちの仕事をどう理解してもらうか、
リファクタリングのススメ、ハンズオン(講師側)のススメといった内容でした(順番違うかも)。

関西Java女子部はTwitterなどで情報は流れてくるものの、
一緒に参加してくれる女性の知り合いはおらんしな〜と思っていたので、活動内容が知れてよかったです。

自分たちの仕事をどう理解してもらうか、というところは、他人事でなく自分も取り組まなきゃいけない内容ですが、
相手も勉強会に賛同してくれた、という話は少しうらやましく感じました。

リファクタリングについては、とにかくネーミングの面白さがwww

speakerdeck.com

真面目なところとしては、私もリファクタリングしようとして、
歯止めが効かなくなって延々と作業し続けてしまうことがあるので、
うまくいかなかったら素直に元に戻す、というのは気をつけていきたいと思います。

ハンズオンについては、講師側を務めたことがないのですが、
今後自分が参加するだけでなく、サポートする側(ハンズオンだけでなく)にもまわってみようかなぁ、と思います。

自分も学ばなきゃいけないことがありすぎるので、バランスが難しそうではありますが(; ・`ω・´)

はてなブックマークAndroidアプリへのKotlinの導入について

speakerdeck.com

以前関モバ(だったと思います)で、Kotlinを使いたい、という話を聴いていたので、
最初このセッションのタイトルを見た時に「おぉ!ついに!」と思っておりました。

というところで前半はKotlin導入への経緯と、既存のJavaコードをどのようにKotlinに置き換えていくか、というお話が、 後半はKotlinの代表的な言語機能をピックアップして説明されていました。

前半については、やっぱり情熱って大事だなぁと感じました。
少々リスクや課題があったとしても、やりたい欲と技術があればまぁなんとかなりますよね。

後半については、プロパティがいまひとつよくわかっていなかったので、
そこに触れてもらえたのが助かりました。

Kansai.kt #3も行われるというし、またKotlinで何か作ってみるかなぁ。

ビアバッシュ

あっという間にセッションもおわり、ビアバッシュに。

今回も懲りずにLTに挑戦しました。

speakerdeck.com

本来の予定では、もう少しJavaよりにするつもりだったのですが、
進捗の具合からAngularメインのお話となりました。

時間は問題なかったのですが、特にビアバッシュに行うLTは、
もう少し聴いている人を楽しませる、ということにも気を遣ってみようと思いました。

まぁビールを出すとか、おっさんギャグを言うとかじゃなくて良いのですがw

あとはせっかくなので、もう少しいろんな人に話しかけるようにしたいところ。

あと岡山の勉強会参加したい。

おわりに

今回に限りませんが、やっぱり複数トラックでセッションがあると、
どれを聴くか迷ったり、自分が聴いている方も良いのだけどあっちも聴きたかった!となりますね。

スタッフの皆様、参加された皆様、会場を提供していただいた日本マイクロソフト様、ありがとうございました!

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

はじめに

引き続きWebページを作成すべくあれこれ試しています。

今回は、Angularでページを作っていく上で必要になった知識?も合わせてまとめることにします。

node_moduleをインストールし直す

1台のマシンだけで開発している場合は問題がないのですが、
他のマシンにプロジェクトを移したい場合、node_moduleは持っていってもエラーになってしまいます。
(.gitignoreに含まれてもいます)

そのため、プロジェクトを移したあとで「npm install」を行い、node_moduleをインストールし直す必要があります。

インストールされる内容は、プロジェクト直下にあるpackage.jsonのものです。

なお、この中のアプリのバージョン(下記参照)を更新したい場合は、手動で書き直すのでしょうか…?

package.json

{
  "name": "anime-crud-sample",
  "version": "0.0.0",
  "license": "MIT",
〜省略〜

ビルドデータの出力先の変更

以前書きましたが、
Angularで作成したページを実際サーバー上で表示するときは、「ng build」でビルドしたデータを使うことになります。

通常ではプロジェクト直下に作成される「dist」というフォルダの中にビルドしたデータが出力されるのですが、
これを任意の場所に出力するためには、プロジェクト直下にある.angular-cli.jsonを変更します。

.angular-cli.json

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "project": {
    "name": "anime-crud-sample"
  },
  "apps": [
    {
      "root": "src",
      "outDir": "ここを変更する"
〜省略〜

CSS Grid Layoutを使う

以前は下記を再現するには、tableを使用して結合などを利用していました。

f:id:mslGt:20170624021504j:plain

しかし、CSS Grid Layoutを使うことでより簡単に再現できるようになります。

さっそく試してみます。

元のHTML

< div class='top-news-item-area'>
    < div class='top-news-item-date'>2017年6月20日< /div>
    < div class='top-news-item-title'>ニュースだよ< /div>
    < div class='top-news-item-article'>中身です< /div>
< /div>

top-news-item-area(クラスを持つdiv)の中に、3つのdivが含まれています。

このHTMLに対して設定していきます。

top-news-item-area

まず、このLayoutが何行何列なのか、またそれぞれの高さ・幅を、
3つのdivの親であるtop-news-item-areaに設定します。

.top-news-item-area{
    display: grid;                  /* Grid layoutを有効にする */
    grid-template-rows: 50px 100px; /* 左から順に1行目、2行目の高さを指定 */
    grid-template-columns: 20% 1fr; /* 左から順に1列目、2列目の幅を指定 */
}
  • 今回は2行2列のため、grid-template-rowsとgrid-template-columnsにはそれぞれ2つずつ値を設定しています。
    行・列数を増やす場合は更に値を追加していきます。
  • grid-template-columnsで指定している「1fr」は、1列目で指定した幅の残りを2列目に全て割り当てるために使用しています。

top-news-item-date・top-news-item-title・top-news-item-article

Layoutの行・列数と高さ・幅が決まれば、あとは子となるdivの配置を決めるだけです。

.top-news-item-date{
    grid-row: 1 / 3;                /* 1行目から3行目まで(つまり2行全て)を指定する */
    grid-column: 1 / 2;             /* 1列目から2列目まで(つまり1列目)を指定する */
    border: 1px solid #000;
}
.top-news-item-title{
    grid-row: 1;                    /* 1行目を指定する */
    grid-column: 2;                 /* 2列目を指定する */
    border: 1px solid #000;
}
.top-news-item-article{
    grid-row: 2 / span 1;           /* 2行目から1行分(つまり2行目のみ)を指定する */
    grid-column: 2;                 /* 2列目を指定する */
    border: 1px solid #000;
}
  • top-news-item-titleのように1行または1列だけを指定する場合は、「/」以降の数字を省略できます。
  • 「/」以降を省略した場合、top-news-item-articleのgrid-rowと同じように、指定の行(または列)から1行(列)分が設定されます。

結果

ここまでのものを表示すると、下記のようになります。

f:id:mslGt:20170624021448j:plain

あとは角丸にしたり、marginなどで幅を調整したりすればOKです。

課題

ChromeFirefoxだけであれば問題なく表示できるのですが、
少なくともAngularでCSS Grid Layoutを使うと、警告出る場合があります。

IEでは、「grid-row: 1 / 3;」のように「grid-row(またはgrid-column)」で「/」以降の数字を指定できないためです。

手元にIEがないため未確認ですが、下記のようにすればIEにも対応できるようです。

.top-news-item-area{
    display: grid;
    display: -ms-grid;
    grid-template-rows: 50px 100px;
    grid-template-columns: 20% 1fr;
    -ms-grid-rows: 50px 100px;
    -ms-grid-columns: 150px 1fr;
}
.top-news-item-date{
    -ms-grid-row: 1;
    -ms-grid-row-span: 2;
    -ms-grid-column: 1;
    -ms-grid-column-span: 1;
    grid-row: 1 / 3;
    grid-column: 1 / 2;
    border: 1px solid #000;
}
.top-news-item-title{
    -ms-grid-row: 1;
    -ms-grid-column: 2;
    grid-row: 1;
    grid-column: 2;
    border: 1px solid #000;
}
.top-news-item-article{
    -ms-grid-row: 2;
    -ms-grid-column: 2;
    grid-row: 2;
    grid-column: 2;
    border: 1px solid #000;   
}

ただし警告は消えません。。。

まぁこれに関する警告をでないようにすれば(たぶんtslint.json)良いのでしょうが、
ちょっと気持ち悪いですね。。。

RxJSでJSONデータ取得

さて、CSS Grid Layoutで作ったフォームに、データを入れてみます。

Spring bootでGETリクエストをした時にJSONを返すようにしておき、
RxJSを使ってそれを受け取ってHTMLに渡す、という処理を行います。

Spring boot

今回は一旦GETリクエストがあった場合に、JSONデータを固定値で生成して返す、という処理をすることにします。

ということで、「spring-boot-starter-web」を含めてSpring bootのプロジェクトを作成し、下記のクラスを追加します。

News.java

public class News {
    public int id;
    public String createdDate;
    public String title;
    public String article;
}
  • JSONデータを返すための値を保持するクラスです。

CrudSampleController.java

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;

@RestController
public class CrudSampleController {
    @GetMapping("/topnewslist")
    public List newsList(){
        List newsItems = new ArrayList<>();
        News newsItem1 = new News();
        newsItem1.id = 0;
        newsItem1.createdDate = "2017.06.22";
        newsItem1.title = "うどん一杯目";
        newsItem1.article = "きつねうどん";
        newsItems.add(newsItem1);

        News newsItem2 = new News();
        newsItem2.id = 1;
        newsItem2.createdDate = "2017.06.23";
        newsItem2.title = "うどん二杯目";
        newsItem2.article = "味噌煮込みうどん";
        newsItems.add(newsItem2);

        return newsItems;
    }
}
  • Controllerクラスです。
  • localhost:8080/topnewslistにGETリクエストがあった場合にnewsList()が呼び出され、
    Newsクラスのリストを返します。
    (リストやクラスを戻り値にすると、自動でJSONとして受け取ることができるの便利ですね)

RxJS

フロント側に戻ります。

AngularのプロジェクトにはデフォルトでRxJSが含まれているため、
インストールなどは特にしなくてもそのまま使用できます。

GETリクエストを送るためには、app.module.tsにHTTPModuleを追加する必要があります。

app.module.ts

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

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';
import { MenulistPageComponent } from './menulist-page/menulist-page.component';

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

あとはHTTPとRxJSを使ってJSONデータを取得します。

top-news.ts

export interface TopNews {
    id: number;
    createdDate: string;
    title: string;
    article: string;
}
  • サーバー側から受け取ったJSONデータを格納するためのインターフェースです。

top-news.component.ts

import { Component, OnInit } from '@angular/core';
import { TopNews } from '../top-news';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';

@Component({
  selector: 'app-top-news',
  templateUrl: './top-news.component.html',
  styleUrls: ['./top-news.component.css']
})
export class TopNewsComponent implements OnInit {
  newsList: TopNews[];
  
  constructor(private http_: Http) {
    http_.get("/topnewslist")
        .map(response => response.json()) /* response.json()の「()」を忘れないこと */
        .subscribe(gotNews => this.newsList = gotNews);
  }
  ngOnInit() {
  }
}
  • 今回は初回ロード時のみデータを読み込むため、constructorで、DIでインスタンスを受け取ったらそのまま処理を行っています。
  • constructor以外でも「this.http_」でアクセスできるため(なんだか不思議な感じがしますが)、任意のタイミングで実行することも可能です。
  • 「map(response => response.json())」で、response.json()の「()」がついていなくてもエラーにはならないのですが、
    戻り値が変わってしまい、subscribeの時に値が取り出せなくなってしまいます。

注意点

上記コードをng serveを使い、ビルドしない状態で実行するとエラーが発生します。

それは、同一ドメイン以外のURLにアクセスしに行く場合に必要なXMLHttpRequestの設定が抜けており、
ブラウザによってアクセスを禁止されるためです。

ただ今回は、最終的には同じドメインでやり取りをすることになるため、
ng buildでビルドを行い、Spring bootのプロジェクトに組み込んだ状態で確認を行うことにします。

おわりに

今回はほとんど触れられませんでしたが、RxJSももう少し突っ込んで調べてみたいと思います。

参照

Angular

CSS Grid Layout

DI