The worst part of using web components is dealing with the Shadow DOM, and templating in general. However when web components are just used to add small bits of interactivity to prerendered markup, they become extremely connivent, portable across frameworks/languages, easy to style, don't require immediate hydration, and do not require a custom server-side rendering step.
Behavior Elements is a simple utility function which aids in the creation of the sort of web components whose primary job is to add small bits of simple interactivity to such prerendered markup. Behavior Elements is best used in websites that are server-driven/multi page, because there is no rendering cycle, diffing, dirty-checking, global hydration, etc. it's best that all/most application state / routing reside with the server.
Behavior Elements aims to strike the balance between being simple, familiar, and easy to work with, yet still providing small additions of functionality/magic that reduces friction in common scenarios. It adds simple functionality for updating values, and managing event listeners.
Behavior Elements is best utilized in applications where one has complete control over the markup, and doesn't require client-side routing, e.g. E-Commerce, marketing, content-based sites, etc. Places where it's feasible for the DOM to ultimately be the source of truth.
Unlike things like htmx, alpine.js, or amp-bind. By default, attributes used by Behavior Elements do not add any interactivity, all interactivity is described in JavaScript, and not through attributes.
Behavior Elements aims to be a flexible foundation to build abstraction on.
<script>
be('counter', _ => {
onClick = () => {
this.innerText = +this.innerText + 1
}
$Roles['increase'] = _ => {
onClick() => {
}
})
increase
})
</script>
<counter-be>
0
</counter-be>
<script>
be('advanced-counter', _ => {
const interval = setInterval(() => {
$Vals['count'] = +Val.count + 1
}, 1500)
$Roles['increment'].onclick = () => {
Vals.count = +Vals.count + +Attrs.countBy
}
$Roles['decrement'] = _ => {
onClick = () => {
Vals.count = +Vals.count - Attrs.countBy
}
}
$On['click']
roles.clear.onclick = () => {
Val.count = attr.initialValue
clearInterval(interval)
}
$Cleanup = () => {
clearInterval(interval)
}
})
</script>
<advanced-counter count-by="2" initial-value="0">
<button $role="decrement"> - </button>
<be val="count">0</be>
<button as="increment"> + </button>
<button $role="advanced-counter.clear"> Clear & Stop </button>
</advanced-counter>
Hello Stranger!
When creating your Behavior Element, the function passed as the second argument to the be
function will "magically" have access to a few useful properties/variables that may aid in the implementation of your components. The use of these magic variables is completely optional, and this argument to Behavior Elements can be used more or less like a standard javascript web component constructor function.
The roles
variable is used to quickly access child elements and apply behavior to them. roles
is a proxy and accessing a property on it, is basically equivalent to doing: component.querySelectorAll([component-name-role="roleName"])
. However, if there is only one element matching, then only that element will be returned for convenience.
if the role is a direct child of the custom element, then the name of the parent custom element is not required
Roles are defined on an element by adding the attribute: whateverYouNamedYourComponent-role="roleNameGoesHere"
to a child element of your component.
Example:
<script>
be('hello-world', () => {
onMouseOver () => roles.subject.innerText = 'Hello World!'
})
</script>
<hello-world">
<div hello-world-role="subject"> Hover Over Me!</div>
</hello-world>
<hello-world>
<div be-role="subject"> Hover Over Me!</div>
<div :as="subject"> Hover Over Me!</div>
</hello-world>
<hello-world id="one">
<div be-role="hello-world#one:subject"> Hover Over Me!</div>
<div :as="hello-world#one:subject"> Hover Over Me!</div>
<div :as="hello-world:subject"> Hover Over Me!</div>
</hello-world>
When setting the value of a property on roles
, one must use a function that will be ran for each instance of the role, and will be passed the element, the index of the element in the list of matching role elements, and the array of role elements. Or optionally, append $all
to the name of the property/role to have the setter function be passed an array of matching elements, instead of just a single element that is called multiple times.
Example:
<script>
be('hello-world', () => {
roles.subject = (element, index) => element.innerText = 'Hello World' + index
roles.subject$all = (elements) => elements.forEach(element => element.classList.add('active'))
$Roles['subject$run'] = () => //funcation ran for each
})
</script>
<hello-world>
<div hello-world-role="subject"> Subject... </div>
<div hello-world-role="subject"> Subject... </div>
<div hello-world-role="subject"> Subject... </div>
</hello-world>
A convenience variable is provided to allow one to easily assign roles. However, if more functionality is required, be reminded that one may directly
<script>
be('game-board', () => {
$AssignRoles({
rows: {
type: 'single', 'many'
selector: 'game-row',
selector: () => document.querySelectorAll('input')
determine_key: () => {},
target: document.
listerToEvents,
isCustomElement,
watchAttributes,
computedProperties: {
name: (target) => target.name
}
assignChildRoles: {
first: {
selector: '
}
}
},
tiles = 'game-tile'
})
$Roles['row']['row_key']
$Roles['row'] = {
onClick() {
$Roles['row']['first'].checked = true
}
}
document.addEventListener('oninput', e => {
let target = context.gameContext.currentTileNumber
$Roles.tiles[target].innerText = e.key.toLowerCase()
$Roles.rows.first
})
// Will create vars for each role? The roles will have access to the parent state? The roles can be instantiated / enrolled as a custom element? Maybe a create role function or upgrade?
rows.zero
})
</script>
<game-board>
<game-row>
<game-tile></game-tile>
</game-row>
</game-board>
The vals
variable represents reactive values/state that is used by the component, but is initially set by the server/DOM in the form of <be val="Val-Name">Val-Content</be>
tags that are children of the component. Or in attributes like: be-val="valueName:value"
It is a proxy object, and when a property is accessed on it, it's basically equivalent to doing: component.querySelector('[val="valueName"]').innerText
or component.querySelector('[be-val^="valueName"]').getAttribute('be-val').split(':')?.[1]
.
Because vals are stored as strings in the DOM, it's important to make sure that any value stored in the val
variable is not sensitive.
Additionally, the type of a val property can be serialized/deserilzed with the $
symbol, followed by the optional type. And if the $
symbol is followed by El
, then the property will return the element that is associated with that value.
If an input element has the be-val
attribute, then it's value will automatically be be bound to a property of the vals
variable that corresponds with the input element's name, or the name provided be be-val
if it is not present.
Example:
<script>
be('counter', () => {
$AssignVals({
letters: {
type: 'class' | 'attribute' | 'element' | 'memory' | 'innerText' | 'innerHTML'
determine_name: () => {},
serialize: ,
deserialize: ,
on_change: () => {},
dependants: [],
determine_value: () => {
},
assignKeys: {
}
}
})
$Val.letters$onChanged['a'] = () => {
}
onClick() {
vals.count = 1 + val.count
Val.double = 2 * val.count
Val.double$El.innerText = val.double
$Vals['double']
Val.counters.count = 'Whats up'
}
})
</script>
<counter-be>
<be val="counter$count" bind=">0</be>
<be :for="counter#id$role" val="counter.first" bind=">1</be>
<be-obj :for="counter#id$role" val="counter">
<be val="first">1</be>
<be val="next"> 2</be>
</be-obj>
<div :val="count:0">0</div>
<input be-val="triple">0</input>
</counter-be>
If the name of the val is preceded by a .
followed by some text, then the value will be located at vals[valObjName][valName]
.
Change detection?
Coersion Serialization Attributes Get Element / Get Store of child element: be-id Forms / Inputs Val changed
be('hello-world', () => {
attrsObserved = ['name', 'message',]
$attrObserved = () => []
or by default, match(attrs.[\w\-_\$]+) then pipe it to static get observedAttributes
}')
<script>
be('example', () => {
attrObserved = [];
attrObserved = () => []
attr.hello$changed = () => {
}
})
</script>
Custom, setup,
be('counter', () => {
}, {
setup: () => {
}, keepDefaults: false.
})
be('counter', () => {
});
When the custom-element is mounted
Uses felte forms under the hood. See their documentation for more.beForm()
<form-be name="name-form">
<form>
<input
name="first-name"
type="text"
default-value="Patrick"
>
Patrick
</input>
<input name="last-name" type="text> </input>
<button type="submit>Submit Form</button>
</form>
</form-be>
<script>
be_form('name-form', () => {
inputs = {
['.class']: {
validates,
validateAsyncOn,
validateAsync,
touched,
change
roles: {},
vals: {},
}
}
inputs.firstName.validate = () => "valid"
inputs.firstName.validateAsyncOn =
})
</script>
The order is important. They will use hooks to insert functionality at certain points. aka add to object for point. when get to that point just run all fns in said object
be('name-form', () => {
beForm();
beNanoStore();
beAnimated();
beLazy() // Manual renders?
beSerialized();
beReactive(); // Adds a onchange function when the magic vals change, with mutation observer.
beRenderable(); // This will make a render function that renders on val change, or attr change, or when onrender event is fired. Auto gets template by holes/magic vals.
beFocusedLoop(); // Adding focus ring functionality
beOnClickOutside();
beBound(); // Bind input elemetns
})
Global Provider
<be-installed >
</be-installed>
<h1> Templating </h1>
be-fragment
<template be-fragment>
<h1 be-class="show"> Hello <be val="world"> </be> </h1>
</template>
# Included Components
Forms / Inputs
Transition
Image
Router
Zag Componenets // Will automatically find roles if markup is a certain template, or manally add roles.
# Context
Context has all the same properties/magic vars as normal components.
All child elements will have an automatic reference to the parent component contexts at: context.name = seroval(data-store)
<context-be name="form-context" data-store={name: "hello"} be-no-store >
<form-be>
<input name="hello" type="input">Hello </input>
</form-be>
</context-be>
<form-be be-context="">
<input name="hello" type="input">Hello </input>
</form-be>
Encapulation? Have child components only wory about internal state? but be controled by partent? Access child element values?
<h2> Show Componenet </h2>
await component
<h2> Animations </h2>
# Typescript
```typescript
interface Be {
roles: {};
}
be('custom-element', _:Be) => {
})
<script>
window.be_elements.example
new Example({
...initialValues for holes, getting the template based on the first instance of element, or the id supplied.
})
</script>
Like typeclasses, Access ID
<router-be>
<be val="rout" for="id" bind="path_children" data="/path">
</be>
</router-be>
Using other functions, higher order construtors, scope issues, dynamic imports, etc...
Create components with initial values/attributes/children. Maybe use templates? Is it possible with a higher order function?
Snuggzi -- For an ergonomic web components library Corset -- For the `assignRoles` functionality inspiration Petite Vue -- For the courage to use non-standard html attributes LitElement -- For the reactive templating Svelte -- For the reactive css classes Felte -- For the form library NanoStores -- For solving state management Voby & amp-fragments -- For automatic determination of templates daisyUI -- For showing me which components to make zagjs -- For the implementation of the components Radix -- The original inspiration of adding behavior with nested roles. Shoelace -- For the roadmap of components<show-be :when="true">
<template :as="true">
<h1> True </h1>
</template>
<template :as="false">
<h1> False </h1>
</template>
<div :as="default">
<h1> False </h1>
</div>
</show-be>
<div show-when="golbalatom | (expression)"> Hello World </div>
<match-be val="true">
<template when="true">
<h1> True </h1>
</template>
<template when="false">
<h1> False </h1>
</template>
<template when="maybe">
<h1> False </h1>
</template>
</match-be>
be('reasons', (beExtended) => { with(beExtended) { $Inputs = { ['.reason']: { touched: () => {
}
}
}
$Roles['reason'].onClick = (e) => {
e.checked
}
$Roles['reason'] = {
onClick() {
$Roles['reason']['input'].checked = true
}
}
} })
be('slider', ($) => {
$.Roles['hello']
$.AssignRoles()
$.On['click']
$.
}
)