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

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

ただいまの
回答率

87.59%

Nuxt, Vueで実現したい挙動のコードがかなり冗長になってしまい、アドバイスいただきたいです。

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 1
  • VIEW 1,085

score 39

概要

Nuxtのプロジェクト内で以下画像のような画面を作成しています。
ひとまず動作するようにしようと思い、以下コードのように記述したのですが、繰り返しも多くかなりダメなコードになってしまっていると思います。
以下を実現させるために、よりよい書き方等あればと思い質問させていただきました。
どなたかアドバイスいただけますでしょうか。

不足点は以降発見次第追記させていただきます。
どうぞ、よろしくお願いいたします。

実現したいこと

前提条件

  • 各カード(li要素)、コンポーネントにはjsonからIDを取得し、そのIDに応じた要素を表示させる。
  • IDの数は増減する可能性がある。

期待している挙動

  • 各カードをクリックすると、対応したコンポーネント(モーダル)を表示。
  • コンポーネントのボタンをクリックすると、コンポーネントを閉じる(非表示)
  • コンポーネントのAgreeボタンをクリックすると対応するカードの状態をtrueに変更(それに伴うスタイルの付与)
  • 全てのカードの状態がtrueになると、画面下部のボタンをactivate

サンプル画面

以下実装中のサンプル画面です。

  • 各カードをクリックすると、それに対応する子コンポーネントがモーダルとして表示されます。
  • モーダル内のボタンをクリックすると、またそれに対応したカードの状態(data)をtrueに変更し、v-ifによる文章の変更及びstyleの付与を行っております。
  • 全てのカードの状態がtrueになるとボタンがアクティベートされクリック可能になります(この部分は現在実装している下記コードではid数の増減に対応できていません)

イメージ説明

該当のソースコード

各コード、style等不要と思われる部分は消しています。

親コンポーネント
以下コードで、特に繰り返しになってしまっている記述についてどうにか省略できないか考えております。
現在はjsonから取得するidを3つに設定しているため、少しすっきりして見えますが、idを10個など取得すると、かなり可読性の悪いコードになってしまっています。

追記
だんだん短くなってきました...!

<template>
  <section class="container">

    <!-- componentを配置 / 子コンポーネントでボタンをクリック時にイベントハンドラを受けとり、イベントを発火 -->
    <agree-document v-if="isShow.id01" @doAgree="closeModal"></agree-document>
    <agree-document2 v-if="isShow.id02" @doAgree="closeModal"></agree-document2>
    <agree-document3 v-if="isShow.id03" @doAgree="closeModal"></agree-document3>

    <div class="wrapper">
        <ul>
      <!-- jsonから取得したidの数だけv-forで繰り返し -->
          <li v-for="id in ids" :key="id">

       <!-- カードの上に重ね、クリックするとコンポーネントを表示 -->
            <div v-if="id == id" @click="openModal(id)" class="list__item-link"></div>

       <!-- カードタイトル -->
            <p v-if="id == 'id01'">カード1</p>
            <p v-if="id == 'id02'">カード2</p>
            <p v-if="id == 'id03'">カード3</p>

            <div v-if="id == id">
              <p v-if="!isAgreed[id]">Not agree</p>
              <p v-if="isAgreed[id]" class="isAgreed">Agreed</p>
            </div>

          </li>
        </ul>

        <!-- button -->
        <button
          :class="{ isActive: completed }"
        >
          <span>次へ</span>
        </button>
    </div>
  </section>
</template>

<script>
import { request } from "~/data/index"; // 別ファイルで定義したrequest関数を取得

// コンポーネントの読み込み
import AegreDocument from "~/components/AgreeDocument.vue";
import AegreDocument2 from "~/components/AgreeDocument2.vue";
import AegreDocument3 from "~/components/AgreeDocument3.vue";

export default {
  components: {
    AgreeDocument,
    AgreeDocument2,
    AgreeDocument3
  },
  data() {
    return {
      ids: [],
      isShow: {
        id01: false,
        id02: false,
        id03: false
      },
      isAgreed: {
        id01: false,
        id02: false,
        id03: false
      }
    };
  },
  computed: {
    // ここもハードで処理を書いているため、
    completed() {
      return (
        this.isAgreed.id01 &&
        this.isAgreed.id02 &&
        this.isAgreed.id03
      );
    }
  },
  mounted() {
    // 別のjsファイルで作成した関数を使用し、jsonからresponseを取得
    this.$nextTick(() => {
      const params = {};
      request(this.$axios, this.$store, params, this.$router)
        .then(response => {
          this.ids = response.data.ids;
        })
    });
  },
  methods: {
    openModal(id) {
      Object.keys(this.isShow).forEach(key => {
        if (key === id) {
          this.isAgreed[key] = true;
          this.isShow[key] = !this.isShow[key];
        }
      });
    },
    closeModal() {
      Object.keys(this.isShow).forEach(key => {
        if (this.isShow[key] === true) this.isShow[key] = false;
      });
    }
  }
};
</script>

<style scoped>
.list__item-link {
  background-color: transparent;
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
}

/* ボタン通常時 */
.btn {
  pointer-events: none;
}
/* ボタンactive時 */
.isActive {
  color: #fff;
  background: blue;
  cursor: pointer;
  pointer-events: auto;
}
/* コンポーネントで同意後にtextの色をactiveにする */
.isAgreed {
  color: red;
}
</style>


取得するjsonファイル

{
  "data": {
    "ids": [
      "id01",
      "id02",
      "id03"
    ]
  }
}


モーダルとして表示する子コンポーネント
以下コンポーネントはjsonから取得するidの数だけ作成してあります。

<template>
  <section>

    <div v-if="id[index] == 'id01'">
      <h1>ID1のタイトル</h1>
    </div>
    <button type="button" class="btn btn-next" @click="doAgree">
      <span class="btn-text">Agree</span>
    </button>

  </section>
</template>

<script>
import { request } from "~/data/index"; // 別ファイルで定義したrequest関数を取得

export default {
  data() {
    return {
      ids: [],
      index: 0
    };
  },
  mounted() {
    this.$nextTick(() => {
      const params = {};
      request(this.$axios, this.$store, params, this.$router)
        .then(response => {
          this.ids = response.data.ids})
    });
  },
  methods: {
    // ボタンクリック時に、親コンポーネントのイベントを発火
    doAgree() {
      this.$emit("doAgree");
    }
  }
};
</script>

補足情報(ツールのバージョン)

"nuxt": "^2.4.2",

  • 気になる質問をクリップする

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

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

    クリップを取り消します

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

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

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

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

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

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

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

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

    質問の評価を下げる

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

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

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

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

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

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

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

    詳細な説明はこちら

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

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

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

回答 1

checkベストアンサー

0

v-if="id == id" など、必ず true になる記述があるのが冗長です。
isShow や isAgreed の数は、ids で決まるようなので、最初は空にしておき、
ids を取得したタイミングで代入すべきです。
あとは、HTML構造(ulがリストでなかったり)がおかしい部分があるので、見直したほうがいいと思います。


前提条件等が細かく書いていないので、そのまま当てはまるとは思いませんが、
参考までにどうぞ。
Nuxt.js を新しく立ち上げて、該当のファイルだけ編集すれば動くと思います。

AgreeDocument1,2,3の詳細がわかりませんが、
同じデザインのモーダルとして共通化しています。

/* /pages/index.vue */

<template>
  <div>
    <agree-document
      v-if="currentIndex !== null"
      :index="currentIndex"
      @agree="handleAgree($event);"></agree-document>

    <div class="wrapper">
      <ul class="cards">
        <li
          v-for="(wrappedId, index) in wrappedIds"
          :key="wrappedId.id"
          @click="openModal(index);"
          class="card">
          <p>カード{{ index + 1 }}</p>
          <div>
            <p class="{ 'agreed': wrappedId.agreed }">
              {{ wrappedId.agreed ? "Agreed" : "Not agreed" }}
            </p>
          </div>
        </li>
      </ul>
      <button :disabled="!completed">
        <span v-if="completed">次へ</span>
        <span v-else>すべてに同意してください</span>
      </button>
    </div>
  </div>
</template>

<script>
import AgreeDocument from "~/components/AgreeDocument";

export default {
  components: {
    AgreeDocument
  },
  data() {
    return {
      wrappedIds: [],
      currentIndex: null // 現在のインデックスを保存するために使用します。
    };
  },
  computed: {
    completed() {
      // Array.prototype.every() で調べてみてください。
      return this.wrappedIds.every(wrappedId => wrappedId.agreed);
    }
  },
  mounted() {
    // HTTP通信の代わりです。
   // `ids` の数を増減しても対応しています。試してみてください。
    const requestMock = Promise.resolve({
      data: {
        ids: ["id01", "id02", "id03"]
      }
    });

    requestMock.then(response => {
    // { id: id, agreed: false } のオブジェクトとして、配列化します。
      // `index`さえあれば「id名」も「そのid名について同意しているかどうか」を取得できます。
      this.wrappedIds = response.data.ids.map(id => ({ id, agreed: false }));
    });
  },
  methods: {
    openModal(index) {
      this.currentIndex = index;
    },
    closeModal() {
      this.currentIndex = null;
    },
    handleAgree(index) {
      this.wrappedIds[index].agreed = true;
      this.closeModal();
    }
  }
};
</script>

<style scoped>
.cards {
  width: 100%;
  display: flex;
  padding: 0;
  margin: 0;
  flex-align: center;
  margin-bottom: 15px;
}
.card {
  box-sizing: border-box;
  width: 140px;
  list-style: none;
  padding: 30px;
  border: 1px solid #000;
  border-radius: 5px;
  margin: 0 10px;
}
</style>
/* /components/AgreeDocument.vue */

<template>
  <div @click="$emit('agree', index);" class="modal">
    <p>Click to agree</p>
  </div>
</template>

<script>
export default {
  name: "AgreeDocument",
  props: {
    index: {
      type: Number,
      required: true
    }
  }
};
</script>

<style scoped>
.modal {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 100;
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  flex-align: center;
  align-items: center;
  color: #fff;
  background-color: rgba(0, 0, 0, 0.8);
}
</style>

投稿

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2019/02/20 14:49

    >> NozomuIkutaさん

    ご回答ありがとうございます!
    上記参考にさせていただき、リファクタを行ったところ、想定していた動作をかなり短いコードで実現することができました!とても助かりました。

    特にdata内にisShow, isAgreedなど全ての状態を定義していたことで、ボタンのactivateなどid数が変わったときに対応できていませんでしたが、上記のように書くことで解決することができました!

    ただ、子コンポーネント(モーダル)内で必要な処理が増え(idのmaxlengthの受け渡しや戻るボタンの設置 etc)index.vueのhtml内でコンポーネントを読み込む際に、バインディングする値が増える+コンポーネントを10個程度読み込むことになりそうなので、その部分がまた長くなってしまっていますがそれはしょうがなさそうですね、、

    大変助かりました、ありがとうございました。

    キャンセル

  • 2019/02/20 23:57

    子コンポーネントの仕様詳細がわからないので回答はできませんが、いくらか参考になったようでよかったです。

    キャンセル

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

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

同じタグがついた質問を見る