npm i -s @ucd-lib/cork-app-utils
The BaseModel exposes the event bus and has helper methods for registering with the global model registry. This global object is used by Element interface mixins to inject the models into elements.
- eventBus : EventBus
- register(name: String)
- Register the model
- emit(event: String, payload: Object)
- Emit event to event bus
import {BaseModel} from '@ucd-lib/cork-app-utils';
import ExampleStore from '../stores/ExampleStore';
import ExampleService from '../service/ExampleService');
class ExampleModel extends BaseModel {
constructor() {
super();
this.store = ExampleStore;
this.service = ExampleService;
this.register('ExampleModel');
// If you want to add other models
// this.inject('AnotherModel');
// Make sure in your main index.js file to call
// import {Registry} from '@ucd-lib/cork-app-utils';
// // import models
// Registry.ready();
}
/**
* @method get
* @description In this example we will fetch some data
* using our service. The service may return some
* data already cached in the store. The returned
* data will be in a State Payload Wrapper.
*
* @returns {Promise}
*/
async get(id) {
// get current state
let state = this.store.get(id);
// wait for the service to do it's thing
// if you are interested in the loading events
// you will want to set event listeners
try {
// check to see if we are already loading this resource
if( state.state === 'loading' ) {
// make sure you store the request promise in the store as 'request'
// then you can await here on it as a semaphore, preventing duplicate
// requests if multiple UI elements request a resource at the same time
await state.request;
} else {
// the return from the service is the network respons
// not the state response which is stored in the store
await this.service.get(id);
}
} catch(e) {
// handle network error here if you want to handle in model
// but error state should be capture by store and UI elements
// should react to the error state event
}
// regardless of the network success/failure of request above
// return the current state
return this.store.get(id);
}
/**
* @method set
* @description Update some app state
*/
set(data) {
this.store.update(data)
}
}
const exampleModel = new ExampleModel();
export default exampleModel;
Here is a sample index.js
file for the ./src
folder. Where ./src
contains ./src/models
, ./src/services
, ./src/stores
, etc.
index.js:
import {Registry} from '@ucd-lib/cork-app-utils';
import ExampleModel from './models/ExampleModel.js';
import AnotherModel from './models/AnotherModel.js';
// This handles inject of other models into models, avoiding
// cyclical dependency issues
Registry.ready();
// then if you want to expose
// export {ExampleModel, AnotherModel};
// or
// if( typeof window !== 'undefined' ) {
// window.APP_MODELS = {ExampleModel, AnotherModel};
// }
The BaseService exposes helper functions to call rest services
- eventBus : EventBus
- request(options: Object)
- Make a fetch request
import {BaseService} from '@ucd-lib/cork-app-utils');
import ExampleStore from '../stores/ExampleStore');
class ExampleService extends BaseService {
constructor() {
super();
this.store = ExampleStore;
}
async get(id) {
return this.request({
url : `/api/get/${id}`,
// optional
// if the state is 'loading' and another request for this object
// comes in, both requests will wait on the same promise preventing
// two network requests for the same object
checkCached : () => this.store.data.byId[id],
// request is a promise to resolves when network request finished (success or failure)
onLoading : request => this.store.setLoading(id, request),
onLoad : result => this.store.setLoaded(id, result.body),
onError : e => this.store.setError(id, e)
});
}
}
const exampleService = new ExampleService();
export default exampleService;
The ServiceModel exposes helper functions to call rest services
- eventBus : EventBus
- request(options: Object)
- Make a fetch request
import {BaseStore, LruStore} from '@ucd-lib/cork-app-utils';
class ExampleStore extends BaseStore {
constructor() {
super();
this.data = {
byId : new LruStore({name: 'by-id'})
}
this.events = {
EXAMPLE_UPDATE = 'example-update'
}
}
// request is the network request promise
// always store this so we can wait on it if a second
// entity requests this same resource
setLoading(id, request) {
this._setState({
state: this.STATE.LOADING,
id, request
});
}
setLoaded(id, payload) {
this._setState({
state: this.STATE.LOADED,
id, payload
});
}
setError(id, error) {
this._setState({
state: this.STATE.ERROR,
id, error
});
}
_setState(id, state) {
// optionally check that state has actually changed
// this is helpful to prevent multiple events of same state that sometimes occur
// if( !this.stateChanged(this.data.byId[id], state) ) return;
this.data.byId[id] = state
this.emit(this.events.EXAMPLE_UPDATE, state);
}
}
const exampleStore = ExampleStore();
export default exampleStore;
The LruStore is a simple LRU cache store. It can used in the BaseStore example above. It is a simple key value store with a max size. When the max size is reached, the least recently used key is removed. The timestamp of a key is updated when the key is set or accessed.
{
// name of the store. This is used when logging.
name : String,
// max number of items to store. defaults to 50
maxSize : Number
}
- get(key: String)
- Get value for key
- set(key: String, value: Any)
- Set value for key
Global instance of EventEmitter class.
import { LitElement } from 'lit-element';
import render from "./my-element.tpl.js"
import {Mixin, LitCorkUtils} from "@ucd-lib/cork-app-utils";
export default class MyElement extends Mixin(LitElement)
.with(LitCorkUtils) {
constructor() {
super();
this.render = render.bind(this);
// if you want a custom logger name call this in the constructor
// this._initLogger('foo');
// this.logger is always available and defaults to the element name
// inject the ExampleModel. Will be available at this.ExampleModel
this._injectModel('ExampleModel');
}
showData(id) {
let data = await this.ExampleModel.get('someId');
// you can do stuff with
}
// LitCorkUtils will automatically wire up this method
// to the example-update event.
_onExampleUpdate(e) {
if( e.state === 'loading' ) {
} else if( e.state === 'loaded' ) {
} else if( e.state === 'error' ) {
}
}
_setExample() {
this.ExampleModel.set({
my : 'new state'
});
}
}
customElements.define('my-element', MyElement);
By default all elements using LitCorkUtils
will have access to a logger. The logger provides methods for different log levels; debug
, info
, warn
and error
:
export default class MyElement extends Mixin(LitElement)
.with(LitCorkUtils) {
constructor() {
super();
this.logger.debug('i am firing from the constructor!')
}
}
The logger library provides two functions:
getLogger(name)
takes a name argument and returns a logger with that name. If a logger does not already exist by that name, a new one is created.setLoggerConfig
sets the default config for all loggers.
The logger's config can have the following properties:
{
// default log level for all loggers. If not provided, they
// will default to 'info'.
logLevel : String,
// individual logger log level override.
// ex: to override logger 'example' to 'debug', set :
// config.logLevels.example = 'debug';
logLevels : Object,
// if true, the logger will not include caller method and source link in log messages
disableCallerInfo : Boolean,
// report errors (both uncaught exceptions on the window and logger.error calls) to a provided url endpoint.
reportErrors : {
// must be set to true to report
enabled : Boolean,
// urls to call
url : String,
// HTTP Method to use. Defaults to POST
method : String,
// key to send as `x-api-key` header
key : String,
// custom headers to send in request
headers : {},
// full url or full path to the bundles source map file
// this is used to map stack traces to the original source
sourceMapUrl : String,
// extension of the source map file. Example to '.map'
// when set the source map url will be the url of the error
// plus this extension
sourceMapExtension : String,
// custom attributes to send in request body
customAttributes : {}
}
}
There are a couple ways to setup the config.
- Manually call
import {setLoggerConfig} from '@ucd-lib/cork-app-utils
. Then callsetLoggerConfig(config)
; - Set
window.LOGGER_CONFIG_VAR
and then make sure your config is located atwindow[window.LOGGER_CONFIG_VAR]
. - Set
window.APP_CONFIG.logger
.
You can update config in realtime by setting window variables or url query params.
- logLevel:
?loglevel=debug
window.logLevel = 'debug'
- logLevels:
?loglevels=example:debug,my-element:info
window.logLevels.example = 'debug';
- disableCallerInfo:
?disableCallerInfo=true
window.disableLoggerCallerInfo = true;
The logger can report; uncaught errors, unhandled promises and logger.error() method calls by setting reportErrors.enabled
to true
and provided a url
to post to. For more information about a client error reporting service see: https://github.com/ucd-library/client-error-reporter.
By default, the error reporter will POST to the provided url with the following headers and body:
headers:
Content-Type: application/json
x-api-key: [key provided in config]
body:
{
// logger name
name : 'String',
// error object if uncaught error/promise, or arguments array if call to logger.error().
error : 'Object|Array',
// current window.location.pathname
pathname : 'String'
// current window.location.search (query params as string)
search : 'String'
}
Install cork-app-utils globally
npm i -g @ucd-lib/cork-app-utils
The command line utility has two main helpers:
In the src or lib directory or your client code, run:
cork-app-utils model example
It will create the following files (and folders if they do not exist):
- lib/models/ExampleModel.js
- lib/services/ExampleService.js
- lib/stores/ExampleStore.js
With boiler plate wiring code already in place.
In the directory you wish to create a new element run:
cork-app-utils lit my-element
This will create the following files:
- my-element.js
- my-element.tpl.js
Where the .js file contains the main element definition and .tpl.js contains the template tag. There is nothing specific to cork-app-utils in the Lit template other than the way we like to split the elements into two files. To bind a Lit Element to a stores events, see the Lit Element section in Wiring to UI.