Quantcast
Channel: DEV Community: David Dal Busco
Viewing all 124 articles
Browse latest View live
↧

GitHub Actions: Hide And Set Angular Environment Variables

$
0
0

I share one trick a day until the original scheduled date of the end of the COVID-19 quarantine in Switzerland, April 19th 2020. Eight days left until this first milestone. Hopefully better days are ahead.

Yesterday I suddenly remembered that I still had to create a GitHub Actions to build and deploy the editor of our project DeckDeckGo.

Even though most of the integrations are already automated, this feature is still on my Todo list because I will have to obfuscate some production tokens before being able to properly finish this task.

When I thought about it, I asked my self if I actually had not already solved such feature in another project recently? Guess what, indeed I have 😉, but in an Angular prototype. A small project I developed for my self in order to help me find a flat in ZĂŒrich a couple of weeks ago (Watamato if interested, check it out).

That’s why I am sharing with you today this new tricks.

Concept

Angular, out of the box, let us handle environments variables thanks to the property fileReplacements of our angular.json . Per default, most probably, your project contains two files, an environment.ts and another one for your productive build, environment.prod.ts .

The idea is the following: In environment.prod.ts we are going to define keys without any values, allowing us to push these in our public GitHub repo safely. Then, with the help of system variables, set these before build within our GitHub Actions.

Setup Environment.ts

To begin with, let’s setup first our environment.ts files. Our goal is to obfuscate a token, let’s say for example that we want to hide our Firebase Api key.

Not really related to the solution but let’s say a goodie, we also inject the version and name of our application in your configuration. Note that this requires the activation of the compiler options resolveJsonModule to true in your tsconfig.json.

Our environment.ts :

import{name,version}from'../../package.json';exportconstenvironment={production:false,firebase:{apiKey:'the-key-you-can-expose',},name,version};

And our environment.prod.ts which contains 'undefined' for the hidden value. The reason behind this being a string is the fact that our upcoming parser is going to inject such value if the key is not defined at build time.

exportconstenvironment={production:true,firebase:{apiKey:'undefined'},name:'enviro-replace',version:'0.0.1'};

Hide Development Variables

In the previous setting, I amend the fact that we are agree to expose our key in our development configuration, but you might also want to hide it. In such case, what I recommend, is extracting the values in a separate local file which you explicitly ignore in your .gitignore.

For example, let’s say we create a new file firebase.environment.ts in which we move our configuration and which add to the list of Git ignored files.

exportconstfirebase={firebase:{apiKey:'the-key-you-can-expose',}};

Then we can update our environment.ts as following:

import{firebase}from'./firebase.environment';import{name,version}from'../../package.json';exportconstenvironment={production:false,...firebase,name,version};

Update Variables Before Build

Our productive environment contains at this point an hidden value 'undefined' which we have to replace before building our application.

For such purpose we can use the “magic file” described in the article of Riccardo Andreatta👍.

We create a new script ./config.index.ts . Basically what it does is overwriting our environment.prod.ts file with new values and notably these we are going to define in your environment or GiHub Actions secret store.

In this parser we notice two interesting things:

  1. It contains the environment variables too. That means that if you would add a new key to your configuration, you will have to update the script too.
  2. We are using the environment process process.env.FIREBASE_API_KEY to inject a value we would path from our environment or from GitHub Actions to overwrite the environment with the effective key we were looking to hide.
import{writeFile}from'fs';import{name,version}from'../package.json';consttargetPath='./src/environments/environment.prod.ts';constenvConfigFile=`export const environment = {
   production: true,
   firebase: {
        apiKey: '${process.env.FIREBASE_API_KEY}'
    },
    name: '${name}',
    version: '${version}'
};
`;writeFile(targetPath,envConfigFile,'utf8',(err)=>{if(err){returnconsole.log(err);}});

Finally we can add the execution of the script to our package.json :

"scripts":{"config":"ts-node -O '{\"module\": \"commonjs\"}' ./config.index.ts","build":"npm run config && ng build --prod",}

Testing

We are all set, we can now give it a try. Let’s first run a build without doing anything.

As you can notice, our apiKey remains equals to 'undefined' and therefor not valid for our build.

Let’s now try to define an environment variable (export FIREBASE_API_KEY="this is my prod key") and run our build again.

Tada, our environment variable has been set and use for our build 🎉.

At this point you may ask yourself “yes but David, if we do so, then each time we run a build our environment.prod.ts file is going to be modified”. To which I would answer “yes you are right 
 but our goal is to automate the build with a GitHub Actions in order to not run productive build locally anymore, therefore the modification is not that a problem for our daily workflow 😇”.

GitHub Actions

The very final piece, the automation with GitHub Actions.

I am not going to cover how is it possible to create such script, Julien Renaux covers well the subject in one of his blog post or alternatively you can check out of my Angular related app.yml GitHub actions.

I assume that your script is ready and that you have defined a FIREBASE_API_KEY in your repos’ secrets.

The related build sequence of your application probably looks like the following:

jobs:build:name:Buildruns-on:ubuntu-lateststeps:-name:Checkout Repouses:actions/checkout@master-name:Install Dependenciesrun:npm ci-name:Buildrun:npm run build

To which we now “only” need to add the following:

jobs:build:name:Buildruns-on:ubuntu-lateststeps:-name:Checkout Repouses:actions/checkout@master-name:Install Dependenciesrun:npm ci-name:Buildrun:npm run buildenv:FIREBASE_API_KEY:${{ secrets.FIREBASE_API_KEY }}

That’s already it. Doing so, GitHub Actions will set the related environment variable for our build and our above script and configuration will take care of the rest.

Summary

GitHub Actions are so handy, there were and are a big asset to my continuous integration workflow.

Stay home, stay safe!

David

Cover photo by jae bano on Unsplash

↧

JavaScript Useful Functions

$
0
0

I share one trick a day until the original scheduled date of the end of the COVID-19 quarantine in Switzerland, April 19th 2020. Seven days left until this first milestone. Hopefully better days are ahead.

We are using a couple of JavaScript functions across the multiple applications and Web Components of DeckDeckGo.

Maybe these can be useful for your projects too?

Unifying Mouse And Touch Events

If you are implementing functions which have to do with user interactions through mouse or touch devices, there is a good change that their outcome are the same.

For example, presenters can draw over their presentations using our remote control. For such purpose we use canvas and are attaching events such as:

constcanvas=document.querySelector('canvas');canvas.addEventListener('mousedown',this.startEvent);canvas.addEventListener('touchstart',this.startEvent);

As you can notice, both mouse and touch events are handled by the same functions, which is good, but unfortunately, can’t work out without a bit of logic if you would like to access to positions information as for example clientX or clientY .

Indeed, touch positioning are not available at the root of the object but rather in an array changedTouches .

functionstartEvent($event){// MouseEventconsole.log($event.clientX);// TouchEventconsole.log($event.changedTouches[0].clientX);}

That’s why, we are using a function we have called unifyEvents to get the positioning regardless of the devices.

exportfunctionunifyEvent($event){return$event.changedTouches?$event.changedTouches[0]:$event;}

Which can be use as the following:

functionstartEvent($event){// TouchEvent and MouseEvent unifiedconsole.log(unifyEvent($event).clientX);}

Debouncing

I covered the topic debouncing with JavaScript in a previous article but if you are looking to add such feature to your application without any dependencies, here’s a function to do so.

exportfunctiondebounce(func,timeout?){lettimer;return(...args)=>{constnext=()=>func(...args);if(timer){clearTimeout(timer);}timer=setTimeout(next,timeout&&timeout>0?timeout:300);};}

User Agent

Earlier this year, Google has announced its decision to drop support for the User-Agent string in its Chrome browser (article / GitHub).

Therefor the following methods should be used wisely, knowing they will have to be replaced in the future.

That being said, here are a couple of handy functions which help detects information about the type of browser or device.

isMobile

Detect Mobile Browsers is providing an open source list of mobile devices which can be use to test our navigator to detect if the user is browsing or using the application on a mobile device or not.

exportfunctionisMobile(){if(!window||!navigator){returnfalse;}consta=navigator.userAgent||navigator.vendor||(windowasany).opera;// Regex Source -> http://detectmobilebrowsers.comreturn(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od|ad)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4)));}

isIOS

To detect if our user is using an Apple mobile devices, we can test the navigator against the keywords iPad|iPhone|iPod .

exportfunctionisIOS(){if(!window||!navigator){returnfalse;}consta=navigator.userAgent||navigator.vendor||(windowasany).opera;return/iPad|iPhone|iPod/i.test(a);}

isIPad

Likewise, we can reduce the query to iPad only to guess if the user is on an iPad.

exportfunctionisIPad(){if(!window||!navigator){returnfalse;}consta=navigator.userAgent||navigator.vendor||(windowasany).opera;return/iPad/i.test(a);}

isFirefox

Likewise, you can detect if the client is using a specific browser as for final example Firefox.

exportfunctionisFirefox(){if(!window||!navigator){returnfalse;}consta=navigator.userAgent||navigator.vendor||(windowasany).opera;return/firefox/i.test(a);}

Full screen

Our presentations can be edited and browse in standalone or in full screen mode, that’s why we have to detect such state. For such purpose, we compare the window.innerHeight to the screen.height , if these are equals, the browser is in full screen mode.

exportfunctionisFullscreen(){if(!window||!screen){returnfalse;}returnwindow.innerHeight==screen.height;}

Remove Attribute From HTML String

Let say your DOM contains an element which you would like to parse to a string with the help of JavaScript.

<divcontentEditable="true"style="color: #ff0000">Red</div>
constdiv=document.querySelector('div').outerHTML;

For some reason, you might not be able to touch or clone the DOM element but would be interested to remove an attribute from string value anyway.

Respectively, you would be interested in such result:

<divstyle="color: #ff0000">Red</div>

To clean up or remove an attribute from a string you can use the following handy RegExp:

constresult=div.replace(/(<.*?)(contentEditable=""
|contentEditable="true"|contentEditable="false"|contentEditable)(.*?>)/gi,'$1$3');

Basically, it searches all attribute possibilities and create a new string containing what’s before and after the selection.

My favorite tricks of all of these 😉.

Summary

DeckDeckGo is open source, you can find the above functions in our GitHub repo or their related npm packages.

Stay home, stay safe!

David

Cover photo by Sam Moqadam on Unsplash

↧
↧

Deeplinking in Ionic Apps With Branch.io

$
0
0

I share one trick a day until the original scheduled date of the end of the COVID-19 quarantine in Switzerland, April 19th 2020. Six days left until this first milestone. Hopefully better days are ahead.

Last Friday I had to quickly implement the deeplinking support for an application that we were finalizing. Its goals was, and still is, to help isolated people to feel less alone to find out easily for help within their relatives, something really useful in normal times, specially for older people we think, but maybe even more when there is a crisis.

Turns out the Apple rejected it on Saturday as they think that our project is only COVID-19 focused and only accept related projects from certified organizations 😱. If interested, CNBC published an article about the subject.

Anyway, not here to share my opinion on companies which became more powerful than states and their people but here to share technical tips and tricks 😉.

That’s why here is how I implemented quickly such a concept including how to catch linked parameters.

What’s Deeplinking?

To me deeplinking solve miscellaneous problematics linked to mobile applications:

  1. An app can be available in the App Store, Google Play, as a Progressive Web Apps and probably even in other stores. For all of them, it will be accessible with the help of a different link or URI, therefor, it makes your communication a bit difficult as you have to communicate multiple links (“If you are using Android, click here. If you are using iOS, click here. Etc.). Thanks to deep links it is possible to provide one single URL which link the user to the appropriate resource, appropriate target.

  2. Users may have or not already installed you application. That’s why, when you provide them a link, you might want to link them either straight to it, if already installed, or automatically to the store if they don’t have it yet installed.

  3. Finally, you might have to provide parameters to your application. What would happen if a user click a link, does not have the application, goes to the store, install the application and then start it? The parameters would be lost. That is also were deep linking can help to maintain and provide the parameters until the user effectively start the application.

Deeplinking Vs Custom URI Scheme

Deeplinking should not be confused with a custom URI scheme.

A custom URI scheme, for example myapp:// , is a scheme which can be handle on the mobile devices to call your application but this works only once the application has been installed. Before that point, the devices has no idea of such scheme.

Branch.io

I generally don’t write about none open source solutions but so far, I did not found better solution than Branch.io to setup deep links.

There is a Cordova Ionic plugin to handle such links which is maintained by the community. When Ionic introduced it some years ago I used it at first but finally went for the Branch solution but to be honest I don’t remember exactly why, probably a specific case.

Branch has a starter free plan.

Setup

Branch provide a comprehensive documentation about how to configure their services and even their platform, once you are registered, is pretty straight forward to be used.

Only important things worth to notice: your application has to be available in store before being able to properly setup deep links. When you configure it, you will have to search it and link it with your account, therefor it has to be public and available first, otherwise you won’t be able to finish the configuration.

But, worth to notice, that you don’t have to wait for publishing to already implement and test it.

That’s It

That’s already it! If you only goal is to provide deep links to our, current and potential, users which point either to your app if installed or to the store with a single link, you are done.

No need to install a plugin, regardless if you are using Cordova or Capacitor.

For example, check out the source code of my app Tie Tracker on GitHub. As you notice, there isn’t any references to Branch but even though, I am able to provide a single link https://tietracker.app.link/ which will guide you either to the installed app, the store or if none of these to the PWA.

Intercept Parameters

That being setup, you may be interested to intercept parameters across the all process by keeping in mind of course that tracking is bad and only anonymous usage is acceptable.

Installation

The related Cordova plugin finds place in npm and can be use as following:

ionic cordova plugin add branch-cordova-sdk

Configuration

One installed, you will have to configure it for your platforms. In your config.xml a related entry will have to be added.

All Branch’s information are going to be provided by them when you set up your application and the iOS team release identifier finds place in your Apple “appstoreconnect” dashboard.

<branch-config><branch-keyvalue="key_live_2ad987a7d8798d7a7da87ad8747"/><uri-schemevalue="myapp"/><link-domainvalue="myapp.app.link"/><ios-team-releasevalue="BE88ABJS2W"/></branch-config>

Implementation

Ionic Native provides support for Branch but I never used it so far, that’s why I will not use it in the following example. I also only implemented it in Angular apps with Cordova, that’s why I’m using here such technologies.

In our main component app.component.ts we first declare a new interface which represent the parameter we are interested to intercept. In this example I named it $myparam . Worth to notice that, out of the box, Branch fill forward parameters prefixed with $.

interfaceDeeplinkMatch{$myparam:string;}

Once our main component is initialized, we add a listener on the platform in order to initialize the interception only once it is mounted.

ngAfterViewInit(){this.platform.ready().then(async()=>{awaitthis.initDeepLinking();});}

According the device of the user, he/she may start the application either as a mobile app or as a web application. These are two different ways of querying the parameters and that’s why we are handling the differently.

privateasyncinitDeepLinking():Promise<void>{if(this.platform.is('cordova')){awaitthis.initDeepLinkingBranchio();}else{awaitthis.initDeepLinkingWeb();}}

When it comes to the web, we can search for the parameters using the platform. It they can be provided and prefixed in different ways, sure thing, I like to test all possibilities.

privateasyncinitDeepLinkingWeb():Promise<void>{constmyparam:string=this.platform.getQueryParam('$myparam')||this.platform.getQueryParam('myparam')||this.platform.getQueryParam('%24myparam');console.log('Parameter',myparam);}

Finally, we can handle the mobile app parameter provided by Branch.

Important to notice is the fact that the parameter is provided asynchronously. That’s why you can’t amend that it is present at startup but have to think that it may be provided afterwards with delay.

privateasyncinitDeepLinkingBranchio():Promise<void>{try{constbranchIo=window['Branch'];if(branchIo){constdata:DeeplinkMatch=awaitbranchIo.initSession();if(data.$myparam!==undefined){console.log('Parameter',data.$myparam);}}}catch(err){console.error(err);}}

And voilĂ  đŸ„ł, we are able to handle deep links with parameters. If for example we would provide an URL such as https://myapp.app.link/?$myparam=yolo to our users, we would be able to intercept “yolo” 😁.

Altogether

In case you would need it, here is the above code in a single piece:

import{AfterViewInit,Component}from'@angular/core';import{Platform}from'@ionic/angular';import{SplashScreen}from'@ionic-native/splash-screen/ngx';import{StatusBar}from'@ionic-native/status-bar/ngx';interfaceDeeplinkMatch{$myparam:string;}@Component({selector:'app-root',templateUrl:'app.component.html',styleUrls:['app.component.scss']})exportclassAppComponentimplementsAfterViewInit{constructor(privateplatform:Platform,privatesplashScreen:SplashScreen,privatestatusBar:StatusBar){this.initializeApp();}initializeApp(){this.platform.ready().then(()=>{this.statusBar.styleDefault();this.splashScreen.hide();});}ngAfterViewInit(){this.platform.ready().then(async()=>{awaitthis.initDeepLinking();});}privateasyncinitDeepLinking():Promise<void>{if(this.platform.is('cordova')){awaitthis.initDeepLinkingBranchio();}else{awaitthis.initDeepLinkingWeb();}}privateasyncinitDeepLinkingWeb():Promise<void>{constmyparam:string=this.platform.getQueryParam('$myparam')||this.platform.getQueryParam('myparam')||this.platform.getQueryParam('%24myparam');console.log('Parameter',myparam);}privateasyncinitDeepLinkingBranchio():Promise<void>{try{constbranchIo=window['Branch'];if(branchIo){constdata:DeeplinkMatch=awaitbranchIo.initSession();if(data.$myparam!==undefined){console.log('Parameter',data.$myparam);}}}catch(err){console.error(err);}}}

Summary

It works out. To be honest with you, not the funniest part of the job, not that it is complicated or anything to set up or to be used, just, not really fun.

Stay home, stay safe!

David

Cover photo by Javier Allegue Barros on Unsplash

↧

Follow-up: Web Push Notifications And PWA In 2020

$
0
0

I share one trick a day until the original scheduled date of the end of the COVID-19 quarantine in Switzerland, April 19th 2020. Five days left until this first milestone. Hopefully better days are ahead.

If you follow me on Twitter, you may have read that an application I developed and recently submitted to the stores have been rejected by both Apple and Google because it was not aligned, according them, with their restrictive policy regarding the current COVID-19 pandemic.

I am not writing these lines to share my opinion on these companies, but to share a follow-up to my one year old tutorial: Web Push Notifications in Progressive Web Apps.

Indeed one core concept of the app which was rejected relies on Push Notifications. As it is developed with Ionic and Angular, we are able to unleash a Progressive Web Apps, but is such feature yet well supported?

Introduction

I am writing this article Tuesday 14th April 2020, that’s why it reflects the status of that specific date. If you would read this in the future and notice improvements or changes, ping me!

This afternoon I ran my tests on my Android phone, a OnePlus 6 running Android v10 and on my iPhone 6s running iOS 13.

Android

It works like a charm, period. I tested Web Push Notifications with my phone in idle mode, awake and with the application open. In all cases, I received the notifications. Great work Google 👍.

iOS

Web Push Notifications are still not supported on iOS. The status didn’t change since I published my tutorial in February 2019. As you can notice with the help of Caniuse, the Notifications API is not yet implemented by iOS Safari.

Setup

The Firebase Cloud Messaging set up I displayed in my previous article still remains valid. Of course maybe some screenshots have changed or have been actualized, but the idea remains the same. Moreover, I have set up the tokens for my application in the exact same way and everything went fine.

An interesting thing to notice though, it the comment from Galilo Galilo. According his/her experience, the Firebase dependencies used in the service worker had to be set as the exact same version number as the one used in package.json . I did not had this problem but it is something which might be worth to keep in mind.

Implementation

To the exception of the following deprecation, which can or not be improved, the implementation displayed in my previous tutorial remains also valid. It is the one I have implemented in our application and therefor the one I successfully tested today on my Android phone.

That being said, I think that there might be an easier way, specially if you are using AngularFire, to implement Web Push Notifications in a Progressive Web Apps. I did not check it out but before following my tutorial it maybe deserves a quick research, just in case you would be able to spare some time 😉.

Deprecation

Not a big deal but while having a look at the code I noticed that await messaging.requestPermission(); was marked as deprecated. It can be updated as following:

if(Notification.permission!=='denied'){awaitNotification.requestPermission();}

Altogether

Altogether, my enhanced Angular service which takes care of registering the Web Push Notifications and requesting the permissions.

import{Injectable}from'@angular/core';import{firebase}from'@firebase/app';import'@firebase/messaging';import{environment}from'../../../environments/environment';@Injectable({providedIn:'root'})exportclassFirebaseNotificationsPwaService{asyncinit(){navigator.serviceWorker.ready.then((registration)=>{if(!firebase.messaging.isSupported()){return;}constmessaging=firebase.messaging();messaging.useServiceWorker(registration);messaging.usePublicVapidKey(environment.firebase.vapidKey);messaging.onMessage((payload)=>{// If we want to display // a msg when the app is in foregroundconsole.log(payload);});// Handle token refreshmessaging.onTokenRefresh(()=>{messaging.getToken().then((refreshedToken:string)=>{console.log(refreshedToken);}).catch((err)=>{console.error(err);});});},(err)=>{console.error(err);});}asyncrequestPermission(){if(!Notification){return;}if(!firebase.messaging.isSupported()){return;}try{constmessaging=firebase.messaging();if(Notification.permission!=='denied'){awaitNotification.requestPermission();}consttoken:string=awaitmessaging.getToken();// User tokenconsole.log(token);}catch(err){console.error(err);}}}

Summary

Hopefully one day we will be able to send Web Push Notifications on iOS devices too đŸ€ž.

Stay home, stay safe!

David

Cover photo by Javier Allegue Barros on Unsplash

↧

Angular And Web Workers

$
0
0

I share one trick a day until the original scheduled date of the end of the COVID-19 quarantine in Switzerland, April 19th 2020. Four days left until this first milestone. Hopefully better days are ahead.

It has been a long time since the last time Angular did not make me say out at loud “Wow, that’s pretty neat”, but today was the day again!

Together with my client’s colleagues we had a new requirement which had to do with IndexedDB. For such purpose we notably had to clear the data. As many entries can have been stored, such process can take a while and it was important to not block the UI and the user interaction.

That’s why we developed our feature using Web Workers and why I am sharing this new blog post.

Adding A Web Worker

The Angular team made an outstanding job. Their CLI integration works seamlessly and the documentation is straight forward.

To add a Web Worker, we run the command ng generate web-worker followed by the target location, most commonly our app .

ng generate web-worker app

The command will take care of adding a new TypeScript compiler configuration for our worker but will also generate a sample and its usage within the app.

The sample will find place in ./src/app/app.worker.ts . It contains the TypeScript reference and register a listener which can be called to start its work in the worker thread.

/// <reference lib="webworker" />addEventListener('message',({data})=>{constresponse=`worker response to ${data}`;postMessage(response);});

Its usage will be added to ./src/app/app.component.ts . It tests if workers are supported and if yes, build a new object and call the worker respectively instructs it to start its job.

if(typeofWorker!=='undefined'){// Create a newconstworker=newWorker('./app.worker',{type:'module'});worker.onmessage=({data})=>{console.log(`page got message: ${data}`);};worker.postMessage('hello');}else{// Web Workers are not supported in this environment.// You should add a fallback so that your program still executes correctly.}

Refactor

In order to use this worker, there is a good chance that we might want to refactor it. I personally like to group my workers in a subfolder ./src/app/workers/ . I do not know if it is a best practice or not, but a bit like the services, I think it is cool.

Moreover, we may have more than workers in our app. That’s why I also suggest to rename it, for example, let’s call it hello.worker.ts .

In the same way, we might want to call the worker from a service and not from app.component.ts .

Note that in the following example I also rename the worker and modify the relative path to point to the correct location.

import{Injectable}from'@angular/core';@Injectable({providedIn:'root'})exportclassHelloService{asyncsayHello(){if(typeofWorker!=='undefined'){constworker=newWorker('../workers/hello.worker',{type:'module'});worker.onmessage=({data})=>{console.log(`page got message: ${data}`);};worker.postMessage('hello');}}}

Finally, in order to be able to run a test, I call my service from the main page of my application.

import{Component,OnInit}from'@angular/core';import{HelloService}from'./hello.service';@Component({selector:'app-home',templateUrl:'home.page.html',styleUrls:['home.page.scss'],})exportclassHomePageimplementsOnInit{constructor(privatehelloService:HelloService){}asyncngOnInit(){awaitthis.helloService.sayHello();}}

All set, we can try to run a test. If everything goes according plan, you should be able to discover a message in the console which follow the exchange between the app and the worker.

Simulate A Blocked User Interface

We might like now to test that effectively our worker is performing a job that is not blocking the UI.

I displayed such a test in a previous article about React and Web Worker, that’s why we kind of follow the same idea here too. We create two buttons, once which increment “Tomato” using the JavaScript thread and ultimately one which increment “Apple” using a worker thread. But first, let’s do all the work in the JavaScript thread.

In our main template we add these two buttons and link these with their related functions. We also display two labels to show their current values.

<ion-content[fullscreen]="true"><ion-label>
     Tomato: {{countTomato}} | Apple: {{countApple}}
  </ion-label><divclassName="ion-padding-top"><ion-button(click)="incTomato()"color="primary">Tomato</ion-button><ion-button(click)="incApple()"color="secondary">Apple</ion-button></div></ion-content>

We also implement these states and functions in our main component. Moreover we are adding explicitly a custom delay in our function incApple() in order to simulate a blocking UI interactions.

import{Component,OnInit}from'@angular/core';import{HelloService}from'../services/hello.service';@Component({selector:'app-home',templateUrl:'home.page.html',styleUrls:['home.page.scss'],})exportclassHomePageimplementsOnInit{privatecountTomato=0;privatecountApple=0;constructor(privatehelloService:HelloService){}asyncngOnInit(){awaitthis.helloService.sayHello();}incTomato(){this.countTomato++;}incApple(){conststart=Date.now();while(Date.now()<start+5000){}this.countApple++;}}

If you would test the above in your browser you would effectively notice that as long the “Apple” counter is not resolved, the GUI will not be rendered again and therefor will not been updated.

Defer Work With Web Workers

Let’s now try to solve the situation by deferring this custom made delay to our worker thread.

Web Workers

We move our blocker code to our hello.worker and we also modify it in order to use the data as input for the current counter value.

/// <reference lib="webworker" />addEventListener('message',({data})=>{conststart=Date.now();while(Date.now()<start+5000){}postMessage(data+1);});

Services

To pass data between services and components you can of course either use RxJS or any other global store solution but for simplicity reason I have use a callback to pass by the result from the web worker to our component state.

What it does is creating the worker object and registering a listener onmessage which listen to the result of the web worker and call our callback with it. Finally it calls the worker to start the job with postMessage and provide the current counter as parameter.

import{Injectable}from'@angular/core';@Injectable({providedIn:'root'})exportclassHelloService{asynccountApple(counter:number,updateCounter:(value:number)=>void){if(typeofWorker!=='undefined'){constworker=newWorker('../workers/hello.worker',{type:'module'});worker.onmessage=({data})=>{updateCounter(data);};worker.postMessage(counter);}}}

Component

Our service has changed, that’s why we have to reflect the modification in the component. On the template side nothing needs to be modified but on the code side we have to use the new exposed function countApple from the service and have to provide both current “Apple” counter value and a callback to update this
state once the worker will have finish its computation.

import{Component}from'@angular/core';import{HelloService}from'../services/hello.service';@Component({selector:'app-home',templateUrl:'home.page.html',styleUrls:['home.page.scss'],})exportclassHomePage{privatecountTomato=0;privatecountApple=0;constructor(privatehelloService:HelloService){}incTomato(){this.countTomato++;}asyncincApple(){awaitthis.helloService.countApple(this.countApple,(value:number)=>this.countApple=value);}}

If you would run the example in your browser you should be able to notice that our interaction and UI aren’t blocked anymore, tada 🎉.

Cherry On Top

You know what’s really, but really, cool with this Angular Web Worker integration? You can use your dependencies in your worker too!

For example, if your application is using idb-keyval, you can import it and use it in your worker out of the box, no configuration needed.

/// <reference lib="webworker" />import{set}from'idb-keyval';addEventListener('message',async({data})=>{awaitset('hello','world');postMessage(data);});

Summary

I like Web Workers 😾

Stay home, stay safe!

David

Cover photo by Darya Tryfanava on Unsplash

↧
↧

Git Commands I Always Forget

$
0
0

I share one trick a day until the original scheduled date of the end of the COVID-19 quarantine in Switzerland, April 19th 2020. Three days left until this first milestone. Hopefully better days are ahead.

When it comes to Git, I probably rely too much on its really well-made integration of my editor, WebStorm. This has for consequence, that I sometimes forget useful commands, even simple ones, which I only have rarely to run in my terminal.

That’s why I am sharing this blog post mostly for my future self 😅.

Revert Last Commit

To revert your last commit if it has not been pushed yet, you can rewind the history from a step back with the help of the reset command.

However, if your commit has already been pushed, you can preserve your history and run a revert command (where eec47301 is the revision number of the commit to revert which can be found for example with the help of the git log command) followed by a commit and push.

git revert eec47301

Alternatively revert it without preserving the history with the help of the reset command and the option --hard followed by a push with the option --force.

git reset --hard eec47301

Needless to say, it has to be use wisely.

Change Last Commit Message

If your last commit message was wrong or if you had for example a typo, you can modify the last, or most recent, commit message with the option --amend .

git commit --amend

If your commit has not been pushed yet, nothing else to do. To the contrary, if you already have pushed it, you can update your repo with --force .

git push --force

Change Multiple Commit Messages

If you are looking to amend multiple commit messages, which might happens for example if you did forget to specify the related issue number, you can proceed with the help of rebase .

Credits for this solution goes to these provided by Jaco Pretorius and Linuxize. Not all heroes wear capes!

To start amending we run the following command where 2 is the number of commits on which we want to perform a rebase .

git rebase -i HEAD~2

This will open a prompt which will allow us to specify changes on each commits.

pick e68a142 my frst update
pick 1613f1e my scnd update

As we want to change the commit message, we modify pick with reword (or the short version r ).

reword e68a142 my frst update
reword 1613f1e my scnd update

Once done, we save ( :wq ) and the prompt will automatically guide us to the first selected commit we would like to change.

my frst update

We can correct the message (x to delete a character, is to switch to insert mode, aa to append and always Esc escape editing mode), save it and the prompt will again automatically guide us to the next commit message we would like to change and so on.

my snd update

Once finished, our terminal will look like the following:

❯ git rebase -i HEAD~2

[detached HEAD 1f02610] my first update

Date: Thu Apr 16 15:55:09 2020 +0200

1 file changed, 1 insertion(+), 1 deletion(-)[detached HEAD 68d3edd] my second update

Date: Thu Apr 16 16:00:29 2020 +0200

1 file changed, 4 insertions(+)

Successfully rebased and updated refs/heads/master.

At this point our history is locally rewritten but not yet updated in our repo, that’s why we push these.

git push --force

Abort Rebase

If you would face problems while running the above process, you would be able to cancel the rebase operation using --abort.

git rebase --abort

Abort Merge

Speaking of abort, it is also possible to quit a merge while using the same option.

git merge --abort

Delete A Tag

When you delete a release on GitHub, it does delete it but it does not delete the related tag. Typically, if you go back to your repo with your browser, it is still displayed.

If you would like to remove such tags, you can do so with the help of a Git push and the option--delete followed by the name of the tag to remove.

git push --delete origin v0.0.1

Summary

Hopefully this time I will remember these command lines but if not, at least I will know where to find them 😁.

Stay home, stay safe!

David

Cover photo by Jonatan Lewczuk on Unsplash

↧

An Open Source Medium Like WYSIWYG Editor

$
0
0

I share one trick a day until the original scheduled date of the end of the COVID-19 quarantine in Switzerland, April 19th 2020. Two days left until this first milestone. Hopefully better days are ahead.

For DeckDeckGo our editor for presentations, we have developed many custom made open source Web Components developed with Stencil.

One of these is a Medium Like WYSIWYG editor. It can be integrated in any modern web applications, regardless of its technologies, and works on any devices (desktop, tablet and mobile).

I am actually not sure if I ever shared this component or not but as I spent my day improving its layout, bringing it some attention (love) it well deserved, and release a new version, that’s why I’m sharing it with you with this new blog post 😉.

Installation

We are providing some guidance in our documentation and Stencil is also displaying how any components can be installed in with any frameworks.

Install From A CDN

To fetch the component from a CDN, as for example Unpkg, add the following to the header of your HTML.

<script type="module"src="https://unpkg.com/@deckdeckgo/inline-editor@latest/dist/deckdeckgo-inline-editor/deckdeckgo-inline-editor.esm.js"></script><script nomodule=""src="https://unpkg.com/@deckdeckgo/inline-editor@latest/dist/deckdeckgo-inline-editor/deckdeckgo-inline-editor.js"></script>

Install From NPM

To install the project from npm, run the following command in your terminal:

npm install @deckdeckgo/inline-editor

According your need, either import it:

import'@deckdeckgo/inline-editor';

Or use a custom loader:

import{defineCustomElementsasdeckDeckGoElement}from'@deckdeckgo/inline-editor/dist/loader';deckDeckGoElement();

Add The Component To Your Application

I like when component’s usage is dead simple. To add it to your application, “just” add it to the DOM and you are good to go.

<deckgo-inline-editor></deckgo-inline-editor>

That’s it, you have added a WYSIWYG editor to your application 🎉.

Editable Elements

Per default, the component will make any elements h1 , h2 , h3 , h4 , h5 , h6 and div for which the attribute contenteditable is defined editable.

If like us with our editor, you would have other need, for example we handle content in section , you can override this list with the help of the property containers .

<deckgo-inline-editorcontainers="h1,h2,section"></deckgo-inline-editor/>

As you can notice, as soon as I do so, the following paragraphs ( p ) are not editable anymore even though they are still set as contenteditable .

Container Element

You may like to allow the interaction only with a specific part of your application and not the all document. For such purpose, it does also expose a property attachTo . If you would provide it, then only mouse or touch events coming from that particular container are going to be considered.

Mobile Devices

On mobile devices, it might be not the best UX to use a floating editor. When users are selecting text on their devices, a default list of system OS options (copy, paste, etc.) are automatically displayed which might conflicts with the editor.

That’s why we had the idea to make optionally the editor sticky on mobile devices.

<deckgo-inline-editorcontainers="h1,h2,section"sticky-mobile="true"></deckgo-inline-editor>

Note that the editor is displayed at the top on iOS and bottom on Android. I rather like this last version but I did not find a clever way to solve this on iOS as the Webview is not resized and the keyboard size is not accessible.

Also worth to notice, the component emit an event stickyToolbarActivated when the toolbar is displayed. Thanks to this event, you can for example hide the header or footer of your applications, avoiding a small design glitch of two layers.

List And Alignment

We recently added the ability to modify the alignment, thanks to a Pull Request provided by Akash Borad. Not all heroes wear capes!

Even though, in our editor, we don’t use these as we are offering these two options in our main toolbar. That’s why the component exposes two properties, list and align , in case you would also not use these.

Likewise, images are not taken in account by the component per default. This can be modified with the help of another property, img-editable .

<deckgo-inline-editorcontainers="h1,h2,section"sticky-mobile="true"list="false"align="false"img-editable="true"></deckgo-inline-editor>

Colors

The colors can be modified with the help of our custom made color picker. It has a default palette but it can be overwritten too with its corresponding property palette .

RTL

If your application’s direction is Right-To-Left the component ordering will remains the same, but, automatically, the alignment feature will notice it and will be displayed as such per default.

CSS Customization

I did not count but many CSS variables are available to style the component, notably everything which has to do with colors, backgrounds and selection.

These are displayed in our documentation.

As always, if something is missing or if you would need a feature, ping us on GitHub. Furthermore, Pull Requests are most welcomed 😁.

And More


There are more options, as being able to provide a custom action or make the component also sticky on desktop.

Summary

To speak frankly, this is probably the most complicated component I ever had to develop. I think the fact that it is shadowed and that the Selection API is not yet that friendly with it did not make things easier. Even though I am happy with the results, it works well in our editor and I hope it can someday be useful to someone somewhere too.

Stay home, stay safe!

David

Cover photo by Silviana Toader on Unsplash

↧

Currency Picker And Formatter With Ionic React

$
0
0

I share one trick a day until the original scheduled date of the end of the COVID-19 quarantine in Switzerland, April 19th 2020. One days left until this first milestone. Hopefully better days are ahead.

I was looking for a subject idea for today’s blog post and it came to my mind that I could maybe share something I learned with Tie Tracker⏱, a simple, open source and free time tracking app I have developed with Ionic and React again.

That’s why I’m sharing with you my solution to develop a custom currency picker and formatter.

Start

If you don’t have an Ionic React application yet, you can follow this tutorial by creating a sample one using their CLI.

ionic start

When prompted, select “React”, your application name and for example the template “blank”.

List Of Currencies

We intend to develop a custom currency picker, that’s why we need, a list of currencies. For such purpose, we can download the one provided on the Xsolla repo as it is free and licensed under MIT license.

curl https://raw.githubusercontent.com/xsolla/currency-format/master/currency-format.json -o public/assets/currencies.json

I use curl because I am using a Macbook but what does matter is to save the list of currencies in the assets folder as it will have to be shipped with the app.

TypeScript Definitions

We are going to need a TypeScript definitions to handle the list we just downloaded. That’s why we create following interfaces in ./src/definitions/currency.d.ts .

exportinterfaceCurrency{name:string;fractionSize:number;symbol:{grapheme:string;template:string;rtl:boolean;};uniqSymbol:boolean;}exportinterfaceCurrencies{[currency:string]:Currency;}

Note that I am not sure that using a subfolder definitions is really the best practice, it is just something I do. Do not think it matters that much, I just like to split my code in, kind of, packages.

Modal: Currency Picker

To develop our picker I suggest that we use a modal. It should display the list of available currencies (currency name and abbreviation), allow the user to filter these and ultimately let him/her select one.

We create a new component ./src/components/CurrenciesModal.tsx which receive as properties the current selected currency and a function to close the modal and pass the user selection.

interfaceProps{closeAction:Function;currency:string;}

It contains also two states. The list of currencies and a filtered one, which is, when component mounted, equals to the all list.

const[currencies,setCurrencies]=useState<Currencies|undefined>(undefined);const[filteredCurrencies,setFilteredCurrencies]=useState<Currencies|undefined>(undefined);

To initiate these we use useEffect hooks and we read the JSON data we downloaded before.

useEffect(()=>{initCurrencies();// eslint-disable-next-line react-hooks/exhaustive-deps},[]);useEffect(()=>{setFilteredCurrencies(currencies);},[currencies]);asyncfunctioninitCurrencies(){try{constres:Response=awaitfetch('./assets/currencies.json');if(!res){setCurrencies(undefined);return;}constcurrencies:Currencies=awaitres.json();setCurrencies(currencies);}catch(err){setCurrencies(undefined);}}

To proceed with filtering, we implement a function which read the user inputs and call another one which effectively takes care of applying a filter on the list we maintain as state objects.

asyncfunctiononFilter($event:CustomEvent<KeyboardEvent>){if(!$event){return;}constinput:string=($event.targetasInputTargetEvent).value;if(!input||input===undefined||input===''){setFilteredCurrencies(currencies);}else{constfiltered:Currencies|undefined=awaitfilterCurrencies(input);setFilteredCurrencies(filtered);}}

Finally we implement our modal’s GUI which contains a searchbar and a list of items , the currencies.

<IonSearchbardebounce={500}placeholder="Filter"onIonInput={($event:CustomEvent<KeyboardEvent>)=>onFilter($event)}></IonSearchbar>
<IonList><IonRadioGroupvalue={props.currency}>{renderCurrencies()}</IonRadioGroup>
</IonList>

Altogether our component looks like the following:

importReact,{useEffect,useState}from'react';import{IonList,IonItem,IonToolbar,IonRadioGroup,IonLabel,IonRadio,IonSearchbar,IonContent,IonTitle,IonHeader,IonButtons,IonButton,IonIcon}from'@ionic/react';import{close}from'ionicons/icons';import{Currencies}from'../definitions/currency';interfaceProps{closeAction:Function;currency:string;}interfaceInputTargetEventextendsEventTarget{value:string;}constCurrenciesModal:React.FC<Props>=(props:Props)=>{const[currencies,setCurrencies]=useState<Currencies|undefined>(undefined);const[filteredCurrencies,setFilteredCurrencies]=useState<Currencies|undefined>(undefined);useEffect(()=>{initCurrencies();// eslint-disable-next-line react-hooks/exhaustive-deps},[]);useEffect(()=>{setFilteredCurrencies(currencies);},[currencies]);asyncfunctioninitCurrencies(){try{constres:Response=awaitfetch('./assets/currencies.json');if(!res){setCurrencies(undefined);return;}constcurrencies:Currencies=awaitres.json();setCurrencies(currencies);}catch(err){setCurrencies(undefined);}}asyncfunctiononFilter($event:CustomEvent<KeyboardEvent>){if(!$event){return;}constinput:string=($event.targetasInputTargetEvent).value;if(!input||input===undefined||input===''){setFilteredCurrencies(currencies);}else{constfiltered:Currencies|undefined=awaitfilterCurrencies(input);setFilteredCurrencies(filtered);}}asyncfunctionfilterCurrencies(filter:string):Promise<Currencies|undefined>{if(!currencies){returnundefined;}constresults:Currencies=Object.keys(currencies).filter((key:string)=>{return((key.toLowerCase().indexOf(filter.toLowerCase())>-1)||(currencies[key].name&&currencies[key].name.toLowerCase().indexOf(filter.toLowerCase())>-1));}).reduce((obj:Currencies,key:string)=>{obj[key]=currencies[key];returnobj;},{});returnresults;}return(<><IonHeader><IonToolbarcolor="primary"><IonTitle>Picker</IonTitle>
<IonButtonsslot="start"><IonButtononClick={()=>props.closeAction()}><IonIconicon={close}slot="icon-only"></IonIcon>
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContentclassName="ion-padding"><IonSearchbardebounce={500}placeholder="Filter"onIonInput={($event:CustomEvent<KeyboardEvent>)=>onFilter($event)}></IonSearchbar>
<IonList><IonRadioGroupvalue={props.currency}>{renderCurrencies()}</IonRadioGroup>
</IonList>
</IonContent>
</>
);functionrenderCurrencies(){if(!filteredCurrencies||filteredCurrencies===undefined){returnundefined;}returnObject.keys(filteredCurrencies).map((key:string)=>{return<IonItemkey={`${key}`}onClick={()=>props.closeAction(key)}><IonLabel>{filteredCurrencies[key].name}({key})</IonLabel>
<IonRadiovalue={key}/>
</IonItem>
});}};exportdefaultCurrenciesModal;

Page: Home

Our picker being ready, we can now use it. For such purpose we integrate it to the main page of our application, the home page. We are also adding a state to display the current selected currency which I initialized with CHF as it is the currency of Switzerland.

Moreover, we are also implementing a function to update the currency according the one the user would pick using our above modal.

importReact,{useState}from'react';import{IonContent,IonHeader,IonPage,IonTitle,IonToolbar,IonModal,IonButton,IonLabel}from'@ionic/react';importCurrenciesModalfrom'../components/CurrenciesModal';constHome:React.FC=()=>{const[currency,setCurrency]=useState<string>('CHF');const[showModal,setShowModal]=useState<boolean>(false);functionupdateCurrency(currency?:string|undefined){setShowModal(false);if(!currency){return;}setCurrency(currency);}return(<IonPage><IonHeader><IonToolbar><IonTitle>Home</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent><IonModalisOpen={showModal}onDidDismiss={()=>setShowModal(false)}><CurrenciesModalcurrency={currency}closeAction={updateCurrency}></CurrenciesModal>
</IonModal>
<h1>123.45{currency}</h1>
<IonButtononClick={()=>setShowModal(true)}><IonLabel>Pickcurrency</IonLabel>
</IonButton>
</IonContent>
</IonPage>
);};exportdefaultHome;

If you implemented the above code you should now be able to run the application and pick currencies.

Format Currency

Being able to select a currency is nice, but being able to use it is even better 😉.

To format our amount, we are going to use the standard built-in object Intl.NumberFormat which is now pretty well supported by any browser.

functionformatCurrency(value:number):string{if(currency===undefined){returnnewIntl.NumberFormat('fr').format(0);}returnnewIntl.NumberFormat('fr',{style:'currency',currency:currency}).format(value);}

Note that in the above function I hardcoded french as it is my mother tongue. This can be replaced by the one of your choice or if you are using i18next with the following dynamic language.

importi18nfrom'i18next';functionformatCurrency(value:number):string{if(currency===undefined){returnnewIntl.NumberFormat(i18n.language).format(0);}returnnewIntl.NumberFormat(i18n.language,{style:'currency',currency:currency}).format(value);}

Finally, we are replacing the static display of the value 123.45 {currency} with the function’s call.

<h1>{formatCurrency(123.45)}</h1>

Altogether our main page now should contain the following code:

importReact,{useState}from'react';import{IonContent,IonHeader,IonPage,IonTitle,IonToolbar,IonModal,IonButton,IonLabel}from'@ionic/react';importCurrenciesModalfrom'../components/CurrenciesModal';constHome:React.FC=()=>{const[currency,setCurrency]=useState<string>('CHF');const[showModal,setShowModal]=useState<boolean>(false);functionupdateCurrency(currency?:string|undefined){setShowModal(false);if(!currency){return;}setCurrency(currency);}functionformatCurrency(value:number):string{if(currency===undefined){returnnewIntl.NumberFormat('fr').format(0);}returnnewIntl.NumberFormat('fr',{style:'currency',currency:currency}).format(value);}return(<IonPage><IonHeader><IonToolbar><IonTitle>Home</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent><IonModalisOpen={showModal}onDidDismiss={()=>setShowModal(false)}><CurrenciesModalcurrency={currency}closeAction={updateCurrency}></CurrenciesModal>
</IonModal>
<h1>{formatCurrency(123.45)}</h1>
<IonButtononClick={()=>setShowModal(true)}><IonLabel>Pickcurrency</IonLabel>
</IonButton>
</IonContent>
</IonPage>
);};exportdefaultHome;

Voilà, both our currency picker and formatter are implemented in our Ionic React application 🎉.

Summary

Ionic and React together are really fun. Checkout Tie Tracker and of course your Pull Requests to improve the app are most welcomed 😁.

Stay home, stay safe!

David

Cover photo by Pawel Janiak on Unsplash

↧

Develop A Konami Code For Any Apps With Stencil

$
0
0

I have shared 35 daily ”One Trick A Day” blog posts in a row until today, the original scheduled date of the end of the COVID-19 quarantine in Switzerland, April 19th 2020.

This milestone has been postponed but even though we have to continue the effort, some small positive signs have emerged. Hopefully better days are ahead.

The Konami Code is a cheat code which appeared in many Konami video games which allow(ed) players to reveal hidden features or unlock achievements while pressing a sequence of buttons on their game controller: âŹ†ïž, âŹ†ïž, âŹ‡ïž, âŹ‡ïž, âŹ…ïž, âžĄïž, âŹ…ïž, âžĄïž, đŸ…±ïž, đŸ…°ïž.

As it found a place in the popular culture, many websites or applications are nowadays using it to provide animation which are going to make us, geeks and nerds, smile 😄.

That’s why I thought it was a good example to introduce Stencil and a fun idea to conclude this series of articles.

Get Started

To get started we create a new standalone components using the Cli.

npm init stencil

When prompted, select component as type of starter and provide konami-code as project name. Once over, jump into the directory and install the dependencies.

cd konami-code && npm install

Blank Component

The starter component is created with some “hello world” type code. That’s why, to make this tutorial easier to follow, we firstly “clean it” a bit.

Note that we are not going to rename the packages and files as we would do if we would publish it to npm afterwards.

We edit ./src/component/my-component/my-component.tsx to modify the attribute tag in order to use our component as <konami-code/> . Moreover it will also render “Hadouken!” because “Street Fighter II Turbo” put the regular code in before the initial splash screen to enable turbo up to 8 Stars ⭐.

import{Component,h}from"@stencil/core";@Component({tag:"konami-code",styleUrl:"my-component.css",shadow:true,})exportclassMyComponent{render(){return<div>Hadouken!</div>;
}}

We don’t modify yet the CSS but we do modify the ./src/index.html for test purpose and to reflect the new tag name.

<!DOCTYPE html><htmldir="ltr"lang="en"><head><metacharset="utf-8"><metaname="viewport"content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0"><title>Stencil Component Starter</title><script type="module"src="/build/konami-code.esm.js"></script><script nomodulesrc="/build/konami-code.js"></script></head><body><h1>Konami Code</h1><p>Develop A "Konami Code" For Any Apps With Stencil</p><p>Hit: âŹ†ïž,  âŹ†ïž, âŹ‡ïž, âŹ‡ïž, âŹ…ïž, âžĄïžïž, âŹ…ïž, âžĄïž, đŸ…±ïž, đŸ…°ïž</p><konami-code></konami-code></body></html>

If we run our project ( npm run start ), your default browser should automatically open itself at the address http://localhost:3333 and you should be able to see the following elements rendered:

Detection

Per default we are going to hide our component content and are looking to display it only if a particular sequence of keyboard keys (âŹ†ïž, âŹ†ïž, âŹ‡ïž, âŹ‡ïž, âŹ…ïž, âžĄïž, âŹ…ïž, âžĄïž, đŸ…±ïž, đŸ…°ïž) are going to be hit.

Therefor we can define it in our ./src/components/my-component/my-component.tsx as a readonly array.

privatereadonlykonamiCode:string[]=["ArrowUp","ArrowUp","ArrowDown","ArrowDown","ArrowLeft","ArrowRight","ArrowLeft","ArrowRight","KeyB","KeyA"];

To listen to events, we generally register and unregister EventListener. One of the cool thing of Stencil is that it makes possible to do such things by using decorators. Pretty neat to keep the code clean.

As we are interested to “track” keyboard keys, we are listening to the keydown event.

Moreover, to compare the list of user keys with the code sequence, we save the keys in a new array. We also take care of limiting its maximal length to the exact same length as the sequence (with shift we remove the first object in the array respectively the oldest key kept in memory) and are finally comparing these as string ( join parse array using the provided delimiter).

privatekeys:string[]=[];@Listen("keydown",{target:"document"})onKeydown($event:KeyboardEvent){this.keys.push($event.code);if(this.keys.length>this.konamiCode.length){this.keys.shift();}constmatch=this.konamiCode.join(",")===this.keys.join(",");}

At this point our layout should not change but if we would add a console.log($event.code, match); at the end of our listener function for demo purpose, we should be able to test our component by observing the debugger.

Conditional Rendering

To render conditionally the outcome of our code, we introduce a new state variable, which, if modified, will cause the component render function to be called again.

We are using it to render conditionally our message “Hadouken!”.

import{Component,h,Listen,State}from'@stencil/core';@Component({tag:"konami-code",styleUrl:"my-component.css",shadow:true,})exportclassMyComponent{@State()privatematch:boolean=false;privatereadonlykonamiCode:string[]=["ArrowUp","ArrowUp","ArrowDown","ArrowDown","ArrowLeft","ArrowRight","ArrowLeft","ArrowRight","KeyB","KeyA",];privatekeys:string[]=[];@Listen("keydown",{target:"document"})onKeydown($event:KeyboardEvent){this.keys.push($event.code);if(this.keys.length>this.konamiCode.length){this.keys.shift();}this.match=this.konamiCode.join(",")===this.keys.join(",");}render(){return<div>{this.match?"Hadouken!":undefined}</div>;
}}

If you would test it in your browser, you should now notice that the message as per default disappear but that you are able to make it appearing as soon as you have hit the Konami code sequence 🎉.

Dynamic Content

You may be interested to let users specify their own message rather “Hadouken!”. After all, maybe some would rather like to display “Shoryuken!” 😁.

That’s why we can transform our fixed text to a <slot/> .

render(){return<div>{this.match?<slot>Hadouken!</slot> : undefined}
</div>;
}

Something I learned recently, we can provide a default value to the <slot/>. Doing so, if a slotted element is provided, it will be displayed, if not, the default “Hadouken!” is going to be used.

For example, <konami-code></konami-code> displays “Hadouken!” but <konami-code>Shoryuken!</konami-code> renders, well, “Shoryuken!”.

Style

Even though it does the job, our component can be a bit styled. That’s why instead of a logical render I suggest that we modify it to be visible or not.

We can also maybe display the message in the center of the screen.

That’s why we are introducing a Host element to style the all component conditionally.

render(){return<Hostclass={this.match?'visible':undefined}><div><slot>Hadouken!</slot></div></Host>;
}

Note that the <Host/> element has to be imported from the @stencil/core .

To style the component we modify its related style ./src/components/my-component/my-components.css . We defined our :host , the component, to cover the all screen and we set our message to be displayed in middle of the screen.

Because we are applying the visibility of the message through a class, set or not, on the container we add a related style :host(.visible) to actually display the message.

:host{display:block;position:absolute;top:0;left:0;right:0;bottom:0;z-index:1;visibility:hidden;opacity:0;background:rgba(0,0,0,0.8);transition:opacity250msease-in;}:host(.visible){visibility:inherit;opacity:1;}div{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:white;font-size:4rem;}

If we try out our component again in the browser the result should be a bit more smooth.

Close The Easter Egg

Fine we have displayed smoothly an easter egg in our application if the Konami code sequence is hit but, as you may have noticed, the message remains open once displayed.

There are several possible way to handle this. One quick solution is a click event on the container which reset our match state.

render(){return<Hostclass={this.match?'visible':undefined}onClick={()=>this.match=false}><div><slot>Hadouken!</slot></div></Host>;
}

Just in case, I also suggest to “block” events on the container when not active using style.

:host{pointer-events:none;}:host(.visible){visibility:inherit;opacity:1;}

We are now able to close our message with a mouse click.

Altogether

Altogether our component contains few codes:

import{Component,h,Listen,State,Host}from'@stencil/core';@Component({tag:"konami-code",styleUrl:"my-component.css",shadow:true,})exportclassMyComponent{@State()privatematch:boolean=false;privatereadonlykonamiCode:string[]=["ArrowUp","ArrowUp","ArrowDown","ArrowDown","ArrowLeft","ArrowRight","ArrowLeft","ArrowRight","KeyB","KeyA",];privatekeys:string[]=[];@Listen("keydown",{target:"document"})onKeydown($event:KeyboardEvent){this.keys.push($event.code);if(this.keys.length>this.konamiCode.length){this.keys.shift();}this.match=this.konamiCode.join(",")===this.keys.join(",");}render(){return<Hostclass={this.match?'visible':undefined}onClick={()=>this.match=false}><div><slot>Hadouken!</slot></div></Host>;
}}

Our style is almost as long as our component 😅.

:host{display:block;position:absolute;top:0;left:0;right:0;bottom:0;z-index:1;visibility:hidden;opacity:0;background:rgba(0,0,0,0.8);transition:opacity250msease-in;pointer-events:none;}:host(.visible){visibility:inherit;opacity:1;pointer-events:all;cursor:pointer;}div{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:white;font-size:4rem;}

Bonus

I also wrote a small component to display to keyboard events for the demo purpose, the first Gif of this article. If interested, here’s its code. Nothing particular regarding what we already have implemented.

The only “tricks” to be aware of are these linked to arrays. If you are manipulating one, you have to create new one to trigger a new call of the function render . Moreover, if it is dynamically rendered, it is safer to set a key attribute to each items.

import{Component,h,Listen,State}from'@stencil/core';@Component({tag:"konami-keys",shadow:true,})exportclassMyKeys{@State()privatekeys:string[]=[];@Listen("keydown",{target:"document"})onKeydown($event:KeyboardEvent){this.keys=[...this.keys,$event.code];// 10 being the length of the Konami Codeif(this.keys.length>10){this.keys.shift();}}render(){returnthis.keys.map((key:string,i:number)=>{return<spankey={i}>{this.renderKey(key)}&nbsp;</span>;
});}privaterenderKey(key:string){if(key==="ArrowUp"){return"âŹ†ïž";}elseif(key==="ArrowDown"){return"âŹ‡ïž";}elseif(key==="ArrowLeft"){return"âŹ…ïž";}elseif(key==="ArrowRight"){return"âžĄïž";}elseif(key==="KeyB"){return"đŸ…±ïž";}elseif(key==="KeyA"){return"đŸ…°ïž";}else{returnkey;}}}

Summary

I am aware none of these 35 daily blog posts have helped or will help solved the current crisis. However, I hope that maybe they might help someone, somewhere, someday.

Stay home, stay safe!

David

Cover photo by Mohamed Nohassi on Unsplash

↧
↧

Showcase Your PWA In Your Website

$
0
0

Two weeks ago, Matt Netkow introduced Ionic React in an online presentation of the Ionic ZĂŒrich Meetup.

After a couple of minutes he displayed a features I never ever had thought about before: he showcased in his presentation an interactive embedded apps đŸ€Ż.

This literally let me speachless. What if anybody would be able to embed any interactive applications and websites easily in any slides?

I was convinced as soon as his idea hit my brain and that’s why I am happy to share with you this new feature of DeckDeckGo, our web editor for presentations, and per extension, the new Web Component we are open sourcing!

Credits

It is not the first time Matt inspired me a feature. If the landing page of our project is also a presentation itself, it is also because once he said that he found the idea interesting. Definitely Matt, thank you for the inspiration!

If you are familiar with the Ionic ecosystem, you may already have noticed that the device’s frame showcased and used in the above example really looks like the one used in their documentation. I can’t argue about that and you are totally right. Instead of reinventing the wheel, we used the style code they published under MIT license. Thank you Ionic for everything you do for the web 🙏.

Installation

We are providing some guidance in our documentation and Stencil is also displaying how any components can be installed in with any frameworks.

Install From A CDN

To fetch the component from a CDN, as for example Unpkg, add the following to the header of your HTML.

<scripttype="module"src="https://unpkg.com/@deckdeckgo/demo@latest/dist/deckdeckgo-demo/deckdeckgo-demo.esm.js"></script>
<scriptnomodule=""src="https://unpkg.com/@deckdeckgo/demo@latest/dist/deckdeckgo-demo/deckdeckgo-demo.js"></script>

Install From NPM

To install the project from npm, run the following command in your terminal:

npm install @deckdeckgo/demo

According to your need, either import it:

import'@deckdeckgo/demo';

Or use a custom loader:

import{defineCustomElementsasdeckDeckGoElement}from'@deckdeckgo/demo/dist/loader';deckDeckGoElement();

Showcase Your Applications

To use the component and showcase your applications, use it as following where the property src is the URI of your Progressive Web Apps or website.

Note that we are also setting the property instant to true to render instantly the content as the default behavior of the component is set to be lazy loaded. In case of DeckDeckGo, to maximize the load performances, only the current and next slides’ assets, and therefore iframe too, are loaded iteratively.

<deckgo-demosrc="https://deckdeckgo.app"instant="true"></deckgo-demo>

That’s it, you are showcasing your application 🎉.

Sizing

The component will automatically calculate the size of its content according the host available size.

privateasyncinitSize(){conststyle:CSSStyleDeclaration|undefined=window?window.getComputedStyle(this.el):undefined;constwidth:number=style&&parseInt(style.width)>0?parseInt(style.width):this.el.offsetWidth;constheight:number=style&&parseInt(style.height)>0?parseInt(style.height):this.el.offsetHeight;constdeviceHeight:number=(width*704)/304;this.width=deviceHeight>height?(height*304)/704:width;this.height=deviceHeight>height?height:deviceHeight;}

That’s why, you can either encapsulate it in a container and make it responsive or assign it a size using styling.

<deckgo-demosrc="https://deckdeckgo.app"instant="true"style="width: 40vw; height: 90vh;"></deckgo-demo>

Note also that the component will listen to resizing of the browser. Therefore, each time its size will change, it will resize itself automatically.

window.removeEventListener('resize',debounce(this.onResizeContent,500));privateonResizeContent=async()=>{awaitthis.initSize();awaitthis.updateIFrameSizeReload();};

Worth to notice too that in order to be sure that the content of your integrated app fits correctly, on each resize of the browser, it will be reloaded too. This is achieved with the following ugly beautiful hack to reload cross-domain iframe .

iframe.src=iframe.src;

Summary

Moreover than in slides, in which I definitely see a use case for such components because I am already looking forward to use it for my personal talks, I think it might be useful too, if for example, you are displaying a showcase of your realization in your website.

I also hope it made you eager to give DeckDeckGo a try for your next presentations 😊.

To infinity and beyond!

David

↧

How To Make Your PWA Offline On Demand

$
0
0

Finally!

After the introduction of our web open source editor for presentations DeckDeckGo last year, one of the most requested feature was being able to work offline.

We have now implemented and launched this new capability and that’s why I would like to share with you our learning: how did we develop such a “download content à la Netflix or Spotify” feature for our Progressive Web Apps.

User Experience (UX)

There are many ways to approach the “offline” subject. One approach I can think of is making the all application, including its content, available offline, all the time.

Another one is what I call a “on demand offline content solution à la Spotify or Netflix” solution. An approach you are probably familiar with, as it is the one offered by these platforms which give their users the ability to download locally content, music or movies, only upon requests.

This approach is the one we implemented, and the one I am sharing with you.

Introduction

To make the content of our PWA available offline we proceeded with following steps:

asyncgoOffline(){awaitthis.lazyLoad();awaitthis.saveContent();awaitthis.cacheAssets();awaitthis.toggleOffline();}

Lazy Load

Our presentations are lazy loaded to improve performances. When you are browsing slides, only the current, previous and next one are loaded. Therefore, the first action required in order to go offline is downloading locally all their assets (images, charts data, code languages etc.).

This can also be the case in your app. Imagine you have got a lazy loaded image down at the bottom of a page or in another location not accessed yet by your user. One solution would be to add it to your service worker precaching strategy but if it is dynamic and unknown at build time, you can’t do so.

Fortunately for us, lazy loading is the core of our solution, and it is supported per default by all our Web Components, that’s why in order to start such a process we only had to call one single function.

privatelazyLoad(){returnnewPromise(async(resolve,reject)=>{try{constdeck=document.querySelector('deckgo-deck');if(!deck){reject('Deck not found');return;}awaitdeck.lazyLoadAllContent();resolve();}catch(err){reject(err);}});}

Such process will take care of iterating through all slides and components to load their content. But these are not yet cached automatically unless you would use, as we do, a service worker.

We are relying on Workbox to manage our strategies and are for example caching images as following. Note that we have two distinct strategies in place in order to to avoid CORS and opaque requests issues with third party providers.

workbox.routing.registerRoute(/^(?!.*(?:unsplash|giphy|tenor|firebasestorage))(?=.*(?:png|jpg|jpeg|svg|webp|gif)).*/,newworkbox.strategies.CacheFirst({cacheName:'images',plugins:[newworkbox.expiration.Plugin({maxAgeSeconds:30*24*60*60,maxEntries:60,}),],}));workbox.routing.registerRoute(/^(?=.*(?:unsplash|giphy|tenor|firebasestorage))(?=.*(?:png|jpg|jpeg|svg|webp|gif)).*/,newworkbox.strategies.StaleWhileRevalidate({cacheName:'cors-images',plugins:[newworkbox.expiration.Plugin({maxAgeSeconds:30*24*60*60,maxEntries:60,}),newworkbox.cacheableResponse.CacheableResponse({statuses:[0,200],}),],}));

If you are curious about all strategies we developed, checkout out our sw.js script in our open source repo.

Save Content

As our users won’t have access to internet anymore, they will not be able to reach the database and fetch their content. That is why it has to be save locally.

Even though we are using Cloud Firestore and libraries are already offering an “offline first” feature or support, we implemented our own custom solution.

That’s why, we have developed our own concept with the help of IndexedDB. For example, in the following piece of code we are fetching a deck from the online database and are saving it locally. Worth to notice that we are using the element unique identifier as storage key and the handy idb-keyval store.

import{set}from'idb-keyval';privatesaveDeck(deckId:string):Promise<Deck>{returnnewPromise(async(resolve,reject)=>{// 1. Retrieve data from online DBconstdeck=awaitthis.deckOnlineService.get(deckId);if(!deck||!deck.data){reject('Missing deck');return;}// 2. Save data in IndexedDBawaitset(`/decks/${deck.id}`,deck);resolve(deck);});}

At this point you may ask yourself what’s the point? It is nice to have the content locally saved but it does not mean yet that the user will be able to use it once offline right? Moreover, you may fear that it would need a full rewrite of the application to consume these data isn’t it?

Fortunately, our application was already separated in different layers and with the help of a new global state, which tells if the application is offline or online , we were able to extend our singleton services to make these behave differently with the databases according the mode.

Concretely, if online it interacts with Firestore, if offline, it interacts with IndexedDB.

exportclassDeckService{privatestaticinstance:DeckService;privateconstructor(){// Private constructor, singleton}staticgetInstance(){if(!DeckService.instance){DeckService.instance=newDeckService();}returnDeckService.instance;}asyncget(deckId:string):Promise<Deck>{constoffline=awaitOfflineService.getInstance().status();if(offline!==undefined){returnDeckOfflineService.getInstance().get(deckId);}else{returnDeckOnlineService.getInstance().get(deckId);}}}

The interaction with the online database remained the same, therefore we only had to move the function to a new service.

get(deckId:string):Promise<Deck>{returnnewPromise(async(resolve,reject)=>{constfirestore=firebase.firestore();try{constsnapshot=awaitfirestore.collection('decks').doc(deckId).get();if(!snapshot.exists){reject('Deck not found');return;}constdeck:DeckData=snapshot.data()asDeckData;resolve({id:snapshot.id,data:deck});}catch(err){reject(err);}});}

Once refactored, we had to create its offline counterpart.

get(deckId:string):Promise<Deck>{returnnewPromise(async(resolve,reject)=>{try{constdeck:Deck=awaitget(`/decks/${deckId}`);resolve(deck);}catch(err){reject(err);}});}

As you can notice, we are using the unique identifier as storage key which makes the all system really handy as we are able to fetch data locally almost as we would do if we would do with the online database. Doing so we did not had to modify the other layers of the application, everything was kind of working offline almost out of the box without any further changes.

Cache Assets

So far we were able to save locally the users’ data with IndexedDB, cache the content with the Service Worker, therefore all the presentation is available offline, but isn’t something else missing?

Yes indeed, something is still not cached: the assets of the applications itself.

Again this can be solved with a pre-cache strategy but if we are not able too, you would have to find an alternative.

Ours was the following. We created a new JSON file in which we listed each and every assets we are using, including icons and fonts.

{..."navigation":[{"src":"/icons/ionicons/open.svg","ariaLabel":"Open"},...}

Then, when user requests the offline mode, we iterate through each entry and are calling the Service Worker from the app context to trigger the caching.

asyncfunctioncacheUrls(cacheName:string,urls:string[]){constmyCache=awaitwindow.caches.open(cacheName);awaitmyCache.addAll(urls);}

If you are eager to know more about this specific feature, I published earlier this year another blog post about it.

Toggle Offline

Finally, as everything is cached and the internet access can now safely be turned off, we can save a global state to instruct our application to works in an offline mode.

Go Online

You know what’s really cool with the above solution? As we did not modify or limit any core features by “only” caching and adding some layers in our architecture, our users are not just able to read their content offline, it also remains editableđŸ”„.

This means that when users are back online, they should be able to transfer their local content to the remote database.

Such process follow the same logic as the one we developed.

asyncgoOnline(){awaitthis.uploadContent();awaitthis.toggleOnline();}

All the local content has to be extracted from the IndexedDB and moreover, all local images or other content the user would have added locally has to be transferred to the remote storage.

privateasyncuploadDeck(deck:Deck){awaitthis.uploadDeckLocalUserAssetsToStorage(deck);awaitthis.uploadDeckDataToDb(deck);}

Happy to develop this process further if requested, ping me with your questions 👋.

Summary

I might only had tipped the top of the iceberg with this article, but I hope that I was at least able to share with you the general idea of our learning and solution.

Of course, I would be also super happy, if you would give our editor a try for your next talk 👉 deckdeckgo.com.

To infinity and beyond!

David

Cover photo by Kym Ellis on Unsplash

↧

Dynamically Import CSS

$
0
0

We recently introduced several theming options to showcase your code in your presentations made with our editor, DeckDeckGo.

If you sometimes read my posts, you might already be aware I do care about performances and that I tend to use the lazy loading concept as much as I can. That’s why, when Akash Board provided a PR to add this nice set of themes, even if it already worked out like a charm, I was eager to try out the possibility to lazy load these new CSS values. Even if I would spare only a couple of bytes, I thought it was a good experiment and goal, which was of course achieved, otherwise I would not share this new blog post 😉.

Introduction

The goal of the solution is loading CSS on demand. To achieve such objective, we can take advantage of the JavaScript dynamic import() . Instead of handling static build styles, we defer the loading by integrating the styles as JavaScript code.

In brief, we inject CSS through JavaScript on the fly.

Dynamic Import

Dynamic import() , which allow asynchronous load and execution of script modules, is part of the official TC39 proposal and has been standardized with ECMAScript 2020. Moreover, it is also already supported by transpiler like Webpack or Typescript.

Setup

Before jumping straight to the solution, let’s start a project with Stencil with the command line npm init stencil.

This component, we are about to develop for demonstration purpose, has for goal to render a text with either a “green” or “red” background. That’s why we can add such a property to ./src/components/my-component/my-component.tsx .

import{Component,Prop,h}from'@stencil/core';@Component({tag:'my-component',styleUrl:'my-component.css',shadow:true})exportclassMyComponent{@Prop()theme:'green'|'red'='green'render(){return<divclass={this.theme}>Hello,World!</div>;
}}

As we are applying the property as class name, we should define the related CSS in ./src/components/my-component/my-component.css. Note that we are currently only setting up a demo project, we are not yet implementing the solution, that’s why we add style to CSS file.

:host{display:block;}.red{background:red;}.green{background:green;}

Finally, in addition to the component, we also add a <select/> field, which should allow us to switch between these colors, to the ./src/index.html for test purpose.

<!DOCTYPE html><htmldir="ltr"lang="en"><head><metacharset="utf-8"><metaname="viewport"content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0"><title>Stencil Component Starter</title><script type="module"src="/build/lazy-css.esm.js"></script><script nomodulesrc="/build/lazy-css.js"></script></head><body><my-component></my-component><selectid="themeToggler"onchange="updateTheme()"><optionvalue="green"selected="selected">green</option><optionvalue="red">red</option></select><script type="text/javascript">functionupdateTheme(){consttoggler=document.getElementById('themeToggler');constelem=document.querySelector('my-component');elem.theme=toggler.value;}</script></body></html>

If we now run the local server, npm run start, to test our component with our favorite browser, we should be able to switch between backgrounds.

More important, if we open our debugger, we should also that both our styles .green and .red are loaded. It means that the client side as fetch these two styles, even if would have not used for example one of these two colors.

Solution

Let’s have fun 😜.

Style

First thing first, we remove the style from ./src/components/my-component/my-component.css, from the component’s related CSS.

:host{display:block;}

Functional Component

Because we have removed the static style, we now need a way to apply them on the fly. That’s why we create a functional component which has for goal to inject <style/> node into our shadowed Web Component.

According the theme property, this new component should either apply the “green” or the “red” background.

For simplicity reason, we declare it into our component main script ./src/components/my-component/my-component.tsx .

import{Component,Prop,h,FunctionalComponent,Host,State}from'@stencil/core';constThemeStyle:FunctionalComponent<{style:string}>=({style})=>{return(<style>{`
        :host ${style};
      `}</style>
);};@Component({tag:'my-component',styleUrl:'my-component.css',shadow:true})exportclassMyComponent{@Prop()theme:'green'|'red'='green'@State()privatestyle:string;// TODO: Dynamically import stylerender(){return<Host><ThemeStylestyle={this.style}></ThemeStyle>
<divclass={this.theme}>Hello,World!</div>
</Host>;
}}

Dynamic Import

The component is set to render dynamically our themes, but we do not yet lazy load these. Moreover, our CSS content has been removed. That’s why we create one JavaScript constant for each and every style we want to fetch at runtime. Concretely, in our project, we create a file ./src/components/my-component/red.ts for the “red” theme.

consttheme:string=`{
  background: red;
}`;export{theme};

And another one ./src/components/my-component/green.ts for the “green” style.

consttheme:string=`{
  background: green;
}`;export{theme};

These are the definitions which are going to be executed with the help of dynamic import() which we are finally adding to our component ./src/components/my-component/my-component.tsx .

privateasyncimportTheme():Promise<{theme}>{if(this.theme==='red'){returnimport('./red');}else{returnimport('./green');}}

Note that unfortunately it isn’t possible currently to use dynamic import() with a variable. The reason behind, as far as I understand, is that bundler like Webpack or Rollup, even if scripts are going to be injected at runtime, have to know which code is use or not in order to optimize our bundles. That’s why for example return import(${this.theme}); would not be compliant.

Loading

We have declared our themes and have implemented the import() but we still need to apply the results to the rendering which we do by loading the values when the component is going to be mounted and when the theme property would be modified by declaring a @Watch() .

import{Component,Prop,h,FunctionalComponent,Host,State,Watch}from'@stencil/core';constThemeStyle:FunctionalComponent<{style:string}>=({style})=>{return(<style>{`
        :host ${style};
      `}</style>
);};@Component({tag:'my-component',styleUrl:'my-component.css',shadow:true})exportclassMyComponent{@Prop()theme:'green'|'red'='green'@State()privatestyle:string;asynccomponentWillLoad(){awaitthis.loadTheme();}@Watch('theme')privateasyncloadTheme(){const{theme}=awaitthis.importTheme();this.style=theme;}privateasyncimportTheme():Promise<{theme}>{if(this.theme==='red'){returnimport('./red');}else{returnimport('./green');}}render(){return<Host><ThemeStylestyle={this.style}></ThemeStyle>
<divclass={this.theme}>Hello,World!</div>
</Host>;
}}

Et voilà, we are able to lazy load CSS using dynamic import()🎉.

If we test again our component in the browser using the development server (npm run start ), we should notice that it still renders different background according our selection.

More important, if we observe the debugger, we should also notice that our theme loads on the fly.

Likewise, if we watch out the shadowed elements, we should notice that only the related <style/> node should be contained.

Summary

It was the first time I used dynamic import() to lazy load CSS in a Web Component and I have to admit that I am really happy with the outcome. Moreover, adding these themes for the code displayed in slides made with DeckDeckGo is a really nice improvement I think. Give it a try for your next talk 😁.

To infinity and beyond!

David

Cover photo by Joshua Eckstein on Unsplash

↧

Fullscreen: Practical Tips And Tricks

$
0
0

Photo by Jr Korpa on Unsplash

There are already a dozen of existing tutorial about the Web Fullscreen API, but as I was restyling last Saturday the toolbar for the presenting mode of DeckDeckGo, our editor for presentations, I noticed that I never shared the few useful tricks we have implemented.

These are:

  • How to implement a toggle for the fullscreen mode compatible with any browser
  • Create a Sass mixin to polyfill the fullscreen CSS pseudo-class
  • Hide the mouse cursor on inactivity

Toggle Fullscreen Mode With Any Browser

The API exposes two functions to toggle the mode, requestFullscreen() to enter the fullscreen or exitFullscreen() for its contrary.

functiontoggleFullScreen(){if(!document.fullscreenElement){document.documentElement.requestFullscreen();}else{if(document.exitFullscreen){document.exitFullscreen();}}}

Even if the methods are well supported across browser, you might notice on Caniuse a small yellow note next to some version number.

Caniuse | Full screen API | Jun 9th 2020

Indeed, currently Safari and older browser’s version, are not compatible with the API without prefixing the functions with their respective, well, prefix. That’s why, if you are looking to implement a cross-browser compatible function, it is worth to add these to your method.

functiontoggleFullScreen(){constdoc=window.document;constdocEl=doc.documentElement;constrequestFullScreen=docEl.requestFullscreen||docEl.mozRequestFullScreen||docEl.webkitRequestFullScreen||docEl.msRequestFullscreen;constcancelFullScreen=doc.exitFullscreen||doc.mozCancelFullScreen||doc.webkitExitFullscreen||doc.msExitFullscreen;if(!doc.fullscreenElement&&!doc.mozFullScreenElement&&!doc.webkitFullscreenElement&&!doc.msFullscreenElement){requestFullScreen.call(docEl);}else{cancelFullScreen.call(doc);}}

Note that I found the above code in the *Google Web Fundamentals.*

Sass Mixin

The :fullscreen CSS pseudo-class (documented here) is useful to style element according the fullscreen mode.

#myId:fullscreen{background:red;}#myId:not(:fullscreen){background:blue;}

It is well supported across browser, as displayed by Caniuse, but you might also again notice some limitation, specially when it comes to Safari. That’s why it might be interesting to polyfill the pseudo-class.

Caniuse | Full screen API | Jun 9th 2020

Moreover, if many elements have to be tweaked regarding the mode, it might interesting to use Sass and a mixin. That’s why, here is the one we are using.

@mixinfullscreen(){#{if(&,"&","*")}:-moz-full-screen{@content;}#{if(&,"&","*")}:-webkit-full-screen{@content;}#{if(&,"&","*")}:-ms-fullscreen{@content;}#{if(&,"&","*")}:fullscreen{@content;}}

With its help, you can now declare it once and group all your fullscreen styles.

@includefullscreen(){#myId{background:blue;}#myMenu{display:none;}#myFooter{background:yellow;}}

I have the filling that I did not write this mixin by myself, entirely at least, but I could not figured out anymore where I did find it, as I am using it since a while now. If you are her/his author, let me know. I would be happy to give you the credits!

Hide Mouse Pointer On Inactivity

Do you also notice, when a presenter has her/his presentation displayed in fullscreen, if the mouse cursor is still displayed somewhere on the screen?

I do notice it and I rather like to have it hidden 😆. And with rather like I mean that when I noticed this behavior in DeckDeckGo, I had to develop a solution asap. even if I was spending surf holidays in India (you can check my GitHub commit history, I am not joking, true story đŸ€Ł).

In order to detect the inactivity, we listen to the event mousemove. Each time the event is fired, we reset a timer and delay the modification of the style cursor to hide the mouse. Likewise, if we are toggling between fullscreen and normal mode, we proceed with the same function.

<!DOCTYPE html><htmldir="ltr"lang="en"><head><metacharset="utf-8"/><metaname="viewport"content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0"/><title>Hide mouse on inactivity</title></head><bodystyle="margin: 0; overflow: hidden;"><script type="text/javascript">letidleMouseTimer;document.addEventListener('mousemove',mouseTimer);functionmouseTimer(){showHideMouseCursor(true);clearTimer();if(!isFullscreen()){return;}idleMouseTimer=setTimeout(async()=>{showHideMouseCursor(false);},2000);}functionclearTimer(){if(idleMouseTimer>0){clearTimeout(idleMouseTimer);}}functionisFullscreen(){returnwindow.innerHeight==screen.height;}functionshowHideMouseCursor(show){document.querySelector('div').style.setProperty('cursor',show?'initial':'none');}functiontoggleFullScreen(){constdoc=window.document;constdocEl=doc.documentElement;constrequestFullScreen=docEl.requestFullscreen||docEl.mozRequestFullScreen||docEl.webkitRequestFullScreen||docEl.msRequestFullscreen;constcancelFullScreen=doc.exitFullscreen||doc.mozCancelFullScreen||doc.webkitExitFullscreen||doc.msExitFullscreen;if(!doc.fullscreenElement&&!doc.mozFullScreenElement&&!doc.webkitFullscreenElement&&!doc.msFullscreenElement){requestFullScreen.call(docEl);}else{cancelFullScreen.call(doc);}mouseTimer();}</script><divstyle="display: block; width: 100vw; height: 100vh;"><buttononclick="toggleFullScreen()"style="position: absolute; 
                     top: 50%; left: 50%; 
                     transform: translate(-50%, -50%);">
        Toggle fullscreen
      </button></div></body></html>

Conclusion

I hope that these above tips, we did apply in our editor and developer kit, are going to be useful to someone, somewhere, someday. If you have any questions, ping me with any comments.

Give a try to DeckDeckGo for your next presentation 😇.

To infinity and beyond

David

↧
↧

The State Of Progressive Web Apps Adoption By Developers

$
0
0

Photo by YTCount on Unsplash

Following this week’s “Apple vs Hey” story I was curious to know if Progressive Web Apps, which can be seen as a solution to such issues, are mostly preached by developers or are actually already adopted too đŸ€”.

To answer my questions, I ran some polls on Twitter and learned some interesting facts and figures which I would like to share with you in this new blog post.

Limitation

Neither am I an expert in statistics nor am I an expert in running and interpreting polls. Moreover, the experiment happened on Twitter, the panel of answers were given by the people I can reach through this social network and therefore it might probably not be enough representative.

That’s why do not take anything you are about to read as granted. Please do see these figures and comments as hints, nothing more, nothing less.

Also please do note that I am a Progressive Web Apps aficionado. I can try to remain impartial but of course in case of equality, I might be more positive than negative 😉.

47% Of The Developers Do Not Use PWA

I am using almost on a daily basis, the Progressive Web Apps of DEV.to and I am now giving a try to the Twitter one too. But beside these, I do not think that I am using any others on my phone on a weekly basis. That’s why I was firstly interested by the question of adoption.

To my surprise, 47% of the developers are not using any PWA, installed on their phone home screen, on a weekly basis.

Even though I tend to think that probably the number should be lower in reality, as many developers are probably using PWA with their browser, this number is quite important.

Of course the main reason for such a rejection rate, as the next chapter will display, is the lack of proper support for PWA on iOS.

Nevertheless, it still means that almost one of two developers do not use PWA and this makes me personally a bit “worried” for the future of the technology on mobile devices. How is it ever going to be accepted by a wider audience, if even developers who program the applications do not or cannot use these?

On the other hand and on a positive note, I should probably not see the glass half empty but rather half full, **29% **of the developers, one of three, are using two or more PWA on a weekly basis. This instantly makes me believe again 😁.

It is also interesting to note that some developers are not pinning PWA on their home screen but are using multiple (4+) PWA on their desktops on a weekly basis.

This leads me to the following idea: What if the future of Progressive Web Apps is actually not the on mobile phones (in a first place) but rather on desktops? What if actually desktops are going to make these popular before they become popular on mobile phones afterwards? If these assumption are true, would that then mean that Google was right with its Chrome Book since the begin? Or does Google push forward PWA because they believe in Chrome Book?

That is probably too much interpretation or assumptions, but I am really looking forward to the future to get to know if this evolves in this way or not.

63% Of Those Who Do Not Use PWA Are On iOS

As I said above, no surprise here, the partial support on iOS is the main reason why the developers do not use PWA. 63% of these who do not use PWA have iPhones. It means that 30% of all developers are not using PWA simply because they have a phone made by Apple.

Moreover than the partial support, it also seems, again according comments I received, that developers are giving up using PWA on iOS because Apple are not displaying any automatic “Add to home screen” popup, making them confuse about where to find the option. Apple forbidding any other browser to implement such a feature on their devices does not ease the problem.

I gave it a try and that’s correct, the UX is kind of frustrating but it is possible to install PWA on your iOS home screen. If you are interested, proceed as I displayed in the following tweet.

Note that you can add any websites to your home screen but only these which are proper PWA are going to act as stand-alone app. Thank you Julio for your accurate feedbacks👍.

Speaking of, I also took the opportunity to gave a try to the “Add to home screen” UX of the Firefox mobile on Android, as I never tried it before. Believe me or not, I think it is actually the best one. The Chrome one is good but I almost had the feeling that Firefox was taking me by the hand and told me “Here David, come, I gonna show you how you can properly add a PWA to your phone”.

I do not know if any designer at Firefox will ever read these lines, but if you do, congratulations, amazing work!

8% Of The Developers Rather Like Apps From Google Play

If 30% of all developers are not using PWA because of their iPhones, 17% do not use these too, even though they own Android phones which are, at least to my eyes, really “PWA friendly”. That’s why I ran a final poll to figure out why?

It took me some times to think about possible solutions and I fear that my suggestions made the poll a bit too oriented. Maybe I should have better use an open question.

That being said, it seems that most developers, 44% of these who do not use PWA on Android, 7% of all of them, do not use Progressive Web Apps but rather get applications for the store, respectively Google Play, because they feel like their UX or design is better.

To be honest with you, I do not know how to interpret this fact. To me, there can be ugly and not performant web applications as much as there can be bad “native” applications (or coming from store) for the same reasons. I think that it is all about concept and execution. Regardless of the technologies, if badly implemented or designed, it will not be stunning at the end.

Worth to notice: Following a feedback which mentioned that PWA are maybe most suited for low end devices, I was curious to know if Kaios does support Progressive Web Apps. Guess what? They do not just support PWA, it is also possible to publish these in their store.

Conclusion

Maybe the future of Progressive Web Apps is the desktop? Or maybe its future on mobile devices is the stores, as it is possible to publish them in both Kaios store and Google Play? Or maybe one day the EU will be able to make Apple become PWA friendly? Who knows


But for sure, I learned some interesting hints and I still do believe in Progressive Web Apps for the future.

Moreover, I just added a reminder for June 2021 in my calendar to run such polls again, let’s see next year how the subject evolved.

Meanwhile, I am most looking forward to hear your feedbacks and thought. Ping me with your best comments!

To infinity and beyond

David

↧

Bundle A CSS Library

$
0
0

We have build DeckDeckGo in a relatively fragmented way 😅. There is our web editor, which can be used to create and showcase slides and which can also be automatically synchronized with our remote control. There is also a developer kit, which supports HTML or markdown and there is even another version of it, we only used to deploy online your presentations as Progressive Web Apps.

All these multiple applications and kits have in common the fact that they share the exact same core and features, regardless of their technologies, thanks to Web Components made with Stencil.

These do also have to share the same layout capabilities. For example, if we define a 32px root font size in full screen mode, it should be applied anywhere and therefore, should be spread easily and consistently across our eco system.

That’s why we had to create our own custom CSS library and why I am sharing with you, how you can also bundle such a utility for your own projects.

Credits

This solution is the one implemented by the CSS framework Bulma. No need to reinvent the wheel when it is already wonderfully solved. Thank you Bulma for being open source🙏.

Getting Started

To initialize our library, we create a new folder, for example bundle-css, and are describing it with a new package.json file. It should contain the name of the library, its version, which is the main file, in our case an (upcoming) sass entry file, the author and a license. Of course, you can add more details, but these give us a quick basis.

{"name":"bundle-css","version":"1.0.0","main":"index.scss","author":"David","license":"MIT"}

In a new folder src we create our style sheet file index.scss . As I was mentioning the fullscreen mode in my introduction, we can for example add a full screen specific style to our file to apply a blue background to the children paragraphs of a “main” element.

:fullscreen#main{p{background:#3880ff;}}

Clean output

We might probably want to ensure that every time we build our lib, the outcome does not contain any previous style we would have deleted previously.

That’s why we firstly add rimraf to our project to delete the output folder at begin of each build.

npm i rimraf -D

Note that all dependencies we are adding to our project have to be added as development dependencies because none of these are part of the output.

Once rimraf installed, we can initiate our build by editing the scripts in package.json .

"scripts":{"build":"rimraf css"}

I selected css for the name of the output folder which will contains the CSS output. You can use another name, what does matter, is adding it to the file package.json in order to include it in the final bundle you might later install in your app or publish to npm.

"files":["css"]

At this point, altogether, our package.json should contains the following:

{"name":"bundle-css","version":"1.0.0","main":"index.scss","scripts":{"build":"rimraf css"},"files":["css"],"author":"David","license":"MIT","devDependencies":{"rimraf":"^3.0.2"}}

SASS

We are using the SASS extension to edit the styles. Therefore, we have to compile it to CSS. For such purpose, we are using the node-sass compiler.

npm i node-sass -D

We enhance our package.json with a new script, which should take care of compiling to CSS, and we are chaining it with our main build script.

"scripts":{"build":"rimraf css && npm run build-sass","build-sass":"node-sass --output-style expanded src/index.scss ./css/index.css"}

We provide the input file and specify the output as compilation parameters. We are also using the option expanded to determine the output format of the CSS. It makes it readable and, as we are about to minify it at a later stage of the pipeline, we do not have yet to spare the size of the newlines.

If we give a try to our build script by running npm run build , we should discover an output file /css/index.css which has been converted from SASS to CSS .

:fullscreen#mainp{background:#3880ff;}

Autoprefixing

In order to support older browser and Safari, it is worth to prefix the selector :fullscreen . This can also be the case for other selectors. To parse automatically CSS and add vendor prefixes to CSS rules, using values from Can I Use, we are using autoprefixer.

npm i autoprefixer postcss-cli -D

We are now, again, adding a new build script to our package.json and are chaining this step after the two we have already declared because our goal is to prefix the values once the CSS has been created.

"scripts":{"build":"rimraf css && npm run build-sass && npm run build-autoprefix",..."build-autoprefix":"postcss --use autoprefixer --map --output ./css/index.css ./css/index.css"}

With this new command, we are overwriting the CSS file with the new values, and are generating a map file which can be useful for debugging purpose.

If we run our build pipeline npm run build , the output css folder should now contain a map file and our index.css output which should have been prefixed as following:

:-webkit-full-screen#mainp{background:#3880ff;}:-ms-fullscreen#mainp{background:#3880ff;}:fullscreen#mainp{background:#3880ff;}/*# sourceMappingURL=index.css.map */

Optimization

Less is always better, that’s why we are in addition optimizing our CSS library with the help of clean-css.

npm i clean-css-cli -D

By adding a last script to our chain, we are able to create a minified version of our CSS file.

"scripts":{"build":"rimraf css && npm run build-sass && npm run build-autoprefix && npm run build-cleancss",..."build-cleancss":"cleancss -o ./css/index.min.css ./css/index.css"}

If we run one last time npm run build we should now discover the minified version of our input CSS in the output folder css .

:-webkit-full-screen#mainp{background:#3880ff}:-ms-fullscreen#mainp{background:#3880ff}:fullscreen#mainp{background:#3880ff}

Altogether

Summarized, here’s the package.json which contains all dependencies and build steps to create our own custom CSS library.

{"name":"bundle-css","version":"1.0.0","main":"index.scss","scripts":{"build":"rimraf css && npm run build-sass && npm run build-autoprefix && npm run build-cleancss","build-sass":"node-sass --output-style expanded src/index.scss ./css/index.css","build-autoprefix":"postcss --use autoprefixer --map --output ./css/index.css ./css/index.css","build-cleancss":"cleancss -o ./css/index.min.css ./css/index.css"},"files":["css"],"author":"David","license":"MIT","devDependencies":{"autoprefixer":"^9.8.4","clean-css-cli":"^4.3.0","node-sass":"^4.14.1","postcss-cli":"^7.1.1","rimraf":"^3.0.2"}}

Summary

Thanks to many open source projects it is possible to create quickly and easily a library for our custom CSS, that’s amazing.

Give a try to DeckDeckGo for your next talk and if you are up to contribute with some improvements to our common deck styles build following above steeps, your help is more than welcomed 😃.

To infinity and beyond!

David

Cover photo by KOBU Agency on Unsplash

↧

App Shortcuts And Maskable Icons: Play It Like Twitter

$
0
0

We recently added App Shortcuts and maskable icons to DeckDeckGo. While I was reading tutorials to implement these features, I came across some questions regarding design such as:

  • What’s the size of the safe area?
  • Should the shortcuts’ icons colors be contrasting?
  • Shortcuts icons are maskable icons or regular ones?
  • Can both maskable and regular icons find place together?

While I would have probably be able to solve these questions by my self, by reading more carefully the related blog posts or documentations, I had instead the idea, that I can do what also works well in such a situation: do the "copycat" 😅.

Twitter, which I am using on a daily basis, is a great example of Progressive Web Apps. Moreover, I am guessing that they have some budget to invest in UX and design development. Therefore, why not using their best practice to unleash our features instead of reinventing the wheels.

Thank you Twitter 🙏

Sneak Peek

In this post I share the answers and resources we used to add App Shortcuts and maskable icons to our editor for presentations.

App Shortcuts

App Shortcuts are now supported by Chrome version 84 and Microsoft Edge.

Moreover, Progressive Web Apps available in Google Play do also support shortcuts. We are using the PWA builder to convert our PWA to the store’s requested TWA format. They recently upgraded their tool to also automatically supports these links, so once again, thank your PWA builder team for the improvements 👍.

Between all tutorials, the one of web.dev is a great resource to get started. It describes which options can or should be provided to add such shortcuts to a web app.

Summarized, it works with a lit of shortcuts which can be added to the web app manifest . For example, we can create two shortcuts as following:

{"name":"Player FM","start_url":"https://player.fm?utm_source=homescreen",
"shortcuts":[{"name":"Open Play Later","short_name":"Play Later","description":"View the list of podcasts","url":"/play-later?utm_source=homescreen","icons":[{"src":"/icons/play.png","sizes":"192x192"}]},{"name":"View Subscriptions","short_name":"Subscriptions","description":"View the list of podcasts you listen to","url":"/subscriptions?utm_source=homescreen","icons":[{"src":"/icons/subs.png","sizes":"192x192"}]}]}

Moreover than the technical questions, I was asking my self which colors are the most suited for these icons? Our logo being blue, should I also provide blue icons? Or should I use contrasting colors? Or other colors taken from our palette and identity?

That’s where Twitter came to the rescue for first time 😉.

I began by having a look at the HTML source of Twitter to find out the url of their web app manifest .

Once found, I opened the manifest file and had a look at their shortcuts section. They are providing four shortcuts (“New Tweet”, “Explore”, “Notifications” and “Direct Messages”).

Finally, I opened these icons to answer my question: yes, they don’t use their primary color as background for the shortcut’s icons but rather use a contrasting color and the primary as color of the symbol. I also noticed that the contrasting color was not white but rather a light grey (#F5F5F5).

Furthermore, it also answered another question I was asking my self: no, shortcuts icons are not maskable, these are regular icons and therefore, can be round.

As I was about to finalize my icons with these colors, a new question bumped into my mind: what about the safe area?

To solve this new question, I downloaded the Twitter shortcut icon, imported it in my design tool (Affinity Designer) and resized mine as it matched the same size. Told you, why reinventing the wheels đŸ€·.

That was it. My icons were ready to be added as shortcuts.

Summary

Not a summary of what are App Shortcuts but a summary of what Twitter does regarding my related questions:

  1. Use a contrasting color for the background of the shortcuts icons and use your primary color for the symbol.
  2. For these background, a light grey color (#F5F5F5) can be used.
  3. The shortcuts’ icons are regular icons. 192x192 PNG. They can be round.

Maskable Icons

Maskable is an icon format to use adaptive icons on supporting platforms. In other terms: provide an icon to your Progressive Web Apps which can be either displayed in a circle, rounded rectangle, square, drop or cylinder according the device expectations.

The web.dev team provided again a handy tutorial about the subject and even linked some nice tools, such as Maskable.app, which help create such icons.

While I was developing these for our editor, even though everything seemed clear at first, I was not sure at some point if only maskable icons had to be provided or if I had to provide regular icons too? If both can be provided, how exactly the purpose field of the web app manifest had to be specified?

I notably was unsure about the best way to approach this because when I ran the first test, I noticed that Chrome was displaying in its tab bar our super cool logo as a square which to my eyes, wasn’t that cute anymore in comparison to a circle or as previously, our shiny bubble.

Once again, Twitter come to the rescue. I checked their manifest and noticed that indeed they are providing four icons.

I noticed that they are providing two pairs of icons, the regular and maskable one, both with two sizes, 192x192 and 512x512, and more important, that they are providing the field purpose only for the maskable one.

I set our definition the same way. I was happy to notice that maskable icons were still use on my Android phone and that our sweet logo was back in the form I wanted in the Chrome tab bar đŸ„ł.

Summary

Not a summary of how to apply and create maskable icons but a summary of what Twitter does regarding my related questions:

  1. Yes, both regular and maskable icons can be provided.
  2. If so, the field purpose of the icons in the web app manifest should be only specified for the maskable ones, respectively the regular ones don’t have to have a purpose .

Altogether

Because we are open source, let me point out the fact that you can find these related icons in our repo and our following web app manifest as well.

{"name":"DeckDeckGo","short_name":"DeckDeckGo","display":"standalone","theme_color":"#ffffff","background_color":"#3a81fe","scope":"/","start_url":"/","orientation":"portrait","icons":[{"src":"/assets/favicon/icon-default-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/assets/favicon/icon-default-512x512.png","sizes":"512x512","type":"image/png"},{"purpose":"maskable","src":"/assets/favicon/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"purpose":"maskable","src":"/assets/favicon/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"shortcuts":[{"name":"Write a presentation","short_name":"Write a presentation","description":"Start writing a new presentation","url":"/editor","icons":[{"src":"/assets/favicon/shortcut-editor-192x192.png","sizes":"192x192"}]},{"name":"Dashboard","short_name":"Dashboard","description":"Access your dashboard","url":"/dashboard","icons":[{"src":"/assets/favicon/shortcut-dashboard-192x192.png","sizes":"192x192"}]}]}

Summary

Beside hoping that this blog post will be useful to someone, someday, I hope that it also transmit the idea that approaching design and UX questions is also having a look at what other do.

I was asked a couple of times by other developers how I design apps, even though I am not a designer, don’t feel like one and probably never gonna be one.

My answer is always them same: I kind of see my self as a “copycat”. I find things I like, I try my best to develop by myself something “a-ok” inspired from these.

Having a dope designer in a team is definitely an asset but if unfortunately you cannot have one, such approach might help I hope. Inspiration is somehow an essential part of the creation no?

To infinity and beyond!

David

Cover photo by Ravi Sharma on Unsplash

↧

Professional presentation building, tailored to your brand.

$
0
0

Did you have a look at Bob’s presentation? He used the wrong logo, again.

It's astonishing how much the look and feel of presentations vary, even within the same company. Templates, formatting, color palettes, fonts, logo – you name it, they are never the same across two decks. Presentations are rarely consistent, even when companies invest time and effort in brand identity and design guidelines.

Today we are thrilled to announce that DeckDeckGo is receiving support for templates for enterprise customers. Create a template once and all presentations will then use the same.

Templates And Options That Match Your CI/CD

With this new feature, we aim to provide you with a new kind of editor for presentations and to assist you in the implementation of your brand's CI/CD into your slides. In close collaboration with your designer and adhering to your guidelines, we will create a set of unbreakable templates to which you can apply your design, logo, colors, fonts, styles and any other specifications. These templates will then be used by all your employees and collaborators going forward, thereby streamlining your company's visual identity.

That's not all! Below are some other features we are rolling out for enterprise customers.

More Than Static Content

Templates are made with the web and have no other limits than the web's own. It means that if your business offers web solutions, you can integrate these within your slides.

Collaborate And Share A Library

Teams are nothing without good communication. That’s why multiple users can edit the same presentation in real time and share ideas and comments.

What's more, collaborators can now upload and use your company's media resources and assets in their own slide decks.

Always Up-To-Date

Over time, your corporate style will evolve. We simplify this stylistic growth.

Custom Domain, In Housing and Share Privately

You can publish your decks under your own domain name. It's also possible to create authenticated links for private content.

Finally, if you prefer to integrate DeckDeckGo into your infrastructure and private network instead of using our SaaS platform, we are here to help.

And So Much More

That rounds up the new features for our enterprise customers - but we have already released a ton of things for everyone else!

Edit Anywhere, Showcase Everywhere

The editor is available anywhere, it is a Progressive Web App!

Presentations work on computers, phones, tablets and more. They are published as standalone apps.

Polls

Engage with your audience in real time by running polls within your slides. Get them involved during your presentations with their smartphones and show the results live.

Editing

Syntax-highlighted code, math formulas, charts, YouTube, Unsplash, Tenor, Google Fonts. We integrated the most useful services directly in the editor and are providing many components to render information smoothly.

Remote control

Control your presentations remotely, draw on your slides and set a timer from your phone or tablet.

Speaker notes

Write speaker notes for your slides. Embed them in your presentation when and where needed.

Open Source

DeckDeckGo is open source. All of the code from our applications and components is available on Github.

We encourage companies to adopt this approach, but we do understand if you prefer to keep your corporate templates private.

Developer Kit

All public features used by our editor are available as a developer kit. Therefore, if you would rather prepare your presentation with HTML or Markdown, go for it by using npm init deckeckgo.

Get In Touch

I hope the above introduction (and giving DeckDeckGo a try for yourself) made you curious about our solutions for enterprise clients. Get in touch for a tailored discussion about your needs by sending us an email.

To infinity and beyond!

David

Cover photo by Headway on Unsplash

↧
↧

Ionic: Fullscreen Modal & Menu Popover

$
0
0

Photo by Dino Reichmuth on Unsplash

Ionic is not just an amazing design system made for mobile devices but also work like a charm on desktop. We use it at DeckDeckGo particularly for that reason.

While we developed our editor for presentations, we implemented the following two tricks I am about to share with you and which I hope, may be one day useful to you too.

Fullscreen Modal

Out of the box, regardless which platform’s style is applied, as soon as your browser reaches the size of 768x600 pixels, an Ionic modal will not be displayed fullscreen anymore but rather as a centered popup.

While that might fits almost all the use cases, there might be one which would request a full screen modal. Indeed, you might want to present multiple information to the user, therefore need space, but might not want to add a page in the history stack.

For example, users editing their slides with our editor are able to ship their decks as Progressive Web Apps. Upon request, we are packaging their presentations in PWAs and are deploying these online. As it is quite a process and something which the user has to explicitly trigger, we have decided to present all the information in a modal rather than using a navigation, avoiding possible back and forth errors 😇.

Assuming you are not looking to make all your modals fullscreen but only some, I suggest that we style the modals with the help of a CSS class selector which we can apply as displayed on the documentation.

Angular:

asyncpresentModal(){constmodal=awaitthis.modalController.create({component:ModalPage,cssClass:'fullscreen'});awaitmodal.present();}

Stencil:

asyncpresentModal(){constmodal=awaitmodalController.create({component:'app-page-modal',cssClass:'fullscreen'});awaitmodal.present();}

React:

<IonModalisOpen={showModal}cssClass="fullscreen"><PageModal></PageModal>
</IonModal>

Vue:

<script>importModalfrom'./modal.vue'exportdefault{methods:{openModal(){returnthis.$ionic.modalController.create({component:Modal,cssClass:'fullscreen'}).then(m=>m.present())},},}</script>

The style, .fullscreen, should be defined on the application level and not higher in the hierarchy than ion-app, because the modals are going to be injected in the DOM in ion-modal elements which are direct descendant of the app container. For example, in our Stencil application I defined it in app.css or, in a React one, I define it in a stylesheet I import in App.tsx .

It should contain the information to apply a full screen sizing (width/height) and indication that the modal should not be displayed with rounded corner.

ion-modal.fullscreen{--width:100%;--height:100%;--border-radius:0;}

That’s it, nothing more, nothing less 😄.

Popover Menu

Not sure anyone else would actually have the following requirement, but you might need, as we did in DeckDeckGo, a menu which is not tied to the navigation respectively which is not the ion-menu .

For example, we had to find a solution to display options without hiding all the content when our users were editing their slides. Even though we could have developed a custom popup for such purpose, we thought that reusing the Ionic popover would be nice. I mean, look at that sweat animation triggered on opening đŸ€©.

As in previous chapter about the modal, I am assuming that we might want to only apply this effect on specific popovers of our application, that’s why we are again going to use a CSS style class. Moreover, we also want to explicitly use the mode md to give the popover a “flat” style and avoid the display of a backdrop. This last point is not mandatory but make sense if you want your user to be still able to see clearly what’s next to the “popover menu”.

Angular:

asyncpresentPopover(){constpopover=awaitthis.popoverController.create({component:PopoverPage,cssClass:'menu',mode:'md',showBackdrop:false});awaitpopover.present();}

Stencil:

asyncpresentPopover(){constpopover=awaitpopoverController.create({component:'app-page-popover',cssClass:'menu',mode:'md',showBackdrop:false});awaitpopover.present();}

React:

<IonPopoverisOpen={showPopover}cssClass="menu"mode="md"showBackdrop={false}><PagePopover></PagePopover>
</IonPopover>

Vue:

<script>importPopoverfrom'./popover.vue'exportdefault{methods:{openPopover(){returnthis.$ionic.popoverController.create({component:Popover,cssClass:'menu',mode:'md',showBackdrop:true}).then(m=>m.present())},},}</script>

We define the style on the root level of the application because the ion-popover elements are added as direct children of the main ion-app element.

We set a width, for example 540px, and a maximal value because we want to fit smaller devices too.

ion-popover.menu{--width:540px;--max-width:100%;}

Thanks to this definition, we were able to change the width of the popover, but we not yet able to set the correct position, the right side of the screen, and specify a height which covers the all window.

Even though we are going to achieve our goal, I have to say that unfortunately, it is only possible with the help of the infamous !important selector. I opened a feature request about it a while ago and it is one of these suggestions which is so rarely used, that the solution should come from the community, what makes sense to my eyes. Unfortunately too, I did not have time yet to provide a pull request, maybe someday.

Meanwhile, let’s use the following style. First of all, we set the popover to the top right and we also transform its origin to match that position too. Moreover, we set a default height to 100% to match the screen and add a bit of strictly styling as defining a background and a light box-shadow.

ion-popover.menudiv.popover-content{top:0!important;left:inherit!important;right:0;transform-origin:righttop!important;--min-height:100%;background:white;box-shadow:-8px016pxrgba(0,0,0,0.12);border-radius:0;}

That’s it, our popover can act as a menu 😃.

Conclusion

Give a try to DeckDeckGo for your next presentation and if you are up to improve these styles or have any other suggestion, please do collaborate to our project in GitHub, we are welcoming any contributions and idea.

To infinity and beyond!

David

↧

Re-implementing document.execCommand()

$
0
0

This feature is obsolete. Although it may still work in some browsers, its use is discouraged since it could be removed at any time. Try to avoid using it. — MDN web docs

Without a clear explanation on why nor when, document.execCommand() has been marked as obsolete in the MDN web docs. Fun fact, it is not marked as deprecated in all languages, as for example French or Spanish which do not mention anything 😜.

For DeckDeckGo, an open source web editor for slides, we have developed and published a custom WYSIWYG editor which relied on such feature.

Because it may be future proof to proactively replace its usage by a custom implementation, I spent quite some time re-implementing it 😄.

Even though my implementation does not look that bad (I hope), I kind of feel, I had to re-implement the wheel. That’s why I am sharing with you my solution, hoping that some of you might point out some improvements or even better, send us pull requests to make the component rock solid 🙏.

Introduction

One thing I like about our WYSIWYG editor is its cross devices compatibility. It works on desktop as on mobile devices where, instead of appearing as a floating popup, it will be attached either at the top (iOS) or bottom of the viewport (Android) according how the keyboard behaves.

It can change text style (bold, italic, underline and strikethrough), fore- and background color, alignment (left, center or right), lists (ordered and not ordered) and even exposes a slot for a custom action.

Limitation

My following re-implementation of document.execCommand do seems to work well but, it does not support an undo functionality (yet), what’s still a bummer 😕.

As for the code itself, I am open to any suggestions, ping me with your best ideas!

Goal

The objective shared in the blog post is the re-implementation of following functions (source MDN web docs):

document.execCommand(aCommandName,aShowDefaultUI,aValueArgument)
  • bold: Toggles bold on/off for the selection or at the insertion point.
  • italic: Toggles italics on/off for the selection or at the insertion point.
  • **underline: **Toggles underline on/off for the selection or at the insertion point.
  • strikeThrough: Toggles strikethrough on/off for the selection or at the insertion point.
  • foreColor: Changes a font color for the selection or at the insertion point. This requires a hexadecimal color value string as a value argument.
  • backColor: Changes the document background color.

Implementation

I feel more comfortable using TypeScript when I develop, well, anything JavaScript related, that’s why the following code is type and why I also began the implementation by declaring an interface for the actions.

exportinterfaceExecCommandStyle{style:'color'|'background-color'|'font-size'|'font-weight'|'font-style'|'text-decoration';value:string;initial:(element:HTMLElement|null)=>Promise<boolean>;}

Instead of trying to create new elements as the actual API does per default, I decided that it should instead modifies CSS attributes. The value can take for example the value bold if the style is font-weight or #ccc if a color is applied. The interface also contains a function initial which I am going to use to determine is a style should be applied or removed.

Once the interface declared, I began the implementation of the function will take cares of applying the style. It begin by capturing the user selected text, the selection , and identifying its container . Interesting to notice that the container can either be the text itself or the parent element of the selection.

It is also worth to notice that the function takes a second parameter containers which defines a list of elements in which the function can be applied. Per default h1,h2,h3,h4,h5,h6,div . I introduced this limitation to not iterate through the all DOM when searching for information.

exportasyncfunctionexecCommandStyle(action:ExecCommandStyle,containers:string){constselection:Selection|null=awaitgetSelection();if(!selection){return;}constanchorNode:Node=selection.anchorNode;if(!anchorNode){return;}constcontainer:HTMLElement=anchorNode.nodeType!==Node.TEXT_NODE&&anchorNode.nodeType!==Node.COMMENT_NODE?(anchorNodeasHTMLElement):anchorNode.parentElement;// TODO: next chapter}asyncfunctiongetSelection():Promise<Selection|null>{if(window&&window.getSelection){returnwindow.getSelection();}elseif(document&&document.getSelection){returndocument.getSelection();}elseif(document&&(documentasany).selection){return(documentasany).selection.createRange().text;}returnnull;}

The idea is to style the text with CSS attributes. That's why I am going to convert the user's selection into span.

Even though, I thought that it would be better to not always add new elements to the DOM. For example, if a user select a background color red and then green for the exact same selection, it is probably better to modify the existing style rather than adding a span child to another span with both the same CSS attributes. That’s why I implemented a text based comparison to either updateSelection or replaceSelection .

constsameSelection:boolean=container&&container.innerText===selection.toString();if(sameSelection&&!isContainer(containers,container)&&container.style[action.style]!==undefined){awaitupdateSelection(container,action,containers);return;}awaitreplaceSelection(container,action,selection,containers);

Update Selection

By updating the selection, I mean applying the new style to an existing element. For example transforming <span style="background-color: red;"/> to <span style="background-color: green;"/> because the user selected a new background color.

Furthermore, when user applies a selection, I noticed, as for example with MS Word, that the children should inherit the new selection. That’s why after having applied the style, I created another function to clean the style of the children.

asyncfunctionupdateSelection(container:HTMLElement,action:ExecCommandStyle,containers:string){container.style[action.style]=awaitgetStyleValue(container,action,containers);awaitcleanChildren(action,container);}

Applying the style needs a bit more work than setting a new value. Indeed, as for example with bold or italic , the user might want to apply it, then remove it, then apply it again, then remove it again etc.

asyncfunctiongetStyleValue(container:HTMLElement,action:ExecCommandStyle,containers:string):Promise<string>{if(!container){returnaction.value;}if(awaitaction.initial(container)){return'initial';}conststyle:Node|null=awaitfindStyleNode(container,action.style,containers);if(awaitaction.initial(styleasHTMLElement)){return'initial';}returnaction.value;}

In case of bold , the initial function is a simple check on the attribute.

{style:'font-weight',value:'bold',initial:(element:HTMLElement|null)=>Promise.resolve(element&&element.style['font-weight']==='bold')}

When it comes to color, it becomes a bit more tricky as the value can either be an hex or a rgb value. That’s why I had to check both.

{style:this.action,value:$event.detail.hex,// The result of our color pickerinitial:(element:HTMLElement|null)=>{returnnewPromise<boolean>(async(resolve)=>{constrgb:string=awaithexToRgb($event.detail.hex);resolve(element&&(element.style[this.action]===$event.detail.hex||element.style[this.action]===`rgb(${rgb})`));});}

With the help of such definition, I can check if style should be added or removed respectively set to initial .

Unfortunately, it is not enough. The container might inherit its style from a parent as for example <div style="font-weight: bold"><span/></div> . That’s why I created the method findStyleNode which recursively iterates till it either find an element with the same style or the container.

asyncfunctionfindStyleNode(node:Node,style:string,containers:string):Promise<Node|null>{// Just in caseif(node.nodeName.toUpperCase()==='HTML'||node.nodeName.toUpperCase()==='BODY'){returnnull;}if(!node.parentNode){returnnull;}if(DeckdeckgoInlineEditorUtils.isContainer(containers,node)){returnnull;}consthasStyle:boolean=(nodeasHTMLElement).style[style]!==null&&(nodeasHTMLElement).style[style]!==undefined&&(nodeasHTMLElement).style[style]!=='';if(hasStyle){returnnode;}returnawaitfindStyleNode(node.parentNode,style,containers);}

Finally, the style can be applied and cleanChildren can be executed. It is also a recursive method but instead of iterating to the top of the DOM tree, in iterates to the bottom of the container until it has processed all children.

asyncfunctioncleanChildren(action:ExecCommandStyle,span:HTMLSpanElement){if(!span.hasChildNodes()){return;}// Clean direct (> *) children with same styleconstchildren:HTMLElement[]=Array.from(span.children).filter((element:HTMLElement)=>{returnelement.style[action.style]!==undefined&&element.style[action.style]!=='';})asHTMLElement[];if(children&&children.length>0){children.forEach((element:HTMLElement)=>{element.style[action.style]='';if(element.getAttribute('style')===''||element.style===null){element.removeAttribute('style');}});}// Direct children (> *) may have children (*) to be clean tooconstcleanChildrenChildren:Promise<void>[]=Array.from(span.children).map((element:HTMLElement)=>{returncleanChildren(action,element);});if(!cleanChildrenChildren||cleanChildrenChildren.length<=0){return;}awaitPromise.all(cleanChildrenChildren);}

Replace Selection

Replacing a selection to apply a style is a bit less verbose fortunately. With the help of a range, I extract a fragment which can be added as content of new span .

asyncfunctionreplaceSelection(container:HTMLElement,action:ExecCommandStyle,selection:Selection,containers:string){constrange:Range=selection.getRangeAt(0);constfragment:DocumentFragment=range.extractContents();constspan:HTMLSpanElement=awaitcreateSpan(container,action,containers);span.appendChild(fragment);awaitcleanChildren(action,span);awaitflattenChildren(action,span);range.insertNode(span);selection.selectAllChildren(span);}

To apply the style to the new span , fortunately, I can reuse the function getStyleValue as already introduced in the previous chapter.

asyncfunctioncreateSpan(container:HTMLElement,action:ExecCommandStyle,containers:string):Promise<HTMLSpanElement>{constspan:HTMLSpanElement=document.createElement('span');span.style[action.style]=awaitgetStyleValue(container,action,containers);returnspan;}

Likewise, once the new span is created, and the fragment applied, I have to cleanChildren to apply the new style to all descendants. Fortunately, again, that function is the same as the one introduced in the previous chapter.

Finally, because I am looking to avoid span elements without style, I created a function flattenChildren which aims to find children of the new style and which, after having been cleaned, do not contain any styles at all anymore. If I find such elements, I convert these back to text node.

asyncfunctionflattenChildren(action:ExecCommandStyle,span:HTMLSpanElement){if(!span.hasChildNodes()){return;}// Flatten direct (> *) children with no styleconstchildren:HTMLElement[]=Array.from(span.children).filter((element:HTMLElement)=>{conststyle:string|null=element.getAttribute('style');return!style||style==='';})asHTMLElement[];if(children&&children.length>0){children.forEach((element:HTMLElement)=>{conststyledChildren:NodeListOf<HTMLElement>=element.querySelectorAll('[style]');if(!styledChildren||styledChildren.length===0){consttext:Text=document.createTextNode(element.textContent);element.parentElement.replaceChild(text,element);}});return;}// Direct children (> *) may have children (*) to flatten tooconstflattenChildrenChildren:Promise<void>[]=Array.from(span.children).map((element:HTMLElement)=>{returnflattenChildren(action,element);});if(!flattenChildrenChildren||flattenChildrenChildren.length<=0){return;}awaitPromise.all(flattenChildrenChildren);}

Altogether

You can find the all code introduced in this blog post in our repo, more precisely:

If you are looking to try it out locally, you will need to clone our mono-repo.

Conclusion

As I am reaching the conclusion of this blog post, looking back at it once again, I am honestly not sure anyone will ever understand my explanations 😅. I hope that at least it has aroused your curiosity for our WYSIWYG component and generally speaking, for our editor.

Give a try to DeckDeckGo to compose your next slides and ping us with your best ideas and feedbacks afterwards 😁.

To infinity and beyond!

David

Cover photo by Nathan Rodriguez on Unsplash

↧

Send Email From Firebase Cloud Functions

$
0
0

As you can probably imagine, at DeckDeckGo, we do not have any collaborator who check that the publicly, published, slides have descent content. Neither do we have implemented a machine learning robot which would do so, yet.

I am taking care of such a task manually. I have to add, it makes me happy to do so. All the presentations published so far are always interesting.

Nevertheless, I have to be informed, when such decks are published. That’s why I have implemented a Firebase Cloud Functions to send my self an email with all the information I need to quickly review the new content.

Setup A New Cloud Function

I assume you already have a Firebase project and, also have already created some functions. If not, you can follow the following guide to get started.

Moreover, note that I am using TypeScript.

Let’s Get Started

A function needs a trigger, that’s why we are registering a function in index.ts on a collection called, for example, demo (of course your collection can have a different name).

import*asfunctionsfrom'firebase-functions';exportconstwatchCreate=functions.firestore.document('demo/{demoId}').onCreate(onCreateSendEmail);

We can use any other triggers or lifecycle, not necessary the create one.

To respond to the trigger's execution, we declare a new function which retrieve the newly created value (const demo = snap.data() ) and are adding, for now on, a TODO which should be replaced with the effective method to send email.

import{EventContext}from"firebase-functions";import{DocumentSnapshot}from"firebase-functions/lib/providers/firestore";interfaceDemo{content:string;}asyncfunctiononCreateSendEmail(snap:DocumentSnapshot,_context:EventContext){constdemo:Demo=snap.data()asDemo;try{// TODO: send email}catch(err){console.error(err);}}

Nodemailer

In order to effectively send email, we are going to use Nodemailer.

Nodemailer is a module for Node.js applications to allow easy as cake email sending. The project got started back in 2010 when there was no sane option to send email messages, today it is the solution most Node.js users turn to by default.

Nodemailer is licensed under MIT license.

As you can notice, Nodemailer is not just compatible with Firebase Cloud Functions but also with any Node.js projects.

To install it in our project, we run the following command:

npm install nodemailer --save

Furthermore, we also install its typings definition.

npm install @types/nodemailer --save-dev

SMTP Transport

Nodemailer uses SMTP as the main transport to deliver messages. Therefore, your email delivery provider should support such protocol. It also supports either the LTS or STARTTLS extension. In this post we are going to use STARTTLS and therefore are going to set the flag secure to false to activate this protocol.

You can find all options in the library documentation.

Configuration

Specially if your project is open source, you might be interested to not hardcode your SMTP login, password, and host in your code but rather hide these in a configuration.

Firebase offers such ability. We can create a script to set these.

#!/bin/sh

firebase functions:config:set mail.from="hello@domain.com" mail.pwd="password" mail.to="david@domain.com" mail.host="mail.provider.com"

To retrieve the configuration in our function, we can access the configuration through functions.config() followed by the keys we just defined above.

constmailFrom:string=functions.config().mail.from;constmailPwd:string=functions.config().mail.pwd;constmailTo:string=functions.config().mail.to;constmailHost:string=functions.config().mail.host;

Send Email

We have the transport, we have the configuration, we just need the final piece: the message.

I rather like to send my self HTML email, allowing me to include links in the
content, that’s why here too, we are using such a format.

constmailOptions={from:mailFrom,to:mailTo,subject:'Hello World',html:`<p>${demo.content}</p>`};

Finally, we can use Nodemailer to create the channel and ultimately send our email.

consttransporter:Mail=nodemailer.createTransport({host:mailHost,port:587,secure:false,// STARTTLSauth:{type:'LOGIN',user:mailFrom,pass:mailPwd}});awaittransporter.sendMail(mailOptions);

Altogether

All in all, our function is the following:

import*asfunctionsfrom'firebase-functions';import{EventContext}from"firebase-functions";import{DocumentSnapshot}from"firebase-functions/lib/providers/firestore";import*asMailfrom"nodemailer/lib/mailer";import*asnodemailerfrom"nodemailer";exportconstwatchCreate=functions.firestore.document('demo/{demoId}').onCreate(onCreateSendEmail);interfaceDemo{content:string;}asyncfunctiononCreateSendEmail(snap:DocumentSnapshot,_context:EventContext){constdemo:Demo=snap.data()asDemo;try{constmailFrom:string=functions.config().info.mail.from;constmailPwd:string=functions.config().info.mail.pwd;constmailTo:string=functions.config().info.mail.to;constmailHost:string=functions.config().info.mail.host;constmailOptions={from:mailFrom,to:mailTo,subject:'Hello World',html:`<p>${demo.content}</p>`};consttransporter:Mail=nodemailer.createTransport({host:mailHost,port:587,secure:false,// STARTTLSauth:{type:'LOGIN',user:mailFrom,pass:mailPwd}});awaittransporter.sendMail(mailOptions);}catch(err){console.error(err);}}

Summary

With the help of a Firebase and Nodemailer, it is possible to relatively quickly set up a function which triggers email. I hope this introduction gave you some hints on how to implement such a feature and that you are going to give a try to DeckDeckGo for your next presentations.

I am looking forward to receiving an email telling me that I have to check your published slides 😉.

To infinity and beyond!

David

Cover photo by Volodymyr Hryshchenko on Unsplash

↧
Viewing all 124 articles
Browse latest View live