Alarm clock using Alpinejs

Email

Alpine.js

In this article, We will build an alarm clock app using Alpinejs. It will be a simple web app to create and manage alarms using Alpinejs. We will not be working with any server API's it’s purely a frontend app using plain JavaScript and HTML.

Technology Description

Alpinejs is a microframework to say precisely. It does provide the functionality of React/Vue in a very simple way. All you need to just import the js file and start working on it.

I have 4 yrs of working experience in Reactjs and I clearly see the need for this library for smaller web-pages or server-side interactive landing pages which are driven from webhooks or external redirects.

We can use Alpinejs, in these cases of smaller needs without thinking of going a heavy way of using any template engine (Jinja/JSP/EJS/PUG).

Note - Here is link of live Webapp for this article and link to github repo

Guideline to learn via this article

  1. Understand through code is a very optimal way of figuring out the new stack. You come to know why and how of the new stack via code and use the relatively in your own projects to get better at it.

  2. The best way is to start writing instead of cloning the project.

Project Structure

As you can see we have created a folder with the project name alarm_clock_alpinejs.

Aplinejs Project Structure

We have created index.html and placed alarm_sound.mp3 in root project and an alarm.jsin the js directory.

Implementation

We will start the implementation with our index.html and then code section by section.

<html>
   <head>
      <title>Alarm clock generation</title>
      <script nomodule src="https://unpkg.com/browser-es-module-loader/dist/babel-browser-build.js"></script>
      <script nomodule src="https://unpkg.com/browser-es-module-loader"></script>
      <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous" />
      <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
      <script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.min.js" defer></script>
      <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.0/css/all.css" integrity="sha384-lZN37f5QGtY3VHgisS14W3ExzMWZxybE1SJSEsQp9S+oqd12jhcu+A56Ebc1zFSJ" crossorigin="anonymous">
      <script src="js/alarm.js"></script>
   </head>
   <body>
      <div class="container" x-data='loadAlarmConfig()' x-init='() =>  {
         statTimer();
         }'>
      <div class="row mt-5" >
      <div class="col">
         <div class="col-md-12">
            <div class="card bg-dark ">
               <div class="card-body">
                  <h3 class="card-title text-white">
                     <i class="fas fa-clock"></i>
                     <span type="text" x-text="time.hour"></span>:
                     <span type="text" x-text="time.minute"></span>:
                     <span type="text" x-text="time.second"></span>
                     <span type="text" x-text="time.a"></span>
                  </h3>
                  <a class="btn btn-light mt-2" @click="createAlarm()">Create Alarm</a>
               </div>
            </div>
         </div>
         <div class="col-md-12">
            <div class="card bg-light mt-3">
               <div class="card-body">
                  <div class="row">
                     <div class="col" x-show="create_alarm">
                        <div class="row">
                           <div class="col-md-12">
                              <div class="row">
                                 <div class="col">
                                    <label for="">Title</label>
                                    <input placeholder="Type alarm title here" type="text" x-model="alarm.title" class="form-control">
                                 </div>
                                 <div class="w-100 mt-4"></div>
                                 <div class="col">
                                    <label for="">Hour</label>
                                    <input type="number" x-model="alarm.hour" class="form-control">
                                 </div>
                                 <div class="col">
                                    <label for="">Minute</label>
                                    <input type="number" x-model="alarm.minute" class="form-control">
                                 </div>
                                 <div class="col">
                                    <label for="">AM/PM</label>
                                    <select class="form-control" name="" x-model="alarm.a" id="">
                                       <option value="AM">AM</option>
                                       <option value="PM">PM</option>
                                    </select>
                                 </div>
                              </div>
                              <div class="row mt-4">
                                 <div class="col-md-12">
                                    <button class="btn btn-dark" @click="saveAlarm()">
                                    <i class="fas fa-check"></i>
                                    </button>
                                 </div>
                              </div>
                              <div x-show="is_alarm_error" class="row">
                                 <div class="col">
                                    <span x-text="alarm_error" class="text text-danger">
                                    </span>
                                 </div>
                              </div>
                           </div>
                        </div>
                     </div>
                  </div>
               </div>
            </div>
         </div>
      </div>
      <div class="col">
         <div x-show="alarms.length > 0" class="row">
            <div class="col-md-12">
               <h2>Alarms</h2>
               <template x-for="item in alarms" :key="item">
                  <div x-bind:class="getAlarmClass(item)">
                     <div class="card-body">
                        <h5 class="title">
                           <span x-text="item.title"></span> will
                           <span x-text="item.label"></span>
                        </h5>
                        <p class="h3 mt-2">
                           <i class="fas fa-bell mr-2"></i><span x-text="item.time_left"></span>
                        </p>
                        <hr>
                        <button x-show="item.active" @click="deactivateAlarm(item.index)" class="btn btn-danger">
                        Stop
                        </button>
                        <button x-show="!item.active" class="btn btn-success">
                        Stopped
                        </button>
                     </div>
                  </div>
               </template>
            </div>
         </div>
      </div>
   </body>
</html>
  • Imports

    Clearly you can see we have included below js in our index.html file

    <script nomodule src="https://unpkg.com/browser-es-module-loader/dist/babel-browser-build.js"></script>
    <script nomodule src="https://unpkg.com/browser-es-module-loader"></script>

    We have imported babel and the es6 module. Yes, we will be using es6 syntax it is cleaner and very efficient. More you can find in this link https://babeljs.io/setup#installation go there and see es6 if you haven’t been in tough till yet.

    <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>

    We have imported momentjs library. Yes, we are building a digital clock in the app. We will be needing a more time util library to deal with it MomentJs is the best option. For more details go to https://momentjs.com/docs/

    <script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.min.js" defer></script>eff

    Alpinejs. Its the main library we are trying to implement go to https://github.com/alpinejs/alpine. for details

    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous" />

    Now we need to use some kind of CSS library we will be using bootstrap-4. We have bootstrap CDN. Go to https://getbootstrap.com/docs/4.4/getting-started/introduction/ for better understanding of bootstrap

    <script src="js/alarm.js"></script>

    Finally, our own js file alarm.js is imported which we will use to implement our own code in alpinejs to hand interactive tasks

    HTML Code/JS implementations

    <div class="container" x-data='loadAlarmConfig()' x-init='() =>  {statTimer();}'></div>

    Now inside our code in the body tag, we have parent div. We have given class container-md to parent div.

  • Alarm.js

    var date = moment().local();
    const alarmSound = new Audio("alarm_sound.mp3");
    
    function loadAlarmConfig() {
    
        return  {
            time: {
                hour: moment().local().format("h"),
                minute: moment().local().format("m"),
                second: moment().local().format("s"),
                a: moment().local().format("A"),
            },
            alarms: [],
            create_alarm: false,
            alarm_error: '',
            is_alarm_error: false,
            alarm: {
                hour: 0,
                minute: 00,
                second: 00,
                title: ""
            },
            statTimer: function() {
    
                const self = this;
                this.create_alarm_button = true;
    
                function updateClock() {
                    self.time.hour = moment().local().format("h");
                    self.time.minute = moment().local().format("m");
                    self.time.second = moment().local().format("s"); 
                    self.time.a = moment().local().format("A");  
                }
    
                setInterval(() => {
                    updateClock();
                }, 1000);
    
                function runAlarms() {
    
                    self.alarms = self.alarms.map((alarm) => {
                        if(alarm.active === true) {
                            const current_date = new moment().local()
                            const diff = diffYMDHMS(alarm.start_time, current_date);
                
                            let label = getAlarmLabel(diff);
                            let completed = isAlarmCompleted(diff);
                            if (completed == true) {
                                console.log("BEEP BEEP")
                                alarmSound.play();
                            } else {
                                alarm.time_left = label;
                            }
                            alarm.completed = completed;
                           
                        }
                        return alarm;
                    })
                }
    
                setInterval(() => {
                    runAlarms();
                }, 2000);
    
    
            },
            createAlarm: function() {
    
                this.create_alarm = true;
                this.alarm = {
                    hour: this.time.hour,
                    minute: this.time.minute,
                    second: this.time.second,
                    title: "",
                    "a": this.time.a
                };
            },
            saveAlarm: function() {
    
                if(this.alarm.title == '') {
                    this.alarm_error = "Please enter why you setting this alarm";
                    this.is_alarm_error = true;
                    return;
                }
    
                const alarm_date = moment(`${this.alarm.hour}:${this.alarm.minute} ${this.alarm.a}`, 'hh:mm A')
                const current_date = new moment().local()
                const diff = diffYMDHMS(alarm_date, current_date);
    
                if (isAlarmCompleted(diff)) {
                    this.alarm_error = "This alarm is not valid. Choose proper time";
                    this.is_alarm_error = true;
                    return ;
                }
    
                let label = getAlarmLabel(diff);
                this.alarm.time = diff;
                this.alarm.start_time = alarm_date;
                this.alarm.label = "ring at : "+ alarm_date.format("h")+":"+alarm_date.format("m")+this.alarm.a;
                this.alarm.time_left = label;
                this.alarm.active = true;
                this.alarm.completed = false;
                this.alarm.index = this.alarms.length;
                this.alarm_error = "";
                this.is_alarm_error = false;
    
                this.alarms.push(copyObject(this.alarm));
            },
            deactivateAlarm: function(item) {
                this.alarms = this.alarms.map((alarm) => {
                    if(alarm.index === item) {
                        
                        alarm.completed = true;
                        alarm.active = false;
                        alarmSound.pause();
                    }
                    return alarm;
                })
            },
            getAlarmClass: function getAlarmClass(item) {
                if(!item.active) {
                    return "card mt-3 bg-dark text-white ";
                } else if(item.completed) {
                    return "card mt-3 bg-warning text-white";
                } else {
                    return "card mt-3 bg-light"
                }
            }
        }
    }
    
    function copyObject(alarm) {
        return  Object.assign({}, alarm);
    }
    
    function isAlarmCompleted(diff) {
    
        return ["hours", "minutes", "seconds"].map((key) =>  {
            return diff[key] <= 0;
        }).filter((key) => key === false).length === 0;
    }
    
    function getAlarmLabel(diff) {
        let label = 'Time left : ';
    
        ["hours", "minutes", "seconds"].map((key) =>  {
            if(diff[key] !== 0) {
    
                if (label !== "") {
                    label = label + ""
                }
                label = label + `${diff[key]}${key[0].toLowerCase()}.` ;
            }
        })
    
        return label;
    }
    
    function diffYMDHMS(date1, date2) {
    
        let hours = date1.diff(date2, 'hours');
        date2.add(hours, 'hours');
        let minutes = date1.diff(date2, 'minutes');
        date2.add(minutes, 'minutes');
        let seconds = date1.diff(date2, 'seconds');
        return { hours, minutes, seconds};
    }

    Tags:

    1. x-data → Now in this attribute we have called loadAlarmConfig() function which is returning a js object with all data and functions we will be needing in our code. After getting this global object from this function we can access each variable and function in child HTML nodes using various attributes (x-text, x-model, x-bind, etc).

      As an example in return of loadAlarmConfig() a variable time and respective function like startTimer/createAlarm which we can directly call in child nodes using AlpineJs directives

    2. x-initNow, this a big virtue of AlphineJS. It is a must-have attribute we all need on various occasions. As we will call x-init to initialize objects and variables to use inside dom elements.

      There are several things we need to do (API calls/Window onload functions etc). Which we can call here. Inside the object returned from loadAlarmConfig() we have created a function statTimer(). Which is updating current time values in time object inside our global object.

      We can directly call this function inside x-init

      x-init='() =>  { statTimer(); }' 

      Call to x-init function is very simple startTimer function is available after getting it via loadAlarmConfig in x-data

      function updateClock() {
          self.time.hour = moment().local().format("h");
          self.time.minute = moment().local().format("m");
          self.time.second = moment().local().format("s");
          self.time.a = moment().local().format("A");
      }
      setInterval(() => {
          updateClock();
      }, 1000);

    Inside startTimer we have saved current object instance in self. As some Async function is written inside setInterval(). in every 1000 milliseconds, we are calling upateValue() function and updating time object inside the global object.

    time: {
        hour: moment().local().format("h"),
        minute: moment().local().format("m"),
        second: moment().local().format("s"),
        a: moment().local().format("A"),
    },

    Above is the time object which we have created globally

  • Part-1 Display Alarm Clock

Aplinejs Alarm Clock

  • Now, let's start by displaying our clock first. We need to show the current clock

    <div class="card-body">
        <h3 class="card-title text-white">
            <i class="fas fa-clock"></i>
            <span type="text" x-text="time.hour"></span>:
            <span type="text" x-text="time.minute"></span>:
            <span type="text" x-text="time.second"></span>
            <span type="text" x-text="time.a"></span>
        </h3>
        <a class="btn btn-light mt-2" @click="createAlarm()">Create Alarm</a>
    </div>

    Here the x-text function is used to display time object values. These variables are getting auto-updated in function startTimer we have explained it already. It will update time in real-time we have called updateClock function at every 1 second. When you will open index.html you can see it getting updated

    (Create Alarm) Button

    Yes, the main important part of our project. Create an alarm button

    <button class="btn btn-info" @click="createAlarm()">Create Alarm</button>

    Yes we can see we have configured click event and call createAlarm function in our global object

    createAlarm: function() {
        this.create_alarm = true;
        this.alarm = {
            "hour": this.time.hour,
            "minute": this.time.minute,
            "second": this.time.second,
            "title": "",
            "a": this.time.a
        };
    },

    This is our function inside the global object in alarm.js. We are setting current global object create_alarm to True. And pre-populate values inside the current alarm object which show that we can autofill current time values while creating a new alarm.

    That’s it. In this way, you can configure click events easily. When we will click on create alarm below UI Is getting populated

    <div class="col" x-show="create_alarm">
        <div class="row">
            <div class="col-md-12">
                <div class="row">
                    <div class="col">
                        <label for="">Title</label>
                        <input placeholder="Type alarm title here" type="text" x-model="alarm.title" class="form-control">
                    </div>
                    <div class="w-100"></div>
                    <div class="col">
                        <label for="">Hour</label>
                        <input type="number" x-model="alarm.hour" class="form-control">
                    </div>
                    <div class="col">
                        <label for="">Minute</label>
                        <input type="number" x-model="alarm.minute" class="form-control">
                    </div>
                    <div class="col">
                        <label for="">AM/PM</label>
                        <select class="form-control" name="" x-model="alarm.a" id="">
                            <option value="AM">AM</option>
                            <option value="PM">PM</option>
                        </select>
                    </div>
                </div>
                <div class="row mt-2">
                    <div class="col-md-12">
                        <button class="btn btn-primary" @click="saveAlarm()">
                            Save
                        </button>
                    </div>
                </div>
            </div>
        </div>
    </div>
  • Part-2 Create Alarm Code

    Now its a code for creating alarm

Aplinejs Create Alram Clock

<div class="col" x-show="create_alarm"></div>

In this parent div component. We have x-show=’create_alarm” look at js code of createAlarm we have set a global variable create_alarm. when it is true we show create alarm div. Otherwise, its display is set to none. Very simple…

In the reset code, we are using an x-model to bind the alarm object attributes to inputs. x-model building a two-way interaction whatever changes we will do in these input types they will be synced automatically to their respective x-model definitions.

<input placeholder="Type alarm title here" type="text" x-model="alarm.title" class="form-control">

Just example we have set alarm.title to this input text box. Whatever we will type will be synced automatically to the alarm.title variable. Easy

Action Button

<button class="btn btn-dark" @click="saveAlarm()">   < i class="fas fa-check"> < /i>< /button>

Let’s see what we are doing in saveAlarm function in alarm.js

saveAlarm: function() {
if (this.alarm.title == '') {
        this.alarm_error = "Please enter why you setting this alarm";
        this.is_alarm_error = true;
        return;
    }
const alarm_date = moment(`${this.alarm.hour}:${this.alarm.minute} ${this.alarm.a}`, 'hh:mm A')
    const current_date = new moment().local()
    const diff = diffYMDHMS(alarm_date, current_date);
if (isAlarmCompleted(diff)) {
        this.alarm_error = "This alarm is not valid. Choose proper time";
        this.is_alarm_error = true;
        return;
    }
let label = getAlarmLabel(diff);
    this.alarm.time = diff;
    this.alarm.start_time = alarm_date;
    this.alarm.label = "ring at : " + alarm_date.format("h") + ":" + alarm_date.format("m") + this.alarm.a;
    this.alarm.time_left = label;
    this.alarm.active = true;
    this.alarm.completed = false;
    this.alarm.index = this.alarms.length;
    this.alarm_error = "";
    this.is_alarm_error = false;
    this.alarms.push(copyObject(this.alarm));
}

In this code at initial steps, we have done some validations if alarm title is empty or alarm time is wrongly set we will show them error respectively

Next, we will create alarm settings time_left and alarm start_time for alarm and set create_alarm false.

Finally when we are done creating alarms. We will push current alarm to alarms global array and see we have used copyObject so that reference of current temporary alarm object is removed

  • Part-3 Displaying Alarm

Alpinejs Displaying Alarm

  • Below is the code

    <template x-for="item in alarms" :key="item">
        <div x-bind:class="getAlarmClass(item)">
            <div class="card-body">
                <h5 class="title">
                    <span x-text="item.title"></span> will
                    <span x-text="item.label"></span>
                </h5>
                <p class="h3 mt-2">
                    <i class="fas fa-bell mr-2"></i><span x-text="item.time_left"></span>
                </p>
                <hr>
                <button x-show="item.active" @click="deactivateAlarm(item.index)" class="btn btn-danger">
                    Stop
                </button>
                <button x-show="!item.active" class="btn btn-success">
                    Stopped
                </button>
            </div>
        </div>
    </template>


    As we pushed created alarm to global alarms array. Below code will show our newly created alarm as we have

    <template x-for="item in alarms" :key="item"></template>

    Setting text values for display. We are showing alarm label and time_left in x-text attribute and setting their respective values from the alarm object inside alarms array

    <div x-bind:class="getAlarmClass(item)"></div>

    We are controlling the display of the current alarm container. let’s look at the code of getAlarmClass

    getAlarmClass: function getAlarmClass(item) {
        if (!item.active) {
            return "card mt-3 bg-dark text-white ";
        } else if (item.completed) {
            return "card mt-3 bg-warning text-white";
        } else {
            return "card mt-3 bg-light"
        }
    }

    We are deciding the alarm card theme here based on the current alarm status. It will become dark if gets deactivated and yellowish in case it is ringing and light if it is active

    Stop alarm action button

    <button x-show="item.active" @click="deactivateAlarm(item.index)" class="btn btn-danger">
        Stop
    </button>

    We are checking if the alarm is active we show this button and on click, we will be calling deactivateAlarm function with current alarm index

    deactivateAlarm: function(item) {
        this.alarms = this.alarms.map((alarm) => {
            if (alarm.index === item) {
                alarm.completed = true;
                alarm.active = false;
                alarmSound.pause();
            }
            return alarm;
        })
    }
  • Part 4 Running Alarms in background

    We need to iterate all alarms one by one in our global object. Check and update each and every alarm status

    Now, these alarms will run automatically after getting pushed into a global object.

    Code to integrate global alarm array is writing inside starTimer which we have called at parent div x-init

    function runAlarms() {
        self.alarms = self.alarms.map((alarm) => {
            if (alarm.active === true) {
                const current_date = new moment().local()
                const diff = diffYMDHMS(alarm.start_time, current_date);
                let label = getAlarmLabel(diff);
                let completed = isAlarmCompleted(diff);
                if (completed == true) {
                    console.log("BEEP BEEP")
                    alarmSound.play();
                }
                alarm.completed = completed;
                alarm.time_left = label;
            }
            return alarm;
        })
    }
    setInterval(() => {
        runAlarms();
    }, 2000);

    We are calling a runAlarms function every 2 milliseconds and iterate over each alarm and set their status respectively and run alarm sound as they get completed.

    Note - Here is link of live Webapp for this article and link to github repo

    This is a basic tutorial I hope you will get a glimpse of what we can achieve with this library. I hope you will find it useful and we'll be able to utilize it in many scenarios.

    Get yourself added to our 2500+ people subscriber family to learn and grow more and please hit the share button on this article to share with your co-workers, friends, and others.

    Check out articles on Javascript, Angular, Node.js, Vue.js

    For more articles stay tuned to overflowjs.com

    Happy coding❤️

Email

About Rakesh Bhatt

Rakesh is a self-learned programmer from around 9 years. He has worked on various programming languages like PHP, java, c#, python, javascript, node js, react, react-native, etc. His forte of working is around the image processing, AR and the building highly scalable web apps.