Classes
Updated on August 22, 2024Source code
In Baleada Logic, lots of UI logic is implemented with JavaScript classes. Classes are designed to colocate pieces of related state, and provide methods for working with that state.
Unlike Baleada Logic's pipes, which are more like utility functions, each class manages multiple pieces of state that you can observe for changes.
Here's a quick example of how you would construct and use one of the classes to manage some of the state needed for a tablist component:
import { Navigateable } from '@baleada/logic'
const tabs = new Navigateable(['Tab #1', 'Tab #2', 'Tab #3'])
tabs.location // 0
tabs.next()
tabs.location // 1
tabs.item // 'Tab #2'
You can pair Baleada Logic classes with reactivity tools like Vue to perform side effects based on state changes:
import { Navigateable } from '@baleada/logic'
import { shallowReactive, shallowRef, watch } from 'vue'
// Create the Navigateable instance as a reactive Vue object
const tabs = shallowReactive(
new Navigateable(['Tab #1', 'Tab #2', 'Tab #3'])
)
const tabPanels = shallowRef([]) // A ref to the tab panels in the DOM
// Watch the instance's location property for changes.
// When a change is detected, update the DOM to activate
// a new tab.
watch(
() => tabs.location,
() => {
tabPanels.value.forEach(panel => panel.setAttribute('aria-selected', 'false'))
const activeTabPanel = tabPanels.value[tabs.location]
activeTabPanel.setAttribute('aria-selected', 'true')
},
{ flush: 'post' }
)
To learn more, visit the docs for each class exported by Baleada Logic. For a complete list of available classes, see the Classes section under the Logic heading in the left sidebar.
If you'd like to dive deeper in the design rules and principles behind Baleada Logic's classes, keep reading!
API design
For any individual piece of UI logic, there are plenty of ways to implement it, and plenty of packages already published that can npm install
your troubles away.
But implementing these things yourself, or learning the APIs of disparate packages, adds complexity and mental overhead to engineering tasks that are usually several steps removed from the actual business logic of the app or site you're building.
Baleada Logic's classes implement all kinds of UI logic for you, which is nice! But arguably more important is the fact that these classes all share a consistent, predictable design.
In other words, you can construct all classes in the same way, you can customize their behavior in the same way, and you can access their state and methods in the same way.
Baleada Logic's classes implement all kinds of UI logic for you, which is nice! But arguably more important is the fact that these classes all share a consistent, predictable design.
To accomplish that, classes all follow strict rules in these specific areas:
- How they are constructed
- How state and methods are made available to you
- How methods accept arguments
- How classes, their constructor options, their state, and their methods are named
- Why classes provide certain state and methods
- Why constructors accept certain state and options
The rest of this guide explains all the rules that classes follow. The words "all", "always", "any", and "never" are displayed in bold, to emphasize that the rules apply to every single class offered in Baleada Logic.
How to construct classes
You can access the functionality of all classes by constructing new instances of them.
const instance = new ExampleClass(...)
That ...
represents the arguments you'll pass to constructor functions. All class constructors accept two parameters:
- A piece of state
- An
options
object.
The state
parameter is always required, and the options
parameter is always optional. Given these parameters, the constructed instance always takes the form of an object with state and methods.
const instance = new ExampleClass(state[, options])
typeof instance // -> 'object'
The state
parameter is always used to pass a piece of state whose core functionality will be enhanced by the class. The options
parameter is always an object that serves as a catch-all for all optional parameters that affect how a class behaves.
const instance = new ExampleClass(state, {
option_1: true,
option_2: 'baleada',
option_3_: thing => doThe(thing)
})
Class constructors never access the DOM internally. This ensures that you can construct any class in a server environment, or on the client side before the document is ready.
How state and methods are made available to you
Instances of classes take the form of JavaScript Objects, and all state and methods are accessible through the properties of those objects.
const instance = new ExampleClass(state)
instance.exampleState // Access state through properties
instance.exampleMethod() // Access methods through properties
Class instances' methods always return the instance itself. The main benefit of this is that you can use method chaining if needed.
const instance = new ExampleClass(state)
instance.exampleMethod() // -> returns instance
instance
.exampleMethod()
.anotherMethod()
.yetAnotherMethod() // -> Works 👍 and returns instance
Class instances always store their constructors' state
in a public getter named after the state's type (e.g. string
, array
, keyframes
, etc.).
Class instances always have a public method you can use to set a new value for that public getter. The method follows a naming convention of set<PropertyName>
(e.g. setString
, setArray
, setKeyframes
, etc.).
The set<PropertyName>
methods have two benefits:
- Since they return the instance itself, you can method chain after updating your state
- Internally, they perform any and all side effects and validation that should happen before or after updating state, so that you don't have to be concerned with those things.
// The Pickable class's constructor accepts an Array
const instance = new Pickable(['Baleada', 'Logic', 'Composition', 'Icons'])
instance.array // -> ['Baleada', 'Logic', 'Composition', 'Icons']
// Update the Pickable instance's array and other state,
// and return the instance:
instance.setArray(['tortilla', 'beans', 'egg', 'avocado'])
instance.array // -> ['tortilla', 'beans', 'egg', 'avocado']
If you don't need to method chain after updated your state, but you do want to feel confident that side effects are performed correctly, you can assign a new value directly to the getter property. These getter properties also have their own setters that pass your new value the set<PropertyName>
method, ensuring that side effects and validation are performed.
const instance = new Pickable(['Vue', 'React', 'Svelte'])
// Internally, the setter that updates `array` also
// performs the side effect of updating the `searcher` property
instance.array = ['tortilla', 'beans', 'egg', 'avocado']
instance.array // -> ['tortilla', 'beans', 'egg', 'avocado']
instance.items // -> Updated based on the new array
Some classes, particularly those that were designed to capture text input from your end users, create additional public getters that you'll frequently need to update.
Those public getters follow all of the same rules:
- They each have a public method you can use to assign a new value to the property and perform necessary side effects and validation
- Those methods follow the same
set<PropertyName>
naming convention - If you assign a value to the property directly, its setter will still perform necessary side effects
// The Completeable class's constructor accepts a String
// It's `selection` property is used to get and set selection ranges
const instance = new Completeable('Baleada')
instance.string // -> 'Baleada'
instance.selection // -> { start: 6, end: 6 }
instance.setString('tortilla') // --> returns instance
// OR
instance.string = 'tortilla' // Works just fine 👍
instance.setSelection({ start: 0, end: instance.string.length }) // --> returns instance
// OR
instance.selection = { start: 0, end: instance.string.length } // Works just fine 👍
All class instances also have one or more non-editable public getters. These getter properties share the following important characteristics:
- They allow you to access state that is useful for building certain UI features, but is not part of the core functionality or benefit of the class.
- All updates to the getter properties are considered side effects of other public methods.
- You'll never find a situation where it would make sense for you to edit the property directly. You'll always rely on the instance to manage the getter properties' values, updating them after other methods are called.
- When applicable, getter functions' behavior can be customized using properties in the
options
object that gets passed to the class constructor.
// The Animateable class's constructor accepts an array of keyframes
const instance = new Animateable(myKeyframes, myOptions)
instance.play() // Plays the animation (and returns the instance)
instance.progress.time // -> A number between 0 and 1 indicating the time progress of the animation. Updated at 60fps.
instance.progress.time = 3 // Doesn't work (and shouldn't work!)
Some classes have side effects in the DOM that need to be cleaned up in order to avoid memory leaks. All of these classes have a public stop
method that you can use to clean up.
// Listenable can be used to listen to DOM events, media queries, Observer entries, and window idle periods.
const instance = new Listenable(myEventType)
instance.listen(myCallback) // Adds event listeners, connects observers, etc.
instance.stop() // Removes all listeners, disconnects all observers, etc.
What arguments methods accept
Some methods don't accept any parameters, but the ones that do accept parameters always follow consistent rules, based on how many parameters are required or optional:
Why classes provide certain state and methods
Baleada Logic follows a consistent process for determining which state and methods are provided by classes:
- Identify one action or several related actions that would be useful in a user interface. These actions become methods on the class.
- Identify the piece(s) of state that the action will be performed on. In other words, answer the question, "If this method were a standalone function, what would be a required argument?" Each answer to that question becomes a public, writeable property on the class.
- Identify the piece(s) of state that would be useful for building certain UI features with the class' methods and public properties, but shouldn't be considered part of the core functionality or benefit of the class. Each identified piece of state becomes a getter on the class.
Why constructors accept certain state types and options
Constructors' state types
Baleada Logic uses a sentence template to decide what state type (e.g. String, Array, HTMLElement, etc.) should be accepted by a constructor:
A `<state type>` can be `<action>`ed (by `<action arguments>`).
For example, the Completeable
class' core action is to help you autocomplete strings. The Completeable
constructor's state
parameter is a string, and the class has a complete
method to update the string with a completed version of it. This fits into the sentence template nicely:
A **string** can be **completed** by a **completion**.
Some classes have core actions that don't take arguments—in those cases, the last part of the sentence template is omitted. Take the Delayable
class for example:
A **function** can be **delayed**.
And some classes have core actions that are actually private methods on the class, with more specific public methods that call the core private method under the hood. The Animateable
class is a great example—its constructor accepts an array of keyframes, and in order to animate those keyframes, it internally calls a private animate
method when you call one of its more specific public methods: play
, reverse
, seek
, or restart
.
These types of classes still use that core action in their sentence template, even though it's accessed via a private method that you'll never directly use:
**Keyframes** (Array) can be **animated**.
This sentence template helps ensure that all classes' methods are affordances. In other words, methods tell you what you can do with a given type of state, rather than what that type of state can do to itself or other things.
In the "API design compliance" section in the documentation for each individual class, you can find the filled-out sentence template, explaining why that class accepts its specific state type.
Constructors' options
Constructors only accept options that:
- Initialize public properties
- Customize the behavior of getter functions
Constructors never accept options that customize the behavior of public methods. Those kinds of options are always passed to the method itself as an optional parameter.
Naming conventions
Classes are named after their core action, suffixed with able
. Class names are proper-cased.
Here are a few examples:
Pickable
Listenable
Navigateable
Copyable
Animateable
Note that in correct English grammar, the -able
form of a word is not always this simple. There are a number of ways the grammar can be more complex:
- Often (but not always), when a word ends in
e
, thee
is omitted before addingable
- Words that end in
y
usually change they
to ani
before addingable
- Some words omit several letters from the end of the word before adding
able
Instead of relying on you to know all these rules of English grammar, Baleada Logic simply breaks them in favor of consistency and predictability. In Baleada Logic classes, the name is always just the core action followed by able
—no strange word modification, no guessing about whether or not the e
is excluded before able
, no replacing y
with i
, etc.
In conclusion, English grammar is annoying, so Baleada Logic ignores it and names everything using the <core action>able
convention.
Proper English grammar is annoying. Baleada Logic's naming convention breaks its rules in favor of simplicity, consistency, and predictability.