2020年12月30日水曜日

Angular・Componentのタグにclassを付与したい【HostBinding】


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

早速ですが、皆さんはAngularをやっていて「コンポーネントのタグに直接classを付与したいんだけどなぁ....。」と思った経験はないでしょうか。

今回は、そんな時に使える @HostBinding についてご紹介したいと思います!

準備

まずは、コンポーネントを用意します。
app.component.html(親コンポーネント)
<header class="header">
  <section class="header__about">
    <h1 class="header__title">@HostBinding Sample</h1>
    <p class="header__lead">動作確認用のサンプルです</p>
  </section>
  <nav class="header-nav">
    <ul class="header-nav__list">
      <app-list-item *ngFor="let name of names" [name]="name"></app-list-item>
    </ul>
  </nav>
</header>

list-item.component.html
<li class="header-nav__item">
  <input type="checkbox" />{{ name }}
</li>
list-item.component.css
.header-nav__item {
  width: 120px;
  height: 64px;
  padding-left: 32px;
  line-height: 64px;
}
list-item.component.ts
import { Component, Input, OnInit } from '@angular/core';

@Component({
  selector: 'app-list-item',
  templateUrl: './list-item.component.html',
  styleUrls: ['./list-item.component.scss']
})
export class ListItemComponent implements OnInit {
  @Input() name: string;

  constructor() { }

  ngOnInit(): void {
  }
}
ビルド後のイメージ

色味がなく、シンプルで寂しい印象です。
ここから、実際にHostBindingを使ってclassを付与してみたいと思います。

HostBindingの使い方

使い方は

1. コンポーネントのタグにclassを付与する
2. 上記1に加えて、動的にclassの付け外しをする

と、主に2つあります。

まずは、1. コンポーネントのタグにclassを付与するについてです。
こちらは、大きく2通りの書き方ができます。
list-item.component.css
import { Component, HostBinding, Input, OnInit } from '@angular/core';  // HostBinding を追加

@Component({
  selector: 'app-list-item',
  templateUrl: './list-item.component.html',
  styleUrls: ['./list-item.component.scss']
})
export class ListItemComponent implements OnInit {
  @HostBinding('class') listItem = 'list-item';  // ①
  @HostBinding('class.first') first = true;  // ②

  @Input() name: string;

  constructor() { }

  ngOnInit(): void {
  }
}

ポイントは、@HostBinding()の、()の中です。
①は、「'class'」のみ指定して、なんのclassを付与するかは変数の値が使用されるパターンです。
値部分に定数を使えば、ロジック側にテンプレートの情報をベタ書きすることも避けられますね。
固定のclassであればこれで十分そうです。

②は、「'class.first'」と付与するclass名まで指定するパターンです。
この場合は、後に続いている値がtrueであれば付与、falseであれば付与しない、ということになります。
(※真偽値を固定で設定したい場合は、「@HostBinding('class.first') readonly first = true;」 といった具合に、readonlyをつけるなどの対策が考えられます。)

また、HostBindingの話とはすこし逸れますが、コンポーネントのタグに使用するclassは
グローバルである(style.cssに記述されている等)か、:hostメタデータを使用する必要があります。

今回は、自コンポーネントのCSSに背景色をつけるclassを用意したいと思います。
list-item.component.css
// 以下を追加
:host.list-item.first {
  background-color: #7ae5ec;
}

:host.list-item.second {
  background-color: #7aecc0;
}

:host.list-item.third {
  background-color: #7ab9ec;
}
ここまでのビルド後イメージ

すべてのコンポーネントにclassが付与されて、背景色がつきました。


次は、2. 上記1に加えて、動的にclassの付け外しをするについてです。
list-item.component.css
export class ListItemComponent implements OnInit {
  @HostBinding('class')
  @Input() name: string; // ①
  
  // 今回formは用意していないので、例としてだけ挙げます
  @HostBinding('class.valid') get valid() {
    return this.form.valid;
  } // ②

  @HostBinding('class.list-item') isClick = false; // ③-A
  ...
  onClick(): void {
    this.isClick = !this.isClick;
  } // ③-B

他にもいろいろな方法があると思うのですが、簡単に思いつくものを挙げてみました。

①は、Input()で受け取った値を使用します。
組み合わせて使うだけで、特別なことをしてる感じがします...!

②について、今回Formは用意していないので記述の例になるのですが、状態など値そのものを監視したい時に使える方法です。

getterってあまり使う機会がなかったのですが、今回試してみたところ使い勝手がいいですね!


また③は、②のようにイベント発生時ではなく、識別子に設定した変数の値を書き換えることによってclassの付け外しをしています。

ここまでで、上記の①と③をプログラムに落とし込んだビルド後のイメージが以下のようになります。

それぞれで別々のclassの付与と、classの付け替えができました!

補足

ここまでclassを付与する使い方を紹介してきましたが、なんとHostBindingはclass以外のバインディングにも使えます。
例えば、「@HostBinding('style.display') flex = 'flex';」としてあげればstyle="display: flex;"がバインドされます。
便利!

今回は公式以外に以下サイトも参考にしたのですが、「HostBinding('value') myValue;は[value]="myValue"と全く同じです。」という一文が特にわかりやすかったです。
@HostBindingと@HostListener:彼らは何をし、何のためにいますか?


以上、HostBindingデコレータについて紹介しました。
参考になればうれしいです。

Spring Fest 2020に参加しました。


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

Spring Fest 2020が開催されましたので、昨年に引き続き参加しました。
公式ページ

今年はオンライン開催ということで、昨年のような雰囲気ではありませんでしたが、
質問をチャットで受け付けてセッション後に回答いただけるなど、良かった点もありました。

また、今回は「Advanced Track」と「Beginner track」の2トラックで分かれて配信されていましたので、初めて参加される方も、セッションを選択し易かったかと思います。
(私はどれも聞いてみたく、結構直前まで迷ってしまいました)

今回も、特に気になったセッションの感想を、ブログに残したいと思います。


今こそ知りたいSpringエコシステム

槙 俊明さん(VMWare)
セッション資料

Springの歴史から始まり、主なSpring Projectを下記の3つの分類に分けて紹介されていました。
・できることを増やす
・バリエーションを増やす
・使いやすくするプロジェクト

改めて、様々なプロジェクトがあることを認識させられたことと、
最新情報は、書籍ではなく、公式ドキュメントと
プロジェクト開発者のGihub、Twitterで集めることが勉強になりました。


今こそ知りたいSpring DI × AOP

多田 真敏さん(株式会社カサレアル)
セッション資料

Springの基礎である、コンテナ、DI、スコープ、プロキシ、AOPについて丁寧に解説されていました。
初心者向けとありましたが、私の勉強不足もあり、なかなか説明に追いつけないところがありましたので、是非、もう一度復習したい内容でした。


CloudNativeな決済サービスの開発と2年間の歩み

鈴木 順也さん(SBペイメントサービス)
セッション資料

大規模な決済サービスのシステムを、2年間運用された実績をもとに、
アプリケーション構成とアプリの役割から、
ライブラリの移行と発生した問題、開発体制、CI / CD、テスト、Observabilityといった内容を
実績で紹介されていました。

1つのサービスの歩みという内容がとても面白く、勉強になりました。
また、2年経過してシステム数やアプリ数が増加しているのでに、
プラットフォーム運用者はほぼ変わらないという点にはとても驚きました。


Spring 5.3 & Spring Boot 2.4

槙 俊明さん(VMWare)
セッション資料

こちらはSpring 5.3 と Spring Boot 2.4 の新機能の紹介でした。
常に新しいもの、いいものを取り込んで進化しているという印象を受けました。

特に気になったのは、「プロパティ読み込み方法の改善」で、YAMLの定義方法が紹介されていました。
過去にSpring Boot のバージョンアップを行った際に、YAMLの定義方法の変更で、
結構ハマってしまうことがあったので、早めのバージョンアップが必要だなと思いました。

またこちらのセッション後にあった、Springについて色々な観点からの質問コーナー(?)は個人的に面白かったです。



今回はオンラインでの開催で、目的のセッションをゆっくり視聴できました。

今回のセッション動画は、今は非公開になっていますが、
来年に公開される予定とのことですので、今回視聴出来なかったセッションも確認してみたいと思います。


,

node.jsでS3上の画像ファイルをPDFファイルに変換する方法。


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

担当したプロジェクトで、S3上の画像ファイルをPDFファイルに変換する要件がありましたので、
変換方法を備忘も兼ねてご紹介しようと思います。

今回使用したライブライは2つです。 ※()は使用したバージョンです
・pdf-lib(1.11.1):PDFファイルを編集するためのnpmパッケージ
・sharp(0.26.3):画像ファイルを編集するためのnpmパッケージ


1.S3上の画像ファイル読み込む

S3から画像ファイルを読み込んで、bufferデータを取得します。
ここで問題になったのは、読み込んだ画像ファイルが意図せずに回転してしまったことです。
読み込んだ画像ファイルの回転情報を保持するために、sharpのライブラリを使用しました。

const aws = require('aws-sdk');
const sharp = require('sharp');

// S3にアクセスするための情報
const s3Client = new aws.S3({
  accessKeyId: [S3アクセスキー],
  secretAccessKey: [S3シークレットアクセスキー],
  region: [S3のリージョン],
});

const params = {
  Bucket: [S3バケット名],
  Key: [S3ファイルパス/ファイル名],
};

// S3の画像ファイルを取得
dataInS3 = await s3Client.getObject(params).promise();

// 画像ファイルのbufferデータを取得(rotateの引数なしで、読み込んだ画像の回転情報を保持する)
bufferData = await sharp(dataInS3.Body).rotate().toBuffer();

2.pdf-libで画像をPDFファイルに埋め込む

読み込んだ画像ファイルをPDFファイルに埋め込みます。
この処理では、読み込んだ画像ファイル容量が大きい場合、メモリ使用率にご注意ください。

const { PDFDocument } = require('pdf-lib');

// 空のPDFを作成
const pdfDoc = await PDFDocument.create();

// 画像のbufferデータをPDFに埋め込む
image = await pdfDoc.embedJpg(bufferData);  // jpgファイルの場合はこちら
image = await pdfDoc.embedPng(bufferData);  // pngファイルの場合はこちら

// 画像の倍率を元サイズのままとして幅と高さの情報を取得
const dims = image.scale(1);

// 画像ファイルと同じサイズのPDFページを追加して、画像を設定する
const page = pdfDoc.addPage([dims.width, dims.height]);
page.drawImage(image, {
  width: dims.width,
  height: dims.height,
});
const pdfBytes = await pdfDoc.save();


3.PDFファイルをS3に保存

最後に変換したPDFファイルをS3に保存にします。

// PDFファイルのbufferデータを取得
const pdfBuffer = Buffer.from(pdfBytes);

// S3に保存するためのパラメータを設定
const putParams = {
  Bucket: [S3バケット名],
  Key: [S3ファイルパス/PDFファイル名],
  Body: pdfBuffer,
  ContentType: 'application/pdf',
};

// S3に保存
await s3Client.putObject(putParams).promise();


以上で、変換したPDFファイルがS3に保存されました。
もし、画像からPDFへの変換が必要になった際に、参考になれば幸いです。


Catalinaにアップデートした後、pod installできないときの対処法。


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

既にBig Surがリリースされているので、少し今更感はありますがMac OSをCatalinaにアップデートした後、pod installが実行できなくなったのでその対処方法を記載します。
※ちなみにCatalinaのデフォルトシェルはzshですが、急いでいたのでひとまずbashで各作業は行っています。

実行環境
Xcodeのバージョンは諸事情で11系です...
$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.15.7
BuildVersion:	19H15

$ xcodebuild -version
Xcode 11.1
Build version 11A1027

1. エラーの内容確認

1-1. pod installのエラーメッセージの確認
エラー内容から、CocoaPodsはrubyの2.3を使用しようとしているようですが、フォルダがないようです。
$ pod install

-bash: /usr/local/bin/pod: /System/Library/Frameworks/Ruby.framework/Versions/2.3/usr/bin/ruby: bad interpreter: No such file or directory

1-2. rubyのバージョン確認
Catalinaではデフォルトでrubyの2.6が入っているようです。
$ ruby --version

ruby 2.6.3p62 (2019-04-16 revision 67580) [universal.x86_64-darwin19]

2. 対処方法

調査した結果、CocoaPodsを入れ直すと書かれていたので入れ直してみます。
gemを使用する方法と、brewを使用する方法があるようですが、今回はgemを使用します。

2-1. Xcodeのコマンドラインツール
その前にXcodeのコマンドラインツールが必要になるのでインストールします。
すでに入っている方は、下記のように表示されます。
$ xcode-select --install

xcode-select: error: command line tools are already installed, use "Software Update" to install updates

うまくインストールできない方は、公式から直接ダウンロードできるようです。
参考:https://developer.apple.com/download/more/

2-2. Xcodeとの紐付け
Xcodeにて、使用するコマンドラインツールの紐付けを行います。
既にこの作業が完了している方は、読み飛ばしてください。
(私はここが完了していなかったのですが、調べた数々のサイトにはこの手順の記載がなかったので時間が溶けました...)
Xcode > Preferences > Locations > Command Line Tools > XCode X.X.X(X.X.Xはバージョン)

2-3. CocoaPodsのインストール
$ sudo gem install cocoapods -n /usr/local/bin

pod insatll を行えたら、確認完了です。
以上となります、参考になりましたら幸いです。

参考にしたページ
This terminal won't let me run or install cocoa pods because I am on MacOS Catalina Beta? (stack overflow)
Brew based CocoaPods CLI fails in MacOS 10.15 Catalina due incorrect fallback to Ruby 2.3 #8955 (GitHubのCocoaPodsレポジトリのissue)

Angularでgetパラメータに配列を設定する方法。

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

前回6月くらいにAngularについて投稿したのですが、そこからお久しぶりのAngularについての投稿になります。
またご存知の方には基本的なお話しかもしれませんが、「angularでgetパラメータに配列をつかってリクエストする方法」についてご紹介しようと思います。
実はすこし前にKoma(Twitterアカウントの中の人&CEO)より「HttpParams を動的に追加したい。」という投稿で、配列形式でクエリパラメータを追加するという投稿があったのですが、これに少し似ている内容で、今回は1つのパラメータに配列を設定する方法になります。

まずは最初こんな感じでいけるのかなーと思って下記作ってみました。
※HogeInfoRequestModelにitemArrayという配列項目があるとします。
  getHogeInfo(req: HogeInfoRequestModel): Observable<HogeInfoModel> {
    const options = {
      params: new HttpParams()
        .set('items[]', req.itemArray)
    };
 
    return this.http.get('xxxxx/yyyyy/hoge/info', options).pipe(tap(console.log));
  }

上記で上手くいきそうな感じはするんですが、もちろん上手くいきません。。。

ここで「HttpParams を動的に追加したい。」の方法で、改めて配列追加という流れでやってももちろん大丈夫ですが、すでに配列になっている項目をリクエストしたい場合、下記のような記述方法でもいけました。

 getHogeInfo(req: HogeInfoRequestModel): Observable<HogeInfoModel> {
    const options = {
      params: new HttpParams()
        .set('items', JSON.stringify(req.itemArray))
    };
 
    return this.http.get('xxxxx/yyyyy/hoge/info', options).pipe(tap(console.log));
  }

JSON.stringifyを使ってここでJSON文字列に変換しちゃおうという方法です。

【注意!】結果的に、「配列を送っている」というのは正しくなく、ただの文字列を送っているにすぎないので、当然受け手側(WebAPI側)が、その文字列を配列に変換する必要があります。

配列項目をリクエストするという時には、用途に合わせて良き書き方で書いていただければと思います。

2020年11月27日金曜日

NgRx 8にて発生した「@ngrx/store: runtime checks are currently opt-in but ...」の警告の解決方法。


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

Angular8 + ngrx8を使用している担当プロジェクトで、ngrx/storeの警告がでているのが気になったので対処しました。

1. エラーメッセージの確認

@ngrx/store: runtime checks are currently opt-in but will be the default in the next major version with the possibility to opt-out, see https://ngrx.io/guide/migration/v8 for more information.
直訳:runtime checksは現在オプトイン形式がデフォルトですが、次のメジャーバージョンでオプトアウト形式になる可能性があります。詳細はサイトで確認してください。

これだけではさっぱりわからなかったので、メッセージに従いサイトを確認します。

2. ngrx バージョン8のアップデートガイド確認

色々書かれていますが、今回関連があるのは下記のようです。
A migration is provided to remove the usage ngrx-store-freeze, remove it from the package.json, and to enable the built-in runtime checks strictStateImmutability and strictActionImmutability.
直訳:ngrx-store-freezeの使用を取り除くマイグレーションが提供されます。package.jsonから削除して、ビルトインのruntime checks(strictStateImmutability とstrictActionImmutability)を有効にしてください。

要するに、ngrx7まではngrx-store-freezeを使用してStoreをimmutable(不変)にする必要があります。
しかし、ngrx8以上ではライブラリ不要になるので、runtime checksの値を設定してくださいとのことです。
弊社ではngrx-store-freezeを使用していませんが、同様にエラーが出ていました... とにかく設定しないといけないみたいです。

3. RuntimeChecksのページ確認

ページ内の記述方法を参考にstrictStateImmutabilitystrictActionImmutabilityの値を設定します。
ngrx-store-freezeを使用している場合は、設定している2つの項目はtrueにしてください。
// app.module.ts

StoreModule.forRoot(reducers),

  // ↓

StoreModule.forRoot(reducers, {
  runtimeChecks: {
    strictStateImmutability: false,
    strictActionImmutability: false
  }
}),

以上となります。
サイトに書いてあることをそのまま実行しただけですが、ngrxについて日本語で書かれているページがあまりないので、よければ参考にしてみてください。🍭

,

2020年11月25日水曜日

WindowsでDockerを利用してMySQLサーバーを立てる場合の注意点。

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

最近はnode.jsを触る機会がまた多くなってきたのですが、あるプロジェクトでAPIはnode.js、DBはMysqlを使うことになったので、Dockerでそれらのローカル環境を作り開発することにしました。
そこでWindowsとDockerの絡みでハマりポイントがあったので、ご紹介したいと思います。

※今回はDockerがWindows10環境にインストールされている前提で以下進めていきます。

まずDockerの起動にはdocker-compose.ymlを利用していきます。
そのdocker-compose.ymlには今回開発につかうAPIとDBの設定を下記のように記載します。
※今回DBの記述がメインとなるので、API側の記述は割愛します。
version: '3'
services:
  api:
	build:・・・
	・・・
  db:
    image: mysql:8.0
    command: mysqld --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_USER: user
      MYSQL_PASSWORD: pass
      TZ: 'Asia/Tokyo'
    ports:
      - "3306:3306"
    volumes:
      - './docker/dev/mysql/data:/var/lib/mysql'
      - './docker/dev/mysql/my.cnf:/etc/mysql/conf.d/my.cnf'
      - './docker/dev/mysql/sql:/docker-entrypoint-initdb.d'

上記のように書いて、あとは通常通り上記ymlを使ってDocker起動するだけです。
しかしながら、上記方法でMacでは上手くいくのですが、Windowsではなぜかうまくいきません。

これはWindowsはMacとは違い、WindowsはdockerをVirtualBox経由で起動させているためのようです。
こちら参考にさせていただきました。
https://qiita.com/waterada/items/1dbf6a977611e0e8f5c8

※ちなみにWindowsは上記が原因で、作りたい環境内容によっては他の問題も発生するようで、実際自分も別環境作成時にまた問題があったのですが、またそのあたり別の投稿で記載しようと思います。

ということでいろいろと調べた結果、mysqlの設定ファイルであるmy.cnfファイルを別途作成し、それをdocker起動時にマウントさせることによって本事象を解消できるということのようでした。

1.docker-compose.ymlを修正する

まずはdocker-compose.ymlを下記のように書き換えます。
version: '3'
services:
  api:
	build:・・・
	・・・
  db:
    image: mysql:8.0
    command: mysqld --default-authentication-plugin=mysql_native_password
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_USER: user
      MYSQL_PASSWORD: pass
      TZ: 'Asia/Tokyo'
    ports:
      - "3306:3306"
    volumes:
      - './docker/dev/mysql/sql:/docker-entrypoint-initdb.d'

2.my.cnfファイルを作成する

もともとDBの文字コードの設定もymlファイルのcommand部分に記載していたのですが、このあたりをmy.cnfに別途下記のように作成します。
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci

[client]
default-character-set=utf8mb4

3.Dockerfileを作成する

最後に上記のmy.cnfファイルをマウントさせるために、下記のようにDockerfileを作成し、そこでADDします。
またファイルの権限もデフォルトだと777となってしまい、mysqlは権限777のcnfファイルは読み込まないということなので、ADDしたあとに権限も変更するように記述します。
FROM mysql:8.0

ADD ./docker/dev/mysql/my.cnf /etc/mysql/conf.d/my.cnf

RUN chmod 644 /etc/mysql/conf.d/my.cnf

上記3つが整った状態で、ymlを使ってDocker起動すると、なんとかうまく起動されました。
上記手順書いてみると、なるほどなーと感じるのですが、これを全く知らないところから調べていったので、かなりハマって環境構築だけで結構時間がかかってしまいました。。。
そもそもやっぱりVirtualBox経由でDockerを利用することがいろいろな弊害を生んでいるようなので、このあたりWindowsユーザーは不利だなーと感じたのでした。

,

Illustrator2021のオブジェクトを再配色を試してみる。


こんにちは、オフィス狛 デザイン部のSatoです。

先日アップデートされましたIllustrator2021で、以前ブログでご紹介した「オブジェクトを再配色」という機能がさらに便利になったようなのでご紹介します。

↓以前アップしたオブジェクトを再配色の記事です。

実は元々の機能の場所などが少し変わっていますので、「オブジェクトを再配色」画面の表示方法から説明していきます。 


色を変えたいオブジェクト全体を選択し、Illustratorメニューバーの「編集」の「カラーの編集」から「オブジェクトを再配色」を選ぶかツールバーの「オブジェクトを再配色アイコン」をクリックします。

以前は「オブジェクトを再配色パレット」が表示……されていたのですが、今回のアップデートから新しい「オブジェクトを再配色」画面が表示されるようになりました。

今まで最初に表示されていた「オブジェクトを再配色パレット」は右下に表示される「詳細オプション」ボタンをクリックすることで表示されるようになっています。
今まで通り再配色パレットを最初に表示したい場合ははオブジェクトを再配色パレットの左下に表示される「起動時に詳細オブジェクトを再配置ダイアログを開く」にチェックを入れましょう。
(新しい「オブジェクトを再配色」のダイアログが表示しづらくなるのであまりおすすめはできません)

新しくなったオブジェクトを再配色には「カラーテーマピッカー」や「目立つカラー」パレットなどの機能が追加されたのでご紹介したいと思います。 


最初にご紹介したいのはカラーテーマピッカーツールです。
なんとこの機能、選択したオブジェクトではない別の画像やオブジェクトの全体的な配色をつかって再配色してくれる機能です。
「カラーテーマピッカー」ボタンを押すとマウスカーソルがスポイトマークに変化するので参考にしたい画像をクリックしたり、Shift キーを押しながらクリックして複数選択したり、ドラッグして選択範囲を選択します。

画像を使い、Illustratorで描いたイラストの色を変えました

選択した画像でよく使われている色に変わりましたが、なんだか配色にメリハリがない配色になってしまいました。
こういう時にもう一つの新機能 「目立つカラー」パレットが役立ちます「目立つカラー」パレットの目立たせたい明度の高い色の端を横にドラッグして広げます。

「目立つカラー」パレットを調整してイラストがメリハリのある配色になりました!


オブジェクトを再配色を駆使し色をさらに調整してイラストを完成させました。

「カラーテーマピッカー」も「目立つカラー」も、よく色の参考に使っている「カラーライブラリ機能」よりもなかなか普段使わないような配色のデザインが作れるので、普段似たような色ばかり使ってしまうなあ……と悩んだ時に使ってみたい機能だと思います。


みなさんも配色に悩んだ際に「オブジェクトを再配色」の新しい機能を使ってみてください。

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をご利用される際に、お役に立てれば幸いです。


,

2020年9月30日水曜日

Photoshopで出会った謎の「インデックス」レイヤーを編集できるようにする!

こんにちは、オフィス狛 デザイン部のSatoです。


先日pngファイルをPhotoshopで開いて編集しようと思った際に編集できず、困ったことがありました。

レイヤーロックを解除しようとレイヤーの上でダブルクリックしても解除できないのです。

よく見てみるとロックされているレイヤー名が「インデックス」になっています。

「インデックス」レイヤーは普段Photoshopでよく見る背景レイヤーと違い、レイヤーパネルの操作だけではロックが解除できずどうしたら編集できるのか分かりづらかったので、ロック解除のしかたを記載します。


まずレイヤーパレットからはインデックレイヤーを普通のレイヤーにすることはできません。レイヤーのカラーモードが関係しているので、レイヤーのカラーモードを通常のカラーモード(RGBやCMYKなど)に変更する必要があります。

Photoshopメニューバーの「イメージ」内の「モード」にマウスカーソルを合わせます。

インデックスカラーにチェックがついているので「RGBカラー」や「CMYKカラー」など使用する用途に合わせた項目をクリックします。

この操作で、今までインデックスレイヤーとして変更ができなかったレイヤーが通常レイヤーに変わり、ロックも外れて編集ができるようになります。


インデックスカラーの事を今回初めて知ったので、Adobeのカラーモードの説明を読んでみた所、

インデックスカラーモードは使用できる色を制限する事で目に見える画質を維持しながらファイルサイズを小さく保存できるというなかなか魅力的なカラーモード……。のようですがレイヤーが編集できないので何も知らないまま開くと少しびっくりしますね。

2020年6月30日火曜日

angularでの複数チェックボックスの取り扱い。


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

最近久々にAngularプロジェクトを開発する機会があり、そこでまたいろいろと勉強になったことがあったので、ご存知の方には基本的なお話しなのかとは思いますが、今回は「angularで複数のチェックボックスを使った記述方法」の一例をご紹介できればと思います。

■作りたい画面

今回作る画面のイメージとしては、下記のような登録画面で、基本情報の入力と共にチェックボックスが複数設置され、その内容をフォーム送信して登録というようなシンプルな画面構造を想定します。

画面イメージ



■コンポーネント

まずコンポーネント側の記述ですが、今回フォーム送信を行うので、Angularのリアクティブフォームから、FormControl、FormGroup、FormBuilder、FormArrayクラスをそれぞれ利用します。 各クラスの細かな説明はAngularの公式ドキュメント内のリアクティブフォームの説明を見ていただいたほうがわかりやすいかもですが、めちゃくちゃざっくり書くとこんな感じかなと思います。
FormControl:単一入力欄の制御
FormGroup:FormControlのグループ化
FormBuilder:FormControlの作成に使う(FormControlをたくさん扱う場合にインスタンス不要)
FormArray:FormGroup内の配列要素コントロールの制御
さらに、今回は入力の必須チェックも行いたいので、Validatorsクラスもインポートします。

では、実際に上記をインポートし、コンポーネント側を記述します。
export class AppComponent implements OnInit {
  favoriteFruitsList: {key: string, value: string}[] = [
    {key: '1', value: 'りんご'},
    {key: '2', value: 'バナナ'},
    {key: '3', value: 'みかん'},
    {key: '4', value: 'ぶどう'},
  ];

  checkBoxFormArray: FormArray;
  groups: FormGroup[] = [];

  form: FormGroup = this.formBuilder.group(
    {
      name: ['', [Validators.required]],
      age: ['', [Validators.required]],
      tel: ['', [Validators.required]],
      formFavoriteFruitsList: this.formBuilder.array(this.groups, [Validators.required, this.checkBoxValidator]),
    }
  );

  constructor(
    private formBuilder: FormBuilder,
    public v: UserValidator,
  ) {}

  ngOnInit() {
    this.v.formGroup = this.form;
  }

  checkBoxValidator(control: FormArray) {
    for (let i = 0, loop_cnt = control.length; i < loop_cnt; i++) {
      if (control.at(i).value) {
        return null;
      }
    }
    return { unChecked: true };
  }

  onChange(value, isChecked: boolean) {
    this.checkBoxFormArray = this.form.get('formFavoriteFruitsList') as FormArray;
    if (isChecked) {
      this.checkBoxFormArray.push(new FormControl(value));
    } else {
      const index = this.checkBoxFormArray.controls.findIndex(x => x.value === value);
      this.checkBoxFormArray.removeAt(index);
    }
  }

  onSubmit() {
   // 登録処理
  }

}

form: FormGroup = this.formBuilder.group(...
の記述で、まずはフォームの初期設定をおこないます。
ここでチェックボックスとなるformFavoriteFruitsListはarray()メソッドでフォーム配列とし、さらにその中でチェックボックスをグループ化しています。これはcheckBoxValidatorでチェックボックスの必須チェックの為にこのような記述にしておきます。
ちなみにここで作成したcheckBoxValidatorは、チェックボックスのいずれかにチェックが入っているかのバリデーションで、Validatorsクラスには無いオリジナルのバリデーションになります。
onChangeについてはhtml側の記載で説明します。

■html

<form [formGroup]="form" (ngSubmit)="onSubmit()">
  <h2>名前</h2><input type="text" formControlName="name">
  <h2>年齢</h2><input type="text" formControlName="age">
  <h2>電話</h2><input type="text" formControlName="tel">
  <h2>好きな果物</h2>
  <ng-container formArrayName="formFavoriteFruitsList">
    <ng-container *ngFor="let item of favoriteFruitsList;">
      <label>
        <input type="checkbox" [value]="item.key" (change)="onChange(item.key, $event.target.checked)"><span>{{ item.value }}</span>
      </label>
    </ng-container>
  </ng-container>
  <button type="submit">登録</button>
</form>

ここでformArrayNameにコンポーネント側で設定したformFavoriteFruitsListを記載します。
これによりこのチェックボックスは配列要素のフォームとして扱います。
またここで、コンポーネント側に作成した、チェックボックスをチェックした時に発火するonChangeを使います。
チェックをON、OFFする際に、checkBoxFormArrayの配列内で、値を追加、削除する関数になります。
これにより、submitした時に、実際にチェックした値だけフォーム送信されます。


今回記載した内容はあくまで一例で、結構他の記載方法もあるようで、もしかしたらもう少し簡単に書ける部分もあるかもしれません。
また画面的には非常にシンプルなのですが、まだAngularのリアクティブフォームの使い方をうまく把握できていないと最初は難しく、結構苦戦しました(苦笑)。

自分のようなAngular初心者の方で、もしご参考になればと思います。

2020年6月4日木曜日

nodemonを使用して、コード変更後に自動でNode.jsのアプリを再起動する方法。


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

Node.jsで動いているアプリケーションのコードの修正をした後、サーバーの再起動が必要になリます。
コードを修正した後、毎回手動で再起動するのは面倒ですし、再起動し忘れることもあります。
nodemonを使用すれば、その煩わしい作業を自動化できます😶

nodemonとは
nodemonとはNode.jsベースのアプリケーションのコードの変更を監視し、変更があった際に自動でサーバーの再起動をしてくれるツールです。
参考:https://nodemon.io/

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

1. nodemonのインストール

使用したいプロジェクト配下に移動し、下記を実行します。
devDependencies オプションは開発環境でのみ使用するため、今回つけています。
$ cd ~/<プロジェクトパス>
$ npm install nodemon --save-dev

--save-dev オプションを使用してインストールすると、 package.json に下記のように追記されます。
devDependencies 配下のパッケージは本番環境で npm install --production コマンドを実行すると除外されます。
// package.json

"devDependencies": {
    "nodemon": "^2.0.3"
  }

2. nodemonでアプリケーションを実行する

package.jsonに nodeコマンドの代わりにnodemonで起動するscriptを追加します。
script名(start-watch)は自分可変です。
   // package.json

"scripts": {
     "start": "node src/index.js",
     "start-watch": "nodemon src/index.js", // 🌟追加
     "test": "echo \"Error: no test specified\" && exit 1"
   },

nodemonを使用して、アプリを起動します。
$ npm run start-watch

下記のように起動ログが出力されれば、起動成功です。
 [nodemon] watching path(s): *.*
 [nodemon] watching extensions: js,mjs,json
 [nodemon] starting `node src/index.js`

コードに何か変更があった場合、下記のようにログが出力されます。
 [nodemon] restarting due to changes...
 [nodemon] starting `node src/index.js`

また手動で再起動したい場合は、 rs コマンドを打ってください
$ rs
 
 [nodemon] starting `node src/index.js`

以上となります。
インストールして少し設定するだけで、開発が楽になるのでまだ使用していない方はぜひお試しください🍭

2020年5月28日木曜日

SUZURIでオリジナルグッズを作りました。


こんにちは、オフィス狛 デザイン部のSatoです。

弊社のWEBサイトなどで使用しているイラストのグッズをSUZURIというサービスで作成し、社員が注文した現物が届きましたので紹介します。


SUZURIはpngまたはjpg画像を1枚アップロードするだけでオリジナルプリントのグッズが販売できるようになるサービスです。
Tシャツなどのポピュラーなアイテム以外にも赤ちゃんのよだれかけなどの様々なアイテムに、注文がある度に印刷して発送してくれます。
在庫の管理をしなくてよいので売れるかどうかを気せずに、気軽にオリジナルの絵柄のグッズをオンライン販売できるのが有難いですね。


試しにいろいろなグッズを作ってしまいました。
グラスなど調整が必要なグッズもありましたが基本的にWEBサイトなどに使用した画像のカラーモードを変えただけの画像をほぼそのままアップロードするだけでオリジナルプリントのアイテムのモックアップが自動的に生成されました。
(ちなみに調整が一番大変だったのはスマートフォンケースでした。自動的にいろいろな端末のケースが作られてしまうのでシンプルな絵柄のものでないと調整が難しく感じました)

また、利益分も個人的に100円単位で設定できるのが有難いと思います。(弊社のグッズは利益分は初期設定または初期設定以下に設定にしてあります。)


今回、サイトで作ったTシャツとステッカーを社員が購入したので実物の写真を撮影してみました。
(Tシャツの上に置いて撮影したステッカー自体の色が暗いので雑なコラージュのようになってしまいました…)

綺麗に印刷されていますね。写真では分かりにくいかもしれませんが、Tシャツ自体は厚手で非常にしっかりした作りです。
あまりにも作業が簡単なのでもっと安っぽい印刷かと思いましたがほとんど普通にお店で売っているプリントTシャツと同じようなプリントなのではないでしょうか…?

カラーモードをRGB(画面で表示する際のカラーモード)→CMYK(印刷する際のカラーモード)に変更したので少々くすんでいるかと思いましたが非常に色も鮮やかに見えます……!
個人的にはTシャツは大満足の出来です。

ステッカーの方はモックアップをPC画面で見た時よりも少し暗めの色になってしまっていますね。ステッカー自体の色が暗い色だからでしょうか…?
でも印刷自体は色ムラもなくかなり綺麗だと思います。


現在オフィス狛のSUZURIのページの方でオリジナルグッズをオンライン販売中ですので気になる方はぜひアクセスしてみてはいかがでしょうか。
私も緊急事態宣言がおさまり次第、トートバックを買おうかと思っています。

2020年5月19日火曜日

MacOS Catalinaはデフォルトのログインシェルがbashではないらしい。



オフィス狛のyuckieee(ゆっきー)です。
最近Angularの環境構築をする機会があったのですが、その際以下の事象に悩まされました。

「ターミナルを起動するたびに"source ~/.bash_profile"を実行しないとパスが通らない!!!」

最初は毎回コマンド叩いてたのですが、煩わしくなって調べたら簡単な話でした...。
そこで本事象の原因や対処方法について軽くまとめておきます。

事象の原因

MacOS Cataliaからデフォルトログインシェルが「bash」から「zsh」に変更された事で、設定対象と読み込み対象のシェルが食い違っていることで本事象が発生していました。

もう少し細かく説明すると、環境構築方法を説明した多くの記事はbash(.bash_profile等)にパス設定する手順が書かれています。 ですが、MacOS Catalinaからはデフォルトのログインシェルが変更されており、ログイン時に読み込まれるのはzshの設定ファイルです。 いくらbashに設定を追加しても読み込まれることはなく、毎回強制読み込みコマンド("source ~/.bash_profile")を実行する必要があるのでした。
そりゃそうだ...。

現状の設定確認

それでは、実際にデフォルトのログインシェルの設定状況を確認してみましょう。
ターミナルを起動し、以下のコマンドを実行してください。

$ echo $SHELL
/bin/zsh

実行結果が"/bin/bash"以外であれば、パスを書き込むべきは".bash_profile"ではありません。

なぜデフォルトログインシェルが切り替わったのか?

公式での見解は見つからなかったのですが、幾つか記事を参照しているとライセンス問題と推測する声が多くありました。 そもそもmacOS Mojave以下のデフォルトログインシェルはbashですが、バージョンは最新(2020/05/08時点でver5.0)ではないようです。
セキュリティ上、最新化すべきと考えるのが自然ですが、ライセンス問題から旧バージョンのままとなっていたと思われます。
そして、この解決策として、ライセンスに課題を抱えるbash最新化ではなく、別のシェル(zsh)への乗り換えを選択したのでしょう。多分。

対応方法

対応方法としては、大きく2パターンが考えられます。

 パターン① zsh用の設定を行う
 パターン② デフォルトのログインシェルをbashに切り替える

切り替わった理由を推し量ると、推奨される対応方法はパターン①だと思います。
また、もしbashに切り替えるなら最新化もセットで行う必要がありそうです。

とはいえ、今回はパターン①、②双方の切り替え方法をご紹介しておきます。

パターン①zsh用の設定を行う

パスを通すだけであれば、基本的に設定ファイル名称を変更すれば済みます。
bash設定ファイルとの対比は以下のとおり。
bash zsh 補足
.bash_profile .zprofile ログイン時 (SSH ログインも含む) に実行
.bashrc .zshrc 新しいターミナルセッションごとに実行

指定例は以下のとおり。
今まで".bash_profile"を指定していた箇所をzsh用の".zprofile"に置き換えるのみです。
$ echo export PATH='/usr/local/bin:$PATH' >> ~/.zprofile
$ vim .zprofile

既に".bash_profile"にパスを設定している場合は、ファイルコピーで対応が可能です。
$ cp .bash_profile .zprofile

パターン②デフォルトのログインシェルをbashに切り替える

推奨ではないと思われますが、以下の手順でbashへの切り替えも可能です。
まず、切り替え対象のシェルが利用可能か確認するため、以下のコマンドを実行します。
bashに切り替える場合、本リスト内に"/bin/bash"があればOKです。
$ cat /etc/shells
# List of acceptable shells for chpass(1).
# Ftpd will not allow users to connect who are not using
# one of these shells.
/bin/bash
/bin/csh
/bin/dash
/bin/ksh
/bin/sh
/bin/tcsh
/bin/zsh

前項の確認結果を基にシェルの切り替えコマンドを実行します。
実行時にパスワードを求められるので入力してください。
$ chsh -s /bin/bash
Changing shell for ユーザー名.
Password for ユーザー名: 🗝

再度デフォルトログインシェルを確認し、切り替わっていればOKです。
$ echo $SHELL
/bin/bash

パスを直ぐに通したい場合は、以下のコマンドでパスを読み込ませます。
$ source ~/.bash_profile

Appleのサポートページでは、この他にもシステム環境設定画面からシェルを切り替える方法なども紹介されています。
Apple公式サポート:zshをMacのデフォルトシェルとして使う

【 おまけ 】

bash切り替え後にターミナルを立ち上げると以下のようなメッセージが表示されます。
「デフォルトシェルはzshに変わったから切り替えて!」と言うようなメッセージです。
 The default interactive shell is now zsh.
 To update your account to use zsh, please run `chsh -s /bin/zsh`.
 For more details, please visit https://support.apple.com/kb/HT208050.

本メッセージは、以下のコマンドを実行することで非表示にできます。
 $ export BASH_SILENCE_DEPRECATION_WARNING=1

事象が解決しない場合に確認すべきこと

ログインシェルの設定はソフトウェア単位にも設定可能な場合がありますので、そちらが有効となっていないか確認してみてください。

例としてターミナルでの設定確認方法をご紹介します。
ターミナルを起動し、[ターミナル] > [環境設定]を開き、環境設定画面[一般]タブの[開くシェル]設定が[コマンド(完全パス)]になっている場合、こちらの設定が優先となっています。





まとめ

セキュリティ上の懸念やAppleの対応(zshに切り替え)を考えると、個人的には利用者側もzshに切り替えるのが最善だと思います。
私もbashに切り替えるのではなく、zshの使用に切り替えるよう対応しました。
また、改めて環境構築周りの知識を強化しなければと反省。

2020年5月8日金曜日

AWS EC2でプロキシサーバー(Linux)を構築する。


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

最近担当したプロジェクトで、開発用に用意した複数のAmazon WorkSpaces(Windows)から、外部の開発用VPNへ接続する必要があったのですが、 VPNに接続可能なIPアドレスが1つという制限がありましたので、EC2でプロキシサーバーを構築することにしました。

AWSであれば、NAT(インスタンス、ゲートウェイ)でも実現可能ですが、 今回の開発環境では、一部ネットワークの接続制限がありましたので、プロキシサーバーを選択しました。

最低限の設定にはなりますが、手順をご紹介します。
※VPC、サブネット等は、事前に作成されている前提です

1.EC2インスタンス作成(Linux)

プロキシサーバー用のEC2インスタンス(Linux)を作成します。
細かい手順は省略させていただきますが、注意点としては以下のようになります。
・インスタンスタイプは最小を選択
・「インスタンスの詳細の設定」で、「自動割り当てパブリックIP」は「無効化」に設定
 ※後で「Elastic IPアドレス」を関連付けます

2.Elastic IPアドレスの割り当てとEC2への関連付け

EC2サービスの「Elastic IP」から、プロキシサーバー用の固定IPアドレスを割り当てます。
・スコープ:VPC
・パブリックIPv4 アドレスプール:Amazon の IPv4 アドレスプール

続けて、割り当てたIPアドレスを選択して、Actionの「Elastic IP アドレスの関連付け」から、1.で作成したEC2に関連付けます。

3.Squidの設定

プロキシサーバー用のEC2インスタンス(Linux)に、プロキシサーバーソフトの「Squid」を、以下の手順で設定します。

①Squidインストール
sudo yum install -y squid

②Squid有効化
sudo systemctl enable squid

③有効化の確認(squid.serviceがenabledであること)
sudo systemctl list-unit-files -t service | grep squid

④Squid設定ファイル修正
設定ファイルに、プロキシサーバーに接続するIPアドレスを設定します。

※注意
必ず、「http_access deny all」の記述より「上」に設定してください。
(上記は「そのほかのアクセスはすべて拒否する」という意味になりますので、下に設定してしまうと有効になりません)


sudo vim /etc/squid/squid.conf

【追記内容】
# 接続したいIPアドレスを指定(複数指定可能)
acl myip src [IPアドレス1]/32
acl myip src [IPアドレス2]/32

# 接続したいIPを許可
http_access allow myip

⑤Squidリロード
設定ファイルを反映する場合、再読み込みを行います。
sudo systemctl reload squid

⑥squidアクセスログ確認
ログでアクセス状況を確認することもできます。
sudo cat /var/log/squid/access.log

4.WorkSpaces(Windows)側の設定

最後に接続側の設定です。
インターネットオプションから、「接続」タブの「LANの設定」ボタン押下します。

続けて「プロキシサーバーを使用する」をチェックして、
・アドレス:2.で割り当てたElastic IPアドレス
・ポート:Squid設定ファイルにの「http_port」に定義されているポート番号(デフォルトは3128)

※プロキシサーバーを使用したくない接続先がある場合は、「詳細設定」ボタンから、例外を指定してください。


以上が、必要最低限の設定になります。

接続側のグローバルIPを確認(確認くん など)すると 「2.Elastic IPアドレス」のIPアドレスが、表示されると思います。


,

2020年4月14日火曜日

JSON ServerとFaker.jsを使って、大量のダミーデータがあるREST APIサーバーの作り方


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

フロント担当になったけど、APIがまだ作成されていない... Databaseもない...
そんな時のために大量のダミーデータがあるREST APIサーバーの作り方をご説明します😶

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

1. JSON Serveのインストール

JSON Serverを使用すると、REST APIのモックサーバーを簡単に作成できます。

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

# もしグローバルインストールしたい場合はこちら
$ npm install -g json-server

2. Faker.jsのインストール

Faker.jsを使用すると、大量のダミーデータを簡単に作成することができます。

Faker.jsのデータを保存する用に新たなファイルを作成します。
$ vi database.json

中身は下記のようにしてください。
{
     "info": []
 }

Faker.jsをインストールします。
$ npm install faker --save

Faker.jsでデータを作成する用のファイルを作成します。
$ vi generate.js

中身は下記のようにしてください。
(これは一例なので、名前とメールアドレスがセットのデータを10個作成していますが、必要なデータに合わせて変更してください)
var faker = require('faker');
 
 var database = { info: []};
 
 for (var i = 1; i<= 10; i++) {
   database.products.push({
     id: i,
     name: faker.name.findName(),
     email: faker.internet.email(),
   });
 }
 
 console.log(JSON.stringify(database));


以下のURLのNameSpacesに 、Faker.jsの用意されているダミーデータが記載されていますので必要なものを使用してください。
http://marak.github.io/faker.js/index.html

generate.jsのコードを実行して、出力結果をdatabase.jsonに書き込みます。
$ node generate.js > database.json

database.json中身が下記のようになりました。
$ cat database.json

 {"products":[
  {"id":1,"name":"Newton Kris","email":"Precious40@hotmail.com"},
  {"id":2,"name":"Dee Hackett","email":"Kasandra.Trantow91@gmail.com"},
  ~ 省略 ~
  {"id":10,"name":"Alvera Russel","email":"Felicia57@hotmail.com"}
 ]}

database.json のデータを返す、jsonServerを起動します。
$ json-server --watch database.json
 
  #  下記のように表示されたら、起動成功です
   \{^_^}/ hi!
 
   Loading database.json
   Done
 
   Resources
   http://localhost:3000/info
 
   Home
   http://localhost:3000

ブラウザにて、http://localhost:3000/info にアクセスしてみましょう。
作成したデータが表示されているはずです。

3. コマンドラインから確認する場合

curlコマンドを使用して確認します。
 # GET:全てのデータ取得
 $ curl -X GET "http://localhost:3000/info"
 
 # GET:「idが1」のデータだけ取得
 $ curl -X GET "http://localhost:3000/info/1/"
 
 # POST:「idが11」「nameがhoge」「emailがfuga@gmail.com」のデータ作成
 $ curl -X POST -d "id=11&name="hoge"&email=fuga@gmail.com" "http://localhost:3000/info"
 
 # PUT:idが1のデータの「nameをtest」「emailをupdate@gmail.com」に更新
 $ curl -X PUT -d "name=test&email=update@gmail.com" "http://localhost:3000/info/1"
 
 # DELETE:「idが11」のデータを削除
 $ curl -X DELETE "http://localhost:3000/info/11"

🍭 以下おまけ

既存プロジェクトのpackage.jsonのscript箇所に、データを作成するコマンドとjsonServerを起動するコマンドを入れると 便利です。

   // 一例として、angularのプロジェクトのpackage.json
   "scripts": {
     "ng": "ng",
     "start": "ng serve",
     "build": "ng build",
     "test": "ng test",
     "lint": "ng lint",
     "e2e": "ng e2e",
     "generate": "node generate.js > database.json", // 追加
     "server": "json-server --watch database.json" // 追加
   },

下記のようにスクリプトを実行することができます。
# ダミーデータの作成
 $ npm run generate
 
 # jsonServerの起動
 $ npm run server


以上となります。
簡単に使用できるので、REST APIサーバーが至急必要な場合はぜひ試してみてください。

2020年3月31日火曜日

iOSでViewの一部分を角丸にしたい時の拡張クラスの作り方。




こんにちは、オフィス狛 モバイル開発担当Aika-yuy です。
今回の投稿は、viewなどの一部分を角丸にしたい場合にインスペクタから設定できるようにする拡張クラスをご紹介します。
iOS11以降は、バージョンアップして以前より簡単に部分的に角丸にできるようになりました。


〜iOS10の設定方法

let path = UIBezierPath(roundedRect:layer.bounds,
                                byRoundingCorners:[//角丸にしたい場所を指定
                                                                    .topRight,
                                                               .topLeft,
                                                                    .bottomRight,
                                                                    .bottomLeft
                                                                   ],
                                cornerRadii: CGSize(width: XX, height: XX))
        let maskLayer = CAShapeLayer()
        maskLayer.path = path.cgPath
        layer.mask = maskLayer


iOS11〜の設定方法

layer.maskedCorners = [//角丸にしたい場所を指定
                                       .layerMaxXMaxYCorner,//右上
                                       .layerMinXMaxYCorner,//左上
                                          .layerMaxXMinYCorner,//右下
                                       .layerMinXMinYCorner//左下
                                         ]


CustomViewを作成しました

角丸の位置の指定は、ビット値で設定しているため、cornersではそれぞれの角丸に設定された値の合計を出しています。
class CustomView : UIView {
    @IBInspectable var cornerRadius: CGFloat = 0.0 { didSet { self.setNeedsLayout() } }
    @IBInspectable var cornerTopRight: Bool = false { didSet { self.setNeedsLayout() } }
    @IBInspectable var cornerTopLeft: Bool = false { didSet { self.setNeedsLayout() } }
    @IBInspectable var cornerBottomRight: Bool = false { didSet { self.setNeedsLayout() } }
    @IBInspectable var cornerBottomLeft: Bool = false { didSet { self.setNeedsLayout() } }
    
    private var corners: UInt {
        return UInt((cornerTopLeft ? 2 : 0) + (cornerTopLeft ? 1 : 0) + (cornerBottomRight ? 8 : 0) + (cornerBottomLeft ? 4 : 0))
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        self.setcornerRadius()
    }
    
    private func setcornerRadius() {
        layer.cornerRadius = self.cornerRadius
        if #available(iOS 11.0, *) {
            layer.maskedCorners = CACornerMask(rawValue: corners)
        } else {
            let path = UIBezierPath(roundedRect: layer.bounds,
                                    byRoundingCorners: UIRectCorner(rawValue: self.corners),
                                    cornerRadii: CGSize(width: self.cornerRadius, height: self.cornerRadius))
            let maskLayer = CAShapeLayer()
            maskLayer.path = path.cgPath
            layer.mask = maskLayer
        }
        layer.masksToBounds = self.cornerRadius > 0
    }
}




CustomViewを使用すると、インスペクタからon/offで部分的に角丸を設定できるようになります。 細かい角丸の設定が必要な際は使ってみてください!

2020年2月28日金曜日

knex.jsで発生した問題と対処方法(Invalid columnエラー、returningで取得できない)


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

弊社ではknex.jsを一部のプロジェクトで使用しているのですが、
これまで開発中にハマってしまった事象と対処方法を2点ご紹介したいと思います。

【環境】
・node.js:12.14.0
・knex.js:0.15.2
・SQL Server 2017

1.「Invalid column name」エラー

内部結合でand条件を追加するため、マニュアルを参考に下記のように記述したのですが、エラーが発生しました。
  const number = '1234';
  return await knex
    .select('id')
    .from('tbl_1')
    .innerJoin('tbl_2', function cond() {
      this.on('tbl_2.id', 'tbl_1.id');
      this.andOn('tbl_2.number', number);
    });

エラー内容)
Invalid column name '1234'.

function内で変数を使用すると、カラム名として扱われてしまいまいた。
対処として、プレースホルダを使用すると、値として正しく扱うことができました。

対処方法:プレースホルダを使用する)
  const number = '1234';
  return await knex
    .select('id')
    .from('tbl_1')
    .innerJoin('tbl_2', function cond() {
      this.on('tbl_2.id', 'tbl_1.id');
      this.andOn(knex.raw('tbl_2.number = ?', [number]),
      );
    });

2.insertした値を「returning」で取得できない

「returning」を使うとinsertした行の、指定したカラムの値を取得することができます。
(マニュアルを見ると、PostgreSQL、MSSQL、およびOracleで使用できるようです)

通常は以下のように記述します。この場合、insertしたデータのidの値「2」を取得できます。
(insertする値を取得するというちょっと意味のないことをしていますが、本来はオートインクリメントの値などを取得したい時などに使用します)

  return await knex
    .insert({ id: 2, memo: 'メモ' })
    .into('memo_tbl')
    .returning('id');

次に、insert句にknex.rawを使用して記述してみると、値が取得できませんでした。

NG:値が取得できない)
  return await knex
    .insert(knex.raw("(id, memo) VALUES (2, 'メモ')"))
    .into('memo_tbl')
    .returning('id');

対処として、returningは使用せず、SQL Serverで同じく操作した行の値を取得するOUTPUT句をそのまま記述することで値を取得することができました。
(OUTPUT句はSQL Serverで使用可能です。他のデータベースは異なります)

対処方法:OUTPUT句をそのまま記述する)
  return await knex
    .insert(knex.raw("(id, memo) OUTPUT INSERTED.id VALUES (2, 'メモ')"))
    .into('memo_tbl');


今回ご紹介した事象のように、一見すると正しい記述のようですが、
思わぬところで期待通りに動作しないことがありますので、ご注意ください。


,

Angularのバージョンアップ(7から8)後に「An unhandled exception occurred: Job name "..getProjectMetadata" does not exist.」が発生した場合の対処。


こんにちは。
最近は、CI/CD周りの環境構築にどっぷり浸かっていましたKoma(Twitterアカウントの中の人&CEO)です。
CI/CD周りはいずれ投稿するとして、今日はAngularのアップデート時に発生したエラーの解消方法です。

結論から言うと、バージョン「9」がリリースされた今だけ限定の事象ではあるのですが、
日本語で解決策を載せているサイトが無かったので、勢いで書きました。

(1)エラーが出る前にやっていたこと

今回やっていたことは以下の通りです。
・既存のAngularプロジェクトのバージョンをアップデート(8.2へ)
・既存のバージョンは「7.2」

バージョンアップ自体は、いつもの通り、Angular Update Guideを参考に行なっています。

(2)エラーの内容

アップデート自体は、うまく行ったのですが、その後の起動(ng serve)で下記のエラーが発生しました。
An unhandled exception occurred: Job name "..getProjectMetadata" does not exist.

(3)対応した内容

実は、本家のissueにも登録されている内容なのですが、
アップデート後に、「npm audit fix」をやってしまった事が原因です。
(というか、普通やりますけど)

「npm audit fix」によって、関連のプラグインがアップデートされますが、
この時、「@angular-devkit/build-angular」のバージョンが上がってしまうことに問題があります。
「npm audit fix」後の package.jsonを見ると・・・・
"@angular-devkit/build-angular": "^0.900.4",
となっています。
これが、Angular 9用のバージョンなのですが、今回アップデートしようとしているのはバージョン8なので、エラーになってしまうのですね。

と言うことで、
"@angular-devkit/build-angular": "~0.803.24",
に戻したところ、エラーはなくなり、無事に起動(ng serve)出来ました。

気持ち的には、「Angular 9」にアップデートしたいところなのですが、使用しているプラグインの対応を待たないといけないので、もどかしいところですね。
(時間さえあれば、プラグインの更新に貢献したいところなのですが・・・)

さて、これから残り3サイトのアップデートを行おうと思います。😭
しんどいけど、アップデートされず放置されているシステムが世の中に溢れていることを考えると、
アップデート出来るだけ、恵まれてますね😄

では、より良いAngularライフを❗️


2020年2月27日木曜日

nodenvでプロジェクトごとにnode.jsのバージョンを切り替える方法。


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

nodenvを使用してローカル環境のプロジェクトごとにnode.jsのバージョンを切り替える方法を説明します。
もちろん全てのプロジェクトで最新のバージョンを使用するべきですが、難しい時もありますよね...¯\_(ツ)_/¯

【環境】
MacOS Mojava
バージョン 10.14.6

1. anyenvのインストール

anyenvをインストールします。
(anyenvとは、ローカル環境で言語ごとに複数のバージョンを使用するためのツールです。node.jsだけでなく、pythonやPHPにも使用できます🍭)
➜ 😶git clone https://github.com/riywo/anyenv ~/.anyenv

パスを通す
➜ 😶echo 'export PATH="$HOME/.anyenv/bin:$PATH"' >> ~/.bash_profile
➜ 😶echo 'eval "$(anyenv init -)"' >> ~/.bash_profile

shelの再起動
➜ 😶exec $SHELL -l

anyenvの初期化
➜ 😶anyenv install --init

2.nodenvとnode-buildのインストール

nodenvをインストールします。
➜ 😶anyenv install nodenv

node-buildをインストールします。
➜ 😶brew install node-build

default-packagesファイル作成する。
➜ 😶echo yarn >> $NODENV_ROOT/default-packages
  // 実行しないと 後続作業でnodeをインストールするときに下記エラーがでる
  // nodenv: default-packages file not found

3.node.jsのインストール

インストールできるnode.jsのバージョン一覧を確認する。
➜ 😶nodenv install --list

使用したいバージョンをインストールする。
➜ 😶nodenv install <バージョン>

4.プロジェクトで使用するnode.jsのバージョンの設定

使用したいプロジェクト配下へ移動する。
➜ 😶cd <プロジェクトパス>

そのプロジェクトで使用するバージョン指定する。
➜ 😶nodenv local <バージョン>

// プロジェクト配下ディレクトリで使用するNode.jsのバージョンが「.node-version」に記載される
➜ 😶cat .node-version
 10.12.0

設定したバージョンと同じものが表示されていれば成功。
➜ 😶node -v
10.12.0 

補足1)インストールしたNode.jsのバージョン一覧を表示
➜ 😶ls ~/.anyenv/envs/nodenv/versions

補足2)アンインストールする場合
➜ 😶nodenv uninstall <バージョン>


以上となります。
最新のバージョンを使用するべきですが、プロジェクトごとに違うバージョンのnode.jsを使用しないといけない際はぜひ参考にしてください。