Creating an Inventory

In this example we will create an inventory using the Gameface components library and the modal component.

The inventory sample shows how to create, style, nest and reuse custom components. The inventory will hold a list of inventory items - weapons and consumables. Each item will be presented with an image. Clicking on an item with the left mouse button would open a details panel for the selected element. The right mouse button will equip/use weapon/consumable respectively. The whole source can be found in Samples/uiresources/Components. The assets are also located there. This is the file structure of the inventory sample.

We’ll create the following components:

  1. Inventory - will hold all items
  2. InventoryItem - will support mouse interaction and will handle the display of the details window
  3. Weapon - a type of InventoryItem that has an image and a name
  4. Consumable - a type of InventoryItem that will be usable and will have a quantity

File structure

We’ll generate most of the files using the CLI. This is how your project should look at the end of this guide:

Components:
|   cohtml.js
|   index.html
|   model.js
|   package-lock.json
|   package.json
|   styles.css
|
+---consumable
|   |   index.js
|   |   package.json
|   |   README.md
|   |   script.js
|   |   style.css
|   |   template.html
|   |
|   \---dist
|           consumable.development.js
|           consumable.production.min.js
|
+---images
|
+---inventory
|   |   index.js
|   |   package.json
|   |   README.md
|   |   script.js
|   |   style.css
|   |   template.html
|   |
|   \---dist
|           inventory.development.js
|           inventory.production.min.js
|
+---inventory-item
|   |   index.js
|   |   inventoryItem.js
|   |   package.json
|   |   README.md
|   |   script.js
|   |   style.css
|   |   template.html
|   |
|   \---dist
|           inventory-item.development.js
|           inventory-item.production.min.js
|
+---node_modules
|
\---weapon
    |   index.js
    |   package.json
    |   README.md
    |   script.js
    |   style.css
    |   template.html
    |
    +---dist
    |       weapon.development.js
    |       weapon.production.min.js
    |

We strongly recommend a domain-driven file structure. It is more maintainable and it scales better in big applications. If we group the files by nature - for example, we put all components in one folder, all styles are another we’ll end up with huge folders and we’ll have to add a file into each folder every time we want to add a new component.

Main files

The main file of the sample is index.html. It includes all components and styles. Before we start we need to install the dependencies. We’ll use the Gameface components library and the modal component. To install them execute the following command in a terminal:

npm i coherent-gameface-components
npm i coherent-gameface-modal

For now, the index.html file should contain only the elements that will hold the UI and a button that we know will show the inventory. It should import the components library, the modal component, the data binding model and cohtml.js:

<head>
    <script src="./cohtml.js"></script>
    <script src="./model.js"></script>
    <script src="./node_modules/coherent-gameface-components/dist/components.development.js"></script>
    <script src="./node_modules/coherent-gameface-modal/dist/modal.development.js"></script>
</head>
<body>
    <div id ="toggle_inventory" class="button">Open Inventory</div>
    <div class="ui">
        <div id="inventory-wrapper"></div>
        <div id="details-container"></div>
    </div>
</body>

The model.js contains a data binding model which holds the inventory items.

engine.createJSModel('InventoryItems', {
    list: {
        'icon_Axe_1_Small': {
            typeId: 0,
            name: 'Axe 1',
            image: 'images/icon_Axe_1_Small.png'
        },
        'icon_HealthPotion_Small': {
            typeId: 1,
            quantity: 10,
            name: 'HealthPotion',
            image: 'images/icon_HealthPotion_Small.png'
        }
    }
});

The styles.css file will hold styles that are shared across the whole application.

The Components Library

The coherent-gameface-components library provides the methods needed to create a new custom component:

  • whenDefined - returns a Promise that resolves when the named element is defined.
  • defineCustomElement - defines a custom component.
  • loadResource - loads the template.

It exposes the components library to the global object. You can use the ES import statement to import it if you don’t want to use it as a global.

This is how to define the JavaScript functionality of a custom element:

import components from 'coherent-gameface-components';
import template from './template.html';

class MyComponent extends HTMLElement {
    constructor() {
        super();
        this.template = template;
    }

    init(data) {
        // update the template to the one with
        // slotted elements
        this.setupTemplate(data, () => {
            components.renderOnce(this);
            // setup state, content etc...
            // synchronize models here if you use
            // Gameface's data binding
        });
    }

    connectedCallback() {
        // loads the template and replaces all
        // slots with slottable elements if any
        components.loadResource(this)
            .then(this.init)
            .catch(err => console.error(err));
    }
}
components.defineCustomElement('my-component', MyComponent);

It’s recommended that the constructor is used to set up the initial state, event handlers or the template. You shouldn’t inspect the element’s attributes and children as they might not be available yet. Access them in the connectedCallback.

The loadResource function will load the template of the component, will find all slots and will put all “slottable” elements in their respective slots.

Creating The Inventory Component

To create an Inventory component, open a console and execute:

coherent-guic-cli create game-inventory

This will create an Inventory folder with the following content:

|   index.js
|   package.json
|   README.md
|   script.js
|   style.css
|   coherent-gameface-components-theme.css
|   template.html
|   index.html
|   demo.js
|

For simplicity we’ll remove files for the demo as we won’t create a demo of the components that we’ll create during this guide:

|   index.js
|   package.json
|   README.md
|   script.js
|   style.css
|   template.html
|

The script.js file is the main source file of the component. The coherent-guic-cli creates basic content:

import { Components } from 'coherent-gameface-components';
import template from './template.html';

const components = new Components();
const BaseComponent = components.BaseComponent;

/**
 * Class description
 */
class GameInventory extends BaseComponent {
    /* eslint-disable require-jsdoc */
    constructor() {
        super();
        this.template = template;
        this.init = this.init.bind(this);
    }

    init(data) {
        this.setupTemplate(data, () => {
            components.renderOnce(this);
            // attach event handlers here
        });
    }

    connectedCallback() {
        components.loadResource(this)
            .then(this.init)
            .catch(err => console.error(err));
    }
    /* eslint-enable require-jsdoc */
}
components.defineCustomElement('game-inventory', GameInventory);
export default GameInventory;

The Inventory has two states - open and close. We’ll use a boolean value to represent that. Add this to the constructor:

this.state = {
    display: false
};

Let’s check the template file. The CLI has generated a div element with a slot and a simple text in a span:

<div class="GameInventory">
    <span>Hello </span>
    <component-slot data-name="name">there!</component-slot>
</div>

We won’t need any of these, so let’s replace it with a container that has an info box and a grid of slots where the item will be held:

<div class="inventory-container">
    <div class="info">
        <span>Left click on an item to show details.</span>
        <span>Right click on an item to use/equip.</span>
    </div>
    <div class="slots">
        <div class="slot"></div>
        <div class="slot"></div>
        <div class="slot"></div>
        <div class="slot"></div>
    </div>
</div>

Put as many div.slot as you would like your inventory to have. We’ll use 24.

Now that we have the correct template, let’s load it in the component. Open script.js and find the generated init function. The CLI has generated the template loading part for us. We need to add the items to the inventory. The inventory might be out of space, so we’ll also need functionality that finds free slots. Let’s create an onTemplateLoaded function that we’ll call in the init:

init(data) {
    this.setupTemplate(data, () => {
        components.renderOnce(this);
        // attach event handlers here
        this.onTemplateLoaded();
    });
}

/**
* Called when the component's template was loaded.
* @param {Array<string>} response the URL and the text of the template.
*/
onTemplateLoaded() {
    // inventory itemSlots, not to be confused with Web Component <slot>
    this.itemSlots = new Array(this.getElementsByClassName('slot').length);
    this.addInventoryItems();
    this.style.display = this.state.display ? '' : 'none';
}

These are the rest of the methods that the Inventory component needs:

  • addInventoryItems - adds the items from the InventoryItems model to the Inventory
  • addInventoryItem - adds a single item
  • addItemAt - adds an item at a specific slot
  • findFreeSocketId - finds the id of a socket that has no item in it
  • isSocketFree - checks if a socket has an item in it
  • show - shows the inventory
/**
* Called when the component's template was loaded.
* @param {Array<string>} response the URL and the text of the template.
*/
onTemplateLoaded() {
    // inventory itemSlots, not to be confused with Web Component <slot>
    this.itemSlots = new Array(this.getElementsByClassName('slot').length);
    this.addInventoryItems();
    this.style.display = this.state.display ? '' : 'none';
}

/**
* Adds the inventory items to the inventory.
* InventoryItems is the data binding model registered in the global
* namespace by Gameface.
*/
addInventoryItems() {
    const itemsIds = Object.keys(InventoryItems.list);

    for (let itemId of itemsIds) {
        this.addInventoryItem(InventoryItems.list[itemId], itemId);
    }
}

/**
* Creates an inventory item instance and adds it into an available slot
* in the inventory.
* @param {Object} item - the inventory item from the model (InventoryItems)
* @param {string} itemId - the item's identifier
* @param {number} [socketId=0] - the inventory socket's id into which the
* item should added. The default is 0 - this will add it in the next free socket.
*/
addInventoryItem(item, itemId, socketId = 0) {
    let WrappedComponent = 'inventory-consumable';
    if (item.typeId === 0) WrappedComponent = 'inventory-weapon';

    const inventoryItem = document.createElement('inventory-item');
    inventoryItem.socket = socketId;
    inventoryItem.itemid = itemId;
    inventoryItem.imageurl = `{{InventoryItems.list.${itemId}.image}}`;
    inventoryItem.description = item.name;
    inventoryItem.WrappedComponent = WrappedComponent;
    this.addItemAt(inventoryItem, socketId);
}

/**
* Adds an inventory item instance to a given inventory socket.
* @param {Object} item - the inventory item from the model (InventoryItems)
* @param {number} socketId - the inventory socket's id into which the
* item should added
*/
addItemAt(item, socketId) {
    const itemSlotElements = this.getElementsByClassName('slot');

    if (!this.isSocketFree(socketId)) socketId = this.findFreeSocketId();
    if (socketId === undefined) return showMessage(`I can't cary anymore!`);

    this.itemSlots[socketId] = socketId;
    itemSlotElements[socketId].appendChild(item);
}

/**
* Finds the first free inventory socket.
* @returns {number} - the id of the socket.
*/
findFreeSocketId() {
    for (let i = 0; i < this.itemSlots.length; i++) {
        if (this.itemSlots[i] === undefined) return i;
    }
}

/**
* Checks if an inventory socket with a given id is free.
* @returns {boolean} - true if it's free, false if it's not
*/
isSocketFree(socketId) {
    return this.itemSlots[socketId] === undefined;
}

/**
* Show the inventory instance.
*/
show() {
    this.state.display = true;
    this.style.display = '';
}

The Inventory component is an ES6 module. It uses import and export statements. Such modules usually must be imported using the script type module, but we can use the CLI to build the component a create a bundle that can be imported as an ES5 IIFE.

To build the component navigate to the Inventory folder and execute:

coherent-guic-cli build

You can use coherent-guic-cli build --watch for the automatic rebuild of the bundles.

The build command will generate the following:

GameInventory
+---dist
|       GameInventory.development.js
|       GameInventory.production.min.js
|

We’ll import the development build to the index.html file.

Place this after the import of the components library;

<script src="./GameInventory/dist/GameInventory.development.js"></script>

The Inventory Item Component

Type coherent-guic-cli create InventoryItem to create a new component. As we mentioned earlier the inventory item will have a details panel that will be opened on click. Only consumable items will have quantity. The weapon and consumable will be different types of components that will be wrapped in an InventoryItem in order to share functionality. The InventoryItem will populate itself with either a weapon or a consumable depending on the type of the current item.

The setup function will create the wrapped component and will append it to the InventoryItem. Call it in the connected callback:

connectedCallback() {
    this.classList.add('inventory-item');
    this.details = document.getElementById('details-container');

    this.setup();
}

setup() {
    const wrappedComponent = document.createElement(this.WrappedComponent);
    wrappedComponent.itemid = this.itemid;
    wrappedComponent.imageurl = this.imageurl;
    wrappedComponent.description = this.description;
    wrappedComponent.onClick = this.onClick;
    this.appendChild(wrappedComponent);
}

We’ll use the Modal component to show the details. Check the Modal documentation to find more on how to use and customize the modal. Basically, it provides three customizable slots - header, body and footer. For the item details, we need the text in the header, the image and the description of the item in the body and nothing in the footer. We create these elements dynamically through JavaScript and we append them to a Gameface-modal element:

constructor() {
    super();
    this.onClick = () => this.showDetailsModal();
}

// connectedCallback and
// setup
// that we added earlier

/**
* Creates the elements which will be added to slots and nests them into a
* wrapper element.
* @returns {HTMLElement} content
*/
createDetailsSlots() {
    const content = document.createElement('div');
    const body = document.createElement('div');
    const header = document.createElement('div');
    const footer = document.createElement('div');

    body.setAttribute('slot', 'body');
    header.setAttribute('slot', 'header');
    footer.setAttribute('slot', 'footer');

    const headerContent = document.createElement('div');
    headerContent.textContent = 'Item Details';
    headerContent.className = 'item-details';
    header.appendChild(headerContent);

    const imageItem = document.createElement('div');
    imageItem.style.backgroundImage = `url(${InventoryItems.list[this.itemid].image})`;
    imageItem.classList.add('info-image');
    body.appendChild(imageItem);

    const description = document.createElement('div');
    description.textContent = this.description;
    body.appendChild(description);

    content.appendChild(body);
    content.appendChild(header);
    content.appendChild(footer);

    return content;
}

/**
* Creates a modal window component and passes the elements created in
* createDetailsSlots to be put in the modal's slots.
*/
showDetailsModal() {
    const details = document.createElement('gameface-modal');
    details.appendChild(this.createDetailsSlots());

    const detailsContainer = document.getElementById('details-container');
    detailsContainer.innerHTML = '';
    detailsContainer.appendChild(details);
    document.querySelector('gameface-modal').style.display = 'flex';
}

Run coherent-guic-cli build and include the umd bundle to the main index.html:

<script src="./InventoryItem/dist/InventoryItem.development.js"></script>

The Weapon Component

Type coherent-guic-cli create Weapon to create a new component. The weapon component is going to have logic for setting up the content - the image and the description and a method for equipping. We set up the content and attach the event listeners in the connectedCallback. We also call engine.synchronizeModels() to make sure that the data binding attributes are updated.

class Weapon extends BaseComponent {
    constructor() {
        super();
        this.onClickBound = (e) => {
            if (e.button === 2) return this.equip();
            // on click is assigned in InventoryItem
            this.onClick();
        };
        this.template = template;
        this.init = this.init.bind(this);
    }

    init(data) {
        this.setupTemplate(data, () => {
            components.renderOnce(this);
            // attach event handlers here
            this.setupContent();
            this.addEventListener('mousedown', this.onClickBound);
            engine.synchronizeModels();
        });
    }

    connectedCallback() {
        components.loadResource(this)
            .then(this.init)
            .catch(err => console.error(err));
    }

    /**
    * Sets the html content of the consumable item and sets the data binding attributes.
    */
    setupContent() {
        this.classList.add('inventory-weapon');
        this.querySelector('.image')
            .setAttribute('data-bind-style-background-image-url', this.imageurl);
        this.querySelector('.weapon')
            .setAttribute('data-bind-class-toggle', `disabled:{{InventoryItems.list.${this.itemid}.equipped}} == true`);
    }

    /**
    * Equips the weapon by setting its equipped property to true.
    */
    equip() {
        if (InventoryItems.list[this.itemid].equipped) return;

        InventoryItems.list[this.itemid].equipped = true;
        engine.updateWholeModel(InventoryItems);
        engine.synchronizeModels();
    }
}

And this is what the template looks like:

<div class="weapon">
    <div class="image"></div>
    <div class="description"></div>
</div>

Run coherent-guic-cli build and include the bundle to the main index.html:

<script src="./Weapon/dist/Weapon.development.js"></script>

The Consumable Component

Type coherent-guic-cli create Consumable to create a new component. The consumable is also an item, but instead of equipping it as right mouse button interaction, it has use(consume). Its template is also a little different than the weapon’s component as the consumable has to have a quantity info badge.

class Consumable extends BaseComponent {
    constructor() {
        super();
        this.template = template;
        this.init = this.init.bind(this);
        this.onClickBound = (e) => {
            if (InventoryItems.list[this.itemid].quantity === 0) return;
            // right mouse button
            if (e.button === 2) return this.use();
            this.onClick();
        };
    }

    init(data) {
        this.setupTemplate(data, () => {
            components.renderOnce(this);
            // attach event handlers here
            this.setupContent();
            this.addEventListener('mousedown', this.onClickBound);
            engine.synchronizeModels();
        });
    }

    connectedCallback() {
        components.loadResource(this)
            .then(this.init)
            .catch(err => console.error(err));
    }

    /**
    * Sets the html content of the consumable item and sets the data binding attributes.
    */
    setupContent() {
        this.classList.add('inventory-consumable');

        this.querySelector('.image').setAttribute('data-bind-style-background-image-url', this.imageurl);
        this.querySelector('.consumable').setAttribute('data-bind-class-toggle', `disabled:{{InventoryItems.list.${this.itemid}.quantity}} == 0`);

        this.querySelector('.quantity').setAttribute('data-bind-value', `{{InventoryItems.list.${this.itemid}.quantity}}`);
    }

    /**
    * Uses one of the consumable items by decreasing its quantity by 1.
    */
    use() {
        InventoryItems.list[this.itemid].quantity -= 1;
        engine.updateWholeModel(InventoryItems);
        engine.synchronizeModels();
    }
}

And the template:

<div class="consumable">
    <div class="image"></div>
    <div class="quantity"></div>
</div>

Run coherent-guic-cli build and include the umd bundle to the main index.html:

<script src="./Consumable/dist/Consumable.development.js"></script>

Using Styles

The coherent-guic-cli creates a style.css file inside each component folder. Put all component-related styles in its style.css file and include them in the main index.html file:

<link rel="stylesheet" href="./node_modules/coherent-gameface-modal/components-theme.css">
<link rel="stylesheet" href="./node_modules/coherent-gameface-modal/style.css">
<link rel="stylesheet" href="./consumable/style.css">
<link rel="stylesheet" href="./inventory/style.css">
<link rel="stylesheet" href="./weapon/style.css">
<link rel="stylesheet" href="./styles.css">

Using slots

As we’ve already mentioned that the <slot> elements are replaced custom elements - <component-slot>. It works via dataset properties. A target slot’s content will be replaced with a source slot’s content if the slot attribute of the source is the same as the data-name of the target. The components library does this automatically.

For example, if a component (let’s call it modal) has three slots defined like this:

template.html:

<div class="modal-wrapper">
    <div class="header">
        <component-slot data-name="header">Put your title here.</component-slot>
    </div>
    <div class="body">
        <component-slot data-name="body">Put the content here.</component-slot>
    </div>
    <div class="footer">
        <component-slot data-name="footer">Put your actions here.</component-slot>
    </div>
</div>

It can be used like this:

<gameface-modal>
    <div slot="header">
        Character name selection
    </div>
    <div slot="body">
        <div class="confirmation-text">Are you sure you want to save this name?</div>
    </div>
    <div slot="footer">
        <div class="actions">
            <button id="confirm" class="close modal-button confirm controls">Yes</button>
            <button class="close modal-button discard controls">No</button>
        </div>
    </div>
</gameface-modal>

And the final result when all elements get slotted will look like this:

<gameface-modal>
    <div class="modal-wrapper">
        <div slot="header">
            Character name selection
        </div>
        <div slot="body">
            <div class="confirmation-text">Are you sure you want to save this name?</div>
        </div>
        <div slot="footer">
            <div class="actions">
                <button id="confirm" class="close modal-button confirm controls">Yes</button>
                <button class="close modal-button discard controls">No</button>
            </div>
        </div>
    </div>
</gameface-modal>