Not every frontend projects needs to use a set of complex tools to build or run. Almost all of the new Javascript projects contains a Framework with this tools, transpilers, transformers, minifiers, css libs and dependencies for every feature. But you dont necessarily need to follow this way to build your application, specially if your project is a small one.
This further article shows some approaches to create a application without that bunch of complex tools. A P.O.C. was created in order to demostrate it. It is about one simple crud created with modern Javascript with no dependencies.
It was written using the functional paradigm because Javascript is more Haskell than Java!
About the css, no one lib or framework was used, just a css reset followed by the app style.
Creating a simple CRUD
In order to sample how to use the language in a simple way, using the "Vanilla Javascript", a Create Retrieve Update Delete user interface have been created and the following text explains a several ways to realise it.
An exemple of a simple CRUD APP in raw Javascript could be found in this repository:
https://github.com/misabitencourt/javascript-made-simple
Importing the main.js script
Using a HTML5 document, it is possible to add a "script" tag with "module" as "type" property. This tag will import aES Module on your web application that could also import another modules.
index.html
<!DOCTYPE html><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>Javascript made simple!</title><linkrel="stylesheet"href="css/reset.css"><linkrel="stylesheet"href="css/todolist.css"></head><body><divclass="app"></div><scripttype="module"src="js/main.js"></script></body></html>
js/main.js
importactionsfrom'./actions/index.js';importtodoAppComponentfrom'./components/app.js';window.todoApp=function(renderInEl){/// ... more code
Note: Thediv
with theapp
class is out target element to render the entire application.
The View - Manipulation Elements with just Javascript
The Javascript runtime available on the current major browsers is powerful. One of its power is theDOM element manipulation. Despite that, the "raw javascript" element API could be simplifyed by our home made functions. For example, if you must create a title for one application section using the built in api, it would be like:
consttitle=document.createElement('h1');// Creates elementtitle.textContent='User list';// Sets the text content of itparentElement.appendChild(title);// Appends the title element as a parentElement child (it must be another element, like document.body or another)
At the first look, it sounds nice and easy, but if you would have a little bit more complex element tree, it would be a pain:
// Creates the four elementsconsttitle=document.createElement('h1');consttitleIcon=document.createElement('img');consttitlePrimaryText=document.createElement('span');consttitleSecondaryText=document.createElement('small');// Adds CSS classes to themtitle.classList.add('app-main-title');titleIcon.classList.add('app-main-title-icon');titlePrimaryText.classList.add('app-main-title-primary-text');titleSecondaryText.classList.add('app-main-title-secondary-text');// Adds text Content and src to themtitlePrimaryText.textContent='User list';titleSecondaryText.textContent='Listing X users from Y';titleIcon.src='assets/img/users-icon.svg';// Appends the title childrentitle.appendChild(titleIcon);title.appendChild(titlePrimaryText);title.appendChild(titleSecondaryText);// Appends the title with children to one parent elementparentElement.appendChild(title);
In order to create a DOM tree in a most efficient way, i reccommend the approach used by some microframeworks like:Mithril,Hyperapp orReact without JSX.
It could solved by create a function with receives, at least, these parameters:
- String: the tag name
- String: css class list
- Element[]: the list of children elements or the text content
The implementation of that is like that:
exportdefaultfunctionel({tag,textContent,classList,children,events,attributes}){constelement=document.createElement(tag||'div');if(textContent){element.textContent=textContent;}if(classList){(classList+'').split('').filter(clazz=>clazz.trim()).forEach(clazz=>element.classList.add(clazz));}if(children){children.forEach(child=>element.appendChild(child));}if(events){events.forEach(event=>{element.addEventListener(event.event,event.listener);});}if(attributes){attributes.forEach(attribute=>{element.setAttribute(attribute.name,attribute.value);});}returnelement;}
This function is used in this way:
// Creates the element tree mentioned above using our helperconsttitle=el({tag:'h1',classList:'app-main-title',children:[el({tag:'img',classList:'app-main-title-icon',attributes:{src:'assets/img/users-icon.svg'}}),el({tag:'span',classList:'app-main-title-primary-text',// OR a string constant from another moduletextContent:'User list'// or getTranslatedText() from another module}),el({tag:'span',classList:'app-main-title-secondary-text',// OR a string constant from another moduletextContent:'Listing X users from Y'// or getListCount() from another module})]});// Appending the created element tree to another elementparentElement.appendChild(title);
We could got a lot of advantages using this kind of functional module to create views. Imagine if you must code a user list of that screeen, you would have got a shortcut in your hands. Take a look:
// Supposing it is in an async functionconstusers=awaituserService.list();constuserList=el({tag:'ul',classList:'app-user-list',children:users.map(user=>(// Transforms user json to user list item elementel({tag:'li',classList:'app-user-list-item',children:[// User name on listel({tag:'span',textContent:user.name}),// ... icons and stuff]})))});
If you choose to use it, you would probably have to create a application state manager. It doesnt needs to be something complicated likeRedux.
I suggest to use an object json witch represents the application state and can't be changed by component functions, just read by them. Once the component needs to trigger some event, it could do it using some callback function in order to sepparate the render function and the state change logic. That state change functions could be tested on unity.
The code above is about the root render of a component tree:
letstate={};letrenderedElement;/** * App state changed! action trigger! * @param {*} actionName Id of the action triggered * @param {*} params action parameters */functionactionTrigger(actionName,params){constaction=actions.find(action=>action.name===actionName);if(!action){console.warn(`Action${actionName} not found`);returnstate;}state=action.exec(state,params);}/** * Single app render, on action trigger or app start */functionrender(){console.log('render called',state);constcurrentRender=todoAppComponent(state,(action,params)=>{actionTrigger(action,params);render();});if(renderedElement){renderedElement.parentElement.removeChild(renderedElement);}renderedElement=currentRender;renderInEl.appendChild(renderedElement);}// First renderrender();
A simple component would be like:
exportdefault(state,action)=>el({classList:'todo-list-app-todo-list-crud-list',children:(()=>{if(!(state.todolist&&state.todolist.length)){return[el({tag:'p',classList:'alert-default',textContent:'No items have been created yet.'}),button({text:'Reload',onClick:()=>action(TODO_LIST_RELOAD_ACTION,null)})];}returnstate.todolist.map(todo=>el('div','todo-list-app-todo-list-crud-list-item',{textContent:todo.text}));})()});
This suggestion is a reactive functional approach. Some procedural or object oriented one also could be done with raw javascript if you are not confortable with functional programming.
Using modules - decoupling code with javascript modules
ES2016 Javascript modules naturally acts like aSingleton object. The script inside a module have it own scope. Unlike the classic javascript code imported with "script src=", when you declare a variable in some module, that variable does not populate the global scope. The programmer can control what variables can be accessible from other modules using the "export" sintax. For example:
some-module.js
// This variable is only accessible from THIS moduleconstpetSizes={small:1,medium:2,large:3};functioncheckPetSize(size){switch(size){case(petSizes.small):return'small';case(petSizes.medium):return'medium';case(petSizes.large):return'LARGE';default:return'unknown';}}// This function is available for another modulesexportcheckPetSize;
By using javascript modules to your advantage, you can naturally create singleton services or someobject factories. Using the javascript "ducktype" to your advantage, the dependency injection also could be done easily.
Fetch HTTP Client
The majority of modern apps must send and retrieve data from backend webservices. It is usually being done with HTTP protocol. Nowadays, there is no need of any lib likeJQuery.ajax oraxios to perform that kind of request.
The modern browsers supportsFetch API by default. This api uses javascript Promises and it is easy to use. See the following sample:
asyncfunctiongetConfigJson(){constresponse=awaitfetch(`https://my-api.com/v1/config/`);if(response.code!==200){thrownewError(`Error no fetch config`);}constconfigJson=awaitresponse.json();returnconfigJson;}
I recommend the wrapping of the fetch api in specific module to perform a http fetch. By this way, some request configurations like the API path and the authorization request headers would be easily managed.
Native Local Databases
The HTML, CSS and JS trio is used not only to create web apps but mobile and desktop too (seeElectron andTauri). All of the software on this platforms needs a local database in a higher or lower level. All online websites sometimes needs to store some cache and configuration in a local machine.
In all of this platforms, the JS code could be exactly the same on doing database operations. We have two secure ways to do it:Localstorage andIndexedDb. This first option is the faster way to save and retrieve data. Check this out:
localStorage.setItem('data-key','data-value');
This API have been available in all major browsers for years but Localstorage have some issues. It is not available in all javascript backend runtimes and it stores only strings. Thease strings are normally one stringified JSON stored like:
// savelocalStorage.setItem('app-config',JSON.stringify(config));// loadconstconfig=JSON.parse(localStorage.getItem('app-config'));
The LocalStorage also have data size limitations and it is different between browsers and runtimes. More than 1 Megabyte datastorage is dangerous to using. For large amount of data, is recommended IndexedDb instead.
Code checking and tests
With all of that APIs you have seen before, you are able to create and entire application with Javascript without no extra transformer, library or framework. You may consider it optional for smaller projects but some code audition tools are recommended. The standart tools for syntax and style checking are:Prettier andESLint.
End to end tests (also known as e2e or integration test) are almost essential for frontend apps. To perform it,Playwright is a good fit for. The original raw javascript code whould not changed or transformed to create it. This lib works just as a bot navigatinig, clicking, typing and checking the app in a browser of your preference.
A good option
As you have seen, it is absolutely possible to create a Javascript application without a bunch of complex polyfills, JSX, transpilers, libs and etc. The first detail you plan about you project do not needs to be a framework choose, why not give a change to the language default resources?
If it is about a huge and complex project with dozens of coders from different ecosystems working on it, the avoiding dependencies could be on your way. But if you have some mid-sized project or an ordinary one, you may claim yourself for simplicity.
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse