Create a JavaScript library. Build MVP
Alex Shulaev
Posted on June 13, 2020
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 👋
Posted on June 13, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.