5 of 10
added 278 characters in body
Michael
  • 161
  • 2

JavaScript module to render and handle a form to add users

I recently switched to modular JavaScript and really like the idea of having the state of your application in JavaScript and not in the DOM. I want to know if what I am doing is considered best practices or not and how I can avoid re-rendering the entire list after each change.

First, the code:

HTML

<form id="add-user-form">
  <input type="text" placeholder="username" name="username">
  <button>add User</button>
</form>
<hr>
<ul id="user-list"></ul>


<script id="user-template" type="text/html">
  <li data-key="${key}">${name}</li>
</script>

JavaScript

// lets you get templates from HTML ( as you can see in the HTML above ). 
// This is to avoid writing out HTML inside JavaScript
const Template = {
  cached: {},
  get(templateId, placeholders) {
   let templateHtml = ''
   if (!this.cached[templateId]) {
      templateHtml = document.getElementById(templateId).innerHTML.trim()
      this.cached[templateId] = templateHtml
   }
   else {
      templateHtml = this.cached[templateId]
   }
   
   for (key in placeholders) {
     templateHtml = templateHtml.replace(new RegExp("\\${\\s*" + key + "\\s*}", "g"), placeholders[key]);
   }
   return templateHtml
  }
}

const Users = {
  users: [],
  init() {
    this.cacheDom()
    this.bindEvents()
  },
  cacheDom() {
    this.addUserFormEl = document.getElementById('add-user-form')
    this.userListEl = document.getElementById('user-list')
  },
  bindEvents() {
    this.addUserFormEl.addEventListener('submit', this.handleAddUser.bind(this))
  },
  render() {
    // gets the HTML from ALL users of our user list and replaces the current user list
    const userHtml = this.users.map( user => Template.get('user-template', user) ).join('')
    this.userListEl.innerHTML = userHtml
  },
  handleAddUser(e) {
    e.preventDefault()

    const username = e.currentTarget.elements['username'].value
    if (!username) return
    e.currentTarget.elements['username'].value = ''
    
    const key = this.users.length // I know I know this is bad practice, please ignore :)
    
    this.users.push({
      key: key,
      name: username      
    })
    
    this.render()
  }
}

Users.init()

The code has a form with an input field where you can type in a name that, once the form gets submitted, will be added to the user list below. It would probably be a good idea to seperate the userlist and the userform into seperate concerns, but for the sake of this example I kept it simple.

I want to keep DOM modifications to a minimum so I put them inside the method Users.render. This however has one side-effect. Whenever I add an user, it reloads the entire list. It might work with this amount of data, but lets say I have thousands of records, maybe even with input fields. These would all be emptied again.

I could of course just add the new entry to the DOM inside the Users.handleAddUser method but what if later, I also want to delete users. I would have to delete the entry within Users.handleRemoveUser and would so already have two methods that handle DOM modifications for the same thing.

Another thing I'd like to get feedback on is the whole template thing. Is it best practice to store the template inside a script tag with an unknown type to the browser? It is certainly cleaner than creating it in Js in my opinion.

I am also interested in the way I handle the form submit. As you can see I am not getting the input by ID but by form.elements and then referencing the name. Input elements with IDs become global variables and also I can be sure the element is actually inside the form. The thing is just, I have never seen it being done like that.

I know this all can be easily and beautifully achieved with frameworks such as VueJs that use a vDOM but I am looking for a vanillaJs way of handling this. (Please note that the code is just an example. Yes Unit testing and typeScript are missing and I am well aware of the fact that the code will not run in browsers that do not support ES6).

Michael
  • 161
  • 2