[How To] Building components with Quasar
-
This is a more detailed write up from my post here: http://forum.quasar-framework.org/topic/673/make-own-component-from-quasar-component/7
This is still work in progress and some topics are missing. If some information is wrong, or I am missing on something, just let me know and I will update this post accordingly.
Building reusable components with Quasar
Vue.js greatly encourages the use of components to encapsulate reusable code and therefore DRY up your code.
Most of the time Vue components are distributed as so called “Single File Components”. Single file components have the
.vue
file extension and allow to write the JS code, template, and style in the same file. These files are then put into a build system like webpack and vue-loader which will transform the template into a render function and extract the styles into a CSS file.Most of Quasars components are also distributed as single file components, you can check out their source here.
Extending components
Quasar is a framework and therefore provides building blocks to build your own Apps on top of it. But often the question arises how one could use the already existing Quasar components to build own components.
The first thing to notice it that Vue.js favors composition over inheritance.
Inheritance is a concept know from object oriented programming, where classes are able to extend another class to reuse its methods and attributes to build a new but similar class. Composition, on the other hand, is also known from object oriented programming, but instead of extending or overwriting an existing class, the class uses other classes to provide some common services.
Mixins
Mixins allow reusing certain features that you need in a set of components to not repeat yourself writing that code over and over.
To define a mixin one has to export an object that looks similar to a normal component. Other components now can use this mixin to implement the mixin functionality.
For example, lets we need to call a
register
method on a lot of different components. This method calls an API and returns some identifier that should be stored in the data object of the component.First, let us define the RegisterMixin:
export const RegisterMixin = { data () => { return { id: '' } }, methods: { register () { // Lets assume we extracted the AJAX call to the Registration class new Registration() .register() .then(response => { this.id = response.id }) } }, created () { this.register() } }
Now that we have defined the mixin, we can use it on any other component and it will be mixed in the component attributes.
import { RegisterMixin } from './registerMixin' export default { mixins: [RegisterMixin] }
A component can use as many mixins as it likes.
But be aware how Vue merges the options. You can read more about mixins in the Vue docs.Quasar uses mixins for some of its internal functionality. For example, the RouterLinkMixin which allows adding link functionality to different components.
But as great as mixins are, you can not use another single file component as mixin because only the attributes are mixed in and not the template or style definition.
Let’s assume we want to build a component called
MySelect
which behaves a bit different fromQSelect
.If we would write the following code:
import { QSelect } from 'quasar' export default { mixin: [QSelect] }
We would end up with a component that has all the internal methods and data from
QSelect
but no template at all. So we would have to get to the source ofQSelect
and copy the whole template definition. This would work as long asQSelect
gets updated and you forget to update the template as well. Even if you only update minor versions it could break, because you are not relying on the external interface ofQSelect
which is described in the docs, but also on the internal code, which normally one shouldn’t have to care about.But how do we build or own
MySelect
component?Custom select component
Let’s take an example from the forum. Someone asked how to build a component that hides some of the props passed to
QSelect
. Specifically, he wanted to build a select component which always had thefilter
prop set to true and always apply a defaultfilter-placeholder
.
A simple implementation of this component could look like this:<template> <q-select :value="value" :options="options" @change="handleChange" filter filter-placeholder="select"/> </template> <script> import { QSelect } from 'quasar' export default { props: ['value', 'options'], methods: { handleChange (newVal) { this.$emit('input', newVal) } }, components: { QSelect } } </script>
Because
v-model="foo"
is just syntactic sugar for:value="foo" @input="foo = $event.target.the value"
we can define a propertyvalue
on our new component, which is then passed asvalue
to the innerQSelect
. We are then listening to thechange
event on theQSelect
which indicates that the value has changed. If we receive such an event, we are emiting aninput
event from our new component and pass the new value as parameter.Now we can use the component like this:
<template> <my-select v-model="selected" :options="myOptions" /> </template> <script> import MySelect from './MySelect' export default { data () => { return { selected: null, myOptions: [] } }, components: { MySelect } } </script>
And this would render a
QSelect
withfilter
set to true andfilter-placeholder
set to “select”.But if we wanted to set other properties on the internal
QSelect
we would have to define all of them on our own component und pass them toQSelect
.Pinpad component
Another user also asked how to build a custom component which is again a good example on how to use composition to create new components. He wanted to build a
Pinpad
component.We can simply achieve that by using
QBtn
s which are aligned on a flexbox grid:<template> <div> <div v-for="row in 3" class="row justify-center"> <div v-for="col in 3" class="col-auto"> <q-btn @click="handleClick((row-1)*3 + col)"> {{ (row-1)*3 + col }} </q-btn> </div> </div> </div> </template> <script> import { QBtn } from 'quasar' export default { data () { return { pin: '' } }, methods: { handleClick (digit) { this.pin += digit } }, components: { QBtn } } </script>
This gives us a whole new component by using existing components.
We could now even extend this component with other Quasar components like anQInput
to allow for manually entered pins.How to style custom components
Styling custom components is easy, just declare your styles in the
<style></style>
section of your component.But what if we want our styles to be consistent and be able to change them in a single place?
Quasar uses Stylus variables for that purpose.
If you want to use some of the variables in your own components you can just import them like so:<template>...</tempalte> <script>...</script> <style lang="stylus"> @import '~src/themes/app.variables.styl' </style>
Now you can use all the variables like colors or breakpoints in your own component.
Todos
- Explain slots
- Explain component communication
- Static components
- Directives
- Quasar Utils
-
awesome. thank you @a47ae !
-
@a47ae That would make for an awesome doc page! Want to create one that you can edit?
-
@a47ae said in [How To] Building components with Quasar:
Most of Quasars components are also distributed as single file components, you can check out their source here.
Link is broken … no major just thought I mention.
-
I tried to follow this guide but I think the quasar API has changed so much that it doesn’t work anymore.
I managed to make a wrapped q-select that works and might be helpful if you are wrapping quasar (or Vue) components
I wanted a q-select that would have basic filtering enabled. e.g. It could replace q-select and not have to setup extra code for filtering.
<ex-select v-model.number="product.supplierId" label="Supplier" :options="supplierListSorted" option-value="supplierId" option-label="companyName" emit-value map-options filled />
Vue components can use v-bind="$attrs" to bind any attributes that do not match the component params e.g. if you use <ex-select filled … then the filled attribute will be passed down to the component automatically.
The same applies for v-on="$listeners" where any event handlers will be passed down to the component. v-model binds the input event so this is why it works without any extra code.
The props “options”, “optionValue” and “optionLabel” are declared because I want to use them in the component code. So if you need to work with an attribute then you need to declare it and add it manually to the wrapped component.
There is code to work with lists that have a key value and display value e.g. supplierId and supplierName. If optionLabel is set, then filter on that property on the object. Otherwise assume the array just contains string values and filter directly.
Here is the full ExSelect.vue code
<template> <q-select v-bind="$attrs" use-input fill-input hide-selected input-debounce="0" :options="selectOptions" :option-label="optionLabel" :option-value="optionValue" v-on="$listeners" @filter="handleFilter"> <template v-slot:no-option> <q-item> <q-item-section class="text-grey"> No results </q-item-section> </q-item> </template> </q-select> </template> <script> export default { name: "ExSelect", // eslint-disable-next-line props: ["options", "optionValue", "optionLabel"], data() { return { selectOptions: null }; }, updated() { if (!this.selectOptions) { console.log("[ExSelect] updated options is ", this.options); // keep a copy of the original options list this.selectOptions = this.options.slice(); } }, methods: { handleFilter(value, doneFunc) { console.log("[ExSelect] handleFilter", value); if (value === "") { doneFunc(() => { // reset the list console.log("[ExSelect] handleFilter reset list", value); this.selectOptions = this.options.slice(); }); return; } doneFunc(() => { const input = value.toLowerCase(); console.log("[ExSelect] handleFilter filtering", value); this.selectOptions = this.options.filter((item) => { if (this.optionLabel) { // search the display property return item[this.optionLabel].toLowerCase().indexOf(input) > -1; } return item.toLowerCase().indexOf(input) > -1; }); }); } } }; </script>
I’m still learning Vue so there are probably better ways to do some things but might help out someone trying to do a similar thing.
-
@minimalicious great work, just to add, don’t need to declare the props that’s accepted by q-select. it’s accessible with
$attrs
. ie.props: ["options", "optionValue", "optionLabel"]
, you can access withthis.$attrs.options
,this.$attrs['option-value']
,this.$attrs['option-label']
respectively. -
Oh thanks, that is a handy shortcut to the attributes.