CSS調整

2018年8月30日

アイコンのCSSアニメーション用便利サイト

CSSアニメーション

難しいですよね。

フェードやスライドから始まり、アイコンひとつでシンプルに処理内容を表したり、それを特定タイミングでユーザーに注目させるために、ピコピコ動かしたり、マウスオーバーで色反転したり。

少し前に比べて、簡素なサイトやアプリであってもその程度は当たり前になってきていますね。

でも、そんなインタラクティブな動き、デザイン素人にはちょっと辛いのですよ。

AfterEffectとかをバリバリ使いこなせるわけでもなく、かといってKeyFrameのパーセンテージを自力で計算するなんて 変態じみた 頭の良いことできないわけですよ。

そんな訳で最近お世話になったのが、以下のサイト


WAIT! Animate




EXAMPLEを使うだけでもアイコンに対する面白い動きを再現させることができ、CUSTOMのKeyFrames欄へKeyFrame定義をコピペすれば更に細かく変更もできます。
あとは出来上がったCSSを利用するだけ。シンプル!

出来上がったものを見て思うのは、こんな細かいパーセンテージの値、やっぱり自力で計算するなんて、やっぱり 変態 頭の良い人にしかできません・・・


サイトデザイン自体がそもそも使い勝手がわかりやすいのもステキですね。

2018年8月25日

JPAのConverterをkotlinのdelegateで代用する

Spring BootでJPA機能のひとつConverterというのがあります。
DB値 -> EntityとEntity -> DBで値を変換する為の例のあれです。

よく例として上げられる(上げやすい)のがintとboolの変換とかですね。
Entityとしてはboolで扱っておいて、DB上はintで1/0で保存する、みたいな。

このConverter自体は深く掘り下げたりはしませんが、正直、なんかうまく動かないことが多いイメージが強いです。
まあ、Entityのインスタンス生成前に挟まれるであろう処理なので、利用しているデータストアの種類やHibernate等のアクセサに深く依存してそう、というか特定ケースでのHibernateでは動作しないようなissueも上がってたような気がします。

そんなこんなで、あまり信用していないのと、ちょっとレイヤーが変わるかもだけれどEntityのfieldにdelegate貼ったら代用できるよね、と思い、試してみました。

Converterをdelegateで代用してみる


Entityを用意


@Entity
@Table
class Memo {
    @Column("title")
    var title = ""
    @Column
    var value = ""
}

特に特筆する点はないかと。

Interfaceを用意してEntityに貼る


interface TitleHaving {
    var _title: String
}

class Memo: TitleHaving {
    @Column("title")
    override var _title = ""

タイトルを持ってるよー、のインターフェースです。
Entityへはtitleを_titleへ変更して実装。

delegateを用意


class TitleQuestionConverter: ReadWriteProperty<TitleHaving, String> {
    override fun setValue(thisRef: TitleHaving, property: KProperty<*>, value: String) { thisRef._title = value }
    override fun getValue(thisRef: TitleHaving, property: KProperty<*>): String = "${thisRef._title}?"
}

タイトル文言を疑問文にしてしまうというどうでもいいdelegateにしました。

Entityへ入れる


class Memo: TitleHaving {
    @Column("title")
    override var _title = ""
    @Column
    var value = ""

    @delegate:Transient
    var title by TitleQuestionConverter()
}

delegateするfield名をtitleとして、あたかも利用する側には@Columnがついてるfieldのように見せかけます。
その実は_titleの値を変換して取得、設定された場合にはそのまま_titleへ格納、@Columnが指定されている_title側はsaveでそのまま保存され、
対する自身は@delegate:Transientで、delegate先に@Transientを指定することで永続化対象ではなくします。

getValueで対象fieldへアクセスされてしまうので、LAZYが指定されたアソシエーション列等の場合はLAZYの意味が無くなったりしますが、簡単な変換なら十分作用すると思います。

いじょ

2018年8月24日

Spring Bootの依存ライブラリのバージョンだけ上げる

Spring Bootの依存ライブラリのバージョンだけを上げる為のメモ。

Spring Bootのライブラリ

Spring Bootでは通常、

・Mavenではspring-boot-starter-parent(が読み込むspring-boot-dependencies)
・Gradleではdependency-managementプラグイン

を利用することで、
例えばjpaが使いたいなあ、と思ったときにspring-boot-starter-data-jpaを読み込むだけで、整合性の取れたバージョンのjpa-core.jarを取ってきてくれる。

逆に言えば、親リリースに含まれないようなbugfix版が欲しい、とか思っても、バージョンBOMを指定するだけでは上げることができなかったりする。

もちろんそこには適当なバージョンの組み合わせじゃコンパイルすら通らないかもしれない、といった理由もあるのでしょうが・・・。

上の依存管理ライブラリを使わなければいい、と思っても入り組んだプロジェクトだとそうもいかないし、全ライブラリを自分で読み込むというのはSpring Bootでは中々難儀。

というところまでが前書きで、実はこの辺はある程度融通がきく仕組みがちゃんと用意されている。

サードパーティライブラリ


サードパーティのライブラリ(hibernate等)はspring-boot-dependenciesのpomのproertiesにバージョンが列挙されている。
この変数値を変更するだけで指定バージョンのものを読み込んでくれる、という具合。

例えば、hibernateであれば

hibernate.version

で定義されているので、同じように自分のプロジェクトのpomに書いて上げればいい。
Gradleの場合は、
ext['hibernate.version'] = "バージョン"
でいける。

Springのライブラリ


Springのライブラリ(spring-data等)も同じようにいけそうに見えるが、Gradleの場合はうまくいかないかもしれない。
例えばspring-dataの場合はspring-data-releasetrainなのだが、Gradleの場合は以下のほうが確実。

dependencyManagement {
    imports {
        mavenBom "org.springframework.data:spring-data-releasetrain:バージョン"
    }
}

ちなみに、どんなバージョンがあるの?とか、どのバージョンのspring-dataに、どのバージョンのjpaが入るの?といったことは、そのものずばりのMaven Repository、
上ならspring-data-releasetrainのリポジトリとpomを見ればいいと思う。

補足:

Spring Cloudのライブラリ


Spring Cloudのライブラリはspring-cloud-dependenciesで管理されていて、考え方は一緒。

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:バージョン"
    }
}

ちなみに、このpomを見ると個々のspring-cloud-xxxのバージョンが指定できそうな雰囲気・・・
だけど試したことはないです。。。

いじょ

2018年8月15日

AngularのRouteReuseStrategyを実装する

AngularのRouteReuseStrategyを実装してみます。

RouteReuseStrategyとは

その名の通り、RouteをReuseする為のStrategyになります。(おい)
例えば、/hogeでHogeComponentが描画されたとして、そこから、/fugaへ画面遷移してFugaComponentが描画され、その後、ブラウザバック等で/hogeに戻りました、というときに、HogeComponentを画面遷移前のまま表示したりできます。
要するにキャッシュのような仕組みということですね。
(実際はAndroidのFragmentManagerのaddに近いのかな?)

パッと思いつくユースケースは、一覧 → 詳細 → 一覧へ戻る、とかでページング状態等を保持するとかでしょうか。

RouteReuseStrategyの実装


まずは、RouteReuseStrategyをimplします。

export class MyRouteReuseStrategy implements RouteReuseStrategy {}

Moduleにprovideも必要です。

providers:[
    { provide: RouteReuseStrategy, useClass: MyRouteReuseStrategy }
]

共通処理の実装


先に共通処理を実装します。

private storedHandles: { [path: string]: DetachedRouteHandle } = {};

private getPath(route: ActivatedRouteSnapshot) {
    let url = route.pathFromRoot.filter(r => r.url.length > 0).map(r => r.url.map(u => u.path).join("/")).join("/");
    return url.length > 0? url: null;
}

private isReuse(route: ActivatedRouteSnapshot) {
    return route.routeConfig && !route.routeConfig.loadChildren &&
           route.routeConfig.data && route.routeConfig.data.reuse;
}

それぞれ、

storedHandlesが、Componentのキャッシュに相当するものを保持しておく入れ物になります。

getPathが、routeからpathの文字列を取得する処理で、取得したpathはstoredHandlesの配列キーになります。

isReuseが、reuse対象かどうかの判定処理で、

・loadChildrenなrouteでないこと=そのrouteがmoduleを跨がないこと
・後述のroute.dataにreuseが指定されていること

としました。

RouteReuseStrategyで実装が必要なメソッドは5つ

shouldDetach、storeの実装


shouldDetach(route: ActivatedRouteSnapshot): boolean {
    return this.isReuse(route);
}

store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
    if (this.isReuse(route)) {
        let path = this.getPath(route);
        if (path) this.storedHandles[path] = handle;
    }
}

shouldDetachは、そのrouteを残すかどうかを判定します。trueを返した場合、storeが呼ばれます。
storeは、実際に保持する処理になります。渡されてくるhandleをpath単位で保持する形としました。

shouldAttach、retrieveの実装


shouldAttach(route: ActivatedRouteSnapshot): boolean {
    if (this.isReuse(route)) {
        let path = this.getPath(route);
        if (path) return !!this.storedHandles[path];
    }
    return false;
}
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
    if (this.isReuse(route)) {
        let path = this.getPath(route);
        if (path) return this.storedHandles[path] || null;
    }
    return null;
}

shouldAttachは、保持されているものを使うかどうか判定します。trueを返した場合、retrieveが呼ばれます。
retrieveは、実際に保持されているものを渡す処理になります。pathを利用して取得できたら返す形としました。ちなみに、nullを返した場合は通常どおりに新しいComponentが作成されます。

shouldReuseRouteの実装


shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
    return future.routeConfig === curr.routeConfig;
}

shouldReuseRouteはreuseする対象かどうかの判定に使います。
futureとcurrはそれぞれ今と次のRouteで、同じrouteであることを条件とすることにしました。

これで一応の機構はできました。
Routesの定義時に上のisReuseで取得しているdata.reuseを設定したrouteが、対象になります。

Routesの定義


const HogeRoutes: Routes = [
  { path: '', component: HogeList, data: { reuse: true } },
  { path: ':id', component: HogeEdit }
];

この場合、HogeList(一覧)とHogeEdit(編集)画面のうち、一覧画面のみ保持され、編集画面から戻ってもHogeListのローカル変数等はそのままになります。
当然ですが、編集画面は:idで都度表示内容が変更される画面を想定しているので、reuseするのは望ましくないです。

さて、これでHogeListは使い回されることになるわけですが、この状態では、例えば、編集画面で項目が削除されたり作成されたとしても、一覧へ反映されないことになります。
もちろんお互いのComponentで同じデータの変更をsubscribeしているような造りであれば問題は無いのですが、それでなくとも、任意にキャッシュしているComponentを再生成したいときも出てきます。

retrieveがnullを返せばいいので、storedHandlesから消す処理があれば、実現できそうです。

refreshの実装


refresh(route: ActivatedRouteSnapshot) {
    let path = this.getPath(route);
    if (path && this.storedHandles[path]) this.storedHandles[path] = null;
}

これをHogeEditでSaveが呼ばれた場合等に以下のように呼び出します。

class HogeEdit {
    constructor(route: ActivatedRoute, router: Router) {}
    
    afterSave() {
        (this.router.routeReuseStrategy as MyRouteReuseStrategy).refresh(this.route.parent.snapshot);
    }
}

RouteReuseStrategyはRouterが持っているので、そのままアクセスできます。
refreshに渡すのは保持されているかもしれないrouteのsnapshotなので、今回はparentから取っています。
これで、afterSaveを通った後にHogeListへ戻った場合に限り、Componentが使い回されなくなりました。

以下、クラス全体です。
最低限は満たせましたが、実際はもう少し調整が必要になるケースが多いかと思います。

export class MyRouteReuseStrategy implements RouteReuseStrategy {
    private storedHandles: { [path: string]: DetachedRouteHandle } = {};

    private getPath(route: ActivatedRouteSnapshot) {
        let url = RouteUtil.getPathFromRoot(route);
        return url.length > 0? url: null;
    }

    private isReuse(route: ActivatedRouteSnapshot) {
        return route.routeConfig && !route.routeConfig.loadChildren &&
               route.routeConfig.data && route.routeConfig.data.reuse;
    }

    shouldDetach(route: ActivatedRouteSnapshot): boolean {
        return this.isReuse(route);
    }
    store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
        if (this.isReuse(route)) {
            let path = this.getPath(route);
            if (path) this.storedHandles[path] = handle;
        }
    }

    shouldAttach(route: ActivatedRouteSnapshot): boolean {
        if (this.isReuse(route)) {
            let path = this.getPath(route);
            if (path) return !!this.storedHandles[path];
        }
        return false;
    }
    retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
        if (this.isReuse(route)) {
            let path = this.getPath(route);
            if (path) return this.storedHandles[path] || null;
        }
        return null;
    }

    shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
        return future.routeConfig === curr.routeConfig;
    }

    refresh(route: ActivatedRouteSnapshot) {
        let path = this.getPath(route);
        if (path && this.storedHandles[path]) this.storedHandles[path] = null;
    }
}

いじょ

2018年8月13日

トミカ博 2018 in 横浜に行ってきました

人生初のトミカ博!トミカ博 in YOKOHAMA 2018


パシフィコ横浜の3日目の土曜日です。
世の大体のお父さん方の休みと重なる最初の日なので、混雑の不安がありました。

まずは駐車場


開場1時間程前に到着しました。
地下1階は満員で、大丈夫かな?と思いましたが、地下2階はまだまだガラガラでした。
会場の方は結構な列でしたが、少し早く開場したらしく、私達はそれほど待ち時間なく、ほどなく列が流れ出しました。

最初に行くべきは?


開場直後は、当然ながら人はまばらですが、そんな中でも列がどんどん膨らんでいっていたのが、

・入口付近の記念写真エリア
・組み立て工場
・ドライバー工房

でした。

このうち、特に組み立て工場とドライバー工房は他と比べても回転が遅いであろうことは容易に想像できるので、考えることは皆同じ、ということでしょうか。
ご多分に漏れず、私達家族も真っ先にドライバー工房に並びました。
とはいえ、プレイチケットの入手が最優先なので、奥さんに並んでもらいつつ、私がプレイチケットの購入や、ついでに空いているうちにマーケットブースに行って限定トミカの購入をしたりしました。
この時点でのドライバー工房の待ち時間は50分と記載されていました。ですが、実際は40分弱程度だったかと。

10:30のイベントが見たかったので、ドライバー工房が終わると同時にイベントの広場のところへ。
ここも既に場所取りしている方がいましたが、座って見れる場所をキープできました。もう少し遅かったら後ろでの立ち見になったかと思います。

そこからはもう、どこのミニゲームも似たり寄ったりで1時間弱の待ち時間のようでした。

そしてお昼~午後


お昼は会場出口を出てすぐの所に移動販売がいくつか。ただし、外なのでかなりの暑さです。
また、いくつかのイスや座れる場所がありますが、圧倒的に数が足りていません。。。
飲み物等の値段もお祭り価格なので、会場入口付近の自販機、また、会場の2階にコンビニ等があるので、それを利用するのも手かと。
あと、再入場する為には出る時にチケットに判を押してもらう必要があり、かつ、出口からしか再入場はできません。
つまり、入口側にしかない自販機や喫煙スペース、エスカレーターやエレベーターを利用する場合、出口から出て、入口まで行って、再度、出口から入る、ということになります。ちと面倒です。

このあたりから、当日券で入ってくる人が増えますが(当日券売り場に列ができてた)、疲れたのか帰る人も多いようで、だいたいのブースが40分前後で利用できるようになります。
(ドライバー工房と、組み立て工場の一番人気(ランボルギーニだったかな?)だけは常に60分超えでしたが・・・)
ミニゲーム系を一通り回りたい場合は、いい時間帯かと。

ちなみにマーケットブースはレジの量が多いので、並ぶということはほぼ無いように見受けられます。
私達も3回ほど買い物をしましたが、すべてレジ待ちはありませんでした。
また、レジの裏手にはガチャガチャが並んでおり、限定カプセルトミカ等もありました。

最後に


私達は入口近辺を無視してミニゲーム等へ行ったので、帰り間際に記念写真を撮りに戻ったりしました。
大きなトミカ箱のある写真撮影場所(スタッフが撮ってくれる)だけは常に列ができていましたが、セルフで撮影するようなところは、サクサク流れますね。

というわけで、パシフィコ横浜自体の土地勘が無かったので、若干の無駄はありましたが、結構うまいこと回れたのではないかなと思います。

以上、ご参考程度に。

2018年8月2日

ng-packagrとnpm publishまで

Angular6でng-packagrが標準搭載(?)されたらしいので、せっかくなので色々絡めて試してみようかと思います

やりたかったこと
  • ng-packagrを利用してAngular用の外部モジュールを生成
  • モジュールのnpmでの公開
  • issueやソースの管理はgithubで

ng-packagrを利用してAngular用の外部モジュールを生成


作るものは何でもいいのですが、できるだけ簡単で、かつ、今自分が作っている他プロジェクトでも多少役に立つものにしたいなあ、ということで、
所謂、Scroll-SpyをメニューとコンテンツにDirectiveを貼るだけで実現するようなのにしようかと思います
※今回の記事の趣旨とは異なるので以後、実装に関しては触れていません

Angularプロジェクト作成


$ ng new simple-scroll-spy-app

このプロジェクトは公開するモジュールのソース管理や動作確認等をするものになります

モジュールを作成


$ cd simple-scroll-spy-app
$ ng generate library simple-scroll-spy

このコマンドでprojectsディレクトリ下にサブプロジェクトが切られます
中に親プロジェクトとは異なるpackage.jsonがあると思いますが、これが公開モジュールに反映されるpackage.jsonになります
テンプレ的なComponentやServiceが自動で作成されますが、今回は使わないので消しました

モジュールを実装します
// コンテンツ側に貼るDirective
@Directive({
  selector: '[scrollSpyContent]'
})
export class ScrollSpyContentDirective

// メニュー側に貼るDirective
@Directive({
  selector: '[scrollSpyMenu]'
})
export class ScrollSpyMenuDirective

// NgModuleで定義追加とexport
@NgModule({
  imports: [
  ],
  declarations: [
    ScrollSpyContentDirective,
    ScrollSpyMenuDirective
  ],
  exports: [
    ScrollSpyContentDirective,
    ScrollSpyMenuDirective
  ]
})
export class SimpleScrollSpyModule { }

このモジュール自体はサブディレクトリに配置されているだけなので、親プロジェクトからは普通に参照できます
なので、app.moduleとapp.componentで軽い動作確認ができます
// app.module.ts
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    SimpleScrollSpyModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule

// app.component.ts
@Component({
  selector: 'app-root',
  template: `
    <ul><li scrollSpyMenu
    ...
    <div scrollSpyContent
    ...
})
export class AppComponent

パッケージ化


サブプロジェクトにあるpublic_api.tsを編集します
ここでexportするものがパッケージ化の対象になります
export * from './lib/simple-scroll-spy.module';

続いて、サブプロジェクトのpackage.jsonを編集していきます
前途したようにこれが公開情報を含んだpackage.jsonになります

github


今更ですがgithubアカウントが無い場合は登録します
自分がgithubに登録したのがはるか昔(その割にBitBucketがメインなのでずっと放置してた)なのでどんなだったか覚えてませんが
まあ、よくあるメアドで認証して~って感じの説明不要なものだったかと

リポジトリを用意します
Create Repositoryから適当な名前で、ライセンスはMITとかでいいんじゃないですかね

作成したgithubリポジトリの情報をpackage.jsonに書いていきます
"repository": {
  "type": "git",
  "url": "git+https://github.com/neko2me/simple-scroll-spy.git"
},
"bugs": {
  "url": "https://github.com/neko2me/simple-scroll-spy/issues"
},
"homepage": "https://github.com/neko2me/simple-scroll-spy#readme",
"license": "MIT",

npm


同様にnpmもアカウントが無い場合は登録します
特に特筆すべきことは無かったかと思いますが、username、password、メアドはローカルのnpmに設定するので忘れないように

npmに登録するモジュール名を決める必要があります
npm install xxxx
のxxxxのことです
npmで検索して使いたい名前が被っていないか(既に存在しないか)確認します
被っているならば別の名前にするか、アカウント名/モジュール名にする必要があります

モジュール名が決まったらpackage.jsonに書いていきます
"name": "angular-simple-scroll-spy",
// バージョンはメジャーでもマイナーでも何でもいいから1以上に
"version": "0.0.1",
// アカウント名でいいんじゃないですかね
"author": "neko2me",

後はdescriptionとkeywordsあたりを必要なら適宜、dependenciesは作成したモジュールが何かを必要とするなら記述しましょう
おそらくデフォルトでpeerDependenciesが指定されていると思いますが、
これは最低限親が持っていてほしいモジュール、みたいなニュアンスなのでとりあえずそのままで

さて、ビルドしてみます
親プロジェクトのディレクトリから子プロジェクト内のng-package.jsonを指定します
$ ng-packagr -p ./projects/simple-scroll-spy/ng-package.json
この時、ng-packagrなんてねぇよ!と言われた場合、以下どちらかだと思います
// ng-packagrをglobalインストールしていないなら
$ npm install -g ng-packagr
// 親プロジェクトにng-packagrがないなら、おそらく以下あたりが必要
$ npm install --save--dev ng-packagr @angular-devkit/build-ng-packagr @angular-devkit/build-angular

成功すると、distというディレクトリにそれっぽいディレクトリが作られます

想定どおりのものが作られたかどうか確認します
以下のコマンドでディレクトリをtarに固めます
$ npm pack ./dist/simple-scroll-spy
作成された*.tgzを他のプロジェクトで
$ npm install --save simple-scroll-spy.tgz
とすると、通常のnpmリポジトリからinstallしたのと同程度の状態にできます
その他の方法としてnpm linkでディレクトリを一時的にモジュールと同等にすることもできるのですが、
自分の環境(windows)だと安定しなかったり・・・

モジュールのnpmでの公開


というわけで出来上がって何となく動いてる風のdistディレクトリ下を公開してみます

まずはローカルのnpmクライアントに作成したアカウントを紐づけます
$ npm adduser
ここで登録時に設定したアカウント名やパスワード、メアドを聞かれます

アカウントが紐付いたら、公開コマンドを実行します
$ npm publish --access=public ./dist/simple-scroll-spy

成功したなら、はい、公開されてしまいました
npmの自分のアカウントページを確認すると、公開されているかと思います

公開されたモジュールを他のモジュールにnpm installして動作確認
$ npm install angular-simple-scroll-spy

最後に、githubへcommit & pushして、目標達成です
今回の成果物は以下になります

npm
https://www.npmjs.com/package/angular-simple-scroll-spy

github
https://github.com/neko2me/simple-scroll-spy

いじょ