Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vuejs 3 (vue-next) support #2505

Closed
brmdbr opened this issue May 13, 2020 · 138 comments
Closed

Vuejs 3 (vue-next) support #2505

brmdbr opened this issue May 13, 2020 · 138 comments

Comments

@brmdbr
Copy link

brmdbr commented May 13, 2020

Description

Vuejs 3 has been in development for a while and is seeing rapid improvements, currently in a alpha state Buetify should probably start looking at supporting it.

@jtommy
Copy link
Member

jtommy commented May 14, 2020

Yes but it'll support only the official version

@rcoundon
Copy link
Contributor

rcoundon commented Jul 5, 2020

New timeline and recommendations for adopting Vue 3
vuejs/rfcs#183

@mllull
Copy link

mllull commented Jul 22, 2020

@jtommy Well, finally Vue 3 is in RC stage (vuejs/rfcs#189), so his API will not suffer more braking changes.

Any plans on implement it on Buefy?

@brendan1212
Copy link

Yes please! Current version is RC5, which means we're getting very close. Since we all love Buefy and will continue to use it, the sooner the better on Vue 3 support. : )

@Tofandel
Copy link
Contributor

Tofandel commented Aug 19, 2020

Just a little info, it's vey hard to have support for Vue 2 and Vue 3 in the same code base, there has been too many breaking changes, one of them is the v-model change

https://v3.vuejs.org/guide/migration/v-model.html#migration-strategy

For all v-models without arguments, make sure to change props and events name to modelValue and update:modelValue respectively

<ChildComponent v-model="pageTitle" />
// ChildComponent.vue

export default {
  props: {
    modelValue: String // previously was `value: String`
  },
  methods: {
    changePageTitle(title) {
      this.$emit('update:modelValue', title) // previously was `this.$emit('input', title)`
    }
  }
}

An other one is the Vue.createApp instead of new Vue

That means for Vue 3 support, a huge breaking change will be needed (since it won't run on vue 2 and vice versa) and 2 code bases will need to be supported

@amir20
Copy link

amir20 commented Aug 19, 2020

I think you are right about v-model. @Tofandel

But I don’t understand your point about createApp. Isn’t that only for mounting an app? Since Buefy is only a set of components, nothing should be mounted except when mounting the app.

I think there are other breaking changes. I am also trying to see if it’s possible to have one branch supporting two versions of Vue.

@Tofandel
Copy link
Contributor

Tofandel commented Aug 19, 2020

@amir20 You are correct regarding createApp, but there are programmatic components within buefy as well that need to be created using new Vue

I looked at all possible angles of supporting both in the same code base, one is to drop all internal .sync for events, add support for two props one modelValue and one value, if value is provided you need to emit input if modelValue then you need to emit update:modelValue, you also need a computed value with getter and setter that returns either value or modelValue depending on definition, not to mention the things that could break, the increase in code size and the possible naming conflicts, though it is possible maybe with a mixin to make things simpler

It would look something like this

import Vue from 'vue'

let def;
if (Vue.createApp) {
    def = {
        props: ['modelValue'],
        computed: {
            vModel: {
                get() {
                    return this.modelValue;
                },
                set(val) {
                    this.$emit('updated:modelValue', val);
                }
            }
        }
    }
} else {
    def = {
        props: ['value'],
        computed: {
            vModel: {
                get() {
                    return this.value;
                },
                set(val) {
                    this.$emit('input', val);
                }
            }
        }
    }
}
export default def;

Then you still need to add the mixin everywhere remove all value prop, use vModel instead and replace all $emit('input', val) to vModel = val

@jtommy
Copy link
Member

jtommy commented Aug 19, 2020

@Tofandel you are right, but considering that I don't want to introduce composition API it might be good to maintain only one version and build it separately

@cjblomqvist
Copy link

@jtommy, out of curiosity, why do prefer to not introduce the composition API? I'm not saying you should in any way, just interested in your arguments (I'm also considering my stance on the subject and to get a feel for when and when not to introduce it).

@jtommy
Copy link
Member

jtommy commented Aug 20, 2020

@cjblomqvist i like composition API but it introduces a refactoring that I don't want to do at the moment since the priority is to be compliant with Vue 3

@cjblomqvist
Copy link

@jtommy - very clear! Thanks!

@bzd2000
Copy link

bzd2000 commented Aug 23, 2020

I'm available to contribute for this migration to vue 3. But will need some guidance about initial setup (like breaking change on plugin etc) and strategy

@ExSpecter
Copy link

ExSpecter commented Aug 27, 2020

@jtommy just wanted to ask, if there is any rough timeline on when buefy will be ready for Vue 3? :)
Really looking forward to use it.

@jtommy
Copy link
Member

jtommy commented Aug 27, 2020

I'm sorry but there is a timeline or ETA about it

@javiercbk
Copy link

javiercbk commented Sep 13, 2020

I've made buefy work with vue3 by changing a lot of stuff in the component registration itself. The components mostly work on their own. I minimal changes. had to be done in the components, but currently tackling errors as I encountered them so more changes are still to be found

  • First, the vueInstance should be now an appInstance...I left it as vueInstance becase I wanted to make buefy work.
  • Implemented a slightly different API for the install function
  • Runned this regexp replace \$scopedSlots\.([a-zA-z]+) replaced with $slots.$1()
  • Renamed emit("value") to emit("update:value") and other variants with other prop names
  • Removed every this.$destroy()and basically threw an error.
  • Some slot="aSlotName" had to be changed to <template #aSlotName>.
  • TableColumn always complains about the Table injected not being defined (this.$table is always undefined or null). I had to change the provide and inject code in Table or TableColumn...I cannot remember exactly where nor what now.
  • Had some issues about vue asking for key="aKey" to be declared in a <template>which would be wrong. I had to install a newer version of eslint-plugin to solve that.
  • Occasionally the linter fails with the error above. I stop and restart the application and hope for the best. It works 80% of the time.

I can share my work in the form of a PR, BUT I would not say is well done nor production ready. I wanted buefy to work so I did "cut some corners".

I have not even attempted to fix unit test, nor I have tried to run e2e tests.

@jtommy
Copy link
Member

jtommy commented Sep 13, 2020

I'm working on a solution without rewrite or duplicate code but at the moment for the other my project https://github.com/oruga-ui/oruga/tree/vue3.
Btw rollup Vue plugin doesn't work well and I'm waiting for an official fix

@cjblomqvist
Copy link

Sounds like it would be beneficial if you guys ( @jtommy @javiercbk ) opened up PRs (albeit being WIP) so that others can learn/see what you've done - or at least reference your own branches here so that one can see and learn from your code.

Possibly, you could also try to join forces and work on one of your branches together.

Anyway, just my 5 cents :)

@jtommy
Copy link
Member

jtommy commented Sep 14, 2020

@cjblomqvist Will be quite easy to migrate my current code for Oruga to Buefy but probably I'll need beta testers

@javiercbk
Copy link

@jtommy I'll alpha, beta and omega test it

@jtommy
Copy link
Member

jtommy commented Sep 16, 2020

@javiercbk Are you able to use table with your code ? In the deep.. are you able to access to componentInstance looping in default slot like current code (v2)

@javiercbk
Copy link

javiercbk commented Sep 19, 2020

@jtommy I had to make some changes:

This is a sample usage:
<template>
  <div>
    <modal-box
      :is-active="isModalActive"
      :trash-object-name="trashObjectName"
      @confirm="trashConfirm"
      @cancel="trashCancel"
    />
    <b-table
      v-model:checked-rows="checkedRows"
      :checkable="checkable"
      :loading="isLoading"
      :paginated="paginated"
      :per-page="perPage"
      :striped="true"
      :hoverable="true"
      default-sort="name"
      :data="clients"
    >
      <template #default>
        <b-table-column class="has-no-head-mobile is-image-cell">
          <template #default="tableProps">
            <div class="image">
              <img :src="tableProps.row.avatar" class="is-rounded" />
            </div>
          </template>
        </b-table-column>
        <b-table-column label="Name" field="name" sortable>
          <template #default="tableProps">
            {{
            tableProps.row.name
            }}
          </template>
        </b-table-column>
        <b-table-column label="Company" field="company" sortable>
          <template #default="tableProps">
            {{
            tableProps.row.company
            }}
          </template>
        </b-table-column>
        <b-table-column label="City" field="city" sortable>
          <template #default="tableProps">
            {{
            tableProps.row.city
            }}
          </template>
        </b-table-column>
        <b-table-column class="is-progress-col" label="Progress" field="progress" sortable>
          <template #default="tableProps">
            <progress
              class="progress is-small is-primary"
              :value="tableProps.row.progress"
              max="100"
            >{{ tableProps.row.progress }}</progress>
          </template>
        </b-table-column>
        <b-table-column label="Created">
          <template #default="tableProps">
            <small
              class="has-text-grey is-abbr-like"
              :title="tableProps.row.created"
            >{{ tableProps.row.created }}</small>
          </template>
        </b-table-column>
        <b-table-column custom-key="actions" class="is-actions-cell">
          <template #default="tableProps">
            <div class="buttons is-right">
              <button
                class="button is-small is-danger"
                type="button"
                @click.prevent="trashModal(tableProps.row)"
              >
                <b-icon icon="trash" size="is-small" />
              </button>
            </div>
          </template>
        </b-table-column>
      </template>
      <template #empty>
        <section class="section">
          <div class="content has-text-grey has-text-centered">
            <template v-if="isLoading">
              <p>
                <b-icon icon="ellipsis-h" size="is-large" />
              </p>
              <p>Fetching data...</p>
            </template>
            <template v-else>
              <p>
                <b-icon icon="frown" size="is-large" />
              </p>
              <p>Nothing's here&hellip;</p>
            </template>
          </div>
        </section>
      </template>
    </b-table>
  </div>
</template>

<script lang="ts" src="./ClientsTableSample.ts"></script>
And I had to make some changes to Table.vue
<template>
  <div class="b-table">
    <slot />

    <b-table-mobile-sort
      v-if="mobileCards && hasSortablenewColumns"
      :current-sort-column="currentSortColumn"
      :sort-multiple="sortMultiple"
      :sort-multiple-data="sortMultipleDataComputed"
      :is-asc="isAsc"
      :columns="newColumns"
      :placeholder="mobileSortPlaceholder"
      :icon-pack="iconPack"
      :sort-icon="sortIcon"
      :sort-icon-size="sortIconSize"
      @sort="(column, event) => sort(column, null, event)"
      @removePriority="(column) => removeSortingPriority(column)"
    />

    <template
      v-if="
        paginated &&
          (paginationPosition === 'top' || paginationPosition === 'both')
      "
    >
      <slot name="pagination">
        <b-table-pagination
          v-bind="$attrs"
          :per-page="perPage"
          :paginated="paginated"
          :icon-pack="iconPack"
          :total="newDataTotal"
          v-model:current-page="newCurrentPage"
          @page-change="(event) => $emit('page-change', event)"
        >
          <slot name="top-left" />
        </b-table-pagination>
      </slot>
    </template>

    <div class="table-wrapper" :class="tableWrapperClasses" :style="tableStyle">
      <table
        class="table"
        :class="tableClasses"
        :tabindex="!focusable ? false : 0"
        @keydown.self.prevent.up="pressedArrow(-1)"
        @keydown.self.prevent.down="pressedArrow(1)"
      >
        <thead v-if="newColumns.length && showHeader">
          <tr>
            <th v-if="showDetailRowIcon" width="40px" />
            <th
              class="checkbox-cell"
              v-if="checkable && checkboxPosition === 'left'"
            >
              <template v-if="headerCheckable">
                <b-checkbox
                  :value="isAllChecked"
                  :disabled="isAllUncheckable"
                  @change.native="checkAll"
                />
              </template>
            </th>
            <th
              v-for="(column, index) in visibleColumns"
              :key="column.type.data.newKey + ':' + index + 'header'"
              :class="[
                column.headerClass,
                {
                  'is-current-sort':
                    !sortMultiple && currentSortColumn === column,
                  'is-sortable': column.sortable,
                  'is-sticky': column.sticky,
                  'is-unselectable': column.isHeaderUnSelectable,
                },
              ]"
              :style="column.style"
              @click.stop="sort(column, null, $event)"
            >
              <div
                class="th-wrap"
                :class="{
                  'is-numeric': column.numeric,
                  'is-centered': column.centered,
                }"
              >
                <template
                  v-if="
                    column.$slots &&
                      column.$slots.header &&
                      column.$slots.header()
                  "
                >
                  <b-slot-component
                    :component="column"
                    scoped
                    name="header"
                    tag="span"
                    :props="{ column, index }"
                  />
                </template>
                <template v-else>
                  <span class="is-relative">
                    {{ column.props.label }}
                    <template
                      v-if="
                        sortMultiple &&
                          sortMultipleDataComputed &&
                          sortMultipleDataComputed.length > 0 &&
                          sortMultipleDataComputed.filter(
                            (i) => i.field === column.props.field
                          ).length > 0
                      "
                    >
                      <b-icon
                        :icon="sortIcon"
                        :pack="iconPack"
                        both
                        :size="sortIconSize"
                        :class="{
                          'is-desc':
                            sortMultipleDataComputed.filter(
                              (i) => i.field === column.props.field
                            )[0].order === 'desc',
                        }"
                      />
                      {{ findIndexOfSortData(column) }}
                      <button
                        class="delete is-small multi-sort-cancel-icon"
                        type="button"
                        @click.stop="removeSortingPriority(column)"
                      />
                    </template>

                    <b-icon
                      v-else
                      :icon="sortIcon"
                      :pack="iconPack"
                      both
                      :size="sortIconSize"
                      class="sort-icon"
                      :class="{
                        'is-desc': !isAsc,
                        'is-invisible': currentSortColumn !== column,
                      }"
                    />
                  </span>
                </template>
              </div>
            </th>
            <th
              class="checkbox-cell"
              v-if="checkable && checkboxPosition === 'right'"
            >
              <template v-if="headerCheckable">
                <b-checkbox
                  :value="isAllChecked"
                  :disabled="isAllUncheckable"
                  @change.native="checkAll"
                />
              </template>
            </th>
          </tr>
          <tr v-if="hasCustomSubheadings" class="is-subheading">
            <th v-if="showDetailRowIcon" width="40px" />
            <th v-if="checkable && checkboxPosition === 'left'" />
            <th
              v-for="(column, index) in visibleColumns"
              :key="column.newKey + ':' + index + 'subheading'"
              :style="column.style"
            >
              <div
                class="th-wrap"
                :class="{
                  'is-numeric': column.numeric,
                  'is-centered': column.centered,
                }"
              >
                <template
                  v-if="
                    column.$slots &&
                      column.$slots.subheading &&
                      column.$slots.subheading()
                  "
                >
                  <b-slot-component
                    :component="column"
                    scoped
                    name="subheading"
                    tag="span"
                    :props="{ column, index }"
                  />
                </template>
                <template v-else>{{ column.subheading }}</template>
              </div>
            </th>
            <th v-if="checkable && checkboxPosition === 'right'" />
          </tr>
          <tr v-if="hasSearchablenewColumns">
            <th v-if="showDetailRowIcon" width="40px" />
            <th v-if="checkable && checkboxPosition === 'left'" />
            <th
              v-for="(column, index) in visibleColumns"
              :key="column.newKey + ':' + index + 'searchable'"
              :style="column.style"
              :class="{ 'is-sticky': column.sticky }"
            >
              <div class="th-wrap">
                <template v-if="column.searchable">
                  <template
                    v-if="
                      column.$slots &&
                        column.$slots.searchable &&
                        column.$slots.searchable()
                    "
                  >
                    <b-slot-component
                      :component="column"
                      :scoped="true"
                      name="searchable"
                      tag="span"
                      :props="{ column, filters }"
                    />
                  </template>
                  <b-input
                    v-else
                    @[filtersEvent].native="onFiltersEvent"
                    v-model="filters[column.props.field]"
                    :type="column.numeric ? 'number' : 'text'"
                  />
                </template>
              </div>
            </th>
            <th v-if="checkable && checkboxPosition === 'right'" />
          </tr>
        </thead>
        <tbody>
          <template v-for="(row, index) in visibleData">
            <tr
              :key="customRowKey ? row[customRowKey] : index"
              :class="[
                rowClass(row, index),
                {
                  'is-selected': isRowSelected(row, selected),
                  'is-checked': isRowChecked(row),
                },
              ]"
              @click="selectRow(row)"
              @dblclick="$emit('dblclick', row)"
              @mouseenter="mouseenter"
              @mouseleave="mouseleave"
              @contextmenu="$emit('contextmenu', row, $event)"
              :draggable="draggable"
              @dragstart="handleDragStart($event, row, index)"
              @dragend="handleDragEnd($event, row, index)"
              @drop="handleDrop($event, row, index)"
              @dragover="handleDragOver($event, row, index)"
              @dragleave="handleDragLeave($event, row, index)"
            >
              <td v-if="showDetailRowIcon" class="chevron-cell">
                <a
                  v-if="hasDetailedVisible(row)"
                  role="button"
                  @click.stop="toggleDetails(row)"
                >
                  <b-icon
                    icon="chevron-right"
                    :pack="iconPack"
                    both
                    :class="{ 'is-expanded': isVisibleDetailRow(row) }"
                  />
                </a>
              </td>

              <td
                class="checkbox-cell"
                v-if="checkable && checkboxPosition === 'left'"
              >
                <b-checkbox
                  :disabled="!isRowCheckable(row)"
                  :value="isRowChecked(row)"
                  @click.native.prevent.stop="checkRow(row, index, $event)"
                />
              </td>

              <template v-for="(column, colindex) in visibleColumns">
                <template v-if="column.children && column.children.default">
                  <b-slot-component
                    :key="column.newKey + ':' + index + ':' + colindex"
                    :component="column"
                    scoped
                    name="default"
                    tag="td"
                    :class="column.rootClasses"
                    :data-label="column.props.label"
                    :props="{ row, column, index }"
                  />
                </template>
              </template>

              <td
                class="checkbox-cell"
                v-if="checkable && checkboxPosition === 'right'"
              >
                <b-checkbox
                  :disabled="!isRowCheckable(row)"
                  :value="isRowChecked(row)"
                  @click.native.prevent.stop="checkRow(row, index, $event)"
                />
              </td>
            </tr>

            <tr
              v-if="isActiveDetailRow(row)"
              :key="(customRowKey ? row[customRowKey] : index) + 'detail'"
              class="detail"
            >
              <td :colspan="columnCount">
                <div class="detail-container">
                  <slot name="detail" :row="row" :index="index" />
                </div>
              </td>
            </tr>
            <slot
              v-if="isActiveCustomDetailRow(row)"
              name="detail"
              :row="row"
              :index="index"
            />
          </template>

          <tr v-if="!visibleData.length" class="is-empty">
            <td :colspan="columnCount">
              <slot name="empty" />
            </td>
          </tr>
        </tbody>

        <tfoot v-if="$slots.footer">
          <tr class="table-footer">
            <slot name="footer" v-if="hasCustomFooterSlot()" />
            <th :colspan="columnCount" v-else>
              <slot name="footer" />
            </th>
          </tr>
        </tfoot>
      </table>

      <template v-if="loading">
        <slot name="loading">
          <b-loading :is-full-page="false" :active.sync="loading" />
        </slot>
      </template>
    </div>

    <template
      v-if="
        (checkable && hasBottomLeftSlot()) ||
          (paginated &&
            (paginationPosition === 'bottom' || paginationPosition === 'both'))
      "
    >
      <slot name="pagination">
        <b-table-pagination
          v-bind="$attrs"
          :per-page="perPage"
          :paginated="paginated"
          :icon-pack="iconPack"
          :total="newDataTotal"
          v-model:current-page="newCurrentPage"
          @page-change="(event) => $emit('page-change', event)"
        >
          <slot name="bottom-left" />
        </b-table-pagination>
      </slot>
    </template>
  </div>
</template>

<script>
import {
  getValueByPath,
  indexOf,
  multiColumnSort,
  escapeRegExpChars,
  toCssWidth,
} from "../../utils/helpers";
import debounce from "../../utils/debounce";
import { VueInstance } from "../../utils/config";
import Checkbox from "../checkbox/Checkbox";
import Icon from "../icon/Icon";
import Input from "../input/Input";
import Loading from "../loading/Loading";
import Pagination from "../pagination/Pagination";
import SlotComponent from "../../utils/SlotComponent";
import TableMobileSort from "./TableMobileSort";
import TableColumn from "./TableColumn";
import TablePagination from "./TablePagination";

export default {
  name: "BTable",
  components: {
    [Checkbox.name]: Checkbox,
    [Icon.name]: Icon,
    [Input.name]: Input,
    [Pagination.name]: Pagination,
    [Loading.name]: Loading,
    [SlotComponent.name]: SlotComponent,
    [TableMobileSort.name]: TableMobileSort,
    [TableColumn.name]: TableColumn,
    [TablePagination.name]: TablePagination,
  },
  emits: ["contextmenu", "click", "select", "update:selected"],
  inheritAttrs: false,
  provide() {
    return {
      $table: this,
    };
  },
  props: {
    data: {
      type: Array,
      default: () => [],
    },
    columns: {
      type: Array,
      default: () => [],
    },
    bordered: Boolean,
    striped: Boolean,
    narrowed: Boolean,
    hoverable: Boolean,
    loading: Boolean,
    detailed: Boolean,
    checkable: Boolean,
    headerCheckable: {
      type: Boolean,
      default: true,
    },
    checkboxPosition: {
      type: String,
      default: "left",
      validator: (value) => {
        return ["left", "right"].indexOf(value) >= 0;
      },
    },
    selected: Object,
    isRowSelectable: {
      type: Function,
      default: () => true,
    },
    focusable: Boolean,
    customIsChecked: Function,
    isRowCheckable: {
      type: Function,
      default: () => true,
    },
    checkedRows: {
      type: Array,
      default: () => [],
    },
    mobileCards: {
      type: Boolean,
      default: true,
    },
    defaultSort: [String, Array],
    defaultSortDirection: {
      type: String,
      default: "asc",
    },
    sortIcon: {
      type: String,
      default: "arrow-up",
    },
    sortIconSize: {
      type: String,
      default: "is-small",
    },
    sortMultiple: {
      type: Boolean,
      default: false,
    },
    sortMultipleData: {
      type: Array,
      default: () => [],
    },
    sortMultipleKey: {
      type: String,
      default: null,
    },
    paginated: Boolean,
    currentPage: {
      type: Number,
      default: 1,
    },
    perPage: {
      type: [Number, String],
      default: 20,
    },
    showDetailIcon: {
      type: Boolean,
      default: true,
    },
    paginationPosition: {
      type: String,
      default: "bottom",
      validator: (value) => {
        return ["bottom", "top", "both"].indexOf(value) >= 0;
      },
    },
    backendSorting: Boolean,
    backendFiltering: Boolean,
    rowClass: {
      type: Function,
      default: () => "",
    },
    openedDetailed: {
      type: Array,
      default: () => [],
    },
    hasDetailedVisible: {
      type: Function,
      default: () => true,
    },
    detailKey: {
      type: String,
      default: "",
    },
    customDetailRow: {
      type: Boolean,
      default: false,
    },
    backendPagination: Boolean,
    total: {
      type: [Number, String],
      default: 0,
    },
    iconPack: String,
    mobileSortPlaceholder: String,
    customRowKey: String,
    draggable: {
      type: Boolean,
      default: false,
    },
    scrollable: Boolean,
    ariaNextLabel: String,
    ariaPreviousLabel: String,
    ariaPageLabel: String,
    ariaCurrentLabel: String,
    stickyHeader: Boolean,
    height: [Number, String],
    filtersEvent: {
      type: String,
      default: "",
    },
    cardLayout: Boolean,
    showHeader: {
      type: Boolean,
      default: true,
    },
    debounceSearch: Number,
  },
  data() {
    return {
      sortMultipleDataLocal: [],
      getValueByPath,
      visibleDetailRows: this.openedDetailed,
      newData: this.data,
      newDataTotal: this.backendPagination ? this.total : this.data.length,
      newCheckedRows: [...this.checkedRows],
      lastCheckedRowIndex: null,
      newCurrentPage: this.currentPage,
      currentSortColumn: {},
      isAsc: true,
      filters: {},
      defaultSlots: [],
      firstTimeSort: true, // Used by first time initSort
    };
  },
  computed: {
    sortMultipleDataComputed() {
      return this.backendSorting
        ? this.sortMultipleData
        : this.sortMultipleDataLocal;
    },
    tableClasses() {
      return {
        "is-bordered": this.bordered,
        "is-striped": this.striped,
        "is-narrow": this.narrowed,
        "is-hoverable":
          (this.hoverable || this.focusable) && this.visibleData.length,
      };
    },
    tableWrapperClasses() {
      return {
        "has-mobile-cards": this.mobileCards,
        "has-sticky-header": this.stickyHeader,
        "is-card-list": this.cardLayout,
        "table-container": this.isScrollable,
      };
    },
    tableStyle() {
      return {
        height: toCssWidth(this.height),
      };
    },

    /**
     * Splitted data based on the pagination.
     */
    visibleData() {
      if (!this.paginated) return this.newData;

      const currentPage = this.newCurrentPage;
      const perPage = this.perPage;

      if (this.newData.length <= perPage) {
        return this.newData;
      } else {
        const start = (currentPage - 1) * perPage;
        const end = parseInt(start, 10) + parseInt(perPage, 10);
        return this.newData.slice(start, end);
      }
    },

    visibleColumns() {
      if (!this.newColumns) {
        return this.newColumns;
      }
      return this.newColumns.filter((column) => {
        return column.visible || column.visible === undefined;
      });
    },

    /**
     * Check if all rows in the page are checked.
     */
    isAllChecked() {
      const validVisibleData = this.visibleData.filter((row) =>
        this.isRowCheckable(row)
      );
      if (validVisibleData.length === 0) return false;
      const isAllChecked = validVisibleData.some((currentVisibleRow) => {
        return (
          indexOf(
            this.newCheckedRows,
            currentVisibleRow,
            this.customIsChecked
          ) < 0
        );
      });
      return !isAllChecked;
    },

    /**
     * Check if all rows in the page are checkable.
     */
    isAllUncheckable() {
      const validVisibleData = this.visibleData.filter((row) =>
        this.isRowCheckable(row)
      );
      return validVisibleData.length === 0;
    },

    /**
     * Check if has any sortable column.
     */
    hasSortablenewColumns() {
      return this.newColumns.some((column) => {
        return column.sortable;
      });
    },

    /**
     * Check if has any searchable column.
     */
    hasSearchablenewColumns() {
      return this.newColumns.some((column) => {
        return column.searchable;
      });
    },

    /**
     * Check if has any column using subheading.
     */
    hasCustomSubheadings() {
      if (this.$slots && this.$slots.subheading) return true;
      return this.newColumns.some((column) => {
        return (
          column.subheading ||
          (column.$slots &&
            column.$slots.subheading &&
            column.$slots.subheading())
        );
      });
    },

    /**
     * Return total column count based if it's checkable or expanded
     */
    columnCount() {
      let count = this.newColumns.length;
      count += this.checkable ? 1 : 0;
      count += this.detailed && this.showDetailIcon ? 1 : 0;

      return count;
    },

    /**
     * return if detailed row tabled
     * will be with chevron column & icon or not
     */
    showDetailRowIcon() {
      return this.detailed && this.showDetailIcon;
    },

    /**
     * return if scrollable table
     */
    isScrollable() {
      if (this.scrollable) return true;
      if (!this.newColumns) return false;
      return this.newColumns.some((column) => {
        return column.sticky;
      });
    },

    newColumns() {
      if (this.columns && this.columns.length) {
        return this.columns.map((column) => {
          const TableColumnComponent = VueInstance.extend(TableColumn);
          const component = new TableColumnComponent({
            parent: this,
            propsData: column,
          });
          component.$slots = {
            default: (props) => {
              const vnode = component.$createElement("span", {
                domProps: {
                  innerHTML: getValueByPath(props.row, column.props.field),
                },
              });
              return [vnode];
            },
          };
          return component;
        });
      }
      const defSlots = this.defaultSlots.filter(
        (vnode) =>
          vnode.type &&
          vnode.type.data &&
          vnode.type.data() &&
          vnode.type.data().$isTableColumn
      );
      return defSlots;
    },
  },
  watch: {
    /**
     * When data prop change:
     *   1. Update internal value.
     *   2. Filter data if it's not backend-filtered.
     *   3. Sort again if it's not backend-sorted.
     *   4. Set new total if it's not backend-paginated.
     */
    data(value) {
      this.newData = value;
      if (!this.backendFiltering) {
        this.newData = value.filter((row) => this.isRowFiltered(row));
      }
      if (!this.backendSorting) {
        this.sort(this.currentSortColumn, true);
      }
      if (!this.backendPagination) {
        this.newDataTotal = this.newData.length;
      }
    },

    /**
     * When Pagination total change, update internal total
     * only if it's backend-paginated.
     */
    total(newTotal) {
      if (!this.backendPagination) return;

      this.newDataTotal = newTotal;
    },

    currentPage(newVal) {
      this.newCurrentPage = newVal;
    },

    newCurrentPage(newVal) {
      this.$emit("update:current-page", newVal);
    },
    /**
     * When checkedRows prop change, update internal value without
     * mutating original data.
     */
    checkedRows(rows) {
      this.newCheckedRows = [...rows];
    },

    /*
        newColumns(value) {
            this.checkSort()
        },
        */

    debounceSearch: {
      handler(value) {
        this.debouncedHandleFiltersChange = debounce(
          this.handleFiltersChange,
          value
        );
      },
      immediate: true,
    },

    filters: {
      handler(value) {
        if (this.debounceSearch) {
          this.debouncedHandleFiltersChange(value);
        } else {
          this.handleFiltersChange(value);
        }
      },
      deep: true,
    },

    /**
     * When the user wants to control the detailed rows via props.
     * Or wants to open the details of certain row with the router for example.
     */
    openedDetailed(expandedRows) {
      this.visibleDetailRows = expandedRows;
    },
  },
  methods: {
    mouseenter() {
      if (this.$attrs.listeners && this.$attrs.listeners.mouseenter) {
        this.$emit("mouseenter", row);
      }
    },
    mouseleave() {
      if (this.$attrs.listeners && this.$attrs.listeners.mouseleave) {
        this.$emit("mouseleave", row);
      }
    },
    onFiltersEvent(event) {
      this.$emit(`filters-event-${this.filtersEvent}`, {
        event,
        filters: this.filters,
      });
    },
    handleFiltersChange(value) {
      if (this.backendFiltering) {
        this.$emit("filters-change", value);
      } else {
        this.newData = this.data.filter((row) => this.isRowFiltered(row));
        if (!this.backendPagination) {
          this.newDataTotal = this.newData.length;
        }
        if (!this.backendSorting) {
          if (
            this.sortMultiple &&
            this.sortMultipleDataLocal &&
            this.sortMultipleDataLocal.length > 0
          ) {
            this.doSortMultiColumn();
          } else if (Object.keys(this.currentSortColumn).length > 0) {
            this.doSortSingleColumn(this.currentSortColumn);
          }
        }
      }
    },
    findIndexOfSortData(column) {
      let sortObj = this.sortMultipleDataComputed.filter(
        (i) => i.field === column.props.field
      )[0];
      return this.sortMultipleDataComputed.indexOf(sortObj) + 1;
    },
    removeSortingPriority(column) {
      if (this.backendSorting) {
        this.$emit("sorting-priority-removed", column.props.field);
      } else {
        this.sortMultipleDataLocal = this.sortMultipleDataLocal.filter(
          (priority) => priority.field !== column.props.field
        );

        let formattedSortingPriority = this.sortMultipleDataLocal.map((i) => {
          return (i.order && i.order === "desc" ? "-" : "") + i.field;
        });
        this.newData = multiColumnSort(this.newData, formattedSortingPriority);
      }
    },
    resetMultiSorting() {
      this.sortMultipleDataLocal = [];
      this.currentSortColumn = {};
      this.newData = this.data;
    },
    /**
     * Sort an array by key without mutating original data.
     * Call the user sort function if it was passed.
     */
    sortBy(array, key, fn, isAsc) {
      let sorted = [];
      // Sorting without mutating original data
      if (fn && typeof fn === "function") {
        sorted = [...array].sort((a, b) => fn(a, b, isAsc));
      } else {
        sorted = [...array].sort((a, b) => {
          // Get nested values from objects
          let newA = getValueByPath(a, key);
          let newB = getValueByPath(b, key);

          // sort boolean type
          if (typeof newA === "boolean" && typeof newB === "boolean") {
            return isAsc ? newA - newB : newB - newA;
          }

          if (!newA && newA !== 0) return 1;
          if (!newB && newB !== 0) return -1;
          if (newA === newB) return 0;

          newA = typeof newA === "string" ? newA.toUpperCase() : newA;
          newB = typeof newB === "string" ? newB.toUpperCase() : newB;

          return isAsc ? (newA > newB ? 1 : -1) : newA > newB ? -1 : 1;
        });
      }

      return sorted;
    },

    sortMultiColumn(column) {
      this.currentSortColumn = {};
      if (!this.backendSorting) {
        let existingPriority = this.sortMultipleDataLocal.filter(
          (i) => i.field === column.props.field
        )[0];
        if (existingPriority) {
          existingPriority.order =
            existingPriority.order === "desc" ? "asc" : "desc";
        } else {
          this.sortMultipleDataLocal.push({
            field: column.props.field,
            order: column.isAsc,
          });
        }
        this.doSortMultiColumn();
      }
    },

    doSortMultiColumn() {
      let formattedSortingPriority = this.sortMultipleDataLocal.map((i) => {
        return (i.order && i.order === "desc" ? "-" : "") + i.field;
      });
      this.newData = multiColumnSort(this.newData, formattedSortingPriority);
    },

    /**
     * Sort the column.
     * Toggle current direction on column if it's sortable
     * and not just updating the prop.
     */
    sort(column, updatingData = false, event = null) {
      if (
        // if backend sorting is enabled, just emit the sort press like usual
        // if the correct key combination isnt pressed, sort like usual
        !this.backendSorting &&
        this.sortMultiple &&
        ((this.sortMultipleKey && event[this.sortMultipleKey]) ||
          !this.sortMultipleKey)
      ) {
        if (updatingData) {
          this.doSortMultiColumn();
        } else {
          this.sortMultiColumn(column);
        }
      } else {
        if (!column || !column.sortable) return;

        // sort multiple is enabled but the correct key combination isnt pressed so reset
        if (this.sortMultiple) {
          this.sortMultipleDataLocal = [];
        }

        if (!updatingData) {
          this.isAsc =
            column === this.currentSortColumn
              ? !this.isAsc
              : this.defaultSortDirection.toLowerCase() !== "desc";
        }
        if (!this.firstTimeSort) {
          this.$emit(
            "sort",
            column.props.field,
            this.isAsc ? "asc" : "desc",
            event
          );
        }
        if (!this.backendSorting) {
          this.doSortSingleColumn(column);
        }
        this.currentSortColumn = column;
      }
    },

    doSortSingleColumn(column) {
      this.newData = this.sortBy(
        this.newData,
        column.props.field,
        column.customSort,
        this.isAsc
      );
    },

    isRowSelected(row, selected) {
      if (!selected) {
        return false;
      }
      if (this.customRowKey) {
        return row[this.customRowKey] === selected[this.customRowKey];
      }
      return row === selected;
    },

    /**
     * Check if the row is checked (is added to the array).
     */
    isRowChecked(row) {
      return indexOf(this.newCheckedRows, row, this.customIsChecked) >= 0;
    },

    /**
     * Remove a checked row from the array.
     */
    removeCheckedRow(row) {
      const index = indexOf(this.newCheckedRows, row, this.customIsChecked);
      if (index >= 0) {
        this.newCheckedRows.splice(index, 1);
      }
    },

    /**
     * Header checkbox click listener.
     * Add or remove all rows in current page.
     */
    checkAll() {
      const isAllChecked = this.isAllChecked;
      this.visibleData.forEach((currentRow) => {
        if (this.isRowCheckable(currentRow)) {
          this.removeCheckedRow(currentRow);
        }
        if (!isAllChecked) {
          if (this.isRowCheckable(currentRow)) {
            this.newCheckedRows.push(currentRow);
          }
        }
      });

      this.$emit("check", this.newCheckedRows);
      this.$emit("check-all", this.newCheckedRows);

      // Emit checked rows to update user variable
      this.$emit("update:checked-rows", this.newCheckedRows);
    },

    /**
     * Row checkbox click listener.
     */
    checkRow(row, index, event) {
      if (!this.isRowCheckable(row)) return;
      const lastIndex = this.lastCheckedRowIndex;
      this.lastCheckedRowIndex = index;

      if (event.shiftKey && lastIndex !== null && index !== lastIndex) {
        this.shiftCheckRow(row, index, lastIndex);
      } else if (!this.isRowChecked(row)) {
        this.newCheckedRows.push(row);
      } else {
        this.removeCheckedRow(row);
      }

      this.$emit("check", this.newCheckedRows, row);

      // Emit checked rows to update user variable
      this.$emit("update:checkedRows", this.newCheckedRows);
    },

    /**
     * Check row when shift is pressed.
     */
    shiftCheckRow(row, index, lastCheckedRowIndex) {
      // Get the subset of the list between the two indicies
      const subset = this.visibleData.slice(
        Math.min(index, lastCheckedRowIndex),
        Math.max(index, lastCheckedRowIndex) + 1
      );

      // Determine the operation based on the state of the clicked checkbox
      const shouldCheck = !this.isRowChecked(row);

      subset.forEach((item) => {
        this.removeCheckedRow(item);
        if (shouldCheck && this.isRowCheckable(item)) {
          this.newCheckedRows.push(item);
        }
      });
    },

    /**
     * Row click listener.
     * Emit all necessary events.
     */
    selectRow(row, index) {
      this.$emit("click", row);

      if (this.selected === row) return;
      if (!this.isRowSelectable(row)) return;

      // Emit new and old row
      this.$emit("select", row, this.selected);

      // Emit new row to update user variable
      this.$emit("update:selected", row);
    },

    /**
     * Toggle to show/hide details slot
     */
    toggleDetails(obj) {
      const found = this.isVisibleDetailRow(obj);

      if (found) {
        this.closeDetailRow(obj);
        this.$emit("details-close", obj);
      } else {
        this.openDetailRow(obj);
        this.$emit("details-open", obj);
      }

      // Syncs the detailed rows with the parent component
      this.$emit("update:openedDetailed", this.visibleDetailRows);
    },

    openDetailRow(obj) {
      const index = this.handleDetailKey(obj);
      this.visibleDetailRows.push(index);
    },

    closeDetailRow(obj) {
      const index = this.handleDetailKey(obj);
      const i = this.visibleDetailRows.indexOf(index);
      this.visibleDetailRows.splice(i, 1);
    },

    isVisibleDetailRow(obj) {
      const index = this.handleDetailKey(obj);
      const result = this.visibleDetailRows.indexOf(index) >= 0;
      return result;
    },

    isActiveDetailRow(row) {
      return (
        this.detailed && !this.customDetailRow && this.isVisibleDetailRow(row)
      );
    },

    isActiveCustomDetailRow(row) {
      return (
        this.detailed && this.customDetailRow && this.isVisibleDetailRow(row)
      );
    },

    isRowFiltered(row) {
      for (const key in this.filters) {
        // remove key if empty
        if (!this.filters[key]) {
          delete this.filters[key];
          return true;
        }
        let value = this.getValueByPath(row, key);
        if (value == null) return false;
        if (Number.isInteger(value)) {
          if (value !== Number(this.filters[key])) return false;
        } else {
          const re = new RegExp(escapeRegExpChars(this.filters[key]), "i");
          if (!re.test(value)) return false;
        }
      }
      return true;
    },

    /**
     * When the detailKey is defined we use the object[detailKey] as index.
     * If not, use the object reference by default.
     */
    handleDetailKey(index) {
      const key = this.detailKey;
      return !key.length || !index ? index : index[key];
    },

    checkPredefinedDetailedRows() {
      const defaultExpandedRowsDefined = this.openedDetailed.length > 0;
      if (defaultExpandedRowsDefined && !this.detailKey.length) {
        throw new Error(
          'If you set a predefined opened-detailed, you must provide a unique key using the prop "detail-key"'
        );
      }
    },

    /**
     * Call initSort only first time (For example async data).
     */
    checkSort() {
      if (this.newColumns.length && this.firstTimeSort) {
        this.initSort();
        this.firstTimeSort = false;
      } else if (this.newColumns.length) {
        if (Object.keys(this.currentSortColumn).length > 0) {
          for (let i = 0; i < this.newColumns.length; i++) {
            if (
              this.newColumns[i].props.field ===
              this.currentSortColumn.props.field
            ) {
              this.currentSortColumn = this.newColumns[i];
              break;
            }
          }
        }
      }
    },

    /**
     * Check if footer slot has custom content.
     */
    hasCustomFooterSlot() {
      if (this.$slots.footer) {
        if (this.$slots.footer().length > 1) {
          return true;
        }
        const tag = this.$slots.footer()[0].tag;
        if (tag !== "th" && tag !== "td") {
          return false;
        }
      }

      return true;
    },

    /**
     * Check if bottom-left slot exists.
     */
    hasBottomLeftSlot() {
      return typeof this.$slots["bottom-left"] !== "undefined";
    },

    /**
     * Table arrow keys listener, change selection.
     */
    pressedArrow(pos) {
      if (!this.visibleData.length) return;

      let index = this.visibleData.indexOf(this.selected) + pos;

      // Prevent from going up from first and down from last
      index =
        index < 0
          ? 0
          : index > this.visibleData.length - 1
          ? this.visibleData.length - 1
          : index;

      const row = this.visibleData[index];

      if (!this.isRowSelectable(row)) {
        let newIndex = null;
        if (pos > 0) {
          for (
            let i = index;
            i < this.visibleData.length && newIndex === null;
            i++
          ) {
            if (this.isRowSelectable(this.visibleData[i])) newIndex = i;
          }
        } else {
          for (let i = index; i >= 0 && newIndex === null; i--) {
            if (this.isRowSelectable(this.visibleData[i])) newIndex = i;
          }
        }
        if (newIndex >= 0) {
          this.selectRow(this.visibleData[newIndex]);
        }
      } else {
        this.selectRow(row);
      }
    },

    /**
     * Focus table element if has selected prop.
     */
    focus() {
      if (!this.focusable) return;

      this.$el.querySelector("table").focus();
    },

    /**
     * Initial sorted column based on the default-sort prop.
     */
    initSort() {
      if (this.sortMultiple && this.sortMultipleData) {
        this.sortMultipleData.forEach((column) => {
          this.sortMultiColumn(column);
        });
      } else {
        if (!this.defaultSort) return;

        let sortField = "";
        let sortDirection = this.defaultSortDirection;

        if (Array.isArray(this.defaultSort)) {
          sortField = this.defaultSort[0];
          if (this.defaultSort[1]) {
            sortDirection = this.defaultSort[1];
          }
        } else {
          sortField = this.defaultSort;
        }

        const sortColumn = this.newColumns.filter(
          (column) => column.props.field === sortField
        )[0];
        if (sortColumn) {
          this.isAsc = sortDirection.toLowerCase() !== "desc";
          this.sort(sortColumn, true);
        }
      }
    },
    /**
     * Emits drag start event
     */
    handleDragStart(event, row, index) {
      this.$emit("dragstart", { event, row, index });
    },
    /**
     * Emits drag leave event
     */
    handleDragEnd(event, row, index) {
      this.$emit("dragend", { event, row, index });
    },
    /**
     * Emits drop event
     */
    handleDrop(event, row, index) {
      this.$emit("drop", { event, row, index });
    },
    /**
     * Emits drag over event
     */
    handleDragOver(event, row, index) {
      this.$emit("dragover", { event, row, index });
    },
    /**
     * Emits drag leave event
     */
    handleDragLeave(event, row, index) {
      this.$emit("dragleave", { event, row, index });
    },

    refreshSlots() {
      if (this.$slots.default) {
        this.defaultSlots = this.$slots.default() || [];
      } else {
        this.defaultSlots = [];
      }
    },
  },
  mounted() {
    this.refreshSlots();
    this.checkPredefinedDetailedRows();
    this.checkSort();
  },
};
</script>

Changes made:

  • sync properties are now using v-model:prop-name
  • this.$slots.default is a function now. If you want the values you need to do this.$slots.default(). Same goes for this.$slots.footer()
  • this.$emit("update:checkedRows")
  • newColumn failed to find columns. this is my change
const defSlots = this.defaultSlots.filter(
        (vnode) =>
          vnode.type &&
          vnode.type.data &&
          vnode.type.data() &&
          vnode.type.data().$isTableColumn
      );
      return defSlots;
  • I believe I made some changes here, but I cannot pinpoint what at the moment.
<template v-if="column.children && column.children.default">
                  <b-slot-component
                    :key="column.newKey + ':' + index + ':' + colindex"
                    :component="column"
                    scoped
                    name="default"
                    tag="td"
                    :class="column.rootClasses"
                    :data-label="column.props.label"
                    :props="{ row, column, index }"
                  />
                </template>

I believe I had to change the render function in SlotComponent.js. Since on is not supported anymore, I used mitt as a dependency. I believe I can get away by making an inner reactive object using vue as a reactivity engine, but I aimed for the "make it work" approach.

SlotComponent.js
import mitt from "mitt";
import { h } from "vue";
import { isVueComponent } from "./helpers";

export default {
  name: "BSlotComponent",
  props: {
    component: {
      type: Object,
      required: true,
    },
    name: {
      type: String,
      default: "default",
    },
    scoped: {
      type: Boolean,
    },
    props: {
      type: Object,
    },
    tag: {
      type: String,
      default: "div",
    },
    event: {
      type: String,
      default: "hook:updated",
    },
  },
  methods: {
    refresh() {
      this.$forceUpdate();
    },
  },
  created() {
    if (isVueComponent(this.component)) {
      if (!this.component.emmiter) {
        this.component.emmiter = mitt();
      }
      this.component.emmiter.on(this.event, this.refresh);
    }
  },
  beforeDestroy() {
    if (isVueComponent(this.component) && this.component.emmiter) {
      this.component.emmiter.off(this.event, this.refresh);
    }
  },
  render() {
    if (isVueComponent(this.component)) {
      let childComponent;
      if (this.scoped) {
        childComponent = h(this.component.children[this.name], this.props);
      } else {
        childComponent = h(this.component.children[this.name]);
      }
      return h(this.tag, {}, childComponent);
    }
  },
};

I'm not proud of my isVueComponent implementation:

export function isVueComponent(c) {
  return c && c.type && c.type.name;
}

Finally, pagination has a weird behaviour I had to fix. All buttons appeared always disabled because vue3 does the following

<component :is="'a'" :disabled="false" />

It seems that buefy expected that vue would not render the disabled prop but instead vue3 did the following

<a disabled="false"/>

Which shows every pagination button as disabled. In order to fix that I changed PaginationButton.vue with this (check the new attributes computed prop)

PaginationButton.vue
<template>
  <component
    :is="tag"
    role="button"
    :href="href"
    class="pagination-link"
    :class="{ 'is-current': page.isCurrent, [page.class]: true }"
    v-bind="attributes"
    @click.prevent="page.click"
    :aria-label="page['aria-label']"
    :aria-current="page.isCurrent"
  >
    <slot>{{ page.number }}</slot>
  </component>
</template>

<script>
import config from "../../utils/config";

export default {
  name: "BPaginationButton",
  props: {
    page: {
      type: Object,
      required: true,
    },
    tag: {
      type: String,
      default: "a",
      validator: (value) => {
        return config.defaultLinkTags.indexOf(value) >= 0;
      },
    },
    disabled: {
      type: Boolean,
      default: false,
    },
  },
  computed: {
    href() {
      if (this.tag === "a") {
        return "#";
      }
    },
    isDisabled() {
      return this.disabled || this.page.disabled;
    },
    attributes() {
      const attrs = Object.assign({}, this.$attrs);
      if (this.isDisabled) {
        attrs.disabled = "disabled";
      }
      return attrs;
    },
  },
};
</script>

Here is a screenshot:

Screen Shot 2020-09-19 at 11 45 32 AM

I'm sorry for my late response, I failed to check my email for comments.

Let me know if I can help you with anything else

@ulf1
Copy link

ulf1 commented Sep 22, 2020

Have you made some sort of decision how and when buefy supports vue 3.0.0?
I assumed that all vue2 code is backward compatible.

@rumblefrog
Copy link

@ulf1 Vue 3 is a breaking change, and since maintaining two codebases is out of the scope, it's due for an upgrade.

@TCLNA
Copy link

TCLNA commented Sep 27, 2020

off-topic, here is a way to collapse code : gist link

@SomethingNew71
Copy link
Contributor

👀 👀 👀 👀 👀

@SomethingNew71
Copy link
Contributor

SomethingNew71 commented Nov 4, 2021

Screen Shot 2021-11-04 at 1 57 35 PM

I am seeing a lot of deprecation warnings for the slash division issue in newer versions of sass

EDIT: I see it was addressed in oruga-ui/theme-bulma#32

@kerrpeter
Copy link

Would there happen to be a Starter Template to get up and running with Vue3, Oruga, and the Bulma/Buefy theme. This would be really useful

@akhi-ninja
Copy link

@kerrpeter i created one starter project with oruga theme + vue3 + Bulma please check https://github.com/akhi-ninja/oruga-vue3-starter

@beeryukov
Copy link

Do I understand correctly that Buefy will never support Vue 3? And you are making another project (oruga) instead?

@jtommy
Copy link
Member

jtommy commented Dec 3, 2021

@beeryukov there is already Oruga and Bulma theme 😉

@akhi-ninja
Copy link

@beeryukov Oruga Bulma theme is working properly in Vue3 ,you can check https://github.com/akhi-ninja/oruga-vue3-starter i have added more bulma sass variable based on oruga Theme and added JEST test case too. working on cypress test case and code coverage i will publish the new version by next week

@a-kriya
Copy link
Contributor

a-kriya commented Dec 17, 2021

@beeryukov there is already Oruga and Bulma theme wink

@jtommy Sure, but that's helpful for new projects. For existing large codebases that rely on Buefy, migrating to Oruga would either involve replacing b- prefix with o- for all markup, or even if Oruga components are registered with a b- prefix, there is no expectation of compatibility between the two and every single element will have to be visually and behaviorally tested.

The question remains: does providing Vue 3 support exist on Buefy's roadmap?

@jtommy
Copy link
Member

jtommy commented Dec 17, 2021

@a-kriya The answer is "No".
I think that migration from Vue 2 and Vue 3 is a great effort that needs a complete re-testing so changing UI library (taglib and other little changes) is only a "minimal" part; as I already told previously, If you use Vue 2 stay on Buefy otherwise you can think to switch to Oruga

@a-kriya
Copy link
Contributor

a-kriya commented Dec 20, 2021

@jtommy Thanks for your response.

migration from Vue 2 to 3 is a great effort that needs a complete re-testing so changing UI library is only a "minimal" part

For web apps that would be true, but for a static site, changing the UI library is the "only" part.

@torressam333
Copy link

So does Buefy still not support Vue 3?

@rcoundon
Copy link
Contributor

So does Buefy still not support Vue 3?

I feel rather sorry for the authors here. It's been stated many times in this thread that Buefy does not and will not support Vue 3. If you want a similar API with Bulma styling and Vue 3 support, there is Oruga with the Bulma plugin which is in active development.

@a-kriya
Copy link
Contributor

a-kriya commented Dec 21, 2021

It's been stated many times in this thread that Buefy does not and will not support Vue 3.

@rcoundon It is clear that it does not support Vue3 as this issue is open, but I can't find anywhere in this thread stated clearly that it will not support it (until #2505 (comment)).

If there's nothing more to discuss here, then @jtommy can probably just lock the thread with his final thoughts and leave the issue open and pinned to avoid duplicates.

@rcoundon
Copy link
Contributor

It's been stated many times in this thread that Buefy does not and will not support Vue 3.

@rcoundon It is clear that it does not support Vue3 as this issue is open, but I can't find anywhere in this thread stated clearly that it will not support it (until #2505 (comment)).

If there's nothing more to discuss here, then @jtommy can probably just lock the thread with his final thoughts and leave the issue open and pinned to avoid duplicates.

Ok, that's fair. It's also been discussed in the Buefy Discord too, I accept that not everyone is aware of that.

@ireznik
Copy link
Contributor

ireznik commented Dec 28, 2021

Since I am new to the vue world, it should at least have a notice in the docs / quick start that Vue3 is not supported.
This sentence in the quick start section is imho pretty misleading:

You need Vue.js version 2.6+.
https://github.com/buefy/buefy#quick-start

@jtommy jtommy closed this as completed Dec 28, 2021
@stevenfokoua
Copy link

Please let us know in the website that Vue 3 is not/will not be supported. We all appreciate your efforts on developing this, but after a good while searching thinking i was doing something wrong i figured out.

Like the guy above mentioned there is no mention anywhere that this doesnt support Vue 3. Thanks

@ireznik
Copy link
Contributor

ireznik commented Dec 31, 2021

@rcoundon I made a pull request to update the quick start about the Vue 3 support
#3626

@yooouuri
Copy link

It is not jet published to npm, but im gonna make a "successor" to Buefy

https://github.com/yooouuri/bulma-vue

Will have the same API as Buefy, made for Vue 3.

@buefy buefy locked as resolved and limited conversation to collaborators Apr 25, 2022
@wesdevpro wesdevpro unpinned this issue Aug 21, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests