Tag: let’s

Let’s Make a Fancy, but Uncomplicated Page Loader

It’s pretty common to see a loading state on sites these days, particularly as progressive web apps and reactive sites are on the rise. It’s one way to improve “perceived” performance — that is, making it feel as though the site is loading faster than it actually is.

There’s no shortage of ways to make a loader — all it takes is a quick search on CodePen to see oodles of examples, from animated GIFs to complex animations. While it’s tempting to build the fanciest of the fancy loaders, we can actually do a pretty darn good job with only a minimal amount of CSS and JavaScript.

Here’s an example we can make together.

See the Pen
Preloader with JavaScript FadeOut
by Maks Akymenko (@maximakymenko)
on CodePen.

SVG and CSS is all we need for the spinner

I’m going to assume that you’ve already created a project, so we’ll jump right in and start with the spinner — or “pre-loader” as it is also called.

SVG is a great option for a spinner. It’s scalable and implementing it is as easy as an image tag. We could make one from scratch, say in an image editor like Illustrator, Sketch or Figma. For this example, I’m just going to grab this one from loading.io, which is a nice resource to make and customize different loaders.

Now that we have an SVG for the visual, we can drop it into HTML:

<div class="preloader">   <img src="spinner.svg" alt="spinner"> </div>

We’re using .preloader as a wrapper, mostly because it helps us position the image on the page, but also because it will help us hide and reveal the page content while the loader works.

Let’s style it up:

.preloader {   align-items: center;   background: rgb(23, 22, 22);   display: flex;   height: 100vh;   justify-content: center;   left: 0;   position: fixed;   top: 0;   transition: opacity 0.3s linear;   width: 100%;   z-index: 9999; }

This is doing a few things that are more than cosmetic:

  • We’re displaying the loader directly in the center of the screen, using flexbox properties and values.
  • We’ve made the element take up the entire width and height of the screen and given it a black (well, actually a really, really dark gray) background. That means anything behind it (like the page content) is completely hidden. If our page was a different background color (e.g. white), then we would adjust the loader’s background color accordingly.
  • The position is fixed so scrolling won’t affect it’s location on the page. Plus, the z-index is high enough that not other element should stack on top of it and block it from view.

This is what we should see so far when opening it up in the browser:

A circle loader that fades out into a black background.

Some light JavaScript handles the hiding

We’ve got a fancy spinner that covers the entire page, whether we’re viewing this on small or large screens. No we can write some logic to make it fade out after a certain amount of time. That’ll take a small dose of JavaScript.

First, let’s select the .preloader element we just styled up:

const preloader = document.querySelector('.preloader');

It would actually be a lot easier to add a class to the loader that sets its opacity to zero. However, it wouldn’t be as smooth as what we’re doing here, which is using a tiny helping of JavaScript to create a fadeOut function.

const fadeEffect = setInterval(() => {   // if we don't set opacity 1 in CSS, then   // it will be equaled to "" -- that's why   // we check it, and if so, set opacity to 1   if (!preloader.style.opacity) {     preloader.style.opacity = 1;   }   if (preloader.style.opacity > 0) {     preloader.style.opacity -= 0.1;   } else {     clearInterval(fadeEffect);   } }, 100);

💡 jQuery has a function that does this right out of the box. Leverage that if you’re already using jQuery in your project. Otherwise, rolling it with vanilla JavaScript is the way to go.

Let me explain the JavaScript a little bit. As the comment says, if our .preloader element doesn’t have the opacity property set, then it will be equal to empty ("") and we can set it to a value of 1 manually to make sure it displays when the document loads.

Once we know the opacity is set, then we set manipulate it. The whole function is wrapped in setInterval and we check if the opacity property every 100 milliseconds to see if it is greater than zero. As long as it is above zero, we decrease its value in 0.1 increment, which creates a smooth effect that fades the element out over time.

Once we hit zero opacity, we clearInterval to stop the script from running infinitely. Feel free to play around with timing and decreasing points to fit your needs.

The last thing that left to do is to call the function. We’ll call it when the window loads:

window.addEventListener('load', fadeEffect);

We are intentionally using the load event instead of DOMContentLoaded because the DOMContentLoaded event is fired when the document has been completely loaded and parsed. That means it doesn’t *wait for stylesheets, images, and subframes to finish loading* *before it executes*. The load event can be used to detect a fully-loaded page, and that is exactly what we are looking for. Otherwise, the function would start before our CSS and SVG are ready.

Drop some content into the HTML and try things out. Here’s the demo again:

See the Pen
Preloader with JavaScript FadeOut
by Maks Akymenko (@maximakymenko)
on CodePen.


Congratulations! You now know how to build a pretty nice loading effect using nothing but an image and a pinch of CSS and JavaScript. Enjoy!

The post Let’s Make a Fancy, but Uncomplicated Page Loader appeared first on CSS-Tricks.

CSS-Tricks

, , , ,

Let’s Not Forget About Container Queries

Container queries are always on the top of the list of requested improvements to CSS. The general sentiment is that if we had container queries, we wouldn’t write as many global media queries based on page size. That’s because we’re actually trying to control a more scoped container, and the only reason we use media queries for that now is because it’s the best tool we have in CSS. I absolutely believe that.

There is another sentiment that goes around once in a while that goes something like: “you developers think you need container queries but you really don’t.” I am not a fan of that. It seems terribly obvious that we would do good things with them if they were available, not the least of which is writing cleaner, portable, understandable code. Everyone seems to agree that building UIs from components is the way to go these days which makes the need for container queries all the more obvious.

It’s wonderful that there are modern JavaScript ideas that help us do use them today — but that doesn’t mean the technology needs to stay there. It makes way more sense in CSS.

Here’s my late 2019 thought dump on the subject:

  • Philip Walton’s “Responsive Components: a Solution to the Container Queries Problem” is a great look at using JavaScript’s ResizeObserver as one way to solve the issue today. It performs great and works anywhere. The demo site is the best one out there because it highlights the need for responsive components (although there are other documented use cases as well). Philip even says a pure CSS solution would be more ideal.
  • CSS nesting got a little round of enthusiasm about a year ago. The conversation makes it seem like nesting is plausible. I’m in favor of this as a long-time fan of sensible Sass nesting. It makes me wonder if the syntax for container queries could leverage the same sort of thing. Maybe nested queries are scoped to the parent selector? Or you prefix the media statement with an ampersand as the current spec does with descendant selectors?
  • Other proposed syntaxes generally involve some use of the colon, like .container:media(max-width: 400px) { }. I like that, too. Single-colon selectors (pseduo selectors) are philosophically “select the element under these conditions” — like :hover, :nth-child, etc. — so a media scope makes sense.
  • I don’t think syntax is the biggest enemy of this feature; it’s the performance of how it is implemented. Last I understood, it’s not even performance as much as it mucks with the entire rendering flow of how browsers work. That seems like a massive hurdle. I still don’t wanna forget about it. There is lots of innovation happening on the web and, just because it’s not clear how to implement it today, that doesn’t mean someone won’t figure out a practical path forward tomorrow.

The post Let’s Not Forget About Container Queries appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]

Let’s Give Grunt Tasks the Marie Kondo Organization Treatment

We live in an era of webpack and npm scripts. Good or bad, they took the lead for bundling and task running, along with bits of Rollup, JSPM and Gulp. But let’s face it. Some of your older projects are still using good ol’ Grunt. While it no longer glimmers as brightly, it does the job well so there’s little reason to touch it.

Though, from time to time, you wonder if there’s a way to make those projects better, right? Then start from “Organizing Your Grunt Tasks” article and come back. I’ll wait. That’ll set the stage for this post and then we’ll take it further together to create a solid organization of Grunt tasks.

Automatic Speed Daemon task loading

It’s no fun writing loading declarations for each task, like this:

grunt.loadNpmTasks('grunt-contrib-clean') grunt.loadNpmTasks('grunt-contrib-watch') grunt.loadNpmTasks('grunt-csso') grunt.loadNpmTasks('grunt-postcss') grunt.loadNpmTasks('grunt-sass') grunt.loadNpmTasks('grunt-uncss')  grunt.initConfig({})

It’s common to use load-grunt-tasks to load all tasks automatically instead. But what if I tell you there is a faster way?

Try jit-grunt! Similar to load-grunt-tasks, but even faster than native grunt.loadNpmTasks.

The difference can be striking, especially in projects with large codebases.

Without jit-grunt

loading tasks     5.7s  ▇▇▇▇▇▇▇▇ 84% assemble:compile  1.1s  ▇▇ 16% Total 6.8s

With jit-grunt

loading tasks     111ms  ▇ 8% loading assemble  221ms  ▇▇ 16% assemble:compile   1.1s  ▇▇▇▇▇▇▇▇ 77% Total 1.4s

1.4 seconds doesn’t really make it a Speed Daemon… so I kinda lied. But still, it’s six times faster than the traditional way! If you’re curious how that’s possible, read about the original issue which led to the creation of jit-grunt.

How is jit-grunt used? First, install:

npm install jit-grunt --save

Then replace all tasks load statements with a single line:

module.exports = function (grunt) {   // Intead of this:   // grunt.loadNpmTasks('grunt-contrib-clean')   // grunt.loadNpmTasks('grunt-contrib-watch')   // grunt.loadNpmTasks('grunt-csso')   // grunt.loadNpmTasks('grunt-postcss')   // grunt.loadNpmTasks('grunt-sass')   // grunt.loadNpmTasks('grunt-uncss')    // Or instead of this, if you've used `load-grunt-tasks`   // require('load-grunt-tasks')(grunt, {   //   scope: ['devDependencies', 'dependencies']    // })    // Use this:   require('jit-grunt')(grunt)    grunt.initConfig({}) }

Done!

Better configs loading

In the last example, we told Grunt how to load tasks itself, but we didn’t quite finish the job. As “Organizing Your Grunt Tasks” suggests, one of the most useful things we’re trying to do here is split up a monolithic Gruntfile into smaller standalone files.

If you read the mentioned article, you’ll know it’s better to move all task configuration into external files. So, instead of a large single gruntfile.js file:

module.exports = function (grunt) {   require('jit-grunt')(grunt)    grunt.initConfig({     clean: {/* task configuration goes here */},     watch: {/* task configuration goes here */},     csso: {/* task configuration goes here */},     postcss: {/* task configuration goes here */},     sass: {/* task configuration goes here */},     uncss: {/* task configuration goes here */}   }) }

We want this:

tasks   ├─ postcss.js   ├─ concat.js   ├─ cssmin.js   ├─ jshint.js   ├─ jsvalidate.js   ├─ uglify.js   ├─ watch.js   └─ sass.js gruntfile.js

But that will force us to load each external configuration into gruntfile.js manually, and that takes time! We need a way to load our configuration files automatically.

We’ll use load-grunt-configs for that purpose. It takes a path, grabs all of the configuration files there and gives us a merged config object which we use for Grunt config initialization.

Here how it works:

module.exports = function (grunt) {   require('jit-grunt')(grunt)    const configs = require('load-grunt-configs')(grunt, {     config: { src: 'tasks/.js' }   })    grunt.initConfig(configs)   grunt.registerTask('default', ['cssmin']) }

Grunt can do the same thing natively! Take a look at grunt.task.loadTasks (or it’s alias grunt.loadTasks).

Use it like this:

module.exports = function (grunt) {   require('jit-grunt')(grunt)    grunt.initConfig({})    // Load all your external configs.   // It's important to use it _after_ Grunt config has been initialized,   // otherwise it will have nothing to work with.   grunt.loadTasks('tasks')    grunt.registerTask('default', ['cssmin']) }

Grunt will automatically load all js or coffee config files from the specified directory. Nice and clean! But, if you’ll try to use it, you’ll notice it does nothing. How is that? We still need to do one more thing.

Let’s look into our gruntfile.js code once again, this time without the comments:

module.exports = function (grunt) {   require('jit-grunt')(grunt)    grunt.initConfig({})    grunt.loadTasks('tasks')    grunt.registerTask('default', ['cssmin']) }

Notice that grunt.loadTasks loads files from tasks directory, but never assigns it to our actual Grunt config.

Compare it with a way load-grunt-configs works:

module.exports = function (grunt) {   require('jit-grunt')(grunt)    // 1. Load configs   const configs = require('load-grunt-configs')(grunt, {     config: { src: 'tasks/.js' }   })    // 2. Assign configs   grunt.initConfig(configs)    grunt.registerTask('default', ['cssmin']) }

We initialize our Grunt config before actually loadings tasks configuration. If you are getting a strong feeling that it will make us end up with empty Grunt config — you’re totally right. You see, on contrary to the load-grunt-configs, grunt.loadTasks just imports files into gruntfile.js. It does nothing more.

Woah! So, how do we make use of it? Let’s explore!

First, create a file inside directory tasks named test.js

module.exports = function () {   console.log("Hi! I'm an external task and I'm taking precious space in your console!") }

Let’s run Grunt now:

$   grunt

We’ll see printed to the console:

> Hi! I'm an external task and I'm taking precious space in your console!

So, upon importing grunt.loadTasks, every function is executed as it loads files. That’s nice, but what’s the use of it for us? We still can’t do a thing we actually want — to configure our tasks.

Hold my beer because there is a way to command Grunt from within external configuration files! Using grunt.loadTasks upon importing provides current Grunt instance as a function first argument and also binds it to this.

So, we can update our Gruntfile:

module.exports = function (grunt) {   require('jit-grunt')(grunt)    grunt.initConfig({     // Add some value to work with     testingValue: 123   })    grunt.loadTasks('tasks')    grunt.registerTask('default', ['cssmin']) }

…and change the external config file tasks/test.js:

// Add `grunt` as first function argument module.exports = function (grunt) {   // Now, use Grunt methods on `grunt` instance   grunt.log.error('I am a Grunt error!')    // Or use them on `this` which does the same   this.log.error('I am a Grunt error too, from the same instance, but from `this`!')    const config = grunt.config.get()    grunt.log.ok('And here goes current config:')   grunt.log.ok(config) }

Now, let’s run Grunt again:

$   grunt

And what we’ll get:

> I am Grunt error! > I am Grunt error too, from the same instance, but from `this`! > And here goes current config: > {     testingValue: 123   }

See how we accessed native Grunt methods from an external file and were even able to retrieve the current Grunt config? Are you thinking about that too? Yeah, the full power of Grunt is already there, right at our fingertips in each file!

If you are wondering why methods inside external files can affect our main Grunt instance, it is because of a referencing. grunt.loadTasks passing this and grunt to our current Grunt instance — not a copy of it. By invoking methods on that reference, we’re able to read and mutate our main Grunt configuration file.

Now, we need to actually configure something! One last thing…

This time, let’s make configuration loading work for real

Alright, we’ve come a long way. Our tasks are loaded automatically and faster. We learned how to load external configs with native Grunt method. But our task configs are still not quite there because they do not end up in Grunt config.

But we’re almost there! We learned that we can use any Grunt instance methods in imported files using grunt.loadTasks. They are available on grunt and this instances.

Among many other methods, there is a precious grunt.config method. It allows us to set a value in an existing Grunt config. The main one, which we initialized in our Gruntfile… remember that one?

What’s important is the way we can define tasks configurations. Exactly what we need!

// tasks/test.js  module.exports = function (grunt) {   grunt.config('csso', {     build: {       files: { 'style.css': 'styles.css' }     }   })    // same as   // this.config('csso', {   //   build: {   //     files: { 'style.css': 'styles.css' }   //   }   // }) }

Now let’s update Gruntfile to log the current config. We need to see what we did, after all:

module.exports = function (grunt) {   require('jit-grunt')(grunt)    grunt.initConfig({     testingValue: 123   })    grunt.loadTasks('tasks')    // Log our current config   console.log(grunt.config())    grunt.registerTask('default', ['cssmin']) }

Run Grunt:

$   grunt

…and here’s what we see:

> {     testingValue: 123,     csso: {       build: {         files: {           'style.css': 'styles.css'         }       }     }   }

grunt.config sets csso value when imported, so the CSSO task is now configured and ready to run when Grunt is invoked. Perfect.

Note that if you used load-grunt-configs previously, you had a code like that, where each file exports a configuration object:

// tasks/grunt-csso.js  module.exports = {   target: {     files: { 'style.css': 'styles.css' }   } }

That needs to be changed to a function, as described above:

// tasks/grunt-csso.js  module.exports = function (grunt) {   grunt.config('csso', {     build: {       files: { 'style.css': 'styles.css' }     }   }) }

OK, one more one more last thing… this time for real!

Taking external config files to the next level

We learned a lot. Load tasks, load external configuration files, define a configuration with Grunt methods… that’s fine, but where’s the profit?

Hold my beer again!

By this time, we’ve externalized all our task configuration files. So, the our project directory looks something like this:

tasks   ├─ grunt-browser-sync.js     ├─ grunt-cache-bust.js   ├─ grunt-contrib-clean.js    ├─ grunt-contrib-copy.js     ├─ grunt-contrib-htmlmin.js      ├─ grunt-contrib-uglify.js   ├─ grunt-contrib-watch.js    ├─ grunt-csso.js     ├─ grunt-nunjucks-2-html.js      ├─ grunt-postcss.js      ├─ grunt-processhtml.js   ├─ grunt-responsive-image.js     ├─ grunt-sass.js     ├─ grunt-shell.js    ├─ grunt-sitemap-xml.js      ├─ grunt-size-report.js      ├─ grunt-spritesmith-map.mustache    ├─ grunt-spritesmith.js      ├─ grunt-standard.js     ├─ grunt-stylelint.js    ├─ grunt-tinypng.js      ├─ grunt-uncss.js    └─ grunt-webfont.js gruntfile.js

That keeps Gruntfile relatively small and things seem to be well organized. But do you get a clear picture of the project just by glancing into this cold and lifeless list of tasks? What actually do they do? What’s the flow?

Can you tell that Sass files are going through grunt-sass, then grunt-postcss:autoprefixer, then grunt-uncss, and finally through grunt-csso? Is it obvious that the clean task is cleaning the CSS or that grunt-spritesmith is generating a Sass file which should be picked up too, as grunt-watch watches over changes?

Seems like things are all over the place. We may have gone too far with externalization!

So, finally… now what if tell you that there’s yet a better way would be group configs… based on features? Instead of a not-so-helpful list of tasks, we’ll get a sensible list of features. How about that?

tasks   ├─ data.js    ├─ fonts.js    ├─ icons.js    ├─ images.js    ├─ misc.js    ├─ scripts.js    ├─ sprites.js    ├─ styles.js    └─ templates.js gruntfile.js

That tells me a story! But how could we do that?

We already learned about grunt.config. And believe it or not, you can use it multiple times in a single external file to configure multiple tasks at once! Let’s see how it works:

// tasks/styles.js  module.exports = function (grunt) {   // Configuring Sass task   grunt.config('sass', {     build: {/* options */}   })      // Configuring PostCSS task   grunt.config('postcss', {     autoprefix: {/* options */}   }) }

One file, multiple configurations. Quite flexible! But there is an issue we missed.

How should we deal with tasks such as grunt-contrib-watch? Its configuration is a whole monolithic thing with definitions for each task that are unable to be split.

// tasks/grunt-contrib-watch.js  module.exports = function (grunt) {   grunt.config('watch', {     sprites: {/* options */},     styles: {/* options */},     templates: {/* options */}   }) }

We can’t simply use grunt.config to set watch configuration in each file, as it will override the same watch configuration in already imported files. And leaving it in a standalone file sounds like a bad option too — after all, we want to keep all related things close.

Fret not! grunt.config.merge to the rescue!

While grunt.config explicitly sets and overrides any existing values in Grunt config, grunt.config.merge recursively merges values with existing values in other Grunt config files giving us a single Grunt config. A simple, but effective way to keep related things together.

An example:

// tasks/styles.js  module.exports = function (grunt) {   grunt.config.merge({     watch: {       templates: {/* options */}     }   }) }
// tasks/templates.js  module.exports = function (grunt) {   grunt.config.merge({     watch: {       styles: {/* options */}     }   }) }

This will produce a single Grunt config:

{   watch: {     styles: {/* options */},     templates: {/* options */}   } }

Just what we needed! Let’s apply this to the real issue — our styles-related configuration files. Replace our three external task files:

tasks   ├─ grunt-sass.js   ├─ grunt-postcss.js      └─ grunt-contrib-watch.js

…with a single tasks/styles.js file that combines them all:

module.exports = function (grunt) {   grunt.config('sass', {     build: {       files: [         {           expand: true,           cwd: 'source/styles',           src: '{,**/}*.scss',           dest: 'build/assets/styles',           ext: '.compiled.css'         }       ]     }   })    grunt.config('postcss', {     autoprefix: {       files: [         {           expand: true,           cwd: 'build/assets/styles',           src: '{,**/}*.compiled.css',           dest: 'build/assets/styles',           ext: '.prefixed.css'         }       ]     }   })    // Note that we need to use `grunt.config.merge` here!   grunt.config.merge({     watch: {       styles: {         files: ['source/styles/{,**/}*.scss'],         tasks: ['sass', 'postcss:autoprefix']       }     }   }) }

Now it’s much easier to tell just by glancing into tasks/styles.js that styles have three related tasks. I’m sure you can imagine extending this concept to other grouped tasks, like all the things you might want to do with scripts, images, or anything else. That gives us a reasonable configuration organization. Finding things will be much easier, trust me.

And that’s it! The whole point of what we learned.

That’s a wrap

Grunt is no longer the new darling it once was when it first hit the scene. But to date, it is a straightforward and reliable tool that does its job well. With proper handling, it gives even fewer reasons to swap it for something newer.

Let’s recap what we can do to organize our tasks efficiently:

  1. Load tasks using jit-grunt instead of load-grunt-tasks. It’s same but insanely faster.
  2. Move specific task configurations out from Gruntfile and into external config files to keep things organized.
  3. Use native grunt.task.loadTasks to load external config files. It’s simple but powerful as it exposes all Grunt capabilities.
  4. Finally, think about a better way to organize your config files! Group them by feature or domain instead of the task itself. Use grunt.config.merge to split complex tasks like watch.

And, for sure, check Grunt documentation. It’s still worth a read after all these years.

If you’d like to see a real-world example, check out Kotsu, a Grunt-based starter kit and static website generator. You’ll find even more tricks in there.

Got better ideas about how to organize Grunt configs even better? Please share them in the comments!

The post Let’s Give Grunt Tasks the Marie Kondo Organization Treatment appeared first on CSS-Tricks.

CSS-Tricks

, , , , , , ,
[Top]

Hey, let’s create a functional calendar app with the JAMstack

Hey, let’s create a functional calendar app with the JAMstack

I’ve always wondered how dynamic scheduling worked so I decided to do extensive research, learn new things, and write about the technical part of the journey. It’s only fair to warn you: everything I cover here is three weeks of research condensed into a single article. Even though it’s beginner-friendly, it’s a healthy amount of reading. So, please, pull up a chair, sit down and let’s have an adventure.

My plan was to build something that looked like Google Calendar but only demonstrate three core features:

  1. List all existing events on a calendar
  2. Create new events
  3. Schedule and email notification based on the date chosen during creation. The schedule should run some code to email the user when the time is right.

Pretty, right? Make it to the end of the article, because this is what we’ll make.

A calendar month view with a pop-up form for creating a new event as an overlay.

The only knowledge I had about asking my code to run at a later or deferred time was CRON jobs. The easiest way to use a CRON job is to statically define a job in your code. This is ad hoc — statically means that I cannot simply schedule an event like Google Calendar and easily have it update my CRON code. If you are experienced with writing CRON triggers, you feel my pain. If you’re not, you are lucky you might never have to use CRON this way.

To elaborate more on my frustration, I needed to trigger a schedule based on a payload of HTTP requests. The dates and information about this schedule would be passed in through the HTTP request. This means there’s no way to know things like the scheduled date beforehand.

We (my colleagues and I) figured out a way to make this work and — with the help of Sarah Drasner’s article on Durable Functions — I understood what I needed learn (and unlearn for that matter). You will learn about everything I worked in this article, from event creation to email scheduling to calendar listings. Here is a video of the app in action:

You might notice the subtle delay. This has nothing to do with the execution timing of the schedule or running the code. I am testing with a free SendGrid account which I suspect have some form of latency. You can confirm this by testing the serverless function responsible without sending emails. You would notice that the code runs at exactly the scheduled time.

Tools and architecture

Here are the three fundamental units of this project:

  1. React Frontend: Calendar UI, including the UI to create, update or delete events.
  2. 8Base GraphQL: A back-end database layer for the app. This is where we will store, read and update our date from. The fun part is you won’t write any code for this back end.
  3. Durable Functions: Durable functions are kind of Serverless Functions that have the power of remembering their state from previous executions. This is what replaces CRON jobs and solves the ad hoc problem we described earlier.

See the Pen
durable-func1
by Chris Nwamba (@codebeast)
on CodePen.

The rest of this post will have three major sections based on the three units we saw above. We will take them one after the other, build them out, test them, and even deploy the work. Before we get on with that, let’s setup using a starter project I made to get us started.

Project Repo

Getting Started

You can set up this project in different ways — either as a full-stack project with the three units in one project or as a standalone project with each unit living in it’s own root. Well, I went with the first because it’s more concise, easier to teach, and manageable since it’s one project.

The app will be a create-react-app project and I made a starter for us to lower the barrier to set up. It comes with supplementary code and logic that we don’t need to explain since they are out of the scope of the article. The following are set up for us:

  1. Calendar component
  2. Modal and popover components for presenting event forms
  3. Event form component
  4. Some GraphQL logic to query and mutate data
  5. A Durable Serverless Function scaffold where we will write the schedulers

Tip: Each existing file that we care about has a comment block at the top of the document. The comment block tells you what is currently happening in the code file and a to-do section that describes what we are required to do next.

Start by cloning the starter form Github:

git clone -b starter --single-branch https://github.com/christiannwamba/calendar-app.git

Install the npm dependencies described in the root package.json file as well as the serverless package.json:

npm install

Orchestrated Durable Functions for scheduling

There are two words we need to get out of the way first before we can understand what this term is — orchestration and durable.

Orchestration was originally used to describe an assembly of well-coordinated events, actions, etc. It is heavily borrowed in computing to describe a smooth coordination of computer systems. The key word is coordinate. We need to put two or more units of a system together in a coordinated way.

Durable is used to describe anything that has the outstanding feature of lasting longer.

Put system coordination and long lasting together, and you get Durable Functions. This is the most powerful feature if Azure’s Serverless Function. Durable Functions based in what we now know have these two features:

  1. They can be used to assemble the execution of two or more functions and coordinate them so race conditions do not occur (orchestration).
  2. Durable Functions remember things. This is what makes it so powerful. It breaks the number one rule of HTTP: stateless. Durable functions keep their state intact no matter how long they have to wait. Create a schedule for 1,000,000 years into the future and a durable function will execute after one million years while remembering the parameters that were passed to it on the day of trigger. That means Durable Functions are stateful.

These durability features unlock a new realm of opportunities for serverless functions and that is why we are exploring one of those features today. I highly recommend Sarah’s article one more time for a visualized version of some of the possible use cases of Durable Functions.

I also made a visual representation of the behavior of the Durable Functions we will be writing today. Take this as an animated architectural diagram:

Shows the touch-points of a serverless system.

A data mutation from an external system (8Base) triggers the orchestration by calling the HTTP Trigger. The trigger then calls the orchestration function which schedules an event. When the time for execution is due, the orchestration function is called again but this time skips the orchestration and calls the activity function. The activity function is the action performer. This is the actual thing that happens e.g. “send email notification”.

Create orchestrated Durable Functions

Let me walk you through creating functions using VS Code. You need two things:

  1. An Azure account
  2. VS Code

Once you have both setup, you need to tie them together. You can do this using a VS Code extension and a Node CLI tool. Start with installing the CLItool:

 npm install -g azure-functions-core-tools  # OR  brew tap azure/functions brew install azure-functions-core-tools 

Next, install the Azure Function extension to have VS Code tied to Functions on Azure. You can read more about setting up Azure Functions from my previous article.


Now that you have all the setup done, let’s get into creating these functions. The functions we will be creating will map to the following folders.

Folder Function
schedule Durable HTTP Trigger
scheduleOrchestrator Durable Orchestration
sendEmail Durable Activity

Start with the trigger.

  1. Click on the Azure extension icon and follow the image below to create the schedule function

    Shows the interface steps going from Browse to JavaScript to Durable Functions HTTP start to naming the function schedule.
  2. Since this is the first function, we chose the folder icon to create a function project. The icon after that creates a single function (not a project).
  3. Click Browse and create a serverless folder inside the project. Select the new serverless folder.
  4. Select JavaScript as the language. If TypeScript (or any other language) is your jam, please feel free.
  5. Select Durable Functions HTTP starter. This is the trigger.
  6. Name the first function as schedule

Next, create the orchestrator. Instead of creating a function project, create a function instead.

  1. Click on the function icon:

  2. Select Durable Functions orchestrator.
  3. Give it a name, scheduleOrchestrator and hit Enter.
  4. You will be asked to select a storage account. Orchestrator uses storage to preserve the state of a function-in-process.
  5. Select a subscription in your Azure account. In my case, I chose the free trial subscription.
  6. Follow the few remaining steps to create a storage account.

Finally, repeat the previous step to create an Activity. This time, the following should be different:

  • Select Durable Functions activity.
  • Name it sendEmail.
  • No storage account will be needed.

Scheduling with a durable HTTP trigger

The code in serverless/schedule/index.js does not need to be touched. This is what it looks like originally when the function is scaffolded using VS Code or the CLI tool.

const df = require("durable-functions"); module.exports = async function (context, req) {   const client = df.getClient(context);   const instanceId = await client.startNew(req.params.functionName, undefined, req.body);   context.log(`Started orchestration with ID = '$ {instanceId}'.`);   return client.createCheckStatusResponse(context.bindingData.req, instanceId); };

What is happening here?

  1. We’re creating a durable function on the client side that is based on the context of the request.
  2. We’re calling the orchestrator using the client’s startNew() function. The orchestrator function name is passed as the first argument to startNew() via the params object. A req.body is also passed to startNew() as third argument which is forwarded to the orchestrator.
  3. Finally, we return a set of data that can be used to check the status of the orchestrator function, or even cancel the process before it’s complete.

The URL to call the above function would look like this:

http://localhost:7071/api/orchestrators/{functionName}

Where functionName is the name passed to startNew. In our case, it should be:

//localhost:7071/api/orchestrators/scheduleOrchestrator

It’s also good to know that you can change how this URL looks.

Orchestrating with a Durable Orchestrator

The HTTP trigger startNew call calls a function based on the name we pass to it. That name corresponds to the name of the function and folder that holds the orchestration logic. The serverless/scheduleOrchestrator/index.js file exports a Durable Function. Replace the content with the following:

const df = require("durable-functions"); module.exports = df.orchestrator(function* (context) {   const input = context.df.getInput()   // TODO -- 1      // TODO -- 2 });

The orchestrator function retrieves the request body from the HTTP trigger using context.df.getInput().

Replace TODO -- 1 with the following line of code which might happen to be the most significant thing in this entire demo:

yield context.df.createTimer(new Date(input.startAt))

What this line does use the Durable Function to create a timer based on the date passed in from the request body via the HTTP trigger.

When this function executes and gets here, it will trigger the timer and bail temporarily. When the schedule is due, it will come back, skip this line and call the following line which you should use in place of TODO -- 2.

return yield context.df.callActivity('sendEmail', input);

The function would call the activity function to send an email. We are also passing a payload as the second argument.

This is what the completed function would look like:

const df = require("durable-functions");  module.exports = df.orchestrator(function* (context) {   const input = context.df.getInput()        yield context.df.createTimer(new Date(input.startAt))        return yield context.df.callActivity('sendEmail', input); });

Sending email with a durable activity

When a schedule is due, the orchestrator comes back to call the activity. The activity file lives in serverless/sendEmail/index.js. Replace what’s in there with the following:

const sgMail = require('@sendgrid/mail'); sgMail.setApiKey(process.env['SENDGRID_API_KEY']);  module.exports = async function(context) {   // TODO -- 1   const msg = {}   // TODO -- 2   return msg; };

It currently imports SendGrid’s mailer and sets the API key. You can get an API Key by following these instructions.

I am setting the key in an environmental variable to keep my credentials safe. You can safely store yours the same way by creating a SENDGRID_API_KEY key in serverless/local.settings.json with your SendGrid key as the value:

{   "IsEncrypted": false,   "Values": {     "AzureWebJobsStorage": "<<AzureWebJobsStorage>",     "FUNCTIONS_WORKER_RUNTIME": "node",     "SENDGRID_API_KEY": "<<SENDGRID_API_KEY>"   } }

Replace TODO -- 1 with the following line:

const { email, title, startAt, description } = context.bindings.payload;

This pulls out the event information from the input from the orchestrator function. The input is attached to context.bindings. payload can be anything you name it so go to serverless/sendEmail/function.json and change the name value to payload:

{   "bindings": [     {       "name": "payload",       "type": "activityTrigger",       "direction": "in"     }   ] }

Next, update TODO -- 2 with the following block to send an email:

const msg = {   to: email,   from: { email: 'chris@codebeast.dev', name: 'Codebeast Calendar' },   subject: `Event: $ Hey, let’s create a functional calendar app with the JAMstack`,   html: `<h4>$ Hey, let’s create a functional calendar app with the JAMstack @ $ {startAt}</h4> <p>$ {description}</p>` }; sgMail.send(msg);  return msg;

Here is the complete version:

const sgMail = require('@sendgrid/mail'); sgMail.setApiKey(process.env['SENDGRID_API_KEY']);  module.exports = async function(context) {   const { email, title, startAt, description } = context.bindings.payload;   const msg = {     to: email,     from: { email: 'chris@codebeast.dev', name: 'Codebeast Calendar' },     subject: `Event: $ Hey, let’s create a functional calendar app with the JAMstack`,     html: `<h4>$ Hey, let’s create a functional calendar app with the JAMstack @ $ {startAt}</h4> <p>$ {description}</p>`   };   sgMail.send(msg);    return msg; };

Deploying functions to Azure

Deploying functions to Azure is easy. It’s merely a click away from the VS Code editor. Click on the circled icon to deploy and get a deploy URL:

Still with me this far in? You’re making great progress! It’s totally OK to take a break here, nap, stretch or get some rest. I definitely did while writing this post.

Data and GraphQL layer with 8Base

My easiest description and understanding of 8Base is “Firebase for GraphQL.” 8Base is a database layer for any kind of app you can think of and the most interesting aspect of it is that it’s based on GraphQL.

The best way to describe where 8Base fits in your stack is to paint a picture of a scenario.

Imagine you are a freelance developer with a small-to-medium scale contract to build an e-commerce store for a client. Your core skills are on the web so you are not very comfortable on the back end. though you can write a bit of Node.

Unfortunately, e-commerce requires managing inventories, order management, managing purchases, managing authentication and identity, etc. “Manage” at a fundamental level just means data CRUD and data access.

Instead of the redundant and boring process of creating, reading, updating, deleting, and managing access for entities in our backend code, what if we could describe these business requirements in a UI? What if we can create tables that allow us to configure CRUD operations, auth and access? What if we had such help and only focus on building frontend code and writing queries? Everything we just described is tackled by 8Base

Here is an architecture of a back-end-less app that relies on 8Base as it’s data layer:

Create an 8Base table for events storage and retrieval

The first thing we need to do before creating a table is to create an account. Once you have an account, create a workspace that holds all the tables and logic for a given project.

Next, create a table, name the table Events and fill out the table fields.

We need to configure access levels. Right now, there’s nothing to hide from each user, so we can just turn on all access to the Events table we created:

Setting up Auth is super simple with 8base because it integrates with Auth0. If you have entities that need to be protected or want to extend our example to use auth, please go wild.

Finally, grab your endpoint URL for use in the React app:

Testing GraphQL Queries and mutation in the playground

Just to be sure that we are ready to take the URL to the wild and start building the client, let’s first test the API with a GraphQL playground and see if the setup is fine. Click on the explorer.

Paste the following query in the editor.

query {   eventsList {     count     items {       id       title       startAt       endAt       description       allDay       email     }   } }

I created some test data through the 8base UI and I get the result back when I run they query:

You can explore the entire database using the schema document on the right end of the explore page.

Calendar and event form interface

The third (and last) unit of our project is the React App which builds the user interfaces. There are four major components making up the UI and they include:

  1. Calendar: A calendar UI that lists all the existing events
  2. Event Modal: A React modal that renders EventForm component to create a component
  3. Event Popover: Popover UI to read a single event, update event using EventForm or delete event
  4. Event Form: HTML form for creating new event

Before we dive right into the calendar component, we need to setup React Apollo client. The React Apollo provider empowers you with tools to query a GraphQL data source using React patterns. The original provider allows you to use higher order components or render props to query and mutate data. We will be using a wrapper to the original provider that allows you query and mutate using React Hooks.

In src/index.js, import the React Apollo Hooks and the 8base client in TODO -- 1:

import { ApolloProvider } from 'react-apollo-hooks'; import { EightBaseApolloClient } from '@8base/apollo-client';

At TODO -- 2, configure the client with the endpoint URL we got in the 8base setup stage:

const URI = 'https://api.8base.com/cjvuk51i0000701s0hvvcbnxg';  const apolloClient = new EightBaseApolloClient({   uri: URI,   withAuth: false });

Use this client to wrap the entire App tree with the provider on TODO -- 3:

ReactDOM.render(   <ApolloProvider client={apolloClient}>     <App />   </ApolloProvider>,   document.getElementById('root') );

Showing events on the calendar

The Calendar component is rendered inside the App component and the imports BigCalendar component from npm. Then :

  1. We render Calendar with a list of events.
  2. We give Calendar a custom popover (EventPopover) component that will be used to edit events.
  3. We render a modal (EventModal) that will be used to create new events.

The only thing we need to update is the list of events. Instead of using the static array of events, we want to query 8base for all store events.

Replace TODO -- 1 with the following line:

const { data, error, loading } = useQuery(EVENTS_QUERY);

Import the useQuery library from npm and the EVENTS_QUERY at the beginning of the file:

import { useQuery } from 'react-apollo-hooks'; import { EVENTS_QUERY } from '../../queries';

EVENTS_QUERY is exactly the same query we tested in 8base explorer. It lives in src/queries and looks like this:

export const EVENTS_QUERY = gql`   query {     eventsList {       count       items {         id         ...       }     }   } `;

Let’s add a simple error and loading handler on TODO -- 2:

if (error) return console.log(error);   if (loading)     return (       <div className="calendar">         <p>Loading...</p>       </div>     );

Notice that the Calendar component uses the EventPopover component to render a custom event. You can also observe that the Calendar component file renders EventModal as well. Both components have been setup for you, and their only responsibility is to render EventForm.

Create, update and delete events with the event form component

The component in src/components/Event/EventForm.js renders a form. The form is used to create, edit or delete an event. At TODO -- 1, import useCreateUpdateMutation and useDeleteMutation:

import {useCreateUpdateMutation, useDeleteMutation} from './eventMutationHooks'
  • useCreateUpdateMutation: This mutation either creates or updates an event depending on whether the event already existed.
  • useDeleteMutation: This mutation deletes an existing event.

A call to any of these functions returns another function. The function returned can then serve as an even handler.

Now, go ahead and replace TODO -- 2 with a call to both functions:

const createUpdateEvent = useCreateUpdateMutation(   payload,   event,   eventExists,   () => closeModal() ); const deleteEvent = useDeleteMutation(event, () => closeModal());

These are custom hooks I wrote to wrap the useMutation exposed by React Apollo Hooks. Each hook creates a mutation and passes mutation variable to the useMutation query. The blocks that look like the following in src/components/Event/eventMutationHooks.js are the most important parts:

useMutation(mutationType, {   variables: {     data   },   update: (cache, { data }) => {     const { eventsList } = cache.readQuery({       query: EVENTS_QUERY     });     cache.writeQuery({       query: EVENTS_QUERY,       data: {         eventsList: transformCacheUpdateData(eventsList, data)       }     });     //..   } });

Call the Durable Function HTTP trigger from 8Base

We have spent quite some time in building the serverless structure, data storage and UI layers of our calendar app. To recap, the UI sends data to 8base for storage, 8base saves data and triggers the Durable Function HTTP trigger, the HTTP trigger kicks in orchestration and the rest is history. Currently, we are saving data with mutation but we are not calling the serverless function anywhere in 8base.

8base allows you to write custom logic which is what makes it very powerful and extensible. Custom logic are simple functions that are called based on actions performed on the 8base database. For example, we can set up a logic to be called every time a mutation occurs on a table. Let’s create one that is called when an event is created.

Start by installing the 8base CLI:

npm install -g 8base

On the calendar app project run the following command to create a starter logic:

8base init 8base

8base init command creates a new 8base logic project. You can pass it a directory name which in this case we are naming the 8base logic folder, 8base — don’t get it twisted.

Trigger scheduling logic

Delete everything in 8base/src and create a triggerSchedule.js file in the src folder. Once you have done that, drop in the following into the file:

const fetch = require('node-fetch');  module.exports = async event => {   const res = await fetch('<HTTP Trigger URL>', {     method: 'POST',     body: JSON.stringify(event.data),     headers: { 'Content-Type': 'application/json' }   })   const json = await res.json();   console.log(event, json)   return json; };

The information about the GraphQL mutation is available on the event object as data.

Replace <HTTP Trigger URL> with the URL you got after deploying your function. You can get the URL by going to the function in your Azure URL and click “Copy URL.”

You also need to install the node-fetch module, which will grab the data from the API:

npm install --save node-fetch

8base logic configuration

The next thing to do is tell 8base what exact mutation or query that needs to trigger this logic. In our case, a create mutation on the Events table. You can describe this information in the 8base.yml file:

functions:   triggerSchedule:     handler:       code: src/triggerSchedule.js     type: trigger.after     operation: Events.create

In a sense, this is saying, when a create mutation happens on the Events table, please call src/triggerSchedule.js after the mutation has occurred.

We want to deploy all the things

Before anything can be deployed, we need to login into the 8Base account, which we can do via command line:

8base login

Then, let’s run the deploy command to send and set up the app logic in your workspace instance.

8base deploy

Testing the entire flow

To see the app in all its glory, click on one of the days of the calendar. You should get the event modal containing the form. Fill that out and put a future start date so we trigger a notification. Try a date more than 2-5 mins from the current time because I haven’t been able to trigger a notification any faster than that.

Yay, go check your email! The email should have arrived thanks to SendGrid. Now we have an app that allows us to create events and get notified with the details of the event submission.

The post Hey, let’s create a functional calendar app with the JAMstack appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]