DEV Community

Cover image for Create a JavaScript library. Build MVP
Alex Shulaev
Alex Shulaev

Posted on

Create a JavaScript library. Build MVP

It's time to use the template that we developed in the previous article 🚀

I want to develop my own small side project. In a nutshell, this will be a JavaScript library for working with modal windows. I want to cover all the steps from creating the library itself (in our case, from creating a template for the library 😄), to publishing documentation and presenting the resulting project

And again, I recorded my entire process on video 👋

Let's start preparing

Creating a repository using a template with a configured build for the project. Then we clone the repository

git clone git@github.com:Alexandrshy/keukenhof.git

and don't forget to install all the dependencies

cd keukenhof
yarn

Since we use the GitHub Actions to build and publish the package, we need to create tokens for GitHub and npm and add them to the secrets

You also need to make the following changes to the package.json file (since it is a copy for the template, it has some irrelevant fields). If you are creating a clean project just add your description

  "name": "keukenhof",
  "description": "Lightweight modal library 🌷",
  "repository": {
    "type": "git",
    "url": "https://github.com/Alexandrshy/keukenhof"
  },
  "keywords": [
    "javascript",
    "modal",
    "dialog",
    "popup"
  ],
  "bugs": {
    "url": "https://github.com/Alexandrshy/keukenhof/issues"
  },
  "homepage": "https://github.com/Alexandrshy/keukenhof",

This is where we finished the preparation, we move on to writing code

MVP Development

Reserve the name of our library in window

export const Keukenhof = ((): KeukenhofType => {})();

window.Keukenhof = Keukenhof;

To describe the Keukenhof type, we need to understand what interface we'll have in MVP. I'll only define the init function which on the basis of markup is to initialize the handler to open the modal

export type ConfigType = {
    selector?: string;
    triggers?: HTMLElement[];
    openAttribute?: string;
    closeAttribute?: string;
    openClass?: string;
};

export type KeukenhofType = {
    init: (config?: ConfigType) => void;
};

The configuration object will have the following fields:

  • openClass: class name that will be added to the modal window when it opens;
  • selector: modal window selector with which to interact;
  • triggers: list of nodes click on which will open a modal window;
  • openAttribute: data attribute of the element's connection (usually a button) with the modal window;
  • closeAttribute: data attribute for the element that will register the click and close the current modal window

Write the init function:

/**
 * Initialize modal windows according to markup
 *
 * @param {ConfigType} config - modal window configur
 */
const init = (config?: ConfigType) => {
    const options = {openAttribute: ATTRIBUTES.OPEN, ...config};
    const nodeList = document.querySelectorAll<HTMLElement>(`[${options.openAttribute}]`);
    const registeredMap = createRegisterMap(Array.from(nodeList), options.openAttribute);
};

return {init};

The init function finds a list of elements containing an attribute to open (if this attribute was not overridden in the configuration object, we use the default ATTRIBUTES.OPEN, we have it moved to a separate file with constants). Since one modal window can be opened by clicking on several elements, we need to map all modal windows to all elements that have openAttribute. To do this, we write the function createRegisterMap:

    const createRegisterMap = (nodeList: HTMLElement[], attribute: string) => {
        // Accumulating an object where the key is the modal window selector, and the value is the element that will open the corresponding modal window
        return nodeList.reduce((acc: {[key: string]: HTMLElement[]}, element: HTMLElement): {
            [key: string]: HTMLElement[];
        } => {
            // Get the value from the attribute
            const attributeValue = element.getAttribute(attribute);
            // If there is no value, just skip the item
            if (!attributeValue) return acc;
            // If the element is encountered for the first time, add it to the accumulator and write an empty array
            if (!acc[attributeValue]) acc[attributeValue] = [];
            acc[attributeValue].push(element);
            return acc;
        }, {});
    };

After we received the map of modal windows that need to be initialized, we iterate each element of the map and create instances of Modal:

for (const selector in registeredMap) {
    const value = registeredMap[selector];
    options.selector = selector;
    options.triggers = [...value];
    modal = new Modal(options);
}

Let's start describing the Modal class itself:

/**
 * Modal window
 */
class Modal {

    /**
     * Modal constructor
     *
     * @param {ConfigType} param - config
     */
    constructor({
        selector = '',
        triggers = [],
        openAttribute = ATTRIBUTES.OPEN,
        closeAttribute = ATTRIBUTES.CLOSE,
        openClass = 'isOpen',
    }: ConfigType) {
        this.$modal = document.querySelector(selector);
        this.openAttribute = openAttribute;
        this.closeAttribute = closeAttribute;
        this.openClass = openClass;

        this.registerNodes(triggers);
    }

    /**
     * Add handlers for clicking on elements to open related modal windows
     *
     * @param {Array} nodeList  - list of elements for opening modal windows
     */
    registerNodes(nodeList: HTMLElement[]) {
        nodeList
            .filter(Boolean)
            .forEach((element) => element.addEventListener('click', () => this.open()));
    }
}

export const ATTRIBUTES = {
    OPEN: 'data-keukenhof-open',
    CLOSE: 'data-keukenhof-close',
}; 

The registerNodes method adds click handlers for buttons with the data-keukenhof-open attribute. I advise you to use constants for string elements to avoid errors and make future refactoring easier. The open method now we can describe in just one line

/**
 * Open moda window
 */
open() {
    this.$modal?.classList.add(this.openClass);
}

Now, we can "open" our modal window 🎉 I think you understand what the close method will look like

/**
 * Close modal window
 */
close() {
    this.$modal?.classList.remove(this.openClass);
}

And to call this method, you need to add click handlers for elements with the attribute data-keukenhof-close. We'll do it when opening a new modal window, so as not to keep handlers for modal windows that are closed

/**
 * Click handler
 *
 * @param {object} event - Event data
 */
onClick(event: Event) {
    if ((event.target as Element).closest(`[${this.closeAttribute}]`)) this.close();
}

We need to bind the this value in constructor

this.onClick = this.onClick.bind(this);

Implementing separate methods for removing and adding click handlers

/**
 * Add event listeners for an open modal
 */
addEventListeners() {
    this.$modal?.addEventListener('touchstart', this.onClick);
    this.$modal?.addEventListener('click', this.onClick);
}

/**
 * Remove event listener for an open modal
 */
removeEventListeners() {
    this.$modal?.removeEventListener('touchstart', this.onClick);
    this.$modal?.removeEventListener('click', this.onClick);
}

We'll add click handlers when opening a modal window, and delete when closing

open() {
    this.$modal?.classList.add(this.openClass);
    this.addEventListeners();
}

close() {
    this.$modal?.classList.remove(this.openClass);
    this.removeEventListeners();
}

Well, that's it, the minimum functionality is ready 🚀 It might seem that our solution is redundant, for a library that simply adds and removes a class. And at the moment this is true, but it gives us the opportunity to expand our functionality in the future, which I plan to do 🙂

Library Usage Example

Link to the repository on GitHub

Link to upcoming improvements in [the Roadmap (https://github.com/Alexandrshy/keukenhof#roadmap)

Conclusion

I hope my article was useful to you. Follow me on dev.to, on YouTube, on GitHub. Soon I'll continue this series of articles and I'll definitely share my results with you 👋

Top comments (0)