2018年12月11日火曜日

Angular5から7へのバージョンアップ・前編


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

少しAngularから離れていたのですが、過去のプロジェクトをアップデートする事になったので、備忘録で作業内容を残しておこうと思います。(他にもいくつかのプロジェクトでも同じ事する必要があるので・・・・)

基本的な作業としては、こちらの手順通りになります。 バージョンは(Angular Version)は、5.2から7.0でいきます。
アプリの複雑度(App Complexity)は・・・・Mediumぐらいですかね、とりあえず。
その他の依存関係(Other Dependencies)ですが、今回のプロジェクトは、AngularJSも混在していませんし、 Angular Materialも使用していないので、チェックは付けません。
パッケージ管理(Package Manager)はnpmを使っているので、npmを選択します。

・・・・・ん???
Warning: We do not recommend moving across multiple major versions.
複数のメジャーバージョンを跨いだアップデートはオススメしない??
・・・・まあ、モノは試し、オススメしない方法でとりあえずやってみよう。

「Show me how to update!」を押すと、下記のように手順が表示されるので、順番にやっていこうと思います。

更新する前に(Before Updating)

・・・これ、もしかして、アプリの複雑度(App Complexity)によって出てくる内容違うのでは?と思ったら、案の定違うので、念の為「Advanced」の方の記載も見ておいた方が良いかもしれません。
今回のプロジェクトはこのまま「Medium」で行きます。

まずは、
If you import any animations services or tools from @angular/core, you should import them from @angular/animations
です。
以前(Angular2ぐらい?)のバージョンでは、animations系の処理はcoreに含まれてたので、それは使えないよ、と言う事ですね。
まあ、今回は元のバージョンがAngular5なので、気にする必要はなさそうです。

続いての
Switch from HttpModule and the Http service to HttpClientModule and the HttpClient service. HttpClient simplifies the default ergonomics (You don't need to map to json anymore) and now supports typed return values and interceptors. Read more on angular.io
です。
これも似たような事ですが、HttpModuleは使えないからHttpClientModuleに変更してね、と言う事ですね。
HttpClientModuleは、確かAngular 4.3 ぐらいから使えるようになった機能です。

最後の
Choose a value of off for preserveWhitespaces in your tsconfig.json to gain the benefits of this setting, which was set to off by default in v6.
です。
Angular5から追加になったpreserveWhitespacesは、tsconfig.jsonに記載するのですが、これをoff(false)にする事で、コンパイル時に余計な空白を削除することができます。 Angular6から、これはデフォルトoff(false)だよ、と注意を促してくれています。

今回は全て問題ないので、そのまま次の手順に進んで行きます。

更新作業(During the Update)

Make sure you are using Node 8 or later

Node のバージョンは8以降である必要があります。
コマンドラインやターミナル等で「node -v」を実行して確認しましょう。

Update your Angular CLI globally and locally, and migrate the configuration to the new angular.json format by running the following:

続いて、Angular CLIの最新をインストールします。
以降は、バージョンアップを行いたいプロジェクトディレクトリで実行して下さい。
npm install -g @angular/cli
npm install @angular/cli
ng update @angular/cli
グローバルのAngular CLIを更新した後、ローカルの更新を行い、最後にローカルのパッケージの更新を行なっています。
ただ、上記「npm install @angular/cli」だとローカル側のAngular CLIのバージョンが変わらなかったので、「npm install @angular/cli@latest」として最新をインストールしました。

正しく動いていると、
DELETE .angular-cli.json
CREATE angular.json (4321 bytes)
こんな感じで、旧バージョン形式である「angular-cli.json」が削除され、新バージョンの「angular.json」が作成されているはずです。

その後、もう一度「ng update @angular/cli」を行なっています。

ちなみに、プロジェクトでngrxを使っていると、モジュールに対してのワーニングが出ます。
ここでは無視して構いません。

Update any scripts you may have in your package.json to use the latest Angular CLI commands. All CLI commands now use two dashes for flags (eg ng build --prod --source-map) to be POSIX compliant.

package.jsonに記載されているscriptsは、最新のコマンドで更新してね、と言っていますね。
よくあるのが、「"build": "ng build --prod",」とかで、コマンドを簡略化するパターンですが、
最終的なコマンドはAngular CLIなので、そこは最新のコマンドに書き換えする必要があります。

Update all of your Angular framework packages to v6, and the correct version of RxJS and TypeScript.

続いて、Angular自体のアップデートを行います。(下記のコマンドを実行)
ng update @angular/core
注意書きとして、
After the update, TypeScript and RxJS will more accurately flow types across your application, which may expose existing errors in your application's typings
とあります。
AngularをUpdatesする事で、型チェック等が厳密になる為、既存のプログラムでエラーが出る可能性があるよ、と言う事ですね。まあ、その辺は想定内というか、多分別のところでエラーになるでしょう・・・。

ngModelChange is now emitted after the value/validity is updated on its control instead of before to better match expectations. If you rely on the order of these events, you will need to begin tracking the old value in your component.

Angular6以前でngModelChangeを使用している場合、実装方法によっては、変更前の値が取れてしまう問題がありましたが、
それがAngular6で修正されているので、ngModelChangeを使用しているところは注意してね、という事です。

Use ng update or your normal package manager tools to identify and update other dependencies.

先程は、Angular自体のアップデートを行ったので、個々のパッケージの依存関係は「ng update」などで更新してね、という事です。
という訳で、
ng update --all
をやっておきましょう。

ただ、ngrxを使っている場合はエラーでupdate出来ないと思います。
Package "@ngrx/store" has an incompatible peer dependency to "@angular/core" (requires "^6.0.0", would install "7.1.1")

ここは一旦強制的にupdateしてしまいます。
ng update --all --force
ng updateの度に、
@ngrx/store@6.1.2 requires a peer of @angular/core@^6.0.0 but none is installed. You must install peer dependencies yourself.
のようなワーニングは出てしまうのですが、
6.1.2はangular7に対応はしているので、一旦このままで行きます。
関連: https://github.com/ngrx/platform/issues/1397

If you have TypeScript configured to be strict (if you have set strict to true in your tsconfig.json file), update your tsconfig.json to disable strictPropertyInitialization or move property initialization from ngOnInit to your constructor. You can learn more about this flag on the TypeScript 2.7 release notes.

TypeScript 2.7から追加されたstrictPropertyInitializationについての記載です。より厳密な初期化チェックを行うかどうかのフラウですが、元々から存在しているstrictが有効になっている場合、サブセットであるstrictPropertyInitializationも有効になってしまうので、tsconfig.jsonで無効にするか、厳密なチェックに対応するようプログラムを変更してね、という事です。
(プロパティの初期化は、ngOnInitではなく、コンストラクタでやらないといけない)

更新後作業(After the Update)

Remove deprecated RxJS 6 features using rxjs-tslint auto update rules For most applications this will mean running the following two commands:

deprecatedされた機能を削除する、という事で、
これは、指示通り、以下のコマンドを実行します。
npm install -g rxjs-tslint
rxjs-5-to-6-migrate -p src/tsconfig.app.json
そうすると、変更されたファイルが表示されます。
Fixed 5 error(s) in /Users/officekoma/hoge-web-prj/src/app/auth/services/hoge1.service.ts
Fixed 1 error(s) in /Users/officekoma/hoge-web-prj/src/app/auth/services/hoge2.service.ts
// (中略)
Fixed 5 error(s) in /Users/officekoma/hoge-web-prj/src/app/home/store/effects/my-page.effect.ts
Fixed 5 error(s) in /Users/officekoma/hoge-web-prj/src/app/home/store/effects/my-page-edit.effect.ts

WARNING: /Users/officekoma/hoge-web-prj/src/app/auth/services/hoge1.service.ts[3, 1]: duplicate RxJS import

Once you and all of your dependencies have updated to RxJS 6, remove rxjs-compat.

続いて、rxjs-compatを削除します。
npm uninstall rxjs-compat

If you use the Angular Service worker, migrate any versionedFiles to the files array. The behavior is the same.

Angular Service workerを使っている場合もマイグレーションを行う必要があるようですが、
今回のプロジェクトでは使用していないので、特に何もしません。

という事で、5.2から7.0へのアップデートを行いました。
後は、個々のプログラムの中を見て、エラーとなっている部分を修正していく事になります。
長くなったので、その辺は次回ブログに記載しようと思います。

それでは、良いAngularライフを!

2018年12月7日金曜日

Photoshopでスライスツールを使わずにパーツを一気に書き出す方法。

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

今回はPhotoshopで作ったモック画像からスライスツールを使わないで細かい画像を書き出す方法をご紹介しようと思います。

モック画像からスライスを設定して書き出しするのは地味に大変な作業だと自分は思っているので、できるだけ楽に作業したいと思っていましたが実はスライスツールを使うよりも、もっと楽に細かいウェブデザインのパーツを書き出す方法があったのでご紹介させていただきます。

方法1 画像アセット機能で書き出す。


1.書き出したいレイヤーのレイヤー名またはグループのグループ名を「(書き出したい画像のタイトル).(書き出したい画像拡張子)」にします。

2.Photoshopのメニューバーの「ファイル」の「生成」にマウスカーソルを合わせると「画像アセット」という項目が出てくるのでクリックします。

psdファイルを保存している場所に「(書き出したpsdファイル名)-assets」というフォルダーができているので、その中に書き出されています。


書き出せる拡張子はJPEGや透過PNG透過GIFはもちろんの事、SVGも書き出せるそうです。
個人的にはSVGが書き出せるのがうれしいですね。

詳しい設定などはAdobeのブログで解説されていますので是非参考にしてみてください。


方法2 選択したレイヤーを「書き出し形式」で書き出す。


1.書き出したいアイコンなどのパーツを移動ツール+shiftキーで全て選択します。

2.レイヤーパネルの上で右クリックして出てきたメニューから「書き出し形式」を選択します。

3.画像のサイズや拡張子を選択する画面になるのでお好みで設定を変えてください。

書き出し設定を全て終えたら、右下の全て書き出しボタンを押します。
保存するフォルダーを選択して開くボタンを押すと指定したフォルダー中に書き出されています。

この方法なら色々なサイズのアイコンを一気に書き出せるのでとても便利です。

ただ、この方法で自分が少し躓いたところが一つあります。
画像のサイズや拡張子を選択する画面で画像の解像度を変えたはずなのに、1ファイルのみその拡張子で書き出され、他の画像はもともとの拡張子のままで書き出されていました。
実は左の書き出す画像の一覧の部分で、選択した画像しか設定が反映されないようです。
書き出す画像全ての拡張子を変えたい場合はCtrlキーを押しながら全てクリックして選択してから拡張子を変えてから書き出すと全部同じ拡張子になってくれます。


どちらの機能も背景を消してスライスツールでスライスを引いて……という作業を省略できる素晴らしい機能なのですごい時短になります!
みなさんもぜひ使ってみてください。

2018年12月5日水曜日

C# foreachでListに要素を追加する方法。


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

最近業務でC#を使うことが多いのですが、個人的にはまだまだやり始めでわからないことが多く、ちょっとしたことで詰まる今日この頃なんですが、基本的なところで「こういう書き方もあるんだなー」とわかったことを今回は書きたいと思います。

■foreachでListに要素追加方法

C#のCollectionであるListクラスは配列っぽいイメージですが、動的に長さ(サイズ)を自由に変えられることと、データを途中に挿入できる点が配列との大きな違いとなっています。

で、例えばListや配列を回しながら、ある条件のとき等にListに要素を追加したいとき等がよくあると思いますが、最初自分は下記のように記述していました。
ちなみに下記は配列を回しながら、indexの値と配列の中身を新たなListに作っているだけの記述です。

public class animalList
{
    public int Seq { get; set; }
    public string AnimalName { get; set; }
}

public void TestMethod()
{
    string[] animals = new string[] { "lion", "zebra", "elephant" };
    var index = 0;

    List<animalList> result = new List<animalList>();
    animalList animalListdata = null;

    foreach (var name in animals)
    {
        animalListdata = new animalList
        {
            Seq = index,
            AnimalName = name
        };
        index++;
        result.Add(animalListdata);
    }
}

上記でも作りたい形になるのですが、ほんの少しだけ簡単に記述できる方法がありました。
※animalListクラスは同じものを使います。

public void TestMethod()
{
    string[] animals = new string[] { "lion", "zebra", "elephant" };
    var index = 0;

    var result = new List<animalList>();
    foreach (var name in animals)
    {
        result.Add(new animalList
        {
            Seq = index,
            AnimalName = name
        });
        index++;
    }
}

たった2~3行だけですが、回すと同時にListにAddしているので個人的にはこっちのほうがスッキリしているように見えます。

というわけで、C#をよくご存知の方にはそんなことは基本中の基本と怒られそうですが、まだ触れてから日の浅い自分にとってはちょっとしたことですが勉強になりました。

2018年11月15日木曜日

AngularのFormでのselect要素の使い方(selected指定の方法)


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

何だか分かり難い題名ですいません。良い題名が思いつきませんでした。

さて、やりたい事から説明しようと思います。

Angularで、Formに入力した値をPOSTする時、ちょっとやり方迷うのがselect要素です。
プルダウンってやつですね。

こんな感じのFormは良くあるかと思います。
初期表示時に先頭を選択状態にしておくのは簡単ですが、
データの修正を行う機能などは、「APIで取得した値を選択状態に」って事は良くあります。

例えば、このようなJSONがAPIから返却されるとします。
{"list": [
        {"api-data-status": 1},
        {"api-data-status": 2},
        {"api-data-status": 3}
 ]}

まずはプルダウンに表示する値(value)と名称(name)を定義します。
【constants.ts】
export namespace testDataStatus {
  export const STATUS_1 = { value: 1, name: 'ステータス1' };
  export const STATUS_2 = { value: 2, name: 'ステータス2' };
  export const STATUS_3 = { value: 3, name: 'ステータス3' };
}

続いて、コンポーネント側でプルダウン用のリスト定義します。
先程の定義した定数を使用しています。
【test-data-status.component.ts】
  testDataStatusOption: Array<{ status: number; name: string }> = [
    {
      status: Constants.testDataStatus.STATUS_1.value,
      name: Constants.testDataStatus.STATUS_1.name
    },
    {
      status: Constants.testDataStatus.STATUS_2.value,
      name: Constants.testDataStatus.STATUS_2.name
    },
    {
      status: Constants.testDataStatus.STATUS_3.value,
      name: Constants.testDataStatus.STATUS_3.name
    }
  ];

そして、最後はView(html)側ですが・・・下記のように記載すると、うまく行きません
【test-data-status.component.html】(NGな例)
<ng-container *ngFor="let item of statusList.list">
    <select class="form-control" formControlName="testDataStatus">
        <option *ngFor="let hoge of testDataStatusOption"
              [ngValue]="hoge.status"
              [selected]="hoge.status === item.api-data-status">
              {{report.name}}
        </option>
    </select>
</ng-container>
「statusList.list」がAPIで取得したデータです。
それを最初の「ngFor」で繰り返してデータ数分のプルダウンを作り、二番目の「ngFor」からプルダウンの値(value)と名称(name)を作り、APIの値と比較して「selected」に値を設定する・・・一見うまく行きそうなんですけどね。これではダメです。

という訳で、うまく行く書き方は以下の通りです。
【test-data-status.component.html】(OKな例)
<ng-container *ngFor="let item of statusList.list">
  <select class="form-control form-control-sm" [(ngModel)]="item.api-data-status" formControlName="testDataStatus">
    <option *ngFor="let hoge of testDataStatusOption"
        [ngValue]="hoge.status">
        {{hoge.name}}
    </option>
  </select>
</ng-container>
まあ、良く考えると当たり前なのですが、
「[(ngModel)]」に値を設定する事で、APIから取得した値で選択状態とする事が出来ます。

view(html)表示だけだと、そこまで難しくないのですが、FormControlを使うと少しややこしくなるので、自分の備忘録の為に記事にしました。

それでは、良いAngularライフを!


2018年11月2日金曜日

C#でプロキシ経由でJSONデータを外部APIにわたす。


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

今回も業務でちょっとハマったことについて、解決方法を書いていこうと思います。
今回はC#でプロキシ経由でAPIを呼び出す方法です。
いつもは社内のネットワーク内でAPIを呼び出していて、あるタイミングで社外のAPIを
呼び出すとなったときに、プロキシ設定しているため通常の呼び出し方ではリクエストが届かないという感じでちょっとハマりました。

まず下記のようなJSONを、あるAPIに送るとします。
{
    "id" : 1,
    "name" : "テスト"
}

プロキシ設定を考えずに書くと、HttpClientを使って下記のような感じでPOSTします。
var json = "{ \"id\" : 1, \"name\" : \"テスト\"  }";

using (var client = new HttpClient())
{
    string apiUrl = "http://hostname/testapi";
    HttpContent content = new StringContent(json, Encoding.UTF8, "application/json");
    HttpResponseMessage response = await client.PostAsync(apiUrl, content).Result;
}
※上記のJSON部分は一旦わかりやすいように書いていますが、実際にはJson.NETを使ってシリアライズしてやり取りします。

で、上記の状態でAPIをコールしてもプロキシに引っかかり、API側にリクエストが届きません。
ですので、上記に利用するプロキシ設定情報を付与してあげると外にでれます。
そのやり方が下記になります。
var json = "{ \"id\" : 1, \"name\" : \"テスト\" }";
var httpClientHandler = new HttpClientHandler
{
    Proxy = new WebProxy("http://localhost:8888", false),
    UseProxy = true
};
using (var client = new HttpClient(httpClientHandler))
{
    string apiUrl = "http://hostname/testapi";
    HttpContent content = new StringContent(json, Encoding.UTF8, "application/json");
    HttpResponseMessage response = await client.PostAsync(apiUrl, content).Result;
}
上記でわかるようにHttpClientHandlerクラスのProxyプロパティを使ってあげます。
これでこのプロキシ設定情報でリクエストされ、API側に届くようになります。

ちなみにこのやり方は認証ない場合で、認証が必要な場合は下記のようにHttpClientHandlerクラスに認証情報のCredentialsプロパティを追加するだけです。
var httpClientHandler = new HttpClientHandler
{
    Proxy = new WebProxy("http://localhost:8888", false),
    Credentials = new NetworkCredential(@"username", @"password");
    UseProxy = true
};

HttpClientHandlerクラスにはその他いろんなプロパティがあるので、HttpClientを使ったリクエストを行う際にはこちらも参考にしてみてください。
https://msdn.microsoft.com/ja-jp/library/system.net.http.httpclienthandler(v=vs.110).aspx

,

2018年10月4日木曜日

CSSでテキストの1文字目を大きく表示する。


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

先日、ドロップキャップをウェブサイトで表現する必要があったのですが調べるまで正しいやり方がわからなかったのでメモしたいと思います。
(テキストの1文字目のみを大きく表示する表現のことをドロップキャップって言うんですね。実は知らなかったです。)

最初大きくしたい1文字目の部分を<span>で囲んでclassで…と思ったのですが、実は「:first-letter」という擬似要素で簡単に実現できます。
いちいち<span>で囲まなくてもいいんです。
この擬似要素はブロックレベル要素の最初の文字にのみスタイルが適用されるので、ドロップキャップを実現できるのですね。


「p:first-letter」にfloat設定しp要素でfloat解除するといい感じになりました。


2018年10月1日月曜日

ESLint のエラー「Use object destructuring prefer-destructuring」に対応する。


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

Node.js + ESLint で開発を行っていると、良く出てくるエラーが、
「Use object destructuring prefer-destructuring」です。

直訳すると、「オブジェクトの非構造化の使用」なのですが、初見だと、何をどう修正すれば良いのか分からないんですよね・・・

例えば、以下のようなプログラムがあったとします。(あくまで例なので、意味の無いプログラムになっています)
  const response = await hogeService.getUser();
  if (response.id) {
    const userId = response.id;
  } else if (response.name) {
    const userName = response.name;
  }

「hogeService.getUser()」は、DBやAPIからデータを取得するイメージです。
「hogeService.getUser()」の戻り値の中には、「{id: 1234, name: "test"}」のような構造化されたデータが入っている事が前提で、そのデータと構成を「response」に設定している、という感じです。

これでESLintのチェックに掛けると、見事に
error Use object destructuring prefer-destructuring
が出ます。

「オブジェクトの非構造化の使用」を促されているので、問題なのは構造化されている「response」だと分かります。
(正確に言うと、構造化された後の使い方ですが)

と言う事で、
  const { id, name } = await hogeService.getUser();
  if (id) {
  } else if (name) {
  }

と直す事で、ESLintのエラーが出なくなります。
「const { id, name }」とする事で、構造化から解放されていますね。

初見だと分かり難いですが、
お決まりのパターンの修正なので、慣れるのが一番ですね。


,

2018年9月26日水曜日

Moment.jsでDeprecation warning(value provided is not in a recognized RFC2822 or ISO format)が発生した場合の対応方法(Angular,Pipe)


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

前回、AngularのPipeを使ってhtml(View)の表示を共通化する。と言う記事を書きましたが、実は実装をしている時に一つハマったことがあります。

最終的な実装の前に記載していたプログラムは以下の通りです。
import { Pipe, PipeTransform } from '@angular/core';
import * as moment from 'moment';

@Pipe({
  name: 'changeDateFormat'
})
export class ChangeDateFormatPipe implements PipeTransform {

  transform(value: string, args?: any): any {
    const postDate = moment(value).format('YYYYMMDD');
    const today = moment().format('YYYYMMDD');
    const yesterday = moment().subtract(1, 'days').format('YYYYMMDD');

    let displayDate: string = value;

    if (postDate === today) {
      displayDate = '今日';
    } else if (postDate === yesterday) {
      displayDate = '昨日';
    }

    return displayDate;
  }

}

上記で実行すると・・・・
Deprecation warning: value provided is not in a recognized RFC2822 or ISO format. moment construction falls back to js Date(), which is not reliable across all browsers and versions. Non RFC2822/ISO date formats are discouraged and will be removed in an upcoming major release. Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.
Arguments: 
[0] _isAMomentObject: true, _isUTC: false, _useUTC: false, _l: undefined, _i: 2018/09/01, _f: undefined, _strict: undefined, _locale: [object Object]
Error

とエラーになってしまいます。
momentに引き渡す日付は、ISO形式ではなくていけないようです。 ただ、そもそもの値が文字列なので、文字列操作でISO形式にフォーマットするのが面倒・・・ と言う事で、前回の実装では、
    const postDate = new Date(value);
    const postDateISO = moment(postDate.toISOString()).format('YYYYMMDD');
上記のように一度日付に戻してから、「toISOString()」を使うようにしました。
元々、トラブルの多い「new Date()」を使いたく無いので、momentにしたのですが・・・

と言う訳で、最終的な実装は、前回も記載した通り、
import { Pipe, PipeTransform } from '@angular/core';
import * as moment from 'moment';

@Pipe({
  name: 'changeDateFormat'
})
export class ChangeDateFormatPipe implements PipeTransform {

  transform(value: string, args?: any): any {
    const postDate = new Date(value);
    const postDateISO = moment(postDate.toISOString()).format('YYYYMMDD');
    const today = moment().format('YYYYMMDD');
    const yesterday = moment().subtract(1, 'days').format('YYYYMMDD');

    let displayDate: string = value;

    if (postDateISO === today) {
      displayDate = '今日';
    } else if (postDateISO === yesterday) {
      displayDate = '昨日';
    }

    return displayDate;
  }

}

となります。
上記実装を参考にされる方は、タイムゾーンに注意して下さいね。


,

2018年9月25日火曜日

AngularのPipeを使ってhtml(View)の表示を共通化する。


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

Angularのhtml(View)で変数を展開する場合、中括弧を二つ(double curly braces)で変数を囲みます。
<div>{{ targetDate }}</div>

さて、例えば、この「targetDate」変数に「2018/09/01」のような日付が設定されていて、
その日付が本日であれば『今日』、昨日だったら『昨日』と表示し、それ以外の時だけ、日付をそのまま表示するとします。
そんな時、どんな実現方法があるでしょうか?

「*ngIf」を使う?そもそもTypeScript側で判定しておく?色々とありますが、「*ngIf」を使うと煩雑になって、他に同じような表示箇所があったら、その度に記載する必要がありますし、APIから取得した値などは、html(View)側でリストのまま繰り返し処理を行う、
<ng-container *ngFor="let item of list">
  <div class="hoge">{{item.targetDate}}</div>
</ng-container>
という方法を使う事が多いので、TypeScript側で編集はしないんですよね。

まあ、『そんなのAPI側の責務でしょ』と言われたらそれまでですが、
表示内容に特化しているなら、フロントエンドでやるのも悪手では無いと思います。

さて、話が逸れてしまいましたが、こんな時に役に立つのが、AngularのPipe機能です。

簡単に言うと、htmlに表示する時に同時に変換処理も行い、表示内容を変えてしまう、と言う機能です。

早速使ってみましょう。
angular-cliを利用しているならば、
ng g pipe [path/name]
と言うコマンドでテンプレートを作成出来ます。

今回は、[path/name]の部分を pipe/change-date-format に変えます。
すると、以下のようなファイルが生成されます。
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'changeDateFormat'
})
export class ChangeDateFormatPipe implements PipeTransform {

  transform(value: string, args?: any): any {
    return null;
  }

}

この状態で、実装を行なっていきます。
今回、日付の編集・判定には、「moment」を使用します。
momentのインストールと使用方法は省きますが、実装結果は以下のようになります。
import { Pipe, PipeTransform } from '@angular/core';
import * as moment from 'moment';

@Pipe({
  name: 'changeDateFormat'
})
export class ChangeDateFormatPipe implements PipeTransform {

  transform(value: string, args?: any): any {
    const postDate = new Date(value);
    const postDateISO = moment(postDate.toISOString()).format('YYYYMMDD');
    const today = moment().format('YYYYMMDD');
    const yesterday = moment().subtract(1, 'days').format('YYYYMMDD');

    let displayDate: string = value;

    if (postDateISO === today) {
      displayDate = '今日';
    } else if (postDateISO === yesterday) {
      displayDate = '昨日';
    }

    return displayDate;
  }

}

処理は単純で、引数に送られてきた値を日付に変換、形式の統一、そして判定を行い、その日付が本日であれば『今日』、昨日だったら『昨日』、それ以外は日付をそのまま return しています。

Pipeの使い方は、
<div>{{ targetDate | changeDateFormat }}</div>
となります。@Pipe の name をvertical lineで変数に繋げて(Pipe)記載するだけです。簡単ですね。

大規模なプロジェクトになればなるほどPipeを上手く使うと、開発効率が上がります。
それでは、より良いAngularライフを!


2018年9月21日金曜日

Amazon Linux 2 にnginxをインストールする。(Amazon Linux Extrasを使う)


オフィス狛 技術部のKoma(Twitterアカウントの中の人&CEO)です。
今回は完全に自分用の備忘録です。
(毎回同じ問題にぶちあたり、同じ情報を調べるので・・・・)

さて、早速ですが、AWS EC2 のAmazon Linux 2 に、nginxをyumでインストールしようとすると・・・
$ sudo yum install -y nginx
読み込んだプラグイン:extras_suggestions, langpacks, priorities, update-motd
パッケージ nginx は利用できません。
エラー: 何もしません

nginx is available in Amazon Linux Extra topic "nginx1.12"

To use, run
# sudo amazon-linux-extras install nginx1.12


と、エラーになってしまいます。
・・・まあ、メッセージに解決策があるのですが、
念の為、説明しておくと、Amazon Linux 2には、
Amazon Linux Extras という仕組みがあって、良く使われるソフトウェアの最新バージョンについて、関連パッケージを含めて簡単にインストールすることが出来ます。
AWS自体が提供している仕組みだけあって、OSの安定性を保ったインストール・利用が可能になります。

実際にどんなものを提供しているのか見てみましょう。「amazon-linux-extras」というコマンドを実行してみます。
$ amazon-linux-extras
  0  ansible2                 available  [ =2.4.2 ]
  1  emacs                    available  [ =25.3 ]
  2  memcached1.5             available  [ =1.5.1 ]
  3  nginx1.12                available  [ =1.12.2 ]
  4  postgresql9.6            available  [ =9.6.6  =9.6.8 ]
  5  postgresql10             available  [ =10 ]
  6  python3                  available  [ =3.6.2 ]
  7  redis4.0                 available  [ =4.0.5  =4.0.10 ]
  8  R3.4                     available  [ =3.4.3 ]
  9  rust1                    available  \
        [ =1.22.1  =1.26.0  =1.26.1  =1.27.2 ]
 10  vim                      available  [ =8.0 ]
 11  golang1.9                available  [ =1.9.2 ]
 12  ruby2.4                  available  [ =2.4.2  =2.4.4 ]
 13  nano                     available  [ =2.9.1 ]
 14  php7.2                   available  [ =7.2.0  =7.2.4  =7.2.5 ]
 15  lamp-mariadb10.2-php7.2  available  \
        [ =10.2.10_7.2.0  =10.2.10_7.2.4  =10.2.10_7.2.5 ]
 16  libreoffice              available  [ =5.0.6.2_15 ]
 17  gimp                     available  [ =2.8.22 ]
 18  docker=latest            enabled    [ =17.12.1  =18.03.1 ]
 19  mate-desktop1.x          available  [ =1.19.0  =1.20.0 ]
 20  GraphicsMagick1.3        available  [ =1.3.29 ]
 21  tomcat8.5                available  [ =8.5.31 ]
DBやWebコンテナ、言語、フレームワークなど、色々ありますね。

さて、nginx1.12 が利用可能(available)になっているので、インストールしてみます。
sudo amazon-linux-extras install nginx1.12

これでインストール完了です。もう一度「amazon-linux-extras」というコマンドを実行してみます。
 $ amazon-linux-extras
  0  ansible2                 available  [ =2.4.2 ]
  1  emacs                    available  [ =25.3 ]
  2  memcached1.5             available  [ =1.5.1 ]
  3  nginx1.12=latest         enabled    [ =1.12.2 ]
  4  postgresql9.6            available  [ =9.6.6  =9.6.8 ]
  5  postgresql10             available  [ =10 ]
  6  python3                  available  [ =3.6.2 ]
  7  redis4.0                 available  [ =4.0.5  =4.0.10 ]
  8  R3.4                     available  [ =3.4.3 ]
  9  rust1                    available  \
        [ =1.22.1  =1.26.0  =1.26.1  =1.27.2 ]
 10  vim                      available  [ =8.0 ]
 11  golang1.9                available  [ =1.9.2 ]
 12  ruby2.4                  available  [ =2.4.2  =2.4.4 ]
 13  nano                     available  [ =2.9.1 ]
 14  php7.2                   available  [ =7.2.0  =7.2.4  =7.2.5 ]
 15  lamp-mariadb10.2-php7.2  available  \
        [ =10.2.10_7.2.0  =10.2.10_7.2.4  =10.2.10_7.2.5 ]
 16  libreoffice              available  [ =5.0.6.2_15 ]
 17  gimp                     available  [ =2.8.22 ]
 18  docker=latest            enabled    [ =17.12.1  =18.03.1 ]
 19  mate-desktop1.x          available  [ =1.19.0  =1.20.0 ]
 20  GraphicsMagick1.3        available  [ =1.3.29 ]
 21  tomcat8.5                available  [ =8.5.31 ]

nginx1.12 が使用出来る状態(enabled)になっています。

念の為、コマンドを実行してみます。
 $ nginx -v
nginx version: nginx/1.12.2

無事にインストール出来ているようです。

以上、本当に備忘録でした。


, , ,

2018年9月19日水曜日

knex.js の transaction でなぜか delete されないときに注目すること。


オフィス狛 技術部 CTOの Taka-yamです。

knex.jsは、Node.jsでActiveRecordを用いてSQLを発行するライブラリです。
fluentにSQLを構築でき、大変便利だなと感じる一方で、
トランザクション中にSELECT分を使用する際にちょっとした落とし穴があり注意が必要です。

例えば、booksというidとnameを保存するシンプルなテーブルがあるとして考えます。
id |  name  |
---+-------------+
 1 |  我輩は猫である|
 2 | こころ       |
 3 | 三四郎       | 

例えば、このテーブルのid1のデータを 削除->存在チェック->登録と実行します。
// knexの定義
const knex = require('../db/knex');

knex.transaction(trx => {
    
    // 全て削除
    knex.from('books').del();

     // 消えているか確認(存在チェック)
    const user = knex.from('books') . where('id', 1).first();

    if (typeof user !== 'undefined') {
        throw new Error(' データがあるよ')
    }

    // 登録
    const books = {id: 1, name: '銀河鉄道の夜'}
    knex.insert(books).into('books');
}

上記の メソッドは存在チェック後のifでエラーを投げます。

なぜか?

これは selectがクロージャ内に渡されているKnex.Transactionインスタンスを使用していない ため、
トランザクションとは別に処理を走らせてしまっているためです。

クロージャ内にいるので一見すると同じトランザクション として扱われるのかなと思ってしまいますが、実際には下記のようにしないといけません。
knex.transaction(trx => {
    
    // 全て削除
    trx.from('books').del();

     // 消えているか確認(存在チェック)
    const user = trx.from('books').where('id', 1).first();

    if (typeof user !== 'undefined') {
        throw new Error(' データがあるよ')
    }

    // 登録
    const books = {id: 1, name: '銀河鉄道の夜'}
    trx.insert(books).into('books');
}

selectに関しては下記のようにも 書けます。
const user = knex.transactioning(trx).from('books') . where('id', 1).first();

例えば処理を共通化して別メソッドとして切り出したりしていると、
思わぬところでトランザクションが途切れてしまっていることがあり、
「???」になることがあります。

また、個人的にですが、Realmなどのライブラリですと、
クロージャ内がトランザクション領域として扱われるので
勘違い して同じように使用してはまってしまったという経緯もありました。

もし同じような現象で困っている方がいらっしゃいましたら、
上記のような可能性も考えてみてください。

,

2018年9月3日月曜日

Illustratorでカラーバリエーションのあるようなアイコンやイラストをできるだけ楽に作る。


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

今回はカラーバリエーションのあるようなアイコンをできるだけ楽に作る方法をご紹介したいと思います。
アプリ内の画像などを作っている時に背景色によってアイコンの色が変わったり、状態によってアイコンカラーが変わったりするときの画像がすこーし簡単に作れるようになります。


Illustratorで描いたイラストです。
パーツが少し多いのでもうちょっと一部の色を明るくしたいなと思ったり、カラーバリエーションのある画像が欲しいと思うとちょっと大変ですよね。
こういう時に使える機能が「オブジェクトを再配色」機能です。


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

下のようなオブジェクトを再配色パレットが出てきます。

編集ボタンを押すと色相環のような画面になるので右下の「ハーモニーカラーをリンク」ボタンをクリックしておきます。(これをクリックするとまとめて色相や彩度が変わります)
色相環のような図の中の丸い色つきの丸を移動させると色相が変わるのでこれで一気に全体的な色が変えられます!

色を変えてみました。


一部だけ色が変えたい場合は「ハーモニーカラーをリンク」ボタンを押さずに変えたい部分の色のみの位置を動かしたり、指定画面(編集ボタンの横にある「指定」ボタンで画面が戻ります)から色を変えたりできます。

一部色を変えてみました。

簡単に絵のカラーパターンができました!

グラデーションで使われている色などもこの機能で変えられるので、カラーバリエーションのある画像がササッと数枚作れます。
使う機会が多そう&時短になる機能なので、ぜひ使ってみてください。


2018年8月30日木曜日

AngularでyoutubeAPI(Iflame Player API)を使ってyoutube動画を埋め込み表示する。

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

いろんなサイトでyoutube動画が見れるように工夫しているところが多いですが、今回はAngularでyoutubeAPI(Iflame Player API)を利用して、サイトにyoutube動画を埋め込み表示する方法を書いてみたいと思います。

■実装環境

・Angular
・ngx-youtube-playerモジュールを利用

今回使用するngx-youtube-playerモジュールはインストール済みとします。
まずモジュールのimportを記述します。
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { YoutubePlayerModule } from 'ngx-youtube-player';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppComponent } from './app';

@NgModule({
  imports: [BrowserModule, YoutubePlayerModule],
  declarations: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule {}

platformBrowserDynamic().bootstrapModule(AppModule);

次にyoutubeを埋め込みたい画面のファイルに下記のように記述します。
このあたりの記述はnpmjs.comのサンプルの記載とほぼ同じ感じです。
import { Component } from '@angular/core';
@Component(
  selector: 'app',
  template: `
        <youtube-player
      [videoId]="id"
      (ready)="savePlayer($event)"
      (change)="onStateChange($event)"
    ></youtube-player>
    `
})

export class AppComponent {
  player: YT.Player;
  private id: string = 'qDuKsiwS5xw';

  savePlayer(player) {
    this.player = player;
  }
}

これで基本的な下準備は完了で、
private id: string = 'qDuKsiwS5xw';
の部分で対象動画のyoutubeのidをセットすれば動画の埋め込みは完了です。
すごく簡単ですね!

で、カスタマイズも結構簡単にできますので、いくつか紹介したいと思います。

■動画が完了したタイミングで何か処理をする 

上記のcomponent.tsファイルに
  onStateChange(event) {
    if (event.data === 0) {
        // 動画終了してからの処理
    }
  }
のように追記して動画終了してから自動で何か処理をさせることもできます。
動画のステータスは
    -1 – 未開始
    0 – 終了
    1 – 再生中
    2 – 一時停止
    3 – バッファリング中
    5 – 頭出し済み
で判定することができますので、ステータスが変わったタイミングでなにかの処理を入れたりすることができます。

■プレイヤーのコントロールを別ボタンでやる

たとえば動画の再生や停止をyoutube内部のコントローラーではなく、プレイヤー外部にボタンを新たに設定してそこで制御したい場合などは下記のように追記するだけで実装可能です。
まずhtml側に下記のようなボタンを作成しておきます。
<button (click)="playVideo()" ><p>再生</p></button>

component.tsファイルに下記関数を設定します。
// 再生ボタン
playVideo() {
    this.player.playVideo();
}
これだけで制御が可能になります。
上記の要領で停止や音量の変更、ミュートやシークも簡単に制御ができます。

■動画終了後の表示のカスタマイズ

動画が終了すると関連の動画も勝手に表示されたりして、表示動画とは全く違うジャンルの動画リスト等もデフォルトで表示され、サイトによっては余計な情報も表示されてしまうので、この表示をいくつか消したいときがあると思います。
このあたりの表示内容についてもカスタマイズ可能です。
動画終了後の関連動画を非表示にする場合、component.tsファイルのtemplate部分を下記のように記述します。
@Component({
  selector: 'app',
  template: `
        <youtube-player
      [videoId]="id"
      (ready)="savePlayer($event)"
      (change)="onStateChange($event)"
      [playerVars]="{'rel': 0}"
    ></youtube-player>
    `
})

上記の[playerVars]="{'rel': 0}"という記述が関連動画を非表示にするパラメータになります。
上記以外にも、動画の再生が始まる前に動画のタイトルやアップロードしたユーザーなどの情報カスタマイズや、動画のプレーヤーコントロールを表示するかどうかを指定したりすることができます。

上記で紹介したのはほんの一部で、下記公式サイトにいろいろなカスタマイズのリファレンスやプレイヤーパラメータの詳細が載っていますので、ぜひ参考にしてみてください。

・Iflame Player APIリファレンス
https://developers.google.com/youtube/iframe_api_reference?hl=ja#Playback_controls
・youtubeプレイヤーパラメータ
https://developers.google.com/youtube/player_parameters?playerVersion=HTML5&hl=ja

,

2018年8月26日日曜日

AngularでChart.jsを使った時の「Failed to create chart: can't acquire context from the given item」に対応する。


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

Chart.jsは、『グラフと言えばChart.js』ぐらいに有名なライブラリです。
Abgularで使用する場合、npm install で簡単に取り込む事が出来ます。

インストールは本家のGitHub、 Angularで使う場合はこちらのサイト
に非常に詳しい説明があります。

さて、上記を参考にいざ使用してみると・・・・

ブラウザのコンソールに
Failed to create chart: can't acquire context from the given item
が出る事が分かりました。
グラフ自体は問題なく動いているんですけどね。

で、色々と試行錯誤した結果、問題ある箇所は、先のサイトで言うところの
<div *ngIf="chart">
  <canvas id="canvas">{{ chart }}</canvas>
</div>

である事が分かりました。これを・・・
<div [hidden]="chart === null">
  <canvas id="canvas">{{ chart }}</canvas>
</div>

多分、
[hidden]="!chart"
でも大丈夫だと思います。(と言うか、上記の方が良いかも)

要は、「*ngIf」を使ってしまうと、falseになった時に、レンダリングされないので、
chart.jsから「id="canvas"」という要素が見つけられないのでしょうね。

ちょっとハマったので、
同じようなエラーが出た方は、上記で回避してください。


,

2018年8月22日水曜日

windowやdocumentのイベントをAngularで使う。(HostListener)


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

Angularは、自コンポーネント内の項目についてのイベントは簡単に定義出来ます。

[View側]
<button class="button_close" type="button" (click)="close()">閉じる</button>

[Component側]
  close() {
    // ここに処理を記載
  }

しかし、自コンポーネント以外のイベントを取るとなると、ちょっと工夫が要ります。

例えば、下記のような要素があったとします。

「Company」リンクをクリックすれば、吹き出しが出るのは簡単に実装出来ますが、
『吹き出し以外をクリックしたら、吹き出しを閉じる』はどうやって実装したら良いでしょうか?

こんな時に、「HostListener」を使用します。
その名の通り、HostのイベントをListen出来ます。

import {
  Component,
  ChangeDetectionStrategy,
  OnInit,
  HostListener,
  ElementRef
} from '@angular/core';
import { Router } from '@angular/router';

@Component({
  selector: 'move-homepage-header',
  changeDetection: ChangeDetectionStrategy.OnPush,
  templateUrl: './homepage-header.component.html'
})
export class HompageHeaderComponent implements OnInit {

  isShownCompanyPopUp = false;

  constructor(
    private eRef: ElementRef,
    private router: Router
  ) {}

  ngOnInit() {}

  @HostListener('document:click', ['$event'])
  onClickOut(event) {
    if (!this.eRef.nativeElement.contains(event.target)) {
      this.isShownCompanyPopUp = false;
    }
  }

  showCompanyPopup() {
    this.isShownCompanyPopUp = !this.isShownCompanyPopUp;
  }

  linkCompanyInfo() {
   this.isShownCompanyPopUp = false;
    this.router.navigateByUrl('/home/companyinfo');
  }

  linkCompanyMap() {
   this.isShownCompanyPopUp = false;
    this.router.navigateByUrl('/home/companymap');
  }

  linkPrivacyPolicy() {
   this.isShownCompanyPopUp = false;
    this.router.navigateByUrl('/home/privacypolicy');
  }
}

HostListenerのimportは忘れないように記載して下さい。
さて、肝心の部分を抜粋すると・・・・
  @HostListener('document:click', ['$event'])
  onClickOut(event) {
    if (!this.eRef.nativeElement.contains(event.target)) {
      this.isShownCompanyPopUp = false;
    }
  }

今回は、「documentのclickイベント」を使用しています。
ただ、これだと、吹き出しの中のクリックイベントまで対象になってしまうので、
自コンポーネント内は除外し、『自コンポーネント内以外をクリック』のイベントを取る事を実現しています。

いかがでしょうか?
この方法で、windowのresizeイベントも取れると思います。


2018年8月20日月曜日

タブの選択状態をAngularで簡単に実装する。


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

画面上のタブに選択状態を表示する・・・下図のような感じの事をやりたい時があります。


Angularなので、Component側でフラグ(変数)を持って、それをView側で判断・・・という事を思い付きますが、もっと簡単な方法があります。

    <div class="header-nav">
      <ul class="header-nav-tabs">
        <li class="nav-tab" routerLinkActive="nav-tab_active"><a class="nav-link" routerLink="/home/test1">テストタブ1</a></li>
        <li class="nav-tab" routerLinkActive="nav-tab_active"><a class="nav-link" routerLink="/home/test2">テストタブ2</a></li>
      </ul>
    </div>

上記のように「routerLinkActive」と「routerLink」の組み合わせで実現出来ます。

routerLink」は該当のhtml要素(今回の場合は「a」タグ)をクリックした際の遷移先を指定します。

routerLinkActive」は、『現在表示している画面(URL)が「routerLink」で指定されているものと一致した場合、指定したクラスを適用する』
という動きをします。

今回の場合は、「nav-tab_active」というCSSクラスに選択状態のスタイルを記述すれば良いわけです。

これでViewでの「*ngIf」地獄から少しは解放されます。


2018年8月6日月曜日

Xcodeでよく使うキーボードショートカット一覧


オフィス狛 技術部 CTOの Taka-yamです。

Xcodeを使用していると複数画面でコードを表示したり、
画面やメニュー間を移動すること多いと思いますが、
その時にキーボードショートカットを知っているとだいぶ時間短縮になり、
作業が楽になります。(と個人的には思っています。)


今回は普段よく使いそうなキーボードショートカットをまとめてみました。

パッと見られるように画像にして印刷できるようにしています。

チートシート的な役割としてお役に立てれば幸いです。

ショートカットを駆使してトラックパッドレス、マウスレスなXcodeライフを!

2018年8月1日水曜日

CSSで背景画像のみぼかして表示したりする際のメモ。

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

iOSのパスコード入力時やWebサイトの背景などでよく見る「背景画像が全体的に表示されていてぼかしがかかっている」状態をCSSで再現したかった際にうまくいかなかったりした所があったのでここに記載しておきます。

最初、画像をcssでぼかす事が「filterという画像の見た目を変えることができるクラスで実装できたはず」という知識はぼんやりあったのでとりあえず親要素に「filter: blur(5px);」を指定してみました。
予想はできていましたが、囲った要素全部にぼかしがかかってるし、なぜか画面の端に滲みが出てかっこ悪くなってしまいました。
これが最初の状態です。中央の白い丸の中は完全に読めなくなっていますね……。


調べた所、全体的にぼかしがかかるのも白い滲みもリストの最初に記号を置いたりする時によく使う「::before」という擬似要素で解決できるそうです。
参考にさせて頂いたサイト
背景を画面いっぱいに表示するためのclassで、参考サイトに記載されている背景を綺麗にぼかすためのCSSを囲ってみた所うまくいきました。


背景を画面いっぱいに表示するための部分とぼかすための部分を分けて指定しないと、画像が表示されている部分のみで画面の高さを計算してしまうようで、フッターの部分が下がってしまいスクロールしないと見えなくなってしまうので気をつけてください。

2018年7月31日火曜日

multer-s3を使ってaws S3へファイルアップロードする時の注意点(Node.js)


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

今回もまた開発をしていく最中にちょっとハマったことについて書こうと思います。
今回は「ファイルアップロード」についてです。
よくあるファイルアップロードの機能ですが、ある条件のときにちょっとしたことなんだけどうまくいかずにハマりました。
まずファイルアップロード機能実装でやろうとしたことと、その環境をザックリ簡単に記します。

■やりたいこと

・AWS S3に動画ファイルをアップロード
・アップロードしたファイルをwebサイトで視聴できるようにする
・バックエンドのnode.jsにS3アップロードのAPIを作成する

■実装環境

・node.js(Express)
・multer-s3モジュールを利用

とりあえずS3にファイルアップロードするためのaws-sdkをインストールしたり、アップロード用のモジュールをもろもろインストールしますが、今回のハマったところはこの先なのでこの辺のインストール方法等は省略します。

さて、上記環境は整っている状態で実際にアップロード処理を作成します。
アップロード処理について今回は「multer-s3」というモジュールをつかって下記のように処理を書きました。 npmjs.comのサンプルの記載とほぼ同じ感じです。

■S3アップロード処理サンプル

var aws = require('aws-sdk')
var express = require('express')
var multer = require('multer')
var multerS3 = require('multer-s3')

var app = express()
var s3 = new aws.S3({ /* ... */ })

var upload = multer({
  storage: multerS3({
    s3: s3,
    bucket: 'some-bucket',
    metadata: function (req, file, cb) {
      cb(null, {fieldName: file.fieldname});
    },
    key: function (req, file, cb) {
      cb(null, ${req.body.fileName})
    }
  })
})

app.post('/upload', upload.array('photos', 3), function(req, res, next) {
  res.send('Successfully uploaded ' + req.files.length + ' files!')
})
上記をroutesディレクトリに任意のファイルを作成して記述します。

ここまでで基本的なアップロード機能は完成です。
で、実際に動かしてみます。今回アップロードしたときにそのファイル名とファイルIDも使いたかったので、requestのbodyにファイルとファイル名、ファイルのIDもつけて上記のAPIになげます。
動作の確認方法はいろいろあると思いますが、個人的にPostmanというツールを使ったほうが楽なのでこちらを使います。

■postmanでリクエスト作成

上記のような感じでリクエストを作成します。
  ①POSTを選択しAPIのURLを入力
  ②ファイルをアップロードするので「form-data」を選択
  ③アップロードするファイルと同時にリクエストするフィールド(key、value)を入力
  ④右上のsendボタンを押下
これで先ほど作成したAPIにリクエストが送られ、ファイルがアップロードされます。

で、ここからがハマったポイント
S3にファイルが上がっているか確認したところファイルは問題なく上がっています。ただ、なぜか②で一緒にbodyに記載していたファイル名やファイルIDがとれていない。そのためS3にアップロードしたファイル名が「undefined」となっています。アップロード自体はエラーとなっていませんでした。

    key: function (req, file, cb) {
      cb(null, ${req.body.fileName})
    }
上記のfileNameの部分でS3に上がったときのファイル名となる予定でしたが、なぜかfileNameがうまく取得できていないようです。
おかしい、同じbodyに入力していて、ファイルはアップロードできているのにファイル名がとれないなんて。。。

いろいろ調べてやってみても原因がわからず。。。
そこで最初のファイルがうまくいっているなら1番目の項目だけうまくいくのかと思い順番を下記のようにかえて再度アップロード。

なんと、ファイル名取れました!
ファイル自体もアップロードできている!
1番目も2番目の項目もうまくっているので1番目の項目だけってことではなさそうだけど、あれ?こんどはファイルIDが取れていない。
ということは、アップロードファイルより上の項目は取得できて、アップロードファイルより下の項目は取得できていないようです。

再度bodyの順番を下記のように並び替えてアップロードしてみます。
うまくいきました!
今度はファイル名、ファイルIDもちゃんと取れていました。

ということで、ファイルアップロード時のrequestのbodyはアップロードファイルの順番を最後にしておかないと、その他の項目が取得できないということがわかりました。

恐らく「multer-s3」というモジュールの特性や、記述の仕方もあるかもしれませんが、「multer-s3」でS3にアップロードするという実装に関しては上記のようなことが発生しますので注意が必要です。

またほかのnpmモジュールを使う際も少し注意したほうがよさそうで、「busboy」というモジュールも処理の組み方次第では同様の事象に注意が必要なようです。
multipartのアップロードストリームをYoutube投稿ストリームに受け流す

, ,

2018年7月8日日曜日

SVGの色をCSS上で変えてしまう方法


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

先日、アイコンをSVGで作成した際に、マウスオーバーすると色が変わったりするようなことがしたいなぁ……と思うことがありました。
普段なら商用フリーでCDNのアイコンフォントをアイコンにしてしまったりするのですが、今回は少し特殊なアイコンでそれもできず……。
SVGの色をテキストのようにCSSで変えたりできないものかと思って調べたらできる様子!
試してみた所、なんとか自分でもできたのでやり方をメモしたいと思います。

やり方は簡単。<svg>か親要素のCSSにfill: カラーコード;を追加するだけです。
ちなみに値を「currentColor」にするとなんとcolor設定されている色になります。
HTML
<a href="#"><svg>(本来はここにインラインで記載)</svg>Button</a>
CSS
a{color: #3ab28f;}
a:active{color: #3ddaa3;}
svg{fill: currentColor;}
こんな感じですね。
ただし、<img>タグや<object>タグではこの方法では色を読み込んでくれないようです。
最初自分はそこで躓いたのでご注意ください。

正直、自分は今までSVGのことを「どんなに拡大しても画像が荒れないすごい画像形式」くらいにしか思っていなかったのですがこんな事もできるんですね!びっくりしました。


,

2018年7月2日月曜日

Amazon S3 で サイト公開(HTTPS)する。(3)問い合わせフォーム準備編その2


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

前回は、Webサイトの問い合わせフォームから問い合わせされた内容をS3バケットへテキスト形式で保存するところまで行いました。
今回は、S3に保存されたら、その内容をメールで送信したいと思います。

※メールを送信する為のAWS SES(Simple Email Service)については説明を省きます。SESの設定が終わった状態で下記の手順を行って下さい。

1)ポリシー(Policy)の作成

まずは、AWSコンソール上のIAMのページへ遷移し、左側のメニューから「Policies」を選択。
その後、左上の「Create policy」をクリックします。


Serviceは、「CloudWatch Logs」
Actionは、「CreateLogStream」、「CreateLogGroup」、「PutLogEvents」
Resourceは「all resources」を選択して、
右下の「Review policy」をクリックします。


最後にNameとDescriptionを入力し、「Create policy」を選択します。


作成に成功しました。


2)ロール(Role)の作成

続いてロールを作成していきます。先程と同じくAWSコンソール上のIAMのページへ遷移し、
左側のメニューから「Roles」を選択します。


Select type of trusted entityは「AWS service」を選択。
Choose the service that will use this roleは「Lambda」を選択して、
右下の「Next: Permissions」をクリックします。


まずはFilterで「Policy type」を選択します。
続いてAttach permissions policiesでは「AmazonSESFullAccess」をチェックします。
(検索ボックスで「SES」と入力すると、フィルターが掛かって選択が楽です)


次にFilterで「Customer managed」を選択し、先程作成したポリシーにチェックを入れます。
(検索ボックスの中身を消さないと一覧に出て来ないのでご注意を)
右下の「Next: Review」をクリックします。


最後、Role nameを入力して、右下の「Create role」をクリックします。


作成に成功しました。


3)Lambda Functionの作成

AWSコンソール上のLambdaのページへ遷移し、「Create a function」を選択。


「Author from scratch」を選択し、Name等の必要な情報を入力します。
Runtimeは「Node.js」を選択して下さい。
Roleは「Choose an existing role」を選択し、Existing roleは、先程作成したロールを選択します。


作成直後の状態です。
ここで、左側の「Add triggers」から、「S3」を選択します。


対象のバケットを選択し、Event typeは「PUT」を選択します。 「Add」をクリックします。

続いて、Node.jsのプログラムを貼り付けます。


貼り付けるプログラムは以下の通りです。
'use strict';

console.log('Loading function');

const aws = require('aws-sdk');
const s3 = new aws.S3({ apiVersion: '2006-03-01' });
const ses = new aws.SES({ apiVersion: '2010-12-01', region: 'us-east-1' });

exports.handler = (event, context, callback) => {
    const bucket = event.Records[0].s3.bucket.name;
    const key = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, ' '));
    const params = {
        Bucket: bucket,
        Key: key
    };

    s3.getObject(params, (err, data) => {
        if (err) {
            console.log(err);
            const message = `Error getting object ${key} from bucket ${bucket}. Make sure they exist and your bucket is in the same region as this function.`;
            console.log(message);
            callback(message);
        } else {
            const json = JSON.parse(data.Body.toString());
            const message = `
                名前: ${json.name}           
                MAIL: ${json.email}
                問い合わせ日時:
                ${json.date}
                問い合わせ内容:
                ${json.comments}
                `
            const params = {
                Destination: {
                    ToAddresses: [process.env.TOADDRESS]
                },
                Message: {
                    Body: {
                        Text: {
                            Charset: "UTF-8",
                            Data: message
                        }
                    },
                    Subject: {
                        Charset: "UTF-8",
                        Data: "フォームからのお問い合わせ"
                    }
                },
                Source: process.env.SOURCEADDRESS
            };
            ses.sendEmail(params, function (err, data) {
                if (err) console.log(err, err.stack);
                else console.log(data);
            });
        }
    });
};

プログラムに環境変数を使っているので、値を設定します。
SOURCEADDRESSが送信元(=from)アドレスで、
TOADDRESSが送信先(=to)アドレスです。


以上でLambdaの設定が完了となります。
Lambda作成後、前回作成した問い合わせフォームから、問い合わせを行うと・・・
この後、S3側のトリガー設定などが残っているのですが、以降は、独自ドメインの設定を行った後にやって行こうと思います。
と言う事で、WebサイトへのアクセスはS3のURLを利用しているので、 次回はRoute53を使用して、独自ドメインでの運用を可能にしていきたいと思います。

次回:Amazon S3 で サイト公開(HTTPS)する。(4)Route 53を使用した独自ドメイン運用
前回:Amazon S3 で サイト公開(HTTPS)する。(3)問い合わせフォーム準備編その1


, , ,