Web Components Guide
Beta

Classes

Modern JavaScript includes classes which allow you to concisely define a blueprint for creating objects that include their own API. Classes are really useful as they can maintain their own private state, have their own private subroutines, and also expose public state and subroutines for other code to call.

Classes are important for Web Components, because most of the time you'll want to use a class to define their logic.

Here's a quick tour of how classes work, but you can read much more about classes and how they work on MDN.

Defining a class

Classes can be defined using the class keyword and providing a name. You can also define a class using a class expression, as long as it's passed to a function or assigned to a variable.

class Point {}

const Point = class {}

console.log(class {})

A class can then be instantiated with the new keyword, and can be checked with instanceof:

// creates a new `Point` object
const mypoint = new Point()

// Check `mypoint` is an `instanceof` `Point`
console.assert(mypoint instanceof Point)

Defining a public API

Classes can have public properties, and methods to give them features which other code can use. They can also have well known methods that get automatically called during certain events:

class Point {
  // Public "Fields"
  x = 0
  y = 0

  // This is called whenever `new Point()` is called
  constructor(x, y) {
    // A class instance refers to itself with `this`.
    this.x = x
    this.y = y
  }

  // Methods
  translate(deltaX, deltaY) {
    this.x += deltaX
    this.y += deltaY
  }

  equals(otherPoint) {
    return this.x === otherPoint.x && this.y === otherPoint.y
  }

  // This is a "well known" method, called whenever
  // a `Point` is converted to a `String`
  toString() {
    return `(${x}, ${y})`
  }
}

const mypoint = new Point(1, 1)
mypoint.translate(5, 5)

console.assert(mypoint.equals(new Point(6, 6)))

// String(mypoint) is the equivalent of mypoint.toString()
console.assert(String(mypoint) === "(6, 6)")

Public fields are initialized every time the class is. If you're familiar with JavaScript classes but haven't used Class Fields before, then you can imagine them as lines of code that would execute in the class constructor. The following two classes are functionally equivalent:

class PublicFields {
  x = 0
  y = 0
}

class ConstructorFields {
  constructor() {
    this.x = 0
    this.y = 0
  }
}

This is important because these fields don't just have to have primitive values in them. You could call a function, or even refer to this within a public field, and it will be executed whenever the class is constructed:

class RandomField {
  x = Math.random()
}
const first = new RandomField()
const second = new RandomField()

console.assert(first.x !== second.x)

One final thing to think about public fields: a class will not know when they change. If knowing when a public field changes is important, then you might want to read on to see how to combine private fields with derived state.

Defining derived state

Classes can also use get <name>() or set <name>() to derive new state, using meta properties. These methods act like properties but you can add custom logic to them, defining new properties that use existing class data to generate a value. If you define a get <name> without a set <name>, then the property will be read only.

class Point {
  x = 0
  y = 0

  constructor(x, y) {
    this.x = x
    this.y = y
  }

  get isEmpty() {
    return this.x === 0 && this.y === 0
  }
}

const mypoint = new Point(0, 0)

console.assert(mypoint.isEmpty)

// This will throw an Error
mypoint.isEmpty = false

Defining a private API

Classes can also have "Private" state and private methods, which cannot be called outside of the class.

class CaseChange {
  #original = ""
  constructor(original) {
    this.#original = original
  }

  upper() {
    return this.#original.toUpperCase()
  }

  lower() {
    return this.#original.toLowerCase()
  }
}

const myupper = new CaseChange("Hello World")

console.assert(myupper.upper() === "HELLO WORLD")
console.assert(myupper.lower() === "hello world")

// This will throw an error
// private fields can't be used outside a class
console.log(myupper.#original)

// This will also throw an error
myupper.#original = "Hello"

Private fields work like public fields with regards to evaluation. They get evaluated with the class instantiation, so this will refer to the class instance, and function calls will be called each time the class is constructed.

Read only fields using private state & public APIs

Private fields are useful to have a value that the internals of a class can change, but that outside code cannot. It's also often useful to allow outside code to read a classes private state, but not change it. To do this you can combine private state with public getters:

class Sentence {
  #original = ""
  constructor(original) {
    this.#original = original
  }

  get sentence() {
    return this.#original
  }

  get firstWord() {
    return this.#original.split(" ").at(0)
  }
}

const mysentence = new Sentence("Hello World.")

console.assert(mysentence.sentence === "Hello World.")
console.assert(mysentence.firstWord === "Hello")

// This will throw an error, accessors that only define
// a get method cannot be set.
myupper.sentence = "Hello Universe"

// This will also throw an error
myupper.firstWord = "Hola"

Reactive fields using private state & public APIs

A public field can be changed on a class instance, and the class will not know when that happens. To make a class react to a change in its public fields, you can combine a private field with get and set functions, like so:

class Timer {
  #startTime = Date.now()

  get startTime() {
    return this.#startTime
  }

  set startTime(newTime) {
    this.#startTime = newTime
    this.resetTimer()
  }

  resetTimer() {
    console.log("Timer has been reset")
  }
}

const mytimer = new Timer()

console.assert(mytimer.startTime === Date.now())

mytimer.startTime = 0

// Will log "Timer has been reset"

This pattern can also be useful for validating values when they are being set.

Extending a class

Classes can extend from other classes. The extended class will receive all the parent classes methods and properties, and it can define new methods and properties. The extended class cannot access private state or private methods from the parent class. If an extended class overrides the class constructor, it must call the original constructor by using super(). If the extended class overrides a method that exists on the parent class, it can optionally call super.<method>().

class Point {
  constructor(x, y) {
    this.x = 0
    this.y = 0
  }

  equals(otherPoint) {
    return this.x === otherPoint.x && this.y === otherPoint.y
  }
}

class Point3D extends Point {
  constructor(x, y, z) {
    super(x, y)
    this.z = z
  }

  equals(otherPoint) {
    return super.equals(otherPoint) && this.z === otherPoint.z
  }
}

You can read much more about classes and how they work on MDN.