Use XState in a nuxt composable - components

I want to use an xstate state machine in Nuxt 3 which is used over multiple components.
I created a small example of how I want this to look like.
I also use the nuxt-xstate module.
State Machine:
export default createMachine(
{
id: 'toggle',
initial: 'switched_off',
states: {
switched_on: {
on: {
SWITCH: {
target: 'switched_off'
}
}
},
switched_off: {
on: {
SWITCH: {
target: 'switched_on'
},
}
},
},
}
)
Composable:
const toggle = useMachine(toggleMachine)
export function useToggleMachine(){
return { toggle }
}
app.vue:
<template>
<div>
State: {{toggle.state.value.value}}
</div>
<br />
<button
#click="toggle.send('SWITCH')"
>
Switch
</button>
</template>
<script>
import { useToggleMachine } from '~/composables/toggle_machine'
export default {
setup(){
const { toggle } = useToggleMachine()
return { toggle }
}
}
</script>
The problem is, that I can have a look at the state of the machine {{state.value.value}} gives me the expected 'turned_off'. But I cannot call the events to transition between states. When clicking on the button, nothing happens.
Here is the console.log for the passed 'toggle' object:
Does anyone know a way how to fix this, or how to use xstate state machines over multiple components.
I am aware that props work, but I don't really want to have an hierarchical approach like that.

In Nuxt3, it's very simple:
in composables/states.ts
import { createMachine, assign } from 'xstate';
import { useMachine } from '#xstate/vue';
const toggleMachine = createMachine({
predictableActionArguments: true,
id: 'toggle',
initial: 'inactive',
context: {
count: 0,
},
states: {
inactive: {
on: { TOGGLE: 'active' },
},
active: {
// eslint-disable-next-line #typescript-eslint/no-explicit-any
entry: assign({ count: (ctx: any) => ctx.count + 1 }),
on: { TOGGLE: 'inactive' },
},
},
});
export const useToggleMachine = () => useMachine(toggleMachine);
In pages/counter.vue
<script setup>
const { state, send } = useToggleMachine()
</script>
<template>
<div>
<button #click="send('TOGGLE')">
Click me ({{ state.matches("active") ? "✅" : "❌" }})
</button>
<code>
Toggled
<strong>{{ state.context.count }}</strong> times
</code>
</div>
</template>

Related

Adminjs adding custom reactjs components to my resource on adminjs

I am using adminjs for the first time and I have written my own custom component, but adding it to the resource keeps producing an error that says "You have to implement action component for your Action" Here is my resource:
const {Patient} = require('./../models/Patient')
const Components = require('./../components/components')
const { Patient } = require('./../models/Patient')
const Components = require('./../components/components')
const PatientResource = {
resource: Patient,
options: {
actions: {
consultation: {
actionType: 'record',
component: Components.MyCustomAction,
handler: (request, response, context) => {
const { record, currentAdmin } = context
return {
record: record.toJSON(currentAdmin),
msg: 'Hello world',
}
},
},
isAccessible: ({ currentAdmin }) => {
action: if (currentAdmin.role_id === 5) {
return false
} else {
return true
}
},
},
navigation: 'Manage Patients',
listProperties: [
'id',
'last_name',
'first_name',
'sex',
'marital_status',
'blood_group',
'genotype',
'existing_health_condition',
],
},
}
module.exports = { PatientResource }
And this is my custom component:
import React from 'react'
import { Box, H3 } from '#adminjs/design-system'
import { ActionProps } from 'adminjs'
const MyCustomActionComponent = (props: ActionProps) => {
const { record } = props
return (
<Box flex>
<Box
variant='white'
width={1 / 2}
boxShadow='card'
mr='xxl'
flexShrink={0}
>
<H3>Example of a simple page</H3>
<p>Where you can put almost everything</p>
<p>like this: </p>
<p>
<img
src='https://i.redd.it/rd39yuiy9ns21.jpg'
alt='stupid cat'
width={300}
/>
</p>
</Box>
<p> Or (more likely), operate on a returned record:</p>
<Box overflowX='auto'> {JSON.stringify(record)}</Box>
</Box>
)
}
module.exports = { MyCustomActionComponent }
I tried to add the component to my custom defined action "Consultation", Hence I expected to see a custom page which i have designed using react as in "mycustomAction" above

Using CKEditor in a component with Laravel/Inertia

I'd like to use Ckeditor in my Laravel/Inertia project and i can't get it to work. I found a tutorial from LaraTips, but that was written for VueJS-2. I am working with the lastest version Inertia which uses VueJS-3.
I want to use Ckeditor in a separate component, and it (sort of) works, but i can't get the old data to show in the editor. I get an error "Uncaught (in promise) TypeError: Cannot read property 'setData' of undefined at Proxy.modelValue (app.js:29)"
What am i doing wrong?
This is my component:
<template>
<ckeditor :editor="editor" v-model="text" :config="editorConfig"></ckeditor>
</template>
<script>
import ClassicEditor from '#ckeditor/ckeditor5-build-classic';
export default {
data() {
return {
text: "",
editor: ClassicEditor,
editorConfig: {
// The configuration of the editor.
},
}
},
props: {
modelValue: {}
},
setup() {
},
watch: {
modelValue: {
immediate: true,
handler(modelValue) {
this.text = modelValue;
}
},
text(text) {
this.$emit('update:modelValue', text);
}
},
}
</script>
Any suggestions??
I am doing the same tutorial (i am using vueJS-3).
this may work for you:
in app.js include CKEditor:
createInertiaApp({
title: (title) => `${title} - ${appName}`,
resolve: (name) => require(`./Pages/${name}.vue`),
setup({ el, app, props, plugin }) {
return createApp({ render: () => h(app, props) })
.use(plugin)
.use( CKEditor)
.mixin({ methods: { route } })
.mount(el);
},
});
In Components/CkEditor.vue check what are you emitting
look for this this.$emit("input", text);
<template>
<ckeditor :editor="editor" v-model="text" :config="editorConfig"></ckeditor>
</template>
<script>
import ClasicEditor from "#ckeditor/ckeditor5-build-classic";
export default {
props: {
value: {},
},
data() {
return {
text: "",
editor: ClasicEditor,
editorConfig: {
// The configuration of the editor.
},
};
},
watch: {
value:{
inmediate: true,
handler(value){
this.text = value;
}
},
text(text) {
this.$emit("input", text);
},
},
};
</script>
let me know if that worked for you
I looked at the answer here, and below is what worked for me:
Hope this helps! :)
I am using laravel/inertia with vue 3.
app.js
import './bootstrap';
import '../css/app.css';
import { createApp, h } from 'vue';
import { createInertiaApp } from '#inertiajs/inertia-vue3';
import { InertiaProgress } from '#inertiajs/progress';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { ZiggyVue } from '../../vendor/tightenco/ziggy/dist/vue.m';
import { createPinia } from 'pinia';
import { _t } from './Utilities/translations';
import CKEditor from '#ckeditor/ckeditor5-vue';
const appName =
window.document.getElementsByTagName('title')[0]?.innerText || 'Laravel';
createInertiaApp({
title: (title) => `${title} - ${appName}`,
resolve: (name) =>
resolvePageComponent(
`./Pages/${name}.vue`,
import.meta.glob('./Pages/**/*.vue')
),
setup({ el, app, props, plugin }) {
const vue_app = createApp({ render: () => h(app, props) });
vue_app.use(plugin);
vue_app.use(ZiggyVue, Ziggy);
vue_app.use(createPinia());
// Register all base components globally
const components = import.meta.globEager('./Components/Base/*.vue');
for (const path in components) {
let componentName;
if (path.split) {
const split_componentName = path.split('/').pop();
if (split_componentName) {
componentName = split_componentName.replace(/\.\w+$/, '');
vue_app.component(componentName, components[path].default);
}
}
}
vue_app.config.globalProperties.$_t = _t;
vue_app.use(CKEditor);
vue_app.mount(el);
return vue_app;
}
});
InertiaProgress.init({ color: '#4B5563' });
CKEditor Component:
<template>
<div id="app">
<ckeditor
v-model="editor_data"
:editor="editor"
:config="editor_config"
></ckeditor>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from '#vue/reactivity';
import * as editor from '#ckeditor/ckeditor5-build-classic';
const editor_data = ref('');
const editor_config = {};
</script>

Use Worker output in a vue component

I tried to send the output from my worker to my component.vue by window.localStorage.
Does anybody know how to show and update my worker's result in my component vue automatically?
This is my code:
worker-api.js
import Worker from "worker-loader!./worker.js";
const worker = new Worker();
worker.addEventListener('message', (e) => {
window.localStorage.setItem('result', JSON.stringify(e.data));
});
export function sendMessage(msg) {
worker.postMessage(msg);
}
worker.js
self.addEventListener("message", (e) => {
var count = e.data;
while(count < 20) {
const result = e.data + 3
self.postMessage(result);
}
});
my-component.vue
<template>
<p>Count: "{{ result }}"</p>
</template>
<script>
import Button from './Button'
import { sendMessage } from './worker-api'
export default {
name: 'my-component',
components: {Button},
data () {
return {
count : 0
}
},
computed: {
result: function () {
return JSON.parse(window.localStorage.getItem('result'))
}
},
methods: {,
postMessage() {
sendMessage(this.count)
}
},
}
</script>
It is not possible to deal with localStorage values as if they were reactive. Probably, that's why your computed property does not work.
One possible solution is to import your worker inside your component and use to update a reactive variable.
Something similar to:
component.vue
<template>
<button #click="increment">Increment Result</button>
{{ result }}
</template>
<script>
export default {
data() {
return {
// the worker path must be relative to the /public folder (in this example, the worker.js file must be at /public/worker.js)
worker: new Worker('/worker.js'),
result: 0
}
},
created() {
const self = this
this.worker.onmessage = function(event) {
self.result = event.data
}
},
methods: {
increment() {
this.worker.postMessage(this.result)
}
}
}
</script>
/public/worker.js
onmessage = function(event) {
// data sent by the Vue component is retrieved from 'data' attribute
postMessage(event.data + 1)
}

Prop being mutated: "isOpen"

I got this error:
vue.runtime.esm.js?2b0e:619 [Vue warn]: Avoid mutating a prop directly
since the value will be overwritten whenever the parent component
re-renders. Instead, use a data or computed property based on the
prop's value. Prop being mutated: "isOpen"
Here is my code
My child component:
<template>
<v-dialog v-model="isOpen" max-width="500px">
<v-card>
<v-card-title>Remove</v-card-title>
<v-card-text>Are you sure to delete?</v-card-text>
<v-card-actions>
<v-btn color="primary" text #click="$emit('closedialog')">Close</v-btn>
<!-- <v-btn color="primary" text #click="deleteItem">Delete</v-btn> -->
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
export default {
// name: 'confirmDelete',
props: {
isOpen: Boolean
// selected: Object
}
};
</script>
Parent Component:
<template>
<div class="container">
<div id="app">
<v-app id="inspire">
<v-data-table
:headers="headers"
:items="contracts"
sort-by="createdAt"
class="elevation-1"
>
<template v-slot:top>
<v-toolbar flat color="white">
<v-toolbar-title>CONTRACTS</v-toolbar-title>
<v-divider class="mx-4" inset vertical></v-divider>
<v-spacer></v-spacer>
<v-dialog v-model="dialog" max-width="500px">
<template v-slot:activator="{ on, attrs }">
<v-btn
color="primary"
dark
class="mb-2"
v-bind="attrs"
v-on="on"
>New Contract</v-btn>
</template>
<v-card>
<v-card-title>
<span class="headline">{{ formTitle }}</span>
</v-card-title>
<v-card-text>
<v-container>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field
label="Start Contract"
name="name"
prepend-icon="person"
placeholder="YYYY-MM-DD"
type="text"
required
v-model="selectedItem.startDate"
:rules="nameErrors"
#input="$v.selectedItem.startDate.$touch()"
#blur="$v.selectedItem.startDate.$touch()"
#keyup="clearServerErrors('name')"
/>
</v-col>
<v-col cols="12" sm="6">
<v-select
v-model="selectedItem.duration"
:items="[1, 2, 3, 6, 12]"
label="Duration Contract."
required
/>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-select
v-model="selectedItem.leave"
:items="[20, 26]"
label="Days off"
/>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text #click="close">Cancel</v-btn>
<v-btn
color="blue darken-1"
text
#click="onSave"
:disabled="!isValid"
>Save</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-toolbar>
</template>
<template v-slot:item.actions="{ item }">
<v-icon small class="mr-2" #click="editItem(item)">mdi-pencil</v-icon>
<v-icon small #click="showDeleteDialog(item)">mdi-delete</v-icon>
<v-icon middle #click="goToRouteLeaves(item)">play_arrow</v-icon>
</template>
</v-data-table>
<CreateOrEditContract :is-open="isDialogDeleteVisible" #closedialog="close()" />
<!-- <v-dialog v-model="isDialogDeleteVisible" max-width="500px">
<v-card>
<v-card-title>Remove</v-card-title>
<v-card-text>Are you sure to delete?</v-card-text>
<v-card-actions>
<v-btn color="primary" text #click="isDialogDeleteVisible = false">Close</v-btn>
<v-btn color="primary" text #click="deleteItem">Delete</v-btn>
</v-card-actions>
</v-card>
</v-dialog>-->
</v-app>
</div>
</div>
</template>
<script>
import ContractService from '../services/ContractService';
import UserContractsService from '../services/UserContractsService';
import { validationMixin } from 'vuelidate';
import { required } from 'vuelidate/lib/validators';
import CreateOrEditContract from './CreateOrEditContract';
var moment = require('moment');
export default {
name: 'Admin',
components: {
CreateOrEditContract
},
mixins: [validationMixin],
validations: {
selectedItem: {
startDate: {
required,
isStartDate(value) {
return this.isStartDate(value);
}
}
}
},
data() {
return {
selectedItem: {
startDate: ''
},
serverErrors: {
startDate: ''
},
errorMessage: '',
error: null,
validationError: false,
contracts: [],
dialog: false,
isDialogDeleteVisible: false,
headers: [
{ text: 'Start', value: 'startDate' },
{ text: 'Duration', value: 'duration' },
{ text: 'Leave', value: 'leave' },
{ text: 'Actions', value: 'actions', sortable: false }
],
defaultItem: {
startDate: '',
duration: '',
leave: ''
}
};
},
created() {
this.selectedItem = { ...this.defaultItem };
},
async mounted() {
try {
const { userId } = this.$route.params;
const { data } = await UserContractsService.index(userId);
this.contracts = data;
} catch (error) {
this.errorMessage =
(error.response && error.response.data ? error.response.data : null) ||
error.message ||
error.toString();
}
},
computed: {
formTitle() {
return this.selectedItem.id ? 'Edit Contract' : 'New Contract';
},
nameErrors() {
const errors = [];
if (!this.$v.selectedItem.startDate.$dirty) return errors;
!this.$v.selectedItem.startDate.required && errors.push('Date is required');
!this.$v.selectedItem.startDate.isStartDate && errors.push('Enter valid date');
return errors;
},
isValid() {
return !this.$v.$invalid;
}
},
watch: {
dialog(val) {
val || this.close();
}
},
methods: {
editItem(item) {
this.selectedItem = { ...item };
this.dialog = true;
},
async deleteItem() {
const index = this.contracts.findIndex((contract) => contract.id === this.selectedItemlete.id);
this.contracts.splice(index, 1);
this.isDialogDeleteVisible = false;
await ContractService.delete(this.selectedItemlete.id);
this.selectedItemlete = { ...this.defaultItem };
},
showDeleteDialog(item) {
this.selectedItemlete = item;
this.isDialogDeleteVisible = true; //!this.isDialogDeleteVisible;
// this.$emit("clicked", !this.isDialogDeleteVisible)
},
close() {
this.isDialogDeleteVisible = false;
this.dialog = false;
this.selectedItem = { ...this.defaultItem };
},
async onSave() {
if (this.selectedItem.id) {
const index = this.contracts.findIndex((contract) => contract.id === this.selectedItem.id);
await ContractService.save(this.selectedItem);
this.$set(this.contracts, index, this.selectedItem);
} else {
this.selectedItem.userId = this.$route.params.userId;
const { data } = await ContractService.save(this.selectedItem);
this.contracts.push(data);
}
this.close();
},
goToRouteLeaves(item) {
this.$router.push(`/leaves/${item.id}`);
},
clearServerErrors(type) {
this.serverErrors[type] = [];
},
isStartDate(value) {
return moment(value, 'YYYY-MM-DD', true).isValid();
}
}
};
</script>
<style scoped>
v-btn {
position: absolute;
}
</style>
v-model="isOpen" your child component is trying to change the props isOpen.
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders.
make change like below:
Parent:
<CreateOrEditContract :is-open.sync="isDialogDeleteVisible" #closedialog="close()" />
Child:
computed: {
open: {
// getter
get: function () {
return this.isOpen
},
// setter
set: function (newValue) {
this.$emit('update:isOpen', newValue)
}
}
}
<v-dialog v-model="open" max-width="500px">
It's because props work from top to bottom pattern. isOpen passed by parent to child, now it's like data flowing from top to bottom. If your child tries to mutate that data, how will parents get informed about that change? The parent will never get informed this way that's why it's a warning to not change the value of prop passed in child. You need to find a way to communicate to parents and parents will update that prop, this way data flow will not break.
Here v-model is two-way binding which means it will set the value of the property which is isOpen prop.
<template>
<v-dialog v-model="isOpen" max-width="500px">
<v-card>
<v-card-title>Remove</v-card-title>
<v-card-text>Are you sure to delete?</v-card-text>
<v-card-actions>
<v-btn color="primary" text #click="$emit('closedialog')">Close</v-btn>
<!-- <v-btn color="primary" text #click="deleteItem">Delete</v-btn> -->
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
export default {
// name: 'confirmDelete',
props: {
isOpen: Boolean
// selected: Object
}
};
</script>
Hint: You can emit event to inform parent about the change and make parent change the value of isOpen. Try computed getter/setter to achieve this

How to access vm.$el properties with single-file components?

I'm trying to access the clientHeightproperty of a vue instance created by a single-file component, but it returns undefined. How can I do this?
<template lang='jade'>
article#article.projectCard.relative.mb5.pa5( v-bind:style="styleObject")
h3 {{ project.projectName }}
p {{ project.projectDescription }}
</template>
<script>
export default {
props: {
project: '',
},
data () {
return {
styleObject: {
backgroundColor: this.project.projectMainColor,
height: '80vh'
},
cardHeight: this.clientHeight,
};
},
</script>
You can access the element after it's mounted with this.$el so you'd actually want this.$el.clientHeight after it's mounted.
You can do like:
data () {
return {
cardHeight: 0,
}
}
Then do:
mounted () {
this.cardHeight = this.$el.clientHeight + 'px'
}
Also, that styleObject would be better as a computed property. That way as things change it'll be automatically updated.
I'd personally do:
data () {
return {
cardHeight: '80vh',
}
},
mounted () {
this.cardHeight = this.$el.clientHeight + 'px'
},
computed: {
styleObject () {
return {
backgroundColor: this.project.projectMainColor,
height: this.cardHeight,
}
}
}

Resources