본문 바로가기
개발/js, vue, nuxt

infinite table - 양방향 무한 스크롤 테이블(vue)

by 개발자종혁 2021. 3. 31.
728x90

사용예제

코드

components/common/InfiniteTable.vue

<template>
  <div
    id="tableFixHeadDiv"
    ref="tableContainer"
    @scroll.passive="onScroll"
    @mousewheel.passive="whenScrollNotExist"
  >
    <table id="scroll-target">
      <thead>
        <tr>
          <th
            v-for="item in headers"
            :key="item.value"
            :class="item.class"
            :style="{ textAlign: item.align }"
          >
            {{ item.text }}
          </th>
        </tr>
      </thead>
      <tbody :key="items.length" class="boxShadow">
        <tr v-show="isUpEnd === true || items == null || items.length == 0">
          <td
            :colspan="headers.length"
            @mousewheel.up="throttleFetchUp"
          >
            시간 내 조회 결과가 존재하지 않습니다.
            <v-btn @click="fetchUp" text color="blue">이어하기</v-btn>
          </td>
        </tr>

        <tr
          v-for="item in items"
          :key="item.id"
          :class="rowClassMethod ? `${rowClassMethod(item)}` : ''"
          style="border-top: 1px solid rgb(220,220,220); border-bottom: 1px solid rgb(220,220,220);"
        >
          <td
            v-for="prop in headerProperties"
            :key="prop.value"
            :style="{ [`text-align`]: prop.align }"
          >
            <template v-if="prop.filter">
              {{ prop["filter"](item[prop.value]) }}
            </template>
            <template v-else>
              {{ item[prop.value] }}
            </template>
          </td>
        </tr>
        <tr v-if="items === null || items.length === 0" class="bg-no-content">
          <td
            :colspan="headers ? headers.length : 1"
          >
            조회 결과가 존재하지 않습니다.
          </td>
        </tr>

        <tr v-show="isEnd === true || items == null || items.length == 0">
          <td
            :colspan="headers.length"
            @mousewheel.down="throttleFetchDown"
          >
            시간 내 조회 결과가 존재하지 않습니다.
            <v-btn @click="fetchDown" text color="blue">이어하기</v-btn>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
export default {
  props: {
    headers: Array,
    isEnd: Boolean,
    isUpEnd: Boolean,
    items: Array,
    fetchDown:Function,
    fetchUp: Function,
    rowClassMethod: Function
  },
  data() {
    return {
      timer: null
    };
  },
  computed: {
    loading() {
      return this.$store.state.common.loading;
    },
    headerProperties() {
      var properties = {};
      var lHeaders = null;
      lHeaders = this.headers == null ? [] : this.headers;
      lHeaders.forEach(item => {
        properties[item.value] = item;
      });
      return properties;
    }
  },
  methods: {
    isScrollVisible() {
      return (
        this.$refs.tableContainer.scrollHeight >
        this.$refs.tableContainer.clientHeight
      );
    },
    onScroll(e) {
      // var horizontalScrollbarHeight =
      //   e.target.offsetHeight - e.target.clientHeight;
      var scrollTop = e.target.scrollTop; // 상단 위치
      var scrollHeight = e.target.scrollHeight;
      var offsetHeight = e.target.offsetHeight; // 기본 위치(scroll 길이)

      // scroll이 top을 찍을때
      if (scrollTop === 0) {
        this.throttleFetchUp();
        e.target.scrollTop += 10;
        // scroll이 bottom을 찍을 때
      } else if (scrollHeight <= offsetHeight + scrollTop) {
        this.throttleFetchDown();

        e.target.scrollTop -= 10;
      }
    },
    whenScrollNotExist(e) {
      if (!this.isScrollVisible()) {
        this.throttleFetch(e);
      }
    },
    throttleFetch(e) {
      if (e.deltaY > 0) {
        this.throttleFetchDown();
      } else {
        this.throttleFetchUp();
      }
    },
    throttleFetchDown() {
      if (this.timer) clearTimeout(this.timer);
      this.timer = setTimeout(this.fetchDown, 200);
    },
    throttleFetchUp() {
      if (this.timer) clearTimeout(this.timer);
      this.timer = setTimeout(this.fetchUp, 200);
    }
  }
};
</script>

<style scoped>
.bg-no-content {
  background-color: whitesmoke !important;
}
.bg-right {
  background-color: white !important;
}
.bg-right:hover {
  background-color: whitesmoke !important;
}
.bg-error {
  background-color: #ef9a9a;
}
.bg-error:hover {
  background-color: #ffcdd2 !important;
}
#tableFixHeadDiv {
  overflow-y: auto;
  height: 56vh;
  border-width: 1px;
}
#tableFixHeadDiv thead th {
  position: sticky;
  top: 0;
}

/* .boxShadow {
  box-shadow: inset 0px 0px 20px 0 rgba(0, 0, 0, 0.3);
} */

/* Just common table stuff. Really. */
#scroll-target {
  border-spacing: 2px;
  border-collapse: collapse;
  /* height: 56vh; */
  width: 100%;
  /* overflow: scroll; */
}
#scroll-target th,
td {
  padding: 8px 16px;
}
#scroll-target tr {
  border: 1px grey;
}
#scroll-target th {
  background: #eee;
}
</style>

InfiniteTable.vue 사용하기


<template>

        <infinite-table
          :isUpEnd="isUpEnd"
          :rowClassMethod="redRowIfStatusCdNot200"
          :isEnd="isEnd"
          :headers="headers"
          :items="items"
          :fetchDown="fetchDown"
          :fetchUp="fetchUp"
        ></infinite-table>
</template>

<script>

import InfiniteTable from "@/components/common/InfiniteTable.vue";


export default {
  components: { InfiniteTable },
  data() {
    return {
      headers:  [ // 예시에 나온 형태 
        {
          text: "날짜",
          value: "reg_dt",
          align: "center",
          class: "grey lighten-2  grey darken-4--text"
        },

        {
          text: "ip",
          value: "ip",
          align: "center",
          class: "grey lighten-2  grey darken-4--text"
        },
        {
          text: "status code",
          value: "status_cd",
          align: "center",
          class: "grey lighten-2  grey darken-4--text",
          width: "2rem"
        },

        {
          text: "url",
          value: "url",
          align: "center",
          class: "grey lighten-2  grey darken-4--text"
        },
        {
          text: "method",
          value: "method",
          align: "center",
          class: "grey lighten-2  grey darken-4--text",
          width: "2rem"
        },


        {
          text: "user agent",
          value: "user_agent",
          align: "center",
          class: "grey lighten-2  grey darken-4--text"
        }


      ]
    };
  },
  computed: {
    headerProperties() {
      var properties = {};
      this.headers.forEach(item => {
        properties[item.value] = item;
      });
      return properties;
    },
    isEnd() {
       // 해당 범위 아래에 더이상 정보 없는지 여부
    },
    isUpEnd() {
      // 해당 범위 아래에 정보 더 없는지 여부
    },

    init: {
      get() {
        // 최초 초기화인지 여부 가져오기
      },
      set(init) {
        // 최초 초기화 설정 설정
      }
    },
    startDate() {
      // 시작 일시
      return 시작일시; 
    },
    endDate: {
      // 종료 일시
      return 시간;
    },

    inquiryDate() {
      // 검색에 사용되는 date;
    },

    totalPage() {
       // 전체 페이지
    },
    totalCount() {
        // 전체 개수
    },
    items() {
      // 정보 array [{}, {}] ;
    },
  },
  methods: {
    redRowIfStatusCdNot200(item) {
      return item.status_cd == 200 ? "bg-right" : "bg-error";
    },
    async fetchDown() {
      // 아래로 내렸을 때 정보 변경
    },
    async fetchUp() {
      // scroll 올렸을 때 정보 변경
    }

    async setInit(init) {
      // 초기화 했는지 여부;
    },
  },
  async beforeMount() {},
  async created() {
    // 정보가져오기
  },
  async destroyed() {}
};
</script>

<style scoped>
#flexStyle {
  display: flex;
}
</style>

 

728x90

'개발 > js, vue, nuxt' 카테고리의 다른 글

VUE: component와 parent간 data 이동  (0) 2020.04.01
개발 시 chrome cache 없애기 전략  (0) 2020.03.11
vue를 사용하는 이유  (0) 2020.03.10

댓글