-
-
Notifications
You must be signed in to change notification settings - Fork 6
Add react implementation of state management #124
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
base: ui-controller
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,108 @@ | ||||||
| # State Management Core | ||||||
|
|
||||||
| This package provides a simple interface for working with multiple reactive framworks. | ||||||
|
|
||||||
| It assumes a view/view model architecture for your app. The view is responsible for what | ||||||
| the user sees, and the view model is responsible for the stateful logic that makes the view | ||||||
| operational. | ||||||
|
|
||||||
| This package establishes a convention for writing a view model in vanilla JavaScript and | ||||||
| your view in React, Svelte, or some other reactive framework. | ||||||
|
|
||||||
| This package exists for view models that are exposed as libraries. Since your view model | ||||||
| is not tied to a specific framework, developers can write their own views using their | ||||||
| framework of choice. | ||||||
|
|
||||||
| ## Fields | ||||||
|
|
||||||
| A field represents a single piece of reactive data. Its usage is best explained with an example. | ||||||
|
|
||||||
| Suppose your app has a search bar. Every time the user changes the search text, the app | ||||||
| should perform a search. | ||||||
|
|
||||||
| Here's how you would define the search text field: | ||||||
|
|
||||||
| ```js | ||||||
| const searchText = new Field("", (newSearchText) => { | ||||||
| // This callback runs whenever the user changes the search text. | ||||||
| searchFor(newSearchText); | ||||||
| }); | ||||||
|
|
||||||
| // Get current value of the field | ||||||
| let text = searchText.value; | ||||||
|
|
||||||
| // Update the field and perform the search. | ||||||
| // This is how the UI should update the field. | ||||||
| searchText.requestUpdate("hello world"); | ||||||
|
|
||||||
| // Update the field without performing the search. | ||||||
| // Your program can use this method internally to update the field without side effects. | ||||||
| searchText.value = "abc"; | ||||||
| ``` | ||||||
|
|
||||||
| ## View Models | ||||||
|
|
||||||
| In the context of this package, a view model is an object with fields. | ||||||
| To define a view model, write a function like the following: | ||||||
|
|
||||||
| ```js | ||||||
| function usePersonViewModel() { | ||||||
| // The view model can have fields... | ||||||
| const name = new Field(); | ||||||
| const age = new Field(); | ||||||
|
|
||||||
| // ...and actions | ||||||
| function haveBirthday() { | ||||||
| age.value++; | ||||||
| } | ||||||
|
|
||||||
| return { name, age, haveBirthday }; | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| ## Usage in UI framework | ||||||
|
|
||||||
| Framework-specific adapters make view models accessible to your UI. For example, | ||||||
| if you framework is Svelte, use the `state-management-svelte` package: | ||||||
|
|
||||||
| ```svelte | ||||||
| <script> | ||||||
| import { svelteViewModel } from "@ethnolib/state-management-svelte"; | ||||||
|
|
||||||
| // svelteViewModel() turns all fields into reactive Svelte properties | ||||||
| const person = svelteViewModel(usePersonViewModel()) | ||||||
|
|
||||||
| person.name = "John" // Behind the scenes, this calls `requestUpdate` | ||||||
| </script> | ||||||
|
|
||||||
| <p>Hello, {person.name}!</p> | ||||||
| ``` | ||||||
|
|
||||||
| For more details, see documentation for `state-management-svelte` or the adapter for your | ||||||
| framework of choice. | ||||||
|
|
||||||
| Behind the scenes, the adapter does something like this to keep the view model in sync with the UI: | ||||||
|
|
||||||
| ```js | ||||||
| const person = usePersonViewModel(); | ||||||
|
|
||||||
| // Replace `defineReactiveState` with your framework's mechanism | ||||||
| const reactiveName = defineReactiveSate(); | ||||||
|
||||||
| const reactiveName = defineReactiveSate(); | |
| const reactiveName = defineReactiveState(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from "./src/use-field"; |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,28 @@ | ||||||||||||
| { | ||||||||||||
| "name": "@ethnolib/state-management-react", | ||||||||||||
| "description": "An adapter to use @ethnolib/state-management-core with React", | ||||||||||||
| "author": "SIL Global", | ||||||||||||
| "license": "MIT", | ||||||||||||
| "version": "0.1.0", | ||||||||||||
| "main": "./index.js", | ||||||||||||
| "types": "./index.d.ts", | ||||||||||||
| "scripts": { | ||||||||||||
| "build": "nx vite:build", | ||||||||||||
| "typecheck": "tsc", | ||||||||||||
| "test": "nx vite:test --config vitest.config.ts", | ||||||||||||
| "testonce": "nx vite:test --config vitest.config.ts --run", | ||||||||||||
| "lint": "eslint ." | ||||||||||||
| }, | ||||||||||||
| "devDependencies": { | ||||||||||||
| "@nx/vite": "^19.1.2", | ||||||||||||
| "@types/react": "^17", | ||||||||||||
| "@types/react-dom": "^17", | ||||||||||||
| "@types/node": "^20.16.11", | ||||||||||||
| "@vitejs/plugin-react-swc": "^3.8.0", | ||||||||||||
| "tsx": "^4.19.2", | ||||||||||||
| "typescript": "^5.2.2" | ||||||||||||
| }, | ||||||||||||
|
||||||||||||
| }, | |
| }, | |
| "peerDependencies": { | |
| "react": "^17.0.0 || ^18.0.0" | |
| }, |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import { useState } from "react"; | ||
| import { Field } from "@ethnolib/state-management-core"; | ||
|
|
||
| export function useField<T>(field: Field<T>): [T, (value: T) => void] { | ||
| const [fieldValue, _setFieldValue] = useState(field.value); | ||
|
|
||
| function setFieldValue(value: T) { | ||
| field.requestUpdate(value); | ||
| _setFieldValue(value); | ||
| } | ||
|
|
||
| field.updateUI = (value) => _setFieldValue(value); | ||
|
||
|
|
||
| return [fieldValue, setFieldValue]; | ||
| } | ||
|
Comment on lines
+4
to
+15
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| { | ||
| "compilerOptions": { | ||
| "jsx": "react-jsx", | ||
| "allowJs": false, | ||
| "esModuleInterop": false, | ||
| "allowSyntheticDefaultImports": true, | ||
| "strict": true | ||
| }, | ||
| "ts-node": { | ||
| "moduleTypes": { | ||
| "*": "esm" | ||
| } | ||
| }, | ||
| "files": [], | ||
| "include": [], | ||
| "references": [ | ||
| { | ||
| "path": "./tsconfig.lib.json" | ||
| } | ||
| ], | ||
| "extends": "../../../tsconfig.base.json" | ||
| } |
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,28 @@ | ||||
| { | ||||
| "extends": "./tsconfig.json", | ||||
| "compilerOptions": { | ||||
| "outDir": "../../../dist/out-tsc", | ||||
| "types": ["node", "vite/client"], | ||||
| "composite": true, | ||||
| "declaration": true, | ||||
| "declarationMap": true | ||||
| }, | ||||
| "exclude": [ | ||||
| "langtagProcessing.ts", | ||||
|
||||
| "langtagProcessing.ts", |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,36 @@ | ||||||
| /// <reference types='vitest' /> | ||||||
| import { defineConfig } from "vite"; | ||||||
| import dts from "vite-plugin-dts"; | ||||||
| import * as path from "path"; | ||||||
| import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin"; | ||||||
|
|
||||||
| export default defineConfig({ | ||||||
| root: __dirname, | ||||||
| cacheDir: | ||||||
| "../../../node_modules/.vite/components/state-management/state-management-core", | ||||||
|
||||||
| "../../../node_modules/.vite/components/state-management/state-management-core", | |
| "../../../node_modules/.vite/components/state-management/state-management-react", |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The library name is incorrectly set to "@ethnolib/find-language". It should be "@ethnolib/state-management-react" to match the package name.
| name: "@ethnolib/find-language", | |
| name: "@ethnolib/state-management-react", |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| /// <reference types="vitest" /> | ||
| import { defineConfig } from "vite"; | ||
|
|
||
| export default defineConfig({ | ||
| test: { | ||
| expect: { | ||
| requireAssertions: true, | ||
| }, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: "framworks" should be "frameworks"