Categories
Quasar

Developing Vue Apps with the Quasar Library — Toolbar

Quasar is a popular Vue UI library for developing good looking Vue apps.

In this article, we’ll take a look at how to create Vue apps with the Quasar UI library.

Toolbar

We can add a toolbar with Quasar’sq-toolbar component.

For instance, we can write:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <q-layout view="lHh Lpr lFf" container style="height: 100vh;">
        <div class="q-pa-md">
          <q-toolbar class="text-primary">
            <q-btn flat round dense icon="menu"></q-btn>
            <q-toolbar-title>
              Toolbar
            </q-toolbar-title>
            <q-btn flat round dense icon="more_vert"></q-btn>
          </q-toolbar>
        </div>
      </q-layout>
    </div>
    <script>
      new Vue({
        el: "#q-app",
        data: {
          targetEl: "#target-img-1"
        }
      });
    </script>
  </body>
</html>

to add a q-btn and q-toolbar-title into the toolbar.

q-toolbar-title has the toolbar title.

We can add a q-avatar in our toolbar:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <q-layout view="lHh Lpr lFf" container style="height: 100vh;">
        <div class="q-pa-md">
          <q-toolbar class="text-primary">
            <q-btn flat round dense icon="menu"></q-btn>
            <q-avatar>
              <img src="https://cdn.quasar.dev/logo/svg/quasar-logo.svg" />
            </q-avatar>
            <q-toolbar-title>
              Toolbar
            </q-toolbar-title>
            <q-btn flat round dense icon="more_vert"></q-btn>
          </q-toolbar>
        </div>
      </q-layout>
    </div>
    <script>
      new Vue({
        el: "#q-app",
        data: {}
      });
    </script>
  </body>
</html>

We can also add a dropdown into the q-toolbar :

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <q-layout view="lHh Lpr lFf" container style="height: 100vh;">
        <div class="q-pa-md">
          <q-toolbar class="bg-primary text-white q-my-md shadow-2">
            <q-btn flat round dense icon="menu" class="q-mr-sm"></q-btn>
            <q-separator dark vertical inset></q-separator>
            <q-btn stretch flat label="Link"></q-btn>

<q-space></q-space>

<q-btn-dropdown stretch flat label="Dropdown">
              <q-list>
                <q-item-label header>Folders</q-item-label>
                <q-item
                  v-for="n in 3"
                  :key="`x.${n}`"
                  clickable
                  v-close-popup
                  tabindex="0"
                >
                  <q-item-section avatar>
                    <q-avatar
                      icon="folder"
                      color="secondary"
                      text-color="white"
                    >
                    </q-avatar>
                  </q-item-section>
                  <q-item-section>
                    <q-item-label>Photos</q-item-label>
                    <q-item-label caption>February 22, 2016</q-item-label>
                  </q-item-section>
                  <q-item-section side>
                    <q-icon name="info"></q-icon>
                  </q-item-section>
                </q-item>
                <q-separator inset spaced></q-separator>
                <q-item-label header>Files</q-item-label>
                <q-item
                  v-for="n in 3"
                  :key="`y.${n}`"
                  clickable
                  v-close-popup
                  tabindex="0"
                >
                  <q-item-section avatar>
                    <q-avatar
                      icon="assignment"
                      color="primary"
                      text-color="white"
                    >
                    </q-avatar>
                  </q-item-section>
                  <q-item-section>
                    <q-item-label>Vacation</q-item-label>
                    <q-item-label caption>February 22, 2016</q-item-label>
                  </q-item-section>
                  <q-item-section side>
                    <q-icon name="info"></q-icon>
                  </q-item-section>
                </q-item>
              </q-list>
            </q-btn-dropdown>
            <q-separator dark vertical></q-separator>
            <q-btn stretch flat label="Link"></q-btn>
          </q-toolbar>
        </div>
      </q-layout>
    </div>
    <script>
      new Vue({
        el: "#q-app",
        data: {}
      });
    </script>
  </body>
</html>

Conclusion

We can add a toolbar into our Vue app with Quasar’s q-toolbar component.

Categories
Vue

Add a Timeline to Your Vue.js App

A timeline is great for displaying chronological items. It looks good and it’s easy to read. We can easily add one to a Vue.js with the timeline-vuejs package, located at https://github.com/pablosirera/timeline-vuejs. It displays a vertical timeline with the year and the content of your choice.

In this article, we will build a journal entry app that lets users enter journal entries in and save them. They can also delete them as they wish. The saved entries will be displayed in a vertical timeline with the year on the left and the content on the right. To start, we will run the Vue CLI by running:

npx @vue/cli create journal-app

Vuex, and Babel.

Next we install some packages. We will use Axios for making HTTP requests, BootstrapVue for styling, Timeline-Vuejs for displaying the user’s diary in a timeline, VueFilterDateFormat for formatting dates in templates, and Vee-Validate for form validation. To install them, we run:

npm i axios bootstrap-vue timeline-vuejs vee-validate vue-filter-date-format

Now we can being building our app. Create a file called JournalForm.vue in the components folder and add:

<template>
  <ValidationObserver ref="observer" v-slot="{ invalid }">
    <b-form @submit.prevent="onSubmit" novalidate>
      <b-form-group label="Date">
        <ValidationProvider name="date" rules="required|date" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.date"
            required
            placeholder="Date"
            name="date"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>

      <b-form-group label="Diary">
        <ValidationProvider name="diary" rules="required" v-slot="{ errors }">
          <b-form-textarea
            :state="errors.length == 0"
            v-model="form.diary"
            required
            placeholder="Diary"
            name="diary"
            rows="3"
          ></b-form-textarea>
          <b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>

      <b-button type="submit" variant="primary" style="margin-right: 10px">Submit</b-button>
      <b-button type="reset" variant="danger" @click="cancel()">Cancel</b-button>
    </b-form>
  </ValidationObserver>
</template>

<script>
import { requestsMixin } from "@/mixins/requestsMixin";

export default {
  name: "JournalForm",
  mixins: [requestsMixin],
  props: {
    edit: Boolean,
    journal: Object
  },
  data() {
    return {
      form: {}
    };
  },
  methods: {
    async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        return;
      }
      const offDate = new Date(this.form.date);
      const correctedDate = new Date(
        offDate.getTime() + Math.abs(offDate.getTimezoneOffset() * 60000)
      );

      const params = {
        ...this.form,
        date: correctedDate
      };

      if (this.edit) {
        await this.editJournal(params);
      } else {
        await this.addJournal(params);
      }
      const { data } = await this.getJournals();
      this.$store.commit("setJournals", data);
      this.$emit("saved");
    },
    cancel() {
      this.$emit("cancelled");
    }
  },
  watch: {
    journal: {
      handler(val) {
        this.form = JSON.parse(JSON.stringify(val || {}));
      },
      deep: true,
      immediate: true
    }
  }
};
</script>

The file is the form for letting users entering their journal entries.

In the onSubmit function, we validate our form with Vee-Validate by calling this.$refs.observer.validate(); to make sure diary is entered and that it’s entered before saving it, and the date field is entered in the YYYY-MM-DD format. We use Vee-Validate to validate the form fields. The ValidationObserver component is for validating the whole form, while the ValidationProvider component is for validating the form fields that it wraps around. The validation rule is specified by the rule prop of the date and diary fields. The state prop is for setting the validation state which shows the green when errors has length 0 and red otherwise. The error messages are shown in the b-form-invalid-feedback component. This page only has the countries drop down.

We correct the dates before submitting since dates in YYYY-MM-DD format has to be corrected by adding an offset for the time zone to save the date in UTC. Once the data is saved, we get the latest data and then put the data in the store. Then we emit the saved event to the HomePage so that we can close the modal.

Next we create a mixins folder in the src folder and create a file called requestsMixin.js file. In there, we add:

const APIURL = "[http://localhost:3000](http://localhost:3000)";
const axios = require("axios");

export const requestsMixin = {
  methods: {
    getJournals() {
      return axios.get(`${APIURL}/journals`);
    },

    addJournal(data) {
      return axios.post(`${APIURL}/journals`, data);
    },

    editJournal(data) {
      return axios.put(`${APIURL}/journals/${data.id}`, data);
    },

    deleteJournal(id) {
      return axios.delete(`${APIURL}/journals/${id}`);
    }
  }
};

These are the functions to get and save our journal data to back end.

Next in the views folder, we replace the code in the Home.vue file with:

<template>
  <div class="page">
    <h1 class="text-center">Journal</h1>

    <div class="text-center">
      <b-button @click="openAddModal()" variant="primary">Add Journal Entry</b-button>
      <b-button @click="openDeleteModal()" variant="primary">Delete Journal Entries</b-button>
    </div>

    <br />

    <Timeline
      :timeline-items="journals"
      message-when-no-items="No Entries Found"
      :unique-year="true"
      order="desc"
    />

    <b-modal id="add-modal" title="Add Journal" hide-footer>
      <JournalForm [@saved](http://twitter.com/saved "Twitter profile for @saved")="closeModal()" [@cancelled](http://twitter.com/cancelled "Twitter profile for @cancelled")="closeModal()" :edit="false" />
    </b-modal>

    <b-modal id="delete-modal" title="Delete Journal Entries" hide-footer>
      <b-card v-for="(j, i) of journals" :key="i">
        <b-card-text>
          <h2>{{j.from | dateFormat('YYYY-MM-DD')}}</h2>
          <p>{{j.description}}</p>
        </b-card-text>
        <b-button @click="deleteOneJournal(j.id)" variant="primary">Delete</b-button>
      </b-card>
    </b-modal>
  </div>
</template>

<script>
// @ is an alias to /src
import JournalForm from "@/components/JournalForm.vue";
import Timeline from "timeline-vuejs";
import { requestsMixin } from "@/mixins/requestsMixin";

export default {
  name: "home",
  components: {
    JournalForm,
    Timeline
  },

  mixins: [requestsMixin],
  computed: {
    journals() {
      return this.$store.state.journals
        .sort((a, b) => +new Date(b.date) - +new Date(a.date))
        .map(j => ({
          id: j.id,
          from: new Date(j.date),
          title: j.title,
          description: j.diary
        }));
    }
  },
  beforeMount() {
    this.getAllJournals();
  },
  data() {
    return {
      selectedJournal: {}
    };
  },
  methods: {
    openAddModal() {
      this.$bvModal.show("add-modal");
    },
    openDeleteModal() {
      this.$bvModal.show("delete-modal");
    },
    closeModal() {
      this.$bvModal.hide("add-modal");
      this.$bvModal.hide("delete-modal");
    },
    async deleteOneJournal(id) {
      await this.deleteJournal(id);
      this.getAllJournals();
    },
    async getAllJournals() {
      const { data } = await this.getJournals();
      this.$store.commit("setJournals", data);
      this.closeModal();
    }
  }
};
</script>

<style lang="scss" scoped>
.timeline {
  margin: 0 auto;
}
</style>

This is the home page our app. We have a button for adding and deleting journal entries at the top of the page. When we click the buttons we open the modals to open a form for entering diary entries and display a list of diaries where you can click Delete button to delete them individually respectively. Below the buttons, there’s the timeline for displaying the journal entries. We pass in the journals array into the timeline-items .

Below that we have the modals for displaying the JournalForm and a list of cards stacked vertically to display the existing items, where they can be read and deleted individually. we handle the saved event from the JournalForm to close the modal after items are saved.

In the delete-modal , we list the items and have a button to delete each entry. The from field is formatted to YYYY-MM-DD format by the VueFilterDateFormat package to display the dates properly.

In the scripts section, we have the journals property in the computed object to get the latest journal entries. We map the fields to the ones required by the Timeline-Vuejs package so that we can display the timeline properly. We sort them by reverse chronological order to view the latest items at the top and older items below it. Also, we have the beforeMount hook to get all the password entries during page load with the getJournals function we wrote in our mixin. To delete a journal entry, we call deleteOneJournal our mixin to make the request to the back end via the deleteJournal function.

Next in App.vue , we replace the existing code with:

<template>
  <div id="app">
    <b-navbar toggleable="lg" type="dark" variant="info">
      <b-navbar-brand to="/">Journal App</b-navbar-brand>

      <b-navbar-toggle target="nav-collapse"></b-navbar-toggle>

      <b-collapse id="nav-collapse" is-nav>
        <b-navbar-nav>
          <b-nav-item to="/" :active="path  == '/'">Home</b-nav-item>
        </b-navbar-nav>
      </b-collapse>
    </b-navbar>
    <router-view />
  </div>
</template>

<script>
export default {
  data() {
    return {
      path: this.$route && this.$route.path
    };
  },
  watch: {
    $route(route) {
      this.path = route.path;
    }
  }
};
</script>

<style lang="scss">
.page {
  padding: 20px;
}

button,
.btn.btn-primary {
  margin-right: 10px !important;
}

.button-toolbar {
  margin-bottom: 10px;
}
</style>

to add a Bootstrap navigation bar to the top of our pages, and a router-view to display the routes we define. This style section isn’t scoped so the styles will apply globally. In the .page selector, we add some padding to our pages. We also add margins to our app’s buttons here.

Next in main.js , we replace the existing code with:

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import BootstrapVue from "bootstrap-vue";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
import "../node_modules/timeline-vuejs/dist/timeline-vuejs.css";
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";
import { required } from "vee-validate/dist/rules";
import VueFilterDateFormat from "vue-filter-date-format";

extend("required", required);
extend("date", {
  validate: value =>
    /([12]d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]d|3[01]))/.test(value),
  message: "Date must be in YYYY-MM-DD format"
});
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);
Vue.use(VueFilterDateFormat);
Vue.use(BootstrapVue);
Vue.config.productionTip = false;

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app");

We added all the libraries we need here, including BootstrapVue JavaScript and CSS, Vee-Validate components along with the validation rules. We created the date rule which verifies that the inputs that used the rule will be in YYYY-MM-DD format. Also, we added the VueFilterDateFormat package to format the dates in the Date column of our table in Home.vue .

In router.js we replace the existing code with:

import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";

Vue.use(Router);

export default new Router({
  mode: "history",
  base: process.env.BASE_URL,
  routes: [
    {
      path: "/",
      name: "home",
      component: Home
    }
  ]
});

to include the home page in our routes so users can see the page.

And in store.js , we replace the existing code with:

import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    journals: []
  },
  mutations: {
    setJournals(state, payload) {
      state.journals = payload;
    }
  },
  actions: {}
});

to add our journals state to the store so we can observer it in the computed block of JournalForm, and HomePage components. We have the setSavings function to update the savings state and we use it in the components by call this.$store.commit(“setJournals”, data); to set the journal entries data for the JournalForm and HomePage .

Finally, in index.html , we replace the existing code with:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
    <title>Journal App</title>
  </head>
  <body>
    <noscript>
      <strong
        >We're sorry but vue-timeline-tutorial-app doesn't work properly without
        JavaScript enabled. Please enable it to continue.</strong
      >
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

to change the title.

After all the hard work, we can start our app by running npm run serve.

To start the back end, we first install the json-server package by running npm i json-server. Then, go to our project folder and run:

json-server --watch db.json

In db.json, change the text to:

{
  "journals": [],
}

So we have the journals endpoints defined in the requests.js available.

Categories
Angular

Add Tool Tips to Your Angular App to Help Users Use Your App

Tool tips are common for providing hints on how to use different parts of a web app. It is easy to add and it helps users understand the app more. They’re also useful for display long text that would be too long.

Ngx-Bootstrap has tool tips built in as a component. It also has many other components that we can use. Adding tool tips using Ngx-Bootstrap is easy. See https://valor-software.com/ngx-bootstrap/#/tooltip for a full set of options for the tool tip. It can have text, position can change, also can change position according to screen size changes. The content can also be HTML. Trigger for tool tips can also be customized.

In this article, we will write an employee manager app that lets users enter their employee data in a form. Users can add, edit and delete their data with tool tips to guide them through the forms when they use the form.

To start the project, we install the Angular CLI if not installed already by running npm i -g @angular/cli . Next we run the Angular CLI to create the project by typing:

ng new employee-manager

In the wizard, we choose to include routing and use SCSS as our CSS preprocessor.

Then we install some packages. We need the Ngx-Bootstrap for styling, modal, and the tool tips, as well as MobX to store the countries in a shared store. To install them, we run:

npm i ngx-bootatrap mobx mobx-angular

Next we create our components, classes and services. To do this, we run:

ng g component employeeForm
ng g component homePage
ng g service employees
ng g class employeeStore
ng g class employee

Now we are ready to write some code. In employee-form.component.html, we replace the existing code with:

<form (ngSubmit)="save(employeeForm)" #employeeForm="ngForm">
  <div class="form-group">
    <label tooltip="Enter employee's name here" placement="right">Name</label>
    <input
      type="text"
      class="form-control"
      placeholder="Name"
      #name="ngModel"
      name="name"
      [(ngModel)]="form.name"
      required
    />
    <div *ngIf="name?.invalid && (name.dirty || name.touched)">
      <div *ngIf="name.errors.required">
        Name is required.
      </div>
    </div>
  </div>

  <div class="form-group">
    <label tooltip="Enter employee's address here" placement="right"
      >Address</label
    >
    <input
      type="text"
      class="form-control"
      placeholder="Address"
      #address="ngModel"
      name="address"
      [(ngModel)]="form.address"
      required
    />
    <div *ngIf="address?.invalid && (address.dirty || address.touched)">
      <div *ngIf="address.errors.required">
        Address is required.
      </div>
    </div>
  </div>

  <div class="form-group">
    <label tooltip="Enter employee's position here" placement="right"
      >Position</label
    >
    <input
      type="text"
      class="form-control"
      placeholder="Position"
      #position="ngModel"
      name="position"
      [(ngModel)]="form.position"
      required
    />
    <div *ngIf="position?.invalid && (position.dirty || position.touched)">
      <div *ngIf="position.errors.required">
        Position is required.
      </div>
    </div>
  </div>

  <div class="form-group">
    <label tooltip="Select employee's employment status" placement="right"
      >Employment Status</label
    >
    <select
      class="form-control"
      #status="ngModel"
      name="status"
      [(ngModel)]="form.status"
      required
    >
      <option value="Employed">Employed</option>
      <option value="Terminated">Terminated</option>
    </select>
    <div *ngIf="status?.invalid && (status.dirty || status.touched)">
      <div *ngIf="status.errors.required">
        Status is required.
      </div>
    </div>
  </div>

  <button class="btn btn-primary">Save</button>
</form>

This is the template for the employee form for letting users add and edit employee data. We have name, address, position, and status fields, and they’re all set as required. Error messages are displayed when any of them aren’t filled in and submitted will be stopped if the user tries to submit without any of these data. The labels will show the tool tips when users hover over them. We set the placementto right so that they display on the right. When users click the Save button the save function in the code is called.

Then in employee-form.component.ts , we replace the existing code with:

import { Component, OnInit, Output, Input, SimpleChanges, EventEmitter } from '@angular/core';
import { employeeStore } from '../employee-store';
import { EmployeesService } from '../employees.service';
import { NgForm } from '@angular/forms';
import { Employee } from '../employee';

@Component({
  selector: 'app-employee-form',
  templateUrl: './employee-form.component.html',
  styleUrls: ['./employee-form.component.scss']
})
export class EmployeeFormComponent implements OnInit {
  form: Employee = <Employee>{};
  @Output('saved') saved = new EventEmitter();
  @Input() edit: boolean;
  @Input() selectedEmployee: Employee;
  store = employeeStore;

  constructor(private employeeService: EmployeesService) { }

  ngOnInit() {
  }

  ngOnChanges(changes: SimpleChanges) {
    this.form = Object.assign({}, this.selectedEmployee);
  }

  save(employeeForm: NgForm) {
    if (employeeForm.invalid) {
      return;
    }
    if (this.edit) {
      this.employeeService.editEmployee(this.form)
        .subscribe(res => {
          this.getEmployees()
          this.saved.emit();
        })
    }
    else {
      this.employeeService.addEmployee(this.form)
        .subscribe(res => {
          this.getEmployees()
          this.saved.emit();
        })
    }
  }

  getEmployees() {
    this.employeeService.getEmployees()
      .subscribe((res: Employee[]) => {
        this.store.setEmployees(res);
      })
  }

}

This file has the functions that we called in the previous template like the save function. We also have the Inputs to get the data from the home page, as well as an Output to emit a saved event to the home page. Since we use the form for editing, we also need to pass in the selected entry with the selectedEmployee Input, so that it can be edited. To update the form object with the selectedEmployee values, we copied the value whenever the selectedEmployee Input changes.

In the save function, we validate the form and call different functions for saving depending on whether the form is being used for adding or editing an entry. The latest entries will be populated in our MobX store by calling the getEmployees function and the saved event will be emitted once that’s done.

Next in home-page.component.html , we replace the code with:

<ng-template #addTemplate>
  <div class="modal-header">
    <h2 class="modal-title pull-left">Add Employee</h2>
  </div>
  <div class="modal-body">
    <app-employee-form (saved)="closeModals()"></app-employee-form>
  </div>
</ng-template>

<ng-template #editTemplate>
  <div class="modal-header">
    <h2 class="modal-title pull-left">Edit Employee</h2>
  </div>
  <div class="modal-body">
    <app-employee-form
      [edit]="true"
      (saved)="closeModals()"
      [selectedEmployee]="selectedEmployee"
    ></app-employee-form>
  </div>
</ng-template>

<h1 class="text-center">Employee Manager</h1>

<div class="btn-group" role="group" id="add-group">
  <button
    type="button"
    class="btn btn-secondary"
    (click)="openAddModal(addTemplate)"
  >
    Add Employee
  </button>
</div>

<br />

<div class="card" *ngFor="let e of store.employees">
  <div class="card-body">
    <h5 class="card-title">{{ e.name }}</h5>
    <p class="card-text">Position: {{ e.position }}</p>
    <p class="card-text">Address: {{ e.address }}</p>
    <p class="card-text">Employment Status: {{ e.status }}</p>
    <button (click)="openEditModal(editTemplate, e)" class="btn btn-primary">
      Edit
    </button>
    <button (click)="deleteEmployee(e.id)" class="btn btn-primary">
      Delete
    </button>
  </div>
</div>

to add buttons for adding, editing, and deleting entries. The employee entries are displayed in the cards. The Edit and Delete buttons are displayed at the bottom of the cards. Also, we have the modals for adding and editing entries that we open with the Add and Edit buttons respectively.

In home-page.component.scss , we add:

$margin: 10px;

#add-group {
  margin-bottom: $margin;
}

button {
  margin-right: $margin;
}

to add some margins to our buttons.

Next in home-page.component.ts , we replace the existing code with:

import { Component, OnInit, TemplateRef } from '@angular/core';
import { employeeStore } from '../employee-store';
import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
import { EmployeesService } from '../employees.service';
import { Employee } from '../employee';

@Component({
  selector: 'app-home-page',
  templateUrl: './home-page.component.html',
  styleUrls: ['./home-page.component.scss']
})
export class HomePageComponent implements OnInit {
  addModalRef: BsModalRef;
  editModalRef: BsModalRef;
  selectedEmployee: Employee = <Employee>{};
  store = employeeStore;

  constructor(
    private modalService: BsModalService,
    private employeeService: EmployeesService
  ) { }

  ngOnInit() {
    this.getEmployees()
  }

  getEmployees() {
    this.employeeService.getEmployees()
      .subscribe((res: Employee[]) => {
        this.store.setEmployees(res);
      })
  }

  openAddModal(template: TemplateRef<any>) {
    this.addModalRef = this.modalService.show(template);
  }

  openEditModal(template: TemplateRef<any>, employee: Employee) {
    this.editModalRef = this.modalService.show(template);
    this.selectedEmployee = employee;
  }

  closeModals() {
    this.addModalRef && this.addModalRef.hide();
    this.editModalRef && this.editModalRef.hide();
  }

  deleteEmployee(id) {
    this.employeeService.deleteEmployee(id)
      .subscribe((res: Employee[]) => {
        this.getEmployees();
      })
  }
}

We have the openAddModal and openEditModal functions to open the Add and Edit modals. The closeModals function is for closing the modals when things are saved in the app-employee-form component. The deleteEmployee function is for deleting the employee, and the getEmployees is used for getting the entries when the page loads when items are deleted and out the items in our store so every component can access it.

In app-routing.module.ts , we put:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomePageComponent } from './home-page/home-page.component';

const routes: Routes = [
  { path: '', component: HomePageComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

so users can see the pages we just added when they click on the links or enter the URLs.

Next in app.component.html , we put:

<nav class="navbar navbar-expand-lg navbar-light bg-light">
  <a class="navbar-brand" routerLink="/">Employee Manager</a>
  <button
    class="navbar-toggler"
    type="button"
    data-toggle="collapse"
    data-target="#navbarSupportedContent"
    aria-controls="navbarSupportedContent"
    aria-expanded="false"
    aria-label="Toggle navigation"
  >
    <span class="navbar-toggler-icon"></span>
  </button>

  <div class="collapse navbar-collapse" id="navbarSupportedContent">
    <ul class="navbar-nav mr-auto">
      <li class="nav-item active">
        <a class="nav-link" routerLink="/">Home </a>
      </li>
    </ul>
  </div>
</nav>

<div class="page">
  <router-outlet></router-outlet>
</div>

to add the links to our pages and expose the router-outlet so users can see our pages.

Then in app.component.scss , we add:

.page {
  padding: 20px;
}

nav {
  background-color: aquamarine !important;
}

to add padding to our pages and change the color of our Bootstrap navigation bar.

In app.module.ts , we replace the existing code with:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomePageComponent } from './home-page/home-page.component';
import { EmployeeFormComponent } from './employee-form/employee-form.component';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { TooltipModule } from 'ngx-bootstrap/tooltip';
import { ModalModule } from 'ngx-bootstrap/modal';
import { EmployeesService } from './employees.service';

@NgModule({
  declarations: [
    AppComponent,
    HomePageComponent,
    EmployeeFormComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule,
    HttpClientModule,
    ModalModule.forRoot(),
    TooltipModule.forRoot()
  ],
  providers: [
    EmployeesService
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

we add our components, services, and libraries that we use in our app. Note that we add each component in Ngx-Bootstrap separately so we only add what we need.

In employee.ts , we add:

export class Employee {
    public id: number;
    public name: string;
    public address: string;
    public position: string;
    public status: string;
}

to add types to our employee form model.

Then in employeeStore.ts , we add:

import { observable, action } from 'mobx-angular';
import { Employee } from './employee';

class EmployeeStore {
    @observable employees: Employee[] = [];
    @action setEmployees(employees) {
        this.employees = employees;
    }
}

export const employeeStore = new EmployeeStore();

to create the MobX store to get our components share the data. Whenever we call this.store.setEmployees our components we set the currencies data in this store since we added the @action decorator before it. When we call this.store.employees in our component code we are always getting the latest value from this store since has the @observable decorator. We add the Employee[] type to employees so we don’t have to guess the fields available.

Then in employee.service.ts , we replace the existing code with:

import { Injectable } from '@angular/core';
import { environment } from 'src/environments/environment';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class EmployeesService {

  constructor(private http: HttpClient) { }

  getEmployees() {
    return this.http.get(`${environment.apiUrl}/employees`);
  }

  addEmployee(data) {
    return this.http.post(`${environment.apiUrl}/employees`, data);
  }

  editEmployee(data) {
    return this.http.put(`${environment.apiUrl}/employees/${data.id}`, data);
  }

  deleteEmployee(id) {
    return this.http.delete(`${environment.apiUrl}/employees/${id}`);
  }

}

so that we can make HTTP requests to our back end to get, save and delete the user’s employee entries.

Next in environment.prod.ts and environment.ts , we replace the existing code with:

export const environment = {
  production: true,
  apiUrl: 'http://localhost:3000'
};

to add our API’s URL.

Finally, in index.html , we replace the code with:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Employee Manager</title>
    <base href="/" />

<meta name="viewport" content="width=device-width, initial-scale=1" />
    <link
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
      rel="stylesheet"
    />
    <link rel="icon" type="image/x-icon" href="favicon.ico" />
    <script
      src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
      integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
      crossorigin="anonymous"
    ></script>
    <script
      src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"
      integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1"
      crossorigin="anonymous"
    ></script>
    <script
      src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
      integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"
      crossorigin="anonymous"
    ></script>
  </head>
  <body>
    <app-root></app-root>
  </body>
</html>

to add the Bootstrap CSS and JavaScript dependencies into our app, as well as changing the title.

After all the work, we can run ng serve to run the app.

To start the back end, we first install the json-server package by running npm i json-server. Then, go to our project folder and run:

json-server --watch db.json

In db.json, change the text to:

{
  "employees": [
  ]
}

So we have the employees endpoints defined in the requests.js available.

Categories
Rxjs

Basic Usage of Rxjs Operators

With operators, we can do more with Rxjs observables. They’re useful for doing complex operations with asynchronous code.

Operators are functions. There’re 2 types of operators:

  • Pipeable operators — they can be piped to Observables using the pipe method available in Observables. They return a new Observable, so multiple pipeable operators can be chained together
  • Creation operators — these are standalone functions to create a new Observable. For example, of(1,2,3) will emit 1, 2, and 3.

Using Operators

We can import operators from Rxjs to use to manipulate our observable results.

For example, we can map values from Observables to another using the map function:

import { of } from "rxjs";
import { map } from "rxjs/operators";

map(x => x * 2)(of(1, 2, 3)).subscribe(val => console.log(val));

We created an Observable that emits 1, 2 and 3 with the of function and then called map with a callback to let us map the values, which returns a function where we can pass in the Observable that we created and returns a new Observable.

map(x => x * 2)(of(1, 2, 3))

returns a new Observable that we can subscribe to. Then we get the values 2, 4, and 6 in the subscribe ‘s callback function.

Piping

We can use the Observable’s pipe method to compose multiple operations into one.

It takes multiple operations as arguments, which is much cleaner than nesting them.

For example, we can use it to rewrite the example we had above:

import { of, pipe } from "rxjs";
import { map } from "rxjs/operators";

of(1, 2, 3)
  .pipe(map(x => x * 2))
  .subscribe(val => console.log(val));

We get the same result, but it’s much cleaner.

Also, we can pass in more than one operation to the pipe method:

of(1, 2, 3)
  .pipe(
    map(x => x * 2),
    map(x => x * 3)
  )
  .subscribe(val => console.log(val));

Then we first multiply each value emitted by the Observable by 2, then we multiply the returned value again by 3. As a result, we get back 6, 12, and 18 in the console.log .

Creation Operators

Creation operators are functions to create an Observable from scratch or by joining other Observables together.

For example, we have the interval function to emit a value from 0 and up in the interval that we specify:

import { interval } from "rxjs";

interval(5000).subscribe(val => console.log(val));

The code above will output an integer from 0 and up every 5 seconds.

Higher-Order Observables

Higher-Order Observables are Observables of Observables. There’re a few things that we can do with different observables.

Rxjs has the concatAll() operator that subscribes to each inner observable and copies all the emitted values until the outer observable completes.

For example, we can use it as follows:

import { of, pipe } from "rxjs";
import { concatAll, map } from "rxjs/operators";

of(1, 2, 3)
  .pipe(
    map(outerVal => {
      console.log(`outerVal ${outerVal}`);
      return of(4, 5, 6);
    }),
    concatAll()
  )
  .subscribe(innerVal => console.log(`innerVal ${innerVal}`));

We should get the output:

outerVal 1
innerVal 4
innerVal 5
innerVal 6
outerVal 2
innerVal 4
innerVal 5
innerVal 6
outerVal 3
innerVal 4
innerVal 5
innerVal 6

As we can see, we get the first value from the of(1,2,3) Observable first, and then all the values from the second. Then we get the second value from of(1,2,3) and then all the values from the second and so on.

Other operator functions include:

  • mergeAll() — subscribes to each inner Observable as it arrives, and emits each value as it arrives.
  • switchAll() — subscribes to the first inner Observable when it arrives, then emits each value as it arrives. It unsubscribes to the previous one then subscribes to the new one.
  • exhaust() — subscribes to the first inner Observable when it arrives, then emits each value as it arrives, discarding all newly arriving Observables as it completes and waits for the next inner Observable.

They all give the same result as concatAll() but the difference is underneath.

We can create new Observables by composing multiple Observables or operators.

For example:

of(1, 2, 3)
  .pipe(
    map(x => x * 2),
    map(x => x * 3)
  )

is an Observable that multiplies each value from Observableof(1, 2, 3) by 6.

With Rxjs, there’re are built-in operators that we can use to create and manipulate Observable values. Also, we can create new Observables and combine them together in different ways.

Observables can also be nested, and the values of all the nested Observables can be obtained by using concatAll() , mergeAll() , switchAll() or exhaust() .

Categories
Quasar

Developing Vue Apps with the Quasar Library — Tooltip Transitions and Position

Quasar is a popular Vue UI library for developing good looking Vue apps.

In this article, we’ll take a look at how to create Vue apps with the Quasar UI library.

Tooltip Transitions

We can add tooltip transitions with the transition-show and transition-hide props:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <q-layout view="lHh Lpr lFf" container style="height: 100vh;">
        <div class="q-pa-md">
          <q-btn color="primary" label="Flip">
            <q-tooltip transition-show="flip-right" transition-hide="flip-left">
              tooltip
            </q-tooltip>
          </q-btn>
        </div>
      </q-layout>
    </div>
    <script>
      new Vue({
        el: "#q-app",
        data: {}
      });
    </script>
  </body>
</html>

transition-show is applied when we show the tooltip.

And transition-hide is applied when we hide it.

Other values include scale and rotate .

Tooltip Target

We can set the target prop of the q-tooltip component to let us set the tooltip to open when we hover over the element with the given selector.

For instance, we can write:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <q-layout view="lHh Lpr lFf" container style="height: 100vh;">
        <div class="q-pa-md">
          <q-img
            src="https://cdn.quasar.dev/img/material.png"
            id="target-img-1"
            style="height: 100px;"
          >
            <div
              class="absolute-bottom-right"
              style="border-top-left-radius: 5px;"
            >
              #target-img-1
            </div>
          </q-img>

          <q-tooltip
            :target="targetEl"
            anchor="center middle"
            self="center middle"
            content-class="bg-black"
          >
            tooltip
          </q-tooltip>
        </div>
      </q-layout>
    </div>
    <script>
      new Vue({
        el: "#q-app",
        data: {
          targetEl: "#target-img-1"
        }
      });
    </script>
  </body>
</html>

We set target to '#target-img-1' , so when we hover over the image, the tooltip will show.

This gives us more flexibility with placing the tooltip.

Tooltip Position

We can change the position of the tooltip with the anchor and self props:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <q-layout view="lHh Lpr lFf" container style="height: 100vh;">
        <div class="q-pa-md">
          <q-btn color="primary" label="hover me">
            <q-tooltip anchor="bottom right" self="top middle">
              tooltip
            </q-tooltip>
          </q-btn>
        </div>
      </q-layout>
    </div>
    <script>
      new Vue({
        el: "#q-app",
        data: {
          targetEl: "#target-img-1"
        }
      });
    </script>
  </body>
</html>

anchor changes the position of the tooltip relative to its target.

self changes its own position relative to its target.

Each of these props can be a combination of top , center , or bottom and left , middle or right .

Conclusion

We can add a tooltip with various positions and transitions with Quasar’s q-tooltip component into our Vue app.