Skip to content

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"
  };
},