Skip to content

Commit

Permalink
Add select and dispatch decorators
Browse files Browse the repository at this point in the history
  • Loading branch information
augustjk committed Mar 20, 2024
1 parent 25ccdc4 commit 9bd7532
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 38 deletions.
20 changes: 20 additions & 0 deletions examples/redux/src/count-display.ts
@@ -0,0 +1,20 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

import {html, LitElement} from 'lit';
import {customElement} from 'lit/decorators.js';
import {select} from '@lit-labs/redux';
import type {RootState} from './store.js';

@customElement('count-display')
export class CountDisplay extends LitElement {
@select((state: RootState) => state.counter.value)
_count!: number;

render() {
return html` <p>The count is: ${this._count}</p> `;
}
}
Expand Up @@ -6,13 +6,14 @@

import {html, css, LitElement} from 'lit';
import {customElement, state} from 'lit/decorators.js';
import {AppConnector} from './app-connector.js';
import {incrementByAmount} from './counter-slice.js';
import {dispatch} from '@lit-labs/redux';
import type {AppDispatch} from './store.js';

@customElement('my-incrementor')
export class MyIncrementor extends LitElement {
// Instantiate connector without selector to just access dispatch.
_connector = new AppConnector(this);
@customElement('count-incrementor')
export class CountIncrementor extends LitElement {
@dispatch()
_dispatch!: AppDispatch;

@state()
_incrementAmount = '2';
Expand All @@ -22,9 +23,7 @@ export class MyIncrementor extends LitElement {
}

_incrementCountByAmount() {
this._connector.dispatch(
incrementByAmount(Number(this._incrementAmount) || 0)
);
this._dispatch(incrementByAmount(Number(this._incrementAmount) || 0));
}

render() {
Expand Down
30 changes: 20 additions & 10 deletions examples/redux/src/index.ts
Expand Up @@ -9,6 +9,8 @@ import {customElement} from 'lit/decorators.js';
import {provide, storeContext} from '@lit-labs/redux';
import {store} from './store.js';
import './my-counter.js';
import './count-incrementor.js';
import './count-display.js';

@customElement('my-app')
export class MyApp extends LitElement {
Expand All @@ -20,25 +22,33 @@ export class MyApp extends LitElement {

render() {
return html`
<div>
<img alt="Lit Logo" src="/assets/lit.svg" />
<img alt="Redux Logo" src="/assets/redux.svg" />
</div>
<my-counter></my-counter>
<main>
<div>
<img alt="Lit Logo" src="/assets/lit.svg" />
<img alt="Redux Logo" src="/assets/redux.svg" />
</div>
<my-counter></my-counter>
<count-incrementor></count-incrementor>
<count-display></count-display>
</main>
`;
}

static styles = css`
div {
:host {
font-size: 2rem;
}
main {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
gap: 2rem;
padding: 2rem;
}
img {
max-height: 150px;
max-width: 150px;
height: 150px;
width: 150px;
}
`;
}
25 changes: 5 additions & 20 deletions examples/redux/src/my-counter.ts
Expand Up @@ -8,7 +8,6 @@ import {html, css, LitElement} from 'lit';
import {customElement} from 'lit/decorators.js';
import {AppConnector} from './app-connector.js';
import {increment, decrement} from './counter-slice.js';
import './my-incrementor.js';

@customElement('my-counter')
export class MyCounter extends LitElement {
Expand All @@ -27,30 +26,16 @@ export class MyCounter extends LitElement {

render() {
return html`
<div class="container">
<div class="row">
<button @click=${this._incrementCount}>+</button>
<span>${this._connector.selected}</span>
<button @click=${this._decrementCount}></button>
</div>
<my-incrementor></my-incrementor>
<div>
<button @click=${this._incrementCount}>+</button>
<span>${this._connector.selected}</span>
<button @click=${this._decrementCount}></button>
</div>
`;
}

static styles = css`
:host {
font-size: 2rem;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.row {
div {
display: flex;
justify-content: center;
align-items: center;
Expand Down
1 change: 1 addition & 0 deletions packages/labs/redux/src/index.ts
Expand Up @@ -6,4 +6,5 @@

export * from './lib/connector.js';
export * from './lib/store-context.js';
export * from './lib/decorators.js';
export {provide, ContextProvider} from '@lit/context';
123 changes: 123 additions & 0 deletions packages/labs/redux/src/lib/decorators.ts
@@ -0,0 +1,123 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

import {ReactiveElement} from '@lit/reactive-element';
import {Connector, type ConnectorOptions} from './connector.js';
import type {Store} from '@reduxjs/toolkit';

export function select<S extends Store, V>(
selector: ConnectorOptions<S, V>['selector'],
equalityCheck?: ConnectorOptions<S, V>['equalityCheck']
): ConnectorDecorator;
export function select<S extends Store, V>(
options: ConnectorOptions<S, V>
): ConnectorDecorator;
export function select<S extends Store, V>(
selectorOrOptions:
| ConnectorOptions<S, V>['selector']
| ConnectorOptions<S, V>,
equalityCheck?: ConnectorOptions<S, V>['equalityCheck']
): ConnectorDecorator {
return <C extends ReactiveElement>(
protoOrTarget: ClassAccessorDecoratorTarget<C, V>,
nameOrContext: PropertyKey | ClassAccessorDecoratorContext<C, V>
) => {
let options: ConnectorOptions<S, V> = {};
if (typeof selectorOrOptions === 'object') {
options = selectorOrOptions;
} else {
options.selector = selectorOrOptions;
options.equalityCheck = equalityCheck;
}
// Map of instances to controllers
const controllerMap = new WeakMap<ReactiveElement, Connector<S, V>>();
if (typeof nameOrContext === 'object') {
// Standard decorators branch
nameOrContext.addInitializer(function (this: ReactiveElement) {
controllerMap.set(this, new Connector(this, options));
});
return {
get(this: ReactiveElement) {
return controllerMap.get(this)!.selected;
},
};
} else {
// Experimental decorators branch
(protoOrTarget.constructor as typeof ReactiveElement).addInitializer(
(element: ReactiveElement): void => {
controllerMap.set(element, new Connector(element, options));
}
);
Object.defineProperty(protoOrTarget, nameOrContext, {
get(this: ReactiveElement) {
return controllerMap.get(this)!.selected;
},
});
return;
}
};
}

export function dispatch<S extends Store>(): ConnectorDecorator {
return <C extends ReactiveElement>(
protoOrTarget: ClassAccessorDecoratorTarget<C, S['dispatch']>,
nameOrContext: PropertyKey | ClassAccessorDecoratorContext<C, S['dispatch']>
) => {
// Map of instances to controllers
const controllerMap = new WeakMap<ReactiveElement, Connector<S, never>>();
if (typeof nameOrContext === 'object') {
// Standard decorators branch
nameOrContext.addInitializer(function (this: ReactiveElement) {
controllerMap.set(this, new Connector(this));
});
return {
get(this: ReactiveElement) {
return controllerMap.get(this)!.dispatch;
},
};
} else {
// Experimental decorators branch
(protoOrTarget.constructor as typeof ReactiveElement).addInitializer(
(element: ReactiveElement): void => {
controllerMap.set(element, new Connector(element));
}
);
Object.defineProperty(protoOrTarget, nameOrContext, {
get(this: ReactiveElement) {
return controllerMap.get(this)!.dispatch;
},
});
return;
}
};
}

/**
* Generates a public interface type that removes private and protected fields.
* This allows accepting otherwise compatible versions of the type (e.g. from
* multiple copies of the same package in `node_modules`).
*/
type Interface<T> = {
[K in keyof T]: T[K];
};

type ConnectorDecorator = {
// legacy
<
K extends PropertyKey,
Proto extends Interface<Omit<ReactiveElement, 'renderRoot'>>,
>(
protoOrDescriptor: Proto,
name?: K
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): any;

// standard
<C extends Interface<Omit<ReactiveElement, 'renderRoot'>>, V>(
value: ClassAccessorDecoratorTarget<C, V>,
context: ClassAccessorDecoratorContext<C, V>
): void;
};

0 comments on commit 9bd7532

Please sign in to comment.