per-each
v0.0.9
Published
[](https://github.com/bahrus/per-each/actions/workflows/CI.yml) [](http://badge.fury.io/js/per-each) [![How bi
Downloads
6
Readme
per-each (🍑) [WIP]
per-each is a custom element enhancement, based on the be-enhanced family of behiviors, that
- Provides for looping support, but
- Imposes little to no requirements as far as binding syntax.
- Promotes use of light-weight classes or function prototypes for encapsulating logic and binding as needed (that is part of a standards proposal), while
- Working around limitations of proper HTML decorum.
- It can "resume" rendering from server-rendered HTML based on WHATWG standard microdata attributes (with a small enhancement proposal)
Avoiding the framework trap
On the web presentation layer, since there is no built-in web standard support for dynamically generating a loop of HTML on the client side, developers naturally flock to a library / framework for this functionality. And that typically involves abiding by some proprietary syntax for all binding. And poof, the developer gets sucked into a framework with no possibility of escape.
Custom Elements have made great inroads in avoiding the framework trap. Each component can adopt any binding syntax it wants within the Shadow DOM realm. However, they fall short when it comes to generating the light children, without a little nudge.
This enhancement provides that nudge. It builds on a proposal that provides a common mechanism for binding a view model to the UI -- the ability for a class instance or function prototype to be attached automatically to an element based on the itemscope attribute, so it can manage the light children of the adorned element.
Example 1 -- No template
Example: Suppose we want to display the medal count and details of the last Olympics, using the HTML table element.
This could look as follows:
<body>
...
<table itemscope=WorldRankingList>
<thead>
<tr>
<th>Rank</th>
<th>NOC</th>
<th>Gold</th>
<th>Silver</th>
<th>Bronze</th>
<th>Total</th>
</thead>
<tbody>
<tr
per-each="Country of WorldRankingList">
<td itemprop=rank></td>
<td itemprop=noc></td>
<td itemprop=gold></td>
<td itemprop=silver></td>
<td itemprop=bronze></td>
<td itemprop=total><span itemprop=total></span> of <span -o=totalMedalCount></span></td>
</tr>
</tbody>
</table>
</body>per-each looks at the element it adorns, the tr element, and turns it into a template. per-each also supports enhancing template elements, which is required for repeating multiple side-by-side elements per loop iteration.
All that per-each does is clone the tr element multiple times, and set the attribute for each one, and it passes each list item to the "ish" property of each such tr element:
<table itemscope=WorldRankingList>
<thead>
...
</thead>
<tbody>
<tr itemscope=Country>
...
</tr>
<tr itemscope=Country>
...
</tr>
</tbody>
</table>"ish" stands for itemscope host.
Being that per-each is a be-hive based custom enhancement, that builds on mount-observer, which is a polyfill for another proposal, each such itemscope attribute:
- Causes the instantiation of a class or function prototype registered by that name....
- ... which gets attached to the element the itemscope attribute adorns, with dynamic property key "ish"
What makes the "ish" property a bit interesting as a property, is that the setter for ish doesn't actually replace the ish class instance, but rather does an Object.assign / shallow merge (?) of the passed in object into the class instance. That is, that's what happens if the object being passed in is not an array.
In the case of getting passed in an array, the ish property setter sets the class instances's "ishList" property. So these "scoped class or function prototype instances" that wish to provide a list of data are expected to follow the convention of reserving that property with name "ishList", which per-each assumes.
Implementing these conventions takes a certain amount of boilerplate effort, shown below. However, a small library or base class or two can easily make developing such cookie cutter classes or function prototypes trivial:
import {regIsh, sym} from 'mount-observer/refid/regIsh.js';
regIsh(document.body, 'WorldRankingList', class {
async 'arr=>'(self, arr){
/**
* Typically the list of data will be passed in via the oElement.ish property,
* or retrieved internally via fetch, for example
*/
const returnArr = arr;
if(returnArr === undefined){
returnArr = [
{rank: 1, noc: 'United States', gold: 40, silver: 44, bronze: 42, total: 126},
{rank: 2, noc: 'China', gold: 40, silver: 27, bronze: 24, total: 91},
{rank: 3, noc: 'Japan', gold: 20, silver: 27, bronze: 13, total: 45},
...
];
}
this.#calculateTotal(returnArr)
return returnArr;
}
/** just an example, entirely optional */
#calculateTotal(arr){
this.#totalMedalCount = arr.reducer((accumulator, currentValue) => accumulator + currentValue.total));
}
#totalMedalCount;
get totalMedalCount(){
return this.#totalMedalCount;
}
'<mount>'(self, el){
//do any rendering / event handling that is desired on the element
//To update the list:
el.ish = [...newList]
}
});
regIsh(document.body, 'Country', class {
/** Optional. First element of cloned template gets passed in here **/
/** For server rendered HTML, the element with itemscope attribute = Country
* in this case gets passed in
*/
async '<mount>'(self, element, {csr: true/false}){
//binding / event handling added here if needed
}
/** Optional.
* Any elements other than the first element of the template
* gets passed in here.
* For SSR generated content, elements get passed in via the itemref attribute references:*/
async '<inScope>'(self, element){
//binding / event handling added here
}
});
The HTML markup in the example is used in the demo examples of this package, and in those demo's the Country class or function prototype chooses to use microdata ("itemprop") for binding clues. But per-each doesn't really care about that, and doesn't look for any itemprop attributes (only itemscope). It just needs a class or function prototype that implements:
interface Ishcycle{
/** optional */
'<mount>'?(self:this, el: Element, {csr?: boolean /* TODO */}): Promise<void>;
/** optional */
'<inScope>'?(self: this, el: Element): Promise<void>;
/** optional */
'arr=>'?(
self: Ishcycle, arr: any[] | undefined,
el: Element & HasIsh,
options: BindishOptions)
: Promise<void | any[]>;
}Libraries that help with developer ergonomics
What we've seen above is that there is a certain amount of ceremony required to define the custom classes and/or function prototypes that are needed for per-each to be able to work. If per-each is used frequently, it is advisable to use a helper library to reduce the boilerplate necessary, and the demos in this package do use such a helper library, which builds on trans-rendering.
Referencing the count
<table itemscope=WorldRankingList>
<thead>
<tr>
<th>Rank</th>
<th>NOC</th>
<th>Gold</th>
<th>Silver</th>
<th>Total</th>
</thead>
<tbody>
<tr
per-each="Country of WorldRankingList"
per-each-map-idx-to="myIndex"
per-each-idx-start="1">
<td itemprop=rank></td>
<td itemprop=noc></td>
<td itemprop=gold></td>
<td itemprop=silver></td>
<td itemprop=bronze></td>
<td itemprop=total><span itemprop=total></span> of <span -o=totalMedalCount></span></td>
</tr>
</tbody>
</table>This sets property "myIndex" of each ish-based class or function prototype equal to the index, with an optional starting index specified as above (defaults to 1).
As you can see, the markup gets a little clunky when specifying numerous options. Two things can be done to reduce the manual effort in configuring the component:
- Adopt a smaller name
- Utilize the options setting
It is easy to define an alternative name for this enhancement that can be used in less formal setting -- names that aren't registered in some package management system like npm, that may conflict with other libraries.
One alternative name that this package supports is the emoji: 🍑.
Also, there's one setting that allows all the others to be specified via the more compact (but more error prone, less semantic) JSON. So the example above:
<tr
per-each="Country of WorldRankingList"
per-each-map-idx-to="myIndex"
per-each-idx-start="1"
>
...
</tr>... can be mocked up as:
<tr
🍑-options='{
"each": "Country of WorldRankingList",
"mapIdxTo": "myIndex",
"idxStart": 1
}'
>
...
</tr>SSR
Due to the heavy reliance on HTML attributes to keep things in sync, this element enhancement integrates seamlessly with server rendered html. For example, expand the section below to see what works:
<table itemscope=WorldRankingList>
<caption>Medal List Summer 2024</caption>
<thead>
<tr>
<th></th>
<th>Rank</th>
<th>NOC</th>
<th>Gold</th>
<th>Silver</th>
<th>Bronze</th>
<th>Total</th>
</tr>
</thead>
<tbody>
<template
per-each="Country of WorldRankingList"
per-each-map-idx-to="idx"
per-each-idx-start="1"
trans-render-idrefs-a0="row_1 row_2 row_3 row_4"
>
<tr>
<td itemprop=rank></td>
<td itemprop=noc></td>
<td itemprop=gold></td>
<td itemprop=silver></td>
<td itemprop=bronze></td>
<td><span itemprop=total></span> of <span -o=totalMedalCount></span></td>
</tr>
</template>
<tr itemscope=Country id=row_1>
<td itemprop=rank>tbd 1</td>
<td itemprop=noc>tbd 1</td>
<td itemprop=gold>tbd 1</td>
<td itemprop=silver>tbd 1</td>
<td itemprop=bronze>tbd 1</td>
<td><span itemprop=total>tbd</span> of <span -o=totalMedalCount>tbd</span></td>
</tr>
<tr itemscope=Country id=row_2>
<td itemprop=rank>tbd 2</td>
<td itemprop=noc>tbd 2</td>
<td itemprop=gold>tbd 2</td>
<td itemprop=silver>tbd 2</td>
<td itemprop=bronze>tbd 2</td>
<td><span itemprop=total>tbd</span> of <span -o=totalMedalCount>tbd</span></td>
</tr>
<tr itemscope=Country id=row_3>
<td itemprop=rank>tbd 3</td>
<td itemprop=noc>tbd 3</td>
<td itemprop=gold>tbd 3</td>
<td itemprop=silver>tbd 3</td>
<td itemprop=bronze>tbd 3</td>
<td><span itemprop=total>tbd</span> of <span -o=totalMedalCount>tbd</span></td>
</tr>
<tr itemscope=Country id=row_4>
<td itemprop=rank>tbd 4</td>
<td itemprop=noc>tbd 4</td>
<td itemprop=gold>tbd 4</td>
<td itemprop=silver>tbd 4</td>
<td itemprop=bronze>tbd 4</td>
<td><span itemprop=total>tbd</span> of <span -o=totalMedalCount>tbd</span></td>
</tr>
</tbody>
</table>Inference
If the name of the itemscope list isn't provided, it is inferred. This can reduce things getting out of sync when refactoring takes place:
<table itemscope=WorldRankingList>
<thead>
<tr>
<th>Rank</th>
<th>NOC</th>
<th>Gold</th>
<th>Silver</th>
<th>Bronze</th>
<th>Total</th>
</thead>
<tbody>
<tr
per-each="Country" >
<td itemprop=rank></td>
<td itemprop=noc></td>
<td itemprop=gold></td>
<td itemprop=silver></td>
<td itemprop=bronze></td>
<td itemprop=total><span itemprop=total></span> of <span -o=totalMedalCount></span></td>
</tr>
</tbody>
</table>Conditional Templates
This library adheres to a strict division of labor between declarative markup in the HTML, and the supporting scoped classes. If filtering of a list is needed, that filtering should be done within these custom classes, for example. The syntax for per-each doesn't provide any ability to filter the list, unlike some alternative looping frameworks.
Within each looped template / scoped class, one can of course apply conditional logic, so that different parts of the template may be visible, and different functionality exposed, depending on conditions within each instance, as calculated via the scoping class and/or css rules.
But there are scenarios, lists containing radically different types of objects, where the differences are so stark that it may feel cleaner to provide for different scoping classes between them, and/or different templates from which to bind.
This library does provide help for that scenario, described below. As we will see, it follows a somewhat novel approach, in order to adhere to the separation of concern guiding principle.
For this scenario, the developer will need to construct a "two dimensional" list -- a list where each item of the list consists of a tuple (array) of objects. The tuples must all be of the same size, and the size must match the number of per-each statements and the number of templates defined, as seen below. The key is that if an element of the tuple is undefined or null, then it will be skipped over, and not rendered. That is how we accomplish the conditional template functionality.
class FormElements {
rawListOfFormElements = [
{
type: 'text',
label: 'First Name',
name: 'firstName',
required: true,
placeholder: 'Enter your first name'
},
{
type: 'checkbox',
label: 'Subscribe to newsletter',
name: 'subscribe',
required: false,
checked: true
},
{
type: 'text',
label: 'Email',
name: 'email',
required: true,
placeholder: 'Enter your email'
}
...
];
get ConditionalListTuples(){
return this.rawListOfElements.map(x => {
switch(type){
case 'text':
return [x,,undefined];
case 'checkbox':
return [, x, undefined];
case 'search':
return [,, x];
}
})
}
}
class TextboxMgr {...}
class CheckboxMgr {...}
class SearchMgr {...}What's important here is that each of of the ConditionalListTuple has three items, between 0 and 3 of them being defined, the others being undefined/empty/null.
So now we need three per-each statements, as shown below. They can reuse the same class, but in this example, we reach for three different classes. And we need three templates (which can also share the same remote definition).
So mathematically, we essentially have 2x3 = 6 possibilities to consider -- 2 represents the pair of scoping class and template choices, and 3 members of the tuple that needs to be mapped out, in case that helps.
<table itemscope=FormElementList>
<caption>Conditional Displays</caption>
<thead>
<tr>
...
</tr>
</thead>
<tbody itemscope=Scope itemprop=ConditionalListTuples>
<template
per-each="TextBoxMgr, CheckBoxMgr, SearchMgr of Scope"
>
<template>
<tr>
...render TextBox info
</tr>
</template>
<template>
...render CheckBox info
</template>
<template>
...render SearchMgr info
</template>
</template>
<template 🎚️="on if isEmpty">
...
</template>
<template 🎚️="on if notAnArray">
...
</template>
</tbody>
</table>Viewing Locally
Any web server that serves static files with server-side includes will do but...
- Install git.
- Fork/clone this repo.
- Install node.
- Install Python 3 or later.
- Open command window to folder where you cloned this repo.
npm install
npm run serve
- Open http://localhost:8000/demo in a modern browser.
Running Tests
> npm run testUsing from ESM Module:
import 'per-each/per-each.js';Using from CDN:
<script type=module crossorigin=anonymous>
import 'https://esm.run/per-each';
</script>
