import { onMounted } from 'vue';
export default {
setup() {
onMounted(() => {
if (performance.navigation.type === 1) {
console.log("This page is reloaded");
} else {
console.log("This page is not reloaded");
}
});
},
};
i
src/views/AboutView.vue
import { useConfirmDialog } from "@/composables/useConfirmDialog";
import ConfirmDialog from "@/components/ConfirmDialog.vue";
const { confirm } = useConfirmDialog({
title: "HOME画面に移動してよろしいですか?",
text: "OKボタンを押すと画面遷移します",
});
const handleDialog = async () => {
if (await confirm()) {
console.log("okがクリックされました");
} else {
console.log("cancelがクリックされました");
}
};
<button @click="handleDialog">Aboutダイアログ表示</button>
<ConfirmDialog></ConfirmDialog>
以上です。シンプルに記述できますね。
必要なファイルは2ファイル 「src/composables/useConfirmDialog.ts」 「src/components/ConfirmDialog.vue」 だけです。
src/composables/useConfirmDialog.ts
import { ref } from "vue";
type DialogText = {
title?: string;
text?: string;
ok?: string;
cancel?: string;
};
const isOpen = ref<boolean>(false);
const dialogText = ref<DialogText>({
title: "○○します。よろしいですか?",
text: "確認して以下のボタンをクリックしてください。",
ok: "OK",
cancel: "キャンセル",
});
let _resolve: (value: boolean | PromiseLike<boolean>) => void;
export function useConfirmDialog(props: DialogText = {}) {
if (props.title) dialogText.value.title = props.title;
if (props.text) dialogText.value.text = props.text;
if (props.ok) dialogText.value.ok = props.ok;
if (props.cancel) dialogText.value.cancel = props.cancel;
const confirm = () => {
isOpen.value = true;
return new Promise<boolean>((resolve) => {
_resolve = resolve;
});
};
const ok = () => {
isOpen.value = false;
_resolve(true);
};
const cancel = () => {
isOpen.value = false;
_resolve(false);
};
const close = () => {
isOpen.value = false;
};
return {
confirm,
ok,
cancel,
close,
isOpen,
dialogText,
};
}
src/components/ConfirmDialog.vue
<template>
<div
v-if="isOpen"
class="confirm-dialog confirm-dialog-show"
id="confirm-dialog149993"
style="margin-top: -86px"
>
<div class="confirm-dialog-title">
{{ dialogText.title }}
</div>
<div class="confirm-dialog-content">
<p class="confirm-dialog-message">
{{ dialogText.text }}
</p>
</div>
<div class="confirm-dialog-action">
<button @click="ok" class="btn-ok">{{ dialogText.ok }}</button>
<button @click="cancel" class="btn-cancel">
{{ dialogText.cancel }}
</button>
</div>
</div>
<div v-if="isOpen" @click="close" class="confirm-dialog-overlay"></div>
</template>
<script lang="ts" setup>
import { useConfirmDialog } from "@/composables/useConfirmDialog";
const { isOpen, ok, cancel, close, dialogText } = useConfirmDialog();
</script>
<style scoped>
.confirm-dialog {
color: #1b1919;
position: fixed;
z-index: 1061;
border-radius: 2px;
width: 400px;
margin-left: -200px;
background-color: #fff;
-webkit-box-shadow: 0 11px 15px -7px rgba(0, 0, 0, 0.2),
0 24px 38px 3px rgba(0, 0, 0, 0.14), 0 9px 46px 8px rgba(0, 0, 0, 0.12);
box-shadow: 0 11px 15px -7px rgba(0, 0, 0, 0.2),
0 24px 38px 3px rgba(0, 0, 0, 0.14), 0 9px 46px 8px rgba(0, 0, 0, 0.12);
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
left: 50%;
top: 50%;
font-size: 16px;
}
.confirm-dialog.confirm-dialog-show {
-webkit-animation: bounceIn 0.35s ease;
-o-animation: bounceIn 0.35s ease;
animation: bounceIn 0.35s ease;
}
.confirm-dialog .confirm-dialog-title {
text-align: center;
padding: 24px 30px 20px 30px;
font-size: 18px;
line-height: 1.4;
font-weight: bold;
color: #1b1919;
}
.confirm-dialog .confirm-dialog-title + .confirm-dialog-content {
padding-top: 0;
}
.confirm-dialog .confirm-dialog-content {
text-align: justify;
padding: 30px 30px 5px 30px;
}
.confirm-dialog .confirm-dialog-content .confirm-dialog-message {
font-size: 14px;
line-height: 1.4;
text-align: center;
margin: 0;
padding: 0;
color: #635a56;
}
.confirm-dialog .confirm-dialog-content .confirm-dialog-prompt input {
width: 100%;
height: 36px;
display: inline-block;
padding: 6px 0;
-webkit-box-shadow: none;
box-shadow: none;
border: none;
outline: none;
font-size: 16px;
color: #1b1919;
border-bottom: 1px solid #d9d6d4;
}
.confirm-dialog .confirm-dialog-action {
display: flex;
flex-direction: column;
margin: 10px 30px;
}
.confirm-dialog .confirm-dialog-action [class*="btn-"] {
font-size: 14px;
margin: 7px 0;
cursor: pointer;
color: #1b1919;
height: 36px;
min-width: 88px;
text-align: center;
display: inline-block;
border: 1px solid #dedede;
border-radius: 12px;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.12);
-webkit-transition: all 0.45s cubic-bezier(0.23, 1, 0.32, 1);
-o-transition: all 0.45s cubic-bezier(0.23, 1, 0.32, 1);
transition: all 0.45s cubic-bezier(0.23, 1, 0.32, 1);
}
.confirm-dialog .confirm-dialog-action .btn-ok {
background-color: black;
border-color: black;
color: #fff;
}
.confirm-dialog .confirm-dialog-action .btn-ok:active {
background-color: #444;
}
.confirm-dialog .confirm-dialog-action .btn-cancel {
background-color: #ececec;
color: #635a56;
}
.confirm-dialog .confirm-dialog-action .btn-cancel:active {
background-color: #dcdcdc;
}
@media all and (max-width: 540px) {
.confirm-dialog {
width: auto;
margin-left: 0;
margin-right: 0;
left: 15px;
right: 15px;
}
}
.confirm-dialog-overlay {
position: fixed;
background-color: #000;
z-index: 1060;
height: 100%;
width: 100%;
left: 0;
right: 0;
top: 0;
bottom: 0;
opacity: 0.4;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
-webkit-transition: opacity 0.45s cubic-bezier(0.23, 1, 0.32, 1);
-o-transition: opacity 0.45s cubic-bezier(0.23, 1, 0.32, 1);
transition: opacity 0.45s cubic-bezier(0.23, 1, 0.32, 1);
}
</style>
npm install --save @sentry/vue @sentry/tracing @sentry/integrations
Sentry ログインする
「Settings」 →「Projects」 →「<あなたのプロジェクト>」 →「client keys(DSN)」
から参照する
import { BrowserTracing } from "@sentry/tracing";
import { CaptureConsole } from "@sentry/integrations";
Sentry.init({
app,
dsn: "xxxxxxxxxxxxxxxx",
denyUrls: [<除外したいホスト名の文字列 or regex>],
ignoreErrors: [<除外したいエラーの文字列 or regex>],
integrations: [
new BrowserTracing({
routingInstrumentation: Sentry.vueRouterInstrumentation(router),
tracePropagationTargets: [
"localhost",
"your.server.com", // 変更する
/^\//,
],
}),
new CaptureConsole({ levels: ["error"] }),
],
tracesSampleRate: 1.0,
});
init のオプション
Sentry.init({
// Client's DSN.
dsn: 'xxxxxxx',
// An array of strings or regexps that'll be used to ignore specific errors based on their type/message
ignoreErrors: [],
// An array of strings or regexps that'll be used to ignore specific errors based on their origin url
denyUrls: [],
// An array of strings or regexps that'll be used to allow specific errors based on their origin url
allowUrls: [],
// Debug mode with valuable initialization/lifecycle informations.
debug: true,
// Whether SDK should be enabled or not.
enabled: true,
// Custom integrations callback
integrations(integrations) {
return [new HappyIntegration(), ...integrations];
},
// A release identifier.
release: '',
// An environment identifier.
environment: '',
// Custom event transport that will be used to send things to Sentry
transport: HappyTransport,
// Method called for every captured event
async beforeSend(event, hint) {
// Because beforeSend and beforeBreadcrumb are async, user can fetch some data
// return a promise, or whatever he wants
// Our CustomError defined in errors.js has `someMethodAttachedToOurCustomError`
// which can mimick something like a network request to grab more detailed error info or something.
// hint is original exception that was triggered, so we check for our CustomError name
if (hint.originalException.name === 'CustomError') {
const serverData = await hint.originalException.someMethodAttachedToOurCustomError();
event.extra = {
...event.extra,
serverData,
};
}
console.log(event);
return event;
},
// Method called for every captured breadcrumb
beforeBreadcrumb(breadcrumb, hint) {
// We ignore our own logger and rest of the buttons just for presentation purposes
if (breadcrumb.message.startsWith('Sentry Logger')) return null;
if (breadcrumb.category !== 'ui.click' || hint.event.target.id !== 'breadcrumb-hint') return null;
// If we have a `ui.click` type of breadcrumb, eg. clicking on a button we defined in index.html
// We will extract a `data-label` attribute from it and use it as a part of the message
if (breadcrumb.category === 'ui.click') {
const label = hint.event.target.dataset.label;
if (label) {
breadcrumb.message = `User clicked on a button with label "${label}"`;
}
}
console.log(breadcrumb);
return breadcrumb;
},
});
デフォルトで有効なインテグレーションです
https://docs.sentry.io/platforms/javascript/guides/gatsby/configuration/integrations/default/
プラグインです
https://docs.sentry.io/platforms/javascript/configuration/integrations/plugin/
https://docs.sentry.io/platforms/javascript/guides/vue/features/component-tracking/
Vueコンポーネントの追跡
SentryのVue SDKは、Vueコンポーネントのパフォーマンスを監視する機能として、コンポーネントトラッキングを提供しています。この機能を有効にすると、コンポーネントのライフサイクルイベントと継続時間を表すスパンがトランザクションに表示されます。これにより、コンポーネントがどのように動作しているかを詳細に把握することができ、遅い初期化や頻繁な更新を特定するなど、アプリのパフォーマンスに影響を与える可能性のあることを行うことができます。
1. Log in to your Sentry account and select the project you want to configure.
2. Navigate to the "Settings" section and click on "Filters."
3. Create a new filter by clicking the "Create Filter" button.
4. In the "Filter Name" field, enter a descriptive name for your filter, such as "Exclude Localhost Errors."
5. In the "Filter Expression" field, enter the following expression to exclude errors from localhost:
throw new Error('TEST ERROR');
console.error("TEST CONSOLE ERROR")
import { Sentry, SentrySeverity } from 'react-native-sentry';
// Error を渡す時
Sentry.captureException(error, {
tags: {
// 補足情報
scene: 'SomeScene', // e.g. どの画面か
fooState: state.foo // e.g. 関連する State はどのような状態か
}
}
// メッセージを渡す時
Sentry.captureMessage("MyCustomError", {
level: "error",
});
引用: https://medium.com/@tsugitta/sentry-on-react-native-1364c3fb407a
オプションで level など追加できます
https://docs.sentry.io/clients/javascript/usage/
Sentry.captureMessage("my-error", {
level: "info",
});
KEY は任意の値です
Sentry.setContext("KEY", {
id: 'xxxxx',
userName: 'yyyyy',
});
引用: Sentry で始める快適エラートラッキング on React Native | by tsugitta | Medium
https://codesandbox.io/s/vue-ios-like-transitions-n6v3d?file=/src/App.vue
https://freefrontend.com/css-page-transitions/
App.vue
<main>
<router-view v-slot="{ Component }">
<transition name="next">
<component :is="Component" />
</transition>
</router-view>
</main>
<style>
* {
box-sizing: border-box;
}
html,
body {
height: 100%;
}
#app {
display: grid;
grid-template-rows: min-content;
min-height: 100%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
}
nav {
display: flex;
align-items: center;
justify-content: space-around;
background-color: #125b7f;
position: sticky;
top: 0;
z-index: 1;
}
a {
color: white;
text-decoration: none;
text-transform: uppercase;
font-weight: bold;
padding: 1em 0;
margin: 0 1em;
border-bottom: 2px solid transparent;
}
a.router-link-exact-active {
border-color: inherit;
}
main {
min-height: 100%;
display: grid;
grid-template: "main";
flex: 1;
background-color: white;
position: relative;
z-index: 0;
overflow-x: hidden;
}
main > * {
grid-area: main; /* Transition: make sections overlap on same cell */
background-color: white;
position: relative;
}
main > :first-child {
z-index: 1; /* Prevent flickering on first frame when transition classes not added yet */
}
/* Transitions */
.next-leave-to {
animation: leaveToLeft 500ms both cubic-bezier(0.165, 0.84, 0.44, 1);
z-index: 0;
}
.next-enter-to {
animation: enterFromRight 500ms both cubic-bezier(0.165, 0.84, 0.44, 1);
z-index: 1;
}
.prev-leave-to {
animation: leaveToRight 500ms both cubic-bezier(0.165, 0.84, 0.44, 1);
z-index: 1;
}
.prev-enter-to {
animation: enterFromLeft 500ms both cubic-bezier(0.165, 0.84, 0.44, 1);
z-index: 0;
}
@keyframes leaveToLeft {
from {
transform: translateX(0);
}
to {
transform: translateX(-25%);
filter: brightness(0.5);
}
}
@keyframes enterFromLeft {
from {
transform: translateX(-25%);
filter: brightness(0.5);
}
to {
transform: translateX(0);
}
}
@keyframes leaveToRight {
from {
transform: translateX(0);
}
to {
transform: translateX(100%);
}
}
@keyframes enterFromRight {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}
</style>
<template>
<div>
<vue-markdown :source="src">
</div>
</template>
<script lang="ts">
import VueMarkdown from 'vue-markdown-render'
export default defineComponent({
name: 'MyComponent',
components: {
VueMarkdown
},
setup(props, ctx) {
const src = ref('# header')
return {
src
}
}
})
</script>
https://www.npmjs.com/package/vue-markdown-render?activeTab=readme
<template>
<h1>typescript google map</h1>
<div ref="mapRef" class="map"></div>
</template>
<script lang="ts" setup>
const mapRef = ref<HTMLElement>();
// このタイミングでは undefined になります
console.log( mapRef.value );
onMounted(() => {
// このタイミングで <div class="map"></div> が入ります
console.log( mapRef.value );
})
</script>
ブラウザ側で画像をリサイズしてFirebase Storageにアップロードする(Vue.js) - Qiita
JavaScript image compression and resizing
5 Best Image Crop Components For Vue.js
カスタムコンポーネントにおけるv-modelは、modelValueプロパティを渡してupdate:modelValueイベントを発火させるのと等価です。
<Child v-model="text"/>
<!-- ↑ ↓ これらは全く同じです -->
<Child :modelValue="text" @update:modelValue="text = $event"/>
引用: https://tekrog.com/v-model-in-vue3/#Vue3-2
Child.vue はこのように記述します
Child.vue
<template>
<input :value="modelValue" type="text" @input="onInputText" />
</template>
<script lang="ts" setup>
const props = defineProps<{modelValue: string}>()
const emits = defineEmits<{(e: 'update:modelValue', text: string): void}>()
const onInputText = (e: Event) => {
const target = e.target as HTMLInputElement
emits('update:modelValue', target.value)
}
</script>
TypeScript
https://ja.vitejs.dev/guide/env-and-mode.html#modes
デフォルトで、開発サーバ(dev コマンド)は development モードで動作し、
build コマンドは production モードで動作します。
明示的にモードを指定したい場合は以下のようにします(モードを staging にします)
vite build --mode staging
.env # 全ての場合に読み込まれる
.env.local # 全ての場合に読み込まれる(git監視対象外)
.env.[mode] # 指定されたモードでのみ読み込まれる
.env.[mode].local # 指定されたモードでのみ読み込まれる(git監視対象外)
.env.development (開発用)
.env.production (本番用)
src/composables/usePolling.ts
import {ref} from "vue"
export default function usePolling() {
let isPollingDisabled = ref<boolean>(false)
async function startPolling(
fn: () => void,
intervalTimeMsec: number = 3000 ,
waitingTimeMsec: number = 0 ,
) {
if (waitingTimeMsec > 0){
await new Promise(resolve => setTimeout(resolve, waitingTimeMsec))
}
// infinite loop
for (; ;) {
fn()
await new Promise(resolve => setTimeout(resolve, intervalTimeMsec))
if (isPollingDisabled.value) {
break;
}
}
}
function stopPolling() {
isPollingDisabled.value = true
}
return {
startPolling,
stopPolling
}
}
src/views/MyComponent.vue
<template>
<h2>{{ message }}</h2>
</template>
<script lang="ts" setup>
import {onBeforeMount, onBeforeUnmount, ref} from 'vue'
import usePolling from "@/composables/usePolling"
import dayjs from "dayjs"
const {startPolling, stopPolling} = usePolling()
const message = ref('start')
const getMessageAsync = async () => {
message.value = 'Ccc:' + dayjs().format('HH:mm:ss')
}
onBeforeMount(() => {
getMessageAsync()
startPolling(getMessageAsync, 1000)
})
onBeforeUnmount(() => {
stopPolling()
})
</script>
npm install element-plus --save
main.ts
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
const app = createApp(App);
......
......
app.use(ElementPlus);
<script setup lang="ts">
import {useRoute} from "vue-router";
const route = useRoute();
console.log(route.path);
</script>
<script setup lang="ts">
import {useRoute} from "vue-router";
const route = useRoute();
console.log(route.query);
</script>
Option APIの書き方
import Vue from "vue"
export default Vue.extend({
})
↓
<script lang="ts">
import { defineComponent } from "@vue/composition-api"
export default defineComponent({
setup() {
const { data: posts } = await useFetch('/api/posts')
}
})
</script>
↓ setup() の糖衣構文が <script setup> になります。次のように記述できます
( <script setup> の中では(現在まだ実験的な機能である Suspense と組み合わせて使用することで)トップレベルの await を使うことができます。その結果、コードは async setup() としてコンパイルされます。
https://bit.ly/3APAi9U
<script setup lang="ts">
const { data: posts } = await useFetch('/api/posts')
</script>
引用 : https://zenn.dev/coedo/articles/86bc31acb4ea47
また script setup はライフサイクルでいうところの、beforeCreate と created のライフサイクルで実行されます
https://v3.ja.vuejs.org/guide/composition-api-lifecycle-hooks.html
data → ref, reactive
dataはComposition APIでrefあるいはreactiveで表現される。
refはプリミティブな値を管理し、reactiveはオブジェクトや配列を管理する。
そのため、reactiveの方が今までの使い方に近い。
ただし、refにオブジェクトや配列を渡すと、内部でreactiveが呼ばれるため問題なく使える。
https://nansystem.com/nuxt-composition-api-v2-diff/
特に使い分けるベストプラクティスがあるわけではなさそう(どちらでもご自由に。)
● ref
・プリミティブ、オブジェクト両方で使用できる
・refで定義したリアクティブな変数の値にアクセスするには.valueを使用する必要がある
// ref の定義
const mydata = ref("スタッフ")
// ref の参照
console.log( mydata.value );
// ref の参照(テンプレート内では自動的に内部の値に浅くアンラップ(ref でラップされた値を取り出す)されます)
<template>
<div>
<span>{{ mydata }}</span>
</div>
</template>
// ref の更新
mydata.value = '社長'
● reactive
・プリミティブ値は受け取れない
・リアクティビティの消失に注意(refも同じだと思われるが。。。要検証)
https://kobatech-blog.com/vue-composition-api-ref-reactive/
https://vuejs.org/guide/typescript/composition-api.html#typing-component-props
<script setup lang="ts">
type Props = {
testName: string;
};
// props を受け取る(デフォルト値なし)
const props = defineProps<Props>();
// props を受け取る(デフォルト値あり)
const props = withDefaults(defineProps<Props>(), {
testName: "hogehoge",
});
</script>
main.ts に 以下の1行を加えるとCSSを読み込むことができます。
import './assets/my.css'
<script setup> 構文 では await で非同期関数を待つことはできません。 以下のようにして非同期関数を待つようにします
元のコード(エラーで動作しない)
<template>
<div>
<h1>MySetupComponent</h1>
<div>{{ result }}</div>
</div>
</template>
<script lang="ts" setup>
// 非同期関数
const getMessageAsync = async () => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return '1秒たちました!!!'
}
// result に取得した値をセットする
const result = ref('')
const message = await getMessageAsync() // ### エラーで動作しない
result.value = message
</script>
<script lang="ts" setup>
import { ref } from 'vue'
// 非同期関数
const getMessageAsync = async () => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return '1秒たちました!!!'
}
// result に取得した値をセットする
const result = ref('')
const getResultAsync = async () => {
const message = await getMessageAsync()
result.value = message
}
getResultAsync()
</script>
<script lang="ts" setup>
import { ref } from 'vue'
// 非同期関数
const getMessageAsync = async () => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return '1秒たちました!!!'
}
// result に取得した値をセットする
const result = ref('')
// 即時関数
;(async function () {
const message = await getMessageAsync()
result.value = message
})()
</script>
<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue'
// 非同期関数
const getMessageAsync = async () => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return '1秒たちました!!!'
}
export default defineComponent({
setup() {
// result に取得した値をセットする
const result = ref('')
onMounted(async () => {
const message = await getMessageAsync()
result.value = message
})
return {
result
}
}
})
</script>
/stores/todos.ts を以下の内容で作成します。
これは次のような機能を持ったストアになります
・変数todos にTODOリストを保存
・todoOrderedDesc() でID大きい順に並べ替えてリストを返す
・addTodo() でリストの最後にメンバーを追加する
・changeName() でidで指定したメンバの名前を変更する
記述方式はOptions APIライク(Vuexに似た昔ながらの書き方)でも、Composition APIライク(Vue3の書き方)でも
どちらでも記述することができます。
ここでは Composition API ライクで記述してみます。
stores/todos.ts
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import _ from 'lodash'
interface Todo {
id: number
name: string
}
export const useTodoStore = defineStore('todo', () => {
const todos = ref<Todo[]>([
{
id: 1,
name: 'TODOその1'
},
{
id: 2,
name: 'TODOその2'
}
])
const todoOrderedDesc = computed(() => {
return _.sortBy(todos.value, 'id').reverse()
})
function addTodo(newTodo: Todo) {
todos.value.push(newTodo)
}
function changeName(id: number, name: string) {
todos.value = todos.value.map((v) => {
return v.id === id
? {
id: id,
name: name
}
: v
})
}
return { todos, todoOrderedDesc, addTodo, changeName }
})
import { storeToRefs } from 'pinia'
import { useTodoStore } from '../stores/todos'
const todoStore = useTodoStore()
const { todos } = storeToRefs(todoStore)
storeToRefs で todos を ref にすることでリアクティブに扱うことができます。
<template>
<ul>
<li v-for="todo in todos" :key="todo.id">
<div>{{ todo.id }} : {{ todo.name }}</div>
</li>
</ul>
</template>
/stores/todos.ts に定義した更新メソッドを呼び出して値を更新します
todoStore.changeName(1, 'TODOその1 ● 変更')
(注意)直接値を更新することもできてしまいます https://github.com/vuejs/pinia/issues/58
todos.value = []
const { todos } = storeToRefs(todoStore)
↓ 次のように修正して readonly にします
const { todos: todosMutable } = storeToRefs(todoStore)
const todos = readonly(todosMutable)
これでこのコンポーネント内では「値の変更やデータの追加、削除などができない事が担保された状態」で扱うことができます。
/stores/todos.ts で定義した更新メソッドもこのコンポーネント内では実行することができなくなります
/stores/todos.ts を以下のように修正します
return { todos, todoOrderedDesc, addTodo, changeName }
↓
return { todos: readonly(todos), todoOrderedDesc, addTodo, changeName }
これで
// これは以下のエラーによって実行できなくなります
// [Vue warn] Set operation on key "value" failed: target is readonly.
todos.value = []
この方法だと更新メソッドは問題なく使用することができます。
https://pinia.vuejs.org/core-concepts/state.html#subscribing-to-the-state
Vuexのsubscribeメソッドと同様に、ストアの$subscribe()メソッドを通じて、状態とその変化を監視することができます。通常の watch() よりも $subscribe() を使う利点は、サブスクリプションがパッチの後に一度だけ起動することです(例:上記の関数版を使用した場合)。
例:userStore の 値を監視してアイコンを変更する
userStore.$subscribe((mutation, state) => {
changeIcon(state.user.iconURL)
});
https://pinia.vuejs.org/core-concepts/actions.html#subscribing-to-actions
store.onAction()でアクションとその結果を観察することが可能です。これに渡されたコールバックは、アクション自体の前に実行されます。 after handle promisesは、アクションが解決した後に関数を実行することができます。
onErrorでは、アクションがスローまたはリジェクトされた場合に関数を実行することができます。これらは、Vueのドキュメントにあるこのヒントと同様に、実行時にエラーを追跡するのに便利です
要約すると
・$onAction は全てのアクション実行前に呼ばれます。(何かのアクション実行前に毎回呼ばれます)
・特定のアクションにのみ紐付けたい場合は、引数の name からアクション名で判断させます。
・特定のアクションの実行後に関数を実行したい場合は after() を利用します
例:userStore の deleteUser() 実行完了後に 関数deleteAllPosts() を実行させたい場合
const unsubscribe = userStore.$onAction( ({ name: actionName, store, after }) => {
if (actionName === "deleteUser") {
after(() => {
deleteAllPosts();
});
}
}
);
コンポーネント内に 以下のように記述します
watch(
() => myStore.value,
(value) => {
console.log("===== changed ! =====");
console.log(value);
}
);
watchの 第一引数は myStore.value ではなくて () => myStore.value と記述します
pinia-plugin-persistのインストール
npm install pinia-plugin-persist
main.ts
import { createPinia } from "pinia";
import piniaPersist from "pinia-plugin-persist";
const pinia = createPinia();
pinia.use(piniaPersist); // ← 追加
app.use(pinia);
store/xxxx.ts
persist: {
enabled: true
}
1. アクセス方法
・スクリプト内からアクセスする場合は .value でアクセスする(Reactivity Transform を使用すると .value なしでアクセスできる)
・テンプレート内からアクセスする場合はそのままアクセスできる(階層が深いと .value か?)
2. 値の直接更新 → OK 。 ref はリアクティブを保ったまま更新することができる
3. 値の再代入 → OK 。refはリアクティブを保ったまま再代入することができる
ref のサンプル
<template>
<ul>
<li v-for="todo in todos" :key="todo.id">
<div>{{ todo.id }} : {{ todo.name }}</div>
</li>
</ul>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
interface Todo {
id: number
name: string
}
const todos = ref<Todo[]>([
{
id: 1,
name: 'TODOその1'
},
{
id: 2,
name: 'TODOその2'
}
])
// OK
setTimeout(() => {
todos.value[0].name = 'TODOその1 ● 変更'
}, 1000)
// OK
setTimeout(() => {
todos.value = [
...todos.value,
{
id: 3,
name: 'TODOその3 ● 追加'
}
]
}, 2000)
</script>
Vue.js は Reactivity Transform でさらに進化する
1. アクセス方法
・スクリプト、テンプレート内からアクセスする場合でもそのままアクセスできる
2. 値の直接更新 → OK 。 reactive はリアクティブを保ったまま更新することができる
3. 値の再代入 → NG × 。const で定義した場合は再代入NG
reactive のサンプル
<template>
<ul>
<li v-for="todo in todos" :key="todo.id">
<div>{{ todo.id }} : {{ todo.name }}</div>
</li>
</ul>
</template>
<script lang="ts" setup>
import { reactive } from 'vue'
interface Todo {
id: number
name: string
}
const todos = reactive<Todo[]>([
{
id: 1,
name: 'TODOその1'
},
{
id: 2,
name: 'TODOその2'
}
])
console.log('### todos.value')
console.log(todos)
// OK
setTimeout(() => {
todos[0].name = 'TODOその1 ● 変更しました'
}, 1000)
// NG × (constへの再代入のためコンパイルエラー)
setTimeout(() => {
todos = [
...todos,
{
id: 3,
name: 'TODOその3 ● 追加'
}
]
}, 2000)
setTimeout(() => {
todos.push({
id: 4,
name: 'TODOその4 ● push'
})
}, 3000)
</script>
あらかじめ作成したいプロジェクトの親ディレクトリに移動しておきます
vue3プロジェクトの新規作成コマンド
npm init vue@3
(Project nameで指定したディレクトリが自動的に作成されます)
✔ Project name: … vue3-todo-ts-app ✔ Add TypeScript? … No / Yes ✔ Add JSX Support? … No / Yes ✔ Add Vue Router for Single Page Application development? … No / Yes ✔ Add Pinia for state management? … No / Yes ✔ Add Vitest for Unit Testing? … No / Yes ✔ Add an End-to-End Testing Solution? › Cypress ✔ Add ESLint for code quality? … No / Yes ✔ Add Prettier for code formatting? … No / Yes
アプリの起動
cd vue3-todo-ts-app
npm install
npm run lint
npm run dev
ESLint と Prettier を Yes でインストールした場合は、次のコマンドでソース整形が実行できます
npm run lint
npm run test:unit
テストを検索するトップディレクトリを / にする場合は以下のコマンドでもokです。
npx vitest --environment jsdom --root ./
デフォルトでは、.env が環境変数ファイルとして読み込まれます。(つまり production )
.env.development を 読み込ませながら、テストを行いたい場合は、次のようにします。
npx vitest --environment jsdom --root ./ --mode development
Nuxt を使わない Vue 3 だけで各 Vue 系 API や自作コンポーネントを自動インポートする | mirumi.tech
npm i -D sass
main.ts
import "./assets/main.scss"; // 拡張子を .scss に変更する
main.css を main.scss に 名称変更します
試しに、ネストしたプロパティを記述してみます
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
font-size: 10px;
* {
color: red;
font-weight: 30px;
}
}
これでアプリを実行して表示される文字が赤くなっていれば成功です。
npm i vee-validate@next --save
npm i yup
<script lang="ts" setup>
import { Form, Field, ErrorMessage } from "vee-validate";
import * as yup from "yup";
const schema = yup.object({
password: yup.string().required().min(4),
sortNo: yup.number(),
});
function getSubmitFn<Schema extends yup.ObjectSchema<Record<string, any>>>(
_: Schema,
callback: (values: yup.InferType<Schema>) => void
) {
return (values: Record<string, any>) => {
return callback(values);
};
}
const handleSubmit = getSubmitFn(schema, (value) => {
console.log("● value");
console.log(value);
});
</script>
<template>
<h1>Samplepageelement</h1>
<Form @submit="handleSubmit" v-slot="{ values }" :validation-schema="schema">
<!-- <Form @submit="handleSubmit"> -->
<div>
<Field name="password" type="password" />
<ErrorMessage name="password" style="color: #c45656" />
</div>
<div>
<Field
name="sortNo"
type="number"
@update:modelValue="values.sortNo = Number(values.sortNo)"
/>
<ErrorMessage name="sortNo" style="color: #c45656" />
</div>
<button>送信</button>
</Form>
</template>
結果
● value
{password: 'asdfasdfads', sortNo: 1234}
number型を期待しているsortNoがそのままだとstringで返ってきてしまうので、@update:modelValue で変換しています。
https://bit.ly/3uRxmpM. https://vee-validate.logaretm.com/v4/guide/components/validation.
const handleSubmit = (values:any) => {
console.log( '● value' );
console.log( value );
};
↓ 以下のように書き換えます
function getSubmitFn<Schema extends yup.ObjectSchema<Record<string, any>>>(
_: Schema,
callback: (values: yup.InferType<Schema>) => void
) {
return (values: Record<string, any>) => {
return callback(values);
};
}
const handleSubmit = getSubmitFn(schema, (value) => {
console.log("● value");
console.log(value);
});
引用 : https://github.com/logaretm/vee-validate/issues/3521
<template>
<div>
<div>
<Field name="age" as="input" type="number" />
<div>{{ errors.age }}</div>
</div>
<div>
<Field name="name" as="input" />
<div>{{ errors.name }}</div>
</div>
<div>
<Field name="password" as="input" type="password" />
<div>{{ errors.password }}</div>
</div>
<pre>Is form dirty: {{ isDirty }}</pre>
<!-- print form values -->
<pre>{{ values }}</pre>
</div>
</template>
<script lang="ts" setup>
import { defineComponent, computed } from 'vue'
import { Field, useForm } from 'vee-validate'
import * as yup from 'yup'
interface UserFormValues {
age: number
name: string
password: string
}
const schema = yup.object().shape({
age: yup.number().required(),
name: yup.string().required(),
password: yup.string().required().min(8)
})
const { meta, values, errors } = useForm<UserFormValues>({
validationSchema: schema
})
const isDirty = computed(() => meta.value.dirty)
</script>
// インポートした「configure」を使用してトリガするイベントを変更
configure({
validateOnBlur: true, // blurイベントで検証をトリガーする必要がある場合、デフォルトはtrue
validateOnChange: true, // changeイベントで検証をトリガーする必要がある場合、デフォルトはtrue
validateOnInput: true, // inputイベントで検証をトリガーする必要がある場合、デフォルトはfalse
validateOnModelUpdate: true, // update:modelValue(v-model)イベントで検証をトリガーする必要がある場合、デフォルトはtrue
});
https://vee-validate.logaretm.com/v4/guide/composition-api/handling-forms#initial-errors
<Field name="password" type="text" />
<ErrorMessage name="password" />
↓
<Field name="password" v-slot="{ value, field, errorMessage }">
<el-form-item :error="errorMessage">
<el-input
v-bind="field"
:validate-event="false"
:model-value="value"
type="password"
show-password
/>
</el-form-item>
</Field>
ルーターの設定を記述するファイル router/index.ts
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/profile",
name: "profile",
component: ProfileView,
},
// リダイレクトさせる場合
{
path: "/",
redirect: "/mypage",
},
// 特定のパラメーター (例:isGuest) を渡す 場合
{
path: "/login",
name: "Login",
component: LoginView,
meta: { isGuest: true },
},
],
});
const aughGuard = () => {
if (認証チェック){
next();
return;
}
next({ path: "/login" });
}
// ルーティング前に認証チェックを実行
router.beforeEach((to, from, next) => {
authGuard(to, next);
});
<template>
<router-link to="/about">Go to About</router-link>
</template>
<button class="btn" @click="$router.push('/login')">戻る</button>
nameで指定する場合
import { useRouter } from "vue-router";
const router = useRouter();
await router.push({ name: "Login" });
pathで指定する場合
import { useRouter } from "vue-router";
const router = useRouter();
await router.push("/login");
/ の下に /child1 /child2 を子画面として追加する
src/router/index.ts
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "home",
component: HomeView,
children: [
{
path: "/child1",
components: {
default: Child1View,
},
},
{
path: "/child2",
components: {
default: Child2View,
},
},
],
}
],
});
親の src/views/HomeView.vue に 子画面用の router-view を追加します
src/views/HomeView.vue
<template>
<div class="container">
<h1>Home</h1>
<router-view></router-view>
</div>
</template>
これで、 /child1 , /child2 を表示しているときは HomeView.vueはアンマウントされません。
引用: https://qiita.com/azukiazusa/items/9f467fdea7298baf3476
useRouter()・useRoute()に置き換える
ルーターオブジェクトを得るためには、vue-rouerからuseRouterまたはuseRoute関数をインポートします。それぞれの関数は以下のように対応しています。
Options API | Composition API |
---|---|
this.$router | useRouter |
this.$route | useRoute |
src/router/index.ts
router.beforeEach((to, from, next) => {
// from.name が取得できる時はパスを保存。できないときは "" を保存
sessionStorage.setItem("referrer", from.name ? from.path : "");
authenticate(to, next);
});
各コンポーネント
function isPageReload() {
return sessionStorage.getItem("referrer") === "";
}
if isPageReload(){
alert("reloadです");
}
とすれば検知できます。
https://github.com/vinicius73/vue-page-title#vue-router-integration
https://router.vuejs.org/guide/advanced/scroll-behavior.html