npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

als-elementor

v0.5.1

Published

Build custom DOM elements on backend(ssr) and frontend and manage their state

Downloads

9

Readme

Als-Elementor (Beta)

The Elementor class provides a simple and efficient way to bind data and methods to HTML elements, making it easier to manage and update the DOM.

Support

The provided code uses ES6 classes and other modern JavaScript features. Most modern browsers support these features, but you might face compatibility issues with older browsers, especially Internet Explorer. Here's the browser support summary:

  • Google Chrome: 49 and later (Released in March 2016)
  • Mozilla Firefox: 45 and later (Released in March 2016)
  • Safari: 10 and later (Released in September 2016)
  • Microsoft Edge: 12 and later (Released in July 2015)
  • Samsung Internet Browser 4: Released in February 2016 (for Android)
  • Opera Mobile 36: Released in March 2016 (for Android)
  • Internet Explorer: Not supported (due to ES6 classes, arrow functions, and other modern JavaScript features)

Setup

Installation:

npm i als-elementor

Add to your document Elementor script:

<script src="/node_modules/als-elementor/elementor.js"></script>

Brief summary

<script>
  let options={
    levelUpdateGap:2 // depth in map hierarchy for searching elements for update
  }
  let methods = {} // methods for component, actions, or just functions
  let props = {} // variables for updating the state
  let $ = new Elementor(methods,props,options)
  $.observe() // listening for html changes and disconnecting after first change
  $.outerHTML(html) // building outerHTML with states - for backend's use
  $.qs(selector,parent=document) // querySelector for parent. Return first element or null
  $.qsa(selector,parent=document) // querySelectorAll for parent. Return array of elements (not collection)
  
  $.props // object which includes all props
  $.$props // proxy getters and setters which updating all elements with this prop
  
  $.methods // object which includes all methods
  $.$methods // proxy getters and setters which updating all elements with method="propName"
  
  $.loaded // false on constructor and turn true the DomContentLoaded
  $.front // true in browser and false in node.js
</script>
<div 
  component="map.to.method" 
  state="map.to.prop"

  onconnect="map.to.method" 
  ondisconnect="map.to.method"
  onupdate="map.to.method"
  onload="this.some = Date.now()"

  api="link to json api if prop=null"
  loader="map.to.method" 
></div>

constructor

constructor has 3 parameters: methods,props,options.

methods

Methods are functions that you define to manipulate or display data. They can be used as event handlers, to update the state, or simply to render content.

In addition to custom methods, Elementor supports several built-in lifecycle hooks, such as onconnect, ondisconnect, and onupdate, which are called at different stages of an element's lifecycle and can use methods.

const methods = {
  exampleMethod: function () {},
  anotherMethod: function () {},
  some:{
    exampleMethod:function(){}
  }
};

props

Props are variables that represent the state of your application. They can be used to store data and trigger updates when their values change.

const props = {
  exampleProp: 'initial value',
  anotherProp: { key: 'value' },
  some:null
};

Options

So far the options has only one parameter levelUpdateGap.

levelUpdateGap

The levelUpdateGap option determines the number of levels deep in the object hierarchy that the Elementor instance will search for changes when updating the elements. This can be useful for performance optimization, as it allows you to control the depth of the search and avoid unnecessary updates.

By setting a higher value, you may reduce the number of updates performed, but it may also result in missed updates if the changes occur deeper in the object hierarchy. Conversely, a lower value will ensure that changes are detected more accurately, but may result in more frequent updates.

The default value for levelUpdateGap is 2.

let options = {
  levelUpdateGap: 3
};

let $ = new Elementor(methods, props, options);

In this example, the Elementor instance will search up to three levels deep in the object hierarchy when updating elements. This can help balance performance and accuracy when working with large, nested objects.

observe method

The observe method is responsible for observing the DOM for changes and managing the addition and removal of elements. It listens for mutations in the specified parent element (defaults to the html element) and its descendants. When elements with the component attribute are added or removed from the DOM, the observe method triggers the appropriate lifecycle hooks and updates the elements accordingly.

$.observe(parent=this.html, ancestors=[]);

observe method has only one cycle. Therefore it disconnect immediately after the first changes ara occured. If you manualy updating DOM tree, you need to run observe method before those changes, for taking effect.

example:

const div = document.createElement('div');
div.setAttribute('component', 'user');
div.setAttribute('state', 'users.0');
$.observe() // running observe method before changing DOM
body.appendChild(div);

map concept

There are several attributes which using map concept for getting the properties and methods.

The attributes are: component,state,onconnect,ondisconnect,onupdate,loader.

The map concept creates array of keys separated by dot(.) for getting property inside $.$props or method inside $.$methods.

For example users.2.addresses.0.city is a road map for $.$props.users[2].addresses[0].city.

Example:

<div 
  component="users.user"
  state="users.2.name"
  onconnect="users.lastUser"
></div>

On example above, there are:

  • component method from $.$methods.users.user
  • state data for state from $.$props.users[2].name
  • onconnect hook method from $.$methods.users.lastUser

$props

Elementor instance has $props property, which is proxy object with proxy objects for props (parameter on constructor). By changing each value inside $props, you calling for callback which updating componented elements with state which starts with limited map (limited by levelUpdateGap), or running actions (further). In other words, by changing some value inside $props, you changing state for all affected componented elements.

Example:

$.$props.users[2].name = 'New name' // Updating all componented elements with prop="users.2"
delete $.$props.user // deleting all elements with this prop^="user"

Element's state

Each element has it state, which is getter and setter for proxy object inside $props by prop's map. In other words, you can get or set the element's state (and other affected by this change elements) by getting or setting element's state.

Example:

$.qs('[state="user.2"]').state = 'dfgdfg' // updating all elements prop^="user.2"

But despite the the setting through $.$props, setting through state, can't delete the state. Only set.

The folowing code will not update componented elements.

delete $.qs('[state="user.2"]').state // will not work

$methods

Elementor instance has $methods property, which is proxy object with proxy objects for methods (parameter on constructor). By changing each value inside $methods, you calling for callback which updating componented elements with component attribute's value starts with limited map (limited by levelUpdateGap).

You can add, remove, or update methods inside $methods.

Hooks

Elementor supports several hooks that are called at different stages of an element's lifecycle. These hooks allow you to execute custom logic when an element is connected to the DOM, updated, or disconnected from the DOM.

  • onload: Called before changing element's innerHTML and has to contain JS code
  • onconnect(): Called when an element is connected to the DOM and contains map to method.
  • onupdate(): Called when an observed property is updated, but element not replaced and contains map to method.
  • ondisconnect(): Called when an element is disconnected from the DOM and contains map to method.

If element has another componented elements, hooks called by reversed hirarchy. First child elements and then parents.

Actions

On $props new value, if propMap not binded and there is method with same map, it will called.

let methods = {
   test:{
      some:function({map,action,args,result,error}) {
         console.log('hello from some',result)
      }
   }
 };
 
const props = {
   test:{some:'hello'}
};

let $ = new Elementor(methods,props,options)
$.$props.test.some = 'new value' // test.some method will be called

API and Loader

Elementor allows you to fetch data from an API and display a loader while the data is being fetched. To do this, you'll need to use the api and loader attributes in your HTML.

API

The api attribute specifies the API endpoint from which data should be fetched. When an element with an api attribute is connected to the DOM, Elementor will fetch data from the specified endpoint and set it as the value of the property specified by the prop attribute.

! element's prop value has to be null, otherwise api will not work. After fetching, prop's value will set to fetched value

Here's an example of how to use the api attribute:

<div component="users" state="users" api="https://example.com/api/users"></div>

In this example, Elementor will fetch data from https://example.com/api/users and set it as the value of the users property.

Loader

The loader attribute specifies a map to method that should be used to render a loader while the data is being fetched. The loader method should return a string containing the HTML to be displayed as the loader. By default loader is () => 'loading...'

Example:

const methods = {
  users: function () { /*...*/ },
  user: function () { /*...*/ },
  loader: function () {
    return '<div class="loader">Loading...</div>';
  },
  // Additional methods
};
<div method="users" state="users" api="https://example.com/api/users" loader="loader"></div>

In this example, while the data is being fetched from https://example.com/api/users, the loader method will be called, and the returned HTML will be displayed. Once the data is fetched and the state is updated, the loader will be replaced with the content generated by the users method.

Here's a complete example:

const methods = {
  users: function () {
    if (this.state == null) return '';
    // Generate the users list HTML
  },
  user: function () {
    // Generate the user details HTML
  },
  loader: function () {
    return '<div class="loader">Loading...</div>';
  },
};

const props = {
  users: null,
};

const $ = new Elementor(methods, props);
$.observe();

document.querySelector('body').insertAdjacentHTML('beforeend', `
  <div component="users" state="users" api="https://example.com/api/users" loader="loader"></div>
`);

When the element is connected to the DOM, the loader method is called and displays the "Loading..." message. After the data is fetched and the users property is updated, the loader is replaced with the content generated by the users method.

Remember to handle potential errors during the API call, such as network errors or incorrect API responses. You can do this by implementing error handling within the onconnect hook, for example.

querySelector

$.qs(selector,parent=document) // querySelector for parent. Return first element or null
$.qsa(selector,parent=document) // querySelectorAll for parent. Return array of elements (not collection)

outerHTML

The outerHTML method in the Elementor class is designed for server-side rendering of components, and its primary goal is to build an HTML string from the state values. It allows you to generate the initial HTML content on the server-side and send it to the client for faster rendering.

Note: The outerHTML method does not execute hooks (such as onconnect, ondisconnect, onupdate, etc.) and does not make use of the api and loader attributes, even if they are provided in the component definition.

const inputHTML = `<div component="exampleComponent"></div>`;
const htmlString = elementorInstance.outerHTML(inputHTML);

Examples

Simple example

const methods = {
   exampleComponent() {
      return /*html*/`
       <h1>Hello, Elementor!</h1>
       <p>This is an example component rendered using Elementor.</p>
       <div component="listComponent" state="items"></div>
      `;
   },
   counterComponent() {
      if ($.props.count == undefined) $.$props.count = 0
      this.setAttribute('state', 'count')

      return /*html*/`
       <h2>Counter</h2>
       <button onclick="$.$props.count--">-</button>
       <span>${this.state}</span>
       <button onclick="$.$props.count++">+</button>
     `;
   },
   listComponent() {
      return /*html*/`
       <h2>List</h2>
       <div component="counterComponent"></div>
       <ul>
         ${this.state.map((item) => `<li>${item}</li>`).join('')}
       </ul>
     `;
   },
};

const props = {
   items: ['Item 1', 'Item 2', 'Item 3'],
};
const $ = new Elementor(methods, props);

$.observe();

On example above, counterComponent setting state inside the component's function by adding state attribute and new prop.

Weather app example

Add your api key and just copy paste it to your html code with Elementor script.

// Define methods
const methods = {
   searchForm: function () {
      let lastCity = localStorage.getItem('lastCity')
      if ($.props.weather === null && lastCity !== 'null' && lastCity !== null) searchCity(lastCity)
      else lastCity = ''
      return /*html*/`
      <label>
         <div class="tcenter"><b>Choose city</b></div>
         <input list="cities" value="${lastCity}"
            onclick="if($.props.cities.length == 0) $.$props.cities = null"
            placeholder="Enter city name" onchange="searchCity(this.value)">
      </label>
      `
   },
   loadingCities: function () {
      return /*html*/`<div>
      <div class="spinner"></div>
         Loading cities...
      </div>`
   },

   loading: function () {
      let msg = this.getAttribute('message')
      return this.state ? /*html*/`<div class="loader">
       <div class="spinner"></div>
       ${msg || ''}
      </div>` : ''
   },

   datalist: function () {
      return /*html*/`
      <datalist id="cities">
         ${this.state.map((city) =>
            /*html*/`<option value="${city}">${city}</option>`)
            .join('')}      
      </datalist>
      `
   },

   weatherInfo: function () {
      if (this.state === null) return /*html*/`<div component="loading" state="loadingWeather" message="loading Weather..."></div>`
      const { city, temperature, description } = this.state;
      return /*html*/`
     <div>
      <hr>
       <h1 class="tcenter">${city}</h1>
       <div class="tcenter" style="font-size:4rem;">${temperature}°C</div>
       <div class="tcenter">${description}</div>
       <hr>
     </div>`;
   },
};

// Define properties
const props = {
   weather: null,
   cities: [],
   loadingWeather: false,
};

// Initialize Elementor
const $ = new Elementor(methods, props);
$.observe();

// Add elements to the DOM
document.querySelector('body').insertAdjacentHTML('beforeend', /*html*/`
 <div id="weather">
   <div component="searchForm"></div>
   <div component="datalist" api="cities.json" state="cities" loader="loadingCities">
      <datalist id="cities"></datalist>
   </div>
   <div component="weatherInfo" state="weather" loader="loader"></div>
 </div>
 `);

// Fetch weather data for a city
async function fetchWeather(city) {
   $.$props.loadingWeather = true

   const apiKey = 'your api key';
   const response = await fetch(`https://api.openweathermap.org/data/2.5/weather?q=${city.split('-')[0]}&appid=${apiKey}&units=metric`);
   const data = await response.json();

   if (data.cod === 200) {
      $.$props.loadingWeather = false
      localStorage.setItem('lastCity', city)
      return {
         city: data.name,
         temperature: data.main.temp.toFixed(1),
         description: data.weather[0].description,
      };
   } else {
      throw new Error(data.message);
   }
}

// Search for a city's weather
async function searchCity(city) {
   try {
      $.props.lastCity = city // will run action to update localStorage
      const weather = await fetchWeather(city);
      $.$props.weather = weather
   } catch (error) {
      alert(`Error: ${error.message}`);
   }
};