質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

89.09%

vue-cliにて、v-forでレンダリングしたアコーディオンを一つ開こうとすると全てが開いてしまう

解決済

回答 2

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 286

take-t.t.

score 356

前提・実現したいこと

いつもお世話になっております。
現在下記のコードで作ったコンポーネントを別の.vueファイル内で使おうとしているのですが、タイトル通りの現象が起きてしまい困っています。
→gifファイル(imgurに飛びます。)

発生している問題・エラーメッセージ

コンポーネントについての公式リファレンスを読む限り、この現象を防ぐにはdata(この場合はshowの真偽値)を別コンポーネントに関数で管理すれば大丈夫だと自分なりに理解しています。
しかしこの場合はv-forで同一コンポーネント内でレンダリングしているのでdataを分けられず、結局タイトルに書いた現象がおきてしまいます。
アコーディオンのコンポーネント内ではなく、使用先の.vueファイル内でv-forする事も考えましたがそれは出来ませんでした。

色々と試行錯誤したものの解決策が見つからず完全に手詰まりになってしまっています。
なにか良い方法はあるのでしょうか、よろしければ何卒お力添えをお願いいたします。

以下がアコーディオンのコードになります。(.vueファイルの中身を分けて記載しています。

該当のソースコード

<div class="accordion" v-for="skill in skills" v-bind:key="skill.id" v-bind:style="{ border: '8px' + ' ' + 'solid' + ' ' + skill.bgColor, backgroundColor: skill.bgColor }">
  <div class="label" v-on:click="toggleAccordion" v-bind:style="{ backgroundColor: skill.bgColor }">
    <h2>{{ skill.name }}<i class="fa fa-angle-down label-icon" v-bind:class="{ rotate : show }"></i></h2>
  </div>
  <transition name="slide">
    <div class="box" v-show="show">
      <img v-bind:src="require(`../assets/images/${skill.name}.png`)">
      <p>{{ skill.description }}</p>
    </div>
  </transition>
</div>
export default {
  name: 'AccordionComponent',
  data() {
    return {
      skills: [
        {id: 1, name: 'HTML・CSS', bgColor: 'red',
          description: `
            text
          `
        },
        {id: 2, name: 'JavaScript', bgColor: 'gold',
          description: `
            text
          `
        },
        {id: 3, name: 'Vue.js', bgColor: 'forestgreen',
          description: `
            text
          `
        },
        {id: 4, name: 'Sass', bgColor: 'hotpink',
          description: `
            text
          `
        },
        {id: 5, name: 'Firebase', bgColor: 'orange',
          description: `
            text
          `
        },
        {id: 6, name: 'Git・Github', bgColor: 'black',
          description: `
            text
          `
        },
        {id: 7, name: 'webpack', bgColor: 'skyblue',
          description: `
            text
          `
        }
      ],
      show: false
    }
  },
  methods: {
    toggleAccordion: function() {
      this.show = !this.show
    }
  }
}
<style lang="scss">
  .accordion {
    width: 400px;
    .label {
      height: 30px;
      margin-bottom: 10px;
      text-align: center;
      color: white;
      cursor: pointer;
      h2 {
        font-weight: bold;
        line-height: 30px;
      }
      .label-icon {
        margin-left: 10px;
      }
    } //.label
    .box {
      overflow: hidden;
      background-color: white;
      width: 100%;
      display: flex;
      img {
        display: block;
        width: 100px;
        height: 100px;
      }
    } //.box
  } //.accordion

  // 回転アニメーション
  .rotate {
    transform: rotate(180deg);
    transition: 0.3s;
  }

  // 開閉アニメーション
  .slide-enter-active {
    transition-duration: 0.3s;
    transition-timing-function: ease-in;
  }
  .slide-leave-active {
    transition-duration: 0.3s;
    transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
  }
  .slide-enter-to, .slide-leave {
    max-height: 100px;
    overflow: hidden;
  }
  .slide-enter, .slide-leave-to {
    max-height: 0;
    overflow: hidden;
  }
</style>
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • LanHma

    2019/07/03 22:30

    すいません!どこでv-forされているのでしょうか?

    キャンセル

  • take-t.t.

    2019/07/03 23:09

    申し訳ありません、肝心なところが抜けてしまっていました。

    質問文を修正させていただきました。

    キャンセル

回答 2

checkベストアンサー

+2

アコーディオンの開閉状態を各skillごとに別に管理して、その状態を参照してv-showを切り替える必要があると思います。
例えば以下のようなコードでしょうか

サンプルコード

一応2パターン載せておきます。①のほうがロジックはシンプルになりますが、管理する状態が増えます。
②は管理する状態は減りますが、ロジックは若干複雑になります。

今回の場合は①のほうが読みやすいかもしれません。

①開閉状態を別の連想配列として管理するパターン

<template>
  <div>
    <div
      v-for="skill in skills"
      class="accordion"
      v-bind:key="skill.id"
      v-bind:style="{ border: '8px' + ' ' + 'solid' + ' ' + skill.bgColor, backgroundColor: skill.bgColor }"
    >
      <div
        class="label"
        v-on:click="toggleAccordion(skill.id)"
        v-bind:style="{ backgroundColor: skill.bgColor }"
      >
        <h2>
          {{ skill.name }}
          <i
            class="fa fa-angle-down label-icon"
            v-bind:class="{ rotate : show[skill.id] }"
          ></i>
        </h2>
      </div>
      <transition name="slide">
        <div class="box" v-show="show[skill.id]">
          <!-- <img v-bind:src="require(`../assets/images/${skill.name}.png`)"> -->
          <p>{{ skill.description }}</p>
        </div>
      </transition>
    </div>
  </div>
</template>

<script>
export default {
  name: "HelloWorld",
  data() {
    return {
      skills: [
        {
          id: 1,
          name: "HTML・CSS",
          bgColor: "red",
          description: `
            text
          `
        },
        {
          id: 2,
          name: "JavaScript",
          bgColor: "gold",
          description: `
            text
          `
        },
        {
          id: 3,
          name: "Vue.js",
          bgColor: "forestgreen",
          description: `
            text
          `
        },
        {
          id: 4,
          name: "Sass",
          bgColor: "hotpink",
          description: `
            text
          `
        },
        {
          id: 5,
          name: "Firebase",
          bgColor: "orange",
          description: `
            text
          `
        },
        {
          id: 6,
          name: "Git・Github",
          bgColor: "black",
          description: `
            text
          `
        },
        {
          id: 7,
          name: "webpack",
          bgColor: "skyblue",
          description: `
            text
          `
        }
      ],
      show: {
        1: false,
        2: false,
        3: false,
        4: false,
        5: false,
        6: false,
        7: false
      }
    };
  },
  methods: {
    toggleAccordion(skillId) {
      this.show[skillId] = !this.show[skillId];
    }
  }
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss" scoped>
.accordion {
  width: 400px;
  .label {
    height: 30px;
    margin-bottom: 10px;
    text-align: center;
    color: white;
    cursor: pointer;
    h2 {
      font-weight: bold;
      line-height: 30px;
    }
    .label-icon {
      margin-left: 10px;
    }
  }
  .box {
    overflow: hidden;
    background-color: white;
    width: 100%;
    display: flex;
    img {
      display: block;
      width: 100px;
      height: 100px;
    }
  }
}
.rotate {
  transform: rotate(180deg);
  transition: 0.3s;
}
.slide-enter-active {
  transition-duration: 0.3s;
  transition-timing-function: ease-in;
}
.slide-leave-active {
  transition-duration: 0.3s;
  transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
}
.slide-enter-to,
.slide-leave {
  max-height: 100px;
  overflow: hidden;
}
.slide-enter,
.slide-leave-to {
  max-height: 0;
  overflow: hidden;
}
</style>

②開閉状態を既存のskills配列の中で管理するパターン

<template>
  <div>
      <div
        v-for="skill in skills"
        class="accordion"
        v-bind:key="skill.id"
        v-bind:style="{ border: '8px' + ' ' + 'solid' + ' ' + skill.bgColor, backgroundColor: skill.bgColor }"
      >
        <div
          class="label"
          v-on:click="toggleAccordion(skill)"
          v-bind:style="{ backgroundColor: skill.bgColor }"
        >
          <h2>
            {{ skill.name }}
            <i class="fa fa-angle-down label-icon" v-bind:class="{ rotate : skill.show }"></i>
          </h2>
        </div>
        <transition name="slide">
          <div class="box" v-show="skill.show">
            <!-- <img v-bind:src="require(`../assets/images/${skill.name}.png`)"> -->
            <p>{{ skill.description }}</p>
          </div>
      </transition>
  </div>
  </div>
</template>

<script>
export default {
  name: "HelloWorld",
  data() {
    return {
      skills: [
        {
          id: 1,
          show: false,     // skillごとに開閉状態を保持
          name: "HTML・CSS",
          bgColor: "red",
          description: `
            text
          `
        },
        {
          id: 2,
          show: false,
          name: "JavaScript",
          bgColor: "gold",
          description: `
            text
          `
        },
        {
          id: 3,
          show: false,
          name: "Vue.js",
          bgColor: "forestgreen",
          description: `
            text
          `
        },
        {
          id: 4,
          show: false,
          name: "Sass",
          bgColor: "hotpink",
          description: `
            text
          `
        },
        {
          id: 5,
          show: false,
          name: "Firebase",
          bgColor: "orange",
          description: `
            text
          `
        },
        {
          id: 6,
          show: false,
          name: "Git・Github",
          bgColor: "black",
          description: `
            text
          `
        },
        {
          id: 7,
          show: false,
          name: "webpack",
          bgColor: "skyblue",
          description: `
            text
          `
        }
      ]
    };
  },
  methods: {
    toggleAccordion: function(skill) {
      this.skills.find(s => s.id === skill.id).show 
      = !this.skills.find(s => s.id === skill.id).show 
    }
  }
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss" scoped>
.accordion {
  width: 400px;
  .label {
    height: 30px;
    margin-bottom: 10px;
    text-align: center;
    color: white;
    cursor: pointer;
    h2 {
      font-weight: bold;
      line-height: 30px;
    }
    .label-icon {
      margin-left: 10px;
    }
  }
  .box {
    overflow: hidden;
    background-color: white;
    width: 100%;
    display: flex;
    img {
      display: block;
      width: 100px;
      height: 100px;
    }
  }
}
.rotate {
  transform: rotate(180deg);
  transition: 0.3s;
}
.slide-enter-active {
  transition-duration: 0.3s;
  transition-timing-function: ease-in;
}
.slide-leave-active {
  transition-duration: 0.3s;
  transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
}
.slide-enter-to,
.slide-leave {
  max-height: 100px;
  overflow: hidden;
}
.slide-enter,
.slide-leave-to {
  max-height: 0;
  overflow: hidden;
}
</style>

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/07/04 12:40 編集

    ご回答、そしてcodesandboxにサンプルコードまで頂きましてありがとうございます。
    私の勉強不足で大変恐縮なのですが、まだ関数を直感的に理解できないためもう少し質問させていただいてもよろしいでしょうか?

    まずtoggleAccordionの引数「skill」はv-forで参照したskills内のオブジェクト一つ一つ、findの引数「s」はdata内の配列skillsのもつオブジェクト全てで、
    返り値がtrueの場合にそのidが合致したskills内のオブジェクトを渡している、という事でしょうか?

    キャンセル

  • 2019/07/04 12:55

    「s」は配列skills内の一個一個のオブジェクト(skillオブジェクト)ですね。find関数の場合は、配列の先頭から要素をひとつずつ取り出して、第一引数に指定した関数の引数に渡し、関数の返り値がtrueになった一番はじめの要素を返します。

    以下のようなコードを実行してみると挙動がわかりやすいかもしれません。
    以下のURLのものを少し改変しました
    https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/find

    var array1 = [5, 12, 8, 130, 44];

    var found = array1.find((s) => {
    console.log('sは' + s)
    return s > 10;
    });

    console.log('findの返り値は' + found);

    キャンセル

  • 2019/07/04 13:00

    ただ、こういうid付きのデータを扱う時は、配列ではなく、idをキーに持った連想配列やMap Objectとして扱うと、findメソッド使わずに、idをキーにして配列要素を取り出すことができるのでよりコードがシンプルになると思います。

    もしくは、アコーディオンの開閉が排他制御で良いのであれば、LanHmaさんのサンプルコードがシンプルで良いと思います。

    キャンセル

  • 2019/07/04 13:13 編集

    ご返信ありがとうございます。
    頂いたコードの値を色々と動かしてみて理解できました、

    今回は個別に開閉を行いたかったので、こちらのコードを使わせていただこうと思います。
    他にもコードをよりシンプルにするために自分で色々といじってみようと思います。

    そして改めまして、ご回答ありがとうございました。

    キャンセル

+2

クリック時にskillのidを引数に持たせて、showに代入し、比較する方法です。

<div class="accordion" v-for="skill in skills" v-bind:key="skill.id" v-bind:style="{ border: '8px' + ' ' + 'solid' + ' ' + skill.bgColor, backgroundColor: skill.bgColor }">
  <div class="label" v-on:click="toggleAccordion(skill.id)" v-bind:style="{ backgroundColor: skill.bgColor }">
    <h2>{{ skill.name }}<i class="fa fa-angle-down label-icon" v-bind:class="{ rotate : show }"></i></h2>
  </div>
  <transition name="slide">
    <div class="box" v-show="show == skill.id">
      <img v-bind:src="require(`../assets/images/${skill.name}.png`)">
      <p>{{ skill.description }}</p>
    </div>
  </transition>
</div>
methods: {
    toggleAccordion: function(id) {
      this.show = id
    }
  }

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/07/04 12:53

    ご回答ありがとうございます。

    v-showには真偽値しか使えないと勝手に思っていたので目からウロコです。
    とても勉強になりました。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 89.09%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる