Categories
Express Nodejs Vue 3

Create a Todo App with Vue 3, Express and Sematext

Vue 3 is the up and coming version of the popular Vue front end framework.

We can pair that we the back end of our choice to create an app that we want to create.

In this article, we’ll create a Vue 3 front end that’s paired with an Express back end that uses Sematext for logging.

Get Started

We can start by creating our scaffold.

First, we create a project folder with a backend and frontend folders inside.

Then we can go into our backend folder and create our Express app.

We can use the Express generator package to make this easy.

To run it, we run:

npx express-generator

in our backend folder to add the files.

Then we run:

npm i

to install the packages.

Next, we create our Vue 3 front end project.

To do that, we go into the project folder root and run:

npm init vite-app frontend

This will create the project files in the frontend folder and install the required packages.

Backend

Now we have the scaffolding for both apps, we can work on the back end app.

We install a few more packages that we’ll need for our back end.

To do that we run:

npm i cors dotenv sematext-agent-express sqlite3

cors is a package to let us communicate between front end and back end regardless of the domain they’re in.

dotenv lets us read the environment variables.

sematext-agent-express is the Sematext package for Express apps.

sqlite3 lets us save data to a SQLite database.

Next, we create a todo.js file in the routes folder.

And then in app.js, we change the existing code to:

require('dotenv').config()
const { stHttpLoggerMiddleware } = require('sematext-agent-express')
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var cors = require('cors');

var indexRouter = require('./routes/index');
var todosRouter = require('./routes/todo');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

app.use(cors())

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use(stHttpLoggerMiddleware)

app.use('/', indexRouter);
app.use('/todos', todosRouter);



// catch 404 and forward to error handler
app.use(function (req, res, next) {
  next(createError(404));
});

// error handler
app.use(function (err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

We added the todosRouter middleware to let us use the todos route file.

And we have:

app.use('/todos', todosRouter);

to use the todosRouter file.

Also, we have:

require('dotenv').config()

to read the .env file which we’ll need for reading the API token for Sematext.

We add the Sematext Express middleware into our app with:

app.use(stHttpLoggerMiddleware)

so we can use its logging capabilities in our app.

We also have:

var cors = require('cors');

to let us do cross-domain communication.

Next, we go to todo.js and replace the existing code with:

var express = require('express');
var sqlite3 = require('sqlite3').verbose();
const { stLogger } = require('sematext-agent-express')

var db = new sqlite3.Database('./todos.db');
var router = express.Router();

router.get('/', (req, res) => {
  db.serialize(() => {
    db.all("SELECT * FROM todos", (err, results) => {
      if (err) {
        stLogger.error(err);
        return res.status(500).send(err);
      }
      stLogger.info(results);
      res.json(results);
    });
  });
});

router.post('/', (req, res) => {
  const { name } = req.body;

  db.serialize(() => {
    db.run("INSERT INTO todos (name) VALUES (?)", name, (err, results) => {
      if (err) {
        stLogger.error(err);
        return res.status(500).send(err);
      }
      stLogger.info(results);
      res.json(results);
    });
  });
});

router.put('/:id', (req, res) => {
  const { id } = req.params;
  const { name } = req.body;

  db.serialize(() => {
    db.run("UPDATE todos SET name = ? WHERE id = ?", name, id, (err, result) => {
      if (err) {
        stLogger.error(err);
        return res.status(500).send(err);
      }
      stLogger.info(result);
      res.json(result);
    });
  });
});

router.delete('/:id', (req, res) => {
  const { id } = req.params;
  db.serialize(() => {
    db.run("DELETE FROM todos WHERE id = ?", id, (err, results) => {
      if (err) {
        stLogger.error(err);
        return res.status(500).send(err);
      }
      stLogger.info(results);
      res.json(results);
    });
  });
});

module.exports = router;

We add the routes with the router methods.

We use the stLogger object from the sematext-agent-express package to let us do the logging.

In each route, we have the stLogger.error method to log errors.

And we have stLogger.info to log other information like the database results.

Each time the route middleware runs, we’ll see something logged.

If there’s an error we return that as the response back to the client with the if statements.

We call db.serialize to run database queries in sequence.

And we use db.run to run INSERT, UPDATE, DELETE statements.

To run SELECT queries, we run db.all to get all the rows.

We use parameterized queries so that values are escaped before we run the queries.

req.params gets the request parameters from the URL.

req.body gets the request body.

We have:

var db = new sqlite3.Database('./todos.db');

The todos.db file will be created if it doesn’t exist.

Once it’s created we can open the file by using the SQLite browser from https://sqlitebrowser.org/.

We can download the file and install it.

Then we can open the todos.db file and run:

CREATE TABLE todos (id INTEGER PRIMARY KEY, name TEXT NOT NULL)

to create the todos table so we can write to it.

Now the SQL statements in our code should run properly.

Then we go into the backend folder and create the .env file.

And then we add the LOGS_TOKEN key so that we can use Sematext for logging:

LOGS_TOKEN=YOUR_OWN_KEY_FROM_SEMATEXT

We can get the key by signing up for an account by going to https://apps.sematext.com/ui/login/.

Once we’re in, we can click on Apps on the left menu.

Then click on New App on the top right to create our app.

Once you see the app entry, we click on the menu button on the right side of the row, and click Integrations.

Then we see Node.js on the left side of what’s open and follow the instructions from there.

Front End

Now that the back end is done, we can move onto the front end.

First, we install the axios HTTP client so that we can make HTTP requests.

We can do that by running:

npm i axios

We go to the frontend/components folder and create a Todo.vue file.

Then we add:

<template>
  <form @submit.prevent="save">
    <input type="text" v-model="todo.name" />
    <input type="submit" value="save" />
    <button type="button" @click="deleteTodo" v-if='todo.id'>delete</button>
  </form>
</template>

<script>
import axios from "axios";
const APIURL = "http://localhost:3000";

export default {
  name: "Todo",
  props: {
    todo: {
      type: Object,
      default() {
        return {};
      },
    },
  },
  data() {
    return {
      name: "",
    };
  },
  methods: {
    async save() {
      const { name } = this.todo;
      if (this.todo.id) {
        await axios.put(`${APIURL}/todos/${this.todo.id}`, { name });
      } else {
        await axios.post(`${APIURL}/todos`, { name });
      }
      this.$emit("saved-todo");
    },

    async deleteTodo() {
      const { data } = await axios.delete(`${APIURL}/todos/${this.todo.id}`);
      this.$emit("saved-todo");
    },
  },
};
</script>

to it.

We use the form to add or edit the files.

Also, we have a delete button to delete the todos.

The save method makes a PUT request to our back end if the todo has an id.

This mneans it’s an existing entry, so we make a PUT request.

Otherwise, we make a POST request.

Once they’re successful, then we emit the save-todos event so that we can get the latest data later.

Also, we have the deleteTodo method so that we can make a DELETE request to delete a item.

The Todo component takes a todo prop, which is optional.

We have the default method to return the default value for it.

The v-if in the template checks if the todo.id exists.

If it does, then it’s displayed so we can call delete to delete the todo item.

We also have a @subnmit.prevent directive to let us submit our form and run the save method.

Next we work on the App.vue file.

We write:

<template>
  <div>
    <Todo @saved-todo="getTodos"></Todo>
    <Todo @saved-todo="getTodos" v-for="t of todos" :todo="t" :key="t.id"></Todo>
  </div>
</template>

<script>
import axios from "axios";
import Todo from "./components/Todo.vue";
const APIURL = "http://localhost:3000";

export default {
  name: "App",
  components: {
    Todo,
  },
  data() {
    return {
      todos: [],
    };
  },
  beforeMount() {
    this.getTodos();
  },
  methods: {
    async getTodos() {
      const { data } = await axios.get(`${APIURL}/todos`);
      this.todos = data;
    },
  },
};
</script>

We add the Todo component that we created earlier.

The first one is for adding the todo item.

The getTodos method lets us get the todo items.

It’s run when the saved-todo event is emitted.

We listen to the event with @saved-todo on the template.

Also, we call getTodos in the beforeMount hook so that we can get the todos when the page loads.

Running Our App

Once this is done, we can run our app.

We first go into the backend folder and run:

npm start

Then we go into the frontend folder and run:

npm run dev

Once that’s done, we can go to http://localhost:3001 to see the app.

Now we should see:

when we go to http://localhost:3001 and when we do something in our app, we see something like:

logged in Sematext.

It’ll log your activities.

Conclusion

Using Sematext with an Express app is easy.

We just use the sematext-express-agent package to let us log with it.

Creating a Vue front end is also easy.

Vue 3 is the up and coming version of Vue. It’s almost ready for production.

Categories
Vue 3

Getting Started with Vue Router with Vue 3

Vue 3 is in beta and it’s subject to change.

To build a single page app easily, we got to add routing so that URLs will be mapped to components that are rendered.

In this article, we’ll look at how to use Vue Router 4 with Vue 3.

Getting Started

To get started, we can create our app by including the scripts and accessing the required methods within the VueRouter object.

For example, we can write:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/vue@next"></script>
    <script src="https://unpkg.com/vue-router@4.0.0-beta.7/dist/vue-router.global.js"></script>
    <title>App</title>
  </head>
  <body>
    <div id="app">
      <p>
        <router-link to="/foo">Go to Foo</router-link>
        <router-link to="/bar">Go to Bar</router-link>
      </p>
      <router-view></router-view>
    </div>
    <script>
      const Foo = { template: "<div>foo</div>" };
      const Bar = { template: "<div>bar</div>" };

      const routes = [
        { path: "/foo", component: Foo },
        { path: "/bar", component: Bar }
      ];

      const router = VueRouter.createRouter({
        history: VueRouter.createWebHistory(),
        routes
      });

      const app = Vue.createApp({});
      app.use(router);
      app.mount("#app");
    </script>
  </body>
</html>

We have the script tags for Vue 3 and Vue Router 4 in the head tag.

In the body , we have the div with ID app to hold the template.

The router-link and router-view components are used for the links and the router view respectively.

Then we call VueRouter.createRouter to create the router and pass in the route.

The VueRouter.createWebHistory method turns on history mode to let us use URLs without the hash sign.

Then we call createApp to create our app.

And we include the router with app.use .

Then finally we mount the app.

Now we can click on the links and see the corresponding component’s content.

The routes have the path and component properties to map the path to the components.

We can access the router data with the this.$router.currentRouter.value.params property.

For instance, we can write:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/vue@next"></script>
    <script src="https://unpkg.com/vue-router@4.0.0-beta.7/dist/vue-router.global.js"></script>
    <title>App</title>
  </head>
  <body>
    <div id="app">
      <p>
        <router-link to="/foo/1">Go to Foo</router-link>
        <router-link to="/bar">Go to Bar</router-link>
      </p>
      <p>{{id}}</p>
      <router-view></router-view>
    </div>
    <script>
      const Foo = { template: "<div>foo</div>" };
      const Bar = { template: "<div>bar</div>" };

      const routes = [
        { path: "/foo/:id", component: Foo },
        { path: "/bar", component: Bar }
      ];

      const router = VueRouter.createRouter({
        history: VueRouter.createWebHistory(),
        routes
      });

      const app = Vue.createApp({
        computed: {
          id() {
            return (
              this.$router.currentRoute.value.params &&
              this.$router.currentRoute.value.params.id
            );
          }
        }
      });
      app.use(router);
      app.mount("#app");
    </script>
  </body>
</html>

We have the /foo/1 URL in the router link.

The :id is the placeholder for the URL parameter called id .

We can get the value of it with the this.$router.current.value.params object.

This has all the URL parameter keys and values.

Conclusion

We can add basic routes with Vue Router 4 with Vue 3.

Categories
Vue 3

Vue Router 4 — Navigation

Vue Router 4 is in beta and it’s subject to change.

To build a single page app easily, we got to add routing so that URLs will be mapped to components that are rendered.

In this article, we’ll look at how to use Vue Router 4 with Vue 3.

Navigation

We can navigate to a route with the this.$router ‘s methods.

For example, we can write:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/vue@next"></script>
    <script src="https://unpkg.com/vue-router@4.0.0-beta.7/dist/vue-router.global.js"></script>
    <title>App</title>
  </head>
  <body>
    <div id="app">
      <p>
        <router-link to="/foo">Go to Foo</router-link>
        <router-link to="/bar">Go to Bar</router-link>
        <a @click="goBack" href="#">Go Back</a>
      </p>
      <router-view></router-view>
    </div>
    <script>
      const Foo = { template: "<div>foo</div>" };
      const Bar = { template: "<div>bar</div>" };

      const routes = [
        { path: "/foo", component: Foo },
        { path: "/bar", component: Bar }
      ];

      const router = VueRouter.createRouter({
        history: VueRouter.createWebHistory(),
        routes
      });

      const app = Vue.createApp({
        methods: {
          goBack() {
            window.history.length > 1
              ? this.$router.go(-1)
              : this.$router.push("/foo");
          }
        }
      });
      app.use(router);
      app.mount("#app");
    </script>
  </body>
</html>

to create a goBack method and call it when we click on the Go Back link.

In the method, we check if there are anything in the browser history.

If there is, then we go back to the previous page.

Otherwise, we go to the /foo page.

Reacting to Params Changes

We can react to route params changes in a few ways.

For instance, we can write:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/vue@next"></script>
    <script src="https://unpkg.com/vue-router@4.0.0-beta.7/dist/vue-router.global.js"></script>
    <title>App</title>
  </head>
  <body>
    <div id="app">
      <p>
        <router-link to="/foo/1">Foo 1</router-link>
        <router-link to="/foo/2">Foo 2</router-link>
      </p>
      <router-view></router-view>
    </div>
    <script>
      const Foo = {
        template: "<div>foo</div>",
        watch: {
          $route(to, from) {
            console.log(to, from);
          }
        }
      };

      const routes = [{ path: "/foo/:id", component: Foo }];

      const router = VueRouter.createRouter({
        history: VueRouter.createWebHistory(),
        routes
      });

      const app = Vue.createApp({});
      app.use(router);
      app.mount("#app");
    </script>
  </body>
</html>

We have the /foo/:id route that maps to the Foo component.

And in the Foo component, we have the $route watcher to watch the route.

to has the route to go to. from has the route that we departed from.

They’re both objects with the path, route metadata, route parameters, query parameters, and more.

Also, we can use the beforeRouteUpdate method to watch for route changes:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/vue@next"></script>
    <script src="https://unpkg.com/vue-router@4.0.0-beta.7/dist/vue-router.global.js"></script>
    <title>App</title>
  </head>
  <body>
    <div id="app">
      <p>
        <router-link to="/foo/1">Foo 1</router-link>
        <router-link to="/foo/2">Foo 2</router-link>
      </p>
      <router-view></router-view>
    </div>
    <script>
      const Foo = {
        template: "<div>foo</div>",
        beforeRouteUpdate(to, from, next) {
          console.log(to, from);
          next();
        }
      };

      const routes = [{ path: "/foo/:id", component: Foo }];

      const router = VueRouter.createRouter({
        history: VueRouter.createWebHistory(),
        routes
      });

      const app = Vue.createApp({});
      app.use(router);
      app.mount("#app");
    </script>
  </body>
</html>

Instead of watching the $route object, we have the beforeRouteUpdate hook, which is made available with the app.use(router) call.

Now when we click on the links, we’ll see the same data as we did with the watcher.

The only difference is that we call next to move to the next route.

Conclusion

We can watch for navigation with watchers or hooks with Vue Router 4.

Categories
Vue 3

Vue Router 4–404 and Nested Routes

Vue Router 4 is in beta and it’s subject to change.

To build a single page app easily, we got to add routing so that URLs will be mapped to components that are rendered.

In this article, we’ll look at how to use Vue Router 4 with Vue 3.

Catch-All / 404 Not Found Route

We can create a catch-all or 404 route by using the asterisk pattern as the path.

For example, we can write:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/vue@next"></script>
    <script src="https://unpkg.com/vue-router@4.0.0-beta.7/dist/vue-router.global.js"></script>
    <title>App</title>
  </head>
  <body>
    <div id="app">
      <p>
        <router-link to="/foo">Foo</router-link>
      </p>
      <router-view></router-view>
    </div>
    <script>
      const Foo = {
        template: "<div>foo</div>"
      };

      const NotFound = {
        template: "<div>not found</div>"
      };

      const routes = [
        { path: "/foo", component: Foo },
        { path: "/:catchAll(.*)", component: NotFound }
      ];

      const router = VueRouter.createRouter({
        history: VueRouter.createWebHistory(),
        routes
      });

      const app = Vue.createApp({});
      app.use(router);
      app.mount("#app");
    </script>
  </body>
</html>

We define a catch-all route with the /:catchAll(.*) capture group instead of * as in Vue Router 3.

Now when we go to any path that isn’t foo , we’ll see the ‘not found’ message.

Nested Routes

We can create nested routes with the children property.

For instance, we can write:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/vue@next"></script>
    <script src="https://unpkg.com/vue-router@4.0.0-beta.7/dist/vue-router.global.js"></script>
    <title>App</title>
  </head>
  <body>
    <div id="app">
      <p>
        <router-link to="/parent/home">parent</router-link>
      </p>
      <router-view></router-view>
    </div>
    <script>
      const Parent = {
        template: `<div>
          parent
          <router-view></router-view>
        </div>`
      };

      const Home = {
        template: `<div>home</div>`
      };

       const routes = [
        {
          path: "/parent",
          component: Parent,
          children: [{ path: "home", component: Home }]
        }
      ];

      const router = VueRouter.createRouter({
        history: VueRouter.createWebHistory(),
        routes
      });

      const app = Vue.createApp({
        watch: {
          $route() {
            console.log(this.$route.resolve);
          }
        }
      });
      app.use(router);
      app.mount("#app");
    </script>
  </body>
</html>

We have a routes array with an object that has the children property.

The property has an object with the path and component properties like a regular route.

In the router-link , the to prop has the path to the nested route.

The Parent component has the router-view component so that we can view the content of the child route.

Therefore, when we click on the parent link, we see the:

parent
home

text displayed.

We can have URL parameters in the parent route.

For example, we can write:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/vue@next"></script>
    <script src="https://unpkg.com/vue-router@4.0.0-beta.7/dist/vue-router.global.js"></script>
    <title>App</title>
  </head>
  <body>
    <div id="app">
      <p>
        <router-link to="/parent/1/home">parent</router-link>
      </p>
      <router-view></router-view>
    </div>
    <script>
      const Parent = {
        template: `<div>
          <div>parent {{ $router.currentRoute.value.params.id }}</div>
          <div><router-view></router-view></div>
        </div>`
      };

      const Home = {
        template: `<div>home</div>`
      };

      const routes = [
        {
          path: "/parent/:id",
          component: Parent,
          children: [{ path: "home", component: Home }]
        }
      ];

      const router = VueRouter.createRouter({
        history: VueRouter.createWebHistory(),
        routes
      });

      const app = Vue.createApp({
        watch: {
          $route() {
            console.log(this.$route.resolve);
          }
        }
      });
      app.use(router);
      app.mount("#app");
    </script>
  </body>
</html>

To add the :id URL parameter placeholder.

Since the router-link ‘s to prop is now /parent/1/home and we have the $router.currentRoute.value.params.id in the Parent ‘s template, we’ll see the number 1 displayed.

The URL parameter placeholder of the parent stays with the parent.

Conclusion

The way we define 404 and nested routes are slightly different from Vue Router 3 in Vue Router 4.

Categories
Vue 3

Vue 3 — Render Functions Events and Plugins

Vue 3 is in beta and it’s subject to change.

Vue 3 is the up and coming version of Vue front end framework.

It builds on the popularity and ease of use of Vue 2.

In this article, we’ll look at how to create render functions and create plugins with Vue 3.

Event Modifiers Equivalents

The event modifiers have the following equivalents.

They are:

  • .stop — event.stopPropagation()
  • .prevent — event.preventDefault()
  • .self — if (event.target !== event.currentTarget) return
  • .enter or .13 — if (event.keyCode !== 13) return
  • .ctrl, .alt, .shift, or .meta — if (!event.ctrlKey) return

For example, we can use it by writing:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>App</title>
    <script src="https://unpkg.com/vue@next"></script>
  </head>
  <body>
    <div id="app">
      <custom-input></custom-input>
    </div>
    <script>
      const app = Vue.createApp({});

      app.component("custom-input", {
        render() {
          return Vue.h("input", {
            onKeyUp: event => {
              if (event.target !== event.currentTarget) return;
              if (!event.shiftKey || event.keyCode !== 23) return;
              event.stopPropagation();
              event.preventDefault();
              //...
            }
          });
        }
      });

      app.mount("#app");
    </script>
  </body>
</html>

We can call the plain JavaScript event methods and check their properties to add the modifier equivalents.

Slots

The this.$slots lets us add slots to our Vue apps.

For example, we can write:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>App</title>
    <script src="https://unpkg.com/vue@next"></script>
  </head>
  <body>
    <div id="app">
      <custom-heading :level="1">foo</custom-heading>
    </div>
    <script>
      const app = Vue.createApp({});
      app.component("custom-heading", {
        render() {
          const { h } = Vue;
          return h(`h1`, {}, this.$slots.default());
        },
        props: {
          level: {
            type: Number,
            required: true
          }
        }
      });
      app.mount("#app");
    </script>
  </body>
</html>

to add or custom h1 element with a slot inside.

this.$slots.default is the default slot.

JSX

We can also use JSX in our render functions if we install this Babel plugin.

This way, we can use the more convenient JSX syntax in our render functions.

For instance, we can write:

import AnchoredHeading from './AnchoredHeading.vue'

app.component("custom-heading", {
  render() {
    return (
      <AnchoredHeading level={1}>
        <span>Hello</span> world!
      </AnchoredHeading>
      );
    }
});

to write JSX instead of plain JavaScript in our render function.

Plugins

Plugins are a self-contained code that can be reused.

They add global-level functionality in Vue.

They can add anything, which includes global methods or properties.

It can also include global assets and mixins.

Instance methods can also be added with plugins.

We can add plugins by exporting an object with the install method:

export default {
  install: (app, options) => {
    //...
  }
}

We can then add our own global properties by writing:

export default {
  install: (app, options) => {
    app.config.globalProperties.$foo = (key) => {
      return 'foo';
    }
  }
}

We can also inject other plugins within a plugin.

For instance, we can write:

export default {
  install: (app, options) => {
    app.config.globalProperties.$foo = (key) => {
      return 'foo';
    }

    app.provide('i18n', options);
  }
}

We can add global mixins and directives right into our plugin code with the app.directive and app.mixin methods:

export default {
  install: (app, options) => {
    app.config.globalProperties.$foo = (key) => {
      return 'foo';
    }

    app.provide('i18n', options)

    app.directive('my-directive', {
      bind (el, binding, vnode, oldVnode) {
        // some logic ...
      }
      ...
    })

    app.mixin({
      created() {
        //...
      }
      //...
    })
  }
}

To use a plugin, we call the app.use method:

app.use(myPlugin, options)

Conclusion

There’re equivalents to directive event modifiers in render functions.

Render functions can also contain JSX if we add a plugin.

We can create plugins with various method calls.