Vue JS @ MindDoc
The progressive road to online therapy
Darío Blanco Iturriaga (CTO Schoen Digital Labs)
April 26, 2018
Life is full of
uncertainty
▪ Do we implement the corporate website on our own?
minddoc.de
▪ Angular? React? Vue?
▪ JavaScript? TypeScript?
2
“
“In his writings, a wise Italian
says that the better is the enemy of good” -
Voltaire
3
Progressive
Developing gradually or in stages
4
5
6A simple “Hello World” is always a good sign
<!-- HTML -->
<script src="https://unpkg.com/vue"></script>
<div id="app">
<p>{{ message }}</p>
</div>
// JavaScript
new Vue({
el: '#app',
data: {
message: 'Hello Vue!',
},
});
Component based development
$props - Pass data to child with properties
$emit() - Send data to parent with events
7
App
SiteHeader
Locations
router-view
Navigation
MapClinics
Fragment
A B C
SiteHeader Locations
Navigation
App
MapClinics Fragment A B C
8Single File Components
HTML template
JavaScript code
SCSS styles
<!-- App.vue -->
<template>
<div id="app">
<p>{{ message }}</p>
</div>
</template>
<script>
export default {
name: 'App',
data() { return { message: 'Hello Vue!' }; }
};
</script>
<style lang="scss">
@import "~bootstrap/scss/bootstrap";
@import "~@minddoc/mdbootstrap/scss/mdb.scss";
@import "assets/scss/_typography.scss";
</style>
9Separation of Concerns
<!-- MyComponent.vue -->
<template>
<div class="container">
<div class="row justify-content-center">
<div v-if="isAuthenticated" class="col-12">
<h1>Hello {{ username }}</h1>
</div>
</div>
</div>
</template>
<script src="./MyComponent.js"></script>
<style src="./MyComponent.scss"></style>
External files
Components
Look like classes
▪ data
▪ props
▪ methods
▪ computed
Types and definitions
JavaScript boilerplate code
Worse readability
Share states
Passing data between many of
them became a nightmare
10
11It could be a class ...
Our methods
Vue lifecycle hook
“Private” attributes
External provided attributes
export default {
name: 'Video',
props: {
username: String,
roomType: {
type: String,
default: 'peer-to-peer',
},
},
data() {
return {
videoRoom: null,
roomName: `video-${this.username}`,
}
},
mounted() { },
methods: {
connectToTwilio() { },
joinRoom() { },
leaveRoom() { },
}
}
12… write “class based” components
import Vue from 'vue';
import Component from 'vue-class-component';
@Component({
props: {
username: String,
roomType: {
type: String,
default: 'peer-to-peer',
},
}
})
export default class Video extends Vue {
// initial data
videoRoom = null;
roomName = `video-${this.username}`;
// lifecycle hook
mounted() { };
// methods
connectToTwilio() { }
joinRoom() { }
leaveRoom() { }
}
Reserved method name
Our methods
Data is implicit
“Old style” can be set in decorator (ES6)
13
Types!
Full Vue support (since 2.5.0+)
Easy to set with vue-cli (3.0+)
Experimental decorators
14
import Vue from 'vue';
import { Component, Prop, Vue } from 'vue-property-decorator';
import { Room } from 'twilio-video';
@Component
export default class Video extends Vue {
@Prop()
username: string;
@Prop({ default: 'peer-to-peer' })
roomType: string;
videoRoom: Room = null;
roomName = `video-${this.username}`;
mounted(): void { };
connectToTwilio(): void { }
joinRoom(): boolean { }
leaveRoom(): boolean { }
}
15TypeScript implementation
Prop decorator
defines external
attributes
Types are optional
16IntelliSense
Useful even if you create your own types from a external library
17IntelliSense
Gives Vue type declarations
18IntelliSense
Shows your own documentation
19
Data flow hell
Depend on same state
▪ e.g Video room details
Mutate the same state
▪ e.g Join room API call
20
Pass props
Emit events
State “singleton”
Extract shared state
Accessible from every component
21
🤔
Vuex
Video “store”
▪ Reactive to state changes
▪ Commit mutations are required to
update the state
▪ Strongly typed
22
23Types
// store/video/types.ts
/* The Twilio video room details */
export interface RoomDetails {
uniqueName: string;
sid: string;
}
/* The Twilio video room credentials */
export interface RoomCredentials {
jwt: string;
username: string;
role: Role;
}
/* The Vuex video state */
export interface VideoState {
room: RoomDetails | null;
token: RoomCredentials | null;
error: Error | null;
}
24
// store/video/state.ts
import { VideoState } from '@/store/video/types';
/* The default video state */
const state: VideoState = {
room: null,
token: null,
error: null,
};
export default state;
State
25Actions
// store/video/actions.ts
import { ActionTree } from 'vuex';
import { postRoom } from '@/api/video';
import { RootState } from '@/store/types';
import { VideoState } from '@/store/video/types';
/* The video action tree */
const actions: ActionTree<VideoState, RootState> = {
async joinRoom({ commit }, patientUuid: UUID): Promise<void> {
try {
const roomResponse = await postRoom(patientUuid);
commit('joinRoomSuccess', roomResponse);
} catch (err) {
commit('joinRoomError', err);
}
},
};
export default actions;
26Mutations
// store/video/mutations.ts
import { MutationTree } from 'vuex';
import { VideoRoomResponse } from '@/api/video';
import { VideoState } from '@/store/video/types';
/* The video mutation tree */
const mutations: MutationTree<VideoState> = {
joinRoomSuccess(state, payload: VideoRoomResponse) {
state.room = { uniqueName: payload.roomName, sid: payload.roomSid };
state.token = {
jwt: payload.roomJwt,
username: payload.username,
role: payload.role,
};
state.error = null;
},
joinRoomError(state, error: Error) {
state.error = error;
},
};
export default mutations;
27Getters
// store/video/getters.ts
import jwtDecode from 'jwt-decode';
import { GetterTree } from 'vuex';
import { RootState } from '@/store/types';
import { VideoState } from '@/store/video/types';
/* The video getter tree */
const getters: GetterTree<VideoState, RootState> = {
decodedJwt(state): string | null {
const { token } = state;
return token ? jwtDecode(token.jwt) : null;
},
roomName(state): string | null {
const { room } = state;
return room ? room.uniqueName : null;
},
};
export default getters;
28
It scales!
Multiple stores
Persistence plugin
Testing
29
100% coverage?
Requires a lot of effort and discipline to achieve. It is worth it.
Could be bullshit. Use coverage skip responsibly.
/* istanbul ignore <word>[non-word] [optional-docs] */
30
31
32
Test Driven Development (TDD)
Mount VS Shallow
Mount and render components into a wrapper
▪ mount() renders the whole tree
▪ shallow() stubs children
33
Thanks!
Any questions?
You can find me at
▪ dblanco@minddoc.de
▪ Twitter: @darioblanco
34
Join our new TypeScript group!
▪ meetup.com/TypeScript-Munich/

Vue JS @ MindDoc. The progressive road to online therapy

  • 1.
    Vue JS @MindDoc The progressive road to online therapy Darío Blanco Iturriaga (CTO Schoen Digital Labs) April 26, 2018
  • 2.
    Life is fullof uncertainty ▪ Do we implement the corporate website on our own? minddoc.de ▪ Angular? React? Vue? ▪ JavaScript? TypeScript? 2
  • 3.
    “ “In his writings,a wise Italian says that the better is the enemy of good” - Voltaire 3
  • 4.
  • 5.
  • 6.
    6A simple “HelloWorld” is always a good sign <!-- HTML --> <script src="https://unpkg.com/vue"></script> <div id="app"> <p>{{ message }}</p> </div> // JavaScript new Vue({ el: '#app', data: { message: 'Hello Vue!', }, });
  • 7.
    Component based development $props- Pass data to child with properties $emit() - Send data to parent with events 7 App SiteHeader Locations router-view Navigation MapClinics Fragment A B C SiteHeader Locations Navigation App MapClinics Fragment A B C
  • 8.
    8Single File Components HTMLtemplate JavaScript code SCSS styles <!-- App.vue --> <template> <div id="app"> <p>{{ message }}</p> </div> </template> <script> export default { name: 'App', data() { return { message: 'Hello Vue!' }; } }; </script> <style lang="scss"> @import "~bootstrap/scss/bootstrap"; @import "~@minddoc/mdbootstrap/scss/mdb.scss"; @import "assets/scss/_typography.scss"; </style>
  • 9.
    9Separation of Concerns <!--MyComponent.vue --> <template> <div class="container"> <div class="row justify-content-center"> <div v-if="isAuthenticated" class="col-12"> <h1>Hello {{ username }}</h1> </div> </div> </div> </template> <script src="./MyComponent.js"></script> <style src="./MyComponent.scss"></style> External files
  • 10.
    Components Look like classes ▪data ▪ props ▪ methods ▪ computed Types and definitions JavaScript boilerplate code Worse readability Share states Passing data between many of them became a nightmare 10
  • 11.
    11It could bea class ... Our methods Vue lifecycle hook “Private” attributes External provided attributes export default { name: 'Video', props: { username: String, roomType: { type: String, default: 'peer-to-peer', }, }, data() { return { videoRoom: null, roomName: `video-${this.username}`, } }, mounted() { }, methods: { connectToTwilio() { }, joinRoom() { }, leaveRoom() { }, } }
  • 12.
    12… write “classbased” components import Vue from 'vue'; import Component from 'vue-class-component'; @Component({ props: { username: String, roomType: { type: String, default: 'peer-to-peer', }, } }) export default class Video extends Vue { // initial data videoRoom = null; roomName = `video-${this.username}`; // lifecycle hook mounted() { }; // methods connectToTwilio() { } joinRoom() { } leaveRoom() { } } Reserved method name Our methods Data is implicit “Old style” can be set in decorator (ES6)
  • 13.
  • 14.
    Types! Full Vue support(since 2.5.0+) Easy to set with vue-cli (3.0+) Experimental decorators 14
  • 15.
    import Vue from'vue'; import { Component, Prop, Vue } from 'vue-property-decorator'; import { Room } from 'twilio-video'; @Component export default class Video extends Vue { @Prop() username: string; @Prop({ default: 'peer-to-peer' }) roomType: string; videoRoom: Room = null; roomName = `video-${this.username}`; mounted(): void { }; connectToTwilio(): void { } joinRoom(): boolean { } leaveRoom(): boolean { } } 15TypeScript implementation Prop decorator defines external attributes Types are optional
  • 16.
    16IntelliSense Useful even ifyou create your own types from a external library
  • 17.
  • 18.
  • 19.
  • 20.
    Data flow hell Dependon same state ▪ e.g Video room details Mutate the same state ▪ e.g Join room API call 20 Pass props Emit events
  • 21.
    State “singleton” Extract sharedstate Accessible from every component 21 🤔
  • 22.
    Vuex Video “store” ▪ Reactiveto state changes ▪ Commit mutations are required to update the state ▪ Strongly typed 22
  • 23.
    23Types // store/video/types.ts /* TheTwilio video room details */ export interface RoomDetails { uniqueName: string; sid: string; } /* The Twilio video room credentials */ export interface RoomCredentials { jwt: string; username: string; role: Role; } /* The Vuex video state */ export interface VideoState { room: RoomDetails | null; token: RoomCredentials | null; error: Error | null; }
  • 24.
    24 // store/video/state.ts import {VideoState } from '@/store/video/types'; /* The default video state */ const state: VideoState = { room: null, token: null, error: null, }; export default state; State
  • 25.
    25Actions // store/video/actions.ts import {ActionTree } from 'vuex'; import { postRoom } from '@/api/video'; import { RootState } from '@/store/types'; import { VideoState } from '@/store/video/types'; /* The video action tree */ const actions: ActionTree<VideoState, RootState> = { async joinRoom({ commit }, patientUuid: UUID): Promise<void> { try { const roomResponse = await postRoom(patientUuid); commit('joinRoomSuccess', roomResponse); } catch (err) { commit('joinRoomError', err); } }, }; export default actions;
  • 26.
    26Mutations // store/video/mutations.ts import {MutationTree } from 'vuex'; import { VideoRoomResponse } from '@/api/video'; import { VideoState } from '@/store/video/types'; /* The video mutation tree */ const mutations: MutationTree<VideoState> = { joinRoomSuccess(state, payload: VideoRoomResponse) { state.room = { uniqueName: payload.roomName, sid: payload.roomSid }; state.token = { jwt: payload.roomJwt, username: payload.username, role: payload.role, }; state.error = null; }, joinRoomError(state, error: Error) { state.error = error; }, }; export default mutations;
  • 27.
    27Getters // store/video/getters.ts import jwtDecodefrom 'jwt-decode'; import { GetterTree } from 'vuex'; import { RootState } from '@/store/types'; import { VideoState } from '@/store/video/types'; /* The video getter tree */ const getters: GetterTree<VideoState, RootState> = { decodedJwt(state): string | null { const { token } = state; return token ? jwtDecode(token.jwt) : null; }, roomName(state): string | null { const { room } = state; return room ? room.uniqueName : null; }, }; export default getters;
  • 28.
  • 29.
  • 30.
    100% coverage? Requires alot of effort and discipline to achieve. It is worth it. Could be bullshit. Use coverage skip responsibly. /* istanbul ignore <word>[non-word] [optional-docs] */ 30
  • 31.
  • 32.
  • 33.
    Mount VS Shallow Mountand render components into a wrapper ▪ mount() renders the whole tree ▪ shallow() stubs children 33
  • 34.
    Thanks! Any questions? You canfind me at ▪ dblanco@minddoc.de ▪ Twitter: @darioblanco 34 Join our new TypeScript group! ▪ meetup.com/TypeScript-Munich/