Skip to main content

Developer guide

In this document is described some core concepts and how to use them.

Application structure

Main entry

The main.ts is the entry point of the application, whereas App.vue is the root component.

The App.vue component uses the <QoreAppWrapper />. This component is responsible for making features like dialogs, notifications etc. work globally. It also makes sure the globals are loaded when the user is authenticated, and will show a loader until everything is ready.

Layouts & App config

By default, the layouts directory contains a MainLayout.vue and AuthLayout.vue. The MainLayout.vue, alongside other app configs, are passed on to the qore Vue plugin in main.ts:

const appConfig = new AppConfig(router, locale.global, import('@/layouts/MainLayout.vue'))
app.use(qorePlugin, appConfig)

This way, Qore will be aware of what the app is using, so it can extend it with its own features (for example appending translations).

PrimeVue

The Qore Vue plugin will use the PrimeVue vue plugin, and register all its components globally. This means you can use PrimeVue components in your components without importing them (e.g. <Button>, <InputText> etc.).

For documentation, visit: https://primevue.org/

PassThrough props

A common pattern in PrimeVue is to use the pt prop. This is a neat way to pass on props to the underlying component. For example, to pass on a class prop to a Button component:

<Button :pt="{ root: { class: 'p-2' } }" />

Icons

PrimeVue supports a set of icons by default. You can add icons simply by adding:

<i class="pi pi-check"></i>

If you would like to use material icons instead, use:

<i class="material-icons">{{ iconName }}}</i>

More information can be found on their docs.

Pinia store

Inside Qore, the following stores exist:

  • api Responsible for storing the most recent responses (e.g. validation messages) and showing backend dd error dialogs
  • auth Responsible for handling the authentication state (e.g. storing the auth user and authentication state)
  • globals Responsible for storing the menu, tenants, resources, modules and plugins
  • ui Responsible for theming (e.g. sidebar width, topbar height, dark mode etc.) and showing global dialogs/notifications

Some state will be stored in local storage, so it will be available after a page refresh.

Using the store

Each store is made globally available in components for easy access in the $q object. For example, to access the auth store:

<Button
:label="$q.auth.user?.name"
/>

Similar to Quasar, you can also use it for media queries:

<AppLayoutConfig v-if="$q.ui.screen.gt.md" />

If you want to access the store inside composition functions, you can use the useStore function:

import { useAuthStore } from 'qore'

const authStore = useAuthStore()

console.log(authStore.user)

Adding or customizing stores

Inside boot/store.ts you can add or extend stores. For example, to extend the auth store:

pinia.use(({ store }) => {
if (store.$id === 'auth') {
store.hello = 'world'
}
})

// Then later in components:
import { useAuthStore } from 'qore'
const auth = useAuthStore()
console.log(auth.hello)

For full documentation please visit: https://pinia.vuejs.org/introduction.html

I18n

Translation messages will be loaded inside boot/locale.ts

Translations

You can use translations in components by using the $t helper:

<Button
:label="$t('Send')"
/>

If you want to use translations inside composition functions, you can use the translate function:

import { translate } from 'qore'

console.log(translate('auth.Logout'))

Fields

Fields can, like in front-end v1, be registered globally (e.g. in components.ts).

General usage

Fields work a bit different compared to v1:

  • It is now compatible with v-model (and v-model is mandatory), so you are not required to listen to @setState event anymore
  • Each field expects a (required) config (type FieldConfig | object) prop, which is a JSON object that describes the field (e.g. name, label, tooltip etc.)
  • Each field accepts an optional formState (type FormState | object) prop, which is a JSON object that describes the form state (e.g. required fields, validation messages etc.)

Using fields outside forms

A common frustration in front-end v1 was that fields could only be used inside forms. This is no longer the case. Below is an example of how to use a field outside a form.

<template>
<div :class="`flex flex-col gap-${$q.ui.fieldsGap}`">
<TextField
v-model="formState.values.name"
:formState="formState"
:config="nameFieldConfig"
/>

<BelongsToManyField
v-model="formState.values.hasMany"
:formState="formState"
:config="{
name: 'hasMany',
label: 'Has Many',
componentProperties: { multiple: true, options: hasManyOptions }
}"
/>

<pre class="mt-4">{{ formState.values }}</pre>
</div>
</template>
import { ref } from 'vue'
import { FieldConfig, FormState, translate } from 'qore'

// Define formstate
const formState = ref(new FormState<{ name: string; hasMany: Array<string> }>())

// Config for name field
const nameFieldConfig = ref<FieldConfig>(new FieldConfig('name'))
nameFieldConfig.value.label = translate('Name')
nameFieldConfig.value.toolTip = translate('Hello from tooltip!')
nameFieldConfig.value.hint = translate('Hello from hint!')
nameFieldConfig.value.attributes = {
autofocus: true,
autocomplete: 'username'
}

// Setting the formstate values
formState.value.values = {
name: 'Koen',
hasMany: ['one', 'two']
}

// Mark the name as required
formState.value.meta.required = ['name']

// The options for hasMany field
const hasManyOptions = [
{
label: 'One',
value: 'one'
},
{
label: 'Two',
value: 'two'
},
{
label: 'Three',
value: 'three'
}
]
info

For a working example that communicates with the back-end, you might take a look at views/auth/LoginView.vue

Creating a form field

To create a custom form field, you can use the following example. Each field should be wrapped inside the FieldWrapper component, which will take care of the form state, validation messages, label etc.

<template>
<FieldWrapper :config="config" :formState="formState">
<InputText
v-model="localValue"
@input="$emit('update:modelValue', localValue)"
v-bind="inputProps"
v-focus="config.autofocus"
/>
</FieldWrapper>
</template>

<script setup lang="ts">
import { FieldConfig, FormState, useField } from 'qore'

const props = defineProps<{
modelValue: string | null
config: FieldConfig
formState?: FormState
}>()

const emit = defineEmits(['update:modelValue'])

const { localValue, inputProps } = useField(props, emit)
</script>

The useField exposes multiple props that might be useful for your custom field:

{
localValue,
inputProps,
fieldMeta,
fieldData,
callAction,
fieldHasValidationMessages,
fieldValidationMessages,
fieldFirstValidationMessage
}

If your field will use options (e.g. for a Dropdown), you might also want to use:

import { useFieldSelectOptions } from 'qore'
const { selectInputProps } = useFieldSelectOptions(props.config) // Contains the options, filter etc.

Notify & Dialog & Confirm

Qore exposes helper functions to show notifications and dialogs via composition api.

Showing a notification

Below is an example of how to show a notification:

import { notify } from 'qore'
notify({
severity: 'success',
detail: apiStore.lastSuccesfulResponseAsString
})

Showing a dialog

The BaseDialog component can be used to show a dialog:

<Button @click="dialog = !dialog">Show base dialog</Button>
<BaseDialog header="Hello from dialog" v-model="dialog">
<p>Hello world</p>
</BaseDialog>

A dialog is usually included inside the component you are working on (see PrimeVue documentation for more information), however you can also do it via the showDialog helper:

import { markRaw } from 'vue'
import { showDialog } from 'qore'
import MyDialogComponent from '@/components/dialogs/MyDialogComponent.vue'
showDialog({
component: markRaw(MyDialogComponent),
options: {
props: {
header: translate('Hello from dialog'),
modal: true,
style: {
width: '90vw'
},
dismissableMask: true
}
}
})

Confirmation popup

You can show a confirmation popup by using the showConfirmPopup helper:

<Button @click="(e) => showConfirmPopup(e, { accept: () => notify('Confirmed') })">
Show confirm popup
</Button>
import { showConfirmPopup } from 'qore'

API

V2 will use axios again, however the syntax is a bit different

Doing requests

You can start doing api requests by using the qore helper:

import { useApi } from 'qore'

const api = useApi()

api.post('reset-password', { email: ''})
.then(() => {

})
.catch(() => {

})

The last validation messages and responses are stored in the api store.

Router

The router works mostly the same as v1. The router is booted inside boot/router.ts, and the routes exist in routes.ts.

General usage

To navigate to a route:

import { useRouter } from 'vue-router'

const router = useRouter()

router.push({ name: 'home' })

To get the current route:

import { useRouter } from 'vue-router'

const router = useRouter()

console.log(router.currentRoute.value)

Types

General usage

Most types you'd expect can be imported from qore, for example:

import { AuthStatus, type User, type QoreResource, type Tenant, type Model, ... } from 'qore'

defineProps<{
user: User
}>()

Helpers

General usage

All helpers from v1 should eventually be available in v2. You can use them like this:

import { onMounted } from 'vue'
import { reloadGlobals } from 'qore'

onMounted(() => {
reloadGlobals()
})

VueUse

Some helpers however are also already available by the vueuse package, so it is recommended to use those instead. For example:

import { watchDebounced, isObject, useArrayUnique } from '@vueuse/core'

watchDebounced(
() => [formState.value.values.password, formState.value.values.password_confirmation],
validatePassword,
{
debounce: 300
}
)

console.log(isObject(['hello']), useArrayUnique(['hello', 'hello']))

String case change

For string case change, you can use the @vueuse/integrations package:

import { useChangeCase } from '@vueuse/integrations/useChangeCase'

console.log(useChangeCase('hello_world', 'camelCase').value)

Uuid

You can generate uuid:

import { v4 as uuidv4 } from 'uuid'

console.log(uuidv4())