20210513のvue.jsに関する記事は8件です。

DjangoとVue.jsを使ってモーダルコメントフォームを実装する

はじめに JavaScriptはほとんど触ったことがないのですが、最近はVue.jsを勉強しています。 ブログを想定したDjangoアプリ内で、モーダルコメントフォームの実装ができましたので、記事を書いていきます。 こういう感じの動きです。 記事に対するコメントと、コメントに対する返信の投稿をモーダルフォームで作っていきます。 なおモーダルと言えばBootStrapで実装することも多いと思うのですが、今回は自力での実装に挑戦してみました。 Django側のアプリ構築 まずは下記のようなモデルを用意します。 # project/models.py from django.db import models from django.conf import settings from django.utils import timezone class Post(models.Model): title = models.CharField('タイトル', max_length=255) text = models.TextField('本文') writer = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.PROTECT, verbose_name='投稿者') created_at = models.DateTimeField('作成日', auto_now_add=True) updated_at = models.DateTimeField('更新日', auto_now=True) def __str__(self): return self.title class Comment(models.Model): """記事に紐づくコメント""" name = models.CharField('名前', max_length=255, default='名無し') text = models.TextField('本文') target = models.ForeignKey( Post, on_delete=models.PROTECT, verbose_name='記事に対するコメント') created_at = models.DateTimeField('作成日', default=timezone.now) def __str__(self): return self.text[:20] class Reply(models.Model): """コメントに紐づく返信""" name = models.CharField('名前', max_length=255, default='名無し') text = models.TextField('本文') target = models.ForeignKey(Comment, on_delete=models.CASCADE, verbose_name='コメントに対する返信') created_at = models.DateTimeField('作成日', default=timezone.now) def __str__(self): return self.text[:20] 記事に対する投稿のCommentとコメントに対する返信のReplyをここで定義しました。 コメントやリプライは独立したmodelを定義して、models.ForeignKeyで親に結びつけるようにすると、 後に取り出す際に簡単になります。 次にURLパターンを定義します。 # app/urls.py from django.urls import path from . import views app_name = 'app' urlpatterns = [ path('post_detail/<int:pk>', views.PostDetail.as_view(), name='post_detail'), path('comment_create/<int:pk>', views.CommentCreate.as_view(), name='comment_create'), path('reply_create/<int:pk>', views.ReplyCreate.as_view(), name='reply_create'), ] 通常は一覧画面を用意すると思いますが、モーダルコメントの動きの部分だけ実装したいので、上記のような内容となりました。 今回は記事ページからコメントやリプライを投稿するので、コメントやリプライの画面は作成しませんが、Postをする先を指定する必要があるので、定義が必要です。 viewを作成する前にフォームを作っておきます。 #app/forms.py from django import forms from .models import Comment, Reply class CommentCreateForm(forms.ModelForm): """コメント投稿フォーム""" class Meta: model = Comment exclude = ('target', 'created_at') class ReplyCreateForm(forms.ModelForm): """返信コメント投稿フォーム""" class Meta: model = Reply exclude = ('target', 'created_at') 名前と投稿だけの簡易的なものです。 viewを定義していきます。 # app/views.py from django.shortcuts import redirect, get_object_or_404 from django.views import generic from .models import Post, Comment, Reply from .forms import CommentCreateForm, ReplyCreateForm class PostDetail(generic.DetailView): template_name = 'app/post_detail.html' model = Post class CommentCreate(generic.CreateView): """記事へのコメント作成ビュー。""" model = Comment form_class = CommentCreateForm def form_valid(self, form): post_pk = self.kwargs['pk'] post = get_object_or_404(Post, pk=post_pk) comment = form.save(commit=False) comment.target = post comment.save() return redirect('blog:post_detail', pk=post_pk) class ReplyCreate(generic.CreateView): """コメントへの返信作成ビュー。""" model = Reply form_class = ReplyCreateForm def form_valid(self, form): comment_pk = self.kwargs['pk'] comment = get_object_or_404(Comment, pk=comment_pk) reply = form.save(commit=False) reply.target = comment reply.save() return redirect('blog:post_detail', pk=comment.target.pk) コメント作成とリプライ作成はgeneric.CreateViewで作ります。画面は必要ないので、テンプレートは定義しません。 htmlの記述 html部分を記述していきます。 <!-- app/templates/app/post_detail.html --> {% load static %} <html lang="ja"> <head> <meta charset="utf-8"> <title>modal comment</title> <link rel="stylesheet" href="{% static "css/style.css" %}"> <!-- vueの読み込み 本番環境では ...global.prod.jsとする --> <script src="https://cdn.jsdelivr.net/npm/vue@3.0.0/dist/vue.global.js"></script> </head> <body> <div class="container"> <h1 class="post-title">{{ post.title }}</h1> <div class="post-text"> {{ post.text | linebreaks }} </div> <!-- vueのマウント開始 --> <div id="comment-vue"> <p class="text-link" v-on:click="openModal('comment',{{ post.pk }})">記事にコメントする</p> <!-- コメント一覧 --> {% for comment in post.comment_set.all %} <div class="comment"> <h3 class="comment-writer">{{ comment.name }}</h3> {{ comment.text | linebreaks}} </div> <p class="text-link" v-on:click="openModal('reply', {{ comment.pk }})"> コメントに返信する </p> <!-- リプライ一覧 --> {% for reply in comment.reply_set.all %} <div class="reply"> <h3>{{ reply.name }}</h3> {{ reply.text | linebreaks }} </div> {% endfor %} <!-- リプライ一覧終わり --> {% endfor %} <!-- コメント一覧終わり --> <!-- modalテンプレート --> <modal-template v-show="isVisible" v-on:close="closeModal" v-bind:actionurl="actionUrl"></modal-template> </div> <!-- vueのマウント終わり --> </div> <script type="text/x-template" id="modal-template"> <transition tag='div' name="modal"> <div class="modal-container"> <!-- モーダル外をクリックしたら閉じる --> <div class="modal-overlay" v-on:click.self="$emit('close')"> <div class="modal-body"> <!-- 親コンポーネントからaction属性に割り当てるurlを受け取る --> <form v-bind:action='actionurl' method="POST" id="comment-form"> <div class="field"> <p>名前</p> <input type="text" name="name" value="名無し" maxlength="255" required="true" id="id_name"> </div> <div class="field"> <p>本文</p> <textarea name="text" cols="40" rows="10" required="true" id="id_text"></textarea> </div> {% csrf_token %} <button type="submit" class="button">送信</button> </form> <!-- closeボタンを押したら閉じる --> <button class="button close-button" v-on:click="$emit('close')">Close</button> </div> </div> </div> </transition> </script> <script src="{% static "js/comment-vue.js" %}"></script> </body> </html> 記事(Post)に紐づくコメントの一覧はmodels.pyで定義した際にForeignKeyで紐づけていたので… {% for comment in post.comment_set.all %} とすると取り出すことができます。コメントに紐づくリプライも同様に取り出せます。 今回はコメントとリプライで処理をわけたいので、モーダル部分をコンポーネント化して、クリックされた箇所に応じて、処理を分岐させるようにします。 同じ画面は使うけど、投稿文を転送する先が分かれる、というイメージです。 <p class="text-link" v-on:click="openModal('comment',{{ post.pk }})">記事にコメントする</p> ここの部分で、ポストに対するコメントの際はJavaScriptサイドに"comment"という文字列と、Postのpkを渡すようにしています。 リプライのほうは <p class="text-link" v-on:click="openModal('reply', {{ comment.pk }})"> コメントに返信する </p> として、"reply"という文字列とコメントのpkを渡します。 JavaScript内で上記の受け取った情報からモーダル内に配置されたフォームのaction属性を割り当てて、投稿の処理を完了させるような流れです。 最低限の見た目を整えるためにcssも書いておきます。 /* static/css/style.css */ @charset "UTF-8"; /* 共通設定 */ html { font-size: 62.5%; } body { font-family: "Yu Gothic Medium", "游ゴシック Medium", YuGothic, "游ゴシック体". "ヒラギノ角ゴ", "ヒラギノ角ゴ Pro W3", sans-serif; background-color: #FAFAFA; color: #333; } /* コンテナ */ .container { max-width: 660px; margin: 50px auto; } /* 記事部分 */ .post-title { font-size: 2.4rem; font-weight: bold; border-bottom: 1px solid #ccc; } .post-text { font-size: 1.4rem; line-height: 1.7; border-bottom: 1px solid #ccc; } .comment { font-size: 1.2rem; margin-left: 10px; border-bottom: 1px dotted #ccc; line-height: 1.7; } .reply { font-size: 1.2rem; margin-left: 30px; border-bottom: 1px solid #ccc; line-height: 1.7; } /* コメントと返信の文字は青文字に */ .text-link { font-size: 1.4rem; color: #00809d; cursor: pointer; } .text-link:hover { color: #00809d; opacity: 0.5; } /* フォーム内のウィジェット */ .label { display: block; margin-bottom: 30px; } input[type=text], textarea { font-size: 1.2rem; padding: 4px 8px; box-sizing: border-box; border-radius: 4px; border: none; background-color: rgba(136, 136, 136, .3); outline: none; } input[type=text]:focus, textarea:focus { box-shadow: 0 0 8px rgba(130, 170, 170, .5); } .button { margin-top: 30px; display: block; width: 100px; padding: 5px; border-radius: 4px; background-color: #82aaaa; color: #fff; letter-spacing: 1px; font-size: 1.2rem; border: none; cursor: pointer; } .close-button { background-color: #888888; color: #fff; } .button:hover, .close-button:hover { opacity: 0.7; } /* モーダル構成要素 */ .modal-container { position: relative; z-index: 9998; width: 100%; height: 100%; } .modal-body { position: relative; z-index: 9999; box-sizing: border-box; height: 80%; width: 80%; max-width: 640px; padding: 16px; margin: auto; background-color: #fff; } .modal-overlay { position: fixed; top: 0; left: 0; display: flex; overflow: auto; width: 100%; height: 100%; padding: 20px 60px; background-color: rgba(0, 0, 0, 0.7); } /* モーダル Transition */ .modal-enter-from, .modal-leave-to { opacity: 0; } .modal-enter-to, .modal-leave-from { opacity: 1; } .modal-enter-active, .modal-leave-active { transition: opacity .7s ease; } JavaScript Vue処理のjsの記述です。 // static/js/comment-vue.js const commentModalForm = { // ③template内で使用するため、action属性に割り当てるurl文字列を受け取る props: { actionurl: { type: String, default: '', }, }, template: "#modal-template", } Vue.createApp({ data: function() { return { isVisible: false, formActionPk: '', commentType: '', } }, methods: { // ①modalオープンイベント時に変数を受け取る openModal: function(strCommentType, pk) { this.isVisible = true this.formActionPk = pk this.commentType = strCommentType }, closeModal: function() { this.isVisible = false }, }, computed: { // ②受け取った変数から、form内のaction属性に割り当てるurl文字列を生成 actionUrl: function() { if (this.commentType === 'comment') { return '/comment_create/' + this.formActionPk } else { return '/reply_create/' + this.formActionPk } } }, components: { 'modal-template': commentModalForm, } }).mount("#comment-vue") 全体的な処理の流れとしては、、、 1. ユーザーがページで、記事に対するコメントか、コメントに対する返信をクリックする 2. comment-vue.jsのopenModalメソッドが呼び出される。その際に受け取った情報から、処理を分岐させて、urlを生成 3. モーダル内のフォームは子コンポーネントに定義をしているので、url文字列を受け取る。 4. ユーザーが送信ボタンを押すと、データが受け取ったurlに送信される。 5. 投稿完了 という風な感じです。 画面遷移をしないので、ちょっとかっこよさが増しますね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js + axiosでLambdaのAPIを叩く方法

開発環境 Windows10 AWS Lambda Vue.js APIの作成 Lambda lambda.function.py(GET) import json def lambda_handler(event, context): items = ['福岡','佐賀','熊本','大分','鹿児島','長崎'] return { 'statusCode': 200, 'body': json.dumps(items), 'headers': { 'Access-Control-Allow-Headers': '*', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'OPTIONS,POST,GET' } } lambda.function.py(OPTIONS) import json def lambda_handler(event, context): return { 'statusCode': 200, 'body': '', 'headers': { 'Access-Control-Allow-Headers': '*', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'OPTIONS,POST,GET' } } 九州の都道府県を返すサンプルです。    サーバーは別のサイト(オリジン)から送られてきたhttpリクエストをブロックすることができます。 CSR (Cross-site Request Forgery) などの対策のためです。    許可する場合はサンプルのようにレスポンスヘッダーでAccess-Control-Allow情報を返す必要があります。 Access-Control-Allow-Origin : 許可するオリジンを指定できます。上の例はワイルドカードですべてのオリジンからのリクエストを許可しています。 Access-Control-Allow-Headers : 許可するリクエストヘッダーを指定できます。上の例はワイルドカードですべてのリクエストヘッダーを許可しています。 Access-Control-Allow-Methods : 許可するリクエストメソッドを指定できます。上の例はGET,POST,OPTIONSを許可しています。    オリジンから別のオリジンにアクセス権を許可する仕組みをCORS(Cross-Origin Resource Sharing)といいます    OPTIONSについては後程説明します。 API Gatewayの設定 リソースの作成 GETおよびOPTIONSメソッドを作成 上の関数をAPI化しました。 OPTIONSメソッドとは サーバーからクライアントに対してアクセス権を返します。 クライアントからサーバーに対してOPTIONSリクエストを送ることをPreflight requestといいます。 サンプルの例では まず、クライアントがPreflight requestをサーバーに送る。 サーバーがOPTIONSレスポンス(アクセス権)を返す。 クライアントはアクセス権を確認し、アクセス可能であればGETメソッドリクエストを送る。 サーバー側はGETレスポンスを返す。 という流れになります。 AWSが用意しているのCORSを使用することもできるのですが、正しく動作させることができなかったため自作することにしました。 Preflight requestはブラウザが自動的に送信するためクライアントの実装では意識する必要はありません APIキーの必要性の設定 ※ OPTIONSはAPIキーの必要性をfalseのままにしておく ブラウザがPreflight requestを送るときにリクエストヘッダを追加することができないため、falseにしておいてください。 GETメソッドにはAPIキーを設定することができます。 デプロイ Vue プロジェクトにaxiosをインストール npm install axios サンプルコード Sample.vue <template> <div> <ul> <li v-for="item in items" :key="item">{{item}}</li> </ul> </div> </template> <script> import axios from 'axios'; export default { data() { return { items: null } }, async mounted() { await this.getData(); }, methods:{ getData: async function(){ const result = await axios( { method:'GET',// GET,POSTなど url:'https://××××.×××',// APIのURL headers:{ 'X-Api-Key':'××××××××××××××××'//リクエストヘッダー }}, ).then(response => this.items = response.data); } }, } </script> APIにGETリクエストを送ってレスポンスをitemsに保存。Viewでitemsの中身を表示しています。 OPTIONSリクエストは自動で送信されるため記述は必要ありません。 確認 APIから情報を取得するのに成功しています。 参考にさせていただいたサイト
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】コンポーネントはパスカルケースで書くのがオススメ

はじめに 仕事で使う事になったので1からVue.jsについて学んだ。 ちゃんと覚えておかないとまずそうな事を備忘録として1つ1つ残しておく。 コンポーネントはパスカルケースで書くのがオススメ コンポーネント(single file component)の命名方法は以下の2つが選択肢にある。 # 具体例 ケバブケース hello-world パスカルケース HelloWorld が、命名時はパスカルケースを用いるのがオススメでその理由は以下。  パスカルケースはJavaScriptでよく使われるため、エディターで開発する際に自動補完されやすい  HTML要素と見分けがつきやすい(HTMLの要素・タグはケバブケースで書くため) 以下の画像のように、LikeNumberは緑色だが、like-headerは青色でHTMLの要素(divタグ)と同じで見分けにくい1  Vue.js以外のWebコンポーネントを使う際に見分けがつきやすい 余談 DOMテンプレート2の場合は、ケバブケースで記載しなければならない。 理由は、 ブラウザがDOMを描画する際には、htmlファイル→JavaScriptsファイルの順番で読み込む ブラウザがhtmlファイルを読み込む際には大文字小文字は区別されない という2つの事があり、DOMテンプレートでパスカルケースを使うと全て小文字と認識され3意図しない動きになるため。 Vue.jsの勉強メモ一覧記事へのリンク Vue.jsについて勉強した際に書いた勉強メモ記事のリンクを集約した記事。 https://qiita.com/yuta-katayama-23/items/dabefb59d16a83f1a1d4 LikeNumberはGlobal登録したコンポーネント。like-headerはLocal登録したコンポーネントで、LikeHeaderをケバブケースで書いたもの。※Vue.jsではコンポーネントをパスカルケースで定義しtemplateではケバブケースで記述するという事ができる。※また、LikeHeader": LikeHeaderの部分は"like-header": LikeHeaderとも書ける。 ↩ HTML上(.htmlファイル)に定義するコンポーネント ↩ HelloWorld → helloworldと見なされる ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vite Ruby で 爆速で Rails + Vue3 + TypeScript 環境を作成する!

Introduction みなさん、Vitejs というのはご存知でしょうか? https://vitejs.dev/ 手間のかかるフロントエンド環境の構築をまとめてやってくれる優れものです。 お好みのフレームワークと開発言語(js or ts)を選択すれば爆速で構築してくれます。 こちらの Ruby統合環境 (Vite Ruby) を作成されている方がいるようでしたので 早速試していこうと思います♪ (個人的には Rails + Vue3 環境が素早く構築できると 嬉C) 初期リリースは2021年1月のようですね。 ? Commits on Jan 19, 2021 https://github.com/ElMassimo/vite_ruby/releases/tag/v1.0.0 Getting Started 早速、試そうとドキュメントを漁ります。 https://vite-ruby.netlify.app/guide/ これ見た限り、既存のRailsプロジェクトにインストールするスタイルみたいですね とりあえず、通常の Rails + Vue 構成でセットアップしてみますか Railsプロジェクトの作成 # 個人の環境に合わせてください $ mkdir -p ~/projects/vite_ruby_sample $ cd ~/projects/vite_ruby_sample # オプションはお好みで... (今回は Rails + Mysql + Vue) $ bundle exec rails new . --database=mysql --webpack=vue --skip-turbolinks --skip-test-unit --skip-coffee --skip-test --skip-active-storage # とりあえずDBのセットアップもやっておく $ mysql.start $ bin/rails db:prepare ViteRubyのインストール vite-rubyのinstallationに従います。 https://vite-ruby.netlify.app/guide/#installation-%F0%9F%92%BF Gemfile gem 'vite_rails' $ bundle $ bundle exec vite install Creating binstub Check that your vite.json configuration file is available in the load path: No such file or directory @ rb_sysopen - ~/projects/vite_ruby_sample/config/vite.json Creating configuration files Installing sample files Installing js dependencies yarn add v1.22.10 [1/4] Resolving packages... [2/4] Fetching packages... [3/4] Linking dependencies... [4/4] Building fresh packages... success Saved lockfile. success Saved 14 new dependencies. info Direct dependencies ├─ vite-plugin-ruby@2.0.4 └─ vite@2.3.2 info All dependencies ├─ @nodelib/fs.scandir@2.1.4 ├─ @nodelib/fs.stat@2.0.4 ├─ @nodelib/fs.walk@1.2.6 ├─ esbuild@0.11.20 ├─ fast-glob@3.2.5 ├─ fastq@1.11.0 ├─ merge2@1.4.1 ├─ nanoid@3.1.23 ├─ queue-microtask@1.2.3 ├─ reusify@1.0.4 ├─ rollup@2.47.0 ├─ run-parallel@1.2.0 ├─ vite-plugin-ruby@2.0.4 └─ vite@2.3.2 Done in 12.04s. Could not install JS dependencies. npx: 1個のパッケージを3.803秒でインストールしました。 warning " > vue-loader@15.9.7" has unmet peer dependency "css-loader@*". Adding files to .gitignore Vite ⚡️ Ruby successfully installed! ? これで vitejs もインストールされるようですね。 【追加されたファイル】 bin/vite vite.config.ts config/vite.json 【変更があったファイル】 app/javascript/entrypoints/application.js // To see this message, add the following to the `<head>` section in your // views/layouts/application.html.erb // // <%= vite_client_tag %> // <%= vite_javascript_tag 'application' %> console.log('Vite ⚡️ Rails') // If using a TypeScript entrypoint file: // <%= vite_typescript_tag 'application.jsx' %> // // If you want to use .jsx or .tsx, add the extension: // <%= vite_javascript_tag 'application.jsx' %> console.log('Visit the guide for more information: ', 'https://vite-ruby.netlify.app/guide/rails') // Example: Load Rails libraries in Vite. // // import '@rails/ujs' // // import Turbolinks from 'turbolinks' // import ActiveStorage from '@rails/activestorage' // // // Import all channels. // import.meta.globEager('./**/*_channel.js') // // Turbolinks.start() // ActiveStorage.start() // Example: Import a stylesheet in app/frontend/index.css // import '~/index.css' app/views/layouts/application.html.erb <!DOCTYPE html> <html> <head> <title>ViteRubySample</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= stylesheet_link_tag 'application', media: 'all' %> <%= javascript_pack_tag 'application' %> <%= vite_client_tag %> <%= vite_javascript_tag 'application' %> <!-- If using a TypeScript entrypoint file: vite_typescript_tag 'application' If using a .jsx or .tsx entrypoint, add the extension: vite_javascript_tag 'application.jsx' Visit the guide for more information: https://vite-ruby.netlify.app/guide/rails --> </head> <body> <%= yield %> </body> </html> Viteのセットアップ とのことで、上記からプラグインを選んでインストールします 今回は Vue3 を利用するので下記を選択しました $ yarn add -D @vitejs/plugin-vue その他依存関係もインストールします $ yarn add -D @vue/compiler-sfc $ yarn add -D typescript # Rails作成時のvueが2系なのでアップデート $ yarn add vue@next # vue3 vite.config.ts import { defineConfig } from 'vite' import RubyPlugin from 'vite-plugin-ruby' import vue from '@vitejs/plugin-vue' // https://vitejs.dev/config/ export default defineConfig({ plugins: [ RubyPlugin(), vue() ], }) とりあえず実行してみる 下記によると bin/vite でviteサーバーを起動できるようなので試してみましょう! https://vite-ruby.netlify.app/guide/development.html#developing-with-vite $ bin/vite Commands: vite build # Bundle all entrypoints using Vite. vite clobber # Clear the Vite cache, temp files, and builds vite dev # Start the Vite development server. vite install # Performs the initial configuration setup to get started with Vite Ruby. vite version # Print version っと、さらにオプションがある模様ですね $ bin/vite dev vite v2.3.2 dev server running at: > Local: http://localhost:3036/vite-dev/ > Network: use `--host` to expose ready in 404ms. 起動したみたいですが、よくわからないですね? (webpack-dev-serverみたいなものと思っておきましょう) 気にせず、いつものRails&Vueのようにアクション作成しましょうか... layouts/application.html.erbでコンパイルしたjsが返るようになってますので、下記だけでいいですね。 $ bin/rails g controller vue index とりあえず、これだけ。 さっそく、実行してみましょう! $ bin/rails server -b 0.0.0.0 -p 3000 => Booting Puma => Rails 6.1.3.2 application starting in development => Run `bin/rails server --help` for more startup options Puma starting in single mode... * Puma version: 5.3.1 (ruby 2.7.2-p137) ("Sweetnighter") * Min threads: 5 * Max threads: 5 * Environment: development * PID: 49577 * Listening on http://0.0.0.0:3000 Use Ctrl-C to stop $ open 'http://0.0.0.0:3000/vue/index' 下記のように表示されました! ちゃんと 'Vite ⚡️ Rails' と表示されてます 接続は大丈夫そうですね! Vueを表示してみる いよいよ本番と、vue3 + ts で記述して表示できるか試してみましょう! app/views/vue/index.html.erb <div id="app"></div> app/javascript/entrypoints/application.js import { createApp } from 'vue' import App from '../app.vue' document.addEventListener('DOMContentLoaded', () => { createApp(App).mount('#app') }) app/javascript/app.vue <template> <p>{{ state.message }}</p> </template> <script lang="ts"> import { defineComponent } from 'vue' export default defineComponent({ name: 'App', setup() { return { state: { message: "Hello Vue3 + TypeScript!" } } } }) </script> <style scoped> p { font-size: 2em; text-align: center; } </style> ページを更新して、いざ、表示! おぉー、表示されましたね。 これで気軽に Rails + Vue3 + TypeScript 環境で開発できますね! まとめ 今回は、爆速で Rails + Vue3 + TypeScript 環境 を構築してみました Vite Ruby を利用することで Rails 環境でも vitejs が気軽に導入できるようになりました 移り変わりの早いフロントエンドに対応していくためにも Vite Ruby で Rails環境を構築してみてはいかがでしょうか? ではでは ? LINKS vitejs https://vitejs.dev/ vite ruby https://vite-ruby.netlify.app/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vueでモーダルを作る

普通にモーダルを作るとなると、同じようなhtmlが羅列し煩わしいですが、Vueで作るとコンポーネント1つでと回せるので見通しが楽になります。 今回は閉じるボタンと背景をクリックして閉じるモーダルを作成してみます。 親コンポーネントを作成する まず、親のコンポーネントを作成します。 親にはモーダルを開くためのボタンと文、そしてモーダルの中身を記述します。 dataにitemの配列をいれてそこにモーダルの中身を記述します。またv-showで表示を切り替えするためにのフラグshowFlagと、該当のモーダルを入れるためのmodalItemを定義しておきます。 About.vue(親コンポーネント) <template> <div class="About"> <h1 class="text-3xl py-8 px-4 bg-gray-400">About</h1> <div class="max-w-screen-xl mx-auto mt-10 px-4"> <div> <p class="text-base">このボタンをクリックすると、画像つきのモーダルが開きます。</p> <div class="mt-6"><button @click="openModal(item,0)" class="btn">モーダルを開く</button></div> </div> <div class="mt-8"> <p class="text-base">このボタンをクリックすると、テキストだけのモーダルが開きます。</p> <div class="mt-6"><button @click="openModal(item,1)" class="btn">モーダルを開く</button></div> </div> </div> </div> </template> <script> export default { name: 'About', data(){ return { showFlag: false, modalItem: '', item: [ { title: 'テキストだけのモーダル01', text: 'サンプルテキストサンプルテキストサンプルテキストサンプルテキストサンプルテキストサンプルテキストサンプルテキストサンプルテキストサンプルテキストサンプルテキストサンプルテキストサンプルテキストサンプルテキストサンプルテキストサンプルテキストサンプルテキスト', img: 'slide01.jpg' }, { title: 'テキストだけのモーダル02', text: 'サンプルテキストサンプルテキスト', img: '' } ] } }, methods: { openModal (item,modalNumber){ this.showFlag = true; this.modalItem = item[modalNumber]; }, closeModal(){ this.showFlag = false; }, } } </script> buttonに設定したクリックイベント:openModalには2つの引数が設定されており、1つはdataにあるitemの配列の取得、もう一つはモーダルのデータの配列番号をいれてます。クリックすることで、dataに定義されたmodalItemに該当のモーダルのデータを格納しました。また、同時に定義したshowFlagの真偽値をtrueに変更しています。 このデータを子コンポーネントであるModal.vueに送ります。送るためには子コンポーネントの読み込みと、そこにv-bindで送りたいデータをバインディングします。 About.vue(親コンポーネント) <template> <div class="About"> <h1 class="text-3xl py-8 px-4 bg-gray-400">About</h1> <div class="max-w-screen-xl mx-auto mt-10 px-4"> <div> <p class="text-base">このボタンをクリックすると、画像つきのモーダルが開きます。</p> <div class="mt-6"><button @click="openModal(item,0)" class="btn">モーダルを開く</button></div> </div> <div class="mt-8"> <p class="text-base">このボタンをクリックすると、テキストだけのモーダルが開きます。</p> <div class="mt-6"><button @click="openModal(item,1)" class="btn">モーダルを開く</button></div> </div> </div> <Modal :modalData="modalItem" v-show="showFlag"/> <!-- 追加 --> </div> </template> <script> import Modal from '@/components/about/Modal.vue' <!-- 追加 --> export default { name: 'About', components: { <!-- 追加 --> Modal }, data(){ return { showFlag: false, modalItem: '', item: [ { title: 'テキストだけのモーダル01', text: 'サンプルテキストサンプルテキストサンプルテキストサンプルテキストサンプルテキストサンプルテキストサンプルテキストサンプルテキストサンプルテキストサンプルテキストサンプルテキストサンプルテキストサンプルテキストサンプルテキストサンプルテキストサンプルテキスト', img: 'slide01.jpg' }, { title: 'テキストだけのモーダル02', text: 'サンプルテキストサンプルテキスト', img: '' } ] } }, methods: { openModal (item,modalNumber){ this.showFlag = true; this.modalItem = item[modalNumber]; }, closeModal(){ this.showFlag = false; }, } } </script> これで子にデータを送る準備ができました。 子コンポーネントでデータを受け取り、展開する データを送ったら受け皿が必要になります。子要素にpropsで親から送られたデータを格納します。 Modal.vue(子コンポーネント) <template> <div class="Modal"> </div> </template> <script> export default { name: "Modal", props: { modalData: { type: String, default: "", require: true } } } </script> propsには親でバインドした名前を記述することで、親から送られてきたデータを受け取ることができます。 ただ受け取る場合はprops:['modalData']という記述でも大丈夫ですが、この場合はどの型でも受け入れてしまいます。エラー検知やコンポーネントの使用方法を明確にするためにm」Vue公式では少なくとも1つの型を指定して受け取ったほうがよいと推奨されております。 受け取れたので、モーダルの中身を作っていきましょう。 Modal.vue(子コンポーネント) <template> <div class="Modal"> <div class="fixed bg-black bg-opacity-60 w-full h-full inset-0" > <div class="absolute w-full h-full inset-0 p-6"> <div class="w-full mx-auto max-w-screen-sm bg-white"> <div class="w-full mx-auto max-w-screen-sm bg-white absolute transform -translate-y-1/2 -translate-x-1/2 top-1/2 left-1/2 max-h-screen rounded-md p-4"> <p class="text-2xl pt-4 pb-2 border-b-2 border-gray-700 border-solid">{{modalData.title}}</p> <div class="max-h-80p min-h-200"> <p class="text-base">{{modalData.text}}</p> <div class="mt-3" v-if="modalImg"> <img :src='require("@/assets/img/common/" + modalData.img)' alt=""> </div> </div> <div class="mt-6 flex"> <button class="Btn">Close</button> </div> </div> </div> </div> </div> </div> </template> <script> export default { name: "Modal", props: { modalData: { type: String, default: "", require: true } }, data() { return { modalImg: false } }, watch: { modalData: function(){ return this.modalImg = this.modalData.img.length ? true : false } } } </script> 受け取ったデータは、modalData.titleのように双方向バイティングで記述することができます。イメージデータを受け取って表示させる場合はsrcをバインドするのですが、そのままだと文字列が表示されてしまいます。今回のように@を使ってパスをたどる場合は、requireでモジュールとして読むこむことで表示ができるようになります。 受け取ったデータの中にイメージがない場合も想定されるので、watchでmodalDataを監視し、データの中にイメージファイルの記述があったらmodalImgの真偽値を変更しv-ifで表示・非表示を切り替えています。 ちなみに、v-ifはundefinedだとエラーが起きるため、データを監視し初期値を設定する必要があります。 子から親にモーダルを閉じる司令をだす モーダルはモーダル内のcloseボタンと背景のオーバーレイをクリックすることで、モーダルが閉じる仕様です。そのためには親コンポーネントにあるshowFlagをfalseにする必要があります。親のデータを変更するために、親コンポーネントにshowFlagをfalseする関数を設定し、子から親にはイベントを$emitで送ってあげます。 Modal.vue(子コンポーネント) <template> <div class="Modal"> <div class="fixed bg-black bg-opacity-60 w-full h-full inset-0" > <div class="absolute w-full h-full inset-0 p-6" @click.self="$emit('close')"><!-- 追加 --> <div class="w-full mx-auto max-w-screen-sm bg-white"> <div class="w-full mx-auto max-w-screen-sm bg-white absolute transform -translate-y-1/2 -translate-x-1/2 top-1/2 left-1/2 max-h-screen rounded-md p-4"> <p class="text-2xl pt-4 pb-2 border-b-2 border-gray-700 border-solid">{{modalData.title}}</p> <div class="max-h-80p min-h-200"> <p class="text-base">{{modalData.text}}</p> <div class="mt-3" v-if="modalImg"> <img :src='require("@/assets/img/common/" + modalData.img)' alt=""> </div> </div> <div class="mt-6 flex"> <button class="Btn" @click.self="$emit('close')">Close</button><!-- 追加 --> </div> </div> </div> </div> </div> </div> </template> <!-- 以下省略 --> 子コンポーネントにclickイベントを設定し、そこに$emit('close')を記述しました。これは、親にclose という名のイベントを送りつけてます。 また、.selfはターゲットが自分自身の場合のみハンドラを呼び出される設定であり、これによってバブリングを阻止しております。 About.vue(親コンポーネント) <template> <div class="About"> <h1 class="text-3xl py-8 px-4 bg-gray-400">About</h1> <div class="max-w-screen-xl mx-auto mt-10 px-4"> <div> <p class="text-base">このボタンをクリックすると、画像つきのモーダルが開きます。</p> <div class="mt-6"><button @click="openModal(item,0)" class="btn">モーダルを開く</button></div> </div> <div class="mt-8"> <p class="text-base">このボタンをクリックすると、テキストだけのモーダルが開きます。</p> <div class="mt-6"><button @click="openModal(item,1)" class="btn">モーダルを開く</button></div> </div> </div> <Modal :modalData="modalItem" v-show="showFlag" @close="closeModal()"/> !-- 追加 --> <Footer></Footer> </div> </template> <script> <!-- 一部省略 --> methods: { openModal (item,modalNumber){ this.showFlag = true; this.modalItem = item[modalNumber]; }, closeModal(){!-- 追加 --> this.showFlag = false; }, } } </script> 親コンポーネントにはインポートしている<Modal />に@close="closeModal()"を追記しました。これで子から送られてきたcloseというイベントをキャッチし、closeModal()という関数を動かすことができます。closeModal()には'showFlag'をfalseにする処理が記述してあり、真偽値が変更されることで、v-showの表示が変更になり、モーダルが閉じられます。 これで1つのコンポーネントで取り回しができ、モーダルの数量が増えてもモーダルデータのみを増やせばOKなモーダルが作成できました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

(vue3でのつまずき) v-modelが動かず、change は2回発火される場合の対処

vue3 を触っていて、2つのつまずきがあったので解決方をまとめました。 ■つまずき1:v-modelが動かない components/AInput.vue <template> <input @input="$emit('input', $event.target.value)" @change="$emit('change', $event.target.value)" /> </template> <script> export default { name: "AInput", }; </script> このコンポーネントにv-modelでバインドする際にvue3だとうまく動かないようでした。 App.vue <template> <div class="container"> コードを入力してください。 <AInput v-model="code" @change="onChange" type="tel" /> <p>コード: {{ code }}</p> </div> </template> <script> import AInput from "./components/AInput.vue"; export default { name: "App", components: { AInput, }, data() { return { code: '', }; }, methods: { onChange(v) { console.log('changed') console.log(v) } } }; </script> ■原因 v-modelの仕様が変わった vue3 の v-modelの仕様が、v2から変わっていました。 https://v3.ja.vuejs.org/guide/migration/v-model.html 破壊的変更: カスタムコンポーネントで使用する場合に、v-model のプロパティとイベントのデフォルト名が変更されます。 プロパティ: value -> modelValue イベント: input -> update:modelValue 破壊的変更: v-bind の .sync 修飾子とコンポーネントの model オプションは削除され、v-model の引数に置き換えられます。 新規: 同じコンポーネントに複数の v-model バインディングが可能になりました。 新規: カスタムの v-model 修飾子を作成する機能が追加されました。 こちらの記事に詳しく記載を頂いていました。 ■解決法 $emit するイベントを update:modelValue に変更 記載の通り、AInput.vue の @input の $emit するイベントを update:modelValue に変更するとv-modelが動きました。 components/AInput.vue <template> <input @input="$emit('update:modelValue', $event.target.value)" @change="$emit('change', $event.target.value)" /> </template> <script> export default { name: "AInput", }; </script> ■つまずき2:changeイベントが2回発火される また同時に、changeが2回$emitされる現象が起こりました。 ■原因 vue3から $emit の仕様が変わった 下記のページとRFCを見ると、コーディングの意図を明示的にするために、$emitするイベントをwhite list化することを求める変更が行われたようでした。 ■解決法 $emit するイベントをemitsに記述 こちらのissueの回答の通り、AInput.vue の $emit するイベントを 事前に把握して export default { emits: ["update:modelValue","change"], } と記述することでchangeが2回発火されなくなりました。 components/AInput.vue <template> <input @input="$emit('update:modelValue', $event.target.value)" @change="$emit('change', $event.target.value)" /> </template> <script> export default { name: "AInput", emits: ["update:modelValue", "change"], }; </script> 以上2つのつまずきについて解決方法をまとめました。 2つ目の方は、2回イベントが$emitされるのは結構危ないので気を付けたいと思いました。 以上です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReferenceError: vuex is not defined エラーの対処

npm install veux をインストールしたのに、 ReferenceError: vuex is not defined \Users\81801\Desktop\vue.js\Vue_firebase\vue-for-blog\src\store.js 4:9 error 'vuex' is not defined no-undef とコンソールに表示されて、沼ったので解決法をメモります。 sotre.js に問題がありそうなので確認します。 store.js import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(vuex);  export default new Vuex.Store({ state: {} }) import Vuex from 'vuex'; では大文字なのに、 引数として扱っているときは、小文字になっていました。 Vue.use(vuex);  大文字で統一してみると store.js import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex);//大文字にする export default new Vuex.Store({ state: {} }) エラーがなくなりました。 ちゃんと文法とかやっていることを理解せずに、コピペするのはよくないですね。 ネットの情報も時々間違えてますね。 以下のサイトのチュートリアルをしていたらエラーになったので、 同じ人はいれば参考に
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VitePressでRouterの変更を検知する

したいこと VitePressでGoogle AnalyticsをしたいけどSPAだからページ移動しても検知してくれない。 なのでVitePressでRouterの変更を検知して検知させたい。 結論 .viteperss/theme/index.js import DefaultTheme from 'vitepress/theme'; import { watch } from 'vue'; export default { ...DefaultTheme, enhanceApp({ app, router }) { watch(router.route, () => { // 変更を検知したタイミングで呼んでくれる }); } } 解説 router.routeはreactiveなのでVue3のwatchを使って検知するだけです 【おまけ】Google Analyticsに対応させる 今回はgtagを利用します .viteperss/theme/index.js import DefaultTheme from 'vitepress/theme'; import { watch } from 'vue'; export default { ...DefaultTheme, enhanceApp({ app, router }) { watch(router.route, () => { gtag('config', window.GA_MEASUREMENT_ID, {'page_path': router.route.path }); }); } } .viteperss/config.js module.exports = { // 中略 head: [ // ここは各自scriptのURLに置きかえる [ 'script', { src: '', async: true } ], [ 'script', {}, `window.GA_MEASUREMENT_ID = 'Google analyticsに書いてあるIDを書く';window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}gtag('js', new Date());`] ] // 中略 } これでヨシ! 誤字などありましたら教えてください~
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む