Günther Debrauwer - March 21st, 2021

Displaying dates in browser's timezone in Livewire

At work, we have developed a couple of admin panels in Livewire. While developing in Livewire, I always wondered how I could show a date based on the user's browser timezone, without having to save it somewhere on the user's profile. I did some research and found a couple of possibilities to show dates in the browser's timezone.

IP-address to timezone in Laravel

The first thing you might try to do is determine the timezone in Laravel itself. If you google this, you will find some packages that allow you to determine timezone based on the IP-address that you have access to in every Laravel request. The only issue with this approach is that it requires an external service. There are apparently quite a few services, but most of them are paid and only have a very limited free service. That's not ideal.

An Alpine.js component

Maybe it's a better idea to perform all the logic in Javascript. As Livewire and Alpine.js work together like a charm, let's try to create an Alpine.js component that can accept a date and display it correctly.

First, in our x-data object of our Alpine.js component, we will need a couple of variables. The datetime will contain the date that will be provided by Livewire. We will also need the page language. This can be determined by checking the lang attribute on the html tag.

Now the only thing that's left to do is set the inner text of the HTML tag. This can be achieved using the x-text attribute. In x-text, we will create a Javascript Date object and format the date using the toLocaleString function. In this example, I'm using a fixed date format. In a real application, you might want to expand this so it can handle multiple date formats.

<span
    x-data="{
        datetime: '',
        language: document.querySelector('html').getAttribute('lang'),
    }"
    x-text="(new Date(datetime)).toLocaleString(language, { weekday:'long', year:'numeric', month:'long', day:'numeric', hour:'numeric', minute:'numeric', second:'numeric' })"
></span>

That's it: we already have a working Alpine.js component to display a date in the browser's timezone. As we are probably using this in multiple blade files, it's a good idea to hide all this logic in a Blade component.

<!-- in resources/views/components/local-time.blade.php -->
@props(['datetime'])
<span
    x-data="{
        datetime: '{{ $datetime->format('c') }}',
        language: document.querySelector('html').getAttribute('lang'),
    }"
    x-text="(new Date(datetime)).toLocaleString(language, { weekday:'long', year:'numeric', month:'long', day:'numeric', hour:'numeric', minute:'numeric', second:'numeric' })"
></span>


<!-- In your Livewire blade files -->
<x-local-time :datetime="now()->format('c')"/>

The result is pretty nice. There's just one small issue with it. Alpine.js is a very lightweight Javascript framework, but it's still a relatively large javascript file that has to be downloaded. If you have a slower internet connection, you will notice that you won't see anything while Alpine.js is being loaded. So I started thinking: can this not achieve using vanilla Javascript? Well, it is possible, using web components.

A local-time web component

While researching this topic I came across web components. GitHub has a package called @github/time-elements, which provides web components to display dates correctly based on the browser's timezone. GitHub uses these web components everywhere in its UI.

But what are web components exactly? Web components allow you to create custom html tags with functionality and logic you write yourself. Creating a web component is pretty easy, as it's supported in all major browsers and does not require any build step. It's just vanilla Javascript code.

As an example, we will create our own local-time html element. It will accept a datetime attribute and it will display the provided datetime value correctly in a fixed date format.

To create a web component, you must create a class that extends the HTMLElement class. Within that class, you can do a lot. In this example, I will focus on the basics we need to create our own local-time html element.

class LocalTimeElement extends HTMLElement
{
    
}

First, we will define a static getter called observedAttributes in our class. This function should return an array containing the attributes of our html element. That way, we can be informed if the values of these attributes change.

class LocalTimeElement extends HTMLElement
{
    static get observedAttributes()
    {
        return ['datetime'];
    }
}

Secondly, let's create a connectedCallback() function in our class. This function will be executed when our html element is added to the DOM. Here we can access the attributes of our element while we also have access to the rest of the DOM. This means we can read the value of our datetime attribute, determine the language of our page via the lang attribute on the html tag, and set the inner text of our html tag using the toLocaleString function on a javascript Date object.

class LocalTimeElement extends HTMLElement
{
    static get observedAttributes()
    {
        return ['datetime'];
    }
  
    connectedCallback()
    {
        var date = new Date(this.attributes.getNamedItem('datetime').value);
        var language = document.querySelector('html').getAttribute('lang');

        var format = { weekday:'long', year:'numeric', month:'long', day:'numeric', hour:'numeric', minute:'numeric', second:'numeric' };

        this.innerText = date.toLocaleString(language, format);
    }
}

Finally we should also create a attributeChangedCallback(name, oldValue, newValue) function. As the name implies, it will be executed if an attribute's value changes. In our example, if an attribute's value changes, we just want to update the displayed text by executing the code within the connectedCallback() function again.

class LocalTimeElement extends HTMLElement
{
    static get observedAttributes()
    {
        return ['datetime'];
    }
  
    connectedCallback()
    {
        var date = new Date(this.attributes.getNamedItem('datetime').value);
        var language = document.querySelector('html').getAttribute('lang');

        var format = { weekday:'long', year:'numeric', month:'long', day:'numeric', hour:'numeric', minute:'numeric', second:'numeric' };

        this.innerText = date.toLocaleString(language, format);
    }
  
    attributeChangedCallback()
    {
        this.connectedCallback();
    }
}

The code of our custom web component is now ready. Before we can use it, we will have to register it as a custom element. This is done using the define method on the global object customElements. The first argument must be the html tag we want to use, while the second argument is the class that should be linked to that html tag. You must ensure that your html tag contains a -, as this is required by the HTML Standard.

customElements.define('local-time', LocalTimeElement);

Now we can start using our custom html tag in our Livewire blade files.

<local-time datetime="{{ $datetime->format('c') }}"></local-time>

There's just one small caveat to using this in Livewire: it will behave correctly when it's added to the DOM by Livewire, but when the Livewire component is updated you will see nothing anymore. To fix this, we must create a plugin for Livewire, similar to the Vue plugin that Livewire provides. The plugin we have to create is not very complicated. First, we have to add a hook for the element.initialized Livewire event. Here we will tell Livewire to ignore all the web components (if a html tag contains a -, then it's a web component, according to the HTML Standard). Secondly, we must add a hook for the element.updating event. When this event occurs on a web component, then we want to explicitly set the new values of each attribute. This will ensure that the attributeChangedCallback function is triggered in the class of our web component.

window.livewire.hook('element.initialized', el => {
   if (!el.tagName.includes("-")) {
       return;
   }
  
   el.__livewire_ignore = true;
})

window.livewire.hook('element.updating', (fromEl, toEl, component) => {
    if (!fromEl.tagName.includes("-")) {
        return;
    }

    for (var i = 0, atts = toEl.attributes, n = atts.length, arr = []; i < n; i++){
        fromEl.setAttribute(atts[i].nodeName, atts[i].nodeValue);
    }
})

That's it: we now can use our local-time html tag just as a regular html tag in our Livewire blade files. If you will compare this to the Alpine.js component you will see that the web component renders quite a bit faster, especially if you have a slower internet connection and Alpine.js has to be downloaded by the browser.

An Alpine.js component or a local-time web component, you could use either of these in your Livewire components to display a date in the browser's timezone. Maybe you know an even better way to achieve this. If that's the case, let me know on Twitter.