Vue
What are we learning here?
- How to use a
Vue
component
You can of course create anytime a vue application and mount it in a component element.
Nevertheless, there is a component that mounts a vue application in its own container. This has
certain advantages that we will get to know in other chapters.
You can pass vue properties to the vue application with the options property props
. Anyway one vue
property will always be passed with the name component
. This is a component instance. Inside of
vue you can with this.component
always access all component features.
The basic vue file should always look like this:
<template>
<!-- Your template here -->
</template>
<script>
export default {
props: {
component: { type: Object, default: null },
},
// Using component example:
// import { toRaw } from "vue";
// toRaw(this.component).registerStage(...);
};
</script>
<style lang="postcss" scoped>
/* Your styles here */
</style>
We will now rebuild the widget component of our first application from the begining as a vue
application. Here is the code first. We explain the individual parts below.
The vue application in one file:
src/demo/apps/component-basics-vue/content.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89 | <template>
<h2>{{ myHeader }}</h2>
<div class="mt-2 card">
<div class="card-header">Loading Components</div>
<div class="card-body"><div ref="tools"></div></div>
</div>
<div class="mt-2 card">
<div class="card-header">Pass Options</div>
<div class="card-body">
Option 1: {{ myOption1 }}<br />
Option 2: {{ myOption2 }}
</div>
</div>
<div class="mt-2 card">
<div class="card-header">
Adding Content
<button class="btn btn-xs btn-success" @click="myParagraphs++">
<i class="fas fa-plus"></i>
</button>
<button
class="btn btn-xs btn-danger"
@click="myParagraphs = Math.max(0, myParagraphs - 1)"
>
<i class="fas fa-minus"></i>
</button>
</div>
<div class="card-body">
<p v-for="n in myParagraphs" :key="n">{{ myParagraphText }}</p>
</div>
</div>
<div class="arrow left"><i class="fas fa-chevron-circle-left"></i></div>
<div class="arrow right"><i class="fas fa-chevron-circle-right"></i></div>
<div class="arrow up"><i class="fas fa-chevron-circle-up"></i></div>
<div class="arrow down"><i class="fas fa-chevron-circle-down"></i></div>
</template>
<script>
import LoadingComponents from "../../components/loading-components";
import { toRaw } from "vue";
export default {
props: {
component: { type: Object, default: null },
myOption1: { type: String, default: "Default 1" },
myOption2: { type: String, default: "Default 2" }
},
data: function () {
return {
myHeader: "Component Basics (Vue)",
myParagraphs: 0,
myParagraphText: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr"
};
},
mounted: function () {
this.$el.parentElement.style.padding = "20px";
toRaw(this.component)
.registerStage(this.$refs.tools)
.load(LoadingComponents);
}
};
</script>
<style lang="postcss" scoped>
h2 {
text-decoration: underline dotted gray;
}
.arrow {
opacity: 0.5;
position: absolute;
color: red;
}
.left {
top: 50%;
transform: translate(0, -100%);
left: 0;
}
.right {
top: 50%;
transform: translate(0, -100%);
right: 0;
}
.up {
left: 50%;
transform: translate(-50%, 0);
top: 0;
}
.down {
left: 50%;
transform: translate(-50%, 0);
bottom: 0;
}
</style>
|
Now loading this vue application into the application main stage will not lead to the desired result
because the stage is not scrollable.
We would need a wigdet component in which we load the vue component and load this in turn into an
application stage. But don't panic! Luckily we don't have to write all that!
The Mixin
function can concatenate two components into one. The function expects two components.
The second component will be loaded into the component element of the first component. Furthermore
the function will take care to pass through all options and changes to the second component.
So we have just write our application that will load the vue component:
src/demo/apps/component-basics-vue/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 | import { Application, Mixin, Widget, Vue } from "@codecoupler/cc-ui";
import Content from "./content.vue";
export default class extends Application {
static defaults = {
"@codecoupler": {
panel: {
panelSize: "640 510",
headerTitle: "Test Vue Features"
}
}
};
async start() {
let widget = await this.stage.load(Mixin(Widget, Vue), {
vue: Content,
props: {
myOption1: "Value 1"
}
});
let interval = setInterval(() => {
if (!widget.isDestroyed) {
widget.options = {
props: {
myOption1: `Random Value ${Math.floor(Math.random() * 100)}`
}
};
} else {
clearInterval(interval);
}
}, 2000);
}
}
|
You can pass the following options to load the vue component:
vue
: The vue application class.
props
: Props to use when creating the vue application.
We load the new application in our root.js file (just by replacing the previous one):
src/root.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | import { Component, Flexbox } from "@codecoupler/cc-ui";
import LoadingComponents from "./demo/components/loading-components";
import TestApp from "./demo/apps/component-basics-vue";
export default class extends Component {
async start() {
await this.stage.load(Flexbox, {
root: {
type: "column",
content: [
{ type: "stage", id: "top", class: "card-header" },
{ type: "stage", id: "$", grow: true }
]
}
});
await this.getStage("top").load(LoadingComponents);
await this.stage.load(TestApp);
}
}
|
We will now compare the class of our origin widget with the vue application.
Widget (content.js)
|
Vue (content.vue)
|
Default values of class and options:
myHeader = "Component Basics";
static defaults = {
"@codecoupler": {
walkthrough: {
myOption1: "Default 1",
myOption2: "Default 2"
}
}
};
|
Default values are defined vue props and data properties:
props: {
myOption1: { type: String, default: "Default 1" },
myOption2: { type: String, default: "Default 2" }
},
data: function () {
return {
myHeader: "Component Basics (Vue)",
};
},
|
Inject Template with Variables:
let tpl = template({
myHeader: this.myHeader
});
this.element.replaceWith(tpl);
|
This is of course not necessary for vue applications as the template is already defined in
this file.
|
Set scoped class name to the widget element:
this.element.classList.add(style.widget);
Used CSS:
:local(.widget) {
padding: 20px;
& h2 {
...
}
& .arrow {
...
}
& .left {
...
}
& .right {
...
}
...
}
|
In the vue file all classnames are scoped already because of <style scoped> .
Therefore we do not need a local parent widget class and many child classes. We use the child
class names in the vue style section directly:
.arrow {
...
}
.left {
...
}
.right {
...
}
...
The style padding: 20px; we set inside of the vue function mounted :
mounted: function () {
this.$el.parentElement.style.padding = "20px";
}
|
Create a stage and load a widget:
let stage = this.registerStage(
this.element.querySelector("[data-role=stage]")
);
await stage.load(LoadingComponents);
|
Just replace this with toRaw(this.component) and the querySelector function
with vue references:
mounted: function () {
toRaw(this.component)
.registerStage(this.$refs.tools)
.load(LoadingComponents);
}
|
React on option changes:
watchEffect(() => {
this.element.querySelector("[data-role=option1").innerHTML =
this.#options.myOption1;
this.element.querySelector("[data-role=option2").innerHTML =
this.#options.myOption2;
});
|
This is of course not necessary for vue applications. The values will be updated
automatically:
<div class="card-body">
Option 1: {{ myOption1 }}<br />
Option 2: {{ myOption2 }}
</div>
|
Add handler for adding/removing text:
let contentEl = this.element.querySelector("[data-role=content]");
let addText = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr";
this.element
.querySelector("[data-role=text-plus]")
.addEventListener("click", () => {
contentEl.insertAdjacentHTML("beforeend", `<p>${addText}</p>`);
});
this.element
.querySelector("[data-role=text-minus]")
.addEventListener("click", () => {
Array.from(contentEl.getElementsByTagName("p")).at(-1)?.remove();
});
|
This will be done of course with reactive components of vue.
<div class="card-header">
Adding Content
<button class="btn btn-xs btn-success" @click="myParagraphs++">
<i class="fas fa-plus"></i>
</button>
<button
class="btn btn-xs btn-danger"
@click="myParagraphs = Math.max(0, myParagraphs - 1)"
>
<i class="fas fa-minus"></i>
</button>
</div>
<div class="card-body">
<p v-for="n in myParagraphs" :key="n">{{ myParagraphText }}</p>
</div>
data: function () {
return {
myParagraphs: 0,
myParagraphText: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr"
};
},
|