- 投稿日:2020-08-12T23:55:49+09:00
【⭐️オススメ便利機能⭐️】~インデントを色で識別!?~
【概要】
1.オススメ便利機能とは
2.なぜ入れた方がいいの?
3.どのように使うのか⁉︎
4.導入してわかったこと
1.オススメ便利機能とは
その名も”indent-rainbow”
今すぐ、入れるべき‼︎
2.なぜ入れた方がいいの?
下記の画像を見てください!
インデントが列によって、
レインボーに色別されています!これにより、どの開閉タグ(ex:divでの表示ミス)
がどこに対応しているのか一発でわかります!またタブではなくスペースキーで1文字開けた時は、
赤くなり教えてくるのも大変便利です!3.どのように入れて、使うの⁉︎
Vscodeの
から”indent-rainbow”と検索するだけ!
あとはインストールを押すだけ!あとは緑のインストールボタン押すだけ!
あとは視覚化されるので簡単です!4.導入してわかったこと
I)実は列がレインボーになるだけと思っていました笑
たまたまスペースを押したら赤くなったのでさらに
便利だなと感じました!II)divで表示ミスをすることが頻繁にあったので
列を見易くすることでミスに気付けました!
- 投稿日:2020-08-12T23:15:58+09:00
wwwなしの、SSL(https)にリダイレクトしたいときのNGINXの設定 -Route 53 + ELB (ACM) + EC2構成-
全てのトラフィックをwwwなしの、SSL(https)にリダイレクトしたい
結論
長いので先に結論だけ書きます
SSL証明にACMを使い、ELBに証明書が適応されている状態を想定
ELB <-> EC2の接続がHTTP: 80の場合、NGINXはport 80でlistenする (ここの気付きが重要でした)
NGINXによるリダイレクトはwwwあり->wwwなしのみでよい
httpsリクエストをNGINXの80で受けるという設定に戸惑ったが、動作上これで良さそう
現状と目標
https
かつ、www
なしのドメインをリクエストしないとSSL接続されない理想は以下の全ての条件でSSL接続にリダイレクトしたいが現状は
# wwwあり http://www.mdclip.xyz -> http://mdclip.xyz (no SSL) https://www.mdclip.xyz -> http://mdclip.xyz (no SSL) # wwwなし http://mdclip.xyz -> http://mdclip.xyz (no SSL) https://mdclip.xyz -> https://mdclip.xyz (SSL)環境
- DNS (Route 53) -> ELB -> EC2 (NGINX -> Puma)
- SSL証明: ACM
- Rails 6 (config/environments/production.rb: config.force_ssl = true)
NGINXの設定は
nginx.conf
に書く?conf.d/*.conf
に書くNGINXの設定は
nginx.conf
に記述されているが
その中で/etc/nginx/conf.d/*.conf
がincludeされており
conf.d
以下の内容が呼び出されるようになっている記事によってこの辺りが曖昧でどのように使い分けるか把握しにくかったが
以下の記事をもとにイメージを掴むことができた参考: nginx連載3回目: nginxの設定、その1 - インフラエンジニアway - Powered by HEARTBEATS
NGINXではserverディレクティブ(サーバー個別の設定、server {...}で記述される)で定義された
仮想サーバーを複数稼働させることができる
そして、このバーチャルサーバー毎の設定を置く場所がconf.d
以下とのことなので
conf.d
以下のfoo.conf
には単一のサーバーについての記述に留め
複数のserverディレクティブが記述されているような運用は良くないのかもしれないただし、今回
conf.d
以下の設定ファイルにドメイン間のリダイレクトを担う
serverディレクティブを複数配置してみたが、動作上は問題なさそうでした全てのトラフィックをwwwなしの、SSL(https://)にリダイレクトしたい場合のNGINXの設定
結論
後述の内容から、以下のように書けば良いと理解した
server { listen 80; server_name example.com; return 301 https://example.com$request_uri; } server { listen 80; listen 443 ssl; server_name www.example.com; return 301 https://example.com$request_uri; }もしくは
server { listen 80; server_name example.com www.example.com; return 301 https://example.com$request_uri; } server { listen 443 ssl; server_name www.example.com; return 301 https://example.com$request_uri; }wwwなし -> wwwあり(その逆パターン)
# add 'www' server { listen 80; #非SSL listen 443 ssl; #SSL こうやって2つ指定できる server_name example.com; #wwwなしを return 301 $scheme://www.example.com$request_uri; #wwwありへ } # 上記と同意 server { listen 80; #非SSL server_name example.com; return 301 $scheme://www.example.com$request_uri; } server { listen 443 ssl; #SSL server_name example.com; return 301 $scheme://www.example.com$request_uri; } # remove 'www' server { listen 80; #非SSL listen 443 ssl; #SSL server_name www.example.com; #wwwありを return 301 $scheme://example.com$request_uri; #wwwなしへ }参考: How to Create NGINX Rewrite Rules | NGINX
Adding and Removing the www Prefix
These examples add and remove the www prefix:
# add 'www' server { listen 80; listen 443 ssl; server_name domain.com; return 301 $scheme://www.domain.com$request_uri; } # remove 'www' server { listen 80; listen 443 ssl; server_name www.domain.com; return 301 $scheme://domain.com$request_uri; }Again,
return
is preferable to the equivalentrewrite
, which follows. Therewrite
requires interpreting a regular expression –^(.*)$
– and creating a custom variable ($1
) that in fact is equivalent to the built‑in$request_uri
variable.# NOT RECOMMENDED rewrite ^(.*)$ $scheme://www.domain.com$1 permanent;http(非SSL) -> https(SSL)にリダイレクト
# http(非SSL) -> https(SSL)に転送 server { listen 80; server_name www.example.com; return 301 https://www.example.com$request_uri; }古いドメインから新しいドメインに移行する場合などは
敢えて$request_uri
を省略することでホームページへ都度リダイレクトすることも可能参考: How to Create NGINX Rewrite Rules | NGINX
Example – Forcing all Requests to Use SSL/TLS
This
server
block forces all visitors to use a secured (SSL/TLS) connection to your site.server { listen 80; server_name www.domain.com; return 301 https://www.domain.com$request_uri; }Some other blogs about NGINX rewrite rules use an
if
test and therewrite
directive for this use case, like this:# NOT RECOMMENDED if ($scheme != "https") { rewrite ^ https://www.mydomain.com$uri permanent; }But this method takes extra processing because NGINX must both evaluate the
if
condition and process the regular expression in therewrite
directive.全てのトラフィックを特定のドメインにリダイレクト
server { listen 80 default_server; # http:// listen 443 ssl default_server; # https:// server_name _; # "_"全一致 return 301 $scheme://www.example.com; }参考: How to Create NGINX Rewrite Rules | NGINX
Redirecting All Traffic to the Correct Domain Name
Here’s a special case that redirects incoming traffic to the website’s home page when the request URL doesn’t match any
server
andlocation
blocks, perhaps because the domain name is misspelled. It works by combining thedefault_server
parameter to thelisten
directive and the underscore as the parameter to theserver_name
directive.server { listen 80 default_server; listen 443 ssl default_server; server_name _; return 301 $scheme://www.domain.com; }We use the underscore as the parameter to
server_name
to avoid inadvertently matching a real domain name – it’s safe to assume that no site will ever have the underscore as its domain name. Requests that don’t match any otherserver
blocks in the configuration end up here, though, and thedefault_server
parameter tolisten
tells NGINX to use this block for them. By omitting the$request_uri
variable from the rewritten URL, we redirect all requests to the home page, a good idea because requests with the wrong domain name are particularly likely to use URIs that don’t exist on the website.
rewrite
よりもreturn
を用いることが望ましい理由
if...
処理を都度行うことは効率的でない.rewrite
を用いるとNGINXが正規表現を処理する必要があり効率的でない.- ヒトにとってもNGINXが301コードを返すことが明示的で解釈しやすい.
以上を踏まえたNGINXの設定(NG)
nginx.conf
いずれのバーチャルサーバーにも該当しない場合のトラフィックを受ける
default_server
についての記述のみ残しましたuser nginx; worker_processes auto; error_log /var/log/nginx/error.log; pid /run/nginx.pid; include /usr/share/nginx/modules/*.conf; events { worker_connections 1024; } http { log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; include /etc/nginx/mime.types; default_type application/octet-stream; include /etc/nginx/conf.d/*.conf; server { listen 80 default_server; listen [::]:80 default_server; server_name _; root /usr/share/nginx/html; # Load configuration files for the default server block. include /etc/nginx/default.d/*.conf; location / { } error_page 404 /404.html; location = /40x.html { } error_page 500 502 503 504 /50x.html; location = /50x.html { } } }conf.d/app.conf
error_log /var/www/myapp/shared/log/nginx.error.log; access_log /var/www/myapp/shared/log/nginx.access.log; upstream myapp { server unix://var/www/myapp/shared/tmp/sockets/puma.sock; } ## #1 以下の2つのserverディレクティブで, wwwなしのhttpsにリダイレクト # http > https server { listen 80; server_name example.com; return 301 https://example.com$request_uri; } # www > no_www server { listen 80; listen 443 ssl; server_name www.example.com; return 301 https://example.com$request_uri; } server { listen 443 ssl; # #2 httpsで受ける client_max_body_size 4G; server_name example.com; keepalive_timeout 5; # Location of our static files root /var/www/myapp/current/public; location ~ ^/assets/ { root /var/www/myapp/current/public; } location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_redirect off; if (!-f $request_filename) { proxy_pass http://myapp; break; } } error_page 500 502 503 504 /500.html; location = /500.html { root /var/www/myapp/current/public; } }ところがこの時点での結果は、
# wwwあり http://www.mdclip.xyz -> http://www.mdclip.xyz (no SSL) https://www.mdclip.xyz -> https://www.mdclip.xyz (SSL) # wwwなし http://mdclip.xyz -> http://mdclip.xyz (no SSL) https://mdclip.xyz -> https://mdclip.xyz (SSL)しかも全てのトラフィックがPumaではなくdefault_serverに到達してしまった
以下再度検証へ...
最終検証・解決
AWS Route 53
example.comだけでなく、
www.example.com
のAレコードについても
ルーティング先にELBのエイリアスを指定する必要がありましたこれがないとWWWありのリクエストがNGINXまで到達せず
”サーバーが見つからない”エラーが出ます
ELBの設定を確認
Target groupの設定はEC2インスタンスにHTTP: 80で接続するようになっていました
(ここもHTTPS: 443に変更する方法もあるようです)この状況を図にすると
(これがやりたかった!)重要なのはHTTPSリクエストに対しても
EC2は80ポートでELBと接続されているということ
(言葉が不適切でしたら訂正願います)だから
- NGINX側は443ではなく80でlistenする必要がある
- 443 > 80へのリダイレクトはNGINXで設定不要(上図の緑の部分のみでいい)
NGINXの設定(再・OK)
nginx.conf
ここは変わりなしuser nginx; worker_processes auto; error_log /var/log/nginx/error.log; pid /run/nginx.pid; include /usr/share/nginx/modules/*.conf; events { worker_connections 1024; } http { log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; include /etc/nginx/mime.types; default_type application/octet-stream; include /etc/nginx/conf.d/*.conf; server { listen 80 default_server; listen [::]:80 default_server; server_name _; root /usr/share/nginx/html; # Load configuration files for the default server block. include /etc/nginx/default.d/*.conf; location / { } error_page 404 /404.html; location = /40x.html { } error_page 500 502 503 504 /50x.html; location = /50x.html { } } }conf.d/app.conf
error_log /var/www/myapp/shared/log/nginx.error.log; access_log /var/www/myapp/shared/log/nginx.access.log; upstream myapp { server unix://var/www/myapp/shared/tmp/sockets/puma.sock; } # www > no_www server { listen 80; #443は不要 server_name www.example.com; return 301 https://example.com$request_uri; } server { listen 80; #80でlisten client_max_body_size 4G; server_name example.com; keepalive_timeout 5; # Location of our static files root /var/www/myapp/current/public; location ~ ^/assets/ { root /var/www/myapp/current/public; } location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_redirect off; if (!-f $request_filename) { proxy_pass http://myapp; break; } } error_page 500 502 503 504 /500.html; location = /500.html { root /var/www/myapp/current/public; } }これで以下のように目標が達成されました
# wwwあり http://www.mdclip.xyz -> https://mdclip.xyz (SSL) https://www.mdclip.xyz -> https://mdclip.xyz (SSL) # wwwなし http://mdclip.xyz -> https://mdclip.xyz (SSL) https://mdclip.xyz -> https://mdclip.xyz (SSL)おまけ
AmazonLinux環境でのNGINX関連コマンド
設定をリロード
sudo systemctl reload nginx再起動
sudo systemctl restart nginx起動
sudo systemctl start nginx終了
sudo systemctl stop nginxNGINXのプロセスを確認 (残ったプロセスをkillするときなど)
sudo lsof -i | grep nginx
- 投稿日:2020-08-12T22:55:38+09:00
Ruby on RailsのDevise gemを使ってエラーCouldn't find User with 'id'=sign_inが出た時の対処法
発生したエラー内容
ユーザー登録やログイン認証のためにDevise gemを入れて、さらに自分でUser Controllerを作ったら、以下のエラーメッセージが出て、deviseのログイン画面やユーザー登録画面が動かなくなる、ユーザー情報画面が表示されないといったエラーが発生しました。
- ActiveRecord::RecordNotFound in UsersController#show
- Couldn't find User with 'id'=sign_in(sign_up)
問題が発生した実行環境
- Cloud9(Ubuntu)
- ruby 2.6.3
- Rails 6.0.3.2
- Devise 4.7.2
問題の原因
この問題が発生したのは、user/:idというルーティングが、user/sign_inやuser/sign_upを包括してしまっていたためでした。Railsのルーティングは上から順に読み込んでいき、合致するルーティングを探しているため、resources :users が先に読み込まれて、sign_inやsign_upをidだと認識してしまったことが原因だったようです。
users GET /users(.:format) users#index user GET /users/:id(.:format) users#show new_user_session GET /users/login(.:format) users/sessions#new user_session POST /users/login(.:format) users/sessions#create destroy_user_session DELETE /users/logout(.:format) users/sessions#destroy new_user_password GET /users/password/new(.:format) devise/passwords#new edit_user_password GET /users/password/edit(.:format) devise/passwords#edit user_password PATCH /users/password(.:format) devise/passwords#update PUT /users/password(.:format) devise/passwords#update POST /users/password(.:format) devise/passwords#create cancel_user_registration GET /users/cancel(.:format) users/registrations#cancel new_user_registration GET /users/signup(.:format) users/registrations#new edit_user_registration GET /users/edit(.:format) users/registrations#edit user_registration PATCH /users(.:format) users/registrations#update PUT /users(.:format) users/registrations#update DELETE /users(.:format) users/registrations#destroy POST /users(.:format) users/registrations#create上のようにuser GET /users/:id(.:format) users#show がDevise Gemのルーティングより上に来てしまっている場合、routes.rbは以下のように記述されているかと思います。
routes.rbRails.application.routes.draw do resources :users, only: [:index, :show] devise_for :users, controllers: { sessions: 'users/sessions', registrations: 'users/registrations'}, path_names: { sign_in: 'login', sign_out: 'logout', sign_up: 'signup' } end解決策
routes.rbの書き方を修正します。具体的には、resources :users, only: :index, :showをdevise for :usersの後ろに移動することで解決します。
routes.rbRails.application.routes.draw do devise_for :users, controllers: { sessions: 'users/sessions', registrations: 'users/registrations'}, path_names: { sign_in: 'login', sign_out: 'logout', sign_up: 'signup' } resources :users, only: [:index, :show] end正しい順番で記載してからrails routesで確認してみると、user GET /users/:id(.:format) users#showよりもDevise gemのルーティングが上に来ていることが確認できるかと思われます(上記コードではpath_namesを使ってsign_inをloginに、sign_outをlogoutに変更していますが、大意に影響はありません)。
new_user_session GET /users/login(.:format) users/sessions#new user_session POST /users/login(.:format) users/sessions#create destroy_user_session DELETE /users/logout(.:format) users/sessions#destroy new_user_password GET /users/password/new(.:format) devise/passwords#new edit_user_password GET /users/password/edit(.:format) devise/passwords#edit user_password PATCH /users/password(.:format) devise/passwords#update PUT /users/password(.:format) devise/passwords#update POST /users/password(.:format) devise/passwords#create cancel_user_registration GET /users/cancel(.:format) users/registrations#cancel new_user_registration GET /users/signup(.:format) users/registrations#new edit_user_registration GET /users/edit(.:format) users/registrations#edit user_registration PATCH /users(.:format) users/registrations#update PUT /users(.:format) users/registrations#update DELETE /users(.:format) users/registrations#destroy POST /users(.:format) users/registrations#create users GET /users(.:format) users#index user GET /users/:id(.:format) users#showこの方法に直したら問題なくログインできるようになりました。
補足:devise_forメソッドのルーティングを整理してみました
devise_forメソッドを使うと、かなり複雑なルーティングになってしまう。もしdeviseを使わずにresources :usersでルーティングした場合との対応表を作りましたので、合わせてご査収ください。
resource :users devise_for users#index なし users#new devise/registration#new users#create devise/registration#create users#edit devise/registration#edit users#show なし users#update devise/registration#update users#destroy devise/registration#destroy
- 投稿日:2020-08-12T22:19:28+09:00
ec2使用時にdockerの容量がいっぱいになった際の対処法
dockerを起動しようとしたら以下のエラーが発生した。
$ docker-compose build Failed to write all bytes for _codecs_cn.so fwrite: No space left on device
容量不足により起動不可となっているようだ。
以下のコマンドを叩くと容量を喰っているファイルを見つけることができる。$ df -h ファイルシス サイズ 使用 残り 使用% マウント位置 devtmpfs 474M 0 474M 0% /dev tmpfs 492M 0 492M 0% /dev/shm tmpfs 492M 13M 479M 3% /run tmpfs 492M 0 492M 0% /sys/fs/cgroup /dev/xvda1 8.0G 8.0G 22M 100% / //容量不足 tmpfs 99M 0 99M 0% /run/user/1001/dev/xvda1 というファイルがかなりの容量を占めている。
logなどの不要ファイルを削除しても解決できる場合もあるが、今回はEC2で使用しているEBSのサイズを変更して対応する。
無料利用枠のt2.microでは8GBがデフォルトで、今回は16GBに変更する。(若干料金が発生する:約1$/月)AWSのEC2に移動し、ボリュームに移動する。
目的のボリュームを選択した状態で アクション→ボリュームの変更→サイズを変更(任意の数字:今回は16)→変更
変更が反映され状態がin-useになったらOK。
あとはターミナルからEC2にログインし、以下のコマンドを順に実行していく。$ sudo growpart /dev/xvda 1 CHANGED: partition=1 start=4096 old: size=16773087 end=16777183 new: size=33550303 end=33554399xvdaが16GBに変更されていることが確認できる。
$ lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT xvda 202:0 0 16G 0 disk └─xvda1 202:1 0 8G 0 part /ファイルシステム上は変更されていないので次のコマンドで変更できる。
$ sudo xfs_growfs /dev/xvda1 meta-data=/dev/xvda1 isize=512 agcount=4, agsize=524159 blks = sectsz=512 attr=2, projid32bit=1 = crc=1 finobt=1 spinodes=0 data = bsize=4096 blocks=2096635, imaxpct=25 = sunit=0 swidth=0 blks naming =version 2 bsize=4096 ascii-ci=0 ftype=1 log =internal bsize=4096 blocks=2560, version=2 = sectsz=512 sunit=0 blks, lazy-count=1 realtime =none extsz=4096 blocks=0, rtextents=0 data blocks changed from 2096635 to 4193787変更が反映されていたら以下のように容量が8GB→16GBに変更されており、
使用率が低減できていることが確認できる。$ lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT xvda 202:0 0 16G 0 disk └─xvda1 202:1 0 16G 0 part / $ df -h ファイルシス サイズ 使用 残り 使用% マウント位置 devtmpfs 474M 0 474M 0% /dev tmpfs 492M 0 492M 0% /dev/shm tmpfs 492M 13M 479M 3% /run tmpfs 492M 0 492M 0% /sys/fs/cgroup /dev/xvda1 16G 5.6G 11G 35% / tmpfs 99M 0 99M 0% /run/user/1001
無事に起動できるようになり、容量にもかなりの余裕ができた。
$ docker-compose build Building web : Successfully built 99f5770ca10a Successfully tagged app_web:latest $ df -h ファイルシス サイズ 使用 残り 使用% マウント位置 devtmpfs 474M 0 474M 0% /dev tmpfs 492M 0 492M 0% /dev/shm tmpfs 492M 14M 479M 3% /run tmpfs 492M 0 492M 0% /sys/fs/cgroup /dev/xvda1 16G 8.1G 8.0G 51% / //容量に余裕ができた tmpfs 99M 0 99M 0% /run/user/1001
参考記事
https://www.souichi.club/aws/aws-ebs/
CentOS 7からは、xfs_growfsというコマンドを使うことになったようだ。
https://qiita.com/ponsuke0531/items/04153c6228516e48a55e
- 投稿日:2020-08-12T21:38:24+09:00
【Rails】定数は、config gemで管理しましょう
環境
$ rails -v Rails 6.0.3.1$ ruby -v ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-darwin19]まずは、Gemのインストール
Gemfilegem 'config'$ bundle install --path vendor/bundleconfigの初期設定を行う
configの初期設定のために関連ファイルをインストールする
$ bundle exec rails g config:install定数定義と、使い方
config/settings.ymlservice: name: 'vdeep' url: 'http://vdeep.net' authentication_password: "foobarbaz"$ rails c > Settings.service.name => "vdeep" > Settings.service[:name] => "vdeep" > Settings[:service][:name] => "vdeep" > Settings.authentication_password => "foobarbaz"
- 投稿日:2020-08-12T20:49:14+09:00
独学半年の実務未経験がRails+Nuxt.jsでSPA作ったので見て欲しい
はじめに
アプリを作ったのはいいものの、フィードバックをくれる人がいなかったのでQiitaで紹介記事を書くことにしました。
独学で周りにアドバイスをもらえる人がいないので、改善点などどんどん指摘してくださると幸いです。自己紹介
作者は今年の2月末ごろから独学でプログラミングを学習しています。
高校時代にほんの少しHTMLを触ったことがある程度で、前提知識はほぼありませんでした。
現在はアプリを作成しながら、10月の基本情報に向けて勉強しています。
アプリ制作は今回で2作目になります。
前作URL
前作リポジトリアプリ概要
これが作成したアプリです。
PolPa(ポルパ)は、学習時間に応じてレベルが上がる学習記録アプリです。
全体像がわかると思うので先にERを貼っておきます。機能紹介
主な機能を一部紹介します。
ゲストログイン
新規登録フォームの下からゲストアカウントでログインできます。
気軽に試すことができるので、見学しに来てください。学習時間を記録する
このアプリのメイン機能です。記録された時間に応じて経験値を獲得できます。
タグをつけてジャンル分けできます。
記録した日には草が生えます。
記録にはコメントやいいねができます。ユーザーをフォローする
ユーザーをフォローすると、タイムラインにそのユーザーの記録が表示されるようになります。
他のユーザーと交流して、モチベーションを高め合いましょう!ユーザーを検索する
ユーザー名で検索できます。
リアルタイムに検索結果が変化します。レスポンシブデザイン
スマホやタブレット用のデザインにも対応しているので、モバイル端末から手軽に利用できます。
他にも紹介してない機能がたくさんあるので、ゲストログインで体験してください。
作った理由
サービスとして
他の学習管理アプリを使っていて、今まで積み上げてきたものをはかる指標があればもっとモチベーションを維持しやすくなるのではないかと考え、「学習時間を記録するとレベルが上がる」という要素をプラスした学習管理アプリがあれば面白いのではないか、と開発を始めました。
個人として
個人的には、Vue.jsとRailsを使って何かを作りたかったから開発を始めたという理由もあります。
作る前はこんな程度のアイデアで開発を始めてもいいのだろうかと思っていましたが、開発を進めていくうちに少し愛着が湧いてきました。技術周りの話
使用している主な技術やリソース
- Ruby、Rails ... バックエンド
- Vue.js、Nuxt.js ... フロントエンド
- Vuetify ... UIフレームワーク
- PostgreSQL ... データベース
- Docker、Docker-compose ... コンテナ仮想化
- Git、GitHub ... バージョン管理システム
- Firebase ... ログイン認証・ホスティング
- heroku ... API用サーバー
Rails
バックエンドにはRailsを使用しています。Railsは学習期間が最も長く、前作でも使用しています。
新しく作るアプリは新しい要素を追加したいと思ったので、バックとフロントで別のフレームワークを使用する手法を取ることにしました。Vue.js、Nuxt.js
Vue.jsは以前から興味があり、Railsとの共存も容易だったため、学習を開始しました。
Vue.jsは書籍一冊学習したのみだったので、雰囲気しか理解していない状態からコードを書き始めました。
Nuxt.js自体は開発を始めてから初めて触りました。Vuetify
フロントのデザインはほとんどVuetifyに頼っています。アプリに必要なデザインコンポーネントが一通り揃っているので、一から作るにはめんどくさいデザインも簡単に実装できました。
種類が豊富なので、Vuetifyの公式リファレンスを見るだけで楽しいです。Firebase
ログイン認証にはFirebaseを用いています。初めての開発手法だったため、自力で安全な認証・認可を実装できないと判断し、Firebaseを使用することにしました。
一人で開発して感じたこと
個人で開発をしていく上で良かったことと、つらかったこと・反省点をいくつかまとめてみます。
良かったこと
前作を制作した時も感じていましたが、アプリ制作は本当に勉強になります。
特に独学だとすべてのエラーを自力で解決しなければならないので、デバック力や問題解決能力が磨かれます。
一人で開発するとWeb開発の技術が全般的に必要になるので、次に勉強したい分野が明確になるのもいい点だと思います。つらかったこと・反省点
- エラーの解決方法がわからないとストレスがたまる
- 開発するときのルールを決めておかないと徐々に雑になる
- あれもこれもと機能を追加しているとキリがない
コードが予想に反する動きをして半日潰れたなんてことは日常茶飯事だったので慣れはしましたが、目に見える成果が全く得られないというのはストレスがたまります。ネットで検索してみてもそれらしい情報が全く出てこなかった時は本当に絶望します。
ただ、良かった点にも書いているようにメリットとは表裏一体です。
なんとか解決してきたので、自分の力にもなっているのかなと思います。最初に張り切って、「今回はこの方法にしたがってコーティングして、この手法を使って開発を進めるぞ!」 なんて思っても大体続きませんでした。だんだん雑になってきて、めんどくさいからいいか、一人だからいいかとなりがちでした。次回があれば、ルールの徹底を意識したいと思います。
これからについて
もっと追加したい機能があるので時間があればアップデートしたいと思っています。
10月中旬の基本情報に向けてJavaを勉強するつもりなので、しばらくは時間がないと思いますが、アプリの完成度を高ていきたいのでフィードバックなどよろしくお願いします。
- 投稿日:2020-08-12T20:45:55+09:00
[備忘録] ``expect``の後ろは``()``か``{}``か。 (Module: RSpec::Matchers)
本編
RSpecのマッチャの書式についての個人的な小メモです。
RSpecのバージョン
RSpec 3.9 - rspec-core 3.9.2 - rspec-expectations 3.9.2 - rspec-mocks 3.9.1 - rspec-rails 4.0.0.beta3 - rspec-support 3.9.3
expect
の後ろは()
か{}
か?結論
下記のgem(
rspec-expectations (3.9.2)
)のドキュメントでMathersのページに各マッチャの記法が列挙されています。用例も多く掲載されています。(#changeなど)Module: RSpec::Matchers — Documentation for rspec-expectations (3.9.2)
expect(..).to be_xxx
に{..}
を誤用した例実際にシステムスペックのファイル内で
(..)
を{..}
と書いてテストを実行した場合のエラーメッセージです。
be successful
のマッチャを使用するには、ブロックではなく引数を渡さなければならないということは教えてくれています。エラーメッセージ例You must pass an argument rather than a block to `expect` to use the provided matcher (be successful), or the matcher must implement `supports_block_expectations?`. # ./spec/system/doing_edit_spec.rb:11:in `block (2 levels) in <top (required)>'
- 投稿日:2020-08-12T17:43:39+09:00
jQueryとancestryを使ったカテゴリー選択機能(Ajax)
はじめに
某プログラミングスクールにてフリマアプリを作成。
自分が実装した内容をメモとしてまとめたものです。開発環境
- Rails 5.2.3
- Ruby 2,5.1
- jQuery
- gem ancestry
やりたいこと
某メルカリサイトにある「カテゴリーから探す」にマウスを当てると動的に切り替わるカテゴリー表示機能。
メルカリ
ただし、正直使い難いと個人的に思うので
プラスで以下の条件を付け加えました。
- 親カテゴリー選択後、マウスを動かしただけでは子カテゴリーの表示は変えないこと。
- カテゴリーの枠からマウスが外れてもすぐに消えないこと。
- カテゴリーの表示枠は必要な分の高さだけにすること。
正直余計な条件かもしれないがけど、実際触って個人的に気になったので導入します。
事前準備
gem ancestry
を導入。(導入方法はたくさん記事があるので省略します)- 次にjQuery
gem jquery-rails
を導入、設定する。ここでポイントとなるのが、turbolinksの無効化。
turbolinksは処理の高速化などのメリットもありますが特定の操作で不安定になることがあります。
今回はturbolinksを無効化することでブラウザバック後でも安定した挙動になります。app/assets/javascripts/applications.js// //= require turbolinksの行を削除 //= require jquery //= require jquery_ujs //= require_tree .app/veiws/layouts/application.html.haml= stylesheet_link_tag 'application', media: 'all' = javascript_include_tag 'application' -# 'data-turbolinks-track': 'reload' stylesheet_link_tagのこの項目を削除 -# 'data-turbolinks-track': 'reload' javascript_include_tagのこの項目を削除
ちなみにカテゴリーを登録するseeds.rbはこちらです
db/seeds.rbcategories=[ {level1:"レディース",level1_children:[ {level2:"トップス", level2_children:["Tシャツ/カットソー(半袖/袖なし)","Tシャツ/カットソー(七分/長袖)","シャツ/ブラウス(半袖/袖なし)","シャツ/ブラウス(七分/長袖)","ポロシャツ","キャミソール","タンクトップ","ホルターネック","ニット/セーター","チュニック","カーディガン/ボレロ","アンサンブル","ベスト/ジレ","パーカー","トレーナー/スウェット","ベアトップ/チューブトップ","ジャージ","その他"] }, {level2:"ジャケット/アウター", level2_children:["テーラードジャケット","ノーカラージャケット", "Gジャン/デニムジャケット","レザージャケット","ダウンジャケット","ライダースジャケット","ミリタリージャケット","ダウンベスト","ジャンパー/ブルゾン","ポンチョ","ロングコート","トレンチコート","ダッフルコート","ピーコート","チェスターコート","モッズコート","スタジャン","毛皮/ファーコート","スプリングコート","スカジャン","その他"] }, {level2:"パンツ", level2_children:["デニム/ジーンズ","ショートパンツ","カジュアルパンツ","ハーフパンツ","チノパン","ワークパンツ/カーゴパンツ","クロップドパンツ","サロペット/オーバーオール","オールインワン","サルエルパンツ","ガウチョパンツ","その他"] }, {level2:"スカート", level2_children:["ミニスカート","ひざ丈スカート","ロングスカート","キュロット","その他"] }, {level2:"ワンピース", level2_children:["ミニワンピース","ひざ丈ワンピース","ロングワンピース","その他"] }, {level2:"靴", level2_children:["ハイヒール/パンプス","ブーツ","サンダル","スニーカー","ミュール","モカシン","ローファー/革靴","フラットシューズ/バレエシューズ","長靴/レインシューズ","その他"] }, {level2:"ルームウェア/パジャマ", level2_children:["パジャマ","ルームウェア","その他"] }, {level2:"レッグウェア", level2_children:["ソックス","スパッツ/レギンス","ストッキング/タイツ","レッグウォーマー","その他"] }, {level2:"帽子", level2_children:["ニットキャップ/ビーニー","ハット","ハンチング/ベレー帽","キャップ","キャスケット","麦わら帽子","その他"] }, {level2:"バッグ", level2_children:["ハンドバッグ","トートバッグ","エコバッグ","リュック/バックパック","ボストンバッグ","スポーツバッグ","ショルダーバッグ","クラッチバッグ","ポーチ/バニティ","ボディバッグ/ウェストバッグ","マザーズバッグ","メッセンジャーバッグ","ビジネスバッグ","旅行用バッグ/キャリーバッグ","ショップ袋","和装用バッグ","かごバッグ","その他"] }, {level2:"アクセサリー", level2_children:["ネックレス","ブレスレット","バングル/リストバンド","リング","ピアス(片耳用)","ピアス(両耳用)","イヤリング","アンクレット","ブローチ/コサージュ","チャーム","その他"] }, {level2:"ヘアアクセサリー", level2_children:["ヘアゴム/シュシュ","ヘアバンド/カチューシャ","ヘアピン","その他"] }, {level2:"小物", level2_children:["長財布","折り財布","コインケース/小銭入れ","名刺入れ/定期入れ","キーケース","キーホルダー","手袋/アームカバー","ハンカチ","ベルト","マフラー/ショール","ストール/スヌード","バンダナ/スカーフ","ネックウォーマー","サスペンダー","サングラス/メガネ","モバイルケース/カバー","手帳","イヤマフラー","傘","レインコート/ポンチョ","ミラー","タバコグッズ","その他"] }, {level2:"時計", level2_children:["腕時計(アナログ)","腕時計(デジタル)","ラバーベルト","レザーベルト","金属ベルト","その他"] }, {level2:"ウィッグ/エクステ", level2_children:["前髪ウィッグ","ロングストレート","ロングカール","ショートストレート","ショートカール","その他"] }, {level2:"浴衣/水着", level2_children:["浴衣","着物","振袖","長襦袢/半襦袢","水着セパレート","水着ワンピース","水着スポーツ用","その他"] }, {level2:"スーツ/フォーマル/ドレス", level2_children:["スカートスーツ上下","パンツスーツ上下","ドレス","パーティーバッグ","シューズ","ウェディング","その他"] }, {level2:"マタニティ", level2_children:["トップス","アウター","インナー","ワンピース","パンツ/スパッツ","スカート","パジャマ","授乳服","その他"] }, {level2:"その他", level2_children:["コスプレ","下着","その他"] } ] }, {level1:"メンズ",level1_children:[ {level2:"トップス", level2_children:["Tシャツ/カットソー(半袖/袖なし)","Tシャツ/カットソー(七分/長袖)","シャツ","ポロシャツ","タンクトップ","ニット/セーター","パーカー","カーディガン","スウェット","ジャージ","ベスト","その他"] }, {level2:"ジャケット/アウター", level2_children:["テーラードジャケット","ノーカラージャケット","Gジャン/デニムジャケット","レザージャケット","ダウンジャケット","ライダースジャケット","ミリタリージャケット","ナイロンジャケット","フライトジャケット","ダッフルコート","ピーコート","ステンカラーコート","トレンチコート","モッズコート","チェスターコート","スタジャン","スカジャン","ブルゾン","マウンテンパーカー","ダウンベスト","ポンチョ","カバーオール","その他"] }, {level2:"パンツ", level2_children:["デニム/ジーンズ","ワークパンツ/カーゴパンツ","スラックス","チノパン","ショートパンツ","ペインターパンツ","サルエルパンツ","オーバーオール","その他"] }, {level2:"靴", level2_children:["スニーカー","サンダル","ブーツ","モカシン","ドレス/ビジネス","長靴/レインシューズ","デッキシューズ","その他"] }, {level2:"バッグ", level2_children:["ショルダーバッグ","トートバッグ","ボストンバッグ","リュック/バックパック","ウエストポーチ","ボディーバッグ","ドラムバッグ","ビジネスバッグ","トラベルバッグ","メッセンジャーバッグ","エコバッグ","その他"] }, {level2:"スーツ", level2_children:["スーツジャケット","スーツベスト","スラックス","セットアップ","その他"] }, {level2:"帽子", level2_children:["キャップ","ハット","ニットキャップ/ビーニー","ハンチング/ベレー帽","キャスケット","サンバイザー","その他"] }, {level2:"アクセサリー", level2_children:["ネックレス","ブレスレット","バングル/リストバンド","リング","ピアス(片耳用)","ピアス(両耳用)","アンクレット","その他"] }, {level2:"小物", level2_children:["長財布","折り財布","マネークリップ","コインケース/小銭入れ","名刺入れ/定期入れ","キーケース","キーホルダー","ネクタイ","手袋","ハンカチ","ベルト","マフラー","ストール","バンダナ","ネックウォーマー","サスペンダー","ウォレットチェーン","サングラス/メガネ","モバイルケース/カバー","手帳","ストラップ","ネクタイピン","カフリンクス","イヤマフラー","傘","レインコート","ミラー","タバコグッズ","その他"] }, {level2:"時計", level2_children:["腕時計(アナログ)","腕時計(デジタル)","ラバーベルト","レザーベルト","金属ベルト","その他"] }, {level2:"水着", level2_children:["一般水着","スポーツ用","アクセサリー","その他"] }, {level2:"レッグウェア", level2_children:["ソックス","レギンス/スパッツ","レッグウォーマー","その他"] }, {level2:"アンダーウェア", level2_children:["トランクス","ボクサーパンツ","その他"] }, {level2:"その他", level2_children:["その他"] } ] }, {level1:"ベビー・キッズ",level1_children:[ {level2:"ベビー服(女の子用) ~95cm", level2_children:["トップス","アウター","パンツ","スカート","ワンピース","ベビードレス","おくるみ","下着/肌着","パジャマ","ロンパース","その他"] }, {level2:"ベビー服(男の子用) ~95cm", level2_children:["トップス","アウター","パンツ","おくるみ","下着/肌着","パジャマ","ロンパース","その他"] }, {level2:"ベビー服(男女兼用) ~95cm", level2_children:["トップス","アウター","パンツ","おくるみ","下着/肌着","パジャマ","ロンパース","その他"] }, {level2:"キッズ服(女の子用) 100cm~", level2_children:["コート","ジャケット/上着","トップス(Tシャツ/カットソー)","トップス(トレーナー)","トップス(チュニック)","トップス(タンクトップ)","トップス(その他)","スカート","パンツ","ワンピース","セットアップ","パジャマ","フォーマル/ドレス","和服","浴衣","甚平","水着","その他"] }, {level2:"キッズ服(男の子用) 100cm~", level2_children:["コート","ジャケット/上着","トップス(Tシャツ/カットソー)","トップス(トレーナー)","トップス(その他)","パンツ","セットアップ","パジャマ","フォーマル/ドレス","和服","浴衣","甚平","水着","その他"] }, {level2:"キッズ服(男女兼用) 100cm~", level2_children:["コート","ジャケット/上着","トップス(Tシャツ/カットソー)","トップス(トレーナー)","トップス(その他)","ボトムス","パジャマ","その他"] }, {level2:"キッズ靴", level2_children:["スニーカー","サンダル","ブーツ","長靴","その他"] }, {level2:"子ども用ファッション小物", level2_children:["靴下/スパッツ","帽子","エプロン","サスペンダー","タイツ","ハンカチ","バンダナ","ベルト","マフラー","傘","手袋","スタイ","バッグ","その他"] }, {level2:"おむつ/トイレ/バス", level2_children:["おむつ用品","おまる/補助便座","トレーニングパンツ","ベビーバス","お風呂用品","その他"] }, {level2:"外出/移動用品", level2_children:["ベビーカー","抱っこひも/スリング","チャイルドシート","その他"] }, {level2:"授乳/食事", level2_children:["ミルク","ベビーフード","ベビー用食器","その他"] }, {level2:"ベビー家具/寝具/室内用品", level2_children:["ベッド","布団/毛布","イス","たんす","その他"] }, {level2:"おもちゃ", level2_children:["おふろのおもちゃ","がらがら","オルゴール","ベビージム","手押し車/カタカタ","知育玩具","その他"] }, {level2:"行事/記念品", level2_children:["お宮参り用品","お食い初め用品","アルバム","手形/足形","その他"] }, {level2:"その他", level2_children:["母子手帳用品","その他"] } ] }, {level1:"インテリア・住まい・小物",level1_children:[ {level2:"キッチン/食器", level2_children:["食器","調理器具","収納/キッチン雑貨","弁当用品","カトラリー(スプーン等)","テーブル用品","容器","エプロン","アルコールグッズ","浄水機","その他"] }, {level2:"ベッド/マットレス", level2_children:["セミシングルベッド","シングルベッド","セミダブルベッド","ダブルベッド","ワイドダブルベッド","クイーンベッド","キングベッド","脚付きマットレスベッド","マットレス","すのこベッド","ロフトベッド/システムベッド","簡易ベッド/折りたたみベッド","収納付き","その他"] }, {level2:"ソファ/ソファベッド", level2_children:["ソファセット","シングルソファ","ラブソファ","トリプルソファ","オットマン","コーナーソファ","ビーズソファ/クッションソファ","ローソファ/フロアソファ","ソファベッド","応接セット","ソファカバー","リクライニングソファ","その他"] }, {level2:"椅子/チェア", level2_children:["一般","スツール","ダイニングチェア","ハイバックチェア","ロッキングチェア","座椅子","折り畳みイス","デスクチェア","その他"] }, {level2:"机/テーブル", level2_children:["こたつ","カウンターテーブル","サイドテーブル","センターテーブル","ダイニングテーブル","座卓/ちゃぶ台","アウトドア用","パソコン用","事務机/学習机","その他"] }, {level2:"収納家具", level2_children:["リビング収納","キッチン収納","玄関/屋外収納","バス/トイレ収納","本収納","本/CD/DVD収納","洋服タンス/押入れ収納","電話台/ファックス台","ドレッサー/鏡台","棚/ラック","ケース/ボックス","その他"] }, {level2:"ラグ/カーペット/マット", level2_children:["ラグ","カーペット","ホットカーペット","玄関/キッチンマット","トイレ/バスマット","その他"] }, {level2:"カーテン/ブラインド", level2_children:["カーテン","ブラインド","ロールスクリーン","のれん","その他"] }, {level2:"ライト/照明", level2_children:["蛍光灯/電球","天井照明","フロアスタンド","その他"] }, {level2:"寝具", level2_children:["布団/毛布","枕","シーツ/カバー","その他"] }, {level2:"インテリア小物", level2_children:["ごみ箱","ウェルカムボード","オルゴール","クッション","クッションカバー","スリッパラック","ティッシュボックス","バスケット/かご","フォトフレーム","マガジンラック","モビール","花瓶","灰皿","傘立て","小物入れ","置時計","掛時計/柱時計","鏡(立て掛け式)","鏡(壁掛け式)","置物","風鈴","植物/観葉植物","その他"] }, {level2:"季節/年中行事", level2_children:["正月","成人式","バレンタインデー","ひな祭り","子どもの日","母の日","父の日","サマーギフト/お中元","夏/夏休み","ハロウィン","敬老の日","七五三","お歳暮","クリスマス","冬一般","その他"] }, {level2:"その他", level2_children:["その他"] } ] }, {level1:"本・音楽・ゲーム",level1_children:[ {level2:"本", level2_children:["文学/小説","人文/社会","ノンフィクション/教養","地図/旅行ガイド","ビジネス/経済","健康/医学","コンピュータ/IT","趣味/スポーツ/実用","住まい/暮らし/子育て","アート/エンタメ","洋書","絵本","参考書","その他"] }, {level2:"漫画", level2_children:["全巻セット","少年漫画","少女漫画","青年漫画","女性漫画","同人誌","その他"] }, {level2:"雑誌", level2_children:["アート/エンタメ/ホビー","ファッション","ニュース/総合","趣味/スポーツ","その他"] }, {level2:"CD", level2_children:["邦楽","洋楽","アニメ","クラシック","K-POP/アジア","キッズ/ファミリー","その他"] }, {level2:"DVD/ブルーレイ", level2_children:["外国映画","日本映画","アニメ","TVドラマ","ミュージック","お笑い/バラエティ","スポーツ/フィットネス","キッズ/ファミリー","その他"] }, {level2:"レコード", level2_children:["邦楽","洋楽","その他"] }, {level2:"テレビゲーム", level2_children:["家庭用ゲーム本体","家庭用ゲームソフト","携帯用ゲーム本体","携帯用ゲームソフト","PCゲーム","その他"] } ] }, {level1:"おもちゃ・ホビー・グッズ",level1_children:[ {level2:"おもちゃ", level2_children:["キャラクターグッズ","ぬいぐるみ","小物/アクセサリー","模型/プラモデル","ミニカー","トイラジコン","プラモデル","ホビーラジコン","鉄道模型","その他"] }, {level2:"タレントグッズ", level2_children:["アイドル","ミュージシャン","タレント/お笑い芸人","スポーツ選手","その他"] }, {level2:"コミック/アニメグッズ", level2_children:["ストラップ","キーホルダー","バッジ","カード","クリアファイル","ポスター","タオル","その他"] }, {level2:"トレーディングカード", level2_children:["遊戯王","マジック:ザ・ギャザリング","ポケモンカードゲーム","デュエルマスターズ","バトルスピリッツ","プリパラ","アイカツ","カードファイト!! ヴァンガード","ヴァイスシュヴァルツ","プロ野球オーナーズリーグ","ベースボールヒーローズ","ドラゴンボール","スリーブ","その他"] }, {level2:"フィギュア", level2_children:["コミック/アニメ","特撮","ゲームキャラクター","SF/ファンタジー/ホラー","アメコミ","スポーツ","ミリタリー","その他"] }, {level2:"楽器/器材", level2_children:["エレキギター","アコースティックギター","ベース","エフェクター","アンプ","弦楽器","管楽器","鍵盤楽器","打楽器","和楽器","楽譜/スコア","レコーディング/PA機器","DJ機器","DTM/DAW","その他"] }, {level2:"コレクション", level2_children:["武具","使用済切手/官製はがき","旧貨幣/金貨/銀貨/記念硬貨","印刷物","ノベルティグッズ","その他"] }, {level2:"ミリタリー", level2_children:["トイガン","個人装備","その他"] }, {level2:"美術品", level2_children:["陶芸","ガラス","漆芸","金属工芸","絵画/タペストリ","版画","彫刻/オブジェクト","書","写真","その他"] }, {level2:"アート用品", level2_children:["画材","額縁","その他"] }, {level2:"その他", level2_children:["トランプ/UNO","カルタ/百人一首","ダーツ","ビリヤード","麻雀","パズル/ジグソーパズル","囲碁/将棋","オセロ/チェス","人生ゲーム","野球/サッカーゲーム","スポーツ","三輪車/乗り物","ヨーヨー","模型製作用品","鉄道","航空機","アマチュア無線","パチンコ/パチスロ","その他"] } ] }, {level1:"コスメ・香水・美容",level1_children:[ {level2:"ベースメイク", level2_children:["ファンデーション","化粧下地","コントロールカラー","BBクリーム","CCクリーム","コンシーラー","フェイスパウダー","トライアルセット/サンプル","その他"] }, {level2:"メイクアップ", level2_children:["アイシャドウ","口紅","リップグロス","リップライナー","チーク","フェイスカラー","マスカラ","アイライナー","つけまつげ","アイブロウペンシル","パウダーアイブロウ","眉マスカラ","トライアルセット/サンプル","メイク道具/化粧小物","美顔用品/美顔ローラー","その他"] }, {level2:"ネイルケア", level2_children:["ネイルカラー","カラージェル","ネイルベースコート/トップコート","ネイルアート用品","ネイルパーツ","ネイルチップ/付け爪","手入れ用具","除光液","その他"] }, {level2:"香水", level2_children:["香水(女性用)","香水(男性用)","ユニセックス","ボディミスト","その他"] }, {level2:"スキンケア/基礎化粧品", level2_children:["化粧水/ローション","乳液/ミルク","美容液","フェイスクリーム","洗顔料","クレンジング/メイク落とし","パック/フェイスマスク","ジェル/ゲル","ブースター/導入液","アイケア","リップケア","トライアルセット/サンプル","洗顔グッズ","その他"] }, {level2:"ヘアケア", level2_children:["シャンプー","トリートメント","コンディショナー","リンス","スタイリング剤","カラーリング剤","ブラシ","その他"] }, {level2:"ボディケア", level2_children:["オイル/クリーム","ハンドクリーム","ローション","日焼け止め/サンオイル","ボディソープ","入浴剤","制汗/デオドラント","フットケア","その他"] }, {level2:"オーラルケア", level2_children:["口臭防止/エチケット用品","歯ブラシ","その他"] }, {level2:"リラクゼーション", level2_children:["エッセンシャルオイル","お香/香炉","キャンドル","リラクゼーショングッズ","その他"] }, {level2:"ダイエット", level2_children:["ダイエット食品","エクササイズ用品","体重計","体脂肪計","その他"] }, {level2:"その他", level2_children:["健康用品","看護/介護用品","救急/衛生用品","その他"] } ] }, {level1:"家電・スマホ・カメラ",level1_children:[ {level2:"スマートフォン/携帯電話", level2_children:["スマートフォン本体","バッテリー/充電器","携帯電話本体","PHS本体","その他"] }, {level2:"スマホアクセサリー", level2_children:["Android用ケース","iPhone用ケース","カバー","イヤホンジャック","ストラップ","フィルム","自撮り棒","その他"] }, {level2:"PC/タブレット", level2_children:["タブレット","ノートPC","デスクトップ型PC","ディスプレイ","電子ブックリーダー","PC周辺機器","PCパーツ","その他"] }, {level2:"カメラ", level2_children:["デジタルカメラ","ビデオカメラ","レンズ(単焦点)","レンズ(ズーム)","フィルムカメラ","防犯カメラ","その他"] }, {level2:"テレビ/映像機器", level2_children:["テレビ","プロジェクター","ブルーレイレコーダー","DVDレコーダー","ブルーレイプレーヤー","DVDプレーヤー","映像用ケーブル","その他"] }, {level2:"オーディオ機器", level2_children:["ポータブルプレーヤー","イヤフォン","ヘッドフォン","アンプ","スピーカー","ケーブル/シールド","ラジオ","その他"] }, {level2:"美容/健康", level2_children:["ヘアドライヤー","ヘアアイロン","美容機器","電気シェーバー","電動歯ブラシ","その他"] }, {level2:"冷暖房/空調", level2_children:["エアコン","空気清浄器","加湿器","扇風機","除湿機","ファンヒーター","電気ヒーター","オイルヒーター","ストーブ","ホットカーペット","こたつ","電気毛布","その他"] }, {level2:"生活家電", level2_children:["冷蔵庫","洗濯機","炊飯器","電子レンジ/オーブン","調理機器","アイロン","掃除機","エスプレッソマシン","コーヒーメーカー","衣類乾燥機","その他"] }, {level2:"その他", level2_children:["その他"] } ] }, {level1:"スポーツ・レジャー",level1_children:[ {level2:"ゴルフ", level2_children:["クラブ","ウエア(男性用)","ウエア(女性用)","バッグ","シューズ(男性用)","シューズ(女性用)","アクセサリー","その他"] }, {level2:"フィッシング", level2_children:["ロッド","リール","ルアー用品","ウエア","釣り糸/ライン","その他"] }, {level2:"自転車", level2_children:["自転車本体","ウエア","パーツ","アクセサリー","バッグ","工具/メンテナンス","その他"] }, {level2:"トレーニング/エクササイズ", level2_children:["ランニング","ウォーキング","ヨガ","トレーニング用品","その他"] }, {level2:"野球", level2_children:["ウェア","シューズ","グローブ","バット","アクセサリー","防具","練習機器","記念グッズ","応援グッズ","その他"] }, {level2:"サッカー/フットサル", level2_children:["ウェア","シューズ","ボール","アクセサリー","記念グッズ","応援グッズ","その他"] }, {level2:"テニス", level2_children:["ラケット(硬式用)","ラケット(軟式用)","ウェア","シューズ","ボール","アクセサリー","記念グッズ","応援グッズ","その他"] }, {level2:"スノーボード", level2_children:["ボード","バインディング","ブーツ(男性用)","ブーツ(女性用)","ブーツ(子ども用)","ウエア/装備(男性用)","ウエア/装備(女性用)","ウエア/装備(子ども用)","アクセサリー","バッグ","その他"] }, {level2:"スキー", level2_children:["板","ブーツ(男性用)","ブーツ(女性用)","ブーツ(子ども用)","ビンディング","ウエア(男性用)","ウエア(女性用)","ウエア(子ども用)","ストック","その他"] }, {level2:"その他スポーツ", level2_children:["ダンス/バレエ","サーフィン","バスケットボール","バドミントン","バレーボール","スケートボード","陸上競技","ラグビー","アメリカンフットボール","ボクシング","ボウリング","その他"] }, {level2:"アウトドア", level2_children:["テント/タープ","ライト/ランタン","寝袋/寝具","テーブル/チェア","ストーブ/コンロ","調理器具","食器","登山用品","その他"] }, {level2:"その他", level2_children:["旅行用品","その他"] } ] }, {level1:"ハンドメイド",level1_children:[ {level2:"アクセサリー(女性用)", level2_children:["ピアス","イヤリング","ネックレス","ブレスレット","リング","チャーム","ヘアゴム","アンクレット","その他"] }, {level2:"ファッション/小物", level2_children:["バッグ(女性用)","バッグ(男性用)","財布(女性用)","財布(男性用)","ファッション雑貨","その他"] }, {level2:"アクセサリー/時計", level2_children:["アクセサリー(男性用)","時計(女性用)","時計(男性用)","その他"] }, {level2:"日用品/インテリア", level2_children:["キッチン用品","家具","文房具","アート/写真","アロマ/キャンドル","フラワー/ガーデン","その他"] }, {level2:"趣味/おもちゃ", level2_children:["クラフト/布製品","おもちゃ/人形","その他"] }, {level2:"キッズ/ベビー", level2_children:["ファッション雑貨","スタイ/よだれかけ","外出用品","ネームタグ","その他"] }, {level2:"素材/材料", level2_children:["各種パーツ","生地/糸","型紙/パターン","その他"] }, {level2:"二次創作物", level2_children:["Ingress","クリエイターズ宇宙兄弟","その他"] }, {level2:"その他", level2_children:["その他"] } ] }, {level1:"チケット",level1_children:[ {level2:"音楽", level2_children:["男性アイドル","女性アイドル","韓流","国内アーティスト","海外アーティスト","音楽フェス","声優/アニメ","その他"] }, {level2:"スポーツ", level2_children:["サッカー","野球","テニス","格闘技/プロレス","相撲/武道","ゴルフ","バレーボール","バスケットボール","モータースポーツ","ウィンタースポーツ","その他"] }, {level2:"演劇/芸能", level2_children:["ミュージカル","演劇","伝統芸能","落語","お笑い","オペラ","サーカス","バレエ","その他"] }, {level2:"イベント", level2_children:["声優/アニメ","キッズ/ファミリー","トークショー/講演会","その他"] }, {level2:"映画", level2_children:["邦画","洋画","その他"] }, {level2:"施設利用券", level2_children:["遊園地/テーマパーク","美術館/博物館","スキー場","ゴルフ場","フィットネスクラブ","プール","ボウリング場","水族館","動物園","その他"] }, {level2:"優待券/割引券", level2_children:["ショッピング","レストラン/食事券","フード/ドリンク券","宿泊券","その他"] }, {level2:"その他", level2_children:["その他"] } ] }, {level1:"自動車・オートバイ",level1_children:[ {level2:"自動車本体", level2_children:["国内自動車本体","外国自動車本体"] }, {level2:"自動車タイヤ/ホイール", level2_children:["タイヤ/ホイールセット","タイヤ","ホイール","その他"] }, {level2:"自動車パーツ", level2_children:["サスペンション","ブレーキ","外装、エアロパーツ","ライト","内装品、シート","ステアリング","マフラー・排気系","エンジン、過給器、冷却装置","クラッチ、ミッション、駆動系","電装品","補強パーツ","汎用パーツ","外国自動車用パーツ","その他"] }, {level2:"自動車アクセサリー", level2_children:["車内アクセサリー","カーナビ","カーオーディオ","車外アクセサリー","メンテナンス用品","チャイルドシート","ドライブレコーダー","レーダー探知機","カタログ/マニュアル","セキュリティ","ETC","その他"] }, {level2:"オートバイ車体", level2_children:["国内産","外国産"] }, {level2:"オートバイパーツ", level2_children:["タイヤ","マフラー","エンジン、冷却装置","カウル、フェンダー、外装","サスペンション","ホイール","シート","ブレーキ","タンク","ライト、ウィンカー","チェーン、スプロケット、駆動系","メーター","電装系","ミラー","外国オートバイ用パーツ","その他"] }, {level2:"オートバイアクセサリー", level2_children:["ヘルメット/シールド","バイクウエア/装備","アクセサリー","メンテナンス","カタログ/マニュアル","その他"] } ] }, {level1:"その他",level1_children:[ {level2:"まとめ売り", level2_children:["セット品","その他"] }, {level2:"ペット用品", level2_children:["ペットフード","犬用品","猫用品","魚用品/水草","小動物用品","爬虫類/両生類用品","かご/おり","鳥用品","虫類用品","その他"] }, {level2:"食品", level2_children:["菓子","米","野菜","果物","調味料","魚介類(加工食品)","肉類(加工食品)","その他 加工食品","その他"] }, {level2:"飲料/酒", level2_children:["コーヒー","ソフトドリンク","ミネラルウォーター","茶","ウイスキー","ワイン","ブランデー","焼酎","日本酒","ビール、発泡酒","その他"] }, {level2:"日用品/生活雑貨/旅行", level2_children:["タオル/バス用品","日用品/生活雑貨","洗剤/柔軟剤","旅行用品","防災関連グッズ","その他"] }, {level2:"アンティーク/コレクション", level2_children:["雑貨","工芸品","家具","印刷物","その他"] }, {level2:"文房具/事務用品", level2_children:["筆記具","ノート/メモ帳","テープ/マスキングテープ","カレンダー/スケジュール","アルバム/スクラップ","ファイル/バインダー","はさみ/カッター","カードホルダー/名刺管理","のり/ホッチキス","その他"] }, {level2:"事務/店舗用品", level2_children:["オフィス用品一般","オフィス家具","店舗用品","OA機器","ラッピング/包装","その他"] }, {level2:"その他", level2_children:["その他"] } ] } ] categories.each.with_index(1) do |category,i| level1_var="@category#{i}" level1_val= Category.create(name:"#{category[:level1]}") eval("#{level1_var} = level1_val") category[:level1_children].each.with_index(1) do |level1_child,j| level2_var="#{level1_var}_#{j}" level2_val= eval("#{level1_var}.children.create(name:level1_child[:level2])") eval("#{level2_var} = level2_val") level1_child[:level2_children].each do |level2_children_val| eval("#{level2_var}.children.create(name:level2_children_val)") end end endこの状態で
rake db:seed
すればancestry
の形式でDBにカテゴリーを登録できます。
- ajaxを行うためのルーティングとコントローラーの設定を行います。 (今回は
categories_controller.rb
に記載します)config/routes.rbRails.application.routes.draw do resources :categories do collection do get :search end end endapp/controllers/categories_controller.rbclass CategoriesController < ApplicationController before_action :set_parents def search respond_to do |format| format.html format.json end private def set_parents @parents = Category.where(ancestry: nil) end end
- 今回の機能を表示させるところには上記のbefore_actionを各コントローラーに追加してください。
コードを書く
まずは表示させる場所を作ります。(今回は部分テンプレートのヘッダーのビューに記載してます。)
app/veiws/shared/_header.html.haml%ul.header__headerInner__nav__listsLeft %li.header__headerInner__nav__listsLeft__item = link_to categories_path, id: 'categoBtn' do カテゴリーから探す #tree_menu %ul.categoryTree - @parents.each do |parent| %li.category_parent = link_to category_path(parent) do %input{type: "button", value: "#{parent.name}", name: "#{parent.id}", class: "parent_btn"} %li.category_parent = link_to categories_path do %input{type: "button", value: "全てのカテゴリー", id: "all_btn"} %ul.categoryTree-child %ul.categoryTree-grandchild
(参考)今回使用したスタイルシートはこちらです
#tree_menu { width: 200px; position: absolute; z-index: 10; display: flex; .categoryTree { display: none; height: 100%; border: 1px solid #34AEB3; background-color: #ffffff; border-radius: 4px; box-shadow: 4px 4px 4px #999; a { padding: 0; input[type="button"] { min-width: 200px; height: 35px; font-size: 14px; text-decoration: none; color: #3ccace; background: #ffffff; outline: none; transition: .4s; border: #ffffff; padding: 0 0.9em; cursor: pointer; } input[type="button"]:hover { background: #62d4d7; color: white; } input[type="button"]:active { -webkit-transform: translateY(2px); transform: translateY(2px); box-shadow: 0 0 1px rgba(0, 0, 0, 0.15); background-image: linear-gradient(#b1e9eb 0%, #30a1a4 100%); } } } .categoryTree-child { display: none; height: 100%; border: 1px solid #34AEB3; background-color: #ffffff; border-radius: 4px; box-shadow: 4px 4px 4px #999; .category_child { a { padding: 0; input[type="button"] { min-width: 230px; height: 35px; font-size: 14px; text-decoration: none; color: #3ccace; background: #ffffff; outline: none; transition: .4s; border: #ffffff; padding: 0 0.9em; cursor: pointer; } input[type="button"]:hover { background: #62d4d7; color: white; } input[type="button"]:active { -webkit-transform: translateY(2px); transform: translateY(2px); box-shadow: 0 0 1px rgba(0, 0, 0, 0.15); background-image: linear-gradient(#b1e9eb 0%, #30a1a4 100%); } } } } .categoryTree-grandchild { display: none; height: 100%; border: 1px solid #34AEB3; background-color: #ffffff; border-radius: 4px; box-shadow: 4px 4px 4px #999; a { padding: 0; input[type="button"] { min-width: 250px; height: 35px; font-size: 14px; text-decoration: none; color: #3ccace; background: #ffffff; outline: none; transition: .4s; border: #ffffff; padding: 0 0.9em; cursor: pointer; } input[type="button"]:hover { border: 1px solid #62d4d7; background: #62d4d7; color: white; } input[type="button"]:active { -webkit-transform: translateY(2px); transform: translateY(2px); box-shadow: 0 0 1px rgba(0, 0, 0, 0.15); background-image: linear-gradient(#b1e9eb 0%, #30a1a4 100%); } } } }
次にjQueryを書いていきます。
まずはマウスが「カテゴリーを探す」ボタンに来たときに最初のイベントを発火させます。app/assets/javascripts/categories.js$(document).ready(function () { // 親カテゴリーを表示 $('#categoBtn').hover(function (e) { e.preventDefault(); e.stopPropagation(); $('#tree_menu').show(); $('.categoryTree').show(); }, function () { // あえて何も記述しない }); });
- スタイルシートでは
categoryTree
,categoryTree-child
,categoryTree-grandchild
をdisplay: none;
で隠してます。ここまでで最初のカテゴリーが表示されます。
次にそれぞれのカテゴリーのボタンにカーソルが来た時に子を表示、子のところに来たら孫を表示させるため、上記の下の行にajax通信を行う記述を追記します。
app/assets/javascripts/categories.js$(document).ready(function () { // 省略 function childBuild(children) { let child_category = ` <li class="category_child"> <a href="/categories/${children.id}"><input class="child_btn" type="button" value="${children.name}" name= "${children.id}"> </a> </li> ` return child_category; } function gcBuild(children) { let gc_category = ` <li class="category_grandchild"> <a href="/categories/${children.id}"><input class="gc_btn" type="button" value="${children.name}" name= "${children.id}"> </a> </li> ` return gc_category; } // 子カテゴリーを表示 $('.parent_btn').hover(function () { let categoryParent = $(this).attr('name'); $.ajax({ url: '/products/search', type: 'GET', data: { parent_id: categoryParent }, dataType: 'json' }) .done(function (data) { $(".categoryTree-grandchild").hide(); $(".category_child").remove(); $(".category_grandchild").remove(); $('.categoryTree-child').show(); data.forEach(function (child) { let child_html = childBuild(child) $(".categoryTree-child").append(child_html); }); }) .fail(function () { alert("カテゴリーを選択してください"); }); }, function () { // あえて何も記述しない }); // 孫カテゴリーを表示 $(document).on({ mouseenter: function () { let categoryChild = $(this).attr('name'); $.ajax({ url: '/products/search', type: 'GET', data: { children_id: categoryChild }, dataType: 'json' }) .done(function (gc_data) { $(".category_grandchild").remove(); $('.categoryTree-grandchild').show(); gc_data.forEach(function (gc) { let gc_html = gcBuild(gc) $(".categoryTree-grandchild").append(gc_html); }); }) .fail(function () { alert("カテゴリーを選択してください"); }); }, mouseleave: function () { // あえて何も記述しない } }, '.child_btn'); });
- 補足 : ここで、子カテゴリーと孫カテゴリーの表示の記述が結構違うところがあります。 これは動的に追加した要素(子カテゴリー)に対しては記述のやり方を変える必要があります。 例えば
click
、change
アクションの様に// 例 $(document).on('hover', "#hoge", function () { // 実行内容の記述 });上記の様に記述すれば動きそうですが、実は
hover
アクションでは動きません。
(これに気付くまで時間がかかってしまった。)
hover
などのマウスイベントについてちょっと理解が必要になります。【参考サイト】
jQueryのhover()でマウスオーバーの処理
mouseenterとmouseoverの違いそれではajax通信を行ってきます。
まず、先ほど設定したコントローラーのsearch
に各ボタンに割り振ってあるname
の値(id)をパラメーターとして取得してきます。
受け取ったパラメータからそれぞれの子カテゴリー、孫カテゴリーを配列で受け取ります。app/controllers/categories_controller.rb# 省略 def search respond_to do |format| format.html format.json do if params[:parent_id] @childrens = Category.find(params[:parent_id]).children elsif params[:children_id] @grandChilds = Category.find(params[:children_id]).children end end end # 以下省略受け取った配列情報をそれぞれjbuilderでjson形式にします。
app/views/categories/search.json.jbuilderjson.array! @childrens do |child| json.id child.id json.name child.name end json.array! @grandChilds do |gc| json.id gc.id json.name gc.name endここまで上手くいけば、イベント発火時に無事、.doneの処理に行くことができます。
子カテゴリー、孫カテゴリーが切り替わる様になれば一旦は成功です。表示したカテゴリーを非表示にする処理
それでは非表示にする処理を追記していきます。
app/assets/javascripts/categories.js$(document).ready(function () { // 省略 // カテゴリーを非表示 $(document).on({ mouseleave: function (e) { e.stopPropagation(); e.preventDefault(); $(".categoryTree-grandchild").hide(); $(".categoryTree-child").hide(); $(".categoryTree").hide(); $(this).hide(); $(".category_child").remove(); $(".category_grandchild").remove(); }, mouseenter: function () { } }, '#tree_menu');問題点
一旦これで上手くいきそうですが、実は問題点があります。
- 現状だと
#tree_menu
の子要素が変化することにより#tree_menu
自身のボックスの大きさが変化してカーソルがボタンが外れても非表示にならないところが出てきます。- また、子の高さが小さいと子を選択する前に消えたり、動かすたびに子カテゴリーの表示が変わったりしてしまいます。
- 子カテゴリー選択すると、どの親から来たのかわからない。
実に不安定な挙動です。
問題点の解決
今回、この問題を解決する方法として行った作業は
- 子要素を表示した後に親の要素(
#tree_menu
)のボックスの大きさを一番上の親カテゴリーの要素の大きさにに変えてやること。- マウスが当たっている判定を一定時間後に行うこと。
- 子カテゴリーを選択したらその親も表示させる。
まずは、1番目。
気付けば、処理の方法は簡単ですが、気付くまで時間がかかった。
試しに大きさを揃えて固定にしようとしたが、そうすると見た目が好みじゃないので固定は却下しました。
- やり方は.done処理後にcssを変えるだけです。
次に、2番目。
メルカリでは
各ボックスの大きさを揃えてカーソルを上下に動かさず、横にずらして子カテゴリーを選択しています。しかし、使い勝手がいいと思えない。
- そこで、
setTimeout()
とclearTimeout()
を駆使することで処理を実行するか否かを判断して解決できそう。 ついでにカテゴリーボタンの周りの挙動も手を加えます。最後に3番目。
子と孫のボタンの
name
からそれぞれの親のidを取得して該当する要素を見つけて表示を変えるだけです。
- コントローラーに孫ボタンのパラメータの処理を追記。jbuilderに
root.id
とparent.id
の取得も追記します。親に戻ったら指定したcssを外せば元に戻ります。
最終的なコードを確認したい方はこちら
app/controllers/categories_controller.rbclass CategoriesController < ApplicationController before_action :set_parents def search respond_to do |format| format.html format.json do if params[:parent_id] @childrens = Category.find(params[:parent_id]).children elsif params[:children_id] @grandChilds = Category.find(params[:children_id]).children elsif params[:gcchildren_id] @parents = Category.where(id: params[:gcchildren_id]) end end end private def set_parents @parents = Category.where(ancestry: nil) end endapp/views/categories/search.json.jbuilderjson.array! @childrens do |child| json.id child.id json.name child.name end json.array! @grandChilds do |gc| json.id gc.id json.name gc.name json.root gc.root_id end json.array! @parents do |parent| json.parent parent.parent_id endapp/assets/javascripts/categories.js$(document).ready(function () { // 非同期にてヘッダーのカテゴリーを表示 function childBuild(children) { let child_category = ` <li class="category_child"> <a href="/categories/${children.id}"><input class="child_btn" type="button" value="${children.name}" name= "${children.id}"> </a> </li> ` return child_category; } function gcBuild(children) { let gc_category = ` <li class="category_grandchild"> <a href="/categories/${children.id}"><input class="gc_btn" type="button" value="${children.name}" name= "${children.id}"> </a> </li> ` return gc_category; } // 親カテゴリーを表示 $('#categoBtn').hover(function (e) { e.preventDefault(); e.stopPropagation(); timeOut = setTimeout(function () { $('#tree_menu').show(); $('.categoryTree').show(); }, 500) }, function () { clearTimeout(timeOut) }); // 子カテゴリーを表示 $('.parent_btn').hover(function () { $('.parent_btn').css('color', ''); $('.parent_btn').css('background-color', ''); let categoryParent = $(this).attr('name'); timeParent = setTimeout(function () { $.ajax({ url: '/products/search', type: 'GET', data: { parent_id: categoryParent }, dataType: 'json' }) .done(function (data) { $(".categoryTree-grandchild").hide(); $(".category_child").remove(); $(".category_grandchild").remove(); $('.categoryTree-child').show(); data.forEach(function (child) { let child_html = childBuild(child) $(".categoryTree-child").append(child_html); }); $('#tree_menu').css('max-height', '490px'); }) .fail(function () { alert("カテゴリーを選択してください"); }); }, 400) }, function () { clearTimeout(timeParent); }); // 孫カテゴリーを表示 $(document).on({ mouseenter: function () { $('.child_btn').css('color', ''); $('.child_btn').css('background-color', ''); let categoryChild = $(this).attr('name'); timeChild = setTimeout(function () { $.ajax({ url: '/products/search', type: 'GET', data: { children_id: categoryChild }, dataType: 'json' }) .done(function (gc_data) { $(".category_grandchild").remove(); $('.categoryTree-grandchild').show(); gc_data.forEach(function (gc) { let gc_html = gcBuild(gc) $(".categoryTree-grandchild").append(gc_html); let parcol = $('.categoryTree').find(`input[name="${gc.root}"]`); $(parcol).css('color', 'white'); $(parcol).css('background-color', '#b1e9eb'); }); $('#tree_menu').css('max-height', '490px'); }) .fail(function () { alert("カテゴリーを選択してください"); }); }, 400) }, mouseleave: function () { clearTimeout(timeChild); } }, '.child_btn'); // 孫カテゴリーを選択時 $(document).on({ mouseenter: function () { let categoryGc = $(this).attr('name'); timeGc = setTimeout(function () { $.ajax({ url: '/products/search', type: 'GET', data: { gcchildren_id: categoryGc }, dataType: 'json' }) .done(function (gc_result) { let childcol = $('.categoryTree-child').find(`input[name="${gc_result[0].parent}"]`); $(childcol).css('color', 'white'); $(childcol).css('background-color', '#b1e9eb'); $('#tree_menu').css('max-height', '490px'); }) .fail(function () { alert("カテゴリーを選択してください"); }); }, 400) }, mouseleave: function () { clearTimeout(timeGc); } }, '.gc_btn'); // カテゴリー一覧ページのボタン $('#all_btn').hover(function (e) { e.preventDefault(); e.stopPropagation(); $(".categoryTree-grandchild").hide(); $(".categoryTree-child").hide(); $(".category_grandchild").remove(); $(".category_child").remove(); }, function () { // あえて何も記述しないことで親要素に外れた際のアクションだけを伝搬する }); // カテゴリーを非表示(カテゴリーメニュから0.8秒以上カーソルを外したら消える) $(document).on({ mouseleave: function (e) { e.stopPropagation(); e.preventDefault(); timeChosed = setTimeout(function () { $(".categoryTree-grandchild").hide(); $(".categoryTree-child").hide(); $(".categoryTree").hide(); $(this).hide(); $('.parent_btn').css('color', ''); $('.parent_btn').css('background-color', ''); $(".category_child").remove(); $(".category_grandchild").remove(); }, 800); }, mouseenter: function () { clearTimeout(timeChosed); } }, '#tree_menu'); // カテゴリーボタンの処理 $(document).on({ mouseenter: function (e) { e.stopPropagation(); e.preventDefault(); timeOpened = setTimeout(function () { $('#tree_menu').show(); $('.categoryTree').show(); }, 500); }, mouseleave: function (e) { e.stopPropagation(); e.preventDefault(); clearTimeout(timeOpened); $(".categoryTree-grandchild").hide(); $(".categoryTree-child").hide(); $(".categoryTree").hide(); $("#tree_menu").hide(); $(".category_child").remove(); $(".category_grandchild").remove(); } }, '.header__headerInner__nav__listsLeft__item'); });
まとめ
以上で、追加条件を満たした実装ができました。
コードの書き方はもっとスマートなやり方があるかもしれません。
もし、間違ったところあればコメントにてアドバイスいただければ幸いです。
- 投稿日:2020-08-12T15:56:56+09:00
Could not load the 'listen' gem. Add `gem 'listen'` to the development group of your Gemfile (LoadError)というエラーの解決法について
Could not load the 'listen' gem. Add
gem 'listen'
to the development group of your Gemfile (LoadError)というエラーの解決法について原因は、gemのlistenが開発環境にあることにある。
つまり、
gem 'listen'をdevelopmentだけではなく、全体に影響させればいいので、gemfileの最後に記述するとかして、bundle install bundle updateすれば、解決するだろう。
- 投稿日:2020-08-12T12:35:40+09:00
GitHubに100MB超えのファイルをプッシュしてエラーになった
Railsアプリを開発中に、GitHubにプッシュできなくなりました。
GitHubに100MB制限があることを知らず、少し苦戦しましたので備忘録として。エラー
いつもどおり、
git add
git comit
git push
と進めるとエラーが出ました。
development.logが100MBを超えてますよ、ということです。・・・ remote: error: GH001: Large files detected. You may want to try Git Large File Storage - https://git-lfs.github.com. remote: error: Trace: 13ec04b062a0cd9bb1a20e6a0b921ec7cf7396c0 remote: error: See http://git.io/iEPt8g for more information. remote: error: File log/development.log is 150.13 MB; this exceeds GitHub's file size limit of 100.00 MB ・・・100MB超ファイルをアップする方法もあるようですが、今回はLogファイルだったので削除する方針で進めます。
100MB超ファイルをプッシュする方法(参考)
https://qiita.com/kanaya/items/ad52f25da32cb5aa19e6解決手順
- 巨大ファイルのコミットを取り消す
- 巨大ファイルを削除(Gitから除外)
- 再度コミット
- 再度プッシュ
1.巨大ファイルのコミットを取り消す
直前のコミットであればこれで取り消せます。
$ git reset --hard HEAD直前ではないときは、該当するリビジョンを調べてその1個前まで戻ります。
$ git log コミット履歴がズラッと出るので、該当リビジョンを確認 commit 13ec04b062a0cd9bb1a20e6a0b921ec7cf7396c0 ↑これが該当リビジョンの1個前$ git reset --soft 13ec04b062a0cd9bb1a20e6a0b921ec7cf7396c0これで巨大ファイルのコミットの前に戻りました。
2.巨大ファイルを削除(Gitから除外)
単純に巨大ファイルを削除(中身を空に)してもよいのですが、Railsの仕様上いずれまた100MBを超えることになるのでGitから除外します。
.gitignoreでGitから除外する
.gitignoreに除外ファイルを追加します。
.gitignore/log/development.log.gitignore 書き方(参考)
https://qiita.com/inabe49/items/16ee3d9d1ce68daa9fff最初から除外したほうが楽
このツールで、各言語に最適化された.gitignoreがゲットできます。
https://www.toptal.com/developers/gitignore3.再度コミット
あとはいつもどおりと言いたいところですが、ちゃんと除外されているか確認しましょう。
自分はここで何度もやり直しました。除外してもコミットされてしまうときは
Gitのキャッシュが残っている場合は、こちらを参考にキャッシュを削除してください。
(ちなみにVS CODEだと、除外ファイルはグレーアウトされます。)
https://qiita.com/fuwamaki/items/3ed021163e50beab71544.再度プッシュ
最後にプッシュすればコンプリートです!
- 投稿日:2020-08-12T11:23:02+09:00
検索機能の実装
検索フォームを作成
検索の入力欄とボタンには、フォームを使います。
app/views/tweets/index.html.erb
<%= form_with(url: search_tweets_path, local: true, method: :get, class: "search-form") do |form| %> <%= form.text_field :keyword, placeholder: "投稿を検索する", class: "search-input" %> <%= form.submit "検索", class: "search-btn" %> <% end %>searchアクションのルーティングを設定
collection do get 'search' endを以下のように追加
config/routes.rb
Rails.application.routes.draw do devise_for :users root to: 'tweets#index' resources :tweets do resources :comments, only: :create collection do get 'search' end end resources :users, only: :show end検索するメソッドをTweetモデルに定義
def self.search(search) if search Tweet.where('text LIKE(?)', "%#{search}%") else Tweet.all end endを以下のように追加
app/models/tweet.rb
class Tweet < ApplicationRecord validates :text, presence: true belongs_to :user has_many :comments def self.search(search) return Tweet.all unless search Tweet.where('text LIKE(?)', "%#{search}%") end endsearchアクションをコントローラーに定義
コントローラーに基本の7つのアクションには含まれない、searchアクションを定義します。
app/controllers/tweets_controller.rb
before_action :move_to_index, except: [:index, :show, :search] def search @tweets = Tweet.search(params[:keyword]) endTweetモデルに書いたsearchメソッドを呼び出しています。seachメソッドの引数にparams[:keyword]と記述して、検索結果を渡しています。
また、未ログイン状態にトップページへリダイレクトされてしまうことを回避するため、before_actionのexceptオプションに:searchを追加しています。検索結果画面のビューを作成しよう
app/views/tweetsディレクトリにsearch.html.erbを作成する。
検索結果を表示するように記述します。
app/views/tweets/search.html.erb
<%= form_with(url: search_tweets_path, local: true, method: :get, class: "search-form") do |form| %> <%= form.text_field :keyword, placeholder: "投稿を検索する", class: "search-input" %> <%= form.submit "検索", class: "search-btn" %> <% end %> <div class="contents row"> <% @tweets.each do |tweet| %> <%= render partial: "tweet", locals: { tweet: tweet } %> <% end %> </div>
- 投稿日:2020-08-12T11:17:22+09:00
【Rails】deviseで行うアクセス制限にエラーメッセージを表示させる。
はじめに
deviseを使っていると必ずと言っていいほど使うアクセス制限をかけるためのヘルパーメソッドauthenticate_user!。
制限をかけた際にログイン画面にページ遷移しますが、何も設定をしないとなぜページ遷移したかわかりにくいので「ログインして下さい。」等々エラーメッセージを表示させた方が、ユーザー目線を考慮したサイトにすることができます。ポートフォリオを作成していた際に、そんな風に思い組み込んだことを備忘録として書き留めておきます。
①エラーメッセージの表示方法
②エラーメッセージの日本語化前提
・gem deviseを使用している。
・before_action :authenticate_user!を使用したいコントローラに記述している。
【参考サイト】
Rails deviseで使えるようになるヘルパーメソッド一覧①エラーメッセージの表示方法
authenticate_user!を使用した際に「ログインもしくはサインアップが必要です」のようなメッセージを表示させたい際は、下記記述を任意のviewにしてあげれば出ます。
application.html.erb<%= alert %>上記記述をしてauthenticate_user!が正常に動作した場合、
以下devise.en.ymlの17行目あたりに記載してあるエラー文が出るはずです。config/locales/devise.en.ymlunauthenticated: "You need to sign in or sign up before continuing."deviseには元々エラー文章が用意されているようなので、
あとはそれを呼び出すための記述をしてあげれば良いわけですね。
簡単簡単!『...余談』
<%= notice %>を使用すればログイン時に「ログインしました」的な文章を出すことも可能です。
簡単ですね。②エラーメッセージの日本語化
devise.ja.ymlの導入
先ほどはdevise.en.ymlの中から文章を引っ張ってました。
今回は日本語化するためにdevise.ja.ymlを導入していきます。
(en=english / ja=japaneseの意)config/localesにdevise.ja.ymlファイルを作成し、
中身はdevise-i18n/ja.yml at master - GitHubからコピペして保存。gem 'rails-i18n'のインストール
Gemfilegem 'rails-i18n'上記記述をして、bundle installを実行。
インストールはこれで完了です。config/application.rbに記述を追加
config/application.rbconfig.i18n.default_locale = :ja config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}').to_s]1行目だけでも機能する場合もありますが、しない時もあるらしく、
2行目は必要に応じて記述してください。以上で、日本語化は完了。
- 投稿日:2020-08-12T10:52:50+09:00
Railsアプリで時間を表示したときの「UTC」を消したい(Railsのタイムゾーンの扱い方)
プログラミングの勉強日記
2020年8月12日 Progate Lv.226
目標
Railsアプリで投稿を更新した時間を表示させるようにした。
views/posts/index.html.erb<%= post.updated_at %>投稿を更新した日時を表示することができたが、時間の後ろに『UTC』という文字列が入ってしまっている。
『2020-08-08 11:56:59 UTC』これを消して、以下のように表示させたい!!
『2020-08-08 11:56:59』方法
RubyにはTime::strftimeメソッドがあり、これで時刻を好きなフォーマットに変更できる。
views/posts/index.html.erb<%= post.updated_at.strftime("%Y-%m-%d %H:%M:%S") %>これでUTCが消され、以下のように表示されるようになった。
『2020-08-08 11:56:59』
対応する日時 フォーマットの指定子 %Y 年(year) %m 月(month) %d 日にち(day) %H 時間(hour) %M 分(minutes) %S 秒(seconds) なので、このように自由にフォーマットを変えることができる。
views/posts/index.html.erb<%# 2020-08-08 11:56 %> <%= post.updated_at.strftime("%Y-%m-%d %H:%M") %> <%# 2020年 08月 08日 %> <%= post.updated_at.strftime("%Y年 %m月 %d日") %>UTCとは
UTCは協定世界時のことである。国や地域ごとに時差があり、タイムゾーンと呼ばれる共通の時間を使うエリアの一帯のことを指す。日本で使われている時間は日本標準時(JST)と呼ばれている。
RailsではデフォルトでUTCが設定されている。UTCの変更方法
Railsのタイムゾーンは
config/application.rb
で設定できる。config.time_zone
とconfig.active_record.default_zimezone
の2つの方法でタイムゾーンを変更することができる。
config.time_zoneは、Time.zone.nowやTime.currentといったTimeWithZoneに対する設定で、"Tokyo"などの都市名を設定することでタイムゾーンを変更できる。
config.active_record.default_zimezoneはデータベースレコードの読み書きに対する設定で、:utc
と:local
のどちらかを設定することができる。:local
を使用すると、データベースが動作するサーバーのタイムゾーンが使用される。config/application.rbconfig.time_zone = "Tokyo" config.active_record.default_timezone = :localapplication.rbを変更した後、Railsサーバーを再起動する!!
参考文献
Timeメソッドで出てくるタイムゾーンの UTC とかいう文字を消したい
Railsでタイムゾーンを扱う方法を現役エンジニアが解説【初心者向け】
- 投稿日:2020-08-12T04:26:44+09:00
【Rails】もう迷いたくない。モデル、テーブル、コントローラ、ビューの新規作成。
はじめに
この記事はRailsアプリを立ち上げた後の作業をまとめたものです。
何らかの機能を作る際には、モデルを作ってコントローラ作ってルーティング書いて...といった作業が頻発します。
毎回構文を忘れてしまうので、一連の流れをまとめました。コントローラの作成
まずはコントローラの作成です。
今回は画像を保存する機能を作るのでimagesとします。rails g controller imagesモデルの作成
下記のコマンドでimageモデルを作りましょう!
rails g model imageこれでimageモデルが作成されたと思います。
必要であれば、モデルにバリデーションやアソシエーションを記述しましょう。テーブル作成
モデルを作成した際にマイグレーションファイルも作成されたと思います。
このファイルを編集して主キーやnull制約などの設定を行います。
私の場合は、以下のようにして”filename”を主キーにし、null制約をかけています。
主キーにしたのでそんな制約要らないかもしれませんが...class CreateImages < ActiveRecord::Migration[5.2] def change create_table :images, id: false, primary_key: :filename do |t| t.string :filename, null: false t.timestamps end end endそれではマイグレートを実行し、正常終了すればテーブルが作成されます!
rails db:migrateビューの作成
画像の一覧を表示するページということで、index.html.hamlを作りましょう。
これまでの作業でimagesディレクトリが作成されているので、そこに作成します。
ビューの内容については、今回の記事で詰めるところではないので適当に"HelloWorld"を出しておきます。%h1 HelloWorldルーティング
ここまで来ればほぼ出来たようなものです。
routes.rbにimagesのルーティングを記載します。
今回はresourcesメソッドを使用してindexアクションのみを設定しました。Rails.application.routes.draw do resources :images, only: [:index] end結果
出来ました!これで仕事が捗る...!
- 投稿日:2020-08-12T00:21:42+09:00
Rails Tutorialを咀嚼する【第1章ゼロからデプロイまで】後半
1.4 Gitによるバージョン管理
■Git
バージョン管理ツール。Gitで都度都度作業を保存することで、やり直しができたりする。
addしてからcommitしてpushする。■Bitbucket
git専門のホスティングサービス。GitHubの仲間。1.5 デプロイする
■Heroku
Paas。FFFTPとか使わず、開発したものをサーバーにアップロードすることができる。SQLiteが使えないため、若干厄介。【演習】
1も2も大したことないので省略。■('a'..'z').to_a.shuffle[0..7].join
ランダムなアルファベット8文字を表示してくれる。
aからzのアルファベットを配列に格納。
配列の順番をシャッフルする。
0から7番目(8個)まで取り出す。
結合して文字列にする。【演習】
1も2も省略。分ける必要はなかった。後で結合する。