Repository
What Will I Learn?
This tutorial can be considered as a small part of Proof of Concept of my previous post that mention about simulating hardware using webcomponent. In this tutorials you will learn:
- How to build Vue components
- How to utilize mathjs for applying UoM (Unit of Measurement) and automatically doing CoU (Conversion of Unit)
- How to build webcomponent
Requirements
- Basic undersatnding of SVG
- Basic understanding of HTML and Typescript
- Basic understanding of Vuejs and Class-Style Vue Components
- Install vue-cli and yarn
- Install some code-editor/IDE (strongly suggest to use VSCode + Vetur)
Difficulty
- Intermediate
Tutorial Contents
In this tutorials, we will create Vue component based on real hardware LED and how it works.
Preparation [commit #2]
First, let's create vue project with name vue-hardware-led
vue create vue-hardware-led
It will ask you step-by-step how would you like to configure your project. For this tutorials fill it with
? Please pick a preset:
default (babel, eslint)
❯ Manually select features
? Check the features needed for your project:
◉ TypeScript
◯ Progressive Web App (PWA) Support
◯ Router
◯ Vuex
◯ CSS Pre-processors
◉ Linter / Formatter
◯ Unit Testing
◯ E2E Testing
? Use class-style component syntax? (Y/n) Y
? Use Babel alongside TypeScript for auto-detected polyfills? (Y/n) Y
? Pick a linter / formatter config: (Use arrow keys)
❯ TSLint
ESLint with error prevention only
ESLint + Airbnb config
ESLint + Standard config
ESLint + Prettier
? Pick additional lint features:
◉ Lint on save
◉ Lint and fix on commit
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? (Use arrow keys)
❯ In dedicated config files
In package.json
Now you can add mathjs to your project
cd vue-hardware-led
yarn add mathjs
yarn add @types/mathjs -D
To avoid typescript error, you need to add shim-math.d.ts until @types/mathjs PR #25860 is merged
Finally, let's run it and keep it open
yarn serve --open
any change in your code is immediately compiled so you can see the change by just saving the file.
Add Led.vue
[commit #3]
After you have setup your project, you can begin to build Led component. First, let's draw Led like shape using SVG (you can use any vector graphics editor or type it manually).
In src/components/Led.vue
<template>
<svg width="108" height="232" viewBox="85 -12 108 232">
<path id="bulb" fill="red" fill-opacity="50" stroke="red" stroke-width="5" d="M 94.8 89 L 94.8 27.9 C 94.8 5.9 114.4 -12 138.5 -12 C 162.6 -12 182.3 5.9 182.3 27.9 L 182.3 89 L 94.8 89 Z " />
<rect id="cathode" width="14" height="82" x="162" y="93" fill="orange" />
<rect id="anode" width="14" height="126" x="103" y="93" fill="orange" />
<rect id="flat-surface" width="108" height="13" x="85" y="82" fill="crimson" />
</svg>
</template>
Then you can test it by registering to src/App.vue
.
In <script>
blocks, import and register Led.vue
import Led from '@/components/Led.vue'
@Component({ components: { Led } })
export default class App extends Vue {}
Now in <template>
blocks, you can add <led />
component
<template>
<div id="app">
<Led />
</div>
</template>
Here is how SVG element in Led.vue
build manually (without vector graphics editor)
Define the usage [commit #4]
Now we have basic Led component based on SVG drawing. Let's break down how LED in the real world works. Brightness in LED depends on power (watt) you feed to it notated as .
In summary
= Total Power Consumption in watt (W)
because
= electric current of the input in ampere (A)
= voltage of the input in volt (V)
then
Now we can define the usage of our component to be like
<Led size="3mm" scale="10px/mm"
input-voltage="3.3V" input-current="50mA"
max-power="43mW" />
And register the vue props
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'
@Component
export default class Led extends Vue {
@Prop() scale!: string
@Prop() size!: string
@Prop() inputVoltage!: string
@Prop() inputCurrent!: string
@Prop() maxPower!: string
}
</script>
Now, let's identify attributes of the SVG element that is dynamic (the value depends on the vue props).
<svg :height="height" viewBox="85 -12 108 232">
<path id="bulb" fill="red" :fill-opacity="brightness" stroke="red" stroke-width="5" d="M 94.8 89 L 94.8 27.9 C 94.8 5.9 114.4 -12 138.5 -12 C 162.6 -12 182.3 5.9 182.3 27.9 L 182.3 89 L 94.8 89 Z " />
then bind it to the computed properties
get height(): number { return 232 }
get brightness(): number { return 50 }
Here we see that attributes fill-opacity
in <path id="bulb">
tag depend on computed properties brightness
, hence <path id="bulb" :fill-opacity="brightness"
. Also, attributes height
in <svg>
tag depend on computed properties height
, hence <svg :height="height"
. Notice that I remove attributes width
in <svg>
tag because to make the component automatically maintain the aspect ratio.
Apply Unit Measurement check using mathjs [commit #4]
After we define properties of our component, we can begin to add validation check based on Unit of Measurement. To do this, we will utilize mathjs so when we provide value to the props like <Led input-voltage="43cm">
it will cause an error.
Before doing that first let's create unit measurement called px
or pixel
(because mathjs
not defined it by default)
import { unit, createUnit } from 'mathjs'
createUnit('px', { aliases: ['pixel', 'pixels', 'dot'] })
then we need to create wrapper function for props validation to make the implementation more easier
// baseUnit('volt')('43cm'); // will return false
const baseUnit = (unitName: string) => (value: string) => unit(value).equalBase(unit(unitName))
now we ready to apply validation check for Unit of Measurement
@Prop({ validator: baseUnit('pixel/meter') }) scale!: string
@Prop({ validator: baseUnit('meter') }) size!: string
@Prop({ validator: baseUnit('watt') }) maxPower!: string
@Prop({ validator: baseUnit('volt') }) inputVoltage!: string
@Prop({ validator: baseUnit('ampere') }) inputCurrent!: string
And now when we assign the wrong Unit, it will produce an error
Calculate brightness and size of LED [commit #5]
Before doing a calculation that use automatic Unit Conversion provided by mathjs
, we can create a wrapper function to make it less to write.
Create a wrapper function calculate(expr: string)
to calculate a mathematical expression
// calculate('1m / 100cm') will return 1
const calculate = (expression: string) => compile(expression).eval()
// calculate('1px/cm / 100cm').toNumber('pixel') will return 100
and don't forget to import function compile
import { unit, createUnit, compile } from 'mathjs'
After that, we can do some calculation.
Because
and luckily attributes fill-opacity
range value are from 0
to 1
, we can write the computed properties for brightness
get brightness(): number { return calculate(`${this.inputVoltage}*${this.inputCurrent} / ${this.maxPower}`) }
If we take a closer look, the dimension SVG drawing of the LED is more like
where a height of the led bulb is 30% of the height of drawing. If we want size
props to reflect the size of the bulb we can calculate it by
= svg led height in pixel (px)
= bulb size in milimeter (mm)
= scaling ratio in pixel per milimeter (px/mm)
Now, we can write the computed properties of height
get height(): number { return calculate(`1.3 * ${this.size}*${this.scale}`).toNumber('pixel') }
We can see that opacity of the led bulb are depend on input voltage/current and also the size of the LED can be adjusted.
Add default value [commit #6]
So far we have define the basic need of LED component. However, if we defined the LED component and not assign all the properties, for example
<Led input-voltage="2.3V" input-current="13mA" />
will give you an error
To avoid that, we can define the default value of all Led props
@Prop({ validator: baseUnit('pixel/meter'), default: '10px/mm' }) scale!: string
@Prop({ validator: baseUnit('meter'), default: '3mm' }) size!: string
@Prop({ validator: baseUnit('watt'), default: '43mW' }) maxPower!: string
@Prop({ validator: baseUnit('volt'), default: '0V' }) inputVoltage!: string
@Prop({ validator: baseUnit('ampere'), default: '0mA' }) inputCurrent!: string
notice that by default the Led is off because default value for input-voltage
and input-current
is 0
. Now the component will work if all or one of the props is not assigned with value.
Make LED broken when over voltage/current [commit #7]
In real world, if we short-circuited the LED (neither by over voltage or over current) then the LED will broke. This is happening because input power is passed the total consumption power that specific LED can handle.
In summary
in short, the brightness value is not allowed to be more than 100%. Now, in brightness
computed property, we need to add a condition where the Led should be broken
broken: boolean = false
get brightness(): number {
const result = calculate(`${this.inputVoltage}*${this.inputCurrent} / ${this.maxPower}`)
this.broken = (result > 1.00)
return this.broken ? 0 : result
}
Notice that I also add vue member data broken
so that we can display text "BROKEN" when we short-circuit it. To apply it
<svg :height="height" viewBox="85 -12 108 232">
<text v-if="broken" x="103" y="45">BROKEN</text>
Now our Led component is more like LED in the real world
Build Led.vue
as webcomponent
As a bonus, vue-cli have the ability to build .vue
component into webcomponent. This makes it easier to share and demonstrate your work. To build our Led.vue
as a webcomponent execute
yarn build --target wc --name hardware-led src/components/Led.vue
Noted that you need to give the webcomponent a name (e.g --name hardware-led
). The name must contain 2 or more word separated with a hyphen(-
). After you build it as a webcomponent, you will see folder dist
in your root project. There is also dist/demo.html
that shows how to use the webcomponent you build.
You can open dist/demo.html
in the browser, edit it, refresh the browser and see the change.
<title>hardware-led demo</title>
<script src="https://unpkg.com/vue"></script>
<script src="./hw-led.js"></script>
<hardware-led input-voltage="1.3V" input-current="13mA"></hardware-led>