CoffeeScriptでAngularJSをモダンに書こう2

ちゃんとまとめる版。どうでもいいけどこのBlogは見出しが見にくいんだよね。Ghostを使っていて、テンプレとか全然気にしてないんだけど、エントリを書くときはわりと気になってる。いいテンプレ探すか自分でカスタムするか…。とりあえずフォントだけ変えてある。

イントロ

なんでこんな記事を書こうかと思ったかというと、日々進化する開発に追いつくため、である。

フロントエンドの開発は日進月歩の勢いで進化している。特にJavaScriptにおいてはES6が6月に公開されて、classのサポートやブロックスコープの採用など、書き方の変化に影響する部分も大きい。 そこで、ES6を意識した書き方をAngularJSで行った場合はどうなるのかというのをAngularJSモダンプラクティスを参考に書き進めることとする。 ただしTypeScriptは利用しない。AngularJS2がTypeScriptを前提にしているとはいえ、僕みたいなSIerが大規模なフロントエンド開発に従事する可能性は低く、であれば腰の軽いCoffeeScriptを覚えたいからだ。

利用する環境は以下の通り。

  • CoffeeScript 1.9.3
  • AngularJS 1.4.4
  • Jade 1.11
  • Stylus 0.52

ExpressとかGulpとかそのあたりの説明はここではしません。

作るもの

自由にD&Dできるようになるディレクティブを作ろうと思う! (車輪の再開発) 名前はui-sticky。付箋って感じで。なおDrag and Drop APIは挙動がダサいので使わない。すげー便利だけどね~。あ、でもイベントは使う…かも。 DnD参考:https://app.codegrid.net/entry/dnd-api-1

あらゆる要素をD&Dできるようにしたいので、restrict: "A"でいきます。まぁようはデフォですね。

作成開始

とりあえずClassを付けるDirectiveをつくろう

D&Dできる要素は結局topやleftの位置をJSで変化させているだけなので、topやleftが効くようにしてやらなきゃいけない。で、topやleftなどが効く要素はpositionにabsolute, relative, fixedのいずれかが指定されている必要がある。それらを指定するためにとりあえずClassをふる。

index.jade

div(ng-app="sticky")  
  p(ui-sticky)

ui-sticky.coffee

"use strict"

class UiSticky  
  @postLink = (scope,element,attr,controller)->
    element.addClass "ui-sticky"

  @getDDO = ()->
    link: UiSticky.postLink

stickyApp = angular.module('sticky',[])  
stickyApp.directive "uiSticky", UiSticky.getDDO  

これで完成。p要素にui-sticky classが振られていることがわかる。

directive()に渡す第二引数には、色々なオプションを指定したオブジェクトを渡してやる。Directive Definition Object(DDO)というらしい。DDOをcompilerが受け取り実際にDirectiveとして動いていく。

ObjectならClassにしましょうよってことで、class UiStickyとして定義してある。これはモダンプラクティスを参考にしていて、linkも切り分けている。メソッド名をgetDDOとしているが、これはJavaエンジニアの慣習的な問題ですかね…とかいうといろいろ突っ込まれそうな気がするが気にしない。

restrictを省略することで、デフォルトのAが指定され、属性として指定できるようになる。

classって書けるのはCoffeeScriptやTypeScriptの恩恵。やっぱり一塊をClassとして宣言すると理解しやすいね。

Classをマークアップしよう

今のままだと真っ白なページにclassのついたp要素があるだけなんで。とりあえず黒塗りにでもしてみましょうか。 あとなんかつかめるっぽくしよう。

ui-sticky.styl

.ui-sticky
  width 100px
  height 100px
  background-color black
  cursor grab
  cursor -moz-grab
  cursor -webkit-grab
  position absolute
  &:active
    cursor grabbing
    cursor -moz-grabbing
    cursor -webkit-grabbing

高さと幅を(わかりやすさのために)100pxにして背景黒塗りにしてる。 cursorは正式対応してないんで、ベンダープレフィクスで対応。これで要素にカーソルが重なると手になってクリックすると握るよ。

positionはabsoluteにしてある。relativeは要素が通常存在した位置に、要素本来の領域を確保したままになるので無駄な空白ができてしまうからだ。

イベントハンドラを設定しよう

DOMに関連するものはDirectiveに書くのが基本。

ひとまずMouseDownとMouseUpイベントに対応しよう

ui-sticky.coffee

"use strict"

class UiSticky

  @getDDO = ()->
    link: UiSticky.postLink

  @postLink = (scope,element,attr,controller)->
    element.addClass "ui-sticky"

    ###
    Event Functions
    ###
    onMouseDown = (event)->
      console.log("hola")

    onMouseUp = (event)->
      console.log("hola")

    ###
    Add Listener
    ###
    element.on "mousedown", onMouseDown
    element.on "mouseup", onMouseUp

stickyApp = angular.module('sticky',[])  
stickyApp.directive "uiSticky", UiSticky.getDDO

elementに対してonでイベントリスナーに登録してやる。Clickするとコンソールに「hola」と文字が2回出れば成功。
どうでもいいけど「hello」って「L」を2回連打しなきゃいけないので打ちにくくない?なので僕は「hola」って打つことが多い。意味は同じだしね。まぁ意味なんて気にせず、表示されればhogeでもfoobarでもなんでもいいんだけどさ。

classを振る

握られてるよ~っていうClassを振る。

イベント部分をこう書き換える

  onMouseDown = (event)->
    event.stopPropagation()
    element = angular.element event.srcElement
    element.addClass "ui-sticky-grabbing"

  onMouseUp = (event)->
    event.stopPropagation()
    element = angular.element event.srcElement
    element.removeClass "ui-sticky-grabbing"

event.stopPropagation()を指定してるのは、uiStickyが重なった場合に誤作動するのを防ぐため。

"use strict"

class UiSticky

  @getDDO = ()->
    link: UiSticky.postLink

  @postLink = (scope,element,attr,controller)->
    element.addClass "ui-sticky"

    ###
    Event Functions
    ###

    onMouseDown = (event)->
      event.stopPropagation()
      element = angular.element event.srcElement
      element.addClass "ui-sticky-grabbing"

    onMouseUp = (event)->
      event.stopPropagation()
      element = angular.element event.srcElement
      element.removeClass "ui-sticky-grabbing"

    ###
    Add Listener
    ###
    element.on "mousedown", onMouseDown
    element.on "mouseup", onMouseUp

stickyApp = angular.module('sticky',[])  
stickyApp.directive "uiSticky", UiSticky.getDDO

Controllerを作る

ドラッグ状態かどうかを判断するためのフラグを持つために、Controllerを作ろう。classをもっているかどうかで判断してもいいんだけど、処理的にいけてないからね。

"use strict"

class UiStickyController  
  constructor: ($scope)->
    UiStickyController.$inject = ['$scope'];
    this.isDragging = false

class UiSticky

  @getDDO = ()->
    link: UiSticky.postLink
    controller: UiStickyController
    controllerAs: "sticky"
    scope: {}

  @postLink = (scope,element,attr,controller)->
    element.addClass "ui-sticky"

    ###
    Event Functions
    ###

    onMouseDown = (event)->
      event.stopPropagation()
      controller.isDragging = true
      element = angular.element event.srcElement
      element.addClass "ui-sticky-grabbing"

    onMouseUp = (event)->
      event.stopPropagation()
      controller.isDragging = false
      element = angular.element event.srcElement
      element.removeClass "ui-sticky-grabbing"

    ###
    Add Listener
    ###
    element.on "mousedown", onMouseDown
    element.on "mouseup", onMouseUp
    element.on "mousemove", onMouseMove

stickyApp = angular.module('sticky',[])  
stickyApp.directive "uiSticky", UiSticky.getDDO

これ、Classを外部ファイルにしたい場合はClass名に@を付けることを忘れないでね。 Controllerを作り、IsolateScopeを作り、mouseup,down時にフラグのオンオフを設定

MouseMoveに対応する

実際にDragするためにMouseMoveEventにも対応する。そのまま実装もしちゃう。 単純に座標を変えてやればOKだから簡単だね。

"use strict"

class UiStickyController  
  constructor: ($scope,$document)->
    UiStickyController.$inject = ["$scope", "$document"];
    this.$document = $document
    this.clickOffsetX = 0
    this.clickOffsetY = 0

class UiSticky

  @getDDO = ()->
    link: UiSticky.postLink
    controller: UiStickyController
    controllerAs: "sticky"
    scope: {}

  @postLink = (scope,element,attr,controller)->
    element.addClass "ui-sticky"

    ###
    Event Functions
    ###

    onMouseDown = (event)->

      controller.element = event.srcElement
      controller.clickOffsetX = event.offsetX
      controller.clickOffsetY = event.offsetY
      element = angular.element event.srcElement
      element.addClass "ui-sticky-grabbing"


    onMouseUp = (event)->
      controller.element = undefined
      controller.clickOffsetX = 0
      controller.clickOffsetY = 0
      element = angular.element event.srcElement
      element.removeClass "ui-sticky-grabbing"

    onMouseMove = (event)->
      if controller.element
        event.stopPropagation()
        event.preventDefault()
        top = event.pageY - controller.clickOffsetY
        left = event.pageX - controller.clickOffsetX
        controller.element.style.top = top + "px"
        controller.element.style.left =  left + "px"

    ###
    Add Listener
    ###
    element.on "mousedown", onMouseDown
    element.on "mouseup", onMouseUp
    angular.element(controller.$document).on "mousemove", onMouseMove

stickyApp = angular.module('sticky',[])  
stickyApp.directive "uiSticky", UiSticky.getDDO

ここで注意するのはmousemove eventはdocumentに対して設定するということだろうか。もしsrcElementに設定してしまうと、マウスを高速で移動したときに要素からはずれてしまい動作が停止してしまう。 でもそうするとsrcElementで取得できなくなってしまうので、controllerに持たせる。こうするとFlagじゃなくて要素を持ってるかどうかで判断してもいいのかもしんないとおもって変えた。

ってかモダンプラクティスに従うとDirectiveにinjectできないのでどうすればいいのやら。とりあえずControllerを使って共有したけどこれでいいのかなぁ…。

現状だと動かせるけどテキストはおけないとか、そもそも子要素をClickしたらsrcElementどうなんのとか まぁ細かい挙動に色々問題があるので続きはまた後日

ソースはここにおいときますね。