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
Comments
Yes but it'll support only the official version |
New timeline and recommendations for adopting Vue 3 |
@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? |
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. : ) |
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
An other one is the 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 |
I think you are right about v-model. @Tofandel But I don’t understand your point about 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. |
@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 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 |
@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 |
@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). |
@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 |
@jtommy - very clear! Thanks! |
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 |
@jtommy just wanted to ask, if there is any rough timeline on when buefy will be ready for Vue 3? :) |
I'm sorry but there is a timeline or ETA about it |
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
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. |
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. |
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 :) |
@cjblomqvist Will be quite easy to migrate my current code for Oruga to Buefy but probably I'll need beta testers |
@jtommy I'll alpha, beta and omega test it |
@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) |
@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…</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:
const defSlots = this.defaultSlots.filter(
(vnode) =>
vnode.type &&
vnode.type.data &&
vnode.type.data() &&
vnode.type.data().$isTableColumn
);
return defSlots;
<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.jsimport 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 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<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: 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 |
Have you made some sort of decision how and when buefy supports vue 3.0.0? |
@ulf1 Vue 3 is a breaking change, and since maintaining two codebases is out of the scope, it's due for an upgrade. |
off-topic, here is a way to collapse code : gist link |
👀 👀 👀 👀 👀 |
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 |
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 |
@kerrpeter i created one starter project with oruga theme + vue3 + Bulma please check https://github.com/akhi-ninja/oruga-vue3-starter |
Do I understand correctly that Buefy will never support Vue 3? And you are making another project (oruga) instead? |
@beeryukov there is already Oruga and Bulma theme 😉 |
@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 |
@jtommy Sure, but that's helpful for new projects. For existing large codebases that rely on Buefy, migrating to Oruga would either involve replacing The question remains: does providing Vue 3 support exist on Buefy's roadmap? |
@a-kriya The answer is "No". |
@jtommy Thanks for your response.
For web apps that would be true, but for a static site, changing the UI library is the "only" part. |
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. |
@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. |
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.
|
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 |
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. |
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.
The text was updated successfully, but these errors were encountered: