Vue

Build for Cloud Phone with Vue

Vue.js is a progressive JavaScript framework for building web user interfaces. It is reactive, compiler-optimized, and originally developed by Evan You in 2014. Learn how to build a Cloud Phone widget using Vue, or skip ahead to the finished sample code and live demo hosted on GitHub Pages.

Need a primer on Cloud Phone?

Getting started with Cloud Phone

Learn how Cloud Phone brings compelling web apps to the next billion users

Setup

Prerequisites

Dependency Versions

This sample was developed using the following libraries and frameworks.

DependencyVersion
Nodev20.11.0 (LTS)
npm10.9.0
Vue3.5.13
Vue Router4.5.0
vue-i18n11.0.1
Vite6

Vue CLI Setup

create-vue is the official project scaffolding tool for Vue.js. Run the command below to get started.

Scaffolding with Vue CLI
npm create vue@latest

Follow the prompts to create a new Vue project then use npm (or your preferred package manager) to install dependencies into the node_modules/ directory.

Vue.js - The Progressive JavaScript Framework

✔ Package name: … cloudphone-vue-sample
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit Testing? … No / Yes
✔ Add an End-to-End Testing Solution? › No
✔ Add ESLint for code quality? › Yes
✔ Add Prettier for code formatting? … No / Yes

Scaffolding project in ~/cloudphone-vue-sample...

Done. Now run:

  npm install
  npm run format
  npm run dev

Once complete, your project setup will look something like this. It includes helpful links as well as toggles for Vue.js Developer Tools and the Component Inspector.

Screenshot of Vue.js scaffold project

The command npm run dev is aliased in the scripts section of the package.json file to run vite. This command starts a local HTTP server on port 5173 (by default), available at http://localhost:5173/

package.json
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "lint": "eslint . --fix",
    "format": "prettier --write src/"
  }

Development

Routing

Vue Router is the official router for Vue.js. Install it using npm if you didn’t already during project scaffolding.

npm install vue-router@4

To prepare for GitHub Pages deployment, update the base path configuration in vite.config.js. GitHub Pages URLs take the following format: https://<USERNAME>.github.io/<REPO-NAME>/. Here <REPO-NAME> is cloudphone-vue-sample, the root directory for this project.

vite.config.js
import process from 'node:process'
import { defineConfig } from 'vite'

// https://vite.dev/config/
export default defineConfig({
  base: process.env.NODE_ENV === 'production' ? '/cloudphone-vue-sample' : '/',
})

Internationalization

vue-i18n is a component-oriented package for handling internationalization (abbreviated i18n) in Vue.js apps. vue-i18n supports Vue 3 and single file components. Install vue-i18n with npm:

npm install vue-i18n@11

For this demo, store translations as JSON files located at src/locales/<language_code>.json. Define a configuration like the one below and register the i18n plugin.

main.js
import { createApp } from 'vue'
import { createI18n } from 'vue-i18n'
import en from './locales/en.json'
import es from './locales/es.json'

const i18n = createI18n({
  legacy: false,
  locale: 'en',
  fallbackLocale: 'en',
  messages: {
    en: en,
    es: es,
  },
})

const app = createApp(App)
app.use(i18n)
app.mount('#app')

As an alternative, use unplugin-vue-i18n available for Vite and Webpack.

The demo includes two globally-scoped translations–English and Spanish–statically bundled with the application. It uses Composition Mode to take advantage of Vue 3’s Composition API, instead of the deprecated Legacy API.

Import useI18n in setup to return a Composer instance that provides access to translations via the function t.

AboutView.vue
<script setup>
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>

<template>
  <section id="app">
    <h2>{{ t('about') }}</h2>
    <p>{{ t('about.description') }}</p>
  </section>
</template>

For larger applications or those with frequently-changing copy, consider lazy-loading locales to dynamically fetch only the translations needed, instead of bundling all translations into your app.

Automatically set the default language based on the navigator.language.

App.vue
<script setup>
import { useI18n } from 'vue-i18n'
const { t, locale, availableLocales } = useI18n()

const defaultLocale = 'en'
const savedLanguage = localStorage.getItem('lang')

// This is a very naive approach. Consider using a library
// that doesn't drop country codes
function getDefaultLanguage(languageSet) {
  const preferredLanguage = (navigator.language || '').split('-')[0].toLowerCase()
  if (languageSet.has(preferredLanguage)) return preferredLanguage
  return defaultLocale
}

// Restore saved locale, or set a reasonable default
if (!locale.value || savedLanguage) {
  locale.value = savedLanguage || getDefaultLanguage(new Set(availableLocales))
}
</script>

Keyboard Navigation

Cloud Phones have directional pads (D-pads) to support keyboard navigation. Unlike desktop that uses the tabindex property and Tab (⇥) key to the next focusable element, Cloud Phone uses Arrow keys (↑↓←→).

OptionsMenu Screenshot
OptionsMenu
Settings Page Screenshot
Settings: Language Selection

Autofocus on the first element in a page, list, or grid to set the user’s bearings. Use the autofocus attribute to take advantage of the browser’s native autofocus capabilities.

<template>
  <button :autofocus="true">
    {{ t('clickme') }}
  </button>
</template>

Alternatively, set up a Watcher to autofocus as a “side effect” of DOM element creation.

views/SettingsView.vue
<script>
import { ref, watch } from 'vue'
const autofocus = (element) => element?.focus();

const langButton = ref(null)

// Auto-focus the language button
watch(langButton, (buttonRef) => autoFocus(buttonRef?.value))
</script>

<template>
  <button ref="langButton">
    {{ t('language') }}
  </button>
</template>

Extend the Watcher approach to autofocus on the first item in a list.

components/OptionsMenu.vue
<script setup>
import { ref, watch } from 'vue'

// i.e. https://github.com/KittyGiraudel/focusable-selectors/blob/main/index.js
const FOCUSABLE_CSS_SELECTOR = `a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), *[tabindex], *[contenteditable]`

function findFirstFocusable(container) {
  return container.querySelector(FOCUSABLE_CSS_SELECTOR)
}

const autoFocusFirstFocusable = (element) => {
  if (element) {
    findFirstFocusable(element)?.focus()
  }
}

const menuRef = ref(null)
const focusedIndex = ref(0)

// Autofocus first element in list
watch(menuRef, (newRef) => autoFocusFirstFocusable(newRef?.value))
</script>

<template>
  <menu ref="menuRef">
    <component
      v-for="(child, index) in slots.default?.() || []"
      :key="index"
      :is="child"
      :class="{ focused: index === focusedIndex }"
    />
  </menu>
</template>

The OptionsMenu component can have links as children, to create a navigation menu triggered by the Left Soft Key (LSK)

views/HomeView.vue
<template>
  <OptionsMenu>
    <RouterLink to="/about">{{ t('about') }}</RouterLink>
    <RouterLink to="/settings">{{ t('settings') }}</RouterLink>
    <a href="https://www.cloudfone.com/dev-privacy">
      {{ t('privacy') }}
    </a>
  </OptionsMenu>
</template>

The snippet above creates an options menu with three links. The first two are local routes to the About and Settings page, while the third is an external link to another website.

Hosting on GitHub Pages

GitHub Pages provides free hosting for public, open-source repositories. This demo uses the github-pages-deploy-action GitHub Action to build and deploy HTML, CSS, and JS. The action is defined in pages.yml.

Set the base path configuration in vite.config.js to match your repository name. Use a two-step GitHub Action to build and deploy the SPA on pushes to the main branch.

pages.yml
name: Deploy Pages
on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: "Checkout 🛎️"
        uses: actions/checkout@v2
      - name: "Setup Node 🚧"
        uses: actions/setup-node@v4
        with:
          node-version: ">=20.11.0"
          cache: npm
      - name: "Dependencies 🏗️"
        run: |
          npm ci
      - name: "Build 🔧"
        run: |
          npm run build
        env:
          BASE_PATH: "/${{ github.event.repository.name }}"
      - name: Upload Artifacts
        uses: actions/upload-pages-artifact@v3
        with:
          path: "dist/"

  deploy:
    needs: build
    runs-on: ubuntu-latest
    permissions:
      pages: write
      id-token: write
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: "Deploy 🚀"
        id: deployment
        uses: actions/deploy-pages@v4

Projects scaffolded using create-vue build in the dist/ directory by default. Ensure this matches the path provided to the upload-pages-artifact GitHub Action.

Wrap Up

Tips

Vue is a powerful yet lightweight framework for developing reactive web apps. Take advantage of the mature Vue ecosystem with the following tips.

Dynamic Imports

Lazy-load routes to facilitate code splitting. By replacing static route imports with dynamic imports, the bundler is able to split each route’s components into separate chunks. With dynamic imports, the browser only fetches route code when entering the page for the first time, then use the cached version on subsequent visits.

index.js
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView,
    },
    {
      path: '/about',
      name: 'about',
      component: () => import('../views/AboutView.vue'),
    },
  ],
})

export default router

Component Library

Build faster with robust component libraries like PrimeVue, Vuetify, or Naive UI. Many libraries are keyboard-accessible by default, making D-pad navigation a breeze.

Standardized Formatting

create-vue provides several configurations like using Prettier for standardized, opinionated code formatting. Consistent code formatting can reduce cognitive strain so you can focus on building your app.

Testing

Develop reliably with testing utilities like Vitest and Playwright. Target Chromium in Playwright to most closely match Cloud Phone.

Next Steps

Register for the Cloud Phone Developer Program to try the live demo on the Cloud Phone Simulator or a physical device. Fork us on GitHub to start coding your first Cloud Phone widget using Vue and set up a custom domain name to point to GitHub Pages.

Contact Us

Reach out when you are ready to publish your first Cloud Phone widget

Get Support