IMG_5313
Releases

新音楽サービスStaart(スターート)をリリースしました!

ハレノヒから新しい音楽サービスStaart(スターート)をリリースしました。

Staartは日本全国のライブ・イベント情報をあつめて、ジャンルや位置情報に応じて次々とおすすめのライブ・イベント情報を紹介してくれるサービスです。

サイトは自由に見られますが、ユーザー機能はまだ公開していません。
しかし一日も早く皆さんに使っていただきたいので目下開発中です。

ユーザー機能のリリース時にすぐご利用していただけるように招待希望の方専用のメールアドレス登録フォームがトップページに用意してあります。

是非Staartを御覧ください!そしてライブをみつけて見に行こう!

ライブ・イベント情報ナビStaart(スターート)

IMG_6953

IMG_5313

標準
ruby-on-rails
Development

[Rails] モデルのデフォルト値セットをService Objectにしてみた

RailsのようなMVCアーキテクチャのフレームワークではControllerに処理を書くことを極力避けることでFat Controllerにならないようにするのはもう常識と言えるまでに浸透してますよね。

しかし最近はModelに何でもかんでも詰め込みすぎてFat Modelにならないようにしようという動きも有ります。
僕は最初、あぁそれでConcernに分けていくのねと思ったのですがConcernだけでかたづく問題ばかりではありません。

詳しくは
ここ 7 Patterns to Refactor Fat ActiveRecord Models
またはここあたり Object Oriented Rails – Writing better controllers
を読むのがいいようですが、そのなかにService Objectというのがあります。

今回そのService Obejctでモデルのデフォルト値をセットする処理を書いてみましたので紹介します。

まずTask管理をするRailsアプリがあったとしますTasksController#newでこんなコードになってました。

class TasksController
  def new
    @task = Task.new
    @task.category = category if params[:category_name].present? && (category = Category.find_by_name(params[:cateogry_name]).present?
    @task.due_date = params[:due_date].present? ? params[:due_date] : Date.tomorrow
  end
end

ごちゃごちゃしていて分かりづらいですね。

いきなり結果ですが、前述のブログを参考に以下のように変えてみました

class TasksController
  before_action :load_task_default_value_service, only: [:new]

  def new
    @task = Task.new
  @task = @task_default_value_service.set(@task, params)
  end

  private
    def load_task_default_value_service(service = TaskDefaultValueService.new)
      @task_default_value_service ||= service
    end
end

大分スッキリしました。

で肝心のService Obejctは app/services ディレクトリを作って配置します。このとき config/application.rb で config.autoload_path に app/services を追加しておかなければいけません。

app/services/task_default_value_service.rb

class TaskDefaultValueService
  def set(task, params)
    @params = params
    task.category = @cateogry if valid_category_name?
    task.due_date = due_date
  end

  def valid_category_name?
    @category = Category.find_by_name(params[:cateogry_name])
    @params[:category_name].present? && @category.present?
  end

  def due_date
    @params[:due_date].present? ? @params[:due_date] : Date.today
  end
end

たったこれだけの処理が専用のクラスに切り分けることでかなり読みやすくなりますね。

ここまでやって気になったんですがControllerからService Objectを呼び出しておく部分って冗長になりそうだなとおもったんです。
例えば今回だと before_action で load_task_default_value_service を実行するようにしてますが、これってService Objectの名称が変わるだけで同じ処理をたくさん書くはめになりそうです。

なので ApplicationController で method_missing を使ってService Objectの呼び出しを簡単に出来るようにしてみました。

class ApplicationController
  def method_missing(method_name, *arg)
    if method_name =~ /^load_([a-z_]+)_service$/
      load_service_object($1)
    else
      super
    end
  end

  private
    def load_service_object(service_name)
      service_snakecase = (service_name + '_service')
      service = service_snakecase.camelize.constantize
      instance_variable_set("@#{ service_snakecase }", service.new)
    end
end

こうしておけば UserAuthenticationService を使いたい時は

  before_action :load_user_authentication_service

と書くだけでいいので簡単ですね!

標準
php-logo-a855de85906d6219bb3f642cddf45168
Development

herokuで公式サポートになったPHPを試してみた

herokuでPHPの公式サポートが発表されましたね。

Introducing the new PHP on Heroku

以前からbuild packを使用すればPHPを動かすことは出来ましたが公式となると面倒な手順なしに動くので便利そうな感じです。

Hello Worldしてみる

まずはハロワしてみます
用意するのは以下のものです。

  • index.php
  • composer.json(空でOK)
  • 上記が含まれたgitリポジトリ
$ heroku create
Creating frozen-beach-1321... done, stack is cedar
http://frozen-beach-1321.herokuapp.com/ | git@heroku.com:frozen-beach-1321.git
Git remote heroku added
$ git push heroku master
Initializing repository, done.
Counting objects: 4, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (4/4), 293 bytes | 0 bytes/s, done.
Total 4 (delta 0), reused 0 (delta 0)

-----> PHP app detected

       NOTICE: Your composer.json is completely empty.
       Consider putting at least "{}" in there to make it valid JSON.
       See https://devcenter.heroku.com/categories/php

-----> Setting up runtime environment...
       - PHP 5.5.11
       - Apache 2.4.9
       - Nginx 1.4.6
-----> Installing PHP extensions:
       - opcache (automatic; bundled, using 'ext-opcache.ini')
-----> Building runtime environment...
       NOTICE: No Procfile, defaulting to 'web: vendor/bin/heroku-php-apache2'
-----> Discovering process types
       Procfile declares types -> web

-----> Compressing... done, 60.7MB
-----> Launching... done, v3
       http://frozen-beach-1321.herokuapp.com/ deployed to Heroku

To git@heroku.com:frozen-beach-1321.git
 * [new branch]      master -> master
$ heroku open
Opening frozen-beach-1321... done

簡単すぎる…

しかも日本語もバッチリでした!

composerでライブラリを動かしてみる

composerもかなり便利そうなのは知ってたんですが使ったことなかったのでこの機会に始めてみようと思います。
まずはcomposerのインストールですが、以下のコマンドで良いようです

 curl -sS https://getcomposer.org/installer | php

これで実行したディレクトリに composer.phar がダウンロードされました。
このファイルは gitignore しておいたほうがいいみたいですね。

とりあえず最近のPHPよくわからないしSinatraみたいな軽量フレームワークでもインストールしてみようと検索したら、Silexというのがなんとなく良さげな感じだったのでこれを試してみることにしました。

composer.json

{
  "require": {
    "silex/silex": "2.0.*@dev"
  }
}
$ ./composer.phar install
Loading composer repositories with package information
Installing dependencies (including require-dev)
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - Installation request for silex/silex 2.0.*@dev -> satisfiable by silex/silex[2.0.x-dev].
    - silex/silex 2.0.x-dev requires pimple/pimple ~2.1@dev -> no matching package found.

Potential causes:
 - A typo in the package name
 - The package is not available in a stable-enough version according to your minimum-stability setting
   see  for more details.

Read  for further common problems.

おっとダメでした。なんだろなこれ、とりあえずバージョンを1つ落として再チャレンジしてみます。

composer.json

{
  "require": {
    "silex/silex": "1.2.*@dev"
  }
}
$ ./composer.phar install
Loading composer repositories with package information
Installing dependencies (including require-dev)
  - Installing symfony/routing (v2.4.4)
    Downloading: 100%

  - Installing psr/log (1.0.0)
    Downloading: 100%

  - Installing symfony/debug (v2.4.4)
    Downloading: 100%

  - Installing symfony/http-foundation (v2.4.4)
    Downloading: 100%

  - Installing symfony/event-dispatcher (v2.4.4)
    Downloading: 100%

  - Installing symfony/http-kernel (v2.4.4)
    Downloading: 100%

  - Installing pimple/pimple (v1.1.1)
    Downloading: 100%

  - Installing silex/silex (1.2.x-dev 6d9e3fe)
    Cloning 6d9e3fe4d7d9ed563800ee8b4559bd51c13457e5

symfony/routing suggests installing symfony/config (For using the all-in-one router or any loader)
symfony/routing suggests installing symfony/yaml (For using the YAML loader)
symfony/routing suggests installing symfony/expression-language (For using expression matching)
symfony/routing suggests installing doctrine/annotations (For using the annotation loader)
symfony/event-dispatcher suggests installing symfony/dependency-injection ()
symfony/http-kernel suggests installing symfony/browser-kit ()
symfony/http-kernel suggests installing symfony/class-loader ()
symfony/http-kernel suggests installing symfony/config ()
symfony/http-kernel suggests installing symfony/console ()
symfony/http-kernel suggests installing symfony/dependency-injection ()
symfony/http-kernel suggests installing symfony/finder ()
silex/silex suggests installing symfony/browser-kit (>=2.3,<2.6-dev)
silex/silex suggests installing symfony/css-selector (>=2.3,<2.6-dev)
silex/silex suggests installing symfony/dom-crawler (>=2.3,<2.6-dev)
silex/silex suggests installing symfony/form (>=2.3,<2.6-dev)
Writing lock file
Generating autoload files

今度は成功しました。
composerの出力、bundlerより見やすいし親切だしいいですねこれ。

Silexのコードはこちらを参考に(というか丸パクリ)させていただきました。
PHP - SilexでHello World - Qiita

<?php
require_once __DIR__.'/vendor/autoload.php';

$app = new Silex\Application();

$app->get('/', function() {
    return "Hello World!";
});

$app->get('/hello/{name}', function($name) use($app) {
    return 'Hello '.$app->escape($name);
});

$app->run();

でPHP5.4からのビルトインサーバーを初起動!

php -S 0.0.0.0:8080
PHP 5.4.16 Development Server started at Fri May  2 22:02:16 2014
Listening on http://0.0.0.0:8080
Document root is /Users/kozo/tmp/heroku_php_kozo
Press Ctrl-C to quit.

すげー動いた!っていつもrubyでやってることとそんなに変わらないんだけど、PHPでこれが出来るのは結構感動しますね。

で、Silexでハロワ確認出来たのでこのまま、herokuにプッシュしてみます。

$ git add .
$ git commit -m "Install Silex"
[master 77d66b2] Install Silex
 3 files changed, 523 insertions(+), 2 deletions(-)
 create mode 100644 composer.lock
$ git push heroku master

heroku上ではトップページは動作したんですが、他のルーティングがNot Foundになってしまいました。
これはhtaccessでrewriteすればいいのかな?と思ったので以下の様な内容で作成してコミットしたら動きました!

<IfModule mod_rewrite.c>
 RewriteEngine On
 RewriteBase /
 RewriteRule ^index\.php$ - [L]
 RewriteCond %{REQUEST_FILENAME} !-f
 RewriteCond %{REQUEST_FILENAME} !-d
 RewriteRule . /index.php [L]
</IfModule>

いやーherokuでこんなにも簡単にPHPが動くともっとPHP使いたくなっちゃいますねー。

標準
ruby-on-rails
Development

[Rails] has_many through な関連をチェックボックスで操作するときに苦労した話

WordPressのカテゴリー選択UIみたいのをつくりたい

Railsで多対多の関係を定義する方法として has_and_belongs_to_many ではなく has_many through を使うようになって久しいですが、accepts_nested_attributes_forfields_forを使って関連データを親モデルのフォームから操作するのは未だに慣れません。

ググって調べたところによると、RailsCastsで紹介されている方法が紹介されている方法が一般的なんですかね。

#196 Nested Model Form (revised)

Github ryanb/nested_form

この方法だとテキストフィールドをつけたり外したりするのが便利なんだけど、既に用意されているリストから選ぶUIの場合、つまりWordPressのカテゴリー選択みたいなのを作るのはキツイのかなと。

スクリーンショット 2014-04-08 9.40.56
↑こんなやつ

[問題]fields_forは関連データを持っていないとブロックを実行しない

そもそも fields_for はオブジェクトが関連データを持っていないとフォームを作成してくれませんよね?

このようなモデルの関係があった場合に

class Article < ActiveRecord::Base
  has_many :categorizations
  has_many :categories, through: :categorizations

  accepts_nested_attributes_for :categorizations, allow_destroy: true
end

class Categorization < ActiveRecord::Base
  belongs_to :article
  belongs_to :category
end

class Category < ActiveRecord::Base
  has_many :categorizations
  has_many :articles, through: :categorizations
end

これだけじゃ fields_for の中は実行されず…

- @article = Article.new

= form_for @article do |f|
  = f.fields_for :categorizations do |cf|
    = cf.collection_select :category_id, Category.all, :id, :name

@article.categorizations.length == 0 だからブロックが実行されないんですよね。
そこで nested_form というgemの出番で、f.link_to_addとかcf.link_to_removeというヘルパーが追加されるので超便利。というのはわかるんだけどこのUIじゃないんですよ作りたいのは。

name属性手書きとか、JavaScriptで対応するの嫌なんです

上記の方法でだめなら fields_for 捨ててinput要素手書きしますか?

- Category.all.each do |cat|
  %label
    %input{ type: 'checkbox', name: 'article[categorizations_attributes][][category_id]', value: cat.id }
    = cat.name

自分で書いててそんなに悪くないかな…とちょっと思ってしまったんですが、やっぱりこんなのダメだと思う。てゆうかこれだと一度つくった関連消せないしね。

となるとチェックボックス外した時にJSで _destroy キーに true をセットする hidden 要素を生成するのか・・・!
いやいやこれも無いでしょ。処理があちこちに散らばるからね。viewをパッと見ただけじゃなんで動いてるのかわからないよ。

そうだ、Helper作ってやろう

となるとどうすりゃいいんだ!あーーー!とか思ってた時にこんな記事を見つけました。

Complex Rails Forms with Nested Attributes

全部読んだわけじゃないんですが、参考にしたのはControllerにもViewにも置きたくない複雑な処理をHelperに書いているところ。なんだ普通のコトじゃないかと思うかもしれませんが、いや僕もこれ書いてて普通だなって思いましたが見つけた時は結構目からウロコだったんですよね。

なので僕のViewコードはこうなりました

= form_for setup_categorizations(@article) do |f|
  %ul
    = f.fields_for :categorizations do |cf|
      %li
        %label
          = cf.hidden_field :id
          = cf.hidden_field :category_id
          = cf.check_box :apply, { checked: cf.object.persisted? }
          = cf.object.category.name

setup_categorizations でArticleモデルを渡して既に関連付けられていないcategoryだけをbuildしてセットしています

module ArticleFormHelper
  def setup_categorizations(article)
    Category.all.each do |cat|
      unless article.categorizations.select{ |c| c.category_id == cat.id }.any?
        article.categorizations.build({ category_id: cat.id })
      end
    end
    article
  end
end

categorizations_attributes として渡すパラメータに apply というキーの真偽値を仕込んであるのでこれを元に以下の処理をController側で行います

  1. applyキーが0でpersistedなデータなら_destroyフラグを立てる
  2. applyキーが1ならcategory_id付きで残しておく

この処理をRails4ならstrong parameter使いつつこんな感じにしておいて

class ArticlesController < ApplicationController
  include ArticleFormHelper

  def create
    @article = Article.new(article_params)
    ....
  end

  private
    def article_params
      _params = params.require(:article).permit(:title, :body, categorizations_attributes: [:id, :category_id, :apply])
      setup_categorizations_attributes(_params)
    end
end

先ほどのArticleFormHelperにsetup_categorizations_attributesメソッドを追加します

module ArticleFormHelper
  ....

  def setup_categorizations_attributes(params)
    # キーが数字の文字列のhashになっていたため配列にする "0" => {id: 1}
    attrs = params[:categorizations_attributes].dup.map{ |k, v| v }
    params[:categorizations_attributes] = attrs.inject([]) { |res, attr|
      if attr[:apply].to_i == 0
        res.push({ id: attr[:id], _destroy: 1 }) if attr[:id].persent?
      else
        res.push({ id: attr[:id], category_id: attr[:category_id] })
      end
      res
    }
    params
  end

自分的にはこれでスッキリしたんですが、もっといい方法あったら是非教えて欲しいです。
あとモデルの命名どうなの?とかもあったらコメントしてもらえると嬉しいです!

標準
iphone
Development

[iOS] ホーム画面に追加したWebアプリのリンクをSafariに飛ばさない方法

今回はiOSの話と言ってもネイティブアプリじゃなくWebアプリの方です。

iOSのSafariに「ホーム画面に追加」というオプションありますよね。
フルスクリーンで見せたかったりするときは使うべしなんですが、困ったことにリンクをタップすると通常のSafariに戻されるんですよね…

まぁフルスクリーンなので戻るボタンとか無いからの配慮なんでしょうけど、ユーザーはinterruptされるし、フルスクリーンで統一できなくなっちゃうし正直うざいです。

対策その1:Ajaxで逃げ切る

対処法の1つとしてページの内容をXHRでとってきて書き換えればページ遷移は起こりませんよね。

$(document).on('click', 'a', function(e) {
  var $a = $(e.target);
  if (!$a.attr('href').match(/^#/)) {
    e.preventDefault();
    $.get($a.attr('href'), function(data) {
      var $body = $('body');
      $body.children().remove();
      $body.append($(data));
    })
  }
});

試してないけど多分こんなかんじでしょうか。
でもこれだとサーバーサイドも合わせなくちゃいけなくてめんどくさいなーとか、ページ遷移したと見せかけてwindowのonloadイベントとか発火されないし $(document).ready() みたいのは動かないですよね。

対策その2(おすすめ):一旦preventDefaultしてからのlocationに突っ込む

でたどり着いたのがこれです。
どうやら一旦preventDefaultしてしまったらページ遷移してもSafariに飛ばされないみたいです。

$(document).on('click', 'a', function(e) {
  var $a = $(e.target);
  if (!$a.attr('href').match(/^#/)) {
    e.preventDefault();
    window.location = $a.attr('href');
  }
});

いい感じに動いてます

標準
ruby-on-rails
Development

[Rails] “\xE5” on US-ASCII で .js.coffee.erb での JSON埋め込み時にエラーした時の対処方法

Railsで開発してて、jsにjsonファイルで定義したデータ埋め込みたくて hoge.js.coffee.erb というファイル作って

data = <%= Rails.root.join('app/assets/javascripts/data.json').read %>

とやろうとしたら

"\xE5" on US-ASCII

だからダメだよみたいなエラー起きた。多分json内に日本語がかいてあるのが原因だと思う。
viewファイルに埋め込めば出来るかもしれないけどあんまりしたくないなぁと思ってググっても出てこない……

悩んだ結果これでいけました

data = <%= JSON.load(Rails.root.join('app/assets/javascripts/data.json').read).to_json %>
標準
ruby-on-rails
Development

Ruby on Rails 4 でアプリを新規作成してherokuで公開するまで

Railsシリーズ始めて見たいと思います。初回は初心者向けにRailsアプリの新規作成とheorkuへのデプロイを紹介したいと思います。Rubyのインストールについてはこちらの記事で紹介しています。

heroku

heroku(ヘロク)とはPaaSというジャンルのサービスで、Rails等で作ったWebアプリケーションをサーバの設定などを行わずにすぐに公開できるサービスです。
基本的には無料で使えて、本番環境として使う場合は有料プランが推奨されています。

phpならレンタルサーバーを借りて作ったファイルをFTPすれば動かせますが、rubyとなるとVPS等でサーバーの設定をして…とかちょっと面倒ですよね。herokuを使うと、gitリポジトリにpushするだけでWebアプリを公開できるのでとっても楽ちんだし、タダで使えるしいい感じです。

herokuを使うためにはまずアカウントを取得して、heroku toolbeltををインストールします。
サインアップ
https://id.heroku.com/signup
ツールベルト
https://toolbelt.heroku.com/

Rails 4 アプリを作る

今回は特に何もしないアプリケーションを作ります。
まずターミナルを開いてアプリを作るディレクトリに移動して、アプリを格納するディレクトリを作成します。
ここで注意してください。作成するディレクトリ名はRailsアプリのネームスペースになりますので後で後悔しないような名前に!

$ cd path/to/workspace
$ mkdir kozo-sample-rails4

ディレクトリを作成したらそこに移動して、Gemfileを作成します。
Gemfileはbundlerというgemを使うために必要なファイルですが、bundlerがインストールされていない場合は先に入れておきます。

$ gem list | grep bundler
bundler (1.3.5)

↑このような表示になればインストールされていますが、インストールされていない場合は以下を実行してください。

$ gem install bundler --no-ri --no-rdoc

bundlerを使ってGemfileを作成するには以下を実行します。

$ bundle init

Gemfileが作成できたらエディタで開き、gem “rails” の部分のコメントを外します。

# A sample Gemfile
source "https://rubygems.org"

- # gem "rails" 
+ gem "rails"

Gemfileを保存して閉じたらrailsを現在のディレクトリ以下にインストールします。
具体的には path/to/workspace/kozo-sample-rails4/vendor/bundle 以下にインストールされます。

$ bundle install --path vendor/bundle --without production

–without オプションをつけておくと、heorku環境のみに必要なgemはローカル環境でインストールされるのを防ぐことが出来ます

bundle installが終わったら以下のコマンドを実行してRailsアプリのひな形を作成します。

$ bundle exec rails new . --skip-bundle

1つずつ説明していくと、bundle execまでが先ほどbundle installでインストールしたgemを使うことを示しています。
rails new . は現在のディレクトリに新しいrailsアプリのひな形を作成します。–skip-bundleはひな形作成後に自動でbundle installが行われるのを防いでいます。
途中でGemfileを上書きする確認(Overwrite /Users/kozo/tmp/kozo-sample-rails4/Gemfile? (enter “h” for help) [Ynaqdh])がありますが上書きして良いのでそのままエンターキーを押してください。

ひな形が出来たらまたGemfileを開いて、以下の記述を追加します。追加する場所はどこでもOKです。

group :production do
  gem 'rails_12factor'
  gem 'pg'
  gem 'unicorn'
end

sqlite3がheroku環境にインストールできないようなのでsqlite3の行を以下のように変更します。

gem 'sqlite3', group: [:development, :test]

追加したらbundle installを実行します。(Gemfileを編集したら必ずbundle installを実行しないと意味がないのでセットで行うと覚えておきましょう)

次にProcfileというファイルをGemfile等と同じ場所に作成していかの内容を入れておきます。

web: bundle exec unicorn -p $PORT -c ./config/unicorn.rb

さらに configフォルダの中にunicorn.rbというファイルを作成して以下の内容を入れておきます。

worker_processes Integer(ENV["WEB_CONCURRENCY"] || 3)
timeout 15
preload_app true

before_fork do |server, worker|
  Signal.trap 'TERM' do
    puts 'Unicorn master intercepting TERM and sending myself QUIT instead'
    Process.kill 'QUIT', Process.pid
  end

  defined?(ActiveRecord::Base) and
    ActiveRecord::Base.connection.disconnect!
end

after_fork do |server, worker|
  Signal.trap 'TERM' do
    puts 'Unicorn worker intercepting TERM and doing nothing. Wait for master to send QUIT'
  end

  defined?(ActiveRecord::Base) and
    ActiveRecord::Base.establish_connection
end

これでherokuに公開するgem等は用意出来たのですが、肝心のトップページがありませんので各箇所に以下の記述を追加します。

config/routes.rb (長いコメントは削除してしまいましょう)

KozoSampleRails4::Application.routes.draw do
  root to: 'pages#home'
end

app/controllers/pages_controller.rb (デフォルトでは用意されていないので作成してください)

class PagesController < ApplicationController
end

app/views/pages/home.html.erb (こちらもデフォルトは用意さていないので作成してください)

<%= Time.now %>

トップページを表示する準備ができたのでサーバーを起動して確認してみます。

$ bundle exec rails s

http://localhost:3000にアクセスして現在時刻が表示されたら成功です。

ここまできたら後はデプロイするだけ!あと一息です。
herokuのデプロイにはgitが必要なのでアプリのディレクトリでgitリポジトリを作成し、最初のコミットを実行します。

$ git init
$ git add .
$ git commit -m "Initial commit"

次にherokuのアプリを登録します。(heorkuコマンドを最初に実行するときはログインが必要です)

$ heroku create kozo-sample-rails4

kozo-sample-rails4の部分はアプリ名ですので作ったアプリの名前に合わせる等してください。kozo-sample-rails4はもう使えませんよ!

herokuにアプリの登録ができたらgit pushを実行します。

$ git push heroku master

ズラズラっとherokuのデプロイログが出力されますので無事終わったら以下のコマンドでブラウザを開きます。

$ heroku open

2014-03-02 08:13:26 +0000 と表示されました!ブラウザをリロードすると時間が進むはずです。

標準
IMG_5256
Development

Vanilla JavaScriptを書いてみよう その3(タブUIを実装)

さて前回はJavaScriptで取得したhtml要素のclass属性を簡単に追加や削除が行えるように Element というクラスの実装を紹介しました。

今回はそのElementクラスをクラスを使ってタブUIを操作できるクラスを実装してみたいと思います。

まず部品として以下の2点が必要かと思いますので、それぞれに対応するクラス名称をつけました。

  • タブボタン => TabNavItem
  • タブ内容ボックス => TabContent

TabNavItem

TabNavItemの仕様として以下の項目があげられます

  1. 生のhtml要素を渡して初期化出来る
  2. 選択中に切り替えられる
  3. 非選択中に切り替えられる
  4. 生のhtml要素と比較して自分自身かどうかをチェックできる

1. 生のhtml要素を渡して初期化出来る

これはコンストラクタですね。new した時の初期化処理ですが、引数で受け取った生のhtml要素を前回作成した Element オブジェクトにして保持しておきます。
後は選択中を示すclass名もここで定義しておきました。

function TabNavItem(el) {
  this.el = new Vanilla.Element(el);
  this.current_class_name = 'tab-nav__item--current';
}

2. 選択中に切り替えられる

自身のElementオブジェクトに対して、選択中のclass名を持っていなければ追加します。

TabNavItem.prototype.toCurrent = function() {
  if (this.isCurrent()) return;
  this.el.addClass(this.current_class_name);
};

3. 非選択中に切り替えられる

自身のElementオブジェクトに対して、選択中のclass名を削除します。

TabNavItem.prototype.toUncurrent = function() {
  this.el.removeClass(this.current_class_name);
};

4. 生のhtml要素と比較して自分自身かどうかをチェックできる

実はこれもタブUIを実装する上で結構重要な機能です。
これはElementオブジェクトの持っている同名のメソッドに処理を渡しています。

TabNavItem.prototype.isEqual = function(targetElement) {
  return this.el.isEqual(targetElement);
};

TabNavItemの実装は以上です。まとめたコードがこちらです。

var TabNavItem = (function() {
  function TabNavItem(el) {
    this.el = new Element(el);
    this.current_class_name = 'tab-nav__item--current';
  }

  TabNavItem.prototype.toCurrent = function() {
    if (this.isCurrent()) return;
    this.el.addClass(this.current_class_name);
  };

  TabNavItem.prototype.toUncurrent = function() {
    this.el.removeClass(this.current_class_name);
  };

  TabNavItem.prototype.isCurrent = function() {
    this.el.hasClass(this.current_class_name);
  };

  TabNavItem.prototype.isEqual = function(targetElement) {
    return this.el.isEqual(targetElement);
  };

  return TabNavItem;
})();

TabContent

TabContentは仕様として以下の項目があげられます。

  1. 生のhtml要素を受け取って初期化出来る
  2. 選択中に切り替えられる
  3. 非選択中に切り替えられる

1. 生のhtml要素を受け取って初期化出来る

TabNavItemとほぼ同じですが、TabContentの場合はhtml要素のid属性を label というプロパティに保持しておきました。

function TabContent(el) {
  this.el = new Vanilla.Element(el);
  this.current_class_name = 'tab-content--current';
  this.label = this.el.id;
}

2. 選択中に切り替えられる

自身のElementオブジェクトに対して、選択中のclass名を持っていなければ追加します。

TabContent.prototype.toCurrent = function() {
  this.el.addClass(this.current_class_name);
};

3. 非選択中に切り替えられる

自身のElementオブジェクトに対して、選択中のclass名を削除します。

TabContent.prototype.toUncurrent = function() {
  this.el.removeClass(this.current_class_name);
};

TabContentの実装は以上です。まとめたコードはこちら

TabContent = (function() {
  function TabContent(el) {
    this.el = new Element(el);
    this.current_class_name = 'tab-content--current';
    this.label = this.el.id;
  }

  TabContent.prototype.toCurrent = function() {
    this.el.addClass(this.current_class_name);
  };

  TabContent.prototype.toUncurrent = function() {
    this.el.removeClass(this.current_class_name);
  };

  return TabContent;
})();

イベントに紐付けて動かしてみる

最低限の役者が揃いましたので実際に動す為のコードを書いてみたいと思います。
まずはなにはともあれDOMが構築されたタイミングを取れるようにwindowのloadイベントにハンドラをあててからの作業とですね。

addEvent(window, 'load', function() {
  // 各部品のhtml要素を取得
  var tabNav = document.querySelector('.tab-nav');
  var tabNavItemElements = var tabNav.querySelectorAll('.tab-nav__item');
  var tabContentElements = document.querySelector('.tab-contents').querySelectorAll('.tab-content');

  // 格納用の配列を初期化
  var tabNavItems = [];
  var tabContents = [];

  // TabNavItem と TabContent のインスタンスを生成
  for (var i in tabNavItemElements) {
    // 配列のプロパティ length を取り出してしまわないようにチェック
    if (typeof tabNavItemElements[i] === 'object') {
      tabNavItems.push(new TabNavItem(tabNavItemElements[i]));
    }
    if (typeof tabContentElements[i] === 'object') {
      tabContents.push(new TabContent(tabContentElements[i]));
    }
  }

  // タブボタンのクリックイベントを一気に登録する
  addEvent(tabNav, 'click', function(e) {
    // ループ中で使う変数を用意
    var tabNavItem = null;
    var tabContent = null;

    // クリックしたタブボタンのhref属性を取得
    var label = e.srcElement.href.replace(/^.+#/, '');

    // クリックしたタブボタンのli要素を取得
    var clickedItem = e.srcElement.parentNode;

    // 全TabNavItemを回しながら選択状態を切り替えていく
    for (var i in tabNavItems) {
      tabNavItem = tabNavItems[i];
      if (tabNavItem.isEqual(clickedItem)) {
        tabNavItem.toCurrent();
      } else {
        tabNavItem.toUncurrent();
      }

      // TabContentの数も一緒なので同じループで選択状態を切り替えていく
      tabContent = tabContents[i];
      if (tabContent.label === label) {
        tabContent.toCurrent();
      } else {
        tabContent.toUncurrent();
      }
    }
  });
});

以上でタブUIの全体の実装が完了です。

実際に動作するサンプルがこちらです。

ポイントは部品ごとにクラス化しておくこと、イベントハンドリングは全てのボタンに割り当てるのではなく親要素にあてておく、見た目の切り替えはcssで書いてjsはclass属性の付け替えなどにとどめておくこと等です。

標準
IMG_5256
Development

Vanilla JavaScriptを書いてみよう その2(タブUIを実装)

前回はイベントハンドリングとjQueryの様にクラス名でhtml要素を検索出来るquerySelectorを紹介しました。

今回はjQuery等の等のライブラリを一切使わずにタブUIを実装して見るための下準備をご紹介したいと思います。

デザイン

デザインは以下の様なもので、タブのボタンとその内容の文章が3つずつ存在し、選択されていないタブをクリックすると内容と共に選択中の表示に切り替わります。

スクリーンショット 2014-02-24 12.23.24

 

各タブに .tab-nav__item–current というクラスが付いていると選択中の表示になり、内容部分はデフォルトで display: none; に設定されているので .tab-content–current というクラスが付いているもののみ display: block; で可視化するという仕様です。

見た目の制御はcssで行うようにしていますので、JavaScriptからhtml要素のclass属性を操作できる必要があります。

クラスを定義してみる

class属性は element.className というプロパティを使って操作できますので、専用のクラス(ここで言うクラスはインスタンスを生成するためのオブジェクトという意味)を用意しておきましょう。

ところがJavaScriptにはクラスを定義する構文は用意されていません。ではどうやるのかというと、関数オブジェクトに備わっているprototypeという仕組みを利用することでクラス定義をエミュレート出来ます。
具体的には以下のように記述します。

var Person = (function() {
  function Person(name) {
    this.name = name;
  }

  Person.prototype.greet = function() {
    alert('Hello ! My name is ' + this.name);
  }

  return Person;
)();

Personクラスは初期化時に name という変数を受け取り自身の name プロパティに格納します。そして greet というメソッドを持っていて、nameプロパティを連結した挨拶文をアラートする動作をします。

このPersonクラスからインスタンスをインスタンスを生成するには new Person(‘name’) という形で初期化関数を実行します。

var yamagata = new Person('yamagata');

greetメソッドの実行はこうです

yamagata.greet();

Elementクラス

そして今回のために実装したElementクラスはこちらです。

var Element = (function() {
  function Element(el) {
    this.el = el;
    this.id = this.el.id;
  }

  Element.prototype.classes = function() {
    return this.el.className.split(/\s+/);
  };

  Element.prototype.removeClass = function(targetClass) {
    var classes = this.classes();
    for (var i in classes) {
      if (classes[i] === targetClass) {
        classes.splice(i, 1);
      }
    }
    this.el.className = classes.join(' ');
  };

  Element.prototype.addClass = function(targetClass) {
    var classes = this.classes();
    for (var i in classes) {
      if (classes[i] === targetClass) {
        return
      }
    }
    classes.push(targetClass);
    this.el.className = classes.join(' ');
  };

  Element.prototype.hasClass = function(targetClass) {
    var classes = this.classes();
    var exist = false;
    for (var i in classes) {
      if (classes[i] === targetClass) {
        exist = true;
      }
      if (exist) break;
    }
    return exist;
  };

  Element.prototype.isEqual = function(targetElement) {
    return this.el === targetElement;
  };

  return Element;
})();

class属性を操作する場合、複数のclassを扱うことを考慮しなければいけないので、スペースで分割した配列を返す classes メソッドを実装しました。
addClass と removeClass はその配列に対して操作を行う仕組みです。

配列に要素を追加する場合は、末尾に追加する push というメソッドがメソッドが便利です。
また、配列から要素を削除する場合は、spliceというメソッドが便利です。これは削除したい要素のインデックスとそのインデックスの位置から何個の要素を削除するかという個数を指定します。今回は常に1つの要素を消すように 2番目の引数には 1 を渡しています。

var fruits = ['apple', 'banana', 'melon'];
fruits.splice(1, 1); // => bananaが削除される

また特定のclass属性を持っているかチェックをする hasClass と html要素自体が一致しているかをチェックする isEqual も実装しておきました。

このElementクラスを使用するためにはhtml要素を渡して new しなければいけませんので、以下のようにしてタブのボタンをElement化します。

var tabNavItems = [];
var tabNavItemElements = document.querySelectorAll('.tab-nav__item');
for (var i in tabNavItemElements) {
  if (typeof tabNavItemELements[i] === 'object') {
    tabNavItems.push(new Element(tabNavItemElements[i]));
  }
}

.tab-nav__item が付いているhtml要素を全て取得し、1つずつnew Elementしているのですが、typeofで取り出した要素が確実にhtml要素であることを確認しています。
これはfor文では配列のプロパティである length (中身は整数)も取り出してしまうためです。

まとめ

class属性を操作する準備は整いました、普段jQueryで行っている処理もわりと簡単に作れてロジックを書く勉強にもなるのでおすすめです。一度くらい車輪の再発明をしてみるのもいいと思います。
次回はクリックイベントを監視してどのようにタブの切替を行うのかをご紹介します。

標準
IMG_5256
Development

Vanilla JavaScriptを書いてみよう その1

ブラウザ内でアニメーションをさせたり、ユーザーの操作に反応して何かを行う場合必要になるのがJavaScriptです。

最近でこそJavaScriptは便利なライブラリやプリプロセッサが充実してるので、JavaScriptを使ったプログラミングは簡単に始められて楽しいものですが、一昔前まではブラウザ間の差異に悩まされめんどくさいという印象が強かったのでは無いかと思います。

そこで登場したのがjQueryでした。便利なセレクターやメソッドチェーンで簡潔にわかりやすく、そしてクロスブラウザで動作するスクリプトを書けるとあって、これ以外に選択肢を考えることすら無くなったのではないかと思います。

ところで昔からweb制作ではIE対応に悩まされてきた方が多いと思いますが、時代は流れ悪名高いIE6や7の対応はほぼ無くなって2014年現在ではIE8以上としている場合が殆どではないでしょうか。IE8ですら最近は切り捨てられることも増えてきているようです。

さらに最近のブラウザのJavaScriptは進化していて、今までjQueryの領域だと認識されていた部分にも素のJavaScriptが追い付いてきているようです。タイトルにも書きましたが、Vanilla JavaScriptというのは新しいライブラリやフレームワーク等ではなく、素で真っ白な状態のJavaScriptという意味です。

Vanilla JavaScriptだと何がいいのか?jQuery等の読み込みが無くなるのでhttpリクエストをひとつ減らせます。ただ最近ではサイトの公開時にJavaScriptをファイル1つにまとめてを圧縮しておくことも多くなったのでそこまで大きなメリットのようには思えません。ただスマートフォンやタブレットが普及しサイトの半数近くがそれらのモバイルデバイスであり、PCにおいてもモダンブラウザが多数を占めているなら簡単なものはVanilla JavaScriptで書くことで読み込みや実行速度を高めることに一役買うでしょう。

またjQueryの内部ではなにが行われているのか?全ての処理がjQueryの独自実装で動いているのか?そんなことはありません。実はブラウザに同様の機能がある場合はそちらを使うように設計されています。ですのでアニメーションなどは行わずにシンプルなDOM操作のみの場合はjQueryの旨味を十分に発揮させていないことがあるのです。

今回はそんなVanilla JavaScriptではコードをどのように記述していくのかを紹介したいと思います。

イベントの登録を行ってみよう

まずはなにはなくともブラウザ上で動くJavaScriptといえばイベントハンドリングが出来ないと話になりません。なぜならhtmlが読み込まれDOMが構築された後でないと満足にページ内の操作が行えないからです。

body要素にonload属性を追加して関数を実行したり、windowオブジェクトのonloadプロパティに関数を代入する方法でも確実にDOM構築後にスクリプトを実行することが可能ですが、せっかくならjQueryで普通にやっていることを置き換える形で試してみたいと思います。

<script type="text/javascript">
   function start() {
     ...
   }
 </script>
 <body onload="start()">
   ...
 </body>

これでも動くし

<script type="text/javascript">
   window.onload = function() {
     ...
   }
 </script>

これでも動くけど…

<script type="text/javascript">
   jQuery(function($) {
     ...
   });
 </script>

せっかくならこれに置き換わる形で書こう!

JavaScriptでイベント登録を行う関数といえばaddEventListenerがありますが、残念ながらこれはIE8では動作しません。ではどうするか?IE8以前にはattachEventという関数があります。これをif文で使い分けるようにするaddEvent関数を作って対応しましょう。

function addEvent(obj, event_name, handler) {
   if (obj.addEventListener) {
     // addEventListenerが使える場合
     obj.addEventListener(event_name, handler, false);
   } else if (obj.attachEvent) {
     // attachEventが使える場合
     obj.attachEvent('on' + event_name, handler);
   }
 }

次にこれを使ってDOM構築が完了したイベントにハンドラを登録するには以下のようにします。

addEvent(window, 'load', function() {
   alert('Hello');
 });

上記のように書けばwindowのloadイベントに対して何度イベントハンドラを登録したとしても先の処理を上書きすること無く全てが実行されます。

addEvent(window, 'load', function() {
   alert('Hello');
 });
 addEvent(window, 'load', function() {
  alert('World');
 });

HelloにつづいてWorldもアラートされるはずです。

jQueryのようにHTML要素を取得できるquerySelector、querySelectorAllを使ってみよう

イベントハンドリングの壁は超えられたので次にhtml要素を取得してみましょう。

jQueryの大きな特徴としてcssのように使える便利なセレクターがあります。これはページ内にあるDOMノードをJavaScriptから見つけるために欠かせない手順ですが、従来のJavaScriptではあまり開発者にやさしくないものでした。

しかしモダンなブラウザはもちろんIE8以降で使える関数でquerySelectorとquerySelectorAllという2つがあります。前者はセレクタに該当する最初の要素を取得し、後者はセレクタに該当する全ての要素を取得して配列で返してくれます。

<h1>Hello</h1>

このような要素があったとして

addEvent(window, 'load', function() {
  var h1 = document.querySelector('h1');
  alert(h1.innerText);
 });

というスクリプトを書くとh1要素の中身のテキストがアラートされます。 ちなみにquerySelectorを使った場合はh1要素が複数あった場合最初の要素のみ取得されるので

<h1>Hello</h1>
<h1>World</h1>

この場合もHelloのみがアラートされます。

querySelectorAllを使った場合は要素は配列に格納されて返されますので

addEvent(window, 'load', function() {
  var h1s = document.querySelectorAll('h1');
  for (var i in h1s) {
    alert(h1s[i].innerText);
  }
});

と書くとHelloにつづいてWorldもアラートされます。

さらにこれらの関数の便利な点はDOMツリーのスコープが適用されるというところです。上記の例ではdocumentオブジェクトというwindowオブジェクトに続く2番めに大きいオブジェクトですので要素の検索はページ全体に渡ります。しかし2つ目のdiv要素の中にあるh2のテキストのみをアラートしたい場合はどうすればよいでしょうか。しかもh2にはclassもidも降ってないとしたら検索は更に困難になります。

そのような場合にquerySelectorのスコープを活用すると便利です。

<h2>zero</h2>
<div>
  <h2>first</h2>
</div>
<div>
  <h2>second</h2>
</div>

このような要素があるとして

addEvent(window, 'load', function() {
  var h2 = document.querySelectorAll('div')[1].querySelector('h2');
  alert(h2.innerText);
});

このようにすればsecondとアラートされます。
まず最初にdocumentの持っているスコープから2番目のdiv要素を見つけ、そのdiv要素の中にあるh2を見つけたという流れです。querySelectorAllで見つかった要素は配列に格納されているので2番目の要素は[1]で取り出せるというわけですね。

まとめ

いかがでしたでしょうか?このエントリーで書いたJavaScriptはIE8でも問題なく動きます。ちょっとした処理ならjQuery無しで書いてみようかな?という気が少しでも持てれば幸いです。
若干jQuery使用時に比べると簡潔ではありませんが、それでも工夫次第(例えばメソッドチェーンを多用しすぎないとか、別の処理は行を分ける等)で綺麗に書くことも可能だと思います。なによりライブラリに頼らないことでよりJavaScriptに対する理解が深まるのはいいことですよね。

次回では取得した要素に対して変更を行い、実践的な内容に挑戦してみたいと思います。

標準