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 the ManagedRecord 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 the ManagedObjectAddedEvent event.
  • When an object has been removed, the removed object is referenced by the object property of the ManagedObjectRemovedEvent 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.

See also: Concepts > Views

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 a UIListController view component) is already an instance of ManagedList, 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 the ManagedObjectAddedEvent event, and the key is stored in the key property.
  • When an object has been removed, the removed object is referenced by the object property of the ManagedObjectRemovedEvent event, and the key is stored in the key 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.