@bagawork/framework
On this page you find the documentation for the npm package @bagawork/framework
(link to npm).
Introduction
The npm package @bagawork/framework
contains the various framework classes, components and functions that is needed to run a BagaWork app. Checkout the Documentation pages to learn how most of these works (that page only contains the documentation for the public things, the package also contains some things that are only used internally).
Quick start
npm install @bagawork/framework
import { App, Button, createPageCreator, Page, Text, } from '@bagawork/framework' export function createApp({a, p}){ class MyApp extends App{ createStartPage(){ return StartPage } } const StartPage = createPageCreator(`StartPage`, class extends Page{ createGui(){ return Button.text(`View greeting`).page(GreetingPage) } }) const GreetingPage = createPageCreator(`GreetingPage`, class extends Page{ createGui(){ return Text.text(`Hello, world!`) } }) return { App: MyApp, Pages: { StartPage, GreetingPage, }, } }
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>My App</title> <style> #screen{ width: 100px; height: 300px; } </style> <script type="module"> // The file exporting your createApp() function, or write the // createApp() function directly in this <script> element. import { createApp } from './create-app.js' // Can't import showAppInElement from the package directly in // a web browser like this, but you get the idea. import { showAppInElement } from '@bagawork/framework' document.addEventListener('DOMContentLoaded', function(){ const screenElement = document.querySelector('#screen') // Read more about runtimeSettings further down. const runtimeSettings = {} showAppInElement( createApp, screenElement, runtimeSettings, ) }) </script> </head> <body> <div id="screen"></div> </body> </html>
Runtime settings
Saving and restoring the state
Each time the app changes which page to show to the user, the app's internal state (app variables, page variables, etc.) might have been changed. If you want the user to be able to close the app and later start it again with the same state as it had as when the user closed the app, you need to store the state the app had when it was closed, and then start the app with that stored state.
Set runtimeSettings.onStateChange
to (newState) => localStorage['state'] = JSON.stringify(newState)
to listen for when the app changes page and store the new state of the app in JSON format in localStorage.
Set runtimeSettings.state
to JSON.parse(localStorage['state'] ?? '""') || null
to start the app with the state (potentially) stored in localStorage
.
Dealing with updates
If a state has been stored, and the app then starts with that state running in a new version of the app, an update in the app needs to happen (the onUpdate()
methods in the app needs to be called, etc.).
Set runtimeSettings.version
to the current version of the app, and the framework will perform the update (call the onUpdate
methods in the app, etc.) if this version is higher than the version of the app stored in the state.
Listening for errors
Set runtimeSettings.onError
to (errorMessage) => console.log(errorMessage)
to listen for when the code in the app crashes. When a crash occurs, the app will also display the error message on the screen.
Previewing
Set runtimeSettings.isPreview
to true
for Page.onBeforeDirections()
not take effect (will probably only be used by the editor).
Debugging
Set runtimeSettings.okToContinue
to async (nextToExecute = `Step description`, continuesImmediately=false) => Promise.resolve()
to make the app run in debug mode. This way, the function you provide will be called each time the app runs a step (described by nextToExecute
), and your function should return back a promise that should be resolved when it's time to run that step.
Some things the debugger can't wait for (like when the user call her own App/Page function (since those aren't async)), and for those steps, continuesImmediately
will be true
, and the promise you return back won't be used, but the current step will be executed immediately.
Implementation details
A BagaWork app is implemented as a function (referred to as the createApp()
function) that returns the App
class that should be used, together with all the Pages
that are used. The reason for this design is so that the App
and Page
classes can have access to the BagaWork variables a
and p
. For an example, see the Quick start code above.
createApp(environment)
When the app should be created, the framework will call the createApp()
function and pass it an environment, which is a plain JS object with the following properties:
a
is a reference to theApp
instancep
is a reference to the currently shownPage
instance
Setting up the environment for createApp()
is not straight forward, since the p
property needs to be updated to contain the current page in the app. To make that happen, the environment is actually implemented as a JS proxy that intercepts property access and send back the correct value, but to the one implementing the createApp()
function it can be seen as a JS object that always contains up-to-date values.
FrameworkApp & FrameworkPage VS App & Page
To hide framework implementation details as much as possible from the ones implementing BagaWork apps, App
and Page
contains no framework logic:
- The logic for
App
is implemented inFrameworkApp
(it has access to theApp
that should be used, and an instance of it) - The logic for
Page
is implemented inFrameworkPage
(it has a access to the currently shownPage
, and an instance of it)
BagaWork then uses FrameworkApp
and FrameworkPage
, and they in turn use the provided App
and Page
classes when they need to get some info from them.
FrameworkApp
and FrameworkPage
also have access to each other.
Using FrameworkApp
as a context
FrameworkApp
does contain "the logic for the app" (as one can expect), but it also serves as a context that contains "global" information. Instead of giving other classes the small amount of extra information they actually need, they are given a reference to the FrameworkApp
instance, so they can obtain whichever information they want from it themselves.
An example of this are GUI components. Some of them need to use frameworkApp.runtimeSettings.isPreview
to know if they should function the real way, or only in a preview way. But instead of giving the components only runtimeSettings
, they receive the entire frameworkApp
object.
This architecture makes the code structure easier to understand; you can always access any information you need from the frameworkApp
object you have access to, no matter where you write the code 😀
Starting the app
To run your app, pass your createApp()
function to a new FrameworkApp
instance:
const runtimeSettings = {}
const frameworkApp = new FrameworkApp(createApp, runtimeSettings)
You can read more about the runtime settings you can use further down on this page.
It is also possible to pass a string that contains JS code that evaluates to the createApp()
function instead of passing it the createApp()
function directly.
Then simply call frameworkApp.start()
to make the machinery start! The figure below shows the order methods are invoked when starting the app for the first time.