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 backenddd
error dialogsauth
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 pluginsui
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
(andv-model
is mandatory), so you are not required to listen to@setState
event anymore - Each field expects a (required)
config
(typeFieldConfig | object
) prop, which is aJSON
object that describes the field (e.g. name, label, tooltip etc.) - Each field accepts an optional
formState
(typeFormState | object
) prop, which is aJSON
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'
}
]
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())