ckEditor Error : editor-isreadonly-has-no-setter - javascript

I'm using ckeditor5 balloon block mode in nuxt project.
I have used online builder and downloaded build files , add the build files to my project and importing them into my editor component and using it!
the only problem that I have is that when the page loads ,
I get an error : editor-isreadonly-has-no-setter.
I tried binding v-model to the editor but the value won't be updated!
note : I have used ckeditor5 classic mode identical to the way that I'm using Balloon Block, donno really what's going on!
this is my component :
<template>
<ckeditor
:id="id"
v-bind="$attrs"
:editor="BalloonBlock"
:config="editorConfig"
v-on="$listeners"
/>
</template>
<script>
let BalloonBlock
let CKEditor
if (process.client) {
BalloonBlock = require('#/plugins/ckeditor/ckeditor')
CKEditor = require('#ckeditor/ckeditor5-vue2')
} else {
CKEditor = { component: { template: '<div></div>' } }
}
export default {
name: 'CKEditor',
components: {
ckeditor: CKEditor.component,
},
props: {
fillErr: {
type: Boolean,
default: false,
required: false,
},
minHeight: {
type: String,
default: '350px',
required: false,
},
label: {
type: String,
default: '',
required: false,
},
},
data() {
return {
classicEditor: BalloonBlock,
editorConfig: {
language: 'fa',
contentsLangDirection: 'rtl',
},
editorElement: null,
id: null,
}
},
computed: {
value() {
return this.$attrs.value
},
},
created() {
this.id = this.uuidv4()
},
mounted() {
if (!document.getElementById('editorFaTranslate')) {
const faScript = document.createElement('script')
faScript.setAttribute('charset', 'utf-8')
faScript.setAttribute('type', 'text/js')
faScript.setAttribute('id', 'editorFaTranslate')
faScript.setAttribute(
'src',
require('##/plugins/ckeditor/translations/fa.js')
)
document.head.appendChild(faScript)
}
const intervalId = setInterval(() => {
const ckEditor = document.getElementById(this.id)
if (ckEditor) {
clearInterval(intervalId)
this.editorElement = ckEditor
}
})
},
methods: {
uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(
/[xy]/g,
function (c) {
const r = (Math.random() * 16) | 0
const v = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
}
)
},
insertTextAtTheEnd(text) {
function findCorrectPosition(htmlStr) {
const lastIndexOfHTMLTag = htmlStr.lastIndexOf('</')
const lastUlTag = htmlStr.lastIndexOf('</ul>')
const lastOlTag = htmlStr.lastIndexOf('</ol>')
if (
lastUlTag === lastIndexOfHTMLTag ||
lastOlTag === lastIndexOfHTMLTag
) {
const lastLiTag = htmlStr.lastIndexOf('</li>')
return lastLiTag
}
return lastIndexOfHTMLTag
}
const currentString = this.value
const correctIndex = findCorrectPosition(currentString)
const firstHalf = currentString.substring(0, correctIndex)
const secondHalf = currentString.substring(correctIndex)
const newString = `${firstHalf}${text}${secondHalf}`
this.$emit('input', newString)
},
},
}
</script>
I would welcome any idea!

I added "#ckeditor/ckeditor5-vue2": "github:ckeditor/ckeditor5-vue2", in my dependencies and all of a sudden my problem was gone!

Related

Quasar QSelect is not opening when performing AJAX call

I have been trying to create a simple auto complete using Quasar's select but I'm not sure if this is a bug or if I'm doing something wrong.
Problem
Whenever I click the QSelect component, it doesn't show the dropdown where I can pick the options from.
video of the problem
As soon as I click on the QSelect component, I make a request to fetch a list of 50 tags, then I populate the tags to my QSelect but the dropdown doesn't show.
Code
import type { PropType } from "vue";
import { defineComponent, h, ref } from "vue";
import type { TagCodec } from "#/services/api/resources/tags/codec";
import { list } from "#/services/api/resources/tags/actions";
import { QSelect } from "quasar";
export const TagAutoComplete = defineComponent({
name: "TagAutoComplete",
props: {
modelValue: { type: Array as PropType<TagCodec[]> },
},
emits: ["update:modelValue"],
setup(props, context) {
const loading = ref(false);
const tags = ref<TagCodec[]>([]);
// eslint-disable-next-line #typescript-eslint/ban-types
const onFilterTest = (val: string, doneFn: (update: Function) => void) => {
const parameters = val === "" ? {} : { title: val };
doneFn(async () => {
loading.value = true;
const response = await list(parameters);
if (val) {
const needle = val.toLowerCase();
tags.value = response.data.data.filter(
(tag) => tag.title.toLowerCase().indexOf(needle) > -1
);
} else {
tags.value = response.data.data;
}
loading.value = false;
});
};
const onInput = (values: TagCodec[]) => {
context.emit("update:modelValue", values);
};
return function render() {
return h(QSelect, {
modelValue: props.modelValue,
multiple: true,
options: tags.value,
dense: true,
optionLabel: "title",
optionValue: "id",
outlined: true,
useInput: true,
useChips: true,
placeholder: "Start typing to search",
onFilter: onFilterTest,
"onUpdate:modelValue": onInput,
loading: loading.value,
});
};
},
});
What I have tried
I have tried to use the several props that is available for the component but nothing seemed to work.
My understanding is that whenever we want to create an AJAX request using QSelect we should use the onFilter event emitted by QSelect and handle the case from there.
Questions
Is this the way to create a Quasar AJAX Autocomplete? (I have tried to search online but all the answers are in Quasar's forums that are currently returning BAD GATEWAY)
What am I doing wrong that it is not displaying the dropdown as soon as I click on the QSelect?
It seems updateFn may not allow being async. Shift the async action a level up to solve the issue.
const onFilterTest = async (val, update /* abort */) => {
const parameters = val === '' ? {} : { title: val };
loading.value = true;
const response = await list(parameters);
let list = response.data.data;
if (val) {
const needle = val.toLowerCase();
list = response.data.data.filter((x) => x.title.toLowerCase()
.includes(needle));
}
update(() => {
tags.value = list;
loading.value = false;
});
};
I tested it by the following code and mocked values.
// import type { PropType } from 'vue';
import { defineComponent, h, ref } from 'vue';
// import type { TagCodec } from "#/services/api/resources/tags/codec";
// import { list } from "#/services/api/resources/tags/actions";
import { QSelect } from 'quasar';
export const TagAutoComplete = defineComponent({
name: 'TagAutoComplete',
props: {
modelValue: { type: [] },
},
emits: ['update:modelValue'],
setup(props, context) {
const loading = ref(false);
const tags = ref([]);
const onFilterTest = async (val, update /* abort */) => {
// const parameters = val === '' ? {} : { title: val };
loading.value = true;
const response = await new Promise((resolve) => {
setTimeout(() => {
resolve({
data: {
data: [
{
id: 1,
title: 'Vue',
},
{
id: 2,
title: 'Vuex',
},
{
id: 3,
title: 'Nuxt',
},
{
id: 4,
title: 'SSR',
},
],
},
});
}, 3000);
});
let list = response.data.data;
if (val) {
const needle = val.toLowerCase();
list = response.data.data.filter((x) => x.title.toLowerCase()
.includes(needle));
}
update(() => {
tags.value = list;
loading.value = false;
});
};
const onInput = (values) => {
context.emit('update:modelValue', values);
};
return function render() {
return h(QSelect, {
modelValue: props.modelValue,
multiple: true,
options: tags.value,
dense: true,
optionLabel: 'title',
optionValue: 'id',
outlined: true,
useInput: true,
useChips: true,
placeholder: 'Start typing to search',
onFilter: onFilterTest,
'onUpdate:modelValue': onInput,
loading: loading.value,
});
};
},
});

React-Quill auto focus on editor other typing other input elements?

i am using react-quill as my editor and recently i configured my image handler function to pass props to the handler and after making the change my editor behaves weirdly and when ever i type something on the other input fields my editor comes into focus and automatically words are typed in it
below is the code for my editor
please any help or suggestion will be greatly appreciated.
Component
} from 'react';
// import {
// Editor
// } from 'react-draft-wysiwyg';
import draftToHtml from 'draftjs-to-html';
import htmlToDraft from 'html-to-draftjs';
import axios from 'axios'
import {
API_URL
} from './../../api_url'
// import * as Icons from 'images/icons';
import * as loadImage from 'blueimp-load-image';
import {
key
} from '../../assets/encryptionkey'
import globalStyles from '../../stylesheets/ui.css'
import blogStyles from './blogs.css'
import bootstrapStyles from '../../stylesheets/bootstrap/css/bootstrap.min.css'
import fontAwesomeStyles from '../../stylesheets/font-awesome/css/font-awesome.min.css'
import actionIconsStyles from '../../stylesheets/action_icons.css'
import cx from 'classnames'
import './editor.css';
import s from './editor.css';
//import CKEditor from '#ckeditor/ckeditor5-react';
//import ClassicEditor from '#ckeditor/ckeditor5-build-classic';
//import ReactQuill, { Quill } from "react-quill";
//var Image = Quill.import('formats/image');
//Image.className = 'custom-class-to-image';
//Quill.register(Image, true);
export default class BlogEditor extends Component {
constructor( loader ) {
super();
this.state = {
editorHtml: '', theme: 'snow',
text:''
}
this.handleChange = this.handleChange.bind(this)
// var that=this;
if (typeof window !== 'undefined') {
this.ReactQuill = require('react-quill')
const ReactQuill=this.ReactQuill;
var Image = ReactQuill.Quill.import('formats/image');
Image.className = 'blog-content-image';
ReactQuill.Quill.register(Image, true);
// ReactQuill.Quill.setContents(editor.clipboard.convert(html));
}
}
componentWillReceiveProps(){
//debugger
let clearContent=this.props.clearContent
if(clearContent){
// this.editorRef.setEditorContents(this.editorRef.getEditor(), '<h1>test</h1>');
}
}
handleChange(value) {
//debugger
this.setState({ text: value })
// this.props.changeInEditor(value)
}
imageHandler({ val, componentProps }) {
// debugger
let self=this
let image;
let image_extension;
const Cryptr = require('cryptr');
const cryptr = new Cryptr(key);
const users = localStorage.getItem('users') ? JSON.parse(cryptr.decrypt(localStorage.getItem('users'))) : {}
// console.log(users[users.lastLoginId])
let loggedinUser = users[users.lastLoginId];
const input = document.createElement('input');
input.setAttribute('type', 'file');
input.setAttribute('accept', 'image/*');
input.setAttribute("class", "Editor-mage");
input.click();
input.onchange = async () => {
//debugger
const file = input.files[0];
var ValidImageTypes = ["image/gif", "image/jpeg", "image/png", "image/jpg", "image/GIF", "image/JPEG", "image/PNG", "image/JPG"];
let file_type = file.type
let filename = file.name
let extension = filename.split('.').pop();
if(ValidImageTypes.indexOf(file_type) >= 0){
if(file.size<=500000&&file.size>=50000){
var fileToLoad = file
loadImage(fileToLoad, (canvas) => {
if(canvas){
// this.setState({
image=canvas.toDataURL()
image_extension=extension
//});
const res = new Promise(function(resolve, reject) {
axios({
method:'post',
url:API_URL+'api/v1/postblogimage',
headers:{
'x-access-handler':loggedinUser.token
},
data:{
image: image,
image_extension:image_extension,
userid:loggedinUser.userid
}
})
//axios.post(API_URL + 'api/v1/postblogimage', formData, config)
.then((response) => {
if (response.data.error == 'false' || response.data.error == false) {
if (response.data.status == 200 && response.data.message == "Image uploaded successfully") {
//debugger
const range = self.quill.getSelection(true);
// Insert temporary loading placeholder image
// this.quill.insertEmbed(range.index, 'image', `${window.location.origin}/images/loaders/placeholder.gif`);
// Move cursor to right side of image (easier to continue typing)
self.quill.setSelection(range.index + 1);
// Remove placeholder image
self.quill.deleteText(range.index, 1);
// Insert uploaded image
let url=response.data.data[0].imageURL;
self.quill.insertEmbed(range.index, 'image', url);
self.quill.pasteHTML(range.index, <img src={url} class="blog-image-content" alt="Responsive image"/>);
}
}else if(response.data.error == 'true' || response.data.status == '500')
componentProps.error('Sorry, Inappropriate image')
// }
}).catch((error) => {
// reject(Error("It broke"));
});
});
}
}, {orientation: true});
}
else{
componentProps.error(" Sorry, File size should be of size between 50 kb to 500kb")
}
}
else{
// this.setState({
// image_warning:'Invalid image type',
// image:'',
// image_extension:''
//})
// this.fileInput.value=''
}
};
}
render() {
const ReactQuill = this.ReactQuill
if (typeof window !== 'undefined' && ReactQuill) {
return (
<div className="editor-container">
<ReactQuill
ref={(el) => this.quillRef = el
}
onChange={this.handleChange}
placeholder={"share your thoughts"}
modules={{
toolbar: {
container: [
[{ header: '1' }, { header: [2,3, 4, 5, 6] }, { font: [] }],
[{ size: [ 'small', false, 'large', 'huge' ] }],
['bold', 'italic', 'underline', 'strike', 'blockquote'],
[{ list: 'ordered' }, { list: 'bullet' }],
['link', 'image', 'video'],
['clean'],
['code-block'],
[{ 'color': [] }, { 'background': [] }], // dropdown with defaults from theme
[{ 'align': [] }],
],
handlers: {
image: (val) => this.imageHandler({ val, componentProps: this.props })
// image: () => this.imageHandler
}
}
}}
/>
</div>
)
}
else {
return <textarea />;
}
}
}```
Each re-render modules object creating, useMemo fixed.
const modules = useMemo(() => ({
imageResize : {
parchment: Quill.import('parchment'),
modules: ['Resize', 'DisplaySize', 'Toolbar'],
},
toolbar: {
container: [
[{ header: [1, 2, 3, 4, false] }],
["bold", "italic", "underline", "strike", "blockquote"],
[
{ list: "ordered" },
{ list: "bullet" },
{ indent: "-1" },
{ indent: "+1" },
],
[{align: [ ]}],
["link", "image"],
["clean"],
],
handlers: {
image: () => {
imageHandler()
}
}
},
}), []);

how to use two marker props on single marker vue leaflet

i need use rotationAngle(vue2-leaflet-rotatedmarker) and duration(vue2-leaflet-movingmarker)
but i want use two props in single marker.
for example :
<l-marker :lat-lng="run.currentPoint.latLong"
:icon="run.currentPoint.icon" :rotationAngle="250" :duration="2000">
</l-marker>
thank you.
Vue2Leaflet Plugins are just wrappers around the original leaflet plugin. Therefore it's quite easy to combine the two wrappers
https://github.com/LouisMazel/vue2-leaflet-movingmarker/blob/master/lib/index.vue and
https://github.com/mudin/vue2-leaflet-rotatedmarker/blob/master/Vue2LeafletRotatedMarker.vue
of the two plugins into one. Call it e.g. LRotatedMovingMarker.vue, import it to your component and you can use it the way you wanted to.
<template>
<div style="display: none;">
<slot v-if="ready" />
</div>
</template>
<script>
import { marker, DomEvent, Icon } from 'leaflet'
import { findRealParent, propsBinder } from 'vue2-leaflet'
import 'leaflet.marker.slideto'
import 'leaflet-rotatedmarker'
const props = {
draggable: {
type: Boolean,
custom: true,
default: false
},
visible: {
type: Boolean,
custom: true,
default: true
},
latLng: {
type: [Object, Array],
custom: true
},
icon: {
custom: false,
default: () => new Icon.Default()
},
zIndexOffset: {
type: Number,
custom: false
},
rotationAngle:{
type: Number,
default: () => 0,
},
rotationOrigin: {
type: String,
default: 'center center'
},
options: {
type: Object,
default: () => ({})
},
duration: {
type: Number,
required: true
},
keepAtCenter: {
type: Boolean,
default: false
}
}
export default {
name: 'LRotatedMovingMarker',
props,
data() {
return {
ready: false
}
},
mounted() {
const options = this.options
if (this.icon) {
options.icon = this.icon
}
options.draggable = this.draggable
options.rotationAngle = this.rotationAngle?this.rotationAngle:0
options.rotationOrigin = this.rotationOrigin
this.mapObject = marker(this.latLng, options)
this.mapObject.on('move', ev => {
if (Array.isArray(this.latLng)) {
this.latLng[0] = ev.latlng.lat
this.latLng[1] = ev.latlng.lng
} else {
this.latLng.lat = ev.latlng.lat
this.latLng.lng = ev.latlng.lng
}
})
DomEvent.on(this.mapObject, this.$listeners)
propsBinder(this, this.mapObject, props)
this.ready = true
this.parentContainer = findRealParent(this.$parent)
this.parentContainer.addLayer(this, !this.visible)
},
beforeDestroy() {
this.parentContainer.removeLayer(this)
},
watch: {
rotationAngle: {
handler: function () {
this.options.rotationAngle = this.rotationAngle
}
}
},
methods: {
setDraggable(newVal) {
if (this.mapObject.dragging) {
newVal
? this.mapObject.dragging.enable()
: this.mapObject.dragging.disable()
}
},
setVisible(newVal, oldVal) {
if (newVal === oldVal) return
if (this.mapObject) {
if (newVal) {
this.parentContainer.addLayer(this)
} else {
this.parentContainer.removeLayer(this)
}
}
},
setLatLng(newVal) {
if (newVal == null) return
if (this.mapObject) {
const oldLatLng = this.mapObject.getLatLng()
const newLatLng = {
lat: newVal[0] || newVal.lat,
lng: newVal[1] || newVal.lng
}
if (
newLatLng.lat !== oldLatLng.lat ||
newLatLng.lng !== oldLatLng.lng
) {
this.mapObject.slideTo(newLatLng, {
duration: this.duration,
keepAtCenter: this.keepAtCenter
})
}
}
}
}
}
</script>

Custom Babel plugin – Change a stringLiteral value to actual Javascript code

I'm writing a plugin which will be kind of my own lightweight babel-plugin-react-css-modules implementation, anyways…
the plugin needs to change the value of the className attribute in JSX elements from a string to an array. Essentially it should map the classNames string to an array "class-one class-two" => [object["class-one"] || "class-one", object["class-two"] || "class-two"];
The part I struggle with is just making babel output JS instead of a string, which I'm not sure how to do.
module.exports = () => {
const styles = "styles" + Math.random().toString(36).substr(2, 9);
let foundStyleImport;
return {
visitor: {
ImportDeclaration: (path) => {
if (
!/(sass|scss)\.js$/.test(path.node.source.value) &&
path.node.specifiers.length > 0
) return;
foundStyleImport = true;
path.node.specifiers[0] = {
type: "ImportDefaultSpecifier",
local: {
type: "Identifier",
name: styles,
loc: { identifierName: styles }
}
}
},
// The part I'm struggling with ⬇
JSXAttribute: ({node: {value, name}}) => {
if (!foundStyleImport || name.name !== "className") return;
// changing this from "stringLiteral" to "arrayExpression"
// results in an error…
value.type = "arrayExpression";
value.value = `[${value.value.split(" ").map(cls => {
return `${styles}["${cls}"] || "${cls}"`;
}).join(", ")}]`;
}
}
};
}
Babel should take this:
import "./header.sass.js";
import { h, Fragment } from "/web_modules/preact.js";
export default function () {
return h("header", {
className: "foobar"
}, h("p", null, "Hello"));
}
And output something like this this:
import styles0u33w7qps from "./header.sass.js";
import { h, Fragment } from "/web_modules/preact.js";
export default function () {
return h("header", {
className: [styles0u33w7qps["foobar"] || "foobar"]
}, h("p", null, "Hello"));
}

Vue js: Vuetify server side Datatable search filter not working

I'm using vuetify for my datatable. Pagination and sort are working except the search filter. The response data from search filter is correct but the problem is its not rendering the response to my template. In vuetify docs theres only have for pagination and sort. I'm trying to implement the search function via server-side.
My User.vue
export default{
data () {
return {
max25chars: (v) => v.length <= 25 || 'Input too long!',
tmp: '',
search: '',
totalItems: 0,
pagination: {
rowsPerPage: 1,
search: ''
},
headers: [
{
text: 'Name',
sortable: true,
value: 'name',
align: 'left'
},
{
text: 'Email Add',
sortable: true,
value:'email',
align: 'left'
},
{
text: 'Roles',
sortable: true,
value:'roles_permissions',
align: 'left'
},
{
text: 'Date joined',
sortable: true,
value:'created_at',
align: 'left'
}
],
items: [],
loading: false,
timer: null
}
},
watch:{
pagination:{
handler(){
this.getDataFromApi()
.then(data => {
const self = this;
self.items = data.items;
self.totalItems = data.total;
})
},
deep: true
}
},
mounted(){
this.getDataFromApi()
.then(data => {
this.items = data.items;
this.totalItems = data.total;
});
},
methods:{
getDataFromApi(search_val){
this.loading = true;
return new Promise((resolve, reject) => {
const { sortBy, descending, page, rowsPerPage } = this.pagination
const search = this.search;
//console.log(search);
clearTimeout(this.timer);
this.timer = setTimeout(function(){
axios({
url: '/prod/api/user_table',
method:'post',
data:{
sortBy : sortBy,
descending: descending,
page : page,
rowsPerPage : rowsPerPage,
search_val : search
}
})
.then(response=>{
if(response.status == 200){
let items = response.data.data;
const total = response.data.totalRecords;
this.loading = false;
resolve({
items,
total
});
}
})
.catch(error=>{
if(error.response){
console.log(error.response);
}
})
},1000);
})
},
fetchDataFromApi(value){
//console.log(value);
}
},
created(){
}
}
Here is my back end side using laravel
public function dataTable(Request $request){
//return Datatable::eloquent(User::query())->make(true);
$sortBy = $request->sortBy;
$descending = $request->descending;
$page = $request->page;
$rowsPerPage = $request->rowsPerPage;
$search_val = $request->search_val;
//echo $rowsPerPage;
if($descending){
$orderedBy = 'desc';
}else{
$orderedBy = 'asc';
}
$start = ($page - 1) * $rowsPerPage;
/*$totalRec = User::all();
if(empty(trim($search_val))){
$user = User::orderBy($sortBy,$orderedBy)->skip($start)->take($rowsPerPage)->get();
}else{
$user = User::where([
]);
}*/
$query = User::query();
$column = ['name', 'email'];
foreach ($column as $col) {
$query->orWhere($col, 'LIKE','%'.$search_val.'%');
}
$query->orderBy($sortBy,$orderedBy)->skip($start)->take($rowsPerPage);
$arr_items = [];
foreach ($query->get()->toArray() as $shit => $v) {
$arr_items['data'][] = array(
'value' => $v['id'],
'name' => $v['name'],
'email' => $v['email'],
'roles_permissions' => '',
'created_at' => $v['created_at']
);
}
$arr_items['totalRecords'] = User::count();
return response()->json($arr_items);
}
server side search & sort of datatable in vuetify.js
If we need server side search and sort in vuetify.js datatable, we have to make some changes in vuejs part.
import {environment} from '../../environment';
export default {
name: "Category",
data() {
return {
categories: [],
search: '',
total: 0,
loading: false,
pagination: {},
headers: [
{text: 'ID', value: 'id'},
{text: 'Name', value: 'name'},
{text: 'Actions', value: 'name', sortable: false, align: 'center'}
],
rowsPerPageItems: [5, 10, 20, 50, 100],
}
},
watch: {
pagination {
this.getCategoriesByPagination();
},
search() {
this.getCategoriesByPagination();
}
},
methods: {
getCategoriesByPagination() {
this.loading = true;
// get by search keyword
if (this.search) {
axios.get(`${environment.apiUrl}/category-filter?query=${this.search}&page=${this.pagination.page}&per_page=${this.pagination.rowsPerPage}`)
.then(res => {
this.categories = res.data.data;
this.total = res.data.meta.total;
})
.catch(err => console.log(err.response.data))
.finally(() => this.loading = false);
}
// get by sort option
if (this.pagination.sortBy && !this.search) {
const direction = this.pagination.descending ? 'desc' : 'asc';
axios.get(`${environment.apiUrl}/category-order?direction=${direction}&sortBy=${this.pagination.sortBy}&page=${this.pagination.page}&per_page=${this.pagination.rowsPerPage}`)
.then(res => {
this.loading = false;
this.categories = res.data.data;
this.total = res.data.meta.total;
});
} if(!this.search && !this.pagination.sortBy) {
axios.get(`${environment.apiUrl}/category?page=${this.pagination.page}&per_page=${this.pagination.rowsPerPage}`)
.then(res => {
this.categories = res.data.data;
this.total = res.data.meta.total;
})
.catch(err => console.log(err.response.data))
.finally(() => this.loading = false);
}
}
}
}
in html part
<v-text-field v-model="search"
append-icon="search"
label="Search"
single-line
hide-details
></v-text-field>
<v-data-table :headers="headers"
:items="categories"
:pagination.sync="pagination"
:total-items="total"
:rows-per-page-items="rowsPerPageItems"
:loading="loading"
></v-data-table>
in Laravel part, i used laravel scout package.
Controller
/**
* Get category
* #return \Illuminate\Http\Resources\Json\AnonymousResourceCollection
*/
public function getAll()
{
$per_page = empty(request('per_page')) ? 10 : (int)request('per_page');
$categories = Category::latest()->paginate($per_page);
return CategoryResource::collection($categories);
}
/**
* Get category by search results
* #return \Illuminate\Http\Resources\Json\AnonymousResourceCollection
*/
public function getBySearch()
{
$per_page = empty(request('per_page')) ? 10 : (int)request('per_page');
$categories = Category::search(request()->query('query'))->paginate($per_page);
return CategoryResource::collection($categories);
}
/**
* Get category by sorting
* #return \Illuminate\Http\Resources\Json\AnonymousResourceCollection
*/
public function getByOrder()
{
$per_page = empty(request('per_page')) ? 10 : (int)request('per_page');
$direction = request()->query('direction');
$sortBy = request()->query('sortBy');
$categories = Category::orderBy($sortBy, $direction)->paginate($per_page);
return CategoryResource::collection($categories);
}
Route
Route::get('category', 'Api\CategoryController#getAll');
Route::get('category-filter', 'Api\CategoryController#getBySearch');
Route::get('category-order', 'Api\CategoryController#getByOrder');
Model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
class Category extends Model
{
use Searchable;
/**
* Get the indexable data array for the model.
*
* #return array
*/
public function toSearchableArray()
{
return [
'name' => $this->name
];
}
}
To enable server-side search to work don't pass the search prop to v-data-table. Otherwise the datatable pagination and search are client side even if you pass the "totalItems" prop.
You can pass the search prop but the initial value needs to be null. I tried it first with an empty string and it didn't work, at least in my case.
<template>
<div class="data-table">
<v-data-table :headers="headers" :items="desserts" :items-per-page="5" :options.sync="options" :server-items-length="totalDesserts" :loading="loading" class="elevation-1" ></v-data-table>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import axios from 'axios'
import { api } from '~/config'
import Form from '~/mixins/form'
export default {
data: () => ({
desserts_s: [],
totalDesserts: 0,
loading: true,
options: {},
headers: [
{ text: 'id', value: 'id' },
{ text: 'lastname', value: 'lastname' },
{ text: 'email', value: 'email' },
],
desserts: [],
}),
watch: {
options: {
handler () {
this.getDataFromApi()
.then(data => {
this.desserts = data.items
this.totalDesserts = data.total
})
},
deep: true,
},
},
mounted () {
this.getDataFromApi()
.then(data => {
this.desserts = data.items
this.totalDesserts = data.total
})
},
methods: {
getDataFromApi () {
this.loading = true
return new Promise((resolve, reject) => {
const { sortBy, sortDesc, page, itemsPerPage } = this.options
axios.get(api.path('test')+"?"+Object.keys(this.options).map(key => key + '=' + this.options[key]).join('&'))
.then((response) => {
let items = response.data.users.data
const total = response.data.users.total
console.log(response.data.users.data)
if (sortBy.length === 1 && sortDesc.length === 1) {
items = items.sort((a, b) => {
const sortA = a[sortBy[0]]
const sortB = b[sortBy[0]]
if (sortDesc[0]) {
if (sortA < sortB) return 1
if (sortA > sortB) return -1
return 0
} else {
if (sortA < sortB) return -1
if (sortA > sortB) return 1
return 0
}
})
}
this.loading = false
resolve({
items,
total,
})
})
.catch((error) => console.log(error.message))
})
},
getDesserts () {
},
},
}
</script>
You should use computed
i'm using server pagination and search. you can inspect my code
<template>
<v-card flat>
<v-data-table
:headers="tableHead"
:items="computedFormData.items"
v-if="computedFormData && computedFormData.items"
:mobile-breakpoint="820"
v-model="selected"
:show-select="true"
:loading="loading"
:form-data="formData"
#update:page="getItemPerPage"
#update:items-per-page="getItemPerPage2"
:server-items-length="paginationTotal"
:schema="schema"
:search="search"
>
<template v-slot:top>
<v-toolbar flat color="white">
<v-toolbar-title class="mr-4" v-if="addHeading">{{ addHeading }}</v-toolbar-title>
</v-toolbar>
</template>
</v-data-table>
</v-card>
</template>
<script>
import {mapMutations, mapGetters, mapActions} from 'vuex'
export default {
name: 'DataTable',
components: { Button, Tab: () => import('#/components/Tabs'), Dialog: () => import('#/components/Dialog'), TableFormBuilder: () => import('#/components/Form/TableFormBuilder'), FormBuilder: () => import('#/components/Form/FormBuilder') },
props: [
'schema',
'formData',
'name',
'itemsTab',
'value',
'headers',
'title',
'nodata',
'addHeading',
'confirmDeleteTabItem',
'tableTitleOptionA',
'tableTitleOptionB',
'items'
],
data: () => ({
loading: false,
selected: [],
companyValid: true,
customerValid: true,
search: '',
dialog: false,
editedIndex: -1,
editedItem: {},
defaultItem: {}
}),
computed: {
...mapGetters('Connection', ['getConnectionPending', 'getAddFirm', 'getUpdateFirm', 'getDeleteFirm', 'getMultipleDeleteFirm', 'getCompanies']),
...mapGetters('Pagination', ['getPage']),
tableHead(){
return this.headers.filter(s => s.show);
},
computedFormData: {
get: function () {
return this.$parent.formData
},
set: function () {
return this.formData
}
},
paginationTotal: {
get: function () {
return this.$parent.formData.totalLength
}
},
tabItems: {
get: function () {
if(this.search!==''){
return this.$parent.formData.items.filter(s => s.firmaAdi === this.search)
}else{
return this.$parent.formData.items
}
},
set: function () {
return this.items
}
},
formTitle () {
return this.editedIndex === -1
? this.tableTitleOptionA
: this.tableTitleOptionB
}
},
methods: {
...mapActions("Snackbar", ["setSnackbar"]),
...mapActions("Connection", ["addFirmCall", "updateFirmCall", "deleteFirmCall", "multipleDeleteCall", "companiesCall"]),
...mapMutations('Selected', ['setSelected']),
...mapMutations('Pagination', ['setPage']),
getItemPerPage (pagination) {
this.loading=true;
this.setPage(pagination)
},
getItemPerPage2 (pagination) {
this.loading=true;
this.setPage(pagination)
},
},
watch: {
getConnectionPending(e){
this.loading=e
},
dialog(val) {
val || this.close();
},
search(e){
this.companiesCall({ page: this.getPage, limit: 10, search: e});
},
selected(e){
this.setSelected(e)
}
},
}
</script>
To late for answer, but I was looking for something similar to work with yajra/laravel-datatables this days and didn't found any examples / libs, so created something that worked:
Install composer require yajra/laravel-datatables-oracle:"~9.0" (And follow the instructions on how to add Provider, Facade, config
We will need to change our controller to Support DataTables:
use DataTables;
------
public function dataTable(Request $request){
//one line of code for simple search /sort / pagination
return DataTables::of(User::query())->make(true);
}
Next we will adjust our Vuetify component
Template
<template>
<v-data-table
:headers="headers"
:items="users"
:pagination.sync="pagination"
:total-items="totalUsers"
:rows-per-page-items="rowsPerPageItems"
:loading="loading"
>
<template v-slot:items="props">
<tr>
<td>
<div class="d-flex">
<v-btn :to="{ name: 'users.edit', params: { id: props.item.id }}">Edit</v-btn>
</div>
</td>
<td>{{ props.item.id }}</td>
<td>{{ props.item.name }}</td>
<td>{{ props.item.email }}</td>
</tr>
</template>
<template v-slot:no-results>
<v-alert :value="true" color="error" icon="warning">
Your search for "{{ searchQuery }}" found no results.
</v-alert>
</template>
</v-data-table>
</template>
JS
<script>
import axios from 'axios';
export default {
data () {
return {
draw: 1,
users: [],
searchQuery: "",
loading: true,
pagination: {
descending: true,
page: 1,
rowsPerPage: 10,
sortBy: "id",
totalItems: 0
},
totalUsers: 0,
rowsPerPageItems: [10, 15, 20, 30, 40, 50],
columns:{},
headers: [
{ text: 'Actions', value: 'actions', sortable: false, searchable: false, width: '210px'},
{ text: 'ID', value: 'id', name: 'id', sortable: true, searchable: true, width: '40px'},
{ text: 'Name', value: 'name', name: 'name', sortable: true, searchable: true, width: '250px'},
{ text: 'Email', value: 'email', sortable: true, searchable: true, width: '80px'},
],
cancelSource: null
}
},
watch: {
//watcher to watch for order/pagination and search criteria.
//
params: {
handler() {
//on params change refetch Data
//We don't do it in mounted method, becuase on first load params will change.
this.getDataFromApi().then(data => {
this.users = data.items;
this.totalUsers = data.total;
});
},
deep: true
}
},
mounted() {
//Based on our Headers we create query data for DataTables
//I've added a new param "searchable" to let DataBales know that this column is not searchable
//You can also set name as "table.column eg users.name" if you select from more then table to avoid "Ambitious column name error from SQL"
for (var i = 0; i < this.headers.length; i++) {
this.columns[i] = {
data: this.headers[i].value,
name: (typeof(this.headers[i].name) != 'undefined' ? this.headers[i].name : this.headers[i].value),
searchable: this.headers[i].searchable,
orderable: this.headers[i].sortable,
search: {
value: '',
regex: false
}
};
}
},
//computed params to return pagination and search criteria
computed: {
params(nv) {
return {
...this.pagination,
query: this.searchQuery
};
}
},
methods: {
cancelRequest() {
//Axios cancelSource to stop current search if new value is entered
if (this.cancelSource) {
this.cancelSource.cancel('Start new search, stop active search');
}
},
getDataFromApi() {
//show loading of Vuetify Table
this.loading = true;
return new Promise((resolve, reject) => {
this.cancelRequest();
this.cancelSource = axios.CancelToken.source();
//copy current params to modify
let params = this.params;
params.length = params.rowsPerPage; //set how many records to fecth per page
params.start = params.page == 1 ? 0 : (params.rowsPerPage * (params.page - 1)); //set offset
params.search = {
value: params.query,
regex: false
}; //our search query
params.draw = this.draw;
//sorting and default to column 1 (ID)
if(params.sortBy){
params.order = {
0: {
column: _.findIndex(this.headers, {
'value': params.sortBy
}),
dir: (params.descending ? 'desc' : 'asc')
}
};
}else{
params.order = {
0: {
column: 1,
dir: 'desc'
}
};
}
params.columns = this.columns; //set our previously created columns
//fecth data
//I used here jQuery $.param() helper, becuase axios submits data as JSON Payload, and we need for data or Query params
//This can be changed
axios.get('/users?'+$.param(params), {
cancelToken: this.cancelSource.token
}).then((res) => {
this.draw++;
this.cancelSource = null;
let items = res.data.data;
let total = res.data.recordsFiltered;
resolve({
items,
total
});
}).catch((err) => {
if (axios.isCancel(err)) {
console.log('Request canceled', err.message);
} else {
reject(err);
}
}).always(() => {
this.loading = false;
});
});
}
}
}
</script>
Conclusion
A simple solution to make vuetify work with Laravel DataTables, for sure is not ideal, but works well. Hope this helped.

Categories