ContextProviderComponent
The ContextProviderComponent
class is a
special WebComponent class
in a sense that it allows you to define the template right in your HTML file with it serving as the data provider for
your template.
// in the browser
const {ContextProviderComponent} = window;
// in node
import {ContextProviderComponent} from "cwco";
Mode
By default, the ContextProviderComponent
is in the none
mode. Which means that its content can be easily
target by CSS selectors of the document or in javascript DOM queries.
Template
By default, the template is a single slot
tag which means you don't need to define the component template inside the
class.
You can define your data in the class...
// todo-app.js
class TodoApp extends ContextProviderComponent {
app = {
title: "Todo App",
description: "My super cool todo app",
}
}
TodoApp.register();
...and then set the content right in the HTML file and reference the data from the class.
<!-- documentation.html -->
<todo-app>
<h1 bind="{app.title}">No Title</h1>
<p bind="{app.description}">No Description</p>
</todo-app>
Note: It is recommended to use the bind directive when you are defining your template directly in the HTML body. This is to avoid curly brace flashes before the javascript loads.
The above example binds app.title
and app.description
with defaults.
Before Javascript loads the title will be No Title
then the value of the title.
Registration
if you intend to have multiple provider with template in the HTML file, you will run into issues of registration.
Below will throw errors
<theme-provider>
<store-data-provider>
<todo-app>
<h1>{app.title}</h1>
<p>{app.description}</p>
</todo-app>
</store-data-provider>
</theme-provider>
This is because, everything render inside, will try to reference data inside the outer most component,
which in this case is theme-provider
which has no app
data. As you can see below:
class ThemeProvider extends ContextProviderComponent {
theme = {
primary: "blue",
secondary: "red",
}
}
class StoreDataProvider extends ContextProviderComponent {
store = {
todos: [],
user: {}
}
}
class TodoApp extends ContextProviderComponent {
app = {
title: "Todo App",
description: "My super cool todo app",
}
}
// register them in the order you use them in the HTML file
ThemeProvider.register()
StoreDataProvider.register();
TodoApp.register();
Here is when you need to rely on context data to deal with this situation.
You can start by moving data to the context by declaring a static initialContext
property.
This creates data that can be accessed by any descendent component or element without via $context
property.
class ThemeProvider extends ContextProviderComponent {
static initialContext = {
theme: {
primary: "blue",
secondary: "red",
}
}
}
class StoreDataProvider extends ContextProviderComponent {
static initialContext = {
store: {
todos: [],
user: {}
}
}
}
class TodoApp extends WebComponent {
app = {
title: "Todo App",
description: "My super cool todo app",
}
get template() {
return `
<h1>{app.title}</h1>
<p>{app.description}</p>
{theme}
{store}
`
}
}
Note: For this now, we just need to make sure to register all context providers before the TodoApp
component.
And then update the template like so.
<store-data-provider>
<theme-provider>
<todo-app></todo-app>
</theme-provider>
</store-data-provider>
As you can see, the order of the context providers registration only matters if you are to put HTML directly in the HTML document body. If the content of the template is placed inside the component class, it is all fine.
In general, it is a good practice to register the context providers before all components of your application. These are components which normally handle global data and should run before the rest of the app.
The slot tag
What really makes the ContextProviderComponent
special is how it handles the slot
tag.
It has a custom slot
handler that gives it its superpowers. This means that no matter the component mode,
the slot tag will be handled the same way which is not something you can do natively in HTML.
The template is a single slot
tag, but you can also define your own template with slots where you wish. The only thing
you need to keep in mind is that the slot is handled differently.
The example below renders the title and description inside and lets the rest of the app to be defined in the HTML file.
// todo-app.js
class TodoApp extends ContextProviderComponent {
app = {
title: "Todo App",
description: "My super cool todo app",
todos: []
}
onMount() {
fetch('http://localhost:3000/api/todo')
.then(res => res.json())
.then(res => {
this.app.todos = res.data;
})
}
openTodo() {
// handle opening todo
}
get template() {
return `
<h1>{app.title}</h1>
<p>{app.description</p>
<slot></slot>
`;
}
}
TodoApp.register();
You can render the todo-item
tag inside the todo-app
tag and it will be placed where the slot tag is defined using
the repeat directive to repeat
for every app.todos
array item.
<!-- todo-app.html -->
<todo-app>
<todo-item repeat="app.todos" name="$item.name" description="$item.description" status="$item.status" onclick="openTodo($item)"></todo-item>
</todo-app>
Data and methods
Any data defined inside the class can be accessed inside the template but not inside the custom components you create.
From the example above, the app
object cannot be accessed inside the todo-item
template. However, you can access
the data and methods inside the todo-item
tag body as this is still considered to be part of the provider template.
<!-- documentation.html -->
<todo-app>
<todo-item repeat="app.todos" name="$item.name" description="$item.description" status="$item.status">
<!-- the todo-item tag has a slot for the button that handles
opening the single todo app -->
<button onclick="openTodo($item)">open</button>
</todo-item>
</todo-app>
What this means is that, any HTML tag that is placed inside the body of the todo-app
tag can access the
ToDoApp class data and methods. Any HTML tag that belongs to other tags template will not. For those, you
need to specify data as context.
Context
ContextProviderComponent
is like any other WebComponent
and therefore, it may contain
context data.
This is the only way you can provide data inside the descendents component templates, and it is excellent for data you want to share deeply for all components.
class ThemeProvider extends ContextProviderComponent {
static initialContext = {
theme: {
colors: {
primary: 'purple',
secondary: '#222',
cta: '#900',
light: '#f2f2f2',
dark: '#111',
},
}
}
}
Simply wrap your global data provider using context around the component tag, and it will be available deeply inside any component templates.
<theme-provider>
<todo-app></todo-app>
</theme-provider>
Styling
By default, all context providers tag will be styled as display block. This can easily be overridden by defining your own stylesheet property.
Styling your context provider component is no different from styling any other web component but because
by default it does not use shadow root, you need to prefix every style with the :host
to make sure
the style does not affect things outside its body.
This is due to the special way style tags are handled in none
mode. You can
Learn more about it here
class TodoApp extends ContextProviderComponent {
// ...
get stylesheet() {
return `
<style>
:host h1 {
color: [theme.colors.primary];
}
</style>
`;
}
get template() {
return `
<h1>{app.title}</h1>
<p>{app.description</p>
<slot></slot>
`;
}
}
One thing to know is that the ::slotted selector
will not work because the way the ContextProviderComponent
handles the slot tags no matter what the mode
of the component. If you need to use ::slotted
please use the WebComponent
instead.