Gridstack.js + Vue 3 components - javascript

I'm trying to create a gridstack.js dashboard with Vue 3 and I want the grid stack items to contain reactive vue 3 components.
The problem is that these grid stack items can only be passed HTML. The documentation states you should be able to add Vue components as content but the examples are for Vue 2 and I'm struggling to implement this in Vue 3.
I have the following code:
<template>
<div class="p-6 h-full flex flex-col">
<header class="flex flex-row items-center justify-between mb-6">
<div>
<h1 class="text-3xl font-bold">
Workbench
</h1>
<p class="leading-6 text-gray-600 text-sm mt-2">
{{ info }}
</p>
</div>
<div class="flex flex-row items-center">
<button type="button" #click="addPanel()">Add Panel</button>
</div>
</header>
<div class="flex-1">
<section class="grid-stack"></section>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, defineComponent, createApp } from "vue"
import TestPanel from "./../components/panels/TestPanel.vue"
let grid = null;
const items = [
{ x: 0, y: 0, h: 4, w: 6 },
{ x: 7, y: 0, h: 4, w: 6 },
{ x: 0, y: 5, h: 4, w: 4 },
{ x: 4, y: 5, h: 4, w: 4 },
{ x: 8, y: 5, h: 4, w: 4 },
];
onMounted(() => {
grid = GridStack.init({
// float: true,
cellHeight: "70px",
minRow: 1,
});
grid.load(items)
});
function addPanel() {
const div = document.createElement("div")
div.id = Math.random().toString(24).substring(8)
const componentInstance = defineComponent({
extends: TestPanel, data() {
return {
test: "this is a test"
}
}
})
const app = createApp(componentInstance)
app.mount(div)
let widget = grid.addWidget({
x: 0,
y: 0,
w: 6,
h: 3,
content: div.outerHTML,
})
app.mount(div.id)
}
</script>
<style>
.grid-stack-item-content {
background-color: #18BC9C;
}
</style>
This will load the vue component in a stack grid item but the component is no longer reactive.
Any help would be greatly appreciated, thanks in advance!

This is probably not exactly what the gridstack-creators had in mind but here you go:
<template>
<button #click="addNewWidget()">Add Widget</button> {{ info }}
<section class="grid-stack">
<div
v-for="(component, key, index) in components"
:key="'component'+index"
:gs-id="key"
class="grid-stack-item"
:gs-x="component.gridPos.x"
:gs-y="component.gridPos.y"
:gs-h="component.gridPos.h"
:gs-w="component.gridPos.w"
gs-auto-position="true"
>
<div class="grid-stack-item-content">
<component :is="component.name" v-bind="component.props" />
</div>
</div>
</section>
</template>
<script>
import { ref, onMounted, reactive, nextTick } from 'vue';
import 'gridstack/dist/gridstack.min.css';
import { GridStack } from 'gridstack';
import YourRandomComponent1 from '../YourRandomComponent1.vue';
import YourRandomComponent2 from '../YourRandomComponent2.vue';
import YourRandomComponent3 from '../YourRandomComponent3.vue';
export default {
name: "WidgetGrid",
setup() {
let info = ref("");
let grid = null;
let components = reactive({
yourRandomComponent1: {
name: "YourRandomComponent1", props: {}, gridPos: { x: 0, y: 1, w: 4, h: 5 }
},
yourRandomComponent2: {
name: "YourRandomComponent2", props: {}, gridPos: { x: 0, y: 1, w: 2, h: 5 }
},
});
onMounted(() => {
grid = GridStack.init({
float: true,
cellHeight: "70px",
minRow: 1,
});
grid.on("dragstop", (event, element) => {
console.log("move event!", event, element);
const node = element.gridstackNode;
info.value = `you just dragged node #${node.id} to ${node.x},${node.y} – good job!`;
});
});
// this will of course only work once because of the object-key
function addNewWidget() {
components.yourRandomComponent3= {
name: "YourRandomComponent3", props: {}, gridPos: { x: 0, y: 1, w: 2, h: 5 }
};
// we have to wait for vue to update v-for,
// until then the querySelector wont find the element
nextTick(() => {
console.log(grid);
let compEl = document.querySelector('[gs-id="yourRandomComponent3"]');
console.log(compEl);
grid.makeWidget(compEl);
});
console.warn("i will only work once, fix my inputs to reuse me");
}
return {
info,
components,
};
},
components: {
// eslint-disable-next-line vue/no-unused-components
YourRandomComponent1,
// eslint-disable-next-line vue/no-unused-components
YourRandomComponent2,
},
}
</script>
<style>
.grid-stack {
background-color: #FAFAFF;
border-style: dashed;
}
.grid-stack-item {
color: #2c3e50;
text-align: center;
border-style: solid;
overflow: auto;
z-index: 50;
}
</style>
In my case, a missing div with the grid-stack-item-content-class wrapping the component made the widgets immobile.
I have also added an add-new-widget function that demonstrates how to add a new widget to the grid. The key is to use reactive() so that Vue will re-render the page. After re-rendering, the component needs to be registered as a grid element using grid.makeWidget. For this we need the component's Dom element, which we get after Vue has re-rendered with nextTick.

You can use own component in Vue3 like this
<div class="grid-stack" :style="{ 'background-color': hex }" >
<widget v-for="widget in widgets" :widget="widget" :key="widget" />
</div>
Import your component
import Widget from "src/components/GridStackComponent.vue";
Add component to export
export default {
name: 'GridStack',
components: {
Widget
},
data() {
...
},
...
}
And that's all. Result can look like this

Related

vue3 child component won't load with computed data as property

Here's the problem I'm having. I have a Leads page, that is my Leads.vue template. It loads my leads and then passes the leads data to other components via props.
The LeadSources component receives a computed method as it's property.
You can see that on the Leads.vue page, the LeadSources component calls the getSourceData() method for it's property data.
When I check the value of the props for LeadSources.vue in the setup() the value for chartData initially is an empty array. If the page hot-reloads then the LeadSources apexchart will populate with the :series data but otherwise I cannot get it to work.
Essentially it works like this.
Leads.vue passes getSourceData() to the LeadsSources.vue component which on the setup() sets it as the variable series and tries to load the apexchart with it.
It will not work if I refresh my page but if I save something in my IDE, the hot-reload will load the updated apexchart and the data appears. It seems like the prop values don't get set in the setup() function the first time around. How do I architecturally get around this? What's the proper way to set this up? I can't tell if the issue is on the leads.vue side of things or with the way that the LeadSources.vue component is being put together.
Any help would be appreciated, I've spent way too long trying to get this to work properly.
Leads.vue
<template>
<!--begin::Leads-->
<div class="row gy-5 g-xl-8 mb-8">
<div class="col-xxl-12">
<LeadTracker
:lead-data="leadData"
:key="componentKey"
/>
</div>
</div>
<div class="row gy-5 g-xl-8 mb-8">
<div class="col-xxl-12">
<LeadSources
chart-color="primary"
chart-height="500"
:chart-data="getSourceData"
:chart-threshold="sourceThreshold"
widget-classes="lead-sources"
></LeadSources>
</div>
</div>
<!--end::Leads-->
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent, onMounted } from "vue";
import { setCurrentPageTitle } from "#/core/helpers/breadcrumb";
import LeadSources from "#/components/leads/sources/LeadSources.vue";
import LeadTracker from "#/components/leads/tracker/LeadTracker.vue";
import LeadService from "#/core/services/LeadService";
import ApiService from "#/core/services/ApiService";
import {Lead} from "#/core/interfaces/lead";
export default defineComponent({
name: "leads",
components: {
LeadTracker,
LeadSources
},
data() {
return {
leadData: [] as Lead[],
}
},
beforeCreate: async function() {
this.leadData = await new LeadService().getLeads()
},
setup() {
onMounted(() => {
setCurrentPageTitle("Lead Activity");
});
const sourceThreshold = 5;
return {
sourceThreshold,
componentKey: 0
};
},
computed: {
getSourceData() {
interface SingleSource {
source: string;
value: number;
}
const sourceData: Array<SingleSource> = [];
// Make array for source names
const sourceTypes = [];
this.leadData.filter(lead => {
if (!sourceTypes.includes(lead.source)) sourceTypes.push(lead.source);
});
// Create objects for each form by name, push to leadSourceData
sourceTypes.filter(type => {
let totalSourceLeads = 1;
this.leadData.filter(form => {
if (form.source == type) totalSourceLeads++;
});
const leadSourceData = {
source: type,
value: totalSourceLeads
};
sourceData.push(leadSourceData);
});
// Sort by source popularity
sourceData.sort(function(a, b) {
return a.value - b.value;
});
return sourceData;
}
}
});
</script>
LeadSources.vue
<template>
<!--begin::Lead Sources Widget-->
<div :class="widgetClasses" class="card card-footprint">
<!--begin::Body-->
<div
class="card-body p-0 d-flex justify-content-between flex-column overflow-hidden"
>
<div class="d-lg-flex flex-stack flex-grow-1 px-9 py-6">
<!--begin::Text-->
<div class="d-flex flex-column text-start col-lg-10">
<span class="card-title">Lead Sources</span>
<span class="card-description">Where your leads are coming from.</span>
<!--begin::Chart-->
<div class="d-flex flex-column">
<apexchart
class="mixed-widget-10-chart lead-sources-donut"
:options="chartOptions"
:series="series"
type="donut"
:height="chartHeight"
:threshold="chartThreshold"
></apexchart>
</div>
<!--end::Chart-->
</div>
<!--begin::Unused Data-->
<div class="d-flex flex-row flex-lg-column lg-col-2 justify-content-between unused-data">
<div class="alt-sources flex-fill">
<div><span class="alt-header">Other Sources:</span></div>
<div v-for="item in otherSources" :key="item.source">
<span>{{ item.source }}</span>
<span>{{ item.value }}%</span>
</div>
</div>
<div class="alt-sources flex-fill">
<div><span class="alt-header">Sources Not Utilized:</span></div>
<div v-for="item in unusedSources" :key="item.source">
<span>{{ item.source }}</span>
</div>
</div>
</div>
<!--end::Unused Data-->
</div>
</div>
</div>
<!--end::Lead Sources Widget-->
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
name: "LeadSource",
props: {
widgetClasses: String,
chartColor: String,
chartHeight: String,
chartLabels: Array,
chartData: Array,
chartThreshold: Number
},
setup(props) {
const sum = (data) => {
let total = 0;
data?.map(function(v) {
total += v;
});
return total;
}
const chartData = ref(props.chartData).value;
const threshold = ref(props.chartThreshold).value;
const usedSourcesLabel: string[] = [];
const usedSourcesData: number[] = [];
const otherSources: any = [];
const unusedSources: any = [];
const splitData = (data, max) => {
// set used, other sources < 5%, unused sources
data.filter((item) => {
if (item.value > max) {
usedSourcesLabel.push(item.source);
usedSourcesData.push(item.value);
} else if (item.value < max && item.value != 0 && item.value !== null) {
otherSources.push(item);
} else if (item.value == 0 || item.value === null) {
unusedSources.push(item);
}
});
};
splitData(chartData, threshold);
const chartOptions = {
chart: {
width: 380,
type: "donut"
},
colors: [
"#1C6767",
"#CD2E3B",
"#154D5D",
"#F1D67E",
"#4F9E82",
"#EF8669",
"#393939",
"#30AEB4"
],
plotOptions: {
pie: {
startAngle: -90,
endAngle: 270
}
},
dataLabels: {
enabled: false
},
fill: {
type: "gradient",
gradient: {
type: "horizontal",
shadeIntensity: 0.5,
opacityFrom: 1,
opacityTo: 1,
stops: [0, 100],
}
},
legend: {
show: true,
position: "left",
fontSize: "16px",
height: 220,
onItemClick: {
toggleDataSeries: false
},
onItemHover: {
highlightDataSeries: false
},
formatter: function (val, opts) {
return val + " - " + opts.w.globals.series[opts.seriesIndex];
}
},
title: {
text: undefined
},
tooltip: {
style: {
fontSize: "14px"
},
marker: {
show: false
},
y: {
formatter: function(val) {
return val + "%";
},
title: {
formatter: (seriesName) => seriesName,
},
}
},
labels: usedSourcesLabel,
annotations: {
position: "front",
yaxis: [{
label: {
text: "text annotation"
}
}],
xaxis: [{
label: {
text: "text xaxis annotation"
}
}],
},
responsive: [{
breakpoint: 480,
options: {
legend: {
position: "bottom",
horizontalAlign: "left"
}
}
}]
};
const series = usedSourcesData;
return {
chartOptions,
series,
otherSources,
unusedSources
};
}
});
</script>
Edited
I will attach the LeadService.ts class as well as the ApiService.ts class so you can see where the data is coming from
LeadService.ts
import ApiService from "#/core/services/ApiService";
import {Lead} from "#/core/interfaces/lead";
export default class LeadService {
getLeads() {
const accountInfo = JSON.parse(localStorage.getItem('accountInfo') || '{}');
ApiService.setHeader();
return ApiService.query("/leads", {params: {client_id : accountInfo.client_id}})
.then(({ data }) => {
let leadData: Lead[] = data['Items'];
return leadData;
})
.catch(({ response }) => {
return response;
});
}
}
ApiService.ts
import { App } from "vue";
import axios from "axios";
import VueAxios from "vue-axios";
import JwtService from "#/core/services/JwtService";
import { AxiosResponse, AxiosRequestConfig } from "axios";
import auth from "#/core/helpers/auth";
/**
* #description service to call HTTP request via Axios
*/
class ApiService {
/**
* #description property to share vue instance
*/
public static vueInstance: App;
/**
* #description initialize vue axios
*/
public static init(app: App<Element>) {
ApiService.vueInstance = app;
ApiService.vueInstance.use(VueAxios, axios);
ApiService.vueInstance.axios.defaults.baseURL = "https://api.domain.com/";
}
/**
* #description set the default HTTP request headers
*/
public static setHeader(): void {
ApiService.vueInstance.axios.defaults.headers.common[
"Authorization"
] = `Bearer ${auth.getSignInUserSession().getIdToken().jwtToken}`;
ApiService.vueInstance.axios.defaults.headers.common[
"Content-Type"
] = "application/json application/vnd.api+json";
}
/**
* #description send the GET HTTP request
* #param resource: string
* #param params: AxiosRequestConfig
* #returns Promise<AxiosResponse>
*/
public static query(
resource: string,
params: AxiosRequestConfig
): Promise<AxiosResponse> {
return ApiService.vueInstance.axios.get(resource, params).catch(error => {
// #TODO log out and send home if response is 401 bad auth
throw new Error(`[KT] ApiService ${error}`);
});
}
}
export default ApiService;
I think the issue is caused when you're calling the data from the api.
This code:
beforeCreate: async function() {
this.leadData = await new LeadService().getLeads()
},
I'd refactor to
async created () {
const service = new LeadService()
const value = await service.getLeads()
}
Also it would be nice to be able to see how you're fetching your data.
Sometimes this code: const value = await axios.get('/api/getStuff').data can be problematic because of paratheses issues. Which causes the same issue you described of, hot reloading working, but fresh not. I suspect the same sort of issue relies of the code executing like => (await new LeadService()).getLeads() Where you're probably awaiting the class, rather than the actual async code.

Use a variable defined in a method inside the template

it's the first time I use Vue (v2 not v3) and I'm stucked trying to use a variable (defined inside a methods) inside the template.
My semplified code:
<template>
<div class="container" #mouseover="isHovered = true" #mouseleave="isHovered = false">
<div class="c-container">
<div ref="topCContainerRef" class="top-c-container">
<div
:class="['top-c', ...]"
:style="{ height: `${isHovered ? 0 : this.scaledHeight}` }" // <-- HERE I need `scaledHeight`
>
</div>
</div>
</div>
</div>
</template>
<script>
import { scaleLinear } from 'd3-scale'
export default {
name: 'MyComponent',
components: { },
props: {
...,
datum: {
type: Number,
required: true,
},
...
},
data: function () {
return {
isHovered: false,
scaledHeight: {},
}
},
mounted() {
this.matchHeight()
},
methods: {
matchHeight() {
const topCContainerHeight = this.$refs.topCContainerRef.clientHeight
const heightScale = scaleLinear([0, 100], [20, topCContainerHeight])
const scaledHeight = heightScale(this.datum)
this.scaledHeight = scaledHeight // I want to use this value inside the template
},
},
}
</script>
How can I get the value of scaledHeight inside the template section?
If I didn't use this, I get no error but the height value is always 0, like scaledHeight is ignored..
I read the documentation but it doesn't help me
I encountered and solved this problem today.
You can change your styles like below.
<div
:class="['top-c', ...]"
:style="{ height: isHovered ? 0 : scaledHeight }"
>
It works fine for me, and hope it will help you~~
Fixed using computed
computed: {
computedHeight: function () {
return this.isHovered ? 0 : this.matchHeight()
},
},
methods: {
matchHeight() {
const topCContainerHeight = this.$refs.topCContainerRef.clientHeight
const heightScale = scaleLinear([0, 100], [20, topCContainerHeight])
return heightScale(this.datum)
},
},

How to make vue swipeable bottom sheet component

i want to make swipeable bottom sheet component, like this https://manufont.github.io/react-swipeable-bottom-sheet/scroll.html
I have also tried this package https://github.com/atsutopia/vue-swipeable-bottom-sheet
but this package not to sweep in all sheets areas.
and i try to do something with vue-recognizer to catch pan events , but it end up like this.
<template>
<div>
<div
class="fixed bg-white rounded w-full text-center"
v-recognizer:pan.end="onPanEnd"
:style="{bottom: '0px', height: height + 'px'}"
v-recognizer:pan.up="onPanUp"
v-recognizer:pan.down="onPanDown"
style="z-index:1000;"
>
<div class="py-4">{{height}}</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
height: 92,
};
},
methods: {
onPanUp() {
this.height = parseInt(this.height + 5);
},
onPanDown() {
this.height = parseInt(this.height - 5);
},
onPanEnd() {
},
},
};
</script>
any suggestions ?

Victory onClick event not updating fill color

I am new to react and trying to use Victory within a react app to make a simple scatter plot. I was working through the examples and cannot get the interactive portion of the scatter plot to work.
Here is my App.js
import ReactDOM from 'react-dom';
import { VictoryScatter } from 'victory';
import {sampleData} from './data_holder'
class App extends React.Component {
render() {
return (
<div>
<h3>Click Me</h3>
<VictoryScatter
style={{ data: { fill: "#c43a31" } }}
size={9}
labels={() => null}
events={[{
target: "data",
eventHandlers: {
onClick: () => {
return [
{
target: "data",
mutation: (props) => {
const fill = props.style && props.style.fill;
return fill === "black" ? null : { style: { fill: "black" } };
}
}, {
target: "labels",
mutation: (props) => {
return props.text === "clicked" ?
null : { text: "clicked" };
}
}
];
}
}
}]}
data={sampleData}
/>
</div>
)
}
}
ReactDOM.render(<App/>, document.getElementById('root'));
export default App
Sample data saved in .data_holder is here:
export const sampleData=[
{ x: 1, y: 2 },
{ x: 2, y: 3 },
{ x: 3, y: 5 },
{ x: 4, y: 4 },
{ x: 5, y: 7 }
]
The onClick event is happening (when I add a log statement in the onClick it is clearly shown), but the fill colors are not updating. Any help is much appreciated.
Use onPressIn in place of onClick will work.
It looks like the issue is caused by enabling strict mode for the component in react. I am uncertain of why, but the plot is able to update once I remove strict mode from react. I will update here once I know the specifics of interaction.

React and Flux - how to tie view to store data

I started learning React with Flux and I am having some trouble.
I have a store and two views (components) and I would like to update one view when the store data changes, but I'm not sure how to tie the two together.
What happens is this:
I click a button in view 1, this modifies data in the store, then this should update view two.
view1 onClick => dispatch action => modify store data => update view2
I have everything except the last part, tying the view and the store when changes are made to the store. All this currently does is modify a class name, but I can see other functions for it later on.
So my question is, how can I tie the store to the state of the view?
view2 (Homepage)
var React = require('react');
var DefaultLayout = React.createFactory(require('../layouts/Default'));
var ReactGridLayout = React.createFactory(require('react-grid-layout'));
var desktopStore = require("../stores/DesktopStore");
// var classNames = require('classnames');
var HomePage = React.createClass({
displayName: 'Index.jsx',
getInitialState: function(){
return {zoomed: desktopStore.get('zoom')};
},
getDefaultProps: function() {
return {
layout: DefaultLayout
};
},
render: function() {
var parentClassString = "desktop"; // base class
if(this.state.zoomed){
parentClassString += " zoomed"; // add a class based on the zoomed property
}
return (
<div className={parentClassString}>
<ReactGridLayout className="layout" cols={80} rowHeight={30} verticalCompact={false}
initialWidth={10} autoSize={false} isResizable={false}>
<div key={1} _grid={{x: 0, y: 0, w: 1, h: 1}} >1</div>
<div key={2} _grid={{x: 0, y: 0, w: 1, h: 1}} >2</div>
<div key={3} _grid={{x: 0, y: 0, w: 1, h: 1}} >3</div>
<div key={4} _grid={{x: 0, y: 0, w: 1, h: 1}} >4</div>
<div key={5} _grid={{x: 0, y: 0, w: 1, h: 1}} >5</div>
<div key={6} _grid={{x: 0, y: 0, w: 1, h: 1}} >6</div>
</ReactGridLayout>
</div>
);
}
});
module.exports = HomePage;
View1 (Header)
var _name = 'TopHeader.jsx';
var React = require('react');
var DesktopApi = require('../utilities/DesktopApi');
var TopHeader = React.createClass({
displayName: _name,
handleClick: function(){
DesktopApi.toggleZoom(); // this goes to the dispatcher and emits a change to update the desktopStore
},
render() {
return (
<div className="top-header">
<span>Header</span>
<span className="plus" onClick={this.handleClick}> [+] </span>
</div>
);
}
});
module.exports = TopHeader;
The official facebook guide helped out here, I can create listeners with componentDidMount and remove them with componentWillUnmount
componentDidMount: function() {
desktopStore.addChangeListener(this._onChange);
},
componentWillUnmount: function() {
desktopStore.removeChangeListener(this._onChange);
},
_onChange: function(){
this.setState({zoom: true});
}
If you want to create a mixin to bind the views to stores, you can read fluxxor's source. Here's the sample of storeWatchMixin. Or you can use higher order component to wrap your component.

Categories