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">
const route = useRoute();
console.log(route.path);
</script>
const route = useRoute();
console.log(route.query);
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 = []
この方法だと更新メソッドは問題なく使用することができます。
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
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 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で指定する場合
await router.push({ name: "Login" });
pathで指定する場合
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 |
https://github.com/vinicius73/vue-page-title#vue-router-integration
https://router.vuejs.org/guide/advanced/scroll-behavior.html