Categories
Vue 3

Vue 3 — List Transitions

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 creating list transitions.

List Move Transitions

We can add list move transitions.

For instance, we can use the transition-group component to display some effect when we the items change position:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>App</title>
    <script src="https://unpkg.com/vue@next"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
    <style>
      .list-move {
        transition: transform 0.8s ease;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <button @click="shuffle">shuffle</button>
      <transition-group name="list" tag="div">
        <p v-for="item in items" :key="item">
          {{ item }}
        </p>
      </transition-group>
    </div>
    <script>
      const app = Vue.createApp({
        data() {
          return {
            items: Array(10)
              .fill()
              .map(() => Math.random()),
          };
        },
        methods: {
          shuffle() {
            this.items = _.shuffle(this.items);
          }
        }
      });
      app.mount("#app");
    </script>
  </body>
</html>

We added the list-move class with our transition effect to display it when the list is shuffled.

The list is shuffled with the Lodash shuffle method.

Staggering List Transitions

We can stagger transitions in a list.

To do that, we use the Greensock library to help us.

We can write;

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>App</title>
    <script src="https://unpkg.com/vue@next"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.3.4/gsap.min.js"></script>
  </head>
  <body>
    <div id="app">
      <input v-model="query" />
      <transition-group
        name="fade"
        tag="div"
        :css="false"
        @before-enter="beforeEnter"
        @enter="enter"
        @leave="leave"
      >
        <p
          v-for="(item, index) in computedList"
          :key="item.name"
          :data-index="index"
        >
          {{ item.name }}
        </p>
      </transition-group>
    </div>
    <script>
      const app = Vue.createApp({
        data() {
          return {
            query: "",
            list: [
              { name: "james" },
              { name: "mary" },
              { name: "alex" },
              { name: "bob" },
              { name: "jane" }
            ]
          };
        },
        computed: {
          computedList() {
            return this.list.filter(item => {
              return item.name.toLowerCase().includes(this.query.toLowerCase());
            });
          }
        },
        methods: {
          beforeEnter(el) {
            el.style.opacity = 0;
            el.style.height = 0;
          },
          enter(el, done) {
            gsap.to(el, {
              opacity: 1,
              height: "1.3em",
              delay: el.dataset.index * 0.55,
              onComplete: done
            });
          },
          leave(el, done) {
            gsap.to(el, {
              opacity: 0,
              height: 0,
              delay: el.dataset.index * 0.45,
              onComplete: done
            });
          }
        }
      });
      app.mount("#app");
    </script>
  </body>
</html>

We included the Greensock library with our app.

In the methods property, we have a few methods.

The beforeEnter method sets the container’s opacity and height to 0.

The enter method has our enter animation effect.

We change the opacity to 1 to make opaque.

height is th height of the container.

delay is the delay of the animation.

onComplete is a function we call to notify Vue that the animation is done.

We do the same thing with the leave transition.

When computedList returns a new value, the animation effects will be applied.

Therefore, when we type in something into the input box, we’ll see the effects applied.

Reusable Transitions

We can make our transitions reusable by moving it into our own component.

For instance, we can write:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>App</title>
    <script src="https://unpkg.com/vue@next"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.3.4/gsap.min.js"></script>
  </head>
  <body>
    <div id="app">
      <input v-model="query" />
      <list-transition>
        <p
          v-for="(item, index) in computedList"
          :key="item.name"
          :data-index="index"
        >
          {{ item.name }}
        </p>
      </list-transition>
    </div>
    <script>
      const app = Vue.createApp({
        data() {
          return {
            query: "",
            list: [
              { name: "james" },
              { name: "mary" },
              { name: "alex" },
              { name: "bob" },
              { name: "jane" }
            ]
          };
        },
        computed: {
          computedList() {
            return this.list.filter(item => {
              return item.name.toLowerCase().includes(this.query.toLowerCase());
            });
          }
        }
      }); 

      app.component("list-transition", {
        template: `
        <transition-group
          name="fade"
          tag="div"
          :css="false"
          @before-enter="beforeEnter"
          @enter="enter"
          @leave="leave"
        >
         <slot></slot>
        </transition-group>
        `,
        methods: {
          beforeEnter(el) {
            el.style.opacity = 0;
            el.style.height = 0;
          },
          enter(el, done) {
            gsap.to(el, {
              opacity: 1,
              height: "1.3em",
              delay: el.dataset.index * 0.55,
              onComplete: done
            });
          },
          leave(el, done) {
            gsap.to(el, {
              opacity: 0,
              height: 0,
              delay: el.dataset.index * 0.45,
              onComplete: done
            });
          }
        }
      }); 

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

to move our transition component and hooks to its own component.

We just create a component and add a slot in between the transition-group tags to add the slot for our content.

Now we can use the list-transition component everywhere.

Conclusion

We can add our list transitions effects with the transition-group component.

It takes various directives to let us add hooks to create JavaScript animations.

Categories
Hapi

Server-Side Development with Hapi.js — MIME Types and Events

Hapi.js is a small Node framework for developing back end web apps.

In this article, we’ll look at how to create back end apps with Hapi.js.

MIME Types

We can get information about various MIME types with the modules.

For instance, we can write:

const Hapi = require('@hapi/hapi');
const Mimos = require('@hapi/mimos');

const options = {
  override: {
    'node/module': {
      source: 'iana',
      compressible: true,
      extensions: ['node', 'module', 'npm'],
      type: 'node/module'
    },
    'application/javascript': {
      source: 'iana',
      charset: 'UTF-8',
      compressible: true,
      extensions: ['js', 'javascript'],
      type: 'text/javascript'
    },
    'text/html': {
      predicate(mime) {
        mime.foo = 'test';
        return mime;
      }
    }
  }
}

const mimos = new Mimos(options);

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });

  server.route({
    method: 'GET',
    path: '/',
    config: {
      handler(request, h) {
        return mimos.type('text/html');
      },
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We add the options object with the override property to set the data for the given MIME types.

Then we call mimos.type with the MIME type we want to look up.

And then we get:

{"source":"iana","compressible":true,"extensions":["html","htm","shtml"],"type":"text/html","foo":"test"}

as the returned value.

Collect Server Ops Data

We can collect server ops data easily with the @hapi/oppsy module.

To use it, we can write:

const Hapi = require('@hapi/hapi');
const Oppsy = require('@hapi/oppsy');

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });

  const oppsy = new Oppsy(server);
  oppsy.on('ops', (data) => {
    console.log(data);
  });

  server.route({
    method: 'GET',
    path: '/',
    config: {
      handler(request, h) {
        return 'hello'
      },
    }
  });

  await server.start();
  oppsy.start(1000);
  console.log('Server running at:', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We create the oppsy object with the Oppsy constructor.

Then we listen to the ops event with the oppsy.on method.

And in the event handler, we log the data from the server.

It includes data like requests, CPU usage, response times, memory usage, and more.

Events

We can create our own event bus with the @hapi/podium module.

For example, we can write:

const Hapi = require('@hapi/hapi');
const Podium = require('@hapi/podium');

const emitter = new Podium()
const context = { count: 0 }

emitter.registerEvent({
  name: 'event',
  channels: ['ch1', 'ch2']
})

const handler1 = function () {
  ++this.count
  console.log(this.count)
};

const handler2 = function () {
  this.count = this.count + 2
  console.log(this.count)
}

emitter.on({
  name: 'event',
  channels: ['ch1']
}, handler1, context);

emitter.on({
  name: 'event',
  channels: ['ch2']
}, handler2, context)

emitter.emit({
  name: 'event',
  channel: 'ch1'
})

emitter.emit({
  name: 'event',
  channel: 'ch2'
})

emitter.hasListeners('event')
emitter.removeAllListeners('event')
const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });

  server.route({
    method: 'GET',
    path: '/',
    config: {
      handler(request, h) {
        return 'hello'
      },
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We import the module, then we create the emitter object with it.

Then we register events with the emitter.registerEvent method.

We can separate events into their own channels.

context is the value of this in the event handlers.

So this.count will be updated and logged within the event handlers.

Conclusion

We can create our own event bus with @hapi/podium and handle MIME types with the @hapi/mimos module.

Categories
Hapi

Server-Side Development with Hapi.js — Tokens, JWT, and Secrets

Hapi.js is a small Node framework for developing back end web apps.

In this article, we’ll look at how to create back end apps with Hapi.js.

Create Tokens

We can create tokens with the @hapi/iron module.

For example, we can write:

const Hapi = require('@hapi/hapi');
const iron = require('@hapi/iron')

const obj = {
  a: 1,
  b: 2,
  c: [3, 4, 5],
  d: {
      e: 'f'
  }
};

const password = 'passwordpasswordpasswordpassword';

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });

  server.route({
    method: 'GET',
    path: '/',
    async handler(request, h) {
      try {
        const sealed = await iron.seal(obj, password, iron.defaults);
        return sealed
      } catch (err) {
        console.log(err);
      }
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We call the iron.seal method with the object we want to encrypt, the password to access the encrypted object, and the settings, which is iron.defaults .

Them the sealed string is a scrambled version of the object in string form.

Then to unseal it, we call the iron.unseal method.

For example, we can write:

const Hapi = require('@hapi/hapi');
const iron = require('@hapi/iron')

const obj = {
  a: 1,
  b: 2,
  c: [3, 4, 5],
  d: {
      e: 'f'
  }
};

const password = 'passwordpasswordpasswordpassword';

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });

  server.route({
    method: 'GET',
    path: '/',
    async handler(request, h) {
      try {
        const sealed = await iron.seal(obj, password, iron.defaults);
        const unsealed = await iron.unseal(sealed, password, iron.defaults);
        return unsealed
      } catch (err) {
        console.log(err);
      }
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

to decrypt the encrypted string with the same password with the iron.unseal method.

JSON Web Token

We can create and verify JSON web tokens with the @hapi/jwt module.

For example, we can use it by writing:”

const Hapi = require('@hapi/hapi');
const Jwt = require('@hapi/jwt');
const jwt = require('jsonwebtoken');

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });

  await server.register(Jwt);

  server.auth.strategy('my_jwt_stategy', 'jwt', {
    keys: 'some_shared_secret',
    verify: {
      aud: 'urn:audience:test',
      iss: 'urn:issuer:test',
      sub: false,
      nbf: true,
      exp: true,
      maxAgeSec: 14400,
      timeSkewSec: 15
    },
    validate: (artifacts, request, h) => {
      return {
        isValid: true,
        credentials: { user: artifacts.decoded.payload.user }
      };
    }
  });

  server.route({
    method: 'GET',
    path: '/',
    config: {
      handler(request, h) {
        const token = jwt.sign({
          aud: 'urn:audience:test',
          iss: 'urn:issuer:test',
          sub: false,
          maxAgeSec: 14400,
          timeSkewSec: 15
        }, 'some_shared_secret');
        return token;
      },

}
  });

  server.route({
    method: 'GET',
    path: '/secret',
    config: {
      handler(request, h) {
        return 'secret';
      },
      auth: {
        strategy: 'my_jwt_stategy',
      }
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We add the jsonwebtoken module to create the token.

Then we call server.auth.strategy to add the JWT auth strategy.

The keys have the keys we use to verify the token.

verify has the fields we want to verify.

validate has a function that returns isValid to indicate that the token is valid.

credentials have the data from the decoded token.

Then we call jwt.sign to sign the token in the / route handler.

And in the /secret route, we have the auth.strategy property to set the auth strategy.

Conclusion

We can create various kinds of tokens and verify them with Hapi addons.

Categories
Hapi

Server-Side Development with Hapi.js — File Names, Composing Server Parts, and Process Monitor

Hapi.js is a small Node framework for developing back end web apps.

In this article, we’ll look at how to create back end apps with Hapi.js.

File Utility

We can generate a file path from path segments with the @hapi/file module.

For example, we can write:

const Hapi = require('@hapi/hapi');
const file = require('@hapi/file');

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      const fileName = file.uniqueFilename('/root', '.txt');
      return fileName
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We call the file.uniqueFilename method to create a unique file name.

We should get something like:

/root/1604965754941-1144-2fbcec5c8927e564.txt

returned as the response.

Compose Different Server Components

We can use the @hapi/glue module to compose different server components.

To use it, we write:

index.js

const Hapi = require('@hapi/hapi');
const Glue = require('@hapi/glue');

const manifest = {
  server: {
    port: 3000,
    host: '0.0.0.0'
  },
  register: {
    plugins: [
      {
        plugin: require('./my-plugin'),
        options: {
          uglify: true
        }
      },
    ],
  }
};

const options = {
  relativeTo: __dirname
};

const init = async () => {
  const server = await Glue.compose(manifest, options);

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      return 'hello'
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

my-plugin.js

const myPlugin = {
  name: 'myPlugin',
  version: '1.0.0',
  async register (server, options) {
    server.route({
      method: 'GET',
      path: '/test',
      handler (request, h) {
        return 'hello, world';
      }
    });
  }
};

module.exports = myPlugin

We created a plugin with one route within the my-plugin.js file.

Then in index.js , we have our manifest object with the server options.

register is a property with the plugin properties inside.

The plugins array has an array of plugins we want to register.

options has extra options we want to set for our server.

Then we compose everything together with the Glue.compose method.

It returns the Hapi server object which we can use to add more routes with the server.route method.

Process Monitor

We can add a simple process monitor to our Hapi app with the @hapi/good module.

To add it, we write:

const Hapi = require('@hapi/hapi');

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });

  await server.register({
    plugin: require('@hapi/good'),
    options: {
      ops: {
        interval: 1000
      },
      reporters: {
        myConsoleReporter: [
          {
            module: '@hapi/good-squeeze',
            name: 'Squeeze',
            args: [{ log: '*', response: '*', ops: '*' }]
          },
          {
            module: '@hapi/good-console'
          },
          'stdout'
        ]
      }
    }
  });

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      return 'hello'
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We add the reporters property to add a logger for monitoring resource usage.

We add the @hapi/good-squeeze module to add the items to log with the args array.

log has the timestamp, response has the HTTP response returned, and ops has the resource usage.

The @hapi/good-console lets us log the numbers to the console.

Conclusion

We can add various modules to our Hapi app to monitor resource usage, generate file names, and compose server components.

Categories
Hapi

Server-Side Development with Hapi.js — Content-Type and Crypto

Hapi.js is a small Node framework for developing back end web apps.

In this article, we’ll look at how to create back end apps with Hapi.js.

Parsing Content-Type Header

We can parse the Content-Type request header by using the @hapi/content module.

For instance, we can do this by writing:

const Hapi = require('@hapi/hapi');
const Content = require('@hapi/content');

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      const type = Content.type('application/json; some=property; and="another"');
      return type
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We call Content.type with the Content-Type request header string to parse it.

Then we get:

{"mime":"application/json"}

as the value of type .

Parse Content-Disposition Header

Also, we can use the Content.disposition method to generate an object from the Content-Disposition request header value.

To do this, we write:

const Hapi = require('@hapi/hapi');
const Content = require('@hapi/content');

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });

  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      const disp = Content.disposition('form-data; name="file"; filename=file.jpg');
      return disp
    }
  });

  await server.start();
  console.log('Server running at:', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

Then we get:

{"name":"file","filename":"file.jpg"}

as the value of disp .

CSRF Crumb Generation and Validation

We can generate the CSRF crumb and validate it with the @hapi/crumb module.

For example, we can use it by writing:

const Hapi = require('@hapi/hapi');
const Crumb = require('@hapi/crumb');

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });

  await server.register({
    plugin: Crumb,
    options: {}
  });

  server.route({
    path: '/login',
    method: 'GET',
    options: {
      plugins: {
        crumb: {}
      },
      handler(request, h) {
        return 'success'
      }
    }
  });
  await server.start();
  console.log('Server running at:', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

We register the plugin with the server.register method.

Then we add a /login route that sets the crumbn property to accept the crumb.

Crypto

We can create random strings to use with our Hapi app with the @hapi/cryptiles module.

For instance, we can write:

const Hapi = require('@hapi/hapi');
const cryptiles = require('@hapi/cryptiles');

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });
  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      return cryptiles.randomString(10)
    }
  });
  await server.start();
  console.log('Server running at:', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

to return a random string response.

We generate the random string with the length 10 with cryptiles.randomString(10) .

Also, we can generate an alphanumeric string with cryptiles.randomAlphanumString:

const Hapi = require('@hapi/hapi');
const cryptiles = require('@hapi/cryptiles');

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });
  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      return cryptiles.randomAlphanumString(10)
    }
  });
  await server.start();
  console.log('Server running at:', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

And we can generate a random number with cryptiles.randomDigits:

const Hapi = require('@hapi/hapi');
const cryptiles = require('@hapi/cryptiles');

const init = async () => {
  const server = new Hapi.Server({
    port: 3000,
    host: '0.0.0.0'
  });
  server.route({
    method: 'GET',
    path: '/',
    handler(request, h) {
      return cryptiles.randomDigits(10)
    }
  });
  await server.start();
  console.log('Server running at:', server.info.uri);
};

process.on('unhandledRejection', (err) => {
  console.log(err);
  process.exit(1);
});
init();

Conclusion

We can parse the Content-Type and Content-Disposition header with the @hapi/content module.

And we can create random strings with the @hapi/cryptiles module.