CSS調整

2018年10月23日

今更聞けない、CSSだけで吹き出しの作成


今更聞けない、というか今更解説するのもアレかもしれない、CSSだけで吹き出し作成をしてみたいと思います。




まずはただの四角を書いてみます。


わかりやすくするために、背景をグレーにしました。



ただの四角を書くコード

<style type="text/css">
    .box {
        width: 40px;
        height: 40px;
        background-color: silver;
    }
</style>
<div class="box"></div>

結果





ここにボーダーを追加してみます。


わかりやすくするために、上下左右を違う色にしました。


また、ボーダーによって四角のサイズが変わってほしくないので、box-sizingを指定しています。



ボーダーを追加したコード

<style type="text/css">
    .box {
        box-sizing: border-box;
        width: 40px;
        height: 40px;
        background-color: silver;
        
        border-top: solid 3px red;
        border-bottom: solid 3px blue;
        border-left: solid 3px green;
        border-right: solid 3px yellow;
    }
</style>
<div class="box"></div>

結果





ボーダーのサイズに四角の半分のサイズを指定してみます。



ボーダーを四角と同じ大きさにしたコード

<style type="text/css">
    .box {
        box-sizing: border-box;
        width: 40px;
        height: 40px;
        background-color: silver;
        
        border-top: solid 20px red;
        border-bottom: solid 20px blue;
        border-left: solid 20px green;
        border-right: solid 20px yellow;
    }
</style>
<div class="box"></div>

結果



ボーダーの1辺(1色)に注目してみると、三角になっています!!





ああ、なるほど、1辺だけボーダーを指定すれば吹き出しに使える三角になるってことね。



ボーダーを1辺だけにしてみるコード

<style type="text/css">
    .box {
        box-sizing: border-box;
        width: 40px;
        height: 40px;
        background-color: silver;
        
        border-bottom: solid 20px blue;
    }
</style>
<div class="box"></div>

結果



あれ・・・。





というわけで、ボーダーの指定はそのままにします。


そのかわり、不要な辺のボーダーの色を透明にします。



下ボーダー以外を透明にしたコード

<style type="text/css">
    .box {
        box-sizing: border-box;
        width: 40px;
        height: 40px;
        background-color: silver;
        
        border-top: solid 20px transparent;
        border-bottom: solid 20px blue;
        border-left: solid 20px transparent;
        border-right: solid 20px transparent;
    }
</style>
<div class="box"></div>

結果



無事に三角ができました。





ところで、このdiv自体は吹き出しの本体なので、もっと大きくして、三角は別の要素にしたいところです。


なので、before疑似要素で三角を作ることにします。


疑似要素にするにはcontentの指定が必須、また、本体とサイズが違うので別で指定します。



div本体を大きくして、三角をbeforeへ指定したコード

<style type="text/css">
    .box {
        box-sizing: border-box;
        width: 300px;
        height: 100px;
        background-color: silver;
    }
    .box::before {
        content: "";
        box-sizing: border-box;
        width: 40px;
        height: 40px;
        
        border-top: solid 20px transparent;
        border-bottom: solid 20px blue;
        border-left: solid 20px transparent;
        border-right: solid 20px transparent;
    }
</style>
<div class="box"></div>

結果



本体と三角が別々に表示されました。





あとは位置と色を合わせればいい感じになりそうです。


三角を絶対配置にしたいので、本体をrelativeにして、三角をabsoluteにします。


これで、本体内での絶対配置にできるので、bottomを100%にします。


bottomに100%を指定することで、本体のbottom(0)位置から本体サイズの100%分上に配置する、という状態になります。


それっぽくleftもずらして、色もグレーに合わせます。



positionで位置を合わせて、同色にしたコード

<style type="text/css">
    .box {
        box-sizing: border-box;
        width: 300px;
        height: 100px;
        background-color: silver;
        
        position: relative;
    }
    .box::before {
        content: "";
        box-sizing: border-box;
        width: 40px;
        height: 40px;
        
        border-top: solid 20px transparent;
        border-bottom: solid 20px silver;
        border-left: solid 20px transparent;
        border-right: solid 20px transparent;
        
        position: absolute;
        bottom: 100%;
        left: 40px;
    }
</style>
<div class="box"></div>

結果



吹き出しになりました。





今度は塗りつぶしの吹き出しじゃないパターンにしてみます。


本体を背景色指定からボーダー指定に変更します。



本体をボーダーにしたコード

<style type="text/css">
    .box {
        box-sizing: border-box;
        width: 300px;
        height: 100px;
        border: solid 2px silver;
        background-color: white;
        
        position: relative;
    }
    .box::before {
        content: "";
        box-sizing: border-box;
        width: 40px;
        height: 40px;
        
        border-top: solid 20px transparent;
        border-bottom: solid 20px silver;
        border-left: solid 20px transparent;
        border-right: solid 20px transparent;
        
        position: absolute;
        bottom: 100%;
        left: 40px;
    }
</style>
<div class="box"></div>

結果





三角もボーダーにしたいですが、三角は既にボーダーです。


なので、もうひとつの要素を重ねてボーダーっぽくしてみます。


もうひとつの要素にはafter疑似要素を利用すると楽できます。



afterを重ねて三角をボーダーっぽくしたコード

<style type="text/css">
    .box {
        box-sizing: border-box;
        width: 300px;
        height: 100px;
        border: solid 2px silver;
        background-color: white;
        
        position: relative;
    }
    .box::before {
        content: "";
        box-sizing: border-box;
        width: 40px;
        height: 40px;
        
        border-top: solid 20px transparent;
        border-bottom: solid 20px silver;
        border-left: solid 20px transparent;
        border-right: solid 20px transparent;
        
        position: absolute;
        bottom: 100%;
        left: 40px;
    }
    .box::after {
        content: "";
        box-sizing: border-box;
        width: 34px;
        height: 34px;
        
        border-top: solid 17px transparent;
        border-bottom: solid 17px white;
        border-left: solid 17px transparent;
        border-right: solid 17px transparent;
        
        position: absolute;
        bottom: 100%;
        left: 43px;
    }
</style>
<div class="box"></div>

結果



ボーダーな吹き出しになりました。



afterのサイズやボーダーサイズ、位置は面倒だったので結構適当です。


sassとかだったらcalcとかするとキレイに表せそうです。




いじょ

2018年9月27日

Spring Data RestのJson SchemaのEnumをJsonIgnoreする

SDRにはJsonSchemaを出力する機構があります。
要するにEntityを元にJsonSchemaを生成するConverterがあるってことですね。

普通にJsonSchemaを出力してみると


class Text {
  @Column
  var title: String = ""
  @Column
  var page: Int = 0
  @Column
  @Enumerated(EnumType.STRING)
  var category: CategoryType = CategoryType.Memo
  
  enum class CategoryType {
    Memo,
    Article,
    Log,
    Code
  }
}

上のEntityがあったとして、

RequestHeader ->
  Accept: application/schema+json

HttpRequest.GET ->
  /profile/texts

となげると

{
  title: "Text",
  type: "object",
  properties: {
    title: {
      title: "Title",
      type: "string"
    },
    page: {
      title: "Page",
      type: "integer"
    },
    category: {
      title: "Category",
      type: "string",
      enum: [
        "Memo",
        "Article",
        "Log",
        "Code"
      ]
    }
  }
}

といったものが返ってきます。(一部不要な部分は削ってます)


このEntityの一部にJsonIgnoreを指定してみます


class Text {
  @Column
  var title: String = ""
  
  @JsonIgnore
  @Column
  var page: Int = 0
  
  @Column
  @Enumerated(EnumType.STRING)
  var category: CategoryType = CategoryType.Memo
  
  enum class CategoryType {
    Memo,
    Article,
  
    @JsonIgnore Log,
    @JsonIgnore Code
  }
}

同じリクエストをなげた場合、返ってくるのは、

{
  title: "Text",
  type: "object",
  properties: {
    title: {
      title: "Title",
      type: "string"
    },
    category: {
      title: "Category",
      type: "string",
      enum: [
        "Memo",
        "Article",
        "Log",
        "Code"
      ]
    }
  }
}

となります。
pageプロパティは期待どおり出力されなくなりましたが、categoryプロパティのenum値は全て出力されてしまっています。


これをどうにかしたいのが、本題。


enum値のJsonSchemaはJsonSchema.EnumPropertyというクラスで実装されています。


標準のconverter相当の作り方をした場合


fun buildSchema(clazz: Class<Any>): JsonSchema.JsonSchemaProperty? {
  return when {
    clazz.isEnum -> buildEnumSchema(clazz)
    else -> null
  }
}
fun buildEnumSchema(clazz: Class<Any>): JsonSchema.JsonSchemaProperty {
  return JsonSchema.EnumProperty("name", "title", clazz, "description", false)
}

少し雑ですが、要するに、Classを渡して生成しています。

Classを渡した場合のコンストラクタは、

* 全Enumメンバーを取得
* toStringしてList化
* 別のコンストラクタへ渡す

という処理になっているので、JsonIgnore分を削ったListを用意して、その「別のコンストラクタ」をダイレクトに呼んであげれば良さそうです。


JsonIgnoreなメンバを省いたEnumPropertyを作る場合


上の処理を書き換えます。

fun buildEnumSchema(clazz: Class<Any>): JsonSchema.JsonSchemaProperty {
  clazz as Class<Enum<*>>
  return JsonSchema.EnumProperty("name", "title",
    clazz.enumConstants.filter { 
      clazz.fields[it.ordinal]
           .getAnnotationsByType(JsonIgnore::class.java)
           .isEmpty() 
    }.map { it.toString() }.toList(),
    "description", false)
}

これをConverterに組み込んで、JsonSchema取得のリクエストをなげてみると、

{
  title: "Text",
  type: "object",
  properties: {
    title: {
      title: "Title",
      type: "string"
    },
    category: {
      title: "Category",
      type: "string",
      enum: [
        "Memo",
        "Article"
      ]
    }
  }
}

categoryのenumの値もフィルタリングされました。


やっていることは単純で、enumメンバのうち、JsonIgnoreが貼られているものを除外してtoStringしたものを渡してEnumPropertyを作っているだけです。

ポイントは、ClassがEnumだと後続処理に教える為に未代入でキャストしているのと、enumConstantsを再度、自身のClassからFieldとして取得してAnnotationを見れるようにするところでしょうか。


最新のSDRならもしかしたら、わざわざやらなくてもいけるのかな?とか思いますが、まあ、メモ程度ってことで。

いじょ

2018年9月5日

html2canvasでgoogleのfeedbackみたいなの

html2canvasというライブラリが面白そうだったので触ってみました。

html2canvasとは?


htmlの要素を解析してcanvas化するライブラリです。

html2canvas - Screenshots with JavaScript

例えばとあるdivにcssでborderが指定されていて、子要素にtextNodeがあったならば、
divと同じ大きさのblockを作って、strokeでborder引いて、textで文字を書いて、同じ見た目のcanvas要素化してくれる、というかんじです。

これを実際にやろうと考えた場合、

タグは見た目にさして関係無いからいいとしても、全cssを変換する処理を用意するなんて、現実的でなくね?

とかなると思いますが、そのものズバリ、html2canvasのgithubを見てもらうとわかりますが、cssごとの変換処理が書かれていたりします。とんでもないですね。

html2canvasを使ってみる


何も考えずに使う場合、使い方はシンプルです。

<script type="text/javascript" src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>

<script type="text/javascript">
    var content = document.getElementById("content");
    html2canvas(content).then(canvas => {
        ...
    });
</script>

これだけで渡した要素がcanvas化されたものが返ってきます。

せっかくなので、Googleのフィードバック送信時の処理
(表示中の画面にマーキングしてお問い合せする機構
ここの「フィードバックを送信」でできるやつ)

みたいなのをやってみます。

ui


適当にベタhtmlで

・画面キャプチャするボタン
・画面キャプチャしたものを入れる要素
・マークした結果をキャプチャするボタン
・マークした結果を表示する要素

を置きます。

<button id="execButton" class="exec-btn" onclick="h2c();"><img src="camera.svg"/></button>
<div id="screenshot" class="screenshot" style="display:none;"></div>
<button id="okButton" class="ok-btn" onclick="c2i();" style="display:none;"><img src="done.svg"/></button>
<div id="resultBg" class="result-bg" onclick="iClose();" style="display:none;"><img id="result" class="result"/></div>


画面をキャプチャする処理


画面をhtml2canvasにお願いしてcanvas化してもらいます。
要素の表示/非表示の切り替えに手抜き感が伺えますが、気にしない。

function h2c() {
    var content = document.getElementById("content");
    html2canvas(content).then(canvas => {
        var ss = document.getElementById("screenshot");
        ss.appendChild(canvas);
        initSelector(canvas);
        
        ss.style.display = "";
        content.style.display = "none";
        document.getElementById("execButton").style.display = "none";
        document.getElementById("okButton").style.display = "";
    });
}


canvasにマークする処理


所謂、自由選択ツールのようなことをします。
イベントの貼り方に手抜き感が伺えますが、良い子はマネしないでね!

function initSelector(screenshot){
    var selector = document.createElement("canvas");
    with (selector) {
        width = screenshot.width;
        height = screenshot.height;
        className = "selector";
        style.zIndex = (screenshot.style.zIndex || 0) + 1;
    }
    
    buildSelector(selector).selectEnd = function(r) {
        with (screenshot.getContext("2d")) {
            fillStyle = "#ffeb3ba1";
            fillRect(r.sx, r.sy, r.ex, r.ey);
        }
    }
    
    screenshot.parentElement.appendChild(selector);
}
function buildSelector(canvas) {
    var context = canvas.getContext("2d");
    with (context) {
        setLineDash([6, 3]);
        strokeStyle = "#3e3e3ecf";
    }
    
    context.rect = {
        sx: 0, sy: 0, ex: 0, ey: 0, dirty: false
    };
    context.selectStarted = false;
    context.clear = function(dirty) {
        this.rect.dirty = dirty;
        this.clearRect(0, 0, this.canvas.clientWidth, this.canvas.clientHeight);
    }
    context.select = function() {
        this.clear(true);
        this.strokeRect(this.rect.sx, this.rect.sy, this.rect.ex, this.rect.ey);
    }
    context.selectEnd = null;

    canvas.addEventListener("mousedown", function (e) {
        context.rect.sx = e.pageX;
        context.rect.sy = e.pageY;
        context.selectStarted = true;
    }, false);
    canvas.addEventListener("mousemove", function (e) {
        if (!context.selectStarted) return;
        context.rect.ex = e.pageX - context.rect.sx;
        context.rect.ey = e.pageY - context.rect.sy;
        context.select();
    }, false);
    canvas.addEventListener("mouseup", function onMouseUp (e) {
        if (context.rect.dirty) {
            context.clear(false);
            if (context.selectEnd) context.selectEnd(context.rect);
        }
        context.selectStarted = false;
    }, false);
    
    return context;
}


マークした結果をキャプチャする処理


canvasをインラインイメージ化してimgタグにぶち込みます。
それ以外でやってることは例によって手抜きですね。

ついでに、プレビューを閉じて最初に戻る処理も書いておきます。

function c2i() {
    var ss = document.getElementById("screenshot");
    var canvas = ss.firstChild;
    var selector = ss.lastChild;
    document.getElementById("result").src = canvas.toDataURL();
    
    ss.removeChild(canvas);
    ss.removeChild(selector);
    ss.style.display = "none";
    document.getElementById("okButton").style.display = "none";
    document.getElementById("resultBg").style.display = "";
}
function iClose() {
    document.getElementById("resultBg").style.display = "none";
    document.getElementById("execButton").style.display = "";
    document.getElementById("content").style.display = "";
}


当たり前といえば当たり前ですが、画面のキャプチャなので、それっぽい画面がなければ意味がない・・・
でも、出来上がっている画面を用意するのって地味に面倒なんですよね。。。というわけで、github pageのデフォルトを利用します。

で、出来上がったのが以下。

Welcome to GitHub Pages

完全版のソースはこれ

なんとも簡単にそれっぽくなりました。

使い所は限られるとは思いますが、アイデア次第では色々なことができそうです。

いじょ

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

いじょ

2018年4月15日

ElasticSearchにRESTでアクセスするメモ

最終的にはコードで書くことになるんだけれど、開発中はどうしても動作確認的にRESTでアクセスしたりするのだけれど忘れやすいので軽くメモ

日本語対応

kuromojiを入れる
$ sudo bin/elasticsearch-plugin install analysis-kuromoji


xxxのマッピング追加

mappings.json
  {
    "aliases": {},
    "mappings": {
      "doc": {
        "_source": { "enabled": true },
        "properties": {
          "name": { "type": "text" },
          "age": { "type": "long" }
        }
      }
    },
    "settings": {}
  }

PUT /xxx mappings.json


エイリアス定義

aliases.json
  {
    "actions": [
      { "add": {
        "index": "xxx",
        "alias": "xxx_admin",
        "filter": {
          "term": { "name": "admin" }
        }
      }}
    ]
  }

POST /_aliases aliases.json


クエリ検索

GET /xxx/_search?q=name:test


DSL検索

BODYでJSON指定するのでクライアントによってはGETが使えない場合はPOSTでもいける
GET /xxx/_search {
  // クエリ
  "query": {
    "match": {"name": "test"}
    }
  }
  // 返却フィールド指定
  "stored_fields": ["name", "age"]
  // ページング
  "from": "0", "size": "2"
}