Skip to content

Instantly share code, notes, and snippets.

@doeixd
Created January 23, 2024 19:33
Show Gist options
  • Save doeixd/0b4793886ac304b4af72b7766dea8ae6 to your computer and use it in GitHub Desktop.
Save doeixd/0b4793886ac304b4af72b7766dea8ae6 to your computer and use it in GitHub Desktop.
Behavior Elements

Behavior Elements

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.

Simple Counter Example

<script>
  be('counter', _ => {
    onClick = () => {
      this.innerText = +this.innerText + 1
    }

    $Roles['increase'] = _ => {
      onClick() => {
      }
    })
    increase

  })
</script>

<counter-be>
  0 
</counter-be>

More Advanced Counter Example

<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!

Welcome

Magic Vars

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.

Roles

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>

Assign Roles

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>

Vals

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>

Object / Arrays

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?

Batch

Binding

Changed

assignVals

Coersion Serialization Attributes Get Element / Get Store of child element: be-id Forms / Inputs Val changed

Store

Events

be('hello-world', () => {
  attrsObserved = ['name', 'message',]
  $attrObserved = () => []

  or by default, match(attrs.[\w\-_\$]+) then pipe it to static get observedAttributes
}')

Attributes

<script>
be('example', () => {
  attrObserved = [];
  attrObserved = () => []

  attr.hello$changed = () => {

  }
})
</script> 

Event Modifiers with -

Lifecycle & Hydration

Setup

Custom, setup,

be('counter', () => {

}, {
  setup: () => {

  }, keepDefaults: false.
})

Children

be('counter', () => {

});

OnMount

When the custom-element is mounted

Forms

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>

Plugins / Upgrades

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>

Styling

Vals auto bind to css custom properties? Make show/hide components work?

Classses

```html



<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) => {
    
  })


Determine Block / Template based on holes defined in vals. See voby template.

<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>

Nested Behaviors

Like typeclasses, Access ID

IS = "Element"

Usage with ___ Framework

Usage with Nanostores

Routing

<router-be>
  <be val="rout" for="id" bind="path_children" data="/path">


  </be>
</router-be>

How it works

Limitations

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?

Api

Inspiraton

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']
    
    $.
  }
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment