A template node can be repeated using the foreach
directive.
Adding a foreach
attribute on an template node causes that node
to be repeated for each item in the array.
{
foreach: [ "Apples", "Pears", "Bananas" ],
type: "div",
text: i => i,
},
On template nodes with a foreach
directive the arguments passed
to dynamic property callbacks changes from (model,context)
to
(item,itemContext)
.
item
- is the current item from the list
itemContext
is an object with additional properties relating
to the list enumeration.
context.outer
- the outer loop context (either an enclosing
foreach
loop context, or the component's context)context.model
- the current itemcontext.key
- the current item's key (see below)context.index
- the current item's zero based index in the collectionAs shown in the above example, the convention is to name the item
argument i
to distinguish it from the c
used for the component
reference.
The context.outer
property can be used to access outer foreach
iterators, or the component itself where context.outer.model
will
give the actual outer component or item.
The above example shows using a foreach
directive with a static
array of items but more typically a dynamic collection is used.
In this example items are randomly added/removed from an array and
by calling invalidate()
on the component, the list is updated to
reflect the new array content.
onAdd()
{
this.items.splice(this.randomPos(true), 0, `New Item ${this.nextItem++}`);
this.invalidate();
}
onRemove()
{
if (this.items.length > 0)
this.items.splice(this.randomPos(false), 1);
this.invalidate();
}
{
foreach: c => c.items,
type: "div",
text: i => i,
},
The itemContext.index
property gives the zero based index of the item
in the items
array.
If the condition
option (see below) is used, it's the index after
non-matching items have been removed. ie: the index in the filtered array.
{
foreach: c => c.items,
type: "div",
class_even: (i,ctx) => (ctx.index % 2) == 0,
class_odd: (i,ctx) => (ctx.index % 2) != 0,
text: (i,ctx) => `${ctx.index}: ${i}`,
},
css`
.odd { color: orange }
.even { color: lime }
`
Often when working with foreach
directives you'll need to specify some
additional settings to control exactly how the foreach
directive works.
To specify anything more than just the array collection, use an
object as the value of the foreach
key and set the items
property
to the array of items to iterate over, or a callback for an array.
{
foreach: {
items: [],
// other options here
}
}
The following options are supported:
itemKey
- a callback function to return a key for an item.condition
- a callback function that indicates if an item should be
includedempty
- template to show when the items array is empty.The itemKey
option is a callback that should provide a key value
for an item. By providing a key, list item DOM elementscan more
efficiently re-used during updates.
The value returned by the itemKey
callback can be any value that can
be directly compared to other keys for equality using the JavaScript ==
operator.
For example, suppose we're displaying a list of items with a name
and id
property:
{
foreach: { foreach directive
items: c => items, This provides items
itemKey: i => i.id, This provides item keys for items
},
type: "div", The rest is repeated for each item
text: i => i.name, `i` is the list item
}
For more details on how item keys are used, see Update Semantics below.
Ideally the key for each item should be unique however it is not strict requirement and the update logic will handle duplicate keys - and still be more efficient that having no keys at all.
The condition
option is a predicate callback that can be used to filter
which items in the array should be shown. Return true
to include
an item, or false
to exlude it.
This example filters the list to only show items with a price below $100.
items = [
{ name: "Bread", price: 4 },
{ name: "Phone", price: 1000 },
{ name: "Gift Voucher", price: 50 },
{ name: "Car", price: 50000 },
{ name: "Wine", price: 40 }
];
foreach: {
items: c => c.items,
condition: i => i.price < 100,
},
type: "div",
text: i => `${i.name}: \$${i.price}`,
The empty
setting can be used to specify a template to be used if the
list of items is empty.
{
foreach: {
items: c => c.items,
empty: { this will be displayed if the list is empty
type: "div",
text: "Nothing Here!"
}
},
type: "div",
text: i => i,
},
The foreach
directive uses one of two strategies to apply updates depending
on whether an itemKey
callback has been supplied.
When item keys are not provided the list is updated by re-using the previous DOM elements in the same order, updating each with the new item at that position, and then adding or removing elements at the end to match the new item count.
With this strategy, the re-used items are not unmounted/remounted - they're simply in-place patched.
This works well for small lists and is easy to use, but for larger lists may not perform well as a single insert or delete near the start of the list may mean every subsequent element needs a full update.
When a keyed foreach
block updates the item keys for the new and old
arrays are compared and each item is determined to be either:
The items are then updated:
In this strategy, when an items is re-purposed it will first be unmounted, changed properties applied and then re-mounted after being added back into the DOM.
This strategy can dramatically improve the performance of typical list updates as most items will be re-used and not require significant updates.
When a foreach
block includes a nested component, the changed properties
of the component will be applied, but the component's update
method
is not called.
This is consistent with how templates update components used elsewhere
but warrants some consideration for foreach
blocks.
Often a foreach
block will be used to display a set of items where
each item is an object that's displayed using a component.
As an example, say you're building a photo management app and you have
a Photo
object with properties for various attributes of each photo:
class Photo
{
filename;
size;
date;
favorite;
}
You also have a PhotoCell
component that can display
a thumbnail view for a single photo:
class PhotoCell extends Component
{
#photo;
get photo()
{
return this.#photo;
}
set photo(val)
{
this.#photo = value;
this.invalidate();
}
static template = { /* ... */ }
}
You could show a collection of photos using a foreach
block:
{
foreach: {
items: c => c.allPhotos,
itemKey: i => i.filename,
},
type: PhotoCell,
photo: i => i,
}
How this works for updates will depend on whether you replace
the Photo
instance when it changes, or just update its properties.
If the Photo
instance is replaced with a new instance, but the same
key, the template will see the PhotoCell.photo
property as being
different and assign the new value to the component. The component will
update itself and the changes will be reflected in the DOM.
If however properties of the Photo
are updated, the template will
consider the PhotoCell.photo
property unchanged, won't re-assign it
and the DOM won't be updated.
While the behaviour in the second case may seem inconvenient and problematic this is actually the exact behaviour you want when managing large collections because it allows you to make small and precise updates.
One way to handle this is to use deep component updates although this is generally not recommended - especially for large collections.
The correct way to handle this is to have the Photo
object fire change events and
for the PhotoCell
component to add listeners for those events. In other
words this is a problem that should be solved by the Photo
object and
PhotoCell
component - not by the foreach
block.
To setup this event/listener mechanism you can either roll your own event
system, use the standard
EventTarget
mechanism, or use CodeOnly's notify
.
Also consider using Component.listen
to simplify adding
and removing event listeners in the item component.