Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[labs/redux] Reactive controller for subscribing to a Redux store #4586

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
5 changes: 5 additions & 0 deletions .changeset/seven-camels-roll.md
@@ -0,0 +1,5 @@
---
'@lit-labs/redux': minor
---

Initial release of `@lit-labs/redux`, a package containing bindings for Redux for use in Lit elements.
6 changes: 6 additions & 0 deletions .eslintignore
Expand Up @@ -310,6 +310,12 @@ packages/labs/react/index.*
packages/labs/react/create-component.*
packages/labs/react/use-controller.*

packages/labs/redux/development/
packages/labs/redux/lib/
packages/labs/redux/test/
packages/labs/redux/node_modules/
packages/labs/redux/index.*

packages/labs/rollup-plugin-minify-html-literals/node_modules
packages/labs/rollup-plugin-minify-html-literals/coverage
packages/labs/rollup-plugin-minify-html-literals/.nyc_output
Expand Down
6 changes: 6 additions & 0 deletions .prettierignore
Expand Up @@ -298,6 +298,12 @@ packages/labs/react/index.*
packages/labs/react/create-component.*
packages/labs/react/use-controller.*

packages/labs/redux/development/
packages/labs/redux/lib/
packages/labs/redux/test/
packages/labs/redux/node_modules/
packages/labs/redux/index.*

packages/labs/rollup-plugin-minify-html-literals/node_modules
packages/labs/rollup-plugin-minify-html-literals/coverage
packages/labs/rollup-plugin-minify-html-literals/.nyc_output
Expand Down
2 changes: 2 additions & 0 deletions examples/redux/.gitignore
@@ -0,0 +1,2 @@
/node_modules/
/dist/
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

who's writing to /dist/, Vite? Leave a comment?

3 changes: 3 additions & 0 deletions examples/redux/README.md
@@ -0,0 +1,3 @@
# Lit Labs Redux example

Adaptation of the [Redux Counter Example App](https://redux.js.org/tutorials/essentials/part-2-app-structure#the-counter-example-app) built with Lit, using `@lit-labs/redux`.
6 changes: 6 additions & 0 deletions examples/redux/assets/lit.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions examples/redux/assets/redux.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions examples/redux/index.html
@@ -0,0 +1,12 @@
<head>
<title>Lit Labs Redux</title>
<script type="module" src="./src/index.ts"></script>
<style>
:root {
font-family: sans-serif;
}
</style>
</head>
<body>
<my-app></my-app>
</body>
35 changes: 35 additions & 0 deletions examples/redux/package.json
@@ -0,0 +1,35 @@
{
"name": "@lit-examples/redux",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "wireit",
"build": "wireit",
"test": "wireit"
},
"dependencies": {
"@lit-labs/redux": "^0.0.0",
"@reduxjs/toolkit": "^2.1.0",
"lit": "^3.0.0"
},
"devDependencies": {
"vite": "^5.1.6"
},
"wireit": {
"dev": {
"command": "vite",
"service": true,
"files": [
"./src/",
"./index.html"
]
},
"build": {
"command": "tsc && vite build"
},
"test": {
"command": "echo \"TODO\""
}
}
}
14 changes: 14 additions & 0 deletions examples/redux/src/app-connector.ts
@@ -0,0 +1,14 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

import {Connector} from '@lit-labs/redux';
import type {AppStore} from './store.js';

// Create a Selector controller providing the AppStore type for better type
// checks on selector functions and returned values.
// Similar idea as:
// https://redux.js.org/tutorials/typescript-quick-start#define-typed-hooks
export const AppConnector = Connector.withStoreType<AppStore>();
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>`;
}
}
58 changes: 58 additions & 0 deletions examples/redux/src/count-incrementor.ts
@@ -0,0 +1,58 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

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

@customElement('count-incrementor')
export class CountIncrementor extends LitElement {
@dispatch()
_dispatch!: AppDispatch;

@state()
_incrementAmount = '2';

_handleInput(e: InputEvent) {
this._incrementAmount = (e.target as HTMLInputElement).value;
}

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

render() {
return html`
<div class="row">
<input @input=${this._handleInput} .value=${this._incrementAmount} />
<button @click=${this._incrementCountByAmount}>Add Amount</button>
</div>
`;
}

static styles = css`
.row {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
}

button {
font-size: 1.5rem;
padding: 10px;
}

input {
font-size: 1.5rem;
padding: 10px;
width: 100px;
text-align: center;
}
`;
}
35 changes: 35 additions & 0 deletions examples/redux/src/counter-slice.ts
@@ -0,0 +1,35 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

import {type PayloadAction, createSlice} from '@reduxjs/toolkit';

export type CounterState = {
value: number;
};

const initialState: CounterState = {
value: 0,
};

export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});

export const {increment, decrement, incrementByAmount} = counterSlice.actions;

export default counterSlice.reducer;
54 changes: 54 additions & 0 deletions examples/redux/src/index.ts
@@ -0,0 +1,54 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

import {html, css, LitElement} from 'lit';
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 {
// Provide the Redux store in a context to any children of this component.
@provide({
context: storeContext,
})
_store = store;

render() {
return html`
<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`
:host {
font-size: 2rem;
}

main {
display: flex;
flex-direction: column;
align-items: center;
gap: 2rem;
}

img {
height: 150px;
width: 150px;
}
`;
}
50 changes: 50 additions & 0 deletions examples/redux/src/my-counter.ts
@@ -0,0 +1,50 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

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';

@customElement('my-counter')
export class MyCounter extends LitElement {
// Select the counter value from the Redux store from parent context.
_connector = new AppConnector(this, {
selector: (state) => state.counter.value,
});

_incrementCount() {
this._connector.dispatch(increment());
}

_decrementCount() {
this._connector.dispatch(decrement());
}

render() {
return html`
<div>
<button @click=${this._incrementCount}>+</button>
<span>${this._connector.selected}</span>
<button @click=${this._decrementCount}>−</button>
</div>
`;
}

static styles = css`
div {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
}

button {
font-size: 1.5rem;
padding: 10px;
}
`;
}
18 changes: 18 additions & 0 deletions examples/redux/src/store.ts
@@ -0,0 +1,18 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

import {configureStore} from '@reduxjs/toolkit';
import counterReducer from './counter-slice.js';

export const store = configureStore({
reducer: {
counter: counterReducer,
},
});

export type AppStore = typeof store;
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
24 changes: 24 additions & 0 deletions examples/redux/tsconfig.json
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2021",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": ["ES2021", "DOM", "DOM.Iterable"],
"skipLibCheck": true,

/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,

/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}