logo

Explanation

What is the point?

There are many high-level frameworks out there (Angular, React, Vue, Svelte etc.) that often come bundled as entire ecosystems - demanding complex setups, build processes, and a rigid way of doing things that sometimes go against your logic.
You don't really know what's happening under the hood and it often feels like you're learning a whole new language when, in reality, you might ask yourself:
Shouldn't good old JavaScript already have everything?

Imagine a framework with a similar developer experience like their big brothers - but made with just a handful (namely six) of fairly simple JavaScript functions. No package managers, no build tools needed - just one file you can drop into any project.
Designed for clarity, it is intended that you can understand its mechanics, debug it effortlessly, and modify it as needed. It is simple yet powerful, easy to use yet endlessly flexible. A tool that enhances your workflow without forcing you into an ecosystem - a framework that can feel truly yours!

RobJS is easy to understand - on purpose

By going now through the whole implementation line by line you will be able to fully understand how it works under the hood.
And you might be surprised how simple it actually is. You will also understand the drawbacks but be enabled to find pragmatic solutions for your project.
After that you are really back into the drivers seat and have even the guts to break out and change the core functions if needed.

Lets dive in:

        
// Everything is wrapped inside a class that gives you later an instance of your app. 
// You can console.log this instance at any time and see whats in it which makes it very easy to debug.
// We export this class to make it available in other files of our project to keep everything organized.
          
export class RobJSApp {

  // When instantiating a new RobJSApp we assign the tagId. This is needed to later render your app inside the tag of the html file. 
  // This gives you the freedom to show your app where you want in the DOM.
  // Further an empty state object, an empty oldState object and and empty array called components is created.

  constructor(tagId) {
    this.tagId = tagId
    this.state = {}
    this.oldState = {}
    this.components = []
  }
  
  ...
}

// usage
const app = new RobJSApp('myapp') // assuming you gave the tag in your html file an id of 'myapp' 
console.log(app)
    
    
    
export class RobJSApp {
  ...

  // This next line of code makes your app variable available in the whole project.
  // The window object is the global object in browser-based JavaScript environments. Adding properties to window essentially creates global variables.
  // A drawback to this is that you could overwrite it accidentially so be aware of that.

  init(app){ window[app] = this }

  ...
}

// usage
const app = new RobJSApp('myapp')
app.init('app')

    
    
  
export class RobJSApp {
  ...

  // We use this next function to create an entry into our state object with a key and a inital value.
  // We do the same to the old state object to make it available there as well.
  // You can create state variables in all possible fashions (see usage).

  defineStateVar(key, initialValue) { 
    this.state[key] = initialValue 
    this.oldState[key] = initialValue 
  }

  ...
}

// usage
app.defineStateVar('count', 0)
app.defineStateVar('name', 'John')
app.defineStateVar('list', ['a', 'b', 'c'])
app.defineStateVar('isGreat', true)
app.defineStateVar('data', [{'name': 'Jane', 'age': 23}, {...}, ...])

    
    
    
export class RobJSApp {
  ...

  // We use this next function to register an component to our app.
  // We must pass the viewFunction which is essentially a JavaScript function that returns html (we call this 'component').
  // We must wrap it inside a div and give it a id, because the Framework will find this component later by that id in the DOM to update it.
  // A drawback here is that you need to be careful of not using ids more than once. Come up with a simple naming convention and it is no longer a problem.
  // However, an advantage is that only components that should update need an id and must be registered. 
  // Further we can decide on which changes of state it should re-render by passing an array with the names of the state variables. 
  // Oftentimes these would be at least the state variables that are used in the component but it is not limited to that.
  // Also, if the component consumes arguments (be it constants or state variables) they are registered into their component here as well.
  // Therefore it is not needed to pass them later again whenever we use the component which keeps things very tidy.

  registerComponent(viewFunction, id, usedStateKeys, ...args) { this.components.push({viewFunction, id, usedStateKeys, args}) }
  ...
}

// usage
// define state variables the component consumes
app.defineStateVar('name', 'Janis')
app.defineStateVar('age', 30)

// create a component
const MyComponent (id, isLegal) => {
  if {!isLegal} return `<div id=${id}></div>`
  return `
    <div id=${id}> 
      <p>${app.state.name}</p>
      <p>${app.state.age}</p>  
    </div>
  `;
};

// then register it
app.registerComponent(MyComponent, 'mycomponent1', ['name', 'age'], true)
    
    
    
export class RobJSApp {
  ...

  // The initialRender function then is mainly used to render the app for the first time into the html element with our tagId. 
  // Usually it would do so with the component that wraps all other components and is the main entry point.
  // If this is a component that is not directly using state, then there is no need to pass an id to this function.

  // Further on the first render the updateComponent function (which is explained next) is called on all registered components. 
  // -> basically feeding all components with their inital state values and other arguments.

  initialRender(viewFunction, id=undefined) { 
    document.getElementById(this.tagId).innerHTML = viewFunction(id)
    this.components.forEach(component => this.updateComponent(component))
  }
  ...
}

// usage
app.initialRender(MyComponent, 'mycomponent1') // oftentimes it will be just a wrapper component and no id is required

    
    
    
export class RobJSApp {
  ...

  // The updateComponent function is a key function managing the re-rendering in the DOM. 
  // It finds the component by its id, creates it again temporarily by calling their viewfunction with all arguments and then replaces the old one.


  updateComponent(component){
    const el = document.getElementById(component.id)
    const tempDiv = document.createElement("div")
    const stateArgs = component.usedStateKeys.map(key => this.state[key])
    tempDiv.innerHTML = component.viewFunction(component.id, ...component.args, ...stateArgs)
    const newElement = tempDiv.firstElementChild
    el.replaceWith(newElement)
  }
  ...
}

// usage
// You can use this function everywhere you want by yourself. 
// But mostly it will be called from within the framework in two places: 
// The initialRender function (that we talked about above) and the updateState function (that we talk about next).

    
    
    
export class RobJSApp {
  ...

  // The final function is to manage the automatic update on state changes.
  // We need to call this function whenever we want a re-render to happen.
  // Pass the key or name of the state variable and the new value it should get.
  // Notice that we create an old state by copying the current state before performing the update.
  // Then we filter all the registered components to find the ones that listen to updates of this particular state variable (as defined before in registerComponent()).
  // For all that are found, we call the updateComponent function on them - essentially triggering a re-render

  updateState(key, newValue) {
    this.oldState = this.state
    this.state = { ...this.state, [key]: newValue }
    this.components
      .filter(component => component.usedStateKeys.includes(key))
      .forEach(component => {
        this.updateComponent(component)
      });
  }
  ...
}

// usage
const Counter = (id) => {

  console.log(app.oldState.count) // oldState is available and sometimes quite useful

  return `
    <div id=${id}> 
      <button onclick='app.updateState("count", app.state.count - 1)'>-</button>
      ${app.state.count}
      <button onclick='app.updateState("count", app.state.count + 1)'>+</button>
    </div>
  `;
};

    
    

And that is all there is to it. Roughly 40 lines of code. Easy, elegant and powerful.

© - RobJS.org - All rights reserved