Open15

Vuejs Challenges をやる

azukiazusaazukiazusa

Hello World

Hello, World!

In Vue.js Challenges, we use the Vue.js SFC Playground based on vuejs/repl to coding online.

For this challenge, you will need to change the following code to make the page show "Hello World" correctly.

<script setup>
import { ref } from "vue"
const msg = ref("Hello World")
</script>

<template>
  <div>
    <!-- The output of the page expected to be Hello World -->
    <h1>msg</h1>
  </div>
</template>

Click the Take the Challenge button to start coding! Happy Hacking!

https://github.com/webfansplz/vuejs-challenges/blob/main/questions/1-hello-word/README.md

回答

<script setup>
import { ref } from "vue"
const msg = ref("Hello World")
</script>

<template>
  <div>
    <h1>{{ msg }}</h1>
  </div>
</template>

https://sfc.vuejs.org/#eNpNT7uOwjAQ/JXVVnfSYevayId0HX9Asw0EB4Lih7wOFJb/nbVDQTezMzuaKfgfo3qsFgc0PKY5ZmCb17gnP7sYUoYCyU5QYUrBAaF4CcmPwXMGx1f4a/oX4cEuS4BjSMuF8Ju80VueJAnJ1sXllK0wAHOZHx0IvP3uS+lBtRotrBv05jD64w9/cKu0c6eo7hy8lC7NTm+BCQfol3brTQcBt5wjD1rzNLapd1YhXbUglVafZ2eVZbc7p/BkmyS47QOo5CvWF9v4Yn8=

解説

<template> 内で変数を展開するためには ”Mustache” 構文({{ }})を使用します。

azukiazusaazukiazusa

Lifecycle Hooks

For this challenge, you'll use Composition API: Lifecycle Hooks to finish it.
Here's what you need to implement 👇:

// Child.vue

<script setup lang="ts">
import { onMounted, inject } from "vue"

const timer = inject('timer')
const count = inject('count')


onMounted(() => {
  // The timer'll work unnormally while toggle child component,Let's go to fixed it.
  timer.value = window.setInterval(() => {
    count.value++
  }, 1000)
})

</script>

<template>
  <div>
    <p>
      Child Component: {{ count }}
    </p>
  </div>
</template>

https://github.com/webfansplz/vuejs-challenges/blob/main/questions/10-lifecycle/README.md

回答

<script setup lang="ts">
import { onMounted, inject, onUnmounted } from "vue"

const timer = inject("timer")
const count = inject("count")

onMounted(() => {
  timer.value = window.setInterval(() => {
    count.value++
  }, 1000)
})
  
onUnmounted(() => {
  if (timer.value) {
    window.clearInterval(timer.value)
    timer.value = null
  }
})

</script>

<template>
  <div>
    <p>
      Child Component: {{ count }}
    </p>
  </div>
</template>

https://sfc.vuejs.org/#eNqNUj1z2zAM/SsoJ+cii+7qk3PtZerQrd24yBLl0KFIHknJg07/PeCHFDlZPOgOAsCHh/cwkd/GlOPAyZFUrrHCeHDcDwZkrS4nRrxj5IUp0RttPUxgeVeAsXoULYcZOqt7YAQBGFm7Xt+EbJdSSeNvmBFamGq0ch5G4cRZcjgFxJ23A39aSl703OaCGqRcC40elM+FA2YzjR2yDE8YKdLbu1J8FEoxwBJT3aAaL7QCry8XyXdPMDEFC6VyrOUQiP24SzA1M1XRpBEqgj+e90bWnuMfQNWKMQYYJgHGvehQwYzCCNClbnKA4XnwHpn8aqRo3oPekVLUPHUA/IuprOqrRo0Vx5UWBJogFmyawCua+FR0w5IUZDXjUcO1+huE420BQl154wtM/Vd9Sn47ga8mpjefFn0zc23IRkWH1qE7NOf0kvyJCKs7N6FafSuR+x9stJje9kLCT93PzyE1F/DzcAh3M+MHEKase2zfig52m1n5OmCZ2Ehe23XmtjG13dMMBxynp7EPn9DniXzx/QjTlMWbEfQRz5OX+7425dVphc7HjVguoOEImqCyj0cM3rw37kip65pwL1dXanuhGJUWh+OSJXf9/mz1zeG+V0RZ9iTzB3FpeBQ=

解説

Child.vue コンポーネントが v-if によって表示・非表示されるたびにカウントがおかしくなるのでこれを修正する必要があります。

この問題が発生している理由は setInterval によって開始したタイマーを、コンポーネントが非表示になった(アンマウントされた)時に取り消していないためです。タイマーのカウントを取り消すために必要な timerID はすでに timer 変数に保持されているようです。やるべきことはアンマウントされたタイミングで clearInterval を呼び出すコードを加えることです。

コンポーネントがアンマウントされたタイミングで処理を実行するには、Lifecycle Hooks の一つである onUnmounted を使用します。

import { onMounted, inject, onUnmounted } from "vue"

// ...

onUnmounted(() => {
  if (timer.value) {
    window.clearInterval(timer.value)
    timer.value = null
  }
})
azukiazusaazukiazusa

Next DOM update flush

When you mutate reactive state in Vue.js, the resulting DOM updates are not applied synchronously.

Vue.js provides a utility for waiting for the next DOM update flush, Let's Go 👇:

<script setup>
import { ref } from "vue"

const count = ref(0)

function increment() {
  count.value++

  /**
   * DOM not yet updated, how to make sure the DOM udpated
   * Make the output to be true
  */

  console.log(+document.getElementById("counter").textContent === 1)
}
</script>

<template>
  <button id="counter" @click="increment">
    {{ count }}
  </button>
</template>


https://github.com/webfansplz/vuejs-challenges/blob/main/questions/11-next-dom-update/README.md

回答

<script setup>
import { ref, nextTick } from "vue"

const count = ref(0)

function increment() {
  count.value++

  nextTick(() => {
    console.log(+document.getElementById("counter").textContent === 1)
  })
}
</script>

<template>
  <button id="counter" @click="increment">
    {{ count }}
  </button>
</template>

https://sfc.vuejs.org/#eNp1Uk1PwzAM/StWTt1XAtdpRXweOCAuHHPZUm8rtEmUOIOp6n/HaTc0gbi0sf388vycTtx5Lw8JxVKsogm1J4hIyd9oW7feBYIOAm7nYPGL3mrzAT1sg2tBC+7SQlttjbORwLhkCcqMLq4mOb9N1lDtLNTWBGzRUjGBTlsYsfKwbhLOZhkKoKbT/IMpPL6+gHUERyRIvloTVnPYu08gB+36AyGmgEB7HJCp8hlx6n3J9VxyiXyi3LLhRGCpDJgqvovOkxSsprwZBWVJNroGZeN2xaxyJmW9cof01AzS74/PVaHFoByDFhNJzPPgOMpjlyVc89AAPX97bVdqdJN95ICw9Q3L5AhgtUlE2ZWqvOCDW9OwKE79uKXFgAfoupO7PTMzgRoZuLpSF9RiLsadLdq1l+/RWd7qMB6TDoWoxfI88GmBSz7siXxcKhW3Jr+F9yhd2Ck+ycC31i1KjO1iE9xnxMDEWswvOBQnDxgWAW2FIc/yP+cv6B/ewUE2UPTfsXnk8Q==

解説

console.log(+document.getElementById("counter").textContent === 1)true を出力するようにコードを修正します。

count.value++ のようにリアクティブな変数を更新した時、DOM への更新は同期的に実施されません。next tick まで更新をバッファリングし、どれだけ状態を変更しても、各コンポーネントの更新が一度だけであることを保証します。

そのため、count.value++ を実行した関数内で document.getElementById("counter").textContent のように DOM を参照しても、更新前の値が返却されます。

DOM が更新されるまで処理を待ちたい場合には nextTick を使用します。nextTick では Promise が完了するまで待つか、コールバック関数で処理を記述することで、更新後の DOM を参照することができます。

// コールバック
nextTick(() => {
  console.log(+document.getElementById("counter").textContent === 1)
})

// Promise
await nextTick()
console.log(+document.getElementById("counter").textContent === 1)
azukiazusaazukiazusa

DOM Portal

Vue.js provides a built-in component that renders its slot content to another part of the DOM.

Do you know what's it, Let's try it 👇:

<script setup>

const msg = "Hello World"

</script>

<template>
  <!-- Renders its to a child element of the `body` -->
  <span>{{ msg }}</span>
</template>


https://vuejs-challenges.netlify.app/questions/13-dom-portal/README.html

回答

<script setup>

const msg = "Hello World"

</script>

<template>
  <Teleport to="body">
     <span>{{ msg }}</span>
  </Teleport>
</template>

https://sfc.vuejs.org/#eNpNT8tqAzEM/BWhc2PfFyfQWz+g0Isum42abvALS9tSFv97bTeB6KSZ0QyaHV9zNt8b44ROlrJmBWHd8okixSVFUQhyhSMQvrH3CT5S8RfCLjv77xi3TjlkPys3BODe2XNORUHTkfCcLr+EQ2njJM/xtO8judYW0/Gw2YevQWefIvEF19CFQ5izuUmK7eO9e+guCOEEg+lcq9Qx4Zdqlsla+Vx6z5uYVK62baZsUdfAhiUcziX9CJcW3KsBVIoV6x9fa2Kf

解説

<Teleport><template> 内の DOM ツリーとは別の場所に「テレポート」できるビルドインコンポーネントです。

テレポート先は to で指定します。

<template>
  <Teleport to="body">
     <span>{{ msg }}</span>
  </Teleport>
</template>
azukiazusaazukiazusa

Dynamic css values

SFC <style> tags support linking CSS values to dynamic component.

Do you know what's it, Let's try it 👇:

<script setup>
import { ref } from "vue"
const theme = ref("red")

const colors = ["blue", "yellow", "red", "green"]

setInterval(() => {
  theme.value = colors[Math.floor(Math.random() * 4)]
}, 1000)

</script>

<template>
  <p>hello</p>
</template>

<style scoped>
/* Modify the code to bind the dynamic color */
p {
  color: red
}
</style>

回答

<script setup>
import { ref } from "vue"
const theme = ref("red")

const colors = ["blue", "yellow", "red", "green"]

setInterval(() => {
  theme.value = colors[Math.floor(Math.random() * 4)]
}, 1000)

</script>

<template>
  <p>hello</p>
</template>

<style scoped>
p {
  color: v-bind(theme);
}
</style>

https://sfc.vuejs.org/#eNpNUUtugzAQvcrIK4gSnEpdURKpyy5ygrgLAkMg8k+2SYQQd+/YbqXu5vs+Myv7tLZ6zshq1vjOTTaAxzDbs9CTssYFWMHhABsMzigQjGYFE7oz2gcIIyqEU5woBHPYC1YK/dftjDTOU/sq2E3GvT0BLCileeU4bcTg7hC1YN9xmfi/dED3bGVRlHA6wyo0ZK6KinNkzNjXSxvGapDGuCKFrtW9UbS1g/eS0LY9vB2PxySq4dkgWaMkoLKyDUgZQGPPY5TV8Gi84f+alPqwSATfGYs9VfgOLqafhiVKIiE9QjBwm3SfCv2iWzV1WSHsuNA2G0iFGp6HOFokO+UHSUzKIgVhsz3LZz+o1lYPbzQ9Jm2L34YXrM54sZa+UVMwhmB9zbkfuvjOh6+Mu3OKKjfrMNHl0KvDzZmXR0fA8YcAxL2x7Qeb+LD/

解説

Vue.js3.2からは、JavaScript の変数を CSS 変数としてバインドできるようになりました。これにより動的に CSS の値を設定できます。

<style> タグ内で v-bind を使用します。

<style scoped>
p {
  color: v-bind(theme);
}
</style>
azukiazusaazukiazusa

Ref family

For this challenge, you'll use Reactivity API: ref to solve the problem at hand.
Here's what you need to implement 👇:

<script setup lang="ts">
import { ref, Ref, reactive } from "vue"

const initial = ref(10)
const count = ref(0)

// Challenge 1: Update ref
function update(value) {
  // impl...
}

/**
 * Challenge 2: Checks if `count` is a ref object.
 * Make the output to be 1
*/
console.log(
  // impl ? 1 : 0
)

/**
 * Challenge 3: Unwrap ref
 * Make the output to be true
*/
function initialCount(value: number | Ref<number>) {
  // Make the output to be true
  console.log(value === 10)
}

initialCount(initial)

/**
 * Challenge 4:
 * create a ref for a property on a source reactive object.
 * The created ref is synced with its source property:
 * mutating the source property will update the ref, and vice-versa.
 * Make the output to be true
*/
const state = reactive({
  foo: 1,
  bar: 2,
})
const fooRef = ref() // change the impl...

// mutating the ref updates the original
fooRef.value++
console.log(state.foo === 2)

// mutating the original also updates the ref
state.foo++
console.log(fooRef.value === 3)

</script>

<template>
  <div>
    <h1>msg</h1>
    <p>
      <span @click="update(count-1)">-</span>
      {{ count }}
      <span @click="update(count+1)">+</span>
    </p>
  </div>
</template>

https://vuejs-challenges.netlify.app/questions/2-ref-family/README.html

回答

<script setup lang="ts">
import { ref, Ref, reactive, isRef, unref, toRef } from 'vue';

const initial = ref(10);
const count = ref(0);

// Challenge 1: Update ref
function update(value) {
  count.value = value;
}

/**
 * Challenge 2: Checks if `count` is a ref object.
 * Make the output to be 1
*/
console.log(isRef(count) ? 1 : 0);

/**
 * Challenge 3: Unwrap ref
 * Make the output to be true
*/
function initialCount(value: number | Ref<number>) {
  return unref(value);
}

initialCount(initial);

/**
 * Challenge 4:
 * create a ref for a property on a source reactive object.
 * The created ref is synced with its source property:
 * mutating the source property will update the ref, and vice-versa.
 * Make the output to be true
*/
const state = reactive({
  foo: 1,
  bar: 2,
});
const fooRef = toRef(state, 'foo');

fooRef.value++;
console.log(state.foo === 2);

state.foo++;
console.log(fooRef.value === 3);
</script>

<template>
  <div>
    <p>
      <span @click="update(count - 1)">-</span>
      {{ count }}
      <span @click="update(count + 1)">+</span>
    </p>
  </div>
</template>

https://stackblitz.com/edit/angular-8kf7jy?file=App.vue

解説

この問題には4つの設問があります。それぞれ見ていきましょう。

Challenge 1: Update ref

ref() で作成したリアクティブな値を更新します。ref オブジェクトはミュータブルですので、.value に値を代入することで更新できます。

function update(value) {
  count.value = value;
}

Challenge 2: Checks if count is a ref object.

count 変数が ref オブジェクトである場合 1 を、そうでない場合 0 を出力します。
値が ref オブジェクトがどうか検査するには isRef 関数を使用します。

console.log(isRef(count) ? 1 : 0);

Challenge 3:Challenge 3: Unwrap ref

numebr または Ref<number> 型の値を受け取り、Ref 型であった場合は中の値を取り出し、そうでない場合は値をそのまま返します。そのための関数として unref が用意されています。

function initialCount(value: number | Ref<number>) {
  return unref(value);
}
``` `

### Challenge 4: create a ref for a property on a source reactive object.

オブジェクト `state` のプロパティ `foo` と同期する値を作成します。`state.foo` を更新すると `fooRef` の値も更新され、反対に `fooRef` の値を更新すると `state.foo` の値も更新されるようにします。

```ts
// mutating the ref updates the original
fooRef.value++
console.log(state.foo === 2)
 
// mutating the original also updates the ref
state.foo++
console.log(fooRef.value === 3)

リアクティブな値を作成するためには ref が使えそうですが、これは state.foo と同期されません。ref に渡されるのは単純な値であるためです。

const fooRef = ref(state.foo)

オブジェクトのプロパティと同期する値を作成するためには toRef 関数を使用します。toRef の第1引数には対象のオブジェクトを、第2引数はオブジェクトのプロパティ名を指定します。

const fooRef = toRef(state, 'foo');
azukiazusaazukiazusa

losing-reactivity

In JavaScript, we always destructure/spread objects.

In Vue.js, we can also destructure/spread the reactive objects, but they will end up losing their reactivity.

How can we make sure that the destructured properties maintain their reactivity? Go !

<script setup lang="ts">
import { reactive } from "vue"

function useCount() {
  const state = reactive({
    count: 0,
  })

  function update(value: number) {
    state.count = value
  }

  return {
    state,
    update,
  }
}

// Ensure the destructured properties don't lose their reactivity
const { state: { count }, update } = useCount()

</script>

<template>
  <div>
    <p>
      <span @click="update(count-1)">-</span>
      {{ count }}
      <span @click="update(count+1)">+</span>
    </p>
  </div>
</template>

https://vuejs-challenges.netlify.app/questions/3-losing-reactivity/README.html

回答

<script setup lang="ts">
import { reactive, toRefs } from 'vue';

function useCount() {
  const state = reactive({
    count: 0,
  });

  function update(value: number) {
    state.count = value;
  }

  return {
    state: toRefs(state),
    update,
  };
}

// Ensure the destructured properties don't lose their reactivity
const {
  state: { count },
  update,
} = useCount();
</script>

<template>
  <div>
    <p>
      <span @click="update(count - 1)">-</span>
      {{ count }}
      <span @click="update(count + 1)">+</span>
    </p>
  </div>
</template>

https://stackblitz.com/edit/angular-tfp368?file=App.vue

解説

Vue.js の reactive オブジェクトは分割代入を行うとプロパティのリアクティブが失われてしまいます。リアクティブが失われないようにするためには、toRefs 関数でそれぞれのプロパティを ref に変換する必要があります。

  return {
    state: toRefs(state),
    update,
  };

一般に、useXXX のような composable 関数から値を公開する際には reactive オブジェクトは toRefsref に変換することが推奨されています。

Returning a reactive object from a composable will cause such destructures to lose the reactivity connection to the state inside the composable.

If you prefer to use returned state from composables as object properties, you can wrap the returned object with reactive() so that the refs are unwrapped. For example:

https://vuejs.org/guide/reusability/composables.html#conventions-and-best-practices

azukiazusaazukiazusa

watch family

For this challenge, you'll use Reactivity API: watch to complete the challenge.
Here's what you need to implement 👇:

<script setup lang="ts">
import { ref, watch } from "vue"

const count = ref(0)

/**
 * Challenge 1: Watch once
 * Make sure the watch callback is only triggered once
*/
watch(count, () => {
  console.log("Only triggered once")
})

count.value = 1
setTimeout(() => count.value = 2)

/**
 * Challenges 2: Watch object
 * Make sure the watch callback is triggered
*/
const state = ref({
  count: 0,
})

watch(state, () => {
  console.log("The state.count updated")
})

state.value.count = 2

/**
 * Challenge 3: Callback Flush Timing
 * Make sure visited the updated eleRef
*/

const eleRef = ref()
const age = ref(2)
watch(age, () => {
  console.log(eleRef.value)
})
age.value = 18

</script>

<template>
  <div>
    <p>
      {{ count }}
    </p>
    <p ref="eleRef">
      {{ age }}
    </p>
  </div>
</template>

https://vuejs-challenges.netlify.app/questions/5-watch-family/README.html

回答

<script setup lang="ts">
import { ref, watch } from 'vue';

const count = ref(0);

/**
 * Challenge 1: Watch once
 * Make sure the watch callback only triggers once
 */
const stop = watch(count, () => {
  console.log('Only triggered once');
  stop();
});

count.value = 1;
setTimeout(() => (count.value = 2));

/**
 * Challenge 2: Watch object
 * Make sure the watch callback is triggered
 */
const state = ref({
  count: 0,
});

watch(
  () => state.value.count,
  () => {
    console.log('The state.count updated');
  }
);

state.value.count = 2;

/**
 * Challenge 3: Callback Flush Timing
 * Make sure visited the updated eleRef
 */

const eleRef = ref();
const age = ref(2);
watch(
  age,
  () => {
    console.log(eleRef.value);
  },
  {
    flush: 'post',
  }
);
age.value = 18;
</script>

<template>
  <div>
    <p>
      {{ count }}
    </p>
    <p ref="eleRef">
      {{ age }}
    </p>
  </div>
</template>

https://stackblitz.com/edit/angular-ftplot?file=App.vue,index.test.ts

解説

この問題には3つの設問があります

Challenge 1: Watch once

watch が1度値が変更された時のみ呼ばれるようにします。watch または watchEffect 関数の返り値には 監視を停止する関数が渡されます。

監視を停止する関数を1度 watch が呼び出された後にコールすればそれ以降値が変化しても watch が呼ばれないようになります。

const stop = watch(count, () => {
  console.log('Only triggered once');
  stop();
});

Challenge 2: Watch object

この設問では watch で監視する値がオブジェクトとなっています。ネストしたプロパティを監視するには、watch の第1引数に 「getter 関数」を渡します。


watch(
  () => state.value.count,
  () => {
    console.log('The state.count updated');
  }
);

別解として、deep オプションを渡すこともできます。deep オプションを渡すとネストされたすべてのミューテーションでコールバックがトリガーされます。

watch(
  state,
  () => {
    console.log('The state.count updated');
  },
  {
    deep: true
  }
);

Challenge 3: Callback Flush Timing

この設問では watch コールバックの中で Template Refs を参照しているため、コールバックが発火するタイミングを DOM の更新後とする必要があります。

通常、watch のコールバックは DOM が更新されるに発火されます。コールバックが発火するタイミングを DOM が更新された後にするためには flush: 'post' を指定します。

watch(
  age,
  () => {
    console.log(eleRef.value);
  },
  {
    flush: 'post',
  }
);

別解として、watchPostEffect を使うこともできます。watchPostEffectflush: 'post' を指定したときと全く同じです。

watchPostEffect(
  age,
  () => {
    console.log(eleRef.value);
  },
);
azukiazusaazukiazusa

shallow ref

For this challenge, you'll use Reactivity API: shallowRef to complete the challenge.
Here's what you need to implement 👇:

<script setup lang="ts">
import { shallowRef, watch } from "vue"

const state = shallowRef({ count: 1 })

// Does NOT trigger
watch(state, () => {
  console.log("State.count Updated")
}, { deep: true })

/**
 * Modify the code so that we can make the watch callback trigger.
*/
state.value.count = 2

</script>

<template>
  <div>
    <p>
      {{ state.count }}
    </p>
  </div>
</template>

https://vuejs-challenges.netlify.app/questions/6-shallow-ref/README.html

回答

<script setup lang="ts">
import { shallowRef, watch } from 'vue';

const state = shallowRef({ count: 1 });

// Does NOT trigger
watch(
  state,
  () => {
    console.log('State.count Updated');
  },
  { deep: true }
);

/**
 * Modify the code so that we can make the watch callback trigger.
 */
state.value = { count: 2 };
</script>

<template>
  <div>
    <p>
      {{ state.count }}
    </p>
  </div>
</template>

https://stackblitz.com/edit/angular-bsowt9?file=App.vue

解説

この問題ではリアクティブな値が ref ではなく shallowRef で宣言されています。shallowRef は名前のとおり、「浅いリアクティブ」を提供します。つまり、問題中の state.value.count はリアクティブな値として扱われなくなります。

そのため、単純に値を代入しても変更は検知されません。「浅いリアクティブ」に対して変更を検知させるためには、オブジェクトごと代入する必要があります。

state.value = { count: 2 } 
azukiazusaazukiazusa

Dependency Injection

For this challenge, you'll use the Composition API: Dependency Injection to complete the challenge.
Here's what you need to implement 👇:

// Child.vue 

<script setup lang="ts">
// Add a piece of code to make the `count` value get injected into the child component.
</script>

<template>
  {{ count }}
</template>

https://vuejs-challenges.netlify.app/questions/9-dependency-injection/README.html

解答

<script setup lang="ts">
import { inject } from 'vue';
// Add a piece of code to make the `count` value get injected into the child component.
const count = inject('count');
</script>

<template>
  {{ count }}
</template>

https://stackblitz.com/edit/angular-iaxvxg?file=Child.vue,App.vue

解説

Provide/Inject を利用します。親コンポーネントでは provide("count", count) で値が提供されているので、キーに count を指定して値を取り出します。

const count = inject('count');
azukiazusaazukiazusa

prevent event propagation

In this challenge,you should make the click event's propagation to be stopped,let's go 👇:

<script setup lang="ts">

const click1 = () => {
  console.log('click1')
}

const click2 = () => {
  console.log('click2')
}

</script>

<template>
  <div @click="click1()">
   <div @click="click2()">
     click me
   </div>
  </div>
</template>

解答

<script setup lang="ts">

const click1 = () => {
  console.log('click1')
}

const click2 = () => {
  console.log('click2')
}

</script>

<template>
  <div @click="click1()">
   <div @click.stop="click2()">
     click me
   </div>
  </div>
</template>

https://sfc.vuejs.org/#eNqFUMtOwzAQ/JWVL00kaoscI6eC//AlGBNc/JJ3Uw5V/h07LkVw4baPmZ3ZubLnlPhlNWxkEnW2iQANrQncHJZJMULFTiqooGNAAu2s/niECboephNcVQCom+gMd3HpDg1w6FXY/rCG/1jDnSVFs7ILSzI+uZlM6QDkq73A044v7ppa1+8e4deWI8X0DRl+IEW5TsCbxhCF0i7fKinuguyBWZ9ipqOfEz9jDCWm3b26LUo6Y/unzkqOtVfsnSjhKAS+6RruGXnMiygVz2sg6w036I8vOX6iyeWwYvVE+X1j2xf0gYV6

解説

「click me」をクリックした時、click2 関数と click1 関数どちらも発火してしまうので click2 関数のみが発火するように修正します。

Vue では イベント修飾子 により event.preventDefault() または event.stopPropagation() などを簡易的に記述することができます。

クリックイベントの伝搬が止めるためには .stop 修飾子 を使用します。.stop 修飾子は内部的に Event.stopPropagation() を呼び出します。

@click.stop="click2()"
azukiazusaazukiazusa

Capitalize

Create a custom modifier for the 'v-model' directive that changes the first letter of the 'v-model' binding value to uppercase.

<script setup>
</script>

<template>
 <input type="text" v-model.capitalize="" />
</template>

https://vuejs-challenges.netlify.app/questions/305-capitalize/README.html

回答

App.vue
<script setup>
import { ref } from 'vue'
import MyInput from './MyInput.vue'
const value = ref("")
</script>

<template>
  <MyInput type="text" v-model.capitalize="value" />
</template>

MyInput.vue
<script lang="ts" setup>
interface Props {
  modelValue: string;
  modelModifiers: { capitalize?: boolean };
}
const props = defineProps<Props>();

interface Emits {
  (e: 'update:modelValue', value: string): void;
}

const emit = defineEmits<Emits>();

const handleInput = (e: Event) => {
  if (e.target instanceof HTMLInputElement) {
    const value = e.target.value;
    if (props.modelModifiers.capitalize) {
      emit('update:modelValue', value.charAt(0).toUpperCase() + value.slice(1));
    } else {
      emit('update:modelValue', value);
    }
  }
};
</script>

<template>
  <input type="text" :value="modelValue" @input="handleInput" />
</template>

https://stackblitz.com/edit/angular-v5a6ae?file=App.vue,MyInput.vue

解説

コンポーネントに対して v-model を使用する場合、ビルドインの v-model.number, v-model.trim のようなカスタム修飾子を作成することができます。まずはコンポーネントに対して v-model を使用する方法から見ていきましょう。

コンポーネントで v-model を使用するには、以下のルールに従った Props と Emit が必要です。

  • modelValue Props:value 属性をバインドする
  • update:modelValue Emit:input イベントのように新しい値を知らせるイベント

https://vuejs.org/guide/components/events.html#usage-with-v-model

MyInput.vue
<script lang="ts" setup>
interface Props {
  modelValue: string;
}
const props = defineProps<Props>();

interface Emits {
  (e: 'update:modelValue', value: string): void;
}

const emit = defineEmits<Emits>();

const handleInput = (e: Event) => {
  if (e.target instanceof HTMLInputElement) {
    const value = e.target.value;
    emit('update:modelValue', value);
  }
};

<template>
  <input type="text" :value="modelValue" @input="handleInput" />
</template>

これで <MyInput> コンポーネントに対して v-model を使用できるようになります。

App.vue
<template>
  <MyInput v-model="value" />
</template>

続いてカスタム修飾子である v-model.capitalize を受け入れるように修正しましょう。コンポーネントに修飾子が付与されているかどうかは、modelModifiers Props から知ることができます。v-model.captialize 修飾子が付与されている場合には、modelModifiers の値は { capitalize: true } となります。

https://vuejs.org/guide/components/events.html#handling-v-model-modifiers

 <script lang="ts" setup>
 interface Props {
   modelValue: string;
+  modelModifiers: { capitalize?: boolean };
 }
 const props = defineProps<Props>();

これで修飾子が付与されているかどうか確認できるので、handleInput 関数内で props.modelModifiers.capitalizetrue の場合は値を capitalize する処理を追加すればよいわけです。

const handleInput = (e: Event) => {
  if (e.target instanceof HTMLInputElement) {
    const value = e.target.value;
    if (props.modelModifiers.capitalize) {
      emit('update:modelValue', value.charAt(0).toUpperCase() + value.slice(1));
    } else {
      emit('update:modelValue', value);
    }
  }
};
azukiazusaazukiazusa

Prop Validation

Please validate the type prop of the Button component. it's accept the following strings primary | ghost | dashed | link | text | default only and the default value is default.

<script setup>
defineProps({
  type: {},
})
</script>

<template>
  <button>Button</button>
</template>

https://vuejs-challenges.netlify.app/questions/323-prop-validation/README.html

回答

<script setup>
defineProps({
  type: {
    type: String,
    default: "default",
    validator: (v) => {
      return ["primary", "ghost", "dashed", "link", "text", "default"].includes(v)
    }
  },
})
</script>

<template>
  <button>Button</button>
</template>

https://sfc.vuejs.org/#eNp9UctOwzAQ/BXLpyK19j0KleALkDhiDm7jti7xQ7ubQlXl31nHSZEQ4uQZr2d2PXuTTzmry+BkI1vcg88k0NGQtyb6kBOQeB6IUhQHSEEYqXTlRWOkia2uKn7PhFzIvSXHTIi2vtRMWn2vyLX8cfija+cOProXSBlXt2JD1+waMcGFvBL4eFzXKxbYoaeGh5uhkXPpYnvfWUrQiNXlQTxuFxshgNtBFG9GZvDBwpVF7HA8JSz6Ymbx5LqKex8/KiL3tdSXZu/Kx30/dA65SfUfyzHyFCNf/BfRbopiWxNp9Ux/B1Y3sQk2qzOmyKlN/zBzAY28B2TktJiSxokoY6M1HvYl6zOqBEfNSMEQyQenHIbNDtInOmDjss5p9FGO36Q7tic=

解説

Props は boolean を返す validator メソッドを定義することにより、値を検証することができます。validator 関数が false を返す場合、コンソールに warning を出力します。

<script setup>
defineProps({
  type: {
    type: String,
    default: "default",
    validator: (v) => {
      return ["primary", "ghost", "dashed", "link", "text", "default"].includes(v)
    }
  },
})
</script>

TypeScript の場合、ユニオン型を利用することができます。この場合、誤った値を Props として渡した場合、コンソール上で warning を出力する代わりに <template> 上で型エラーを報告します。

<script setup lang="ts">
interface Props {
  type?: "primary" | "ghost" | "dashed" | "link" | "text" | "default"
}
  
withDefaults(defineProps<Props>(), {
  type: "default"          
})
</script>
azukiazusaazukiazusa

Raw API

For this challenge, you'll use Reactivity API: [xx]Raw to complete the challenge.
Here's what you need to implement 👇:

<script setup lang="ts">
import { reactive, isReactive } from "vue"

const state = { count: 1 }
const reactiveState = reactive(state)

/**
 * Modify the code so that we can make the output be true.
*/
console.log(reactiveState === state)

/**
 * Modify the code so that we can make the output be false.
*/
const info = { count: 1 }
const reactiveInfo = reactive(info)

console.log(isReactive(reactiveInfo))

</script>

<template>
  <div>
    <p>
      {{ reactiveState.count }}
    </p>
  </div>
</template>


https://vuejs-challenges.netlify.app/questions/7-raw-api/README.html

回答

<script setup lang="ts">
import { reactive, isReactive, toRaw, markRaw } from 'vue';

const state = { count: 1 };
const reactiveState = toRaw(reactive(state));

/**
 * Modify the code so that we can make the output be true.
 */
console.log(reactiveState === state);

/**
 * Modify the code so that we can make the output be false.
 */
const info = markRaw({ count: 1 });
const reactiveInfo = reactive(info);

console.log(isReactive(reactiveInfo));
</script>

<template>
  <div>
    <p>
      {{ reactiveState.count }}
    </p>
  </div>
</template>

https://stackblitz.com/edit/angular-7fs1rl?file=App.vue

解説

toRaw()
リアクティブなオブジェクトを生のオブジェクトに変換します。

const reactiveState = toRaw(reactive(state));

markRaw() により返却されたオブジェクトは reactive を呼ばれたとしても proxy に変換されることはありません。