A framework to build SPA web applications based on web components and web standards.
Léelo en Español
Open Cells is made to be light, easy to use and will help you to create SPAs faster. To do so it will handle the basics of every SPA:
- Routing.
- State management, based on a pub-sub reactive pattern implementing RxJS.
- App Configuration.
- Bootstrapping.
Besides, as it is based on web standards its learning curve is really low: having knowledge about HTML, CSS and Javascript you only need to learn a couple of APIs and conventions.
In this mono repo we have the modules that builds up Open Cells:
core
: implements routing, state handling, app configuration and bootstrapping of the application.element-controller
: provides the components the mechanisms to use the core API (navigation, state, configuration)page-controller
: extendselement-controller
and provides lifecycle hooks to handle page loading.
To create an application with Open Cells run:
npx @open-cells/create-app
You will be asked to enter a name for the application and once you confirm it a folder will be created with the application inside.
This command creates a recipes application as an example, but the folder and files structure may be the same for every Open Cells application.
After this you need to install the dependencies.
npm install
Once everything is installed, you can try the application running:
npm run start
Open Cells won't require an specific folder structure but, it needs for these to exists:
- routes configuration
- web components to implement the pages
The app created in the previous step follows this structure as a suggestion:
Root Directory/
|── package.json
|── tsconfig.json
|── index.html
|── images/
| └── favicon.svg
└── src/
|── components/
| |── app-index.ts
| └── app-index.css.js
|── pages/
| └── home/
| | └── home-page.ts
| └── second/
| └── second-page.ts
|── css/
| |── home.css
| |── main.css
| └── second.css
└── router/
└── routes.ts
The index.html
file is the document in which the app will be mounted on. Its body contains the <app-index id="app-content">
element that will contain the app pages, and the <script>
tag that invokes all the Open Cells logic.
The src/components/app-index.ts
file includes the imports of Open Cells core library and the app initialization.
import { startApp } from '@open-cells/core';
import { routes } from '../router/routes.js';
startApp({
routes,
mainNode: 'app-content',
});
startApp
is the functions that initialize the application which requires:
routes
: the routes that the application will handle. These routes we'll get them from the filesrc/router/route.js
which exposes an array of routes.mainNode
: the id of HTML element fromindex.html
where every page will be rendered.
The router from Open Cells works with the association of a route with a component (page). Every time the url fragment changes, the router look for the component for that route and it will render it inside the element specified in mainNode
.
A route is defined with an object like this:
{
path: '/category/:category',
name: 'category',
component: 'category-page',
notFound: false,
action: async () => {
await import('../pages/category/category-page.js');
},
},
In this example:
path
: is the defined route. It may contain dynamic parameters, like :category, which may be used inside the page component. Query parameters are supported too.name
: is the name assigned to the route. The purpose of this is to ease the programmatic navigation, because pages can use the functionnavigate(name, params)
indicating the name of the route and the needed parameters.component
: (optional) is thetag name
of the web component associated to the route. This component will be rendered when the route matches the path. If component is not specified, the router will assume thetag name
of the component to be rendered will bename
concatenated with-page
suffix.notFound
: allows us to identify the page that we want to use when a requested route doesn't exist.action
: is the asynchronous function to be executed before rendering the page component.
As we explained earlier, startApp
input is a routes array. This array have objects like the one over this line, each one defining a route.
👉🏻 The application always starts with the path /
so it's mandatory to have a route defined for it.
To navigate programmatically, be sure your origin page component has a page controller so you can use function navigate(name, params)
providing the name of the route and the params if needed.
pageController.navigate('category', { category: 'example' });
The state of the application is handled in a reactive way using RxJS through the functions publish and subscribe provided by ElementController or PageController.
Reactive channels are implemented with RxJS, a Javascript library for reactive programming. These channels allow global communication and the handling of the state efficiently.
When a value is published in a channel, this value stays in the channels until another publication is done. This allows the components to read the value when they subscribe to a channel even if it's published before the subscription.
publish
: allows to send a value into a channel, where it will stays until another publication is made.subscribe
: allows the components to create a subscription to a channel to receive and react to the published values.unsubscribe
: allows the components to stop the subscription to a channel so it won't be updated with values anymore.
Reactivity: Reactive programming provides event handling and data flow, which allows a dynamic communication among the application components.
Control centralization: As publish
and subscribe
functions are invoked from a central controller, it makes easier to manage the application state and it promotes a more organized and efficient architecture.
Efficiency: RxJS offers tools to manage efficiently the communication and the state of the application, giving a great performance even in big and complex applications.
With RxJS, the application may benefit of a reactive and efficient communication, which makes easy the development of dynamic and responsive user interfaces.
When a page is initialized OpenCells automatically include in its lifecycle a couple of hooks to allow the component know when the router navigates to and from the page.
onPageEnter
page hook that allows the component handle its state when it enters the viewport.onPageLeave
page hook that allows the component handle its state when it exits the viewport.
The functions of state handling and routing in Open Cells are provided through reactive controllers
. A controllers is an object that can be included in any component to give it some functionality.
Open Cells controllers are:
ElementController
: provides the functions to publish on and subscribe to channels and to navigate between pages.PageController
: extends ElementController and on top of that it includes hooks to handle the entry/exit of a page.
subscribe(channelName, callback)
: it subscribes to the channel indicated inchannelName
. If the channel doesn't exist, Open Cells will create it at that point. Functioncallback
will be called when there is a new state in the channel (a new value is published).unsubscribe(channelName)
: it stops the subscription of the component to the channelchannelName
.publish(channelName, value)
: it publishesvalue
in the channelchannelName
.publishOn(channelName, htmlElement, eventName)
: every time the elementhtmlElement
dispatches the eventeventName
, thedetail.value
of the event is published onchannelName
.navigate(page, params)
: navigates topage
passing the parametersparams
(must be a key/value object).backStep()
: navigates to the previous page in the history.getCurrentRoute()
get the information about the current route.
As extends the ElementController it gets all its functionality and these two hooks:
onPageEnter
: page hook that allows the component handle its state when it enters the viewport.onPageLeave
: page hook that allows the component handle its state when it exits the viewport.