Lists and maps
Components are frequently stored in list and map structures, especially when referenced by other components.
Overview
In JavaScript, creating a list of values is a simple task. Arrays hold any type of data, and they are easily created using the [
and ]
operators, so they are usually the best solution if you need to iterate (loop) over a set of values.
let data = ["a", "b", "c"];
data.forEach(d => { console.log(d) });
In a front-end framework however, JavaScript arrays cause some issues. For one, their values are not checked: they might include a mix of strings, numbers, objects, and other types of values. Any value can be assigned to any array index at any time. This means that they may include duplicates, undefined values, and gaps (which occur when a certain array index is not set at all, which is not the same as setting the array property to undefined
).
Therefore, Typescene implements two different types of lists of its own, which are much stricter about their contents than regular JavaScript arrays and objects.
ManagedList
Represents an ordered set of components
ManagedMap
Represents a set of components that are indexed using a string value
let list = new ManagedList(
new Component(),
new Component(),
new Component()
);
let first = list.first();
let second = list.get(1);
let third = list.last();
let map = new ManagedMap()
map.set("One", new ManagedList());
let one = map.get("One");
By design, managed lists and maps circumvent issues with duplicates and gaps: once an item is removed from the list other items take its place, and the same item cannot be included in the same list more than once. Internally, both ManagedList
and ManagedObject
are implemented in a way that’s very efficient, and doesn’t require looping over the entire list to add or remove items.
Restricting item types
The restrict
method on both lists and maps restricts the type of component objects that can be added to the list — ensuring that all items in the list/map are instantiated by a particular class (or sub class).
ManagedList.restrict()
Restrict objects within this list to be of a given type (class)
ManagedMap.restrict()
Restrict objects within this map to be of a given type (class)
class MyComponent extends Component { }
class SameComponent extends MyComponent { }
class OtherComponent extends Component { }
let list = new ManagedList().restrict(MyComponent);
list.add(new MyComponent()); // OK
list.add(new SameComponent()); // OK
list.add(new OtherComponent()); // ERROR
Nested lists/maps
Both lists and maps (instances of ManagedList
and ManagedMap
) may directly contain other lists and maps. They may even refer to themselves in a circular pattern, directly or indirectly — unless the restrict
method has been used to limit the particular object type that can be added.
This is because by default, lists and maps may contain instances of the ManagedObject
class, a superclass of Component
as well as ManagedList
, ManagedMap
, and ManagedReference
.
Managed record references
The ManagedRecord
class implements several methods that are useful for use with records that are stored in a list — a common way to model ordered or indexed ‘record’ data.
ManagedRecord
Represents data that is managed as a component, often within a list structure
record.getParentRecord()
— retrieves the ‘parent’ instance (containing a property that has been marked with the@managedChild
decorator which indirectly references the record) of theManagedRecord
class itself, even if the parent record references a list/map that contains this record.record.getNextSibling()
— retrieves the next record in a parent list (i.e. a list that is assigned to a property marked with@managedChild
in another record).record.getPreviousSibling()
— retrieves the previous record in a parent list.
Parent-child references
Often, components within a list should be considered to be fully ‘contained’ by the component that includes the list itself — for example in the case of a row of buttons on screen, the buttons certainly belong to the row; moving or removing the row should also move the buttons within. Similarly, a list of addresses may ‘belong’ to a contact record.
For this reason, component lists/maps extend parent-child relationships to all components within the list, if the list itself is assigned to a property marked with @managedChild
(refer to Components for details on how this property decorator works).
class PersonAddress extends ManagedRecord {
street: string;
// ... etc.
}
class PersonContact extends ManagedRecord {
name: string;
// ... etc.
@managedChild
addresses = new ManagedList().restrict(PersonAddress);
}
let contact = new PersonContact();
let address = new PersonAddress(); // ... etc
contact.addresses.add(address);
contact.addresses.first().getParentComponent() // contact
Weak parent-child references
To mark a property containing a list or map as parent-child reference, but not the contained components, use the weakRef
method:
ManagedList.weakRef()
Stop newly referenced objects from becoming child objects
class PersonContact extends ManagedRecord {
// ...
@managedChild
colleagues = new ManagedList()
.restrict(PersonContact)
.weakRef();
}
Note: The use case for this pattern is relatively limited. You may only need to mark the list as a child component (but not its items) if you notice a memory leak when new lists are created frequently, containing many of the same items. Using a child reference, the list itself will be destroyed as soon as a new list is assigned to the same property, even if the contained items are not.
Lists
ManagedList
Represents an ordered set of components
Lists of components can be created using the ManagedList
constructor. This constructor accepts an initial set of objects, which are added to the list immediately.
Afterwards, the list’s methods can be used to add objects:
let a = new MyComponent();
let b = new MyComponent();
let list = new ManagedList(a, b);
// add an item to the back of the list:
list.add(new MyComponent());
// insert an item *before* `b`
list.insert(new MyComponent(), b);
ManagedList.add()
Add one or more objects (or managed lists or maps) to the end of this list
ManagedList.insert()
Insert an object before another object in this list
ManagedList.remove()
Remove an object from this list
ManagedList.clear()
Remove all objects from this list
ManagedList.splice()
Add and/or remove objects at the same time
ManagedList.replace()
Replace all items in this list with a given set of new items, in one go
Iteration
You can loop over all objects in a list using a standard for...of
statement.
class SomeComponent extends Component {
constructor(public s: string) { super() }
}
let list = new ManagedList(
new SomeComponent("a"),
new SomeComponent("b"),
new SomeComponent("c")
);
for (let component of list) {
console.log(component.s);
}
Alternatively, you can use the following methods to iterate over the objects in a list.
ManagedList.forEach()
Invoke a callback function for each object in this list
ManagedList.map()
Invoke a callback function to each object, and return an Array with all results
ManagedList.pluck()
Take a given property from each object, and return an Array with all values
ManagedList.some()
Returns true if a given callback function returns true
for at least one object in this list
ManagedList.every()
Returns true if a given callback function returns true
for all objects in this list
ManagedList.toArray()
Returns an Array with all objects in this list
Events
Lists emit a ManagedListChangeEvent
event when objects are added, removed, or moved.
- When an object is added, the object itself is referenced by the
object
property of theManagedObjectAddedEvent
event. - When an object has been removed, the removed object is referenced by the
object
property of theManagedObjectRemovedEvent
event.
Furthermore, lists can be made to ‘propagate’ (i.e. re-emit) all events that are emitted by objects within the list, using the propagateEvents
method.
ManagedList.propagateEvents()
Propagate events from all objects in this list
class PersonContact extends ManagedRecord {
@managed
friends = new ManagedList()
.restrict(PersonContact)
.propagateEvents();
}
PersonContact.addObserver(class {
constructor(public contact: PersonContact) { }
onFriendsChange() {
// ...[1]
}
})
In the code above, the onFriendsChange
method [1] is called when:
- the friends list is assigned the first time (an empty list),
- any objects are added to, removed from, or moved within the friends list,
- any object within the friends list emits a change event — since those events are immediately also emitted by the list itself.
Views and bindings
ManagedList
instances are frequently used to manage lists of objects that need to be displayed in the application UI. The UIListController
view component can be used for this purpose. Its items
property refers to a ManagedList
instance, which are turned into views using a ‘view adapter’ — a component that takes an object and produces a view component. The generic UIListCellAdapter
component usually serves this purpose well.
Note that Typescene will automatically convert arrays to objects in a managed list when updating a binding.
- If the existing property value of a bound property (e.g. the
items
property of aUIListController
view component) is already an instance ofManagedList
, and - if the bound value (i.e. the value of the bound parent’s property) is a
ManagedList
, then the list reference itself is copied to the target component. - if the bound value is
undefined
, then the list is cleared. - if the bound value is an array of
ManagedObject
instances (e.g. components), then all objects in the list are replaced with the objects from the array. - if the bound value is an array of other values (e.g. strings), then those values are encapsulated in an anonymous component type first, whose
valueOf()
method returns the original value.
This makes it possible to bind the items
property of a list view either to a ‘real’ list of components, or to a simple array:
const view = UICell.with(
UIListController.with(
// items is actually a ManagedList:
{ items: bind("items") },
UIListCellAdapter.with(
UIRow.with(
UILabel.with(bind("value")) // a, b, c
)
)
)
)
class MyActivity extends PageViewActivity.with(view) {
// ...
// plain array elements get converted:
items = ["a", "b", "c"]
}
Maps
ManagedMap
Represents a set of components that are indexed using a string value
Indexed sets of components can be created using the ManagedMap
constructor. This constructor does not accept any arguments.
Afterwards, the map’s methods can be used to add objects:
let map = new ManagedMap();
map.set("one", new MyComponent());
map.set("two", new MyComponent());
ManagedMap.set()
Add an object (or managed list or map) to this map using given string index (key)
ManagedMap.unset()
Remove an object from this map using a given string index (key)
ManagedMap.remove()
Remove an object from this map
ManagedMap.clear()
Remove all objects from this map
Iteration
Unlike with the ManagedList
class, you cannot loop over objects in a map using a for...of
statement.
Instead, you can use the following methods to iterate over the indexes (keys) or objects in a list.
ManagedMap.forEach()
Invoke a callback function for each key and object in this map
ManagedMap.objects()
Returns an Array with all objects in this map
ManagedMap.keys()
Returns an Array with all keys in this map
ManagedMap.toObject()
Returns a plain JavaScript object with all objects in this map, with keys as property names
class SomeComponent extends Component {
constructor(public s: string) { super() }
}
let map = new ManagedMap();
map.set("1", new SomeComponent("One"));
map.set("2", new SomeComponent("Two"));
map.set("3", new SomeComponent("Three"));
map.forEach((key, component) => {
console.log(key + ": " + component.s);
// => 1: One
// ...etc
}
Events
Maps emit a ManagedListChangeEvent
event when objects are added, removed, or moved — similar to lists.
- When an object is added, the object itself is referenced by the
object
property of theManagedObjectAddedEvent
event, and the key is stored in thekey
property. - When an object has been removed, the removed object is referenced by the
object
property of theManagedObjectRemovedEvent
event, and the key is stored in thekey
property.
Furthermore, maps can also be made to ‘propagate’ (i.e. re-emit) all events that are emitted by objects within the map, using the propagateEvents
method (see Lists above for an example).
ManagedMap.propagateEvents()
Propagate events from all objects in this map
Next steps
Find out how lists and maps are used by Typescene to store application components.
- Learn more about views, which represent the user interface as a tree structure of components.
- Learn more about activities, which encapsulate views and represent the possible states of an application.
- Learn more about services, which encapsulate the application’s global state, available from within all other components.