Are You a Collector?
Tags: webdev, javascript, tutorial, productivity
Published: 2022-12-16 03:11:00

In javascript, when we're thinking about "groupings of like things," we tend to think of arrays. And that's not wrong, arrays are powerful, intentful, and very useful. But they're also pretty basic.

When we start building projects, we often find ourselves reaching for a way to group things, but to do so at a higher level than that of an array. It might be handy to add to a grouping without having to consider how it is being implemented. It might be useful to find an thing by an id, for example, or to be able to implement a `find` function such as the array uses, while not necessarily using an *array* to store that stuff internally.

We want to abstract the data structuring away within a `Collection`, and simply provide a useful *interface* that we can consume.

## What might a `Collection` look like?

We define the `Collection`, from the outside, by what it *does*. What behaviors does it have? How can we talk to it? What can it tell us, and what can we tell it? Let's start by defining some basic methods:

```
Collection:
  .add( item )
  .find( findFunction )
  .findAll()
  .update( updateFunction )
  .remove( item )
```

That's the basic, core functionality we might want. The idea is, a collection should contain instances of the same type of thing, making our `find` or `update` pretty universal (for a collection of `Book` objects, we might want a `find` function that can find by ISBN for all of them).

So we might use it like this:

```js
const playlist = Collection();
playList.add(Song('Si Mi Clean', 'Buju Banton', '3:00'));
playlist.add(Song("Eleanor Rigby - 2022 Mix", "The Beatles", "2:06"));
// we can find things...
const aSong = playlist.find( song => song.title.startsWith("Si")[0];
// and we can update things.
playList.update(aSong.id, song => song.setLike(true) )
```

So we might create that basic functionality:

```js
const Collection = (title='Default Collection') => {
  let stuff = [];

  const add = (item)=> {
    stuff = [...stuff, item];
  }
  const remove = (item) => {
    stuff = stuff.filter(thing => thing != item);
  }
  const find = (func) => stuff.filter( func );
  const findAll = () => [...stuff];
  const update = (updateFunc)=>stuff=stuff.map(updateFunc)

  return {
    get title(){ return title;},
    add,
    remove,
    find,
    findAll,
    update
  }
}
```

Looks pretty clean, does what we want... but its not *great*. I mean, really, all it actually does is wrap an array, and act as an intermediary to an array. It's not like we are adding any functionality to it, right?

So what's the point?

## The Point

Let's revisit the original idea, and use a more concrete example. Suppose we have a `Library` - it's just a collection. It holds a variety of media, and when queried, can retrieve a unique thing, can interact with that thing, can update or remove it. We can add items to the `Library`, without needing to sweat "Hey, so how is *this* thing unique?"

If we were to implement that, we might create a `Book` factory (or constructor, up to you):

```js
const Book = (
  title='Default title',
  author='Default Author',
  publicationDate=Date.now(),
  pages=0
) => {
  const toString = () => `${title} by ${author} (${pages}pp)`
  return {
    get title(){ return title },
    get author(){ return author },
    get publicationDate(){ return publicationDate },
    get pages(){ return pages },
    toString
  }
}
```

So that defines the basic structure of a `Book`: title, author, publication date, pages. And as far as it goes, it's exactly what we'd need.

But if you were to go down that path in one of the online courses you might be advised, "Okay, but now what? are you going to use the title or title-author combo to *uniquely identify* each book?" When we use these `Book` things, we will likely want to be able to find a particular one, in a library of hundreds or thousands. We need an `id`.

And that, right there, has bugged me for a while. You see, the `id` of a book (or of most objects) is fundamentally not a part of the data. The `Book` doesn't care about it, it doesn't use it internally, it doesn't need to reference it. It is not *intrinsic* to the object itself.

But we still need it. It might not be a part of the `Book`, but it helps us to identify the book. It isn't intrinsic, so it's *extrinsic*. By that, I mean that the `id` might help us recognize a book as unique, but it is not a fundamental part of the `Book` "blueprint".

## A Step Closer

If we know that the `id` should indicate the thing it is tagging, but it isn't a core *part* of that thing's functionality, how might that look? Well, we might start where we want to end:

```js
const myBook = {
  _id: 'eddc45f3-c35b-4717-940a-a92021a450b3',
  data: // our Book object
}
```

So we can wrap our `Book`, or whatever we're collecting, inside another object - and that one simply contains two properties: `_id` and `data`. I'm using `crypto.randomUUID()` or a shim here to generate that `_id` value, but whatever makes sense. 

Doesn't look like much, but it's a bit of functionality we will see often. If we store a value to Google's Firestore, it will come back looking very similar to this - it is just a container for our data, with a unique key attached.

## Yeah, But How Does That Help?

Let's revisit the `Collection`, and keep this wrapping-id thing in mind as we do.

```js
// helper function to generate random UUID
const createUUID = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)}

const Collection = (title='Default Collection', _id=createUUID()) => {
  let stuff = [];

  const add = (item, _id=createUUID() )=> {
    const thing = Object.freeze({_id, data: item})
    stuff = [...stuff, thing];
  }
  const remove = (id) => {
    stuff = stuff.filter(thing => thing._id != id);
  }
  const findById = (id) => stuff.find(thing=>thing._id === id)
  const find = (func) => stuff.filter(({data})=>func(data) );
  const findAll = () => [...stuff];
  const update = (id, updateFunc) => {
    stuff = stuff.map(
      (thing) => thing._id===id ?
        Object.freeze({_id, data: updateFunc(thing.data)}) :
        thing
      );
    return findById(id);
  }

  return {
    get _id(){ return _id; },
    get title(){ return title;},
    add,
    remove,
    find,
    findAll,
    update
  }
}
```

This took a little more thinking: rather than dealing with our objects *directly*, we now have an added layer of object in there. So when we pass in a `find` function, we want to find on the passed in thing, not on our container. When we `update`, we want to leave the container alone - the user can consume the `data`, but shouldn't be able to alter our `_id`.

Also note that, rather than rely on the system's `crypto.randomUUID()`, I'm using a pretty common `createUUID()`. Does much the same thing, and won't be facing the security limitations that `crypto` might.

## Yeah, But Why?

A good question, "what's the point?" Let's see what that gets us:

```js
const myDictionary = Collection(`Toby's Private Word Bank`);
myDictionary.add({term: "Cabbage", definition: "n. A familiar kitchen-garden vegetable about as large and wise as a man's head."});
myDictionary.add({term: "Cat", definition: "n. A soft, indestructible automaton provided by nature to be kicked when things go wrong in the domestic circle."})
myDictionary.add({term: "Famous", definition: "adj. Conspicuously miserable."})
myDictionary.add({term: "History", definition: "n. An account mostly false, of events mostly unimportant, which are brought about by rulers mostly knaves, and soldiers mostly fools."});
myDictionary.add({term: "Resident", definition: "adj. Unable to leave."});
```

So we can add things to it. And as we added each one, we were returned a value that we could have caught: the original object in its new wrapper. That could be handy, if we wanted to report its addition or wanted to add something to, for example, the DOM.

Let's see if we can find stuff:

```js
console.log(
  myDictionary.find(
    (el)=>el.definition.startsWith('adj')
  )
)
// [
//  {
//    _id: '6a9fd02e-10b9-4e96-a454-50a4b53839ff',
//    data: { term: 'Famous', definition: 'adj. Conspicuously miserable.' }
//  },
//  {
//    _id: 'ed277d50-f9f7-4c5f-888c-67dbd09ca416',
//    data: { term: 'Resident', definition: 'adj. Unable to leave.' }
//  }
//]
```

And again, when we find the two that match (`Famous` and `Resident`), we are getting the item itself, and its container.

We can place *anything* in that collection, and each placed thing will receive a unique `_id` we can use elsewhere. Build a collection of words, each word will be tagged. Build a collection of books, each book will be tagged. And so on.

## One More Possibility...

Suppose we want to take this, and build a Project Manager type thing. Basically, a todo list manager app. Well, each todo list is just a `Collection`, so we could:

```js
const todoLists = [
  Collection("Personal Tasks"),
  Collection("Work: Group Projects"),
  Collection("Work: Personal Goals"),
  Collection("Family Chores")
]
```

And we have any number of Todo lists, just like that. Each will let us tag our Todos as we add them, giving them a unique id for us.

But that array? It's also a collection:

```js
const todoAppManager = Collection('My Todo App Manager')

todoLists.forEach( todoList =>{
  // each created TodoList has its own unique id already. Let's 
  // leverage that!
  todoAppManager.add(todoList, todoList._id)
})
```

So we have a collection... of collections!

## Next Steps

In the next one, I'd like to look at how we might make this handle callbacks. It could be useful, when an item is added to a collection, to fire off an *observer*, handling activities outside of the collection (saving the collection to `localStorage` or a remote database, updating the DOM, popping up a flyout notification... whatever we might like).