# SortedMap & SortedRenderMap

`SortedMap` and `SortedRenderMap` are key-based data structures. They are designed to store and sort the data inside of them by the position that is defined by a developer or generated automatically.

## Motivation

We wanted a data structure that will be flexible and works well with our [Extensibility mechanism](https://scandipwa.gitbook.io/shopify/architecture/extensibility). More specifically we wanted it to:

* Sort content by the `position` that is assigned to each element manually or automatically,
* Give the ability to retrieve any element from the list by some unique key,
* Be easily extensible with our plugin mechanism,
* Be convenient and easy to use.

## Usage

Both `SortedMap` and `SortedRenderMap` behave similarly, but the SortedRenderMap is a preferred way of storing React Components.

A new instance is declared by using `createSortedMap()` or `createSortedRenderMap()` functions from the `@scandipwa/framework` package:

```javascript
import {
    createSortedMap,
    createSortedRenderMap
} from '@scandipwa/framework/src/util/SortedMap';

export class MyComponent extends Component {
    sortedMap = createSortedMap({
        firstItem: 1,
        thirdItem: 3
    });
    
    sortedRenderMap = createSortedRenderMap({
        firstComponent: this.renderFirstComponent.bind(this),
        thirdComponent: this.renderThirdComponent.bind(this)
    });
}
```

{% hint style="info" %}
Make sure that values in the `createSortedMap` declaration return functions.
{% endhint %}

Both functions accept a single argument that is an object containing the data. As you might notice, we don't pass the `position` argument in any of the items and this is because the position is assigned automatically for items in the initial declaration. Now let's fill in the gap between one and three using the [Extensibility mechanism](https://scandipwa.gitbook.io/shopify/architecture/extensibility).

### Adding an item

```javascript
const addSecondItem = (member) => {
    const SECOND_ITEM_POSITION = 1500;
    member.addItem(2, 'secondItem', SECOND_ITEM_POSITION);
    return member;
};

export default {
    'MyModule/Component/MyComponent/sortedMap': {
        'member-property': addSecondItem
    }
};
```

In this code snippet, we plug into the `sortedMap` property to add a new item to the sorted map. The addition of new items is done via the `addItem()` method that is present on all instances of `SortedMap` and `SortedRenderMap`. This method accepts 3 arguments:

* `item`, which is the item that you want to add to the list.
* `key`, which is the unique identifier for the newly added item.
* `position`, which is optional and specifies the position that the item should be inserted at.

The `SECOND_ITEM_POSITION` variable was used to define the position of the new item. Its value is `1500` because of the positions that were assigned to the initial declaration: the first item is always assigned a position of `1000` and it increments by another `1000` for each item. So, in our example that would be:

```javascript
sortedMap = createSortedMap({
    firstItem: 1, // Position is 1000
    thirdItem: 3 // Position is 2000
});
```

In order to add an item in-between two items, a position value has to be between the position values of the surrounding items as well. I chose `1500` just for convenience, but it can be any number between 1001 and 1999 in this case.

{% hint style="info" %}
If the position argument is missing, the item will be assigned to the end of the map.
{% endhint %}

The process of adding a new item is very straightforward. Just make sure that:

* The key is unique to that declaration of `SortedMap`/`SortedRenderMap`
* The member argument is returned at the end

```javascript
member.addItem(2, 'secondItem', SECOND_ITEM_POSITION);
return member;
```

### Removing an item

In order to remove an item from the SortedMap, you will need the key of that item and the removal process looks like this:

```javascript
const removeFirstItem = (member) => {
    const ITEM_KEY = 'firstItem'; 
    member.removeItemBykey(ITEM_KEY);
    return member;
};

export default {
    'MyModule/Component/MyComponent/sortedMap': {
        'member-property': removeFirstItem
    }
};
```

{% hint style="info" %}
Positions of items in the `SortedMap` are **not** affected by this action and remain the same.
{% endhint %}

### Retrieving an item

In order to get an item from the SortedMap, you will need the key of that item and the process looks like this:

```javascript
import { createSortedMap } from '@scandipwa/framework/src/util/SortedMap';

export class MyComponent extends Component {
    sortedMap = createSortedMap({
        firstItem: 1,
        thirdItem: 3
    });
    
    getFirstItem() {
        return this.sortedMap.getItemByKey('firstItem');
    }
}
```

### Retrieving sorted map

At this point, the only thing that's left is to sort the data inside the map and retrieve it! With SortedRenderMap we can even get the array of React Components that are ready to be rendered:

```javascript
export class MyComponent extends Component {
    sortedMap = createSortedMap({
        firstItem: 1,
        thirdItem: 3
    });
    
    sortedRenderMap = createSortedRenderMap({
        firstComponent: this.renderFirstComponent.bind(this),
        thirdComponent: this.renderThirdComponent.bind(this)
    });
    
    getSortedMapValues() {
        return this.sortedMap.getSortedMap();
    }
    
    renderContent() {
        return this.sortedRenderMap.render();
    }
}
```

## Extension

### Why would you extend?

You would want to extend `SortedMap` and `SortedRenderMap` for various reasons:

1. Rendering a new HTML block inside a React Component. For example, to add product recommendations on the product page
2. Adding a new React Context to the root component. For example, to make shop data available to every component in the app
3. Defining a new route for the app. For example, to add the login page under the route `/login`.

### Examples

Learn how to extend `SortedMap` and `SortedRenderMap` with these examples taken directly from the code.

#### Add a new item to SortedRenderMap

```javascript
import { createElement } from 'react';

import ProductCardPrice from '../component/ProductCardPrice';

const addPriceRender = (member) => {
    const PRICE_POSITION = 2500;
    member.addItem(
        () => createElement(ProductCardPrice),
        'productCardPrice',
        PRICE_POSITION
    );

    return member;
};

export default {
    'ShopifyProducts/Component/ProductCard/Component/ProductCardComponent': {
        'member-property': {
            sortedRenderMap: addPriceRender
        }
    }
};
```

#### Add a new route to SortedRenderMap

```javascript
import { createElement, lazy, Suspense } from 'react';
import { Route } from 'react-router';

import ProductFallbackPage from '../component/ProductFallbackPage';

const ProductPage = lazy(() => import('../component/ProductPage'));

const addProductPage = (member) => {
    const PRODUCT_PAGE_POSITION = 5000;

    member.addItem(
        () => createElement(Route, {
            path: [
                '/products/:handle',
                '/collections/:collectionHandle/products/:handle'
            ],
            exact: true,
            render: (props) => (
                <Suspense fallback={ <ProductFallbackPage /> }>
                    <ProductPage { ...props } />
                </Suspense>
            )
        }),
        'routerProductPage',
        PRODUCT_PAGE_POSITION
    );

    return member;
};

export default {
    'Router/Component/Router/Component/RouterComponent': {
        'member-property': {
            _switchRenderMap: addProductPage
        }
    }
};
```

#### Add a Higher-Order Component (HOC) to SortedMap

```javascript
import CustomerProvider from '../context/Customer.provider';

const addCustomerProvider = (member) => {
    const CUSTOMER_POSITION = 2000;

    member.addItem(
        (children) => (
            <CustomerProvider>
                { children }
            </CustomerProvider>
        ),
        'appCustomerProvider',
        CUSTOMER_POSITION
    );

    return member;
};

export default {
    'Framework/Component/App/Component/AppComponent': {
        'member-property': {
            contextProvidersRenderMap: addCustomerProvider
        }
    }
};
```
