2020年10月29日木曜日

Angular・【Simple Modal Module】モーダルを手軽に実装する。


こんにちは、オフィス狛 技術部のpinoです。

今回は、AngularのSimple Modal Moduleの使い方についてご紹介したいと思います。

Simple Modal Moduleは、とても簡単にモーダルが実装できるプラグインです。
bootstrapなどでモーダルを作成されていた方も、この機会にぜひ目を通していただきたい内容です。

作れるもの

Simple Modal Moduleで実装できるモーダルは、以下のデモで試せます。
https://ngx-simple-modal-demo.stackblitz.io/

準備

まずは、以下のコマンドでインストールします。
npm install ngx-simple-modal

基本となるCSSはすでに用意されていますので、angular.jsonに以下を定義します。
angular.json
"styles": [
   "styles.css",
   "../node_modules/ngx-simple-modal/styles/simple-modal.css"
],
これで、モーダル表示時のスタイルやアニメーションはバッチリです。

HTMLは、上記のCSSに定義されているスタイルを使用するような形でテンプレートがある程度決まっていますので、以下の形から派生させていくことになります。
HTML
<div class="modal-content">
    <div class="modal-header">
      <!-- モーダルのタイトル -->
    </div>
    <div class="modal-body">
      <!-- モーダルの本文 -->
    </div>
    <div class="modal-footer">
      <!--
        フッターはbutton要素を置く
        ex.: <button (click)="close()">Cancel</button>
      -->
    </div>
</div>

1. AppModuleにImport

Simple Modal Moduleをimportします。
app.module.ts
import { NgModule} from '@angular/core';
import { CommonModule } from "@angular/common";
import { BrowserModule } from '@angular/platform-browser';
import { SimpleModalModule } from 'ngx-simple-modal'; // 追加🍨
import { AppComponent } from './app.component';
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    CommonModule,
    BrowserModule,
    SimpleModalModule // 追加🍨
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

また、冒頭のデモでは、モーダル以外をクリックしたり、escキーを押すことでモーダルを閉じたりできたかと思います。
そのようにオプションを変更したい場合は、以下のようにも定義できます。

imports: [
    ...
    SimpleModalModule.forRoot({container: 'modal-container'}, {...defaultSimpleModalOptions, ...{
      closeOnEscape: true,
      closeOnClickOutside: true,
      wrapperDefaultClasses: 'o-modal o-modal--fade',
      wrapperClass: 'o-modal--fade-in',
      animationDuration: 300,
      autoFocus: true
    }})
  ]

・項目名 ... 説明(型, デフォルト値)
・closeOnEscape ... escapeキー押下でモーダルを閉じる(boolean, false)
・closeOnClickOutside ... モーダル以外をクリックしてモーダルを閉じる(boolean, false)
・bodyClass ... モーダルが開いている間、bodyタグに付与するクラス(string, 'modal-open')
・wrapperDefaultClasses ... <simple-modal-wrapper>に付与するクラス(string, 'modal fade-anim')
・wrapperClass ... モーダルが開いている間、<simple-modal-wrapper>に付与するクラス(string, 'in')
・animationDuration ... モーダルの開閉にかける時間(number, 300)
・autoFocus ... モーダルを閉じた後に、フォーカスをモーダルが開く直前の状態に戻す(boolean, false)

* <simple-modal-wrapper>に関して
デフォルトのラッパーとして、モーダルはこのタグの配下に配置されます。

* wrapperDefaultClasses、wrapperClassに関して
前述で紹介した基本のCSSではデフォルト値が使用される前提でスタイリングされているので、通常はあまり変更する必要がないと思います。
以降で紹介するaddModal(), close()といったモーダルの仕組みだけ利用し、スタイリングは1から行いたい場合などに活用できそうです。

2. モーダルコンポーネントを作成

モーダルのコンポーネントを作成していきます。
HTML
<div class="modal-content">
    <div class="modal-header">
      <h4>{{title || 'Confirm'}}</h4>
    </div>
    <div class="modal-body">
      <p>{{message || 'Are you sure?'}}</p>
    </div>
    <div class="modal-footer">
      <button type="button" class="btn btn-outline-danger" (click)="close()">Cancel</button>
      <button type="button" class="btn btn-primary" (click)="confirm()">OK</button>
    </div>
</div>
TS
import { Component } from '@angular/core';
import { SimpleModalComponent } from "ngx-simple-modal";
export interface ConfirmModel {
  title:string;
  message:string;
}

@Component({
    selector: 'confirm',
    template: './confirm.component.html'
})
export class ConfirmComponent extends SimpleModalComponent<ConfirmModel, boolean> implements ConfirmModel {
  title: string;
  message: string;

  constructor() {
    super();
  }

  confirm() {
    this.result = true; //🍀
    this.close();
  }
}

ポイントは🍀マークのthis.result = true;です。
理由は後述します。

また、close()でモーダルを閉じることができます。こちらはモーダルコンポーネント自らで呼び出すことができるため、TS上だけでなくHTML上でclickイベントに直接割り当てるのもOKです。

3. 作成したコンポーネントをモジュールに登録

App.module.tsに作成したコンポーネントをimportします。

import { NgModule } from '@angular/core';
import { CommonModule } from "@angular/common";
import { BrowserModule } from '@angular/platform-browser';
import { SimpleModalModule } from 'ngx-simple-modal';
import { ConfirmComponent } from './confirm.component'; // 追加🍨
import { AppComponent } from './app.component';
@NgModule({
  declarations: [
    AppComponent,
    ConfirmComponent  // 追加🍨
  ],
  imports: [
    CommonModule,
    BrowserModule,
    SimpleModalModule
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

* 公式ではentryComponentsに定義するようになっていますが、ver.9から非推奨となっているようです。
弊社ではdeclarationsに定義するだけで問題なく使用しています。
参考: https://dev.to/angular-jp/entrycomponents-53mo

4. 実際に使ってみる

モーダルを表示させたいコンポーネントに組み込みます。
例なのでapp.component.tsに組み込んでみます。

import { Component } from '@angular/core';
import { ConfirmComponent } from './confirm.component';
import { SimpleModalService } from "ngx-simple-modal";

@Component({
  selector: 'app',
  template: './app.component.ts'
})
export class AppComponent {
    constructor(private simpleModalService:SimpleModalService) {}

    showConfirm() {
      this.simpleModalService.addModal(ConfirmComponent, { // 🍀①
        title: 'Confirm title',
        message: 'Confirm message'
      })
      .subscribe(isConfirmed =>{ // 🍀②
        if(isConfirmed) {
          alert('accepted');
        } else {
          alert('declined');
        }
      });
    }
}

🍀①のaddModal()でモーダルを表示させることができます。
モーダルを使用する側でモーダルを閉じたい場合は、removeModal()removeAll()が使用できます。

また、ポイントは🍀②部分です。
「2. モーダルコンポーネントを作成」 でresultにセットした値はここで取得することができます。
resultにはbool値以外も使用することができますので、
Submitされたか」「"はい"が押されたか」「処理に成功したか」など
様々なフラグをカスタマイズするのもいいですね。

注意点

これは実際に私がハマったことなのですが、いくらsubscribeしているからといってresultの変更を常々検知できるわけではありません。
ngOnDestroy時にObserverbleとしてresultが返される仕組みだそうなので、resultを取りたい場合はモーダルを閉じる必要があります。

今回記事を書くにあたり公式を確認したところ、その旨記載がありました。
ハマったとき、公式をちゃんと確認する癖をつけなければ...!


補足

公式にはBootstrapと一緒に使う方法も載っていますので、「Bootstrapと完全に縁を切るのは難しいけど、モーダルをコンポーネントとして管理したいんだよなぁ...。」といった場合にもおすすめだと思いました。
https://www.npmjs.com/package/ngx-simple-modal#what-if-i-want-to-use-bootstrap-3-or-4


今回は公式の内容を参考に、Simple Modal Moduleについて紹介しました。
参考になればうれしいです。

Node.jsを使用して、S3上のPDFをマージする方法。


オフィス狛 技術部のmmm(むー)です。

先日、業務でPDFのマージ作業が必要でしたので、その備忘録を残します。
マージ作業には、pdf-libというライブラリを使用しました。

pdf-libとは
pdf-libとは、PDFを作成・マージするためのnpmパッケージです。
今回はNode.jsで作業を行いましたが、ブラウザ側のJavaScriptでも使用することができます。また、PDFのマージだけではなく、実際にコードベースでPDFの作成もできるようです。
参考:https://www.npmjs.com/package/pdf-lib (公式)

【前提条件】
Nodeとnpmがインストールされていること。

1. pdf-libのインストール

使用したいプロジェクト配下に移動し、下記を実行します。
$ cd ~/<プロジェクトパス>
$ npm install --save pdf-lib

package.json に下記が追加されました。
// package.json

"dependencies": {
    "pdf-lib": "^1.11.1",
  }

2. S3からファイルを読み込む

< ... > 内は可変ですので、ご自身の環境の値を設定してください。
   const aws = require('aws-sdk');

// S3にアクセスするための情報
const s3Client = new aws.S3({
  accessKeyId: <accessKeyId>,
  secretAccessKey: <secretAccessKey>,
  region: <region>,
});

// バケットの情報 
// (バケット内にディレクトリを作成していない場合は、Keyにファイル名のみ指定してください)
const params = {
      Bucket: <bucketName>,
      Key: <s3DirectoryName>/<file.pdf>,
    };

// S3にある対象ファイル情報取得
const dataInS3 = await s3Client.getObject(params).promise();

// bufferデータを取得
const bufferData = dataInS3.Body;

// 💡 S3ではなく実行環境上にファイルがある場合は、下記のみでファイルの情報を取得できます
const fs = require('fs');
const file1 = await PDFDocument.load(fs.readFileSync('./file1.pdf'));


3. 読み込んだPDFをマージする

本来であれば複数のPDFをS3から読み込むと思いますが、今回は同じPDFファイルでマージします。
const fs = require('fs');
const { PDFDocument } = require('pdf-lib');

// マージ用の空PDF空を作成
const mergedPdf = await PDFDocument.create();

// マージ対象のファイル情報取得
const targetPdf = await PDFDocument.load(bufferData);

// マージ対象のファイルの全てのページ情報取得
const targetPdfPages = await mergedPdf.copyPages(
  targetPdf,
  targetPdf.getPageIndices(),
);

// 最初に作成した空のマージ用PDFに、マージ対象の全てのページを追加
for (const page of targetPdfPages) {
  mergedPdf.addPage(page);
}

// 💡 1ページだけ取得したい場合はこちら (下記例は、最初の1ページのみ取得)
const [targetPdfOnePage] = await mergedPdf.copyPages(
  targetPdf,
  [0],
);

// マージ用PDFに、1ページ更に追加
mergedPdf.addPage(targetPdfOnePage);

4. マージしたファイルを書き出す

ディレクトリを指定してファイルを書き出す場合、ディレクトリが存在しないとエラーになるので注意してください。
// PDFファイル書き出し
fs.writeFileSync('./merge.pdf', await mergedPdf.save());

5. マージしたファイルをレスポンスする

下記コードは、Expressを使用しています。
const pdfBytes = await mergedPdf.save();
const pdfbuffer = Buffer.from(pdfBytes);

res
    .attachment('merge.pdf')
    .set('Content-Type', 'application/pdf')
    .set('isBase64Encoded', true)
    .send(pdfBuffer)
    .toString('base64');

以上となります。
PDFをマージする必要がある場合、参考にしてみてください🍭

, ,

2020年10月27日火曜日

AWS Lambdaの関数コードでzipファイルをアップロードする。(DeprecationWarningの対応)

オフィス狛 技術部のJoeです。

弊社のあるプロジェクトで、AWSのLambda関数でPOSTリクエストする機能があるのですが、
ある日、ログを確認すると以下のようなメッセージが出力されていました。

DeprecationWarning: You are using the post() function from 'botocore.vendored.requests'.  This dependency was removed from Botocore and will be removed from Lambda after 2021/01/30. https://aws.amazon.com/blogs/developer/removing-the-vendored-version-of-requests-from-botocore/. Install the requests package, 'import requests' directly, and use the requests.post() function instead.

【直訳】
非推奨警告:「botocore.vendored.requests」のpost()関数を使用しています。 この依存関係はBotocoreから削除され、2021/01/30以降にLambdaから削除されます。 https://aws.amazon.com/blogs/developer/removing-the-vendored-version-of-requests-from-botocore/。 リクエストパッケージをインストールし、「リクエストをインポート」して、代わりにrequests.post()関数を使用します。

このメッセージの対応として、Lambda関数のコードでzipファイルをアップロードして、
「botocore.vendored.requests」からPythonのRequestsモジュールに変更した手順をご紹介します。
(Windowsでの手順になります)


1.Pythonをインストール

公式サイトから、Pythonをダウンロードして、インストールしてください。
※今回はVersion 3.9.0 を使用しました

環境変数(Path)の追加をお忘れなく。
[インストールフォルダ]\Python39
[インストールフォルダ]\Python39\Scripts

2.pipでRequestsを指定のフォルダにインストール

適当な場所に作業用のフォルダを作成し、
コマンドプロンプトを起動し、作成したフォルダに移動してください。

以下のコマンドで、インストールします。
pip install requests -t .

成功すると、作業用フォルダの中に、以下のフォルダが作成されます。
bin
certifi
certifi-2020.6.20.dist-info
chardet
chardet-3.0.4.dist-info
idna
idna-2.10.dist-info
requests
requests-2.24.0.dist-info
urllib3
urllib3-1.25.11.dist-info

3.zipファイルの作成

Lambda関数にアップロードするzipファイルを作成します。
上記2のフォルダをzipファイルにするだけなのですが、注意点があります。

①「.dist-info」のフォルダは不要なので削除します。

②今回のように、zipファイルをアップロードするLambda関数に、既に関数コードを記述している場合、
アップロードしたタイミングで、消えてしまいます。

既存のコードを使用する場合、「2」で作成したの作業フォルダの中に、
新規で「lambda_function.py」ファイルを作成して、
既存の関数コードをコピーしてください。

上記①、②の対応をした場合、zipファイルの中身は以下のようになります。
bin
certifi
chardet
idna
requests
urllib3
lambda_function.py

4.zipファイルのアップロード

Lambdaの関数コードのアクションで、「.zip ファイルをアップロード」を選択して、
「3」で作成したzipファイルをアップロードします。

Environmentにzipしたファイルとフォルダが表示されていると思いますので、
これでPythonのRequestsモジュールが使用できます。

最後にimportを修正して、「Deploy」すれば完了です。
修正前:from botocore.vendored import requests
修正後:import requests



上記の対応で、ログから警告メッセージが消えて、問題なく動作しました。


,

2020年10月23日金曜日

Angular・リアクティブフォームにおけるdisabled属性の扱いについて。


はじめまして、オフィス狛 技術部のpinoと申します。

今回は、Angularのリアクティブフォームにおけるdisabled属性の扱いについて書いてみたいと思います。

先日、Angularの実装中に以下のWarningに出会いました。
It looks like you're using the disabled attribute with a reactive form directive. If you set disabled to true
when you set up this control in your component class, the disabled attribute will actually be set in the DOM for
you. We recommend using this approach to avoid 'changed after checked' errors.

Example:
form = new FormGroup({
     first: new FormControl({value: 'Nancy', disabled: true}, Validators.required),
     last: new FormControl('Drew', Validators.required)
});

(かなり砕いた感じに)意訳すると、リアクティブフォームを使っているのに、disabled属性をHTMLに直に設定していませんか?例の書き方ならdisabled属性をDOMで設定することができます!といった具合でしょうか。

Warningなので最悪このままにしておいてもいいかもしれませんが、できることなら解消しておきたいですよね。

上記のWarningは、文言に忠実に従うことで回避できます。
以下、応用も含めて例を紹介します!

Formの定義

修正前は、以下のFormを定義していることにします。
(わかりやすく、Warningの文中にあった例を流用したいと思います。)
HTML
... // 中略

  <form [formGroup]="form">
    <input type="text" disabled formControlName="first">
    <input type="text" formControlName="last">
  </form>

...
TS
... // 中略

  form = new FormGroup({
    first: new FormControl('Nancy', Validators.required),
    last: new FormControl('Drew', Validators.required)
  });

...
FormBuilderを使用する場合
... // 中略

  form = this.fb.group({
    first: ['Nancy', Validators.required],
    last: ['Drew', Validators.required]
  });

  constructor(private fb: FormBuilder) { }

...
* FormBuilderを使用する場合は、DIが必要です。 以降は、記述を省略します。

Warningの出る書き方

HTMLで以下のようにdisabled属性を設定していると、上記のWarningが出ます。
例)
  // ①HTMLの書き方そのまま
  <input type="text" disabled>

  // ②バインディング構文使用、設定値は固定
  <input type="text" [disabled]="true">

  // ③バインディング構文使用、設定値は可変
  <input type="text" [disabled]="form.invalid">

③もダメなんですね...。


解消方法

1. 基本に従う
Warningの文言にある通りに書き換えます。
HTML
... // 中略

  <form [formGroup]="form">
    <input type="text" formControlName="first">
    <input type="text" formControlName="last">
  </form>

...
TS
... // 中略

  form = new FormGroup({
    first: new FormControl({value: 'Nancy', disabled: true}, Validators.required),
    last: new FormControl('Drew', Validators.required)
  });

...

HTMLからdisabledの記述を無くし、
コンポーネント側に {value: 'Nancy', disabled: true} という記述を増やしました。
ちなみにdisabled: trueで非アクティブ、disabled: falseでアクティブとなります。

FormBuilderを使用する場合は、以下となります。
... // 中略

  form = this.fb.group({
    first: [{value: 'Nancy', disabled: true}, Validators.required],
    last: ['Drew', Validators.required]
  });

...
こちらも、{value: 'Nancy', disabled: true} を追記すればOKです。

2. 動的に設定する
動的に切り替えたい時の方法です。
HTML
... // 中略

  <input type="text" formControlName="first">
  <input type="text" formControlName="last">
  <input type="checkbox" (click)="switchDisabled()">チェックするとfirstが非アクティブになります

...
TS
... // 中略

  isDisabled = true;

  form = this.fb.group({
    first: [{value: 'Nancy', disabled: this.isDisabled}, Validators.required],
    last: ['Drew', Validators.required]
  });

...

  switchDisabled() {
    if (this.isDisabled) {
      this.form.controls.address.enable();
    } else {
      this.form.controls.address.disable();
    }
    this.isDisabled = !this.isDisabled;
  }

...

切り替え用のチェックボックスを置いてみました。
ポイントは、コンポーネント側のenable()disable()です。
これにより、アクティブ・非アクティブを切り替えることができます。

今回はチェックボックスを使用しましたが切り替えるためのトリガーはなんでもOKですし、
切り替えが必要ない場合は、ngOnInit()に処理を置いて初期状態の判断に使用するのも良さそうです!

補足

ここまで説明してきましたが、実はdisabledをこのように指定しなくてもWarningが出ない要素もあります。
今のところそのように把握しているのは、button要素とoption要素です。

さらに、前述したWarningの出る書き方の例でいうと

button要素 → ①, ②, ③ どの書き方をしても怒られない
option要素 → ①は怒られる。②と③なら怒られない

という結果になりました。

この辺りは、余裕ができたらもう少し詳しく調べてみたいですね。


今回はここまで。
簡単な内容だったと思いますが、参考になれば嬉しいです。
,

2020年10月21日水曜日

Angular・HttpParams を動的に追加したい。


オフィス狛 技術部のKoma(Twitterアカウントの中の人&CEO)です。


Angularで、バックエンドAPIとやり取りする時は、
HttpClient」と「HttpParams」を使うと思います。

「HttpParams」はGETやDELETEの時のクエリパラメータを設定する時に使いますね。

  getHogeInfo(req: HogeInfoRequestModel): Observable<HogeInfoModel> {
    const options = {
      params: new HttpParams()
        .set('hogeId', req.hogeId)
        .set('testCode', req.testCode)
    };

    return this.http.get('xxxxx/yyyyy/hoge/info', options).pipe(tap(console.log));
  }

この時、クエリパラメータ「koma」が配列型式で追加になったとします。

よっしゃ、追加したろ、という感じで、
  getHogeInfo(req: HogeInfoRequestModel): Observable<HogeInfoModel> {
    const options = {
      params: new HttpParams()
        .set('hogeId', req.hogeId)
        .set('testCode', req.testCode)
    };

    body.komaArray.forEach((item, index) => {
      options.params.append(`koma[${index}]`, item);
    });

    return this.http.get('xxxxx/yyyyy/hoge/info', options).pipe(tap(console.log));
  }
とやると、うまくいきません・・・・。

ちょっとハマったのですが、

HttpParamsのappend()は、appendを実施したHttpParams自身には影響を及ぼさず、単純に追加したオブジェクトを返却するのみ。

と言う事です。つまり、
    body.komaArray.forEach((item, index) => {
      options.params = options.params.append(`koma[${index}]`, item);
    });

こんな感じで、「options.params」に値を再設定しないといけません。

結構単純なところにハマってしまうものですね。

Amazon WorkSpaces で気になったことを調べてみました。


オフィス狛 技術部のJoeです。

先日、担当したプロジェクトの開発で、AWS仮想デスクトップサービスの WorkSpaces を利用しました。

WorkSpacesの概要は、Amazon WorkSpacesのページを見ると判るのですが、
利用するにあたり、事前に気になって調べた内容を共有しようと思います。
※こちらは、2020年7月時点の情報になります


1.どのOSが選択できるのか?

以下から選択可能です。
・Windows 10(バンドルでoffice2010、2013、2016を選択できました)
・Linux2

今回はWindows10を選択して使用したのですが、
実際にWorkSpacesに接続して、OSのシステムを確認すると、
なぜか「Windows Server 2016 Datacenter」になっていました。

2.ボリュームサイズは拡張できるのか?

WorkSpacesを起動(作成)する時に、ユーザーボリュームサイズを10~100GBを選択できます。
WorkSpacesを起動(作成)後も、最大2,000GBまで拡張できます。
注意点としましては、サイズを縮小することはできませんでした。

3.接続方法は?

以下で接続可能です。
ただし、WorkSpacesを起動(作成)するディレクトリの「アクセス制御のオプション」で、有効化する必要があります。
・クライアント
 ※WorkSpacesのページからダウンロードできます。(Windows版、Mac版などあります)
・Webブラウザ

4.1つのWorkSpacesに複数ユーザーで同時接続できるか?

できませんでした。1セッションのみ有効です。

5.固定IP(Elastic IP)を利用できるか?

利用できます。
ただし、注意点としまして、WorkSpacesを起動(作成)するディレクトリの「インターネットへのアクセス」が有効になっていると、
WorkSpacesを起動(作成)時に、パブリック IPが自動で割り当てられてしまうため、
Elastic IPを関連付けすると、エラー「既にパブリックIPが関連付けられています」になってしまいました。

いろいろ試してみましたが、新たにElastic IPを関連付けできず、結局、下記のように新たにWorkSpacesを起動して対応しました。
①WorkSpacesを起動(作成)するディレクトリの設定で「インターネットへのアクセス」を無効化する
②WorkSpacesを起動(作成)する
②WorkSpacesにElastic IPを関連付けする

6.WorkSpacesに接続するIPアドレスを制限できるか?

制限できます。
まず、WorkSpacesサービスのIPアクセスコントロールで、接続するIPの「IPグループ」を作成して、
WorkSpacesを起動(作成)するディレクトリの「IPアクセスコントロールグループ」で、
作成した「IPグループ」を選択することで制御できます。

7.クライアントからWorkSpacesにファイルをコピーできるのか?

直接ファイルをコピーすることはできません。
Amazon WorkDocsを利用することで、ファイルの共有が可能になります。


WorkSpacesをご利用される際に、お役に立てれば幸いです。


,