Payment of ₹ requested by HONEY VIG. Click the link to pay securely via Razorpay: Make 5000 payment Payment of ₹ requested by HONEY VIG. Click the link to pay securely via Razorpay: Make 5000 payment ⭐ If you would like to buy me a coffee, well thank you very much that is mega kind! : https://www.buymeacoffee.com/honeyvig
Hire a web Developer and Designer to upgrade and boost your online presence with cutting edge Technologies
Showing posts with label Framework. Show all posts
Showing posts with label Framework. Show all posts

Friday, September 17, 2021

ISR vs DPR: Big Words, Quick Explanation

 There are two strategies for incrementally building websites that are growing in popularity: Incremental Static Regeneration and Distributed Persistent Rendering. What’s the difference? Let’s figure it out.

If you’ve been dabbling in the Jamstack/page rendering/Next.js world, chances are you’ve heard of the terms “Incremental Static Regeneration” (ISR) and “distributed persistent rendering” (DPR) floating around. And if you haven’t, you might be like, “Wow, these are long words that I’ll never understand.” That’s where you’re wrong! You’re about to understand them now.

What Are These Things? #

These terms are strategies for incrementally building websites. Normally, when you deploy a website that isn’t server-side or client-side rendered, it has to be compiled and built for the browser to natively load it (so, for example, your JSX is transpiled to vanilla JavaScript, your SCSS compiled to vanilla CSS, your templates into HTML).

As your websites get large, you might start to run into having to have fairly long build times, because that’s a lot to compile. Generally, your websites might have pages in two categories:

  • Type A
    “Critical” pages (home page, about us, contact us)
  • Type B
    “Deferred” pages that might not be hit as often as the Type A pages (product catalog, certain documentation pages)

If you were to incrementally build this website, you could theoretically break up your build where your Type A pages are built upfront, and then Type B pages are built later!

Both ISR and DPR follow this approach but do it in slightly different ways.

What’s The Difference Between ISR And DPR? #

For both approaches, Type A pages are built upfront right when you deploy. With Type B pages, they vary a little more. The key thing that is different between these two approaches is immutability. When I say immutability, I mean that once a page is added to a build, it doesn’t change, and every user hitting a URL during that deployment will always see the exact same data.

With ISR, your Type B pages are built at runtime when a user goes to the page. Each of these pages has an “expiration time” (or “revalidation time”) where it will re-build based on new content that comes in at that time (fetched in the background). It is based on a caching strategy called “stale while revalidate”, meaning a page can be “stale” with old information until it’s re-generated and the cache is updated. This approach does not guarantee immutability across builds. When I say that, I mean that you can’t necessarily go back to a previous deployment with full confidence that all the content is going to be as it was at that time you originally ran that page.

There are pros and cons to this; the major pro meaning that you can have your data that populates the page regularly update without rebuilding the site, and the major con being that debugging will be very difficult without that immutable piece (because some users might see a stale page and others might see the correctly updated one).

With DPR, your Type B pages are also built at runtime when a user goes to the page. When the non-built pages are requested for the first time, they are built and cached at the edge so they don’t need to be built again. Once it’s a part of the build, it will not change until a redeploy. There are also pros and cons to this approach. The major pro is that this guarantees immutability. When you go to a URL, every user will always see the same exact information. The con is that if you want to have one of those pages updated, you’ll need to trigger a rebuild of the site.

How Can I Use These Approaches And Compare For Myself? #

In an ideal world, these approaches are built into the frameworks you use and you won’t have to do much to use them. Right now though, you can test it out with Next.js! ISR is built-in by default to Next.js when you deploy it to a Node.js-driven platform like Vercel, and DPR is built in when you deploy to Netlify.

You can use DPR in other frameworks as well (Zach Leatherman has some demos of it with Eleventy where he defers building hundreds of pages), and the Next.js team said that ISR will be coming to other frameworks in the future (like Nuxt and SvelteKit). You can also leave a comment on how DPR is implemented (for any Jamstack platform!) in this RFC.

 

 

Thursday, September 16, 2021

Three Insights Gained While Researching Vue.js Accessibility

 Web app accessibility appears difficult because it seems that there is little information on the subject available online. But while researching for my eBook, I found that the situation is better than it seems. What follows are three insights about the accessible use of framework features, concrete Vue.js traits you can use for the inclusive cause, as well as community initiatives and vetted patterns (and where to find them).

JavaScript frameworks like React, Angular and Vue have a very bad reputation when it comes to web accessibility. But is this due to inherent technical limitations or insurmountable problems of those tools? I think not. During the research phase  I gained three insights regarding web app accessibility in general and the framework in particular. Considering these, perhaps it’s worth taking another perspective around accessible Vue apps.

Insight 1: JavaScript Framework Features For Accessibility Are Underused #

Component-based design, enabled and enforced by modern JavaScript frameworks, does not only provide great developer experiences and project ergonomics when used in a smart way, but it can also offer advantages for accessibility. The first is the factor of reusability, i.e. when your component gets used in several places within your app (perhaps in different forms or shapes) and it only has to be made accessible only once. In this case, an increased developer experience actually helps the user and “baking accessibility into components” (as Hidde de Vries puts it) creates a win-win scenario for everyone.

The second aspect that comes with component based-designs are props — namely in the form that one component can inherit or get context from its parent environment. This forwarding of “environment data” can serve accessibility as well.

Take headlines, for example. A solid and comprehensible headline structure is not only good for SEO but especially for people using screen readers. When they encounter a sound document outline, constructed with headlines that structure a web page or app, screen reader users gain a quick overview of the web page they are on. Just like visually-abled users don’t read every word on a page but scan for interesting things, blind screen reader users don’t make their software read each and every word. Instead, they are checking a document for content and functionality they are interested in. Headlines, for that matter, are keeping pieces of content together and are at the same time providing a structural frame of a document (think timber frame houses).

What makes headlines providing a structure is not only their mere existence. It is also their nesting that creates an image inside a user’s mind. For that, a web developer’s headline toolbox contains six levels (<h1> to <h6>). By applying these levels, both editors and developers can create an outline of content and a reliable functionality that users can expect in the document.

For example, let’s take the (abridged) headline tree from the GOV.UK website:

1 — Welcome to GOV.UK
  2 — Popular on GOV.UK
  2 — Services and information
    3 — Benefits
    3 — Births, deaths, marriages and care
    3 — Business and self-employment
    // …etc
  2 — Departments and policy
    3 — Coronavirus (COVID 19)
    3 — Travel abroad: step by step
…etc

Even without visiting the actual page and without actually perceiving it visually, this headline tree created a table of contents helping you understand what sections can be expected on the front page. The creators used headline elements to herald data following it and didn’t skip headline levels.

So far, so familiar (at least in correlation with search engines, I guess). However, because a component can be used in different places of your app, hardwired headline levels inside them can sometimes create a suboptimal headline tree overall. Relations between headlines possibly aren’t conveyed as clear as in the example above (“Business and self-employment” does not stand on its own but is related to “Services and information”).

For example, imagine a listing of a shop’s newest products that can be placed both in the main content and a sidebar — it’s quite possible that both sections live in different contexts. A headline such as <h1>Our latest arrivals</h1> would make sense above the product list in the main content — given it is the central content of the whole document or view.

The same component sporting the same <h1> but placed in a sidebar of another document, however, would suggest the most important content lives in the sidebar and competes with the <h1> in the main content. While what I described above is a peculiarity of component-based design in general this gives us a perfect opportunity to put both aspects together — the need for a sound headline tree and our knowledge about props:

Context Via props #

Let’s progress from theoretical considerations into hands-on code. In the following code block, you see a component listing the newest problems in an online shop. It is extremely simplyified but the emphasis is on line 3, the hardcoded <h1>:

<template>
    <div>
        <h1>Our latest arrivals</h1>
        <ol>
            <li>Product A</li>
            <li>Product B</li>
            <!-- etc -->
        </ol>
  </div>
</template>

To use this component in different places of the app without compromising the document’s headline tree, we want to make the headline level dynamic. To achieve this, we replace the <h1> with Vue’s dynamic component name helper called, well, component:

<component :is="headlineElement">Our latest arrivals</component>

In the script part of our component, we now have to add two things:

  • A component prop that receives the exact headline level as a string, headlineLevel;
  • A computed property (headlineElement from the code example above) that builds a proper HTML element out of the string h and the value of headlineLevel.

So our simplified script block looks like this:

<script>
export default {
    props: {
      headlineLevel: {
        type: String
    },
    computed: {
        headlineElement() {
          return "h" + this.headlineLevel;
        }
    }
}
</script>

And that’s all!

Of course, adding checks and sensible defaults on the prop level is necessary — for example, we have to make sure that headlineLevel can only be a number between 1 and 6. Both Vue’s native Prop Validation, as well as TypeScript, are tools at your disposal to do just that, but I wanted to keep it out of this example.

If you happen to be interested in learning how to accomplish the exact same concept using React, friend of the show magazine Heydon Pickering wrote about the topic back in 2018 and supplied React/JSX sample code. Tenon UI’s Heading Components, also written for React, take this concept even further and aim to automate headline level creation by using so-called “LevelBoundaries” and a generic <Heading> element. Check it out!

Insight 2: There Are Established Strategies To Tackle Web App Accessibility Problems

While web app accessibility may look daunting the first time you encounter the topic, there’s no need to despair: vested accessibility patterns to tackle typical web app characteristics do exist. In the following Insight, I will introduce you to strategies for supplying accessible notifications, including an easy implementation in Vue.js, then point you towards recommended patterns and their Vue counterparts . Lastly, I recommend taking a look at both Vue’s emerging and React’s established accessibility community .

 

Strategy 1: Announcing Dynamic Updates With Live Regions

While accessibility is more than making things screen reader compatible, improving the screen reader experience plays a big part of web app accessibility. This is rooted in the general working principle of this form of assistive technology: screen reader software transforms content on the screen into either audio or braille output, thus enabling blind people to interact with the web and technology in general.

Like keyboard focus, a screen reader’s output point, the so-called virtual cursor, can only be at one place at once. At the same time, one core aspect of web apps is a dynamic change in parts of the document without page reload. But what happens, for example, when the update in the DOM is actually above the virtual cursor’s position in the document? Users likely would not notice the change because do not tend to traverse the document in reverse — unless they are somehow informed of the dynamic update.

In the following short video, I demonstrate what happens (or rather, what not happens) if an interaction causes a dynamic DOM change nowhere near the virtual cursor — the screen reader just stays silent

 

But by using ARIA Live Regions, web developers can trigger accessible announcements, namely screen reader output independent of the virtual cursor’s position. The way live regions work is that a screen reader is instructed to observe certain HTML elements’ textContent. When it changes due to scripting, the screen reader picks up the update, and the new text will be read out.

As an example, imagine a list of products in an online shop. Products are listed in a table and users can add every product to their shopping cart without a page reload by the click of a button. The expected asynchronous update of the DOM, while perceivable for visual users, is a perfect job for live regions.

Let’s write a piece of simplified dream code for this situation. Here’s the HTML:

<button id="addToCartOne">Add to cart</button>

<div id="info" aria-live="polite">
<!-- I’m the live region. For the sake of this example, I'll start empty. 
     But screen readers detect any text changes within me! -->
</div>

Now, both DOM updates and live regions announcements are only possible with JavaScript. So let’s look at the equally simplified script part of our “Add to Cart” button click handler:

<script>
const buttonAddProductOneToCart = document.getElementById('addToCartOne');
const liveRegion = document.getElementById('info');

buttonAddProductOneToCart.addEventListener('click', () => {
        // The actual adding logic magic 🪄

        // Triggering the live region:
        liveRegion.textContent = "Product One has been added to your cart";
});
</script>

You can see in the code above that when the actual adding happens (the actual implementation depends on your data source and tech stack, of course), an accessible announcement gets triggered. The once empty <div> with the ID of info changes its text content to “Product One has been added to your cart”. Because screen readers observe the region for changes like this, a screen reader output regardless of the virtual cursor position is prompted. And because the live region is set to polite, the announcement waits in case there is a current output.

If you really want to convey an important announcement that doesn’t respect the current screen reader message but interrupts it, the aria-live attribute can also be set to assertive. Live regions in themselves are powerful tools that should be used with caution, which is even more valid for this more “aggressive” kind. So please limit their use to urgent error messages the user must know about, for example, “Autosave failed, please save manually”.

Let’s visit our example from above again, this time with implemented live regions: Screen reader users are now informed that their button interaction has worked and that the particular item has been added to (or removed from) their shopping cart.

If you want to use live regions in Vue.js applications, you can, of course, recreate the code examples above. However, an easier way would be to use the library vue-announcer. After installing it with npm install -S @vue-a11y/announcer (or npm install -S @vue-a11y/announcer@next for the Vue 3 version) and registering it a Vue plugin, there are only two steps necessary:

  1. The placement of <VueAnnouncer /> in your App.vue’s template. This renders an empty live region (like the one from above that had the ID of info).
    Note: It is recommended only to use one live region instance, and it makes sense to place it in a central place, so that many components can refer to it.
<template>
  <div>
    <VueAnnouncer />
    <!-- ... -->
  </div>
</template>
  1. The triggering of the live region, for example from within a method or lifecycle hook. The easiest way to accomplish this is using the .set method or this.$announcer. The method’s first parameter is the text the live regions is updated with (equivalent to the text that the screen reader will output). As a second parameter, you can explicitly supply polite and assertive as a setting). But, as you’ll notice it is optional — in case the parameter gets omitted the announcement will be a polite one:
methods: {
  addProductToCart(product) {
    // Actual adding logic here

    this.$announcer.set(`${product.title} has been added to your cart.`);
  }
}

This was just a small peek into the amazing world of ARIA live regions. As a matter of fact, more options than polite and assertive are available (such as log, timer and even marquee) but with varying support in screen readers.

If you want to dive deeper into the topic, here are three recommended resources:

  • “ARIA Live Regions,” MDN Web Docs
  • “The Many Lives Of A Notification,” Sarah Higley (video)
  • NerdeRegion, a Chrome extension that lets you roughly emulate live region output in your dev tools without the need to fire up a screen reader. However, this should not replace conscientious testing in real screen readers!

Strategy 2: Using Undisputed WAI-ARIA Authoring Practices

The moment you encounter WAI-ARIA’s authoring practices, you’ll likely feel a great relief. It seems that the web’s standard body, the W3 Consortium, offers some kind of pattern library that you just have to use (or convert to your framework of choice), and boom, all your web app accessibility challenges are solved.

The reality, however, is not so simple. While it is true that the W3C offers a plethora of typical web app patterns like combo boxes, sliders, menus and treeviews, not all authoring practices are in a state that they are recommended for production. The actual idea behind authoring practices was to demonstrate the “pure” use of ARIA states, roles and widget patterns.

But in order to be a truly vetted pattern, its authors have to make sure that every practice has solid support among assistive technologies and also works seamlessly for touch devices. Alas, that’s the place where some patterns listed in the Authoring Practices fall short. Web development is in a state of constant flux, and likely web app evolution even more so. A good place to stay updated with the state of single authoring practices is W3C’s authoring-practices repo on GitHub. In the Issues section, web accessibility experts exchange their current ideas, experiences, and testing research for every pattern.

All that being said does not mean that the practices have no value at all for your web app accessibility ventures, though. While there are widgets that are mere proofs of concept there are solid patterns. In the following, I want to highlight three undisputed Authoring Practices and their counterparts in built-in Vue.js:

  • Disclosure Widgets are simple and straightforward concepts that can be used in a variety of ways: as a basis for your accessible accordion, as part of a robust dropdown navigation menu, or to show and hide additional information, like extended image descriptions.

    The great thing about the pattern is that it only consists of two elements: A trigger (1) that toggles the visibility of a container (2). In HTML terms, trigger and container must directly follow each other in the DOM. To learn more about the concept and the implementation in Vue, read my blog article about Disclosure Widgets in Vue, or check out the corresponding demo on CodeSandBox.

  • Modal dialogs are also considered an established pattern. What makes a dialog “modal” is its property to render the parts of the interface that are not the modal’s content inactive when it’s open.

    Furthermore, developers must ensure that the keyboard focus is sent into the modal upon activation, can’t leave an open modal and is being sent back to the triggering control after deactivation. Kitty Giraudel’s A11y Dialog component takes care of all the things I just described. For developers using Vue.js, there is a plugin called vue-a11y-dialog available.

  • Tab Components are a common dynamic pattern that works with the metaphor of physical folder tabs and thus, helps authors to pack larger amounts of content into “tab panels”. The authoring practice comes in two variants related to panel activation (automatic or manual).

    What is even more important, tab components enjoy good support in assistive technology and can be therefore considered a recommended pattern (as long as you test which activation mode works best for your users). Architecturally speaking, there are multiple ways to build the tab component with the help of Vue.js: In this CodeSandBox, I decided to go for a slot-based approach and automatic activation.

Strategy 3: View And Help Vue’s Accessibility Initiatives Grow

While there is still a way to go, it is true to state that the topic of accessibility in Vue.js is finally on the rise. A milestone for the topic was the addition of an “Accessibility” section in Vue’s official docs, which happened related to the release of Vue 3.

But even apart from official resources, the following people from the Vue community are worth following because they provide either education material, accessible components, or both:

  • Maria Lombardo has the status of “Vue community partner”, penned the accessibility documentation linked above, is giving a11y-related workshops at Vue conferences, and has a (paid) Web Accessibility Fundamentals course at vueschool.io.
  • An article about accessibility in Vue.js would not be complete without a mention of Alan Ktquez, project lead of vue-a11y.com. He and his community initiative create and maintain plugins like the aforementioned vue-announcer, vue-skipto for creating skip links, vue-axe as a Vue wrapper around Deque’s axe-core testing engine, and especially awesome-vue-a11y, an ever-growing link list of accessibility resources in the Vueniverse.
  • Fellow Berliner Oscar Braunert has a special focus on inclusive inputs and shows how to implement them in Vue.js, for example in the form of talks and articles. With the tournant UI library, Oscar and I are aiming to supply accessible components both based on (undisputed) WAI Authoring Practices (see Strategy 2) and Heydon Pickering’s Inclusive Components.
  • Moritz Kröger built a Vue wrapper for Kitty Giraudel’s a11y-dialog dubbed vue-a11y-dialog, which provides everything a developer needs in term of semantics and focus management (see above).

Strategy 4: Learn From React Accessibility Leads

If you compare it with the top dog React.js, Vue.js is not a niche product, but you must admit that it has (not yet?) reached its popularity. However, this does not have to be a disadvantage when it comes to accessibility. React — and Angular before it — are in a sense pioneering in accessibility by their proliferation alone.

The more popular frameworks become, the higher the likelihood of good work in terms of inclusivity. Be it due to community initiatives on the subject or simply government authorities with web accessibility obligations doing a “buy-in”. If they also share their findings and accessible code via open-source, it’s a win-win-win situation. Not only for the framework itself and its community but also for “competitors”.

This actually has happened in the case of React (and the government project that I talked about so abstractly is the Australian Government Design System). Teams and developers caring for accessibility and working with React can check out projects like these and use the provided components and best practices.

Teams and developers caring for accessibility but using Vue.js, Angular, Svelte etc. can look into the React code and learn from it. Although there may be natural differences in the syntax of each framework, they have many basic concepts in common. Other React libraries that are considered accessible and are available as a basis for learning:

For improving Vue.js accessibility, it’s also worth following accessibility people from the React world:

  • Marcy Sutton is a freelance web accessibility expert who worked in the past for Deque and improved accessibility and related documentation at Gatsby.js, which is a static site generator based on React. She is very hands-on, conducts research and conveys important topics regarding web app accessibility in great presentations, blog posts, and workshops. You can find Marcy Sutton on Twitter at @marcysutton, web app-related courses on Egghead.io and TestingAccessibility.com or an overview of all of her projects by visiting her website.
  • Lindsey Kopacz is a web developer specializing in inaccessibility. She cares for inclusive experiences on the web, about overcoming ableism and educating her fellow web developers about the importance of accessibility. Apart from writing on her blog a11ywithlindsey.com, she also has courses on Egghead.io and lately published her ebook “The Bootcampers Guide to Web Accessibility”. On Twitter, she is @littlekope.
  • Ryan Florence and Michael Jackson created Reach UI, a collection of components and tools that aims “to become the accessible foundation of your React-based design system.” Besides the fact that they have created some accessible standard components, it is especially noteworthy that their “Reach UI Router” (along with its accessibility features) will merge with the “official” React Router in the future.

    Although React does not do “first-class plugins” like Vue.js, this is excellent news because they created their router with inbuilt focus management. A feature and accessibility enhancement that will soon benefit everyone using React Router and their users.

Insight 3: Vue’s $refs Are Great For Focus Management

Focus Management?

You encountered a way of sending accessible announcements by the use of ARIA Live Regions in the last Insight. Another way to deal with the problems a highly dynamic document presents to screen-reader and keyboard users is to manage focus programmatically. Before I start explaining focus management any further, please be aware: Usually, it is bad to change focus via scripting, and you should refrain from doing so.

Users don’t want their focus to be messed with. If a focus change does happen and is entirely unexpected, such an intervention is either a nuisance or even becomes a real barrier. On the other hand, changing focus programmatically is sometimes the only sensible option in JavaScript-based widgets or apps to help users who rely on keyboard usage. However, in this case, the focus change has to be predictable. A good way of ensuring this predictability is to ensure that a focus change only happens after an interaction, such as a button or link click.

What are circumstances in which focus management is can improve an app’s accessibility?

  1. Focus management is needed when the content that is being affected by an interaction (e.g. <div>) does not follow the trigger (e.g. <button>) directly in the document. For example, the disclosure widget concept assumes the container the button toggles is directly below the button in the DOM tree.

    This document structure, this proximity of trigger and reacting container, cannot be ensured in every widget, so the focus has to be managed actively. When a modal dialog opens after a button activation, it can’t be ensured that its HTML nodes directly follow the triggering button in the DOM. Thus, the focus has actively sent into the modal, ensuring that keyboard-only and screen reader users can use the particular widget.
  2. When parts of the document have changed without a page reload or parts of the DOM have been updated (again, after an interaction such as a button click), it is appropriate to send focus to the added or changed content.

    An example of this is the navigation between routes (“pages”) in Single Page Apps: Since they don’t reload the HTML document (like static websites do), a keyboard-only or screen reader user is not sent to the top of the “new page”. Because what is happening is not a “proper” page load — but a modification of a certain part of the same page.

You can see examples of bad and good focus management in the following demos, provided by Manuel Matuzović. Although the underlying framework (React) and the underlying UI pattern (modal dialog) differs, the problem remains the same:

That leaves responsible developers with the task of sending the keyboard focus to particular elements for your template. Luckily, JavaScript frameworks have the concept of DOM node references, or “refs”. In Vue.js, it is sufficient to add the ref attribute in an HTML node. Subsequently, a reference to this node is available in the $this.refs object. Finally, focusing an element programmatically is as easy as calling JavaScript’s native .focus() method on it.

For the next example, let’s assume we have a button somewhere in our component and apply a ref named triggerButton to it. We want to set focus to it once the user hits the ESC key. Our code for this would look as follows:

<template>
    <div @keydown.esc="focusTriggerBtn">
        <button ref="triggerButton">Trigger</button>
    </div>
</template>
<script>
export default {
//...
methods: {
    focusTriggerBtn() {
        this.$refs.triggerButton.focus();
    }
}
//...
}
</script>

Another use of both refs and focus management would be the accessible implementation of an off-canvas navigation.

 

In this case, you need to establish at least two refs: One for the trigger button that opens the navigation (let’s call it navTrigger), and one for the element that gains focus as soon as the navigation is visible (navContainer in this example, an element which needs tabindex="-1" to be programmatically focusable). So that, when the trigger button is clicked, the focus will be sent into the navigation itself. And vice versa: As soon as the navigation closes, the focus must return to the trigger.

After having read the paragraphs above, I hope one thing becomes clear for you, dear reader: Once you understand the importance of focus management, you realize that all the necessary tools are at your fingertips — namely Vue’s this.$refs and JavaScript’s native .focus()

Conclusion

By highlighting some of my core findings regarding web app accessibility, I hope that I have been able to help reduce any diffuse fear of this topic that may have existed, and you now feel more confident to build accessible apps with the help of Vue.js (if you want to dive deeper into the topic, check out if my little ebook “Accessible Vue” can help you along the journey).

More and more websites are becoming more and more app-like, and it would be sad if these amazing digital products were to remain so barrier-laden only because web developers don’t know exactly where to start with the topic. It’s a genuinely enabling moment once you realize that a vast majority of web app accessibility is actually “good old” web accessibility, and for the rest of it, cowpaths are already paved.

 

Monday, August 23, 2021

Creating An Interactive Gantt Chart Component With Vanilla JavaScript

  With a Gantt chart, you can visualize schedules and assign tasks. In this article, we will code a Gantt chart as a reusable Web component. We will focus on the architecture of the component, rendering the calendar with CSS Grid and managing the state of the draggable tasks with JavaScript Proxy Objects.

If you work with time data in your app, a graphical visualization as a calendar or Gantt chart is often very useful. At first glance, developing your own chart component seems quite complicated. Therefore, in this article, I will develop the foundation for a Gantt chart component whose appearance and functionality you can customize for any use case.

These are the basic features of the Gantt chart that I would like to implement:

  • The user can choose between two views: year/month or month/day.
  • The user can define the planning horizon by selecting a start date and an end date.
  • The chart renders a given list of jobs that can be moved by drag and drop. The changes are reflected in the state of the objects.
  • Below you can see the resulting Gantt chart in both views. In the monthly version, 

Below you can see the resulting Gantt chart in both views. In the monthly version, I have included three jobs as an example.

Sample Files And Instructions For Running The Code

You can find the full code snippets of this article in the following files:

Since the code contains JavaScript modules, you can only run the example from an HTTP server and not from the local file system. For testing on your local PC, I’d recommend the module live-server, which you can install via npm.

Basic Structure Of The Web Component

I decided to implement the Gantt chart as a web component. This allows us to create a custom HTML element, in my case <gantt-chart></gantt-chart>, which we can easily reuse anywhere on any HTML page.

You can find some basic information about developing web components in the MDN Web Docs. The following listing shows the structure of the component. It is inspired by the “counter” example from Alligator.io.

The component defines a template containing the HTML code needed to display the Gantt chart. For the complete CSS specifications, please refer to the sample files. The specific selection fields for year, month or date cannot be defined here yet, as they depend on the selected level of the view.

The selection elements are projected in by one of the two renderer classes instead. The same applies to the rendering of the actual Gantt chart into the element with the ID gantt-container, which is also handled by the responsible renderer class.

The class VanillaGanttChart now describes the behavior of our new HTML element. In the constructor, we first define our rough template as the shadow DOM of the element.

The component must be initialized with two arrays, jobs, and resources. The jobs array contains the tasks that are displayed in the chart as movable green bars. The resources array defines the individual rows in the chart where tasks can be assigned. In the screenshots above, for example, we have 4 resources labeled Task 1 to Task 4. The resources can therefore represent the individual tasks, but also people, vehicles, and other physical resources, allowing for a variety of use cases.

Currently, the YearMonthRenderer is used as the default renderer. As soon as the user selects a different level, the renderer is changed in the changeLevel method: First, the renderer-specific DOM elements and listeners are deleted from the Shadow DOM using the clear method of the old renderer. Then the new renderer is initialized with the existing jobs and resources and the rendering is started.

import {YearMonthRenderer} from './YearMonthRenderer.js';
import {DateTimeRenderer} from './DateTimeRenderer.js';

const template = document.createElement('template');

template.innerHTML = 
 `<style> … </style>

  <div id="gantt-settings">

    <select name="select-level" id="select-level">
      <option value="year-month">Month / Day</option>
      <option value="day">Day / Time</option>
    </select>

    <fieldset id="select-from">
      <legend>From</legend>
    </fieldset>

    <fieldset id="select-to">
      <legend>To</legend>
    </fieldset>
  </div>

  <div id="gantt-container">
  </div>`;

export default class VanillaGanttChart extends HTMLElement {

    constructor() {
      super();
      this.attachShadow({ mode: 'open' });
      this.shadowRoot.appendChild(template.content.cloneNode(true));
      this.levelSelect = this.shadowRoot.querySelector('#select-level');
    }
 
    _resources = [];
    _jobs = [];
    _renderer;

    set resources(list){…}
    get resources(){…}
    set jobs(list){…}
    get jobs(){…}
    get level() {…}
    set level(newValue) {…} 
    get renderer(){…}
    set renderer(r){…}

    connectedCallback() {
      this.changeLevel = this.changeLevel.bind(this);

      this.levelSelect.addEventListener('change', this.changeLevel);
      this.level = "year-month";   

      this.renderer = new YearMonthRenderer(this.shadowRoot);
      this.renderer.dateFrom = new Date(2021,5,1);
      this.renderer.dateTo = new Date(2021,5,24);
      this.renderer.render();
    }

    disconnectedCallback() {  
      if(this.levelSelect)
        this.levelSelect.removeEventListener('change', this.changeLevel);
      if(this.renderer)
        this.renderer.clear();
    }

    changeLevel(){
      if(this.renderer)
        this.renderer.clear();

      var r;   

      if(this.level == "year-month"){
        r = new YearMonthRenderer(this.shadowRoot);    
      }else{
        r = new DateTimeRenderer(this.shadowRoot);
      }

      r.dateFrom = new Date(2021,5,1);
      r.dateTo = new Date(2021,5,24);
      r.resources = this.resources;
      r.jobs = this.jobs;
      r.render();
      this.renderer = r;
    }
  }
 
  window.customElements.define('gantt-chart', VanillaGanttChart);

Before we get deeper into the rendering process, I would like to give you an overview of the connections between the different scripts:

  • index.html is your web page where you can use the tag <gantt-chart></gantt-chart>
  • index.js is a script in which you initialize the instance of the web component that is associated with the Gantt chart used in index.html with the appropriate jobs and resources (of course you can also use multiple Gantt charts and thus multiple instances of the web component)
  • The component VanillaGanttChart delegates rendering to the two renderer classes YearMonthRenderer and DateTimeRenderer.

 

 

Rendering Of The Gantt chart With JavaScript And CSS Grid

In the following, we discuss the rendering process using the YearMonthRenderer as an example. Please note that I have used a so-called constructor function instead of the class keyword to define the class. This allows me to distinguish between public properties (this.render and this.clear) and private variables (defined with var).

The rendering of the chart is broken down into several sub-steps:

  1. initSettings
    Rendering of the controls which are used to define the planning horizon.
  2. initGantt
    Rendering of the Gantt chart, basically in four steps:
    • initFirstRow (draws 1 row with month names)
    • initSecondRow (draws 1 row with days of the month)
    • initGanttRows (draws 1 row for each resource with grid cells for each day of the month)
    • initJobs (positions the draggable jobs in the chart)
export function YearMonthRenderer(root){

    var shadowRoot = root;
    var names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];    
 
    this.resources=[];
    this.jobs = [];
 
    this.dateFrom = new Date();
    this.dateTo = new Date();

    //select elements
    var monthSelectFrom;
    var yearSelectFrom;
    var monthSelectTo;
    var yearSelectTo;

    var getYearFrom = function() {…}
    var setYearFrom = function(newValue) {…}

    var getYearTo = function() {…}
    var setYearTo = function(newValue) {…}

    var getMonthFrom = function() {…}
    var setMonthFrom = function(newValue) {…}

    var getMonthTo = function() {…}
    var setMonthTo = function(newValue) {…}  

    this.render = function(){
      this.clear();
      initSettings();
      initGantt();
    }

    //remove select elements and listeners, clear gantt-container 
    this.clear = function(){…}

    //add HTML code for the settings area (select elements) to the shadow root, initialize associated DOM elements and assign them to the properties monthSelectFrom, monthSelectTo etc., initialize listeners for the select elements
    var initSettings = function(){…}

    //add HTML code for the gantt chart area to the shadow root, position draggable jobs in the chart
    var initGantt = function(){…}

    //used by initGantt: draw time axis of the chart, month names
    var initFirstRow = function(){…}

    //used by initGantt: draw time axis of the chart, days of month
    var initSecondRow = function(){…}

    //used by initGantt: draw the remaining grid of the chart
    var initGanttRows = function(){…}.bind(this);

    //used by initGantt: position draggable jobs in the chart cells
    var initJobs = function(){…}.bind(this);    

   //drop event listener for jobs
   var onJobDrop = function(ev){…}.bind(this);

   //helper functions, see example files
   ...
}

Rendering The Grid

I recommend CSS Grid for drawing the diagram area because it makes it very easy to create multi-column layouts that adapt dynamically to the screen size.

In the first step, we have to determine the number of columns of the grid. In doing so, we refer to the first row of the chart which (in the case of the YearMonthRenderer) represents the individual months.

Consequently, we need:

  • one column for the names of the resources, e.g. with a fixed width of 100px.
  • one column for each month, of the same size and using the full space available.

This can be achieved with the setting 100px repeat(${n_months}, 1fr) for the property gridTemplateColumns of the chart container.

This is the initial part of the initGantt method:

var container = shadowRoot.querySelector("#gantt-container");
container.innerHTML = "";

var first_month = new Date(getYearFrom(), getMonthFrom(), 1);
var last_month = new Date(getYearTo(), getMonthTo(), 1);
 
//monthDiff is defined as a helper function at the end of the file
var n_months =  monthDiff(first_month, last_month)+1;
 
container.style.gridTemplateColumns = `100px repeat(${n_months},1fr)`;
 

After we have defined the outer columns, we can start filling the grid. Let’s stay with the example from the picture above. In the first row, I insert 3 divs with the classes gantt-row-resource and gantt-row-period. You can find them in the following snippet from the DOM inspector.

In the second row, I use the same three divs to keep the vertical alignment. However, the month divs get child elements for the individual days of the month.

<div id="gantt-container"
  style="grid-template-columns: 100px repeat(2, 1fr);">
  <div class="gantt-row-resource"></div>
  <div class="gantt-row-period">Jun 2021</div>
  <div class="gantt-row-period">Jul 2021</div>
  <div class="gantt-row-resource"></div>
  <div class="gantt-row-period">
    <div class="gantt-row-period">1</div>
    <div class="gantt-row-period">2</div>
    <div class="gantt-row-period">3</div>
    <div class="gantt-row-period">4</div>
    <div class="gantt-row-period">5</div>
    <div class="gantt-row-period">6</div>
    <div class="gantt-row-period">7</div>
    <div class="gantt-row-period">8</div>
    <div class="gantt-row-period">9</div>
    <div class="gantt-row-period">10</div>
  ...
  </div>
  ...
</div>

For the child elements to be arranged horizontally as well, we need the setting display: grid for the class gantt-row-period. In addition, we do not know exactly how many columns are required for the individual months (28, 30, or 31). Therefore, I use the setting grid-auto-columns. With the value minmax(20px, 1fr); I can ensure that a minimum width of 20px is maintained and that otherwise the available space is fully utilized:

#gantt-container {
  display: grid;
}

.gantt-row-resource {
  background-color: whitesmoke;
  color: rgba(0, 0, 0, 0.726);
  border: 1px solid rgb(133, 129, 129);
  text-align: center;
}

.gantt-row-period {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(20px, 1fr);
  background-color: whitesmoke;
  color: rgba(0, 0, 0, 0.726);
  border: 1px solid rgb(133, 129, 129);
  text-align: center;
}

The remaining rows are generated according to the second row, however as empty cells.

Here is the JavaScript code for generating the individual grid cells of the first row. The methods initSecondRow and initGanttRows have a similar structure.

var initFirstRow = function(){

  if(checkElements()){
        var container = shadowRoot.querySelector("#gantt-container");

        var first_month = new Date(getYearFrom(), getMonthFrom(), 1);
        var last_month = new Date(getYearTo(), getMonthTo(), 1);
 
        var resource = document.createElement("div");
        resource.className = "gantt-row-resource";
        container.appendChild(resource);   
 
        var month = new Date(first_month);

        for(month; month <= last_month; month.setMonth(month.getMonth()+1)){    
          var period = document.createElement("div");
          period.className = "gantt-row-period";
          period.innerHTML = names[month.getMonth()] + " " + month.getFullYear();
          container.appendChild(period);
        }
  }
}

Rendering The Jobs

Now each job has to be drawn into the diagram at the correct position. For this I make use of the HTML data attributes: every grid cell in the main chart area is associated with the two attributes data-resource and data-date indicating the position on the horizontal and vertical axis of the chart (see function initGanttRows in the files YearMonthRenderer.js and DateTimeRenderer.js).

 

Let’s now see what this means for the function initJobs. With the help of the function querySelector, it is now quite easy to find the grid cell into which a job should be placed.

The next challenge is to determine the correct width for a job element. Depending on the selected view, each grid cell represents a unit of one day (level month/day) or one hour (level day/time). Since each job is the child element of a cell, the job duration of 1 unit (day or hour) corresponds to a width of 1*100%, the duration of 2 units corresponds to a width of 2*100%, and so on. This makes it possible to use the CSS calc function to dynamically set the width of a job element, as shown in the following listing.

var initJobs = function(){

    this.jobs.forEach(job => {

        var date_string = formatDate(job.start);

        var ganttElement = shadowRoot.querySelector(`div[data-resource="${job.resource}"][data-date="${date_string}"]`);

        if(ganttElement){

          var jobElement = document.createElement("div");
          jobElement.className="job";
          jobElement.id = job.id;

          //helper function dayDiff - get difference between start and end in days
          var d = dayDiff(job.start, job.end);           
          
          //d --> number of grid cells covered by job + sum of borderWidths
          jobElement.style.width = "calc("+(d*100)+"% + "+ d+"px)";
          jobElement.draggable = "true";

          jobElement.ondragstart = function(ev){
              //the id is used to identify the job when it is dropped
              ev.dataTransfer.setData("job", ev.target.id); 
          };

          ganttElement.appendChild(jobElement);
        }
    });
  }.bind(this);

In order to make a job draggable, there are three steps required:

  • Set the property draggable of the job element to true (see listing above).
  • Define an event handler for the event ondragstart of the job element (see listing above).
  • Define an event handler for the event ondrop for the grid cells of the Gantt chart, which are the possible drop targets of the job element (see function initGanttRows in the file YearMonthRenderer.js).

The event handler for the event ondrop is defined as follows:

var onJobDrop = function(ev){
 
      // basic null checks
      if (checkElements()) {
 
        ev.preventDefault(); 
 
        // drop target = grid cell, where the job is about to be dropped
        var gantt_item = ev.target;
        
        // prevent that a job is appended to another job and not to a grid cell
        if (ev.target.classList.contains("job")) {
          gantt_item = ev.target.parentNode;
        }
        
        // identify the dragged job
        var data = ev.dataTransfer.getData("job");               
        var jobElement = shadowRoot.getElementById(data);  
        
        // drop the job
        gantt_item.appendChild(jobElement);
 
        // update the properties of the job object
        var job = this.jobs.find(j => j.id == data );
 
        var start = new Date(gantt_item.getAttribute("data-date"));
        var end = new Date(start);
        end.setDate(start.getDate()+dayDiff(job.start, job.end));
 
        job.start = start;
        job.end = end;
        job.resource = gantt_item.getAttribute("data-resource");
      }
    }.bind(this);

All changes to the job data made by drag and drop are thus reflected in the list jobs of the Gantt chart component.

Integrating The Gantt Chart Component In Your Application

You can use the tag <gantt-chart></gantt-chart> anywhere in the HTML files of your application (in my case in the file index.html) under the following conditions:

  • The script VanillaGanttChart.js must be integrated as a module so that the tag is interpreted correctly.
  • You need a separate script in which the Gantt chart is initialized with jobs and resources (in my case the file index.js).
<!DOCTYPE html>
<html>
 <head>
   <meta charset="UTF-8"/>
   <title>Gantt chart - Vanilla JS</title>
   <script type="module" src="VanillaGanttChart.js"></script>   
 </head>
    
 <body>
 
  <gantt-chart id="g1"></gantt-chart> 
 
  <script type="module" src="index.js"></script>
 </body> 
</html>

For example, in my case the file index.js looks as follows:

import VanillaGanttChart from "./VanillaGanttChart.js";
 
var chart = document.querySelector("#g1");
 
chart.jobs = [
    {id: "j1", start: new Date("2021/6/1"), end: new Date("2021/6/4"), resource: 1},
    {id: "j2", start: new Date("2021/6/4"), end: new Date("2021/6/13"), resource: 2},
    {id: "j3", start: new Date("2021/6/13"), end: new Date("2021/6/21"), resource: 3},
];
 
chart.resources = [{id:1, name: "Task 1"}, {id:2, name: "Task 2"}, {id:3, name: "Task 3"}, {id:4, name: "Task 4"}];

However, there is still one requirement open: when the user makes changes by dragging jobs in the Gantt chart, the respective changes in the property values of the jobs should be reflected in the list outside the component.

We can achieve this with the use of JavaScript Proxy Objects: Each job is nested in a proxy object, which we provide with a so-called validator. It becomes active as soon as a property of the object is changed (function set of the validator) or retrieved (function get of the validator). In the set function of the validator, we can store code that is executed whenever the start time or the resource of a task is changed.

The following listing shows a different version of the file index.js. Now a list of proxy objects is assigned to the Gantt chart component instead of the original jobs. In the validator set I use a simple console output to show that I have been notified of a property change.

import VanillaGanttChart from "./VanillaGanttChart.js";
 
var chart = document.querySelector("#g1");
 
var jobs = [
    {id: "j1", start: new Date("2021/6/1"), end: new Date("2021/6/4"), resource: 1},
    {id: "j2", start: new Date("2021/6/4"), end: new Date("2021/6/13"), resource: 2},
    {id: "j3", start: new Date("2021/6/13"), end: new Date("2021/6/21"), resource: 3},
];
var p_jobs = [];
 
chart.resources = [{id:1, name: "Task 1"}, {id:2, name: "Task 2"}, {id:3, name: "Task 3"}, {id:4, name: "Task 4"}];
 
jobs.forEach(job => {
 
    var validator = {
        set: function(obj, prop, value) {
 
          console.log("Job " + obj.id + ": " + prop + " was changed to " + value);
          console.log();
 
          obj[prop] = value;
          return true;
        },
 
        get: function(obj, prop){
 
            return obj[prop];
        }
    };
 
    var p_job = new Proxy(job, validator);
    p_jobs.push(p_job);
});
 
chart.jobs = p_jobs;

Outlook

The Gantt chart is an example that shows how you can use the technologies of Web Components, CSS Grid, and JavaScript Proxy to develop a custom HTML element with a somewhat more complex graphical interface. You are welcome to develop the project further and/or use it in your own projects together with other JavaScript frameworks.