Skip to the content.

Custom createElement

see also CED

The goal is to simplify development and gracefully handle annoying edge cases. AND To use connical names. For example, use ‘tagName’ vs ‘name’. TagName is in the spec, and when you read for the property on the element, it’s the same language. For this to work, after the element is created, we obviously do not want to add that property. So the internals will need to have a list of properties not to add.

Features

I’ve tried several methods and the cleanest is to pass in on object, having argumnets get too weird given all the potential flags

Example: basic element - not a custom element

const myDiv = createElement({
  tagName:'div',
  attributes: {
    class:'large'
  }
 });
 // <div class="large"></div>

Example: same as above but class as an array

const color = 'gray'
const myDiv = createElement({
  tagName:'div',
  attributes: {
    class:['large', color]
  }
 });
 // <div class="large gray"></div>

Example: setting the text of an element with typescript support

Allows setting any valid and ‘secure’ element property. innerHTML is not considered secure, so it cannot be set.

const myDiv = createElement<div>({
  tagName:'div',
  textContent: 'Hello World'
 });
 // <div>Hello World</div>

Example: basic custom element using ‘is’ where custom element extends HTMLDivElement vs. HTMLElement

const myDiv = createElement({
  tagName:'div',
  attributes: {
    is:'my-modal',
    class:'modal large primary',
    show: true
  }
 });
 // <div show="true" class="modal large primary"></div>

Example: custom element with typescript support

import {MyModal} from './myModal.js'
const myDiv = createElement<MyModal>({
  tagName:'div',
  attributes: {
    is:'my-modal',
    class:'modal large primary',
    show: true
  }
 });
 // <div show="true" class="modal large primary"></div>
 console.info(myDiv.show) // does not throw typescript error.

Example: properties -similar to props but do not get bound as attributes like in other frameworks. Custom element inherits HTMLElement - no ‘is’ syntax required. Custom element has a property of user.

import {UserDetails} from './userDetails.js'
const userDetails = createElement<UserDetails>({
  tagName:'user-details',
  properties:{
    user: {
      firstName: 'Jane',
      lastName: 'Doe',
      'user-id': '12345'
    }
  }
 });

 console.info(myDiv.user.userId) // user-id becomes userId
 console.info(myDiv.user.firstName) // returns 'Jane'

Example: defineProperties see MDN for details

Use Cases:

import {UserDetails} from './userDetails.js'
const userDetailsCED = { // CED is component element description
  tagName:'user-details',
  defineProperties:{
    userId:{
      value:'12345',
      writable: false,
      enumerable: true
    }
  }
 });

const router = this.closest('router') // find a parent component router element
router.change('#/user/12345', userDetailsCED)

// the router would have code something like this
export class Router{
  ...
  change(route, ced){
    const component = createElement(ced)
    ... // do work to insert the component
  }
 }

Example: Older example with several extra use cases


// for the unsafe string, instead you should have a tagged template literal that sanitizes and returns a documnetFragment

const unsafeString = `<span>${somethingUnsafe}</span>`
const myDiv = createElement({
  tagName:'div',
  textContent:'I am text',
  innerHtml = unsafeString
  ['foo-bar']:'hello', // adds property to the elemnt as fooBar
  attributes:{
    show:'true',
    off:'false',
    is:'my-fancy-component',
    ['aria-busy']:'true', // or can be a javaScript boolean of true
    class: ['show','green'], // or could be a string
    ['bar-bar']:'i am an attribute', // since attribute the attribute is bar-bar and if flag to add attributes as props, the prop will be camel case
  }
 });
 
 myDiv.textContent // 'I am text'
 myDiv.innerHTML // nothing, it is ignored, it does not escape the html.  For this type of work create a secure tagged template literal function that returns a document fragment then you can just append.
 myDiv.fooBar // hello
 myDiv.getAttribute('aria-busy') // 'true'
 myDiv.hasAttribute('show') // true
 myDiv.hasAttribute('off') // false
 myDiv.getAttribute('is') // 'my-fancy-component'
 

An incomplete example of tagged template literal

import {html} from 'my-project-essentials'; // you will have to make your own. I have one, but can't share it.
const userSuppliedInput = html`<span>${somethingUnsafe}</span>`
myDiv.appendChild(userSuppliedInput);

Update calling a constructor. This has limitations, but it useful.

Update 2023-09-06 - I have recently updated my createElement to also support a calling a constructor with one argument. I’ll post more info later but the quick is

// Notice the constructor accepts an argument. This works as long as you plan to only create the element is JavaScript.
// Normaly custom elements do not accept argumnets in the constructor. But it can be done. Can be a nice alternative to the more traditional methods when passing around complex objects/props/properties. 
class HelloMsg extends HTMLElement{
  constructor(arg){
    super()
    this.msg = arg.msg
  }
  connectedCallback(){
    this.textContent = this.msg
  }
}
customElements.define('hello-msg', HelloMsg)

const myEle = createElement({
  ctor: HelloMsg,
  ctorArg: { msg:'Hello World'}
})

document.body.append(myEle)