Skip to content

Write reactive UI components using render functions.

License

Notifications You must be signed in to change notification settings

dolanske/cascade

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

95 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

cascade

I swear this is the last DOM library I make (for now)

Create simple, reusable and reactive UI components using render functions and add more complex functionality through method chaining. These methods can be mounted anywhere in the DOM, static applications, with added reactivity only where needed.

npm i @dolanske/cascade

Concept

Create UI components by calling a component function. All supported HTML elements have their own factory function.

  • Provide children when calling the component
  • Chain function to extend the functionality
const button = button('Click me').on('click', () => console.log('I got clicked!!'))

Many function allow you to pass a ref or a getter function. These allow you to reactively update the UI. To familiarize yourself with these concepts, read the Vue documentation on this topic.

Components

There are two ways of creating components. The instanceless and reactive components.

  • Instanceless components are basic UI. Think of it as scaffolding. For instance <div class="wrapper"></div> does not hold any state, it's there to provided a container with some styling, but that's where its journey ends
  • Reusable the meat of your application. These components for instance provide interactivity and/or fetch data. Based on their state, we want to update the UI.

It is heavily discouraged to reuse instanceless components multiple times. Every single component has an instance.

const Container = div().className('container')

// In component A
Container.nest(h1('Hello'))
// In component B
Container.nest(h2('World'))
// Both components will have <h2>World</h2> as the `Container` component has just one instance.

To create a reusable component, you need to either use the reusable function. This will create a unique component instance each time it is used.

const Container = reusable('div', (ctx, props) => {
  // Create a component which will wrap the provided child nodes in 3 divs
  ctx.nest(
    h1(props.title),
    div(ctx.children).class('wrapper')
  )
})

// Later used in a component
const app = App(
  Container(
    span('Subtitle')
  ).prop('title', 'Hello world')
)

app.mount('#app')

Note

API docs are work in progress

API

Creating a component returns a component instance. This instance contains a few useful properties and a lot of methods.

Instance

const ctx = div(span('hi'))

// Unique ID of the component
ctx.identifier
// Reference to the mounted DOM node
ctx.el
// Child component instances
ctx.componentChildren
// Stores child components which were passed during component initialization.
ctx.children
// Reference to the parent component, if there's one
ctx.parent

Content

Type definition. Most functions allow the usage of the following type. Using ref or getter function makes the UI reactive. All the examples below assume you're writing code inside the setup() function.

type MaybeRefOrGetter<T> = T | Ref<T> | () => T

.text()

Sets the textContent of the Component's DOM node

ctx.text(value: MaybeRefOrGetter<Primitive>)

Example

ctx.text('Hello world')
// Will update text each time `name` ref changes
ctx.text(() => `Hello ${name.value}`)

.html()

Sets the innerHTML of the Component's DOM node

ctx.html(value: MaybeRefOrGetter<Primitive>)

Example

ctx.html('Hello <b>world</b>')
ctx.html(() => SVGIcon.value)

.nest()

Allows nesting of components and HTML elements.

// NOTE: it is planned to allow refs and getter functions, but that does not work yet.
type ComponentChildren = string | number | Component | Element | Fragment
ctx.nest(...value: ComponentChildren | ComponentChildren[])

Example

ctx.nest(
  h1('Hi'),
  'Hmm',
  document.createElement('input'),
  ctx.children
)

.for()

Iterate over the provided object / array / number and execute the provided callback for each item. Components returned from the callback are then rendered.

It is recommended not to use other chained methods when using for, because the base element is replaced with the return value of the callback function. All logic should therefore be handled there.

export type Source = any[] | number | object

export type CallbackType<T> =
  T extends any[]
    ? (value: T[number], index: number) => ComponentChildrenItems
    : T extends object
      ? (value: keyof T, key: string, index: number) => ComponentChildrenItems
      : (index: number) => ComponentChildrenItems

ctx.for(
  source: MaybeRefOrGetter<Source>,
  callback: CallbackType<UnwrapRef<Source>>,
)

Example

ctx.for(['One', 'Two', 'Three'], (item, index) => {
  return li(`${index + 1} ${item}`)
})

Events

Register event listeners.

.on()

Bind an event listener to the underlying HTML node. The event listener is removed when the component is destryoyed/unmounted. Event modifiers are planned and will be added later.

ctx.on(type: keyof HTMLElementEventMap, listener: ListenerFn, options?: Options)
ctx.on('click', (e) => {
  e.stopPropagation()
  clicked.value = true
})

Event shorthands

A few event definition shorthands are available to make development faster

ctx.click(listener: ListenerFn, options?: Options)
ctx.submit(listener: ListenerFn, options?: Options)
ctx.focus(listener: ListenerFn, options?: Options)
ctx.blur(listener: ListenerFn, options?: Options)
ctx.change(listener: ListenerFn, options?: Options)
ctx.input(listener: ListenerFn, options?: Options)

Keyboard events

Detect keyboard presses on the component.

ctx.keydown(listener: ListenerFn, options?: Options)
ctx.keyup(listener: ListenerFn, options?: Options)
ctx.keypress(listener: ListenerFn, options?: Options)

You can also listen for a specific key combination using the exact suffix. The options object also receives a new property called detect which can be set to every or some. The default is every and it controls wether the function detects keys in the exact order, or any of the provided keys.

ctx.keydownExact(requiredKeyOrKeys: string | string[], listener: ListenerFn, options?: Options)
ctx.keyupExact(requiredKeyOrKeys: string | string[], listener: ListenerFn, options?: Options)
ctx.keypressExact(requiredKeyOrKeys: string | string[], listener: ListenerFn, options?: Options)

Example

ctx.keypressExact(['Shift', 'A'], () => {
  // Fired when SHIFT and A are pressed in succession
})

ctx.keypressExact(['A', 'B', 'C'], () => {
  // Fired whenever A, B or C are pressed
}, { detect: 'some' })

.model()

Two way binding to control and element's value with ref. You can use mode()l on input, select, textarea and details.

// A function which transforms the value of the element before it's assigned to the provided ref
type ModelTransform<Returns = string> = (value: string) => Returns

interface ModelOptions {
  lazy?: boolean
  transforms?: ModelTransform[]
  eventOptions?: EventListenerOptions
}

ctx.model(value: Ref<Primitive | Primitive[]>, options: ModelOptions)

The implementation follows the basic usage of Vue's v-model implementation.

Additionally, you can control wether the <details> element is open using model by providing it a Ref<Boolean>.


Attributes

Reactively bind attributes to the underlying HTML element.

.class()

Bind static or reactive class / class object.

type ClassObject = Record<string, MaybeRefOrGetter<boolean>>
type ClassnameOrCLassObject = string | ClassObject

ctx.class(classNames?: ClassnameOrCLassObject, value?: MaybeRefOrGetter<boolean>)

Example

// Single ref class
const largeText = ref(false)
ctx.class('text-xl', largeText)

// Object, which can contain both static and refs/getter functions
ctx.class({
  'will-never-show': false,
  'could-show': () => maybeShow.value && shouldShow.value
})

.style()

Add static or reactive inline styles.

ctx.style(key: keyof CSSStyle | CSSStyle | MaybeRefOrGetter<CSSStyle>, value?: MaybeRefOrGetter<LimitedPrimitive>)

Example

// Single reactive property using getter function
ctx.style('display', () => show.value ? 'block' : 'none')
// Getter function returning a style object
ctx.style(() => ({
  display: show.value,
  width: `${width.value}px`
}))
// Static style object
ctx.style({
  position: 'relative',
  left: '10px'
})

.attr() & .attrs()

Bind static or reactive attributes.

ctx.attr(key: string, value?: MaybeRefOrGetter<Primitive>)
ctx.attrs(data: MaybeRefOrGetter<Record<string, Primitive>>)

Example

// Single attribute
const dynamicName = ref('element-name')
ctx.attr('name', dynamicName)

// Attribute object
ctx.attrs({
  disabled: true,
  inert: true,
})

Attribute shorthands

ctx.id(value: MaybeRefOrGetter<Primitive>) // id attribute
ctx.disabled(value: MaybeRefOrGetter<boolean>) // disabled attribute

Conditional rendering

Used when we want to display / hide certain components based on a condition.

.if()

Conditionally add / remove elements from the DOM.

ctx.if(condition: MaybeRefOrGetter)

Example

// Inside .setup(() => {})
const display = ref(true)
ctx.nest(
  button('Toggle').click(() => display.value = !display.value),
  span('Now you see me').if(display)
)

.show()

Works just like if but leaves the component in the DOM, but appends display: none if false.

ctx.show(condition: MaybeRefOrGetter)

Lifecycle

Hooks which can execute code at different stages of component's life cycle.

.onInit()

Executes provided callback function when the component is initialized. Before being mounted in the DOM.

ctx.oninit(callback: () => void)

.onMount()

Fires the provided callback when the Component is mounted to the DOM.

ctx.onMount(callback: () => void)

.onDestroy()

Fires the provided callback when the Component is removed from the DOM.

ctx.onDestroy(callback: () => void)

Utilities

This set of utilities might be more useful for people who want to extend the functionality or build a library with Cascade.

// Mounts the selected Component to the DOM
ctx.mount(selector: string)
// Destroys the component instance and removes it from the DOM
ctx.destroy()
// Copies the current instance and returns a fresh copy. This component instance is not mounted in the DOM and should be used as a child component elsewhere.
const clonedEl = ctx.clone()

Components

Cascade divides all of its components into two groups

  • normal
  • void (can't have children)

There are a few custom components which extend the base functionality

Image

const image = img("https://i.imgur.com/naMnD3H.jpeg")
// Has extra two custom attributes
image.alt(alt: string)
image.src(src: string)

Input & Textarea

// The first argument is the input type
const text = input('text')
// Extra attributes
text.type(inputType: string)
text.value(value: MaybeRefOrGetter<Primitive>)
text.placeholder(value: MaybeRefOrGetter<string | undefined>)
text.name(value: MaybeRefOrGetter<string | undefined>)
text.required(value: MaybeRefOrGetter<boolean>)
// No arguments provided during initialization. Otherwise shares all the extra props as `input`.
const textarea = textarea()

Option

Used within the select component. You always Type definition

option(label?: string, value?: MaybeRefOrGetter<Primitive>)
  .value(inputValue: MaybeRefOrGetter<Primitive>)
  .selected()

Example

const selected = ref()
const select = select(
  option('John', 24).selected(),
  option('Andrew', 81),
  option('HOnza', 111)
).model()
// ref's value will be 24 as an option was preselected

About

Write reactive UI components using render functions.

Topics

Resources

License

Stars

Watchers

Forks

Languages