1.29.2.1
Gameface
Custom Components

Gameface supports the creation and usage of custom elements. They provide a way to encapsulate a component specific functionality and reuse it. The Web Components suite is widely used for the creation of components. In Gameface we have a component system similar to the standard web components.

Gameface Component System

The fundamental idea in the Gameface Component System is the same as in the standard Web Components. However there are some key points which make them different.

  • <template> elements are replaced with traditional html tags such as <div>s
  • templates are loaded using file loader or defined as string literals
  • <slot> elements are replaced by custom <component-slot> elements
  • styles are manually imported <link> tags in the <head> of the document

The Gameface Component System has a command line interface that provides a fast and easy way to create and reuse components.

To install the CLI open a console and execute:

npm coherent-guic-cli i -g

The available commands are:

  • coherent-guic-cli create <name> <destination> - Creates a new component with given name at a given destination folder.
  • coherent-guic-cli build - Creates cjs and umd bundles of the component. The bundles are created in a cjs/ and umd/ folders next to the component source.
  • coherent-guic-cli build –watch - Pass –watch to watch for changes to the source and rebuild.
  • coherent-guic-cli build:demo - Create a production build of the demo that bundles the game ui components library, the component and the demo.
  • coherent-guic-cli start:demo - Start a development server which hosts the demo and listens for changes and automatically rebuilds the demo.

The next section goes through the process of creating an Inventory using the custom component system in Gameface.

Creating an Inventory

Inventory.PNG

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 by an image. Clicking on an item with the left mouse button would open a details panel for the selected element. 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 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
| |
| \---umd
| consumable.development.js
| consumable.production.min.js
|
+---images
|
+---inventory
| | index.js
| | package.json
| | README.md
| | script.js
| | style.css
| | template.html
| |
| \---umd
| inventory.development.js
| inventory.production.min.js
|
+---inventory-item
| | index.js
| | inventoryItem.js
| | package.json
| | README.md
| | script.js
| | style.css
| | template.html
| |
| \---umd
| inventory-item.development.js
| inventory-item.production.min.js
|
+---node_modules
|
\---weapon
| index.js
| package.json
| README.md
| script.js
| style.css
| template.html
|
+---umd
| 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 is 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/umd/components.development.js"></script>
<script src="./node_modules/coherent-gameface-modal/umd/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 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;
}
connectedCallback() {
// loads the template and replaces all
// slots with slottable elements if any
components.loadResource(this)
.then((result) => {
// update the template to the one with
// slotted elements
this.template = result.template;
components.renderOnce(this);
// setup state, content etc...
// synchronize models here if you use
// Gameface's data binding
})
.catch(err => console.error(err));
}
}
components.defineCustomElement('my-component', MyComponent);

It's recommended that the constructor is used to setup 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 a the Inventory component open a console and execute:

coherent-guic-cli create Inventory

This will create an Inventory folder with the following content:

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

For simplicity we'll remove the demo folder 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 a basic content:

import components from 'coherent-gameface-components';
import template from './template.html';
class Inventory extends HTMLElement {
constructor() {
super();
this.template = template;
this.url = '/components/Inventory/template.html';
}
connectedCallback() {
components.loadResource(this)
.then((result) => {
this.template = result.template;
components.renderOnce(this);
})
.catch(err => console.error(err));
}
}
components.defineCustomElement('Inventory-Component', Inventory);
export default Inventory;

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="Inventory">
<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 connectedCallback function. The CLI has generated the template loading part for us. We need 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 a onTemplateLoaded function that we'll call in the connectedCallback:

connectedCallback() {
components.loadResource(this)
.then((result) => {
this.template = result.template;
components.renderOnce(this);
this.onTemplateLoaded();
})
.catch(err => console.error(err));
}
/**
* 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 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 automatic rebuild of the bundles.

The build command will generate CommonJS(cjs) and Universal Module Definition(umd) builds:

Inventory
+---cjs
| inventory.development.js
| inventory.production.min.js
|
\---umd
inventory.development.js
inventory.production.min.js

Both builds contain development and minified production builds. We'll import the UMD development build to the index.html file.

Place this after the import of the components library;

<script src="./inventory/umd/inventory.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 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="./inventory-item/umd/inventory-item.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 setup the content 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 HTMLElement {
constructor() {
super();
this.onClickBound = (e) => {
if (e.button === 2) return this.equip();
// on click is assigned in InventoryItem
this.onClick();
};
this.template = template;
}
connectedCallback() {
components.loadResource(this)
.then((result) => {
this.template = result.template;
components.renderOnce(this);
this.setupContent();
this.addEventListener('mousedown', this.onClickBound);
engine.synchronizeModels();
})
.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 how the template looks like:

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

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

<script src="./weapon/umd/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 equip 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 HTMLElement {
constructor() {
super();
this.template = template;
this.onClickBound = (e) => {
if(InventoryItems.list[this.itemid].quantity === 0) return;
// right mouse button
if(e.button === 2) return this.use();
this.onClick();
};
}
connectedCallback() {
components.loadResource(this)
.then((result) => {
this.template = result.template;
components.renderOnce(this);
this.setupContent();
this.addEventListener('mousedown', this.onClickBound);
engine.synchronizeModels();
})
.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/umd/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 it 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>