Vue(ページネーション)・Laravel

Laravel(Controller)

    /**
     * Display the listing of the resource.
     *
     * @return View
     */
    public function index(TagRequest $requset): View
    {
        $names = explode(',', $requset->input('name'));
        $tags = $this->tagRepository->getByNames($names);

        return view('admin.tags.index', ['tags' => $tags]);
    }

Laravel(Repository)

    /**
     * @param array<string> $names
     * @return LengthAwarePaginator
     */
    public function getByNames(array $names): LengthAwarePaginator
    {
        $query = $this->tag->query();
        foreach ($names as $name) {
            $keyword = trim($name);
            if ($keyword === '') {
                continue;
            }
            $query->where('name', 'LIKE', "%{$keyword}%");
        }

        return $query->latest()->paginate();
    }

index.blade.php

@extends('admin.layouts.app')

@section('title', 'Tags')

@section('content')
    <page-admin-tags :laravel-paginator="{{$tags->toJson()}}" />
@endsection

index.vue

<template>
  <div class="tags">
    <header class="tags__header">
      <AppHeading :level="1" :visual="1" dark class="tags__title">タグ一覧</AppHeading>
      <div class="tags__createBtn">
        <CreateBtn href="/tags/create">新規追加</CreateBtn>
        <CreateBtn @click="isSearchVisible = true" icon="search" primary>検索</CreateBtn>
      </div>
    </header>

    <template v-if="toastText">
      <ToastBox :caution="hasRequestError" class="posts__toaster">{{ toastText }}</ToastBox>
    </template>

    <template v-if="paginator.total">
      <DataCounter :from="paginator.from" :to="paginator.to" :total="paginator.total" class="tags__counter" dark />
    </template>

    <FadeTransition>
      <template v-if="selected.length">
        <BulkActions v-model="bulkAction" :actions="bulkActions" @click="handleBulkAction" />
      </template>
    </FadeTransition>

    <TagTable v-model="selected" :items="tags" @delete="handleDelete" editable deletable dark class="tags__table" />

    <template v-if="paginator.lastPage > 1">
      <AppPager
        :current="paginator.currentPage"
        :last="paginator.lastPage"
        @click="handlePager"
        dark
        class="tags__pager"
      />
    </template>

    <form ref="delete" :action="`/tags/${deleteId}`" method="post">
      <input type="hidden" name="_method" value="delete" />
      <input :value="csrfToken" type="hidden" name="_token" />
    </form>

    <TagSearch
      :is-visible="isSearchVisible"
      :searched-name="searchQuery.name"
      @search="handleSearchSubmit"
      @cancel="handleSearchCancel"
    />
  </div>
</template>

<script lang="ts">
import Vue, { PropType } from 'vue'
import { AppHeading, AppPager } from '~/components/atoms'
import { CreateBtn, DataCounter, TagTable, BulkActions } from '~/components/molecules'
import { TagSearch } from '~/components/organisms'
import { FadeTransition } from '~/components/presenters'
import { LaravelPaginator, Paginator, FormOption } from '~/types/models'
import { Tag } from '~/types/Tags'
import recursiveToCamels from '~/utils/recursiveToCamels'
import { AxiosResponse } from 'axios'

interface Data {
  bulkAction: string
  selected: string[]
  tags: Tag[]
  deleteId: number | null
  isSearchVisible: boolean
  searchQuery: {
    name: string
  }
  toastText: string
  hasRequestError: boolean
}

const Tags = Vue.extend({
  components: { AppHeading, AppPager, CreateBtn, DataCounter, TagTable, BulkActions, FadeTransition, TagSearch },
  props: {
    laravelPaginator: { type: Object as PropType<LaravelPaginator>, required: true }
  },
  data(): Data {
    return {
      bulkAction: '',
      selected: [],
      tags: [],
      deleteId: null,
      isSearchVisible: false,
      searchQuery: {
        name: ''
      },
      toastText: '',
      hasRequestError: false
    }
  },
  computed: {
    paginator(): Paginator {
      return recursiveToCamels(this.laravelPaginator) as Paginator
    },
    csrfToken(): string {
      return this.$store.getters.csrfToken
    },
    bulkActions(): FormOption[] {
      return [{ id: 0, label: '削除', value: 'delete' }]
    }
  },
  created() {
    this.tags = this.paginator.data as Tag[]
  },
  methods: {
    apiUrl() {
      return window.Laravel.apiUrl
    },
    async requestBulk(action: string, payload: { [key: string]: unknown }): Promise<AxiosResponse> {
      const endPoint = `${window.Laravel.apiUrl}/tags`
      let response

      switch (action) {
        case 'delete':
          response = await this.$axios.delete(endPoint, { data: payload })
          break
        default:
          return
      }

      return response
    },
    async deleteTags() {
      const ids: number[] = this.selected.map(tagId => Number(tagId))
      await this.requestBulk('delete', { id: ids })
      this.tags = this.tags.filter(tag => {
        return ids.indexOf(tag.id) === -1
      })
      this.selected = []
    },
    handleBulkAction() {
      switch (this.bulkAction) {
        case 'delete':
          if (window.confirm('選択したタグを削除します。よろしいですか?')) {
            this.deleteTags()
          }
          break
      }
    },
    handleDelete(id: number) {
      this.deleteId = id
      if (window.confirm('削除してもよろしいですか?')) {
        this.$nextTick(() => {
          ;(this.$refs.delete as HTMLFormElement).submit()
        })
      }
    },
    handlePager(page: number) {
      const currentPage = new URL(window.location.href)
      currentPage.searchParams.set('page', String(page))
      window.location.href = currentPage.toString()
    },
    handleSearchSubmit(values: { [key: string]: string }) {
      const currentPage = new URL(window.location.href)
      const searchParams = new URLSearchParams(values)
      searchParams.forEach((value: string, key: string) => {
        searchParams.set(key, value.replace(/\s/g, ''))
      })
      window.location.href = `${currentPage.pathname}?${searchParams.toString()}`
    },
    handleSearchCancel() {
      this.isSearchVisible = false
      this.searchQuery = {
        name: ''
      }
    }
  }
})

export default Vue.component('page-admin-tags', Tags)
</script>

<style lang="scss">

</style>

models.ts

export interface FormOption {
  label: string
  value: string
  id: number
}

export interface LaravelFormValue {
  dataSource?: any[] | { [key: string]: any }
  hasError: boolean
  msgError: string
  value?: any
}

export interface LaravelPaginator {
  current_page: number
  data: any[]
  first_page_url: string
  from: number
  last_page: number
  last_page_url: string
  next_page_url?: string
  path: string
  per_page: number
  prev_page_url?: string
  to: number
  total: number
}

export interface Paginator {
  currentPage: number
  data: any[]
  firstPageUrl: string
  from: number
  lastPage: number
  lastPageUrl: string
  nextPageUrl?: string
  path: string
  perPage: number
  prevPageUrl?: string
  to: number
  total: number
}

export interface Tab {
  id: number
  to: string
  label: string
}

export interface ECRow {
  id: number
  name: string
  createdAt: string
  updatedAt: string
}

export interface Pager {
  id: number
  to: string | number
}

export interface Link {
  id: number
  to: string
  label: string
}

export interface NavLink extends Link {
  icon?: string
  slug?: string
}

export interface NavGroup {
  id: number
  name: string
  items: NavLink[]
}

export interface NavTab extends Tab {
  icon: string
}

export interface DescriptionGroup {
  id: number
  name: string
  items: string[]
}

export interface BulkActionType {
  label: string
  action: string
}

export interface AutocompleteOption {
  id: number
  label: string
  disabled?: boolean
}

AppPager.vue

<template>
  <ul :class="styledClasses" class="appPager">
    <template v-if="last > 1">
      <li class="appPager__item">
        <button :disabled="current < 2" @click="$emit('click', current - 1)" class="appPager__btn">Prev</button>
      </li>
    </template>

    <template v-if="last > 1">
      <li class="appPager__item">
        <button :aria-current="String(1 === current)" @click="$emit('click', 1)" class="appPager__btn">1</button>
      </li>
    </template>

    <template v-if="displayBeforeEllipsis">
      <li class="appPager__item">...</li>
    </template>

    <template v-for="item in pages">
      <li :key="item" class="appPager__item">
        <button :aria-current="String(item === current)" @click="$emit('click', item)" class="appPager__btn">
          {{ item }}
        </button>
      </li>
    </template>

    <template v-if="displayAfterEllipsis">
      <li class="appPager__item">...</li>
    </template>

    <template v-if="last > 1">
      <li class="appPager__item">
        <button :aria-current="String(last === current)" @click="$emit('click', last)" class="appPager__btn">
          {{ last }}
        </button>
      </li>
    </template>

    <template v-if="last > 1">
      <li class="appPager__item">
        <button :disabled="current >= last" @click="$emit('click', current + 1)" class="appPager__btn">Next</button>
      </li>
    </template>
  </ul>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  props: {
    current: { type: Number, required: true },
    last: { type: Number, required: true },
    small: { type: Boolean },
    dark: { type: Boolean }
  },
  computed: {
    styledClasses(): string[] {
      const classes: string[] = []

      if (this.small) classes.push('-small')
      if (this.dark) classes.push('-dark')

      return classes
    },
    displayBeforeEllipsis(): boolean {
      if (this.last <= 7) return false
      return this.current - 3 > 1
    },
    displayAfterEllipsis(): boolean {
      if (this.last <= 7) return false
      return this.current + 3 < this.last
    },
    pages(): number[] {
      const items: number[] = []

      if (this.last <= 6) {
        for (let i = 2; i < this.last; i++) {
          items.push(i)
        }
        return items
      }

      if (this.current < 4) return [2, 3, 4, 5, 6]

      if (this.current + 3 >= this.last) {
        for (let i = this.last - 5; i < this.last; i++) {
          items.push(i)
        }
        return items
      }

      for (let i = this.current - 2, length = this.current + 3; i < length; i++) {
        items.push(i)
      }

      return items
    }
  }
})
</script>

<style lang="scss" scoped>

</style>

コメント

タイトルとURLをコピーしました