Extending Collections
Tags: webdev, javascript, tutorial, productivity
Published: 2022-12-16 23:28:06

So in the [last article](https://dev.to/parenttobias/are-you-a-collector-2ll8), I wrote about how a `Collection` is an abstract constructor (not a `constructor`, but a builder of objects) that, in essence, builds an array with some added functionality. We are using it to store things, but we're also *wrapping* those stored things with a unique `_id` tag, so we can find them later.

At this point, we haven't actually made those `_id` values unique - the downside to using an array for the internal implementation. That is a thing we can address in this one, whether we simply add a check or use a different internal store within the `Collection`.

## What Does That Even Mean?

When we say a thing is *Observable*, we are saying "this thing will have some actions (or *events*) that we can observe, and we can act on the result of those actions." 

When we write event handlers in javascript:

```js
const handleButtonClick = (event) => {
  event.target.classList.add("someClass");
  console.log(`the ${event.id} button was clicked!`);
}

document.querySelectorAll("button").forEach((button)=>{
  // we *observe* the click event on each button
  button.addEventListener( "click", handleButtonClick )
})
```

That is adding a listener on (or *subscribing to*) an event that might, at some point, take place on that `<button>` element. The button is an Observable, and our `handleButtonClick` is the observer.

The neat thing is, this is not exclusive to the core javascript language. This is a thing we can do ourselves, and pretty easily. Just... with a little knowledge and application.

## A Little Background

In order to make something observable, we need to define at what points our stuff can be observed, and how we want to handle that observation. In the case of the `Collection`, we don't need to observe the find events, as that isn't *changing* anything - so perhaps the add, remove and update functions are good observation points.

And how might we do that? We'll steal a page from the `Events` API, with a string to identify which action and a function to run when that action occurs. If you aren't sure about passing in functions, take a look at [Understanding Higher-Order Functions](https://dev.to/parenttobias/understanding-higher-order-functions-4f6d) to see what I mean. We'll take this same idea and build upon it.

So, looking at our `Collection`, we have this:

```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
  }
}
```

To begin, we'll need to have some way to keep track of those functions. As we're expecting a string for the action and a function, an object might be useful:

```js

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

  const add = (item, _id=createUUID() )=> {
    const thing = Object.freeze({_id, data: item})
    stuff = [...stuff, thing];
    return thing;
  }

  /* and all the rest of our handy-dandy functionality */
  return {
    get _id(){ return _id; },
    get title(){ return title;},
    add,
    remove,
    find,
    findAll,
    update
  }
}
```

So we will add them to `observers` as we are given them. But we also need a couple of methods, just like `addEventListener` and `removeEventListener`. In our case, we will call them `subscribe` and `unsubscribe`, as the pattern we're using is often referred to as **Publish/Subscribe** or **PubSub**. So what might those two methods look like?

```js

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

  const add = (item, _id=createUUID() )=> {
    const thing = Object.freeze({_id, data: item})
    stuff = [...stuff, thing];
    return thing;
  }

  // when a function is subscribed, we simply add it to the
  //  observers object, in an array stored under the action.
  const subscribe = (action, observerFunction) => {
    if(!observers.hasOwnProperty(action)){
      observers[action]=[];
    }
    observers[action].push(observerFunction)
  }

  // and to unsubscribe, we reverse that. Just as with
  //  event listeners, we can only remove functions with
  //  the same reference by which we subscribed 'em.
  const unsubscribe = (action, observerFunction) => {
    observers[action] = observers[action].filter(
      func => func !== observerFunction
    );
  }

  /* and all the rest of our handy-dandy functionality */
  return {
    get _id(){ return _id; },
    get title(){ return title;},
    add,
    remove,
    find,
    findAll,
    update,
    subscribe,
    unsubscribe
  }
}
```

That will let us add and remove observer functions, placing them into or removing them out of the `observers` object as we like. To use it, we might:

```js
const myDictionary = Collection("Toby's Devils Dictionary");
myDictionary.subscribe("add", (item)=> console.log(`**${item.term}**: *${item.definition}*`))
```

And that would add an inline function to our `observers.add` array. Note, though, as we've defined it as an inline function, we have no reference to it with which we can remove it! This is exactly the same behavior as we see in `Events` API, so I'm not too worried about it.

If we wanted to be able to remove it later, we would need to keep a reference:

```js
const logInUpperCase = (item) => console.log(`**${item.term.toUpperCase()}**: *${item.definition}*`);

myDictionary.subscribe("add", logInUpperCase)

// and later, if we wanted:
myDictionary.unsubscribe("add", logInUpperCase)
```

So long as we have a reference to the function we subscribed, we can always unsubscribe it.

## Well Yeah, But What Do We **DO** With It?

At this point, we have an object of arrays, but we aren't doing anything with them. We're storing them, and they're there, but we haven't actually executed anything yet!

Patience. Here it comes.

When we add something to the `Collection`, we do a few things:
* We change the array stored within the `Collection`,
* We generate a new `_id` for the thing we're adding,
* We return the thing we've added, in its new container.

When we call our `"add"` observer function, those are the things we will want to pass into it. Do note that the parameters we will pass change, depending on the action. If we're calling the `"delete"` observer, the data being passed will likely be different.

But in this case, let's work with the `"add"` observers, and see how we might use them. To begin, we need to tell the `add()` method about the its observers, if there are any:

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

  const add = (item, _id=createUUID() )=> {
    const thing = Object.freeze({_id, data: item})
    stuff = [...stuff, thing];
    observers.add?.forEach(
      /* and in here, we do something for each observer function */
    )
    return thing;
  }


  /* and all the rest of our handy-dandy functionality */
  return {
    get _id(){ return _id; },
    get title(){ return title;},
    add,
    remove,
    find,
    findAll,
    update,
    subscribe,
    unsubscribe
  }
}
```

Note the syntax of the line I added there: `observers.add?.forEach()`. That's a pretty funky syntax, when we look at it. What's that question mark doing in there?

The `?.` is a pretty neat new feature referred to as [Optional Chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining). What it says is, "If the thing before the `?` exists, go ahead and run the function after the `.` - but if the thing before the `?` does not exist, stop now and evaluate to `undefined`." In effect, if we have no `add` property on `observers`, we simply stop evaluating the expression and bypass the `.forEach()` bit.

This is well-supported in modern browsers, but if we wanted to avoid using newish features, we could:

```js
  const add = (item, _id=createUUID() )=> {
    const thing = Object.freeze({_id, data: item})
    stuff = [...stuff, thing];
    if(observers.hasOwnProperty("add")){
	  observers.add.forEach(
	    /* and in here, we do something for each observer function */
	  )
    }
    return thing;
  }
```

Does exactly the same thing.

Either way, we now have a `forEach()` we need to handle. And each thing is a function that we've been passed, and that we want to execute, *remembering to pass in the values we've outlined*.

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

  const add = (item, _id=createUUID() )=> {
    const thing = Object.freeze({_id, data: item})
    stuff = [...stuff, thing];
    observers.add?.forEach( (observerFunc) =>
      // we'll pass as arguments:
      //  - the wrapped item,
      //  - the collection as pure data
      observerFunc(thing, {_id, title, collection:[...stuff]} )
    )
    return thing;
  }


  /* and all the rest of our handy-dandy functionality */
  return {
    get _id(){ return _id; },
    get title(){ return title;},
    add,
    remove,
    find,
    findAll,
    update,
    subscribe,
    unsubscribe
  }
}
```

With that, each time we run `add()`, we also execute each `observers.add` function, passing in the arguments we said we would. And just as with the `Event` API, we can't actually *return* anything from those functions.

## To Recap

So the idea here is to add some functionality to the `Collection`, letting it execute functions on our behalf at some future time. As we've discussed, this is a Publish/Subscribe pattern, as we "publish" events to which other objects or functions can "subscribe". In a formal sense, this is an Observer pattern.

The usefulness of the pattern might be observed by how we could use it later:

```js
// suppose we've defined each of the parts we'll use
//  as factories (a Collection factory)
//  or imported modules (TodoAppDom and saveToLocalStorage)
const myTodoApp = ((manager, dom, storage)=>{
  // create our Collection,
  const myDictionary = Collection("Toby's Devils Dictionary");
  
  // set specific parameters for the DOM and storage, perhaps:
  dom.container = document.querySelector('#my-todo-app');
  storage.key = myDictionary._id;

  // listen to collection changes and reflect them to the DOM
  myDictionary.subscribe("add", dom.addTodo );
  myDictionary.subscribe("remove", dom.displayAllTodos );
  myDictionary.subscribe("update", dom.updateOneTodo );

  // listen for those same changes, and update localStorage
  myDictionary.subscribe("add", storage.save );
  myDictionary.subscribe("remove", storage.save );
  myDictionary.subscribe("update", storage.save );

})(Collection('My Todo App'), TodoAppDom, saveToLocalStorage)
```

If we know the *function signature* for each of those listeners, then we can write methods on the DOM module or the storage module to pass right in. And, in this case, the function signatures are pretty straightforward:

* `"add", function( wrappedItem, collectionObject)`
* `"remove", function(deletedItemId, collectionObject)` 
* `"update", function(wrappedItem, collectionObject)`

So each observer receives the same *number* of parameters, allowing us to define a `saveToLocalStorage` object:

```js
const saveToLocalStorage = () =>{
  let key='Default Key';
  const load = () =>
    JSON.parse(localStorage.getItem(key));
  const save = async ( _, collection ) => 
    localStorage.setItem(
      key, JSON.stringify(collection)
    );
  return {
    save,
    load
    set key(value){key=value;}
  }
}
export default saveToLocalStorage;
```

Note that I haven't defined the observers entirely - you get somehomework! How might the `delete` observer work? And the `update`?

How about, what kinds of actions might you be able to trigger on each action? What possibilities have you got? Adding a sound effect on save, perhaps, or causing a flyout notification?

What other actions might make sense? Is there a benefit to having a `"beforeadd"` and `"afteradd"` action? Or a more general, `"before<action>"` and `"after<action>"` type? How many make sense to you?

Let's have a conversation about this!