
This application note walks through a basic scenario demonstrating how to use the KOS UI SDK to help build user interfaces for KOS-based dispensers.
The goal here is not to provide the specific implementation that should be used for a consumer user interface (CUI) but rather to use this case as a means to introduce some key concepts in the KOS Model Framework and how it can be used to solve common problems. |
In this project we will build out the common use case of showing and selecting a beverage from a cascading list of groups and beverages.
The beverage information will be provided from the backend via a brandset which will also include the assets for the various beverages.
Once completed the user will be able to see the list of beverage groups available:
Selecting one of the groups will drill down into it’s list of beverages:
At the end of this exercise you will be able to demonstrate the following concepts:
Creating and registering a root KOS model.
Creating and registering dependent KOS models
Data retrieval and websocket topic handling
Using KOS model data structures to manage a list of models (one to many)
Using KOS model data structures to manage a list of model references (one to many)
Using KOS React hooks to consume models in React UI Components
Before jumping into code, it’s helpful to consider and design the data model that will be used to drive the user interface. This helps to visualize the relationships and dependencies needed to drive the user experience. Doing this will also help to recognize how and when certain parts of the data model are required relative to the application lifecycle.
As described in the Basic Legacy Dispenser Application Notes there is a data model that provides a dispenser abstraction. For the user interface, that abstraction proves useful for presenting the list of beverages available for selection and pouring. Ultimately, for a consumer user interface (CUI) you will need access to concepts like the list of nozzles and associated brandset pipelines in order to provide appropriate context for making calls to the backend when pouring beverages. This would result in a data model similar to the following:
For the purposes of this exercise, we will leave discussion of the additional parts of the model (Nozzle, Pipelines, Availability, Select and Pour) to future Application Notes and will focus on the initial use case of retrieving and displaying beverage information based on a brandset:
The goal is more to lay out the general shape of the models required and to enure it’s clear how the model framework can be used to reflect the application state for a user interface relative to the backend data model.
All KOS User Interfaces start by initializing the SDK and wrapping the application in a context that provides KOS services to the underlying components.
The KOS SDK provides a KosCoreContextProvider
that should be used to provide an application with access to KOS Core services.
Initialize the KOS Core should be done as close to the top of your React application component tree as possible. Typically, in the App.tsx
or equivalent.
import the KOS initialization module
initialize the KosCoreContextProvider with any models that are required for the application
wrap the application with the KosCoreContextProvider
As a best practice, it is recommended to create a registration.ts file at the source root to capture all of the model registrations. This will provide a single location to better visualize your application data model.
|
Start by creating an empty IKosRegistry
object that will be used to specify the models used in your application. This will initially be empty but we’ll start adding information here
.Empty Registration
import type { IKosRegistry } from "@coca-cola/kos-ui-core";
/* Enum that will capture the various type IDs
* for our application models. This is a convenience
* that will make accessing and creating models easier and
* more consistent */
export enum ModelTypes {
}
export const Registry: IKosRegistry = {
models: {
},
preloadModels: [ModelTypes.Dispenser],
};
The model registration can be made available to the application by using the initKosProvider
utility to initialize a React Context provider and wrapping your application:
// App.tsx
import type { IKosRegistry } from "@coca-cola/kos-ui-core";
import {
ErrorBoundaryWithFallback,
initKosProvider,
} from "@coca-cola/kos-ui-components";
...
// create an empty KOS model registry.
export const Registry: IKosRegistry = {
models: {
},
preloadModels: [],
};
const { KosCoreContextProvider } = initKosProvider(Registry);
function App() {
return (
<ErrorBoundaryWithFallback>
<Suspense fallback={<div>LOADING</div>}>
<KosCoreContextProvider>
// application components goes here
</KosCoreContextProvider>
</Suspense>
</ErrorBoundaryWithFallback>
);
}
A common use case is that models will be initialized with a baseline dataset that is populated from the backend during the init(), load() or activate() lifecycle phases. Once active, all models have the ability to listen for delta events that are published from backend and keep the model data in sync based on these lower-level messages. The KOS framework provides a simple mechanism to allow models to subscribe to backend update events that hooks into the overall model lifecycle.
The Dispenser Model serves as the model root for the application and will be used to load any initial data or dependent models that will be needed by the application. Think of it as the root data store for your application and the starting point for the application lifecycle.
To start with, create a type for your dispenser model that extends from IKosDataModel
that will provide access to all of the model lifecycle hooks and framework services. For now just create a minimal interface that includes an ID.
declare interface IDispenserOptions {
}
declare interface IDispenserModel extends IDispenserOptions, IKosDataModel {
id: string;
}
In this example notice the additional interface IDispenserOptions
, which is empty for now. This type represents the data and properties that can passed in through the constructor to initialize the model. The framework relies on this convention to help make instantiating models in a running system more predictable. This will become more clear as we start working with models.
Next create a model class that implements the IDispenserModel
interface:
import { KosLog, kosModel } from '@coca-cola/kos-ui-core';
import {ModelTypes} from "../../registration";
import { IDispenserModel, IDispenserOptions } from './types';
const log = KosLog.getLogger("dispenser-model");
@kosModel<IDispenserModel, IDispenserOptions>(ModelTypes.Dispenser)
export class DispenserModel implements IDispenserModel {
id: string;
constructor(modelId: string, options: IBeverageOptions ) {
// assign the id from the passed in model id
this.id = modelId;
}
}
There are two important concepts demonstrated even in this smallest of KOS Models:
The DispenserModel
class is decorated with a @kosModel
annotation which will ensure that the model will participate in the application by registering with the KOS model framework, including the model type ID and will initiate the overall model lifecycle.
The constructor for a KOS model takes two parameters, the model instance ID and an options parameter that can be used to hydrate the model with an initial set of data. The second parameter’s type was defined above and ensure a consistent contract when the framework is instantiating new models
We will continue to add to this model as we go through this exercise but for now this provides enough to get started.
Lastly, update the registration.ts
file in the root of the project to add an entry for the new Dispenser to bind the Model type ID to the class so the framework will know which type of model to instantiate when requested:
import type { IKosRegistry } from "@coca-cola/kos-ui-core";
import { DispenserModel} from "./models/dispenser/dispenser-model";
/* Enum that will capture the various type IDs
* for our application models. This is a convenience
* that will make accessing and creating models easier and
* more consistent */
export enum ModelTypes {
// Dispenser model type ID
Dispenser = "dispenser",
}
export const Registry: IKosRegistry = {
models: {
[ModelTypes.Dispenser]: {
class: DispenserModel,
singleton: true,
},
},
preloadModels: [ModelTypes.Dispenser],
};
Notice that in the registration specification, the Dispenser model has been identified as a singleton which ensures that only one instance of this model will ever be created. This setting is enforced by the framework an is useful for cases were you need a single global instance of a model and its state rather than recreating it in multiple locations.
The Dispenser model has also been registered in the preloadModels
section which will hook into the KOS model framework application lifecycle and ensure that the model and any of it’s dependencies (defined later) will be fully loaded, as per the KOS model lifecycle, before indicating the framework is ready for use by the UI.
When hooked into the UI vai the KosCoreDataProvider
(later), the UI will show a loading indicator until all of the preloaded models and their dependencies have had an opportunity to go through their lifecycle until they reach the ready
state.
In this section we highlighted KOS model definition and registration as well as the concept of the root model. |
The KOS framework provides first-class support for the concept of Brandsets which can be provided by the backend to tie together Nozzles, Pipelines, Ingredients and Recipes into single endpoint that allows a UI to dynamically present to a user what beverages are currently available to be poured. The model also allows the UI to direct requests to pour to the appropriate Nozzle without requiring all of the logic to live in the UI.
For the purposes of this exercise, the Brandset Model will contain the list of beverages and beverage groups that are contained within a dispenser brandset.
{
"brands": [
{
"id": "18",
"name": "Dr Pepper",
"beverages": [
1522597,
1582394
]
},
{
"id": "4",
"name": "Barqs",
"beverages": [
1475644
]
}
],
"beverages": [
{
"id": "1522597",
"name": "Dr Pepper",
"icon": "/assets/f9k_DrPepper_Parent_fos.v1.143x143.png"
},
{
"id": "1582394",
"name": "Dr Pepper Cherry",
"icon": "/assets/f9k_DrPepper_Child_fos.v1.143x143.png"
},
{
"id": "1475644",
"name": "Barqs",
"icon": "/assets/f9k_BarqsRootBeer_Parent_fos.v1.143x143.png"
}
]
}
It should be noted that the structure and data returned from the backend can vary depending on the application depending on the complexity of the beverage information (icons, labels, images) as well as nature of the groupings. For that reason, it’s not feasible to provide a "standard" Brandset Model but rather we can demonstrate the general flow and best practice. It’s up to each consumer UI to model the Group and Beverage models depending on their specific needs. |
We will use the load
lifecycle hook in the IKosDataModel
interface to ensure that the brandset data is retrieved.
import {kosModel, KosModelFactory} from "@coca-cola/kos-ui-core";
import {getBrandset} from "./services/brandset-services";
import {kosAction, KosLog, KosModelContainer} from "@coca-cola/kos-ui-core";
import {IKosModelContainer} from "@coca-cola/kos-ui-core";
import {BrandsetResponse, IBrandsetModel} from "./types";
import {IGroupModel, IGroupOptions} from "../group/types";
import {ModelTypes} from "../../constants";
const log = KosLog.getLogger("brandset-model");
@kosModel<IBrandsetModel, {}>(ModelTypes.Brandset)
export class BrandsetModel implements IBrandsetModel {
id: string;
beverages: IKosModelContainer<IBeverageModel>;
groups: IKosModelContainer<IGroupModel>;
constructor(modelId: string, options: IBeverageOptions ) {
// assign the id from the passed in model id
this.id = modelId;
this.beverages = new KosModelContainer();
this.groups = new KosModelContainer();
}
async init(): Promise<void> {
console.log("initialized brandset model");
}
/**
* The load lifecycle method provides an opportunity to hydrate the model with data fetched
* from the backend.
*
*
*/
async load(): Promise<void> {
try {
log.info("loading brandset model");
const response = await getBrandset(this.id);
log.debug(`received response ${response}`);
} catch(e) {
log.error(e);
throw e;
}
}
getChildren() {
return [...this.beverages.data];
}
}
We will further update the load()
method as we introduce Group and Beverage models that can be populated with the data from the brandset response.
We can register the Brandset model with the framework in the same way as the Dispenser Model:
/* Enum that will capture the various type IDs
* for our application models. This is a convenience
* that will make accessing and creating models easier and
* more consistent */
export enum ModelTypes {
// Dispenser model type ID
Dispenser = "dispenser",
Brandset = "brandset"
}
export const Registry: IKosRegistry = {
models: {
[ModelTypes.Dispenser]: {
class: DispenserModel,
singleton: true,
},
[ModelTypes.Brandset]: {
class: BrandsetModel,
singleton: true,
},
},
preloadModels: [ModelTypes.Dispenser],
};
The Brandset Model invokes the brandset data access services to communicate with the backend. The services expose both low-level HTTP/fetch-like operations as well as higher level CRUD-like operations available via the SDK-provided ServiceFactory
.
From an architecture perspective, the service layer is providing an facade over the backend’s WebSocket transport that presents an API based on the browser fetch
capability that will be familiar to frontend developers. The SDK services allow React developers to work with WebSocket services in a way that does not require a deep knowledge of the underlying transport. From a developer perspective, the experience is more like traditional web applications with built in support for request/response interactions provided via an async API.
import { ServiceFactory, resolveServiceUrl } from "@coca-cola/kos-ui-core";
import {BrandsetResponse} from "../types";
const { URL } = resolveServiceUrl("BRANDSET_SERVICE");
const { getOne } = ServiceFactory.build({
destinationAddress: "",
basePath: `${URL}/system/brandset/uiSchema.json`,
});
/**
* @category Service
* Retrieves the initial brandset data.
*/
export const getBrandset = async (id: string) => {
const response = await getOne<BrandsetResponse>({});
return response;
};
You can read more about Service Factories in the Begin Here section |
To wire up the Brandset Model into the dispenser lifecycle we need to declare a dependency in the Dispenser model:
import { kosDependency, KosLog, kosModel } from '@coca-cola/kos-ui-core';
import {ModelTypes} from "../../constants";
import { IBrandsetModel } from '../brandset/types';
import { IDispenserModel, IDispenserOptions } from './types';
const log = KosLog.getLogger("dispenser-model");
@kosModel<IDispenserModel, IDispenserOptions>(ModelTypes.Dispenser)
export class DispenserModel implements IDispenserModel {
id: string;
@kosDependency({modelType: ModelTypes.Brandset})
brandset!: IBrandsetModel
constructor(modelId: string, options: IBeverageOptions ) {
// assign the id from the passed in model id
this.id = modelId;
}
getChildren() {
return [this.brandset]
}
}
The dependency on the brandset is declared via a @kosDependency
decorator on the class member brandset
. What’s happening behind the scenes is retrieving, from the framework model cache, an instance of a registered Model that has a typeID of brandset
which in this case is defined in an enum ModelTypes
. The Brandset is also declared as a singleton so only the modelType is required. Otherwise the id of the dependent model will also need to be passed in via the decorator options.
If an instance with the given ID ( or type for singletons) does not exist in the model cache then the SDK will automatically create a new instance of the model and have it run through it’s lifecycle. If the instance already exists then it will be returned directly. |
As part of the creation process, any declared dependencies will be instantiated and act as a gate before the model can pass through to a new lifecycle phase. So in this case, before the Dispenser is initialized, the framework will ensure that the Brandset model is also initialized. The same holds true for all of the lifecycle phases.
As the SDK guides the model through it’s lifecycle, the model lifecycle hooks are called if they are available. This provides an opportunity for a model to ensure it’s state is loaded and validated before making it available for consumption.
You can learn more about the KOS Model Lifecycle in the Concepts section |
Once initialized, the framework will automatically inject the retrieved instance of the 'BrandsetModel' into the brandset
property on the DispenserModel
and it will be available within model for further reference and mutation.
In this section we learned about the load() lifecycle hook, how to perform data loading and model dependencies.
|
Based on the JSON payload for the brandset describe above, we can now create a model to represent the beverage data and add that to the Brandset model as a collection that can be viewed.
Create the Beverage interface to describe the model state. In this case the name and icon is going to be passed from the JSON payload so it becomes part of the Options interface.
declare interface IBeverageOptions {
name: string;
icon: string;
}
declare interface IBeverageModel extends IBeverageOptions, IKosDataModel {
id: string;
}
The model itself will implement IBeverageModel
which in itself extends IBeverageOptions
and IKosDataModel
.
import {kosModel, KosLog} from "@coca-cola/kos-ui-core";
import {ModelTypes} from "../../constants";
const log = KosLog.getLogger("beverage-model");
@kosModel<IBeverageModel, IBeverageOptions>(ModelTypes.Beverage)
export class BeverageModel implements IBeverageModel {
id: string;
name: string;
icon: string;
constructor(modelId: string, options: IBeverageOptions ) {
// assign the id from the passed in model id
this.id = modelId;
this.name = options.name;
this.icon = options.icon;
}
}
Lastly the Brandset model can be updated to parse and map the the beverage data out of the JSON payload and .Update Brandset Model
import {kosModel} from "@coca-cola/kos-ui-core";
import {getBrandset} from "./services/brandset-services";
import {kosAction, KosLog, KosModelContainer} from "@coca-cola/kos-ui-core";
import {IKosModelContainer, Kos} from "@coca-cola/kos-ui-core";
import {BrandsetResponse, IBrandsetModel} from "./types";
import {ModelTypes} from "../../registration";
const BeverageFactory = Kos.Factory.create<IBeverageModel, IBeverageOptions>(ModelTypes.Beverage); (1)
const log = KosLog.getLogger("brandset-model");
@kosModel<IBrandsetModel, {}>(ModelTypes.Brandset)
export class BrandsetModel implements IBrandsetModel {
id: string;
beverages: IKosModelContainer<IBeverageModel>; (2)
constructor(modelId: string, options: IBeverageOptions ) {
// assign the id from the passed in model id
this.id = modelId;
this.beverages = new KosModelContainer(); (3)
}
async load(): Promise<void> {
try {
log.info("loading brandset model");
const response = await getBrandset(this.id);
log.debug(`received response ${response}`);
kosAction(() => { (4)
const brandset = response?.data;
brandset?.beverages.forEach((beverage) => { (5)
const beverageOptions = {name: beverage.name, icon: beverage.icon, color: beverage.color}
const beverageModel = BeverageFactory(beverage.id)(beverageOptions);
this.beverages.addModel(beverageModel);
})
});
} catch(e) {
log.error(e);
throw e;
}
}
getChildren() {
return [...this.beverages.data]; (6)
}
}
1 | Create a model factory for Beverages. This is just creating a shorthand version of the KosModelFactory that can be used to create Beverage Models |
2 | Create a new beverages member that is a IKosModelContainer to represent a collection of models. It is using generics to indicate that the collection includes IBeverageModel instances. |
3 | Initialize the beverages member to be a KosModleContainer |
4 | Place any model state changes inside an action. |
5 | After loading the data from the data service, iterate and map the beverage data into instances of BeverageModels . |
6 | Indicate that the beverage data has a child containment relationship. |
The Brandset Model, after parsing the incoming data during the loading lifecycle method, will instantiate a number of Group and Beverage models that represent the collection of beverages that can ultimately be poured by the dispenser.
The KOS UI SDK provides a set of data structures that make working with collections of KOS models easier, optimizing for data access and observability. The Brandset Model in particular makes use of the KosModelContainer
class (IKosModelContainer
interface) that will store the Group and Beverage models.
The KosModelContainer
is backed by a Map data structure that allows for random access to the models based on their ID. However, the KosModelContainer
also allows for arbitrary indexes based any model properties allowing for one or more indexes that can optimize retrieval depending on the use case.
The Group Model manages the data and lifecycle for a single instance of a beverage group defined in a brandset. It’s primary purpose is to provide logical groupings of beverages to help manage the combinations flavors and varieties (recipes) available to a user.
In this project, a Group Model is defined with a lightweight interface containing basic information like the id
and name
but in reality might contain significant amount of data retrieved from a CMS or other store including icons, labels and colors that can be used to drive the UI.
Like the other models we need to create the required interfaces and implementations to represent the model.
import type {IKosReferenceContainer} from "@coca-cola/kos-ui-core"
declare interface IGroupOptions {
name: string;
beverageRefs: string[];
}
declare interface IGroupModel extends Omit<IGroupOptions, "beverageRefs">, IKosDataModel { (1)
id: string;
beverages: IKosReferenceContainer<IBeverageModel>; (2)
}
1 | Of note in the interface is how the IGroupModel interface extends IGroupOptions as a convenience. However, the public interface doesn’t need to expose the list of beverage references. Hence the Omit type being used to filter out passed in options that are not needed in the public interface. |
2 | The beverages property will be derived from the passed in beverageRefs option. It’s type is of IKosReferenceContainer indicating that it will be a collection of IBeverageModel classes that are referenced by their model IDs. |
The following implementation of the interface will demonstrate how to capture the beverage references into a usable form.
import {kosModel, KosLog, IKosReferenceContainer, KosReferenceContainer ,kosDependency, KOS_MODEL_ID} from "@coca-cola/kos-ui-core";
import {ModelTypes} from "../../constants";
import {IAvailabilityBeverage} from "../availability/types";
import {IGroupModel, IGroupOptions} from "./types";
const log = KosLog.getLogger("group-model");
@kosModel<IGroupModel, IGroupOptions>(ModelTypes.Brand)
export class GroupModel implements IGroupModel {
id: string;
name: string;
beverages: IKosReferenceContainer<IBeverageModel>;
private _beverageRefs: string[]; (1)
constructor(modelId: string, options: IGroupOptions ) {
// assign the id from the passed in model id
this.id = modelId;
this.name = options.name;
this._beverageRefs = options.beverageRefs;
this.beverages = new KosReferenceContainer(); (2)
}
async init(): Promise<void> { (3)
log.info("initialized group model");
this._beverageRefs.forEach((ref) => {
this.beverages.addModel(ref);
})
}
async ready() { (4)
return this.beverages.whenReady();
}
}
1 | beverageRefs passed in via options are stored in a private member. |
2 | beverages is initialized as an implementation of the IKosReferenceContainer interface: KosReferenceContainer |
3 | the init() lifecycle hook is used to add all of the references passed in via options into the KosReferenceContainer |
4 | the ready() lifecycle hook is used to inform the model to block it’s ready state until all of the references have been resolved and the underlying models are ready. |
The brandset provides an interface that exposes to the UI the relationship between Groups and Beverages. The brandset model contains an entry for each Brand as follows:
{
id: "1",
name: "Coke",
beverages: ["11", "12", "13"]
},
The group specifies the list of Beverages via the beverage ID. Essentially, the Group, Coke in this case, holds a reference to three beverages with IDs 11, 12 and 13.
Rather than requiring the developer to resolve these references by ID and store them in an array or Map, the KOS UI SDK provides a KosReferenceContainer
class (IKosReferenceContainer
interface) that behaves similarly to the KosModelContainer
with the primary difference being that the KosReferenceContainer
will accept a set of model references (IDs) and will automatically resolve the underlying models.
The brandset model need to be updated to map all of the Group data into Group models in much the same way that Beverages were handled previously.
import {kosModel} from "@coca-cola/kos-ui-core";
import {getBrandset} from "./services/brandset-services";
import {kosAction, KosLog, KosModelContainer} from "@coca-cola/kos-ui-core";
import {IKosModelContainer, Kos} from "@coca-cola/kos-ui-core";
import {BrandsetResponse, IBrandsetModel} from "./types";
import {IGroupModel, IGroupOptions} from "../group/types";
import {ModelTypes} from "../../constants";
const BrandFactory = Kos.Factory.create<IGroupModel, IGroupOptions>(ModelTypes.Brand);
const BeverageFactory = Kos.Factory.create<IBeverageModel, IBeverageOptions>(ModelTypes.Beverage);
const log = KosLog.getLogger("brandset-model");
@kosModel<IBrandsetModel, {}>(ModelTypes.Brandset)
export class BrandsetModel implements IBrandsetModel {
id: string;
beverages: IKosModelContainer<IBeverageModel>;
groups: IKosModelContainer<IGroupModel>;
constructor(modelId: string, options: IBeverageOptions ) {
// assign the id from the passed in model id
this.id = modelId;
this.beverages = new KosModelContainer();
this.groups = new KosModelContainer();
}
async load(): Promise<void> {
try {
log.info("loading brandset model");
const response = await getBrandset(this.id);
log.debug(`received response ${response}`);
kosAction(() => {
const brandset = response?.data;
brandset?.beverages.forEach((beverage) => {
const beverageOptions = {name: beverage.name, icon: beverage.icon, color: beverage.color}
const beverageModel = BeverageFactory(beverage.id)(beverageOptions);
this.beverages.addModel(beverageModel);
})
brandset?.brands.forEach((brand) => { (1)
const brandOptions: IGroupOptions = {name: brand.name, beverageRefs: brand.beverages.map((b) => String(b))};
const brandModel = BrandFactory(String(brand.id))(brandOptions);
this.groups.addModel(brandModel);
})
});
} catch(e) {
log.error(e);
throw e;
}
}
getChildren() {
return [...this.beverages.data, ...this.groups.data]; (2)
}
}
1 | Map the incoming brandset data into the Group models by using the BrandFactory |
2 | Add the group models as a child to the brandset to ensure the framework can automatically track the relationship |
import "./App.css";
import {
ErrorBoundaryWithFallback,
initKosProvider,
LoadingMessage,
} from "@coca-cola/kos-ui-components";
import { KosLog } from "@coca-cola/kos-ui-core";
import React, { Suspense } from "react";
import { DispenserRoot } from "./Demo";
import { Registry } from "./registration";
KosLog.setLevel("INFO");
// create an empty KOS model registry.
const { KosCoreContextProvider } = initKosProvider(Registry);
function App() {
return (
<ErrorBoundaryWithFallback>
<Suspense fallback={<LoadingMessage></LoadingMessage>}>
<KosCoreContextProvider>
<DispenserRoot></DispenserRoot>
</KosCoreContextProvider>
</Suspense>
</ErrorBoundaryWithFallback>
);
}
export default App;
import { kosComponent, useKosModel } from "@coca-cola/kos-ui-components";
import { useCallback, useEffect, useState } from "react";
import { BeverageContainer } from "./components/beverage-container";
import { GroupContainer } from "./components/group-container";
import { ModelTypes } from "./constants";
import { IBrandsetModel } from "./models/brandset/types";
type Routes = "brand" | "beverage";
export const DispenserRoot = kosComponent(function Dispenser() {
const [route, setRoute] = useState<Routes>("brand");
const [selectedGroup, setSelectedGroup] = useState("");
const [selectedBeverage, setSelectedBeverage] = useState("");
// This hooks is where models are made available to a component
// the hook will return a status object and a loader component that can
// be used together to expose a loading screen until such time as the model
// is loaded and ready for consumption.
const { KosModelLoader, status } = useKosModel<IBrandsetModel>({
modelId: ModelTypes.Brandset,
});
// handle routing between the beverage and the groups
const goBack = useCallback(() => {
setSelectedGroup("");
setSelectedBeverage("");
setRoute("brand");
}, [setSelectedBeverage, setRoute, setSelectedGroup]);
// logic to handle beverage selection. This will be passed
// into our beverage component
const selectBeverage = useCallback(
(beveregeId: string) => {
setSelectedBeverage(beveregeId);
},
[setSelectedBeverage]
);
// logic to handle group selection. In a real application this
// would normally be handled in a dedicated component with hooks
// into a router of some sort
const selectGroup = useCallback(
(groupId: string) => {
setSelectedGroup(groupId);
setRoute("beverage");
},
[setSelectedGroup, setRoute]
);
return (
<>
<KosModelLoader {...status}>
<div style={{ margin: 10 }}>
{route === "beverage" ? (
<BeverageContainer
groupId={selectedGroup}
goBack={goBack}
selectBeverage={selectBeverage}
selectedBeverage={selectedBeverage}
></BeverageContainer>
) : (
<GroupContainer selectGroup={selectGroup}></GroupContainer>
)}
</div>
x
</KosModelLoader>
</>
);
});
In the above example, the BrandsetModel
is made available to the UI via the useKosModel
hook. In it’s simplest form you need to pass in the model ID that you want to use. In this example, the Brandset
model is a singleton so the typeID can be used as there is only one instance.
The useKosModel
hook returns a several items including:
the actual model requested. This will be undefined until the model is loaded and ready.
a ready
flag that indicates that the model has passed through the ready lifecycle phase. This can be used as part of a guard condition in a ternary operator to decide whether to show content or not.
an error
flag that will only be populated if the loading process encounters an error that results in a thrown exception.
a status
object that includes all of the previous item. This is intended to be used as a parameter that can be passed into the KosModelLoader
component.
a KosModelLoader
that can be used in your component to simplify the logic of waiting for a model to be ready before displaying content.
Using the KosModelLoader
as shown will show a fallback/loading message while waiting for the model to be ready. It will also expose the model via a context to any components that are contained as children so that access to the provided model can be more easily managed.
import { useContextModel } from "@coca-cola/kos-ui-components";
import { IBrandsetModel } from "../models/brandset/types";
import { Group } from "./group";
import { ButtonContainer } from "./button-container";
interface Props {
selectGroup: (id: string) => void;
}
export const GroupContainer: React.FunctionComponent<Props> = ({
selectGroup,
}) => {
// the useContextModel hook will grab the model provided in the `KosModelLoader` component when it's available.
// Because the `KosModelLoader` will only render children once the model is available, you don't need to continually
// build that logic throughout your components.
const model = useContextModel<IBrandsetModel>();
const groupItems =
model?.groups.map((group) => (
<div key={group.id} style={{ margin: 10 }}>
<Group key={group.id} group={group} onSelection={selectGroup}></Group>
</div>
)) || [];
return (
<div>
<ButtonContainer>{groupItems}</ButtonContainer>
</div>
);
};
In this case the groups are being passed directly into a child component that can consume it’s properties directly.
When a group is selected we want to display the list of beverages assigned to that group. The BeverageContainer
will accept the selected groupId as a property and will retrieve the GroupModel
to determine which beverages to display based on the list of references retrieved from the brandset.
import { useKosModel } from "@coca-cola/kos-ui-components";
import { IGroupModel } from "../models/group/types";
import { Beverage } from "./beverage";
import { ButtonContainer } from "./button-container";
interface Props {
groupId: string;
selectBeverage: (id: string) => void;
selectedBeverage: string;
goBack: () => void;
}
export const BeverageContainer: React.FunctionComponent<Props> = ({
groupId,
selectBeverage,
selectedBeverage,
goBack,
}) => {
// fetch the group model whenever the groupId prop changes
const { model } = useKosModel<IGroupModel>({ modelId: groupId });
// create the list of Beverages to display from the group model's beverages collection
// the list will be empty until the model is properly resolved and ready due to the nature
// of the useKosModel hook
const beverageItems =
model?.beverages.map((beverage) => (
<div style={{ margin: 10 }} key={beverage.id}>
<Beverage
key={beverage.id}
beverage={beverage}
onSelection={selectBeverage}
selected={beverage.id === selectedBeverage}
></Beverage>
</div>
)) || [];
return (
<div>
<button onClick={goBack}>Back</button>
<div style={{ margin: 10 }}>
<ButtonContainer>{beverageItems}</ButtonContainer>
</div>
</div>
);
};
You should now be able to wire up a component that will consume the passed in BeverageModel and render an appropriate button:
import { kosComponent } from "@coca-cola/kos-ui-components";
import "./beverage.css";
interface BeverageProps {
beverage: IBeverageModel;
selected: boolean;
onSelection: (id: string) => void;
}
export const Beverage: React.FunctionComponent<BeverageProps> = kosComponent(
function Beverage({ beverage, onSelection, selected }) {
const classes = ["beverage-box"];
if (selected) {
classes.push("selected");
}
return (
<div
data-tetstid={beverage.id}
title={beverage.name}
className={classes.join(" ")}
onClick={() => beverage.available && onSelection(beverage.id)}
>
<img
style={{
objectFit: "fill",
width: "100%",
height: "100%",
zIndex: 2,
}}
src={`http://localhost:8081/system/brandset${beverage.icon}`}
alt={beverage.name}
></img>
</div>
);
}
);
The component can directly use the model properties and
Any component that is consuming a KOS model or it’s properties should be wrapped in the kosComponent higher order component. This will register the component to re-render for any changes to model properties that are directly referenced in the component. Model properties that aren’t directly used by a component will not result in re-renders.
|
In this application note we built out a UI that can display brandset data. Some of the key concepts covered include:
Creating and registering a root KOS model.
Creating and registering dependent KOS models
Data retrieval
Using KOS model data structures to manage a list of models (one to many)
Using KOS model data structures to manage a list of model references (one to many)
Using KOS React hooks to consume models in React UI Components
INFO: For a more complete view of these concepts in a runnable form please refer to the Demo Dispenser repo.