Gathering detailed insights and metrics for @domx/statecontroller
Gathering detailed insights and metrics for @domx/statecontroller
npm install @domx/statecontroller
Typescript
Module System
Node Version
NPM Version
TypeScript (99.06%)
JavaScript (0.78%)
HTML (0.17%)
Total Downloads
1,582
Last Day
1
Last Week
5
Last Month
43
Last Year
575
8 Stars
552 Commits
1 Forks
1 Watching
20 Branches
2 Contributors
Minified
Minified + Gzipped
Latest Version
0.4.0
Package Id
@domx/statecontroller@0.4.0
Unpacked Size
378.99 kB
Size
54.83 kB
File Count
64
NPM Version
7.18.1
Node Version
16.4.2
Cumulative downloads
Total Downloads
Last day
-66.7%
1
Compared to previous day
Last week
-50%
5
Compared to previous week
Last month
34.4%
43
Compared to previous month
Last year
62.9%
575
Compared to previous year
1
The StateController
is a Reactive Controller
that provides state to a LitElement
.
Highlights
Installation
Basic Usage
Defining State Properties
Handling Events
StateController "instances"
Requesting Updates
Using Product (Immer)
Root State and State Syncing
Redux DevTools
StateController Composition
1npm install @domx/statecontroller
This is a contrived example to show a simple usage scenario.
1import { StateController } from "@domx/statecontroller"; 2import { stateProperty, hostEvent } from "@domx/statecontroller/decorators"; 3 4export class SessionStateController extends StateController { 5 6 @stateProperty() 7 state:ISessionState = { 8 loggedInUserName: "", 9 loggedInUsersFullName: "" 10 }; 11 12 @hostEvent(UserLoggedInEvent) 13 userLoggedIn(event:UserLoggedInEvent) { 14 this.state = { 15 ...this.state, 16 loggedInUserName: event.userName, 17 loggedInUsersFullName: event.fullName 18 }; 19 this.requestUpdate(event); 20 } 21} 22 23export class UserLoggedInEvent extends Event { 24 static eventType = "user-logged-in"; 25 userName:string; 26 fullName:string; 27 constructor(userName:string, fullName:string) { 28 super(UserLoggedInEvent.eventType); 29 this.userName = userName; 30 this.fullName = fullName; 31 } 32} 33 34interface ISessionState { 35 loggedInUserName: string; 36 loggedInUsersFullName: string; 37}
By subclassing the Event class, The
UserLoggedInEvent
acts as a great way to document what events a StateController can handle. This is similar to action creators in Redux. They can be defined in the same file as the StateController (or in a separate file if that works better for you) and used by UI components to trigger events.
The SessionStateController
can be used with any LitElement.
1import { LitElement, html } from "lit"; 2import { customElement } from "lit/decorators.js"; 3import { SessionStateController, UserLoggedInEvent } from "./SessionStateController"; 4 5@customElement("current-user") 6class CurrentUser extends LitElement { 7 session = new SessionStateController(this); 8 9 render() { 10 const state = this.session.state; 11 return html` 12 <button @click=${this.updateUserClicked}>Update user</button> 13 <div> 14 Logged in as: ${state.loggedInUserName} 15 (${state.loggedInUsersFullName}) 16 </div> 17 `; 18 } 19 20 updateUserClicked(event) { 21 this.dispatchEvent(new UserLoggedInEvent("juser", "Joe User")); 22 } 23}
To set a property as a state property, simply use the @stateProperty()
decorator
as in the example above.
This can also be done by defining a static field on the controller.
1export class SessionStateController extends StateController { 2 static stateProperties = ["state"]; 3 4 state:ISessionState = { 5 loggedInUserName: "", 6 loggedInUsersFullName: "" 7 }; 8 9 //... 10}
For the unidirectional data flow pattern, the state should only change in response to an event.
There are two decorators available to help setup event listeners on both the host (the UI element) and the window.
Window events are great for application level communication, whereas, host events are better suited for local changes that occur in the element that the StateController is attached to (or any child elements).
1import { StateController } from "@domx/statecontroller"; 2import { stateProperty, hostEvent, windowEvent } from "@domx/statecontroller/decorators"; 3 4export class SomeController extends StateController { 5 @hostEvent(SomeHostEvent) 6 someHostEvent(event:SomeHostEvent) { 7 this.state = {/*...*/}; 8 this.requestUpdate(event); 9 } 10 11 @windowEvent(SomeWindowEvent) 12 someWindowEvent(event:SomeWindowEvent) { 13 this.state = {/*...*/}; 14 this.requestUpdate(event); 15 } 16}
Both decorators require an event that has a static
eventType
property on them. Since these are just DOM events, the decorators are not required if you have some other way of setting up listeners.
When the event is meant for the window or could be fired by child elements, make sure to set the bubbles and composed options in the event constructor to true.
1export class SomeEvent extends Event { 2 static eventType = "some-event"; 3 constructor() { 4 super(SomeEvent.eventType, {bubbles: true, composed: true}); 5 } 6}
By default, all events handled when using the decorators have stopImmediatePropagation
called on the event
so it is not handled by multiple controllers.
There are some cases where you may want multiple controllers to handle the same event. For this, you can
set a capture
option to false.
1@windowEvent(SomeWindowEvent, {capture: false})
2someWindowEvent(event:SomeWindowEvent) {
3 this.state = {/*...*/};
4 this.requestUpdate(event);
5}
In some cases, the state that the controller contains is specific to an instance. For example, a specific User or Product.
To keep instance state separate, the UI element can declare a stateId
getter.
1import { LitElement, html } from "lit"; 2import { customElement, property } from "lit/decorators.js"; 3import { SomeStateController } from "./SessionStateController"; 4 5@customElement("user-card") 6class UserCard extends LitElement { 7 8 // declaring a stateId 9 get stateId() { return this.userId; } 10 11 @property({type:String, attribute: "user-id"}) 12 userId:string; 13 14 user = new UserStateController(this); 15}
The UserStateController will use the
stateId
property on its host if it is defined.
Controllers that control instance type data can require using a stateId with a simple type constraint.
1class SomeStateController extends StateController { 2 // this controller requires a stateId on the host 3 constructor(host:LitElement & {stateId:string}) { 4 super(host); 5 } 6}
refreshState()
for stateId changesIf the host elements stateId changes, the internal state of the StateController will be out of sync.
Calling refreshState()
on the controller will check if the stateId has in fact changed,
and if so, the controllers hostDisconneced
and hostConnected
methods will be called.
To force a state refresh, you can call refreshState(true)
.
If you are using the event decorators,
refreshState()
is called for you.
Because refreshState
disconnects and reconnects the controller, it is not a good
idea to have state initialization in the hostConnected
method. If there are
multiple state controllers in the DOM with the same stateId, this could cause
multiple requests to re-fetch data.
Instead, use a host or window event handler since when handled, the events do not automatically propagate and state will be synced during the expected lifecycle.
The requestUpdate
method is a pass through to the ReactiveController
requestUpdate method.
It also has an event
argument which can be an instance of an Event
class or a string
description.
This event is primarily for logging and debugging purposes to track what action occurred to require the update.
See the Root State and State Syncing section below.
Since LitElement works with immutable state, it can get tedious to make changes to large state objects.
Immer is a great library that simplifies state changes.
There is a Product
"Monad like" class which integrates Immer with a StateController to provide a more
functional approach to state changes (similar to Redux reducers).
See the Product documentation.
Using Immer directly
The StateController makes the Immer produce
method available if you would like to use it directly.
1import { produce } from "@domx/statecontroller/product"; 2import { StateController } from "@domx/statecontroller"; 3import { stateProperty, hostEvent } from "@domx/statecontroller/decorators"; 4 5export class SomeController extends StateController { 6 @stateProperty() 7 state = { foo: "bar" }; 8 9 @hostEvent(FooChangedEvent) 10 someHostEvent(event:FooChangedEvent) { 11 // using Immer's produce method directly 12 this.state = produce(this.state, state => { 13 state.foo = event.foo; 14 }); 15 this.requestUpdate(event); 16 } 17}
One of the features of the StateController is that all state in the controller is stored in a RootState
class. This allows state to be synced across the same StateController if used multiple times in the DOM.
The RootState
class contains an object mapping to all state that is "connected" (in the DOM).
Every StateController has a state path / key that is used to reference its state in the RootState
.
This key is the derived using "<ClassName>.<StateId?>.<StateName>
".
When a StateControllers element is connected to the DOM, the state will be looked up using its key. If found, the state is initialized with the state that is already connected to the DOM. If not found, the controllers state will be pushed to the RootState.
Any time a call to requestUpdate
is made, the state change is pushed to the RootState
.
All connected controllers with the same state key are also updated.
The RootState class has a small set of static methods. It is mostly used for internal purposes but has a few methods that may be useful for logging/debugging purposes.
The most important being the addRootStateChangeEventListener
which provides updates to
every change made to the RootState:
1import { RootState, RootStateChangeEvent } from "@domx/statecontroller" 2 3const abortController = new AbortController(); 4RootState.addRootStateChangeEventListener((event:RootStateChangeEvent) => { 5 const changeEvent = event.changeEvent; // the Event or string description for the change 6 const rootState = event.rootState; // the key/object state mapping 7 8 // add logging or a break point for debugging 9 10}, abortController.signal); 11 12// detach the listener 13abortController.abort();
The
RootStateChangeEvent
contains a handful of properties, the most useful being thechangeEvent
and therootState
.
The abort controller is optional and allows you to detach the listener when calling abort.
The StateController contains a method that can connect the RootState to Redux DevTools.
1import { connectRdtLogger } from "@domx/statecontroller"; 2 3connectRdtLogger("my-logger");
The method takes a single optional argument which will be the name of the RDT instance. The default is the document title.
This method returns an instance of the logger which can be used to disconnect the logger.
1const rdtLogger = connectRdtLogger("my-logger"); 2rdtLogger.disconnect();
StateControllers can use other StateControllers which can provide for some useful patterns and code re-use:
1class UserProductsController implements StateController { 2 private userState: UserStateController; 3 private productsState: ProductsStateController; 4 5 constructor(host: LitElement) { 6 this.userState = new UserStateController(host); 7 this.productsState = new ProductsStateController(host); 8 } 9 10 get user() { return this.userState.user; } 11 get userProducts() { return this.productsState.products; } 12}
The getters can also do additional transformations of the data specific to the
UserProductsController
Anytime the state is updated a noop stateUpdated
method is called on the controller.
This allows controllers to react to other controllers being updated and can be useful during composition.
1class UserProductsController implements StateController { 2 private userState: UserStateController; 3 private productsState: ProductsStateController; 4 5 constructor(host: LitElement) { 6 this.userState = new UserStateController(host); 7 this.userState.stateUpdated = this.stateUpdated; 8 this.productsState = new ProductsStateController(host); 9 this.productState.stateUpdated = this.stateUpdated; 10 } 11 12 // override the base class stateUpdated noop method 13 stateUpdated() { 14 // react to state change 15 } 16 17 get user() { return this.userState.user; } 18 get userProducts() { return this.productsState.products; } 19}
No vulnerabilities found.
Reason
no binaries found in the repo
Reason
0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Reason
Found 0/12 approved changesets -- score normalized to 0
Reason
no effort to earn an OpenSSF best practices badge detected
Reason
security policy file not detected
Details
Reason
license file not detected
Details
Reason
project is not fuzzed
Details
Reason
branch protection not enabled on development/release branches
Details
Reason
SAST tool is not run on all commits -- score normalized to 0
Details
Reason
19 existing vulnerabilities detected
Details
Score
Last Scanned on 2025-01-27
The Open Source Security Foundation is a cross-industry collaboration to improve the security of open source software (OSS). The Scorecard provides security health metrics for open source projects.
Learn More