Reactive element
Classes
Typedefs
ReactiveElement
Kind: global class
- ReactiveElement
- new ReactiveElement()
- .observableAttributes(attributes) ⇒
void
- .effect(effectFn) ⇒
void
- .connect(store, key) ⇒
ObservableProxy
- .stream(subscribeFn) ⇒
ObservableStream
- .template() ⇒
void
- .query(options) ⇒
ObservableProxy
- .mutation(options) ⇒
ObservableProxy
- .invalidateQueries(queryKey) ⇒
void
- .onCreate() ⇒
void
- .connectedCallback() ⇒
void
- .onConnect() ⇒
void
- .disconnectedCallback() ⇒
void
|void
- .onDisconnect() ⇒
void
- .attributeChangedCallback(name, oldValue, newValue) ⇒
void
- .onAttributeChange() ⇒
void
- .adoptedCallback() ⇒
void
|void
- .onAdopt() ⇒
void
new ReactiveElement()
This class is needed to create reactive web components that can automatically update their view when their state changes. All properties are automatically converted to observables. This is achieved by using creating an ObservableProperty, which provides a getter and setter for the property. The getter returns the current value of the property, and the setter updates the value of the property and triggers a re-render of the component.
Example
const { html, ReactiveElement } = cami;
class CounterElement extends ReactiveElement {
// Here, 'count' is automatically initialized as an ObservableProperty.
// This means that any changes to 'count' will automatically trigger a re-render of the component.
count = 0
template() {
return html`
<button @click=${() => this.count--}>-</button>
<button @click=${() => this.count++}>+</button>
<div>Count: ${this.count}</div>
`;
}
}
customElements.define('counter-component', CounterElement);
reactiveElement.observableAttributes(attributes) ⇒ void
Creates ObservableProperty or ObservableProxy instances for all properties in the provided object.
Kind: instance method of ReactiveElement
Param | Type | Description |
---|---|---|
attributes | Object |
An object with attribute names as keys and optional parsing functions as values. |
Example
// In _009_dataFromProps.html, the todos attribute is parsed as JSON and the data property is extracted:
this.observableAttributes({
todos: (v) => JSON.parse(v).data
});
reactiveElement.effect(effectFn) ⇒ void
Creates an effect and registers its dispose function. The effect is used to perform side effects in response to state changes. This method is useful when working with ObservableProperties or ObservableProxies because it triggers the effect whenever the value of the underlying ObservableState changes.
Kind: instance method of ReactiveElement
Param | Type | Description |
---|---|---|
effectFn | function |
The function to create the effect |
Example
// Assuming `this.count` is an ObservableProperty
this.effect(() => {
console.log(`The count is now: ${this.count}`);
});
// The console will log the current count whenever `this.count` changes
reactiveElement.connect(store, key) ⇒ ObservableProxy
Subscribes to a store and creates an observable for a specific key in the store. This is useful for synchronizing the component's state with a global store.
Kind: instance method of ReactiveElement
Returns: ObservableProxy
- An observable property or proxy for the store key
Param | Type | Description |
---|---|---|
store | ObservableStore |
The store to subscribe to |
key | string |
The key in the store to create an observable for |
Example
// Assuming there is a store for cart items
// `cartItems` will be an observable reflecting the current state of cart items in the store
this.cartItems = this.connect(CartStore, 'cartItems');
reactiveElement.stream(subscribeFn) ⇒ ObservableStream
Creates an ObservableStream from a subscription function.
Kind: instance method of ReactiveElement
Returns: ObservableStream
- An ObservableStream that emits values produced by the subscription function.
Param | Type | Description |
---|---|---|
subscribeFn | function |
The subscription function. |
Example
// In a FormElement component
const inputValidation$ = this.stream();
inputValidation$
.map(e => this.validateEmail(e.target.value))
.debounce(300)
.subscribe(({ isEmailValid, emailError, email }) => {
this.emailError = emailError;
this.isEmailValid = isEmailValid;
this.email = email;
this.isEmailAvailable = this.queryEmail(this.email);
});
reactiveElement.template() ⇒ void
Kind: instance method of ReactiveElement
Throws:
Error
If the method template() is not implemented
Example
// Here's a simple example of a template method implementation
template() {
return html`<div>Hello World</div>`;
}
reactiveElement.query(options) ⇒ ObservableProxy
Fetches data from an API and caches it. This method is based on the TanStack Query defaults: https://tanstack.com/query/latest/docs/react/guides/important-defaults.
Kind: instance method of ReactiveElement
Returns: ObservableProxy
- A proxy that contains the state of the query.
Param | Type | Default | Description |
---|---|---|---|
options | Object |
The options for the query. | |
options.queryKey | Array | string |
The key for the query. | |
options.queryFn | function |
The function to fetch data. | |
[options.staleTime] | number |
0 |
The stale time for the query. |
[options.refetchOnWindowFocus] | boolean |
true |
Whether to refetch on window focus. |
[options.refetchOnMount] | boolean |
true |
Whether to refetch on mount. |
[options.refetchOnReconnect] | boolean |
true |
Whether to refetch on network reconnect. |
[options.refetchInterval] | number |
|
The interval to refetch data. |
[options.gcTime] | number |
1000 * 60 * 5 |
The garbage collection time for the query. |
[options.retry] | number |
3 |
The number of retry attempts. |
[options.retryDelay] | function |
(attempt) => Math.pow(2, attempt) * 1000 |
The delay before retrying a failed query. |
Example
// In _012_blog.html, a query is set up to fetch posts with a stale time of 5 minutes:
const posts = this.query({
queryKey: ["posts"],
queryFn: () => fetch("https://api.camijs.com/posts?_limit=5").then(res => res.json()),
staleTime: 1000 * 60 * 5
});
reactiveElement.mutation(options) ⇒ ObservableProxy
Performs a mutation and returns an observable proxy. This method is inspired by the TanStack Query mutate method: https://tanstack.com/query/latest/docs/react/guides/mutations.
Kind: instance method of ReactiveElement
Returns: ObservableProxy
- A proxy that contains the state of the mutation.
Param | Type | Description |
---|---|---|
options | Object |
The options for the mutation. |
options.mutationFn | function |
The function to perform the mutation. |
[options.onMutate] | function |
The function to be called before the mutation is performed. |
[options.onError] | function |
The function to be called if the mutation encounters an error. |
[options.onSuccess] | function |
The function to be called if the mutation is successful. |
[options.onSettled] | function |
The function to be called after the mutation has either succeeded or failed. |
Example
// In _012_blog.html, a mutation is set up to add a new post with optimistic UI updates:
const addPost = this.mutation({
mutationFn: (newPost) => fetch("https://api.camijs.com/posts", {
method: "POST",
body: JSON.stringify(newPost),
headers: {
"Content-type": "application/json; charset=UTF-8"
}
}).then(res => res.json()),
onMutate: (newPost) => {
// Snapshot the previous state
const previousPosts = this.posts.data;
// Optimistically update to the new value
this.posts.update(state => {
state.data.push({ ...newPost, id: Date.now() });
});
// Return the rollback function and the new post
return {
rollback: () => {
this.posts.update(state => {
state.data = previousPosts;
});
},
optimisticPost: newPost
};
}
});
reactiveElement.invalidateQueries(queryKey) ⇒ void
Invalidates the queries with the given key by clearing the cache. To reflect the latest state in the UI, one will still need to manually refetch the data after invalidation. This method is particularly useful when used in conjunction with mutations, such as in the onSettled
callback, to ensure that the UI reflects the latest state.
Kind: instance method of ReactiveElement
Param | Type | Description |
---|---|---|
queryKey | Array | string |
The key for the query to invalidate. |
Example
// In a mutation's `onSettled` callback within a `BlogComponent`:
this.addPost = this.mutation({
// ...mutation config...
onSettled: () => {
// Invalidate the posts query to clear the cache
this.invalidateQueries(['posts']);
// Manually refetch the posts to update the UI with the true state
this.fetchPosts(); // this assumes something like this.posts = this.query({ ... })
}
});
reactiveElement.onCreate() ⇒ void
Called when the component is created. Can be overridden by subclasses to add initialization logic. This method is a hook for the connectedCallback, which is invoked each time the custom element is appended into a document-connected element.
Kind: instance method of ReactiveElement
Example
onCreate() {
// Example initialization logic here
this.posts = this.query({
queryKey: ["posts"],
queryFn: () => {
return fetch("https://api.camijs.com/posts?_limit=5")
.then(res => res.json())
},
staleTime: 1000 * 60 * 5 // 5 minutes
});
}
reactiveElement.connectedCallback() ⇒ void
Invoked when the custom element is appended into a document-connected element. Sets up initial state and triggers initial rendering. This is typically used to initialize component state, fetch data, and set up event listeners.
Kind: instance method of ReactiveElement
Example
// In a TodoList component
connectedCallback() {
super.connectedCallback();
this.fetchTodos(); // Fetch todos when the component is added to the DOM
}
reactiveElement.onConnect() ⇒ void
Invoked when the custom element is connected to the document's DOM.
Kind: instance method of ReactiveElement
Returns: void
- Subclasses can override this to add initialization logic when the component is added to the DOM.
Example
// In a UserCard component
onConnect() {
this.showUserDetails(); // Display user details when the component is connected
}
reactiveElement.disconnectedCallback() ⇒ void
| void
Invoked when the custom element is disconnected from the document's DOM. This is a good place to remove event listeners, cancel any ongoing network requests, or clean up any resources.
Kind: instance method of ReactiveElement
Example
// In a Modal component
disconnectedCallback() {
super.disconnectedCallback();
this.close(); // Close the modal when it's disconnected from the DOM
}
reactiveElement.onDisconnect() ⇒ void
Invoked when the custom element is disconnected from the document's DOM. Subclasses can override this to add cleanup logic when the component is removed from the DOM.
Kind: instance method of ReactiveElement
Example
// In a VideoPlayer component
onDisconnect() {
this.stopPlayback(); // Stop video playback when the component is removed
}
reactiveElement.attributeChangedCallback(name, oldValue, newValue) ⇒ void
Invoked when an attribute of the custom element is added, removed, updated, or replaced. This can be used to react to attribute changes, such as updating the component state or modifying its appearance.
Kind: instance method of ReactiveElement
Param | Type | Description |
---|---|---|
name | string |
The name of the attribute that changed |
oldValue | string |
The old value of the attribute |
newValue | string |
The new value of the attribute |
Example
// In a ThemeSwitcher component
attributeChangedCallback(name, oldValue, newValue) {
super.attributeChangedCallback(name, oldValue, newValue);
if (name === 'theme') {
this.updateTheme(newValue); // Update the theme when the `theme` attribute changes
}
}
reactiveElement.onAttributeChange() ⇒ void
Invoked when an attribute of the custom element is added, removed, updated, or replaced.
Kind: instance method of ReactiveElement
Returns: void
- Subclasses can override this to add logic that should run when an attribute changes.
Example
// In a CollapsiblePanel component
onAttributeChange(name, oldValue, newValue) {
if (name === 'collapsed') {
this.toggleCollapse(newValue === 'true'); // Toggle collapse when the `collapsed` attribute changes
}
}
reactiveElement.adoptedCallback() ⇒ void
| void
Invoked when the custom element is moved to a new document. This can be used to update bindings or perform re-initialization as needed when the component is adopted into a new DOM context.
Kind: instance method of ReactiveElement
Example
// In a DragDropContainer component
adoptedCallback() {
super.adoptedCallback();
this.updateDragDropContext(); // Update context when the component is moved to a new document
}
reactiveElement.onAdopt() ⇒ void
Invoked when the custom element is moved to a new document. Subclasses can override this to add logic that should run when the component is moved to a new document.
Kind: instance method of ReactiveElement
Example
// In a DataGrid component
onAdopt() {
this.refreshData(); // Refresh data when the component is adopted into a new document
}
ObservableProperty
Kind: global typedef
Properties
Name | Type | Description |
---|---|---|
get | function |
A getter function that returns the current value of the property. If the property is a primitive value, this will return the value directly from the ObservableState instance. If the property is a non-primitive value, this will return an ObservableProxy that wraps the ObservableState instance. This getter is used when accessing the property on a ReactiveElement instance. This polymorphic behavior allows the ObservableProperty to handle both primitive and non-primitive values, and handle nested properties (only proxies can handle nested properties, whereas getters/setter traps cannot) |
set | function |
A setter function that updates the value of the property. It updates the ObservableState instance with the new value. This setter is used when assigning a new value to the property on a ReactiveElement instance. |
Example
// Primitive value example from _001_counter.html
// this.count is an ObservableProperty, where if you get the value, it returns the current value of the property, and if you set the value, it updates the property with the new value
// ObservableProperty is just Object.defineProperty with a getter and setter, where the Object is the ReactiveElement instance
class CounterElement extends ReactiveElement {
count = 0
template() {
return html`
<button @click=${() => this.count--}>-</button>
<button @click=${() => this.count++}>+</button>
<div>Count: ${this.count}</div>
`;
}
}
// Non-primitive value example from _003_todo.html
// this.query returns an ObservableProperty / ObservableProxy
// this.todos is an ObservableProxy, where if you get the value, it returns the current value of the property, and if you set the value, it updates the property with the new value
// We use Proxy instead of Object.defineProperty because it allows us to handle nested properties
class TodoListElement extends ReactiveElement {
todos = this.query({
queryKey: ['todos'],
queryFn: () => {
return fetch("https://api.camijs.com/todos?_limit=5").then(res => res.json())
},
staleTime: 1000 * 60 * 5 // 5 minutes
})
template() {
// ...template code...
}
}
// Array value example from _010_taskmgmt.html
// this.tasks is an ObservableProxy, where if you get the value, it returns the current value of the property, and if you set the value, it updates the property with the new value
// We use Proxy instead of Object.defineProperty because it allows us to handle nested properties
class TaskManagerElement extends ReactiveElement {
tasks = [];
filter = 'all';
// ...other methods...
template() {
// ...template code...
}
}
ObservableState
Kind: global typedef
Properties
Name | Type | Description |
---|---|---|
value | any |
The current value of the observable state. This is the value that is returned when accessing a primitive property on a ReactiveElement instance. It can also be used to set a new value for the observable state. |
update | function |
A function that updates the value of the observable state. It takes an updater function that receives the current value and returns the new value. This is used when assigning a new value to a primitive property on a ReactiveElement instance. It allows deeply nested updates. |
[dispose] | function |
An optional function that cleans up the observable state when it is no longer needed. This is used internally by ReactiveElement to manage memory. |