{"id":15063607,"url":"https://github.com/guess/live-view-model","last_synced_at":"2025-04-10T11:25:23.512Z","repository":{"id":255446183,"uuid":"852324936","full_name":"guess/live-view-model","owner":"guess","description":"⚡️ Communicate with Phoenix Channels \u0026 sync state with mobx ","archived":false,"fork":false,"pushed_at":"2024-11-09T15:24:50.000Z","size":7162,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-24T10:12:01.820Z","etag":null,"topics":["elixir","phoenix","phoenix-channels","phoenix-framework","websockets"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/guess.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-09-04T16:00:46.000Z","updated_at":"2025-01-23T19:37:32.000Z","dependencies_parsed_at":"2024-09-14T15:26:18.965Z","dependency_job_id":"a4476eaf-2c79-43f5-8c06-9af13842985a","html_url":"https://github.com/guess/live-view-model","commit_stats":null,"previous_names":["guess/live-view-model"],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guess%2Flive-view-model","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guess%2Flive-view-model/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guess%2Flive-view-model/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guess%2Flive-view-model/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/guess","download_url":"https://codeload.github.com/guess/live-view-model/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248208618,"owners_count":21065203,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["elixir","phoenix","phoenix-channels","phoenix-framework","websockets"],"created_at":"2024-09-25T00:04:49.271Z","updated_at":"2025-04-10T11:25:23.494Z","avatar_url":"https://github.com/guess.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# LiveViewModel\n\n![LiveViewModel](repo.png)\n\nLiveViewModel is an Elixir library for building interactive web and mobile applications with a focus on real-time, event-driven architecture. It offers an alternative approach to state management and client-server communication, particularly suited for applications that require real-time updates and don't rely on server-side HTML rendering.\n\nThis project is currently under active development. The API and features are subject to change.\n\n**Use with caution in production environments.**\n\n## Key features\n\n- 🏛️ **Centralized State Management**: Application state is maintained on the server, reducing complexity in state synchronization.\n- 🎭 **Event-Driven Architecture**: Clients dispatch events to the server, which handles them and updates the state accordingly.\n- ⚡ **Real-Time Updates**: The server pushes state changes to clients, facilitating real-time interactivity.\n- 🧘 **Simplified Client Logic**: Client-side code primarily focuses on rendering state and dispatching events.\n- 🌐 **Platform Agnostic**: Suitable for web applications and mobile apps that manage their own UI rendering.\n- 🏷️ **TypeScript Support**: Includes TypeScript definitions for improved developer experience.\n- 🔄 **Reactive Programming**: Utilizes RxJS for handling asynchronous events and state changes.\n- 🔍 **MobX Integration**: Leverages MobX for efficient client-side state management and reactivity.\n\n## How it works\n\n1. Clients connect to the server using WebSocket or long-polling.\n2. Clients send events to the server using a defined protocol.\n3. The server processes events and updates the application state.\n4. Updated state is sent back to clients for rendering, either as full state updates or optimized patches.\n\n## Table of contents\n\n- [Server-side components](#server-side-components)\n- [Client-side components](#client-side-components)\n- [Use cases](#use-cases)\n- [Getting started](#getting-started)\n  - [Server setup](#server-side-setup)\n  - [Client setup](#client-side-setup)\n- [Decorators](#decorators)\n  - [@liveViewModel](#liveviewmodel-1)\n  - [@liveObservable](#liveobservable)\n  - [@localObservable](#localobservable)\n  - [@liveEvent](#liveevent)\n  - [@handleEvent](#handleevent)\n  - [@liveError](#liveerror)\n  - [@action](#action)\n  - [@computed](#computed)\n- [Advanced features](#advanced-features)\n- [Testing](#testing)\n- [Using with React](#using-with-react)\n- [Comparison to LiveView](#comparison-to-liveview)\n- [Contributing](#contributing)\n- [License](#license)\n\n## Server-side components\n\n- `LiveViewModel.Channel`: A behavior module for creating Phoenix channels that handle LiveViewModel logic.\n- `LiveViewModel.Encoder`: A protocol for customizing how data is encoded before being sent to clients.\n- `LiveViewModel.Event`: A struct representing events that can be sent from the server to clients.\n- `LiveViewModel.MessageBuilder`: A module for creating state change and patch messages.\n\n## Client-side components\n\n- `LiveConnection`: Manages the connection to the server and provides methods for joining channels and sending events.\n- `LiveViewModel`: A decorator and base class for creating view models that sync with the server state.\n- Various decorators (`@liveObservable`, `@localObservable`, `@action`, `@computed`, `@liveEvent`, `@handleEvent`, `@liveError`) for defining reactive properties and methods.\n\n## Use cases\n\nLiveViewModel is particularly well-suited for:\n\n- Real-time dashboards and monitoring applications\n- Collaborative tools and multi-user applications\n- Mobile applications that require live updates from a server\n- Single-page applications (SPAs) with complex state management needs\n- Any scenario where a unified backend can serve multiple client types (web, mobile, etc.)\n\n## Getting started\n\n### Server setup\n\n1. Add LiveViewModel to your dependencies in `mix.exs`:\n\n   ```elixir\n   defp deps do\n     [\n       {:live_view_model, \"~\u003e 0.3\"}\n     ]\n   end\n   ```\n\n2. Create a channel using `LiveViewModel.Channel`:\n\n   ```elixir\n   defmodule MyAppWeb.MyChannel do\n     use LiveViewModel.Channel, web_module: MyAppWeb\n\n     @impl true\n     def init(_channel, _payload, _socket) do\n       {:ok, %{count: 0}}\n     end\n\n     @impl true\n     def handle_event(\"update_count\", %{\"value\" =\u003e value}, state) do\n       {:noreply, %{state | count: value}}\n     end\n   end\n   ```\n\n3. Add the channel to your socket in `lib/my_app_web/channels/user_socket.ex`:\n\n   ```elixir\n   defmodule MyAppWeb.UserSocket do\n     use Phoenix.Socket\n\n     channel \"room:*\", MyAppWeb.MyChannel\n\n     # ... rest of the socket configuration\n   end\n   ```\n\n### Client setup\n\n1. Install the npm package:\n\n   ```bash\n   npm install live-view-model\n   ```\n\n2. Create a view model:\n\n   ```typescript\n   import {\n     liveViewModel,\n     LiveConnection,\n     liveObservable,\n     liveEvent,\n   } from \"live-view-model\";\n\n   @liveViewModel(\"room:lobby\")\n   class MyViewModel {\n     constructor(private conn: LiveConnection) {}\n\n     @liveObservable()\n     count: number = 0;\n\n     @liveEvent(\"update_count\")\n     updateCount(value: number) {\n       return { value };\n     }\n   }\n   ```\n\n3. Connect and use the view model:\n\n   ```typescript\n   import { connect, join } from \"live-view-model\";\n\n   const conn = connect(\"ws://localhost:4000/socket\");\n   const viewModel = new MyViewModel(conn);\n   join(viewModel);\n\n   autorun(() =\u003e console.log(\"Count changed:\", viewModel.count));\n\n   viewModel.updateCount(5);\n   viewModel.updateCount(4);\n   ```\n\n## Decorators\n\n### @liveViewModel\n\n`@liveViewModel(topic: string)`\n\nSets up a class as a live view model, connecting it to a specific Phoenix channel.\n\n**Usage:**\n\n```typescript\n@liveViewModel(\"room:{roomId}\")\nclass LobbyViewModel {\n  // ...\n}\n```\n\n**Functionality:**\n\n- Creates a channel subscription based on the provided topic\n- Sets up event listeners for incoming messages\n- Channels can have dynamic topics using `{}` placeholders (e.g., `\"room:{roomId}\"`) that are replaced with the corresponding `params` when `join`ing the channel\n\n### @liveObservable\n\n`@liveObservable(serverKey?: string)`\n\nMarks a property for synchronization with the server and integrates with MobX to create observable properties.\n\n**Usage:**\n\n```typescript\n@liveObservable(\"server_count\")\ncount: number = 0;\n\n@liveObservable.deep()\nmessages: ChatMessage[] = [];\n```\n\n**Functionality:**\n\n- Makes the property a MobX observable\n- Maps the property to a server-side key (uses the property name if not specified)\n- Sets up the property for automatic updates when receiving data from the server\n- Provides variants for different MobX observable types:\n  - `@liveObservable.ref`: Creates a reference observable\n  - `@liveObservable.struct`: Creates a structural observable\n  - `@liveObservable.deep`: Creates a deep observable\n  - `@liveObservable.shallow`: Creates a shallow observable\n\n### @localObservable\n\n`@localObservable()`\n\nMarks a property as a local observable, not synchronized with the server.\n\n**Usage:**\n\n```typescript\n@localObservable()\nlocalCount: number = 0;\n\n@localObservable.ref()\nlocalReference: SomeType | null = null;\n```\n\n**Functionality:**\n\n- Makes the property a MobX observable\n- Does not synchronize the property with the server\n- Provides variants for different MobX observable types:\n  - `@localObservable.ref`: Creates a reference observable\n  - `@localObservable.struct`: Creates a structural observable\n  - `@localObservable.deep`: Creates a deep observable\n  - `@localObservable.shallow`: Creates a shallow observable\n\n### @liveEvent\n\n`@liveEvent(eventName: string)`\n\nDefines a method that sends events to the server when called. Returns the payload to be sent to the server. Alternatively, you can use `pushEvent(eventName, payload)` to send events manually.\n\n**Usage:**\n\n```typescript\n@liveEvent(\"notify\")\nnotify(message: string) {\n  return { message };\n}\n```\n\n**Functionality:**\n\n- Wraps the original method\n- Sends the returned payload to the server using the specified event name\n\n### @handleEvent\n\n`@handleEvent(eventName: string)`\n\nDefines a method that handles events received from the server.\n\n**Usage:**\n\n```typescript\n@handleEvent(\"navigate\")\nhandleMessage(payload: any) {\n  console.log(\"Navigating to:\", payload.path);\n}\n```\n\n## **Functionality:**\n\n- Handles incoming events from the server\n- Calls the decorated method with the event payload from the server\n\n### @liveError\n\n`@liveError()`\n\nSpecifies an error handler for the view model.\n\n**Usage:**\n\n```typescript\n@liveError()\nhandleError(error: any) {\n  console.error(\"View model error:\", error);\n}\n```\n\n**Functionality:**\n\n- Sets up a central error handler for the view model\n- Called when channel errors occur\n\n### @action\n\n`@action()`\n\nAlias for MobX action decorator.\n\n**Usage:**\n\n```typescript\n@action()\nsetCount(count: number) {\n  this.count = count;\n}\n```\n\n**Functionality:**\n\n- Wraps the method in a MobX action for optimal performance when modifying observables\n\n### @computed\n\n`@computed()`\n\nAlias for MobX computed decorator.\n\n**Usage:**\n\n```typescript\n@computed()\nget messageCount() {\n  return this.messages.length;\n}\n```\n\n**Functionality:**\n\n- Creates a MobX computed property, which is automatically updated when its dependencies change\n\n## Advanced features\n\n- **Custom Encoders**: Implement the `LiveViewModel.Encoder` protocol to customize how data is serialized before being sent to clients.\n\n## Testing\n\nThe library includes `LiveViewModel.TestHelpers` module for writing tests for your LiveViewModel channels.\n\n## Using with React\n\nLiveViewModel integrates seamlessly with React using mobx-react-lite for efficient rendering and state management. Here's an example of how to use LiveViewModel in a React component:\n\n1. First, install the necessary dependencies:\n\n```bash\nnpm install live-view-model mobx mobx-react-lite react\n```\n\n2. Create your recat components:\n\n```tsx\nimport React, { useMemo, useEffect } from 'react';\nimport { observer } from 'mobx-react-lite';\nimport { connect, join, leave } from 'live-view-model'';\n\nconst App = () =\u003e {\n  const conn = useMemo(() =\u003e {\n    return connect('ws://localhost:4000/socket');\n  }, []);\n\n  return (\n    \u003cLobbyComponent conn={conn} /\u003e\n  );\n}\n\nconst LobbyComponent = observer(({ conn }) =\u003e {\n  const vm = useMemo(() =\u003e {\n    return new LobbyViewModel(conn);\n  }, [conn]);\n\n  useEffect(() =\u003e {\n    join(vm);\n    return () =\u003e leave(vm);\n  }, [vm]);\n\n  return (\n    \u003cdiv\u003e\n      \u003ch1\u003eLobby\u003c/h1\u003e\n      \u003cp\u003eCount: {viewModel.count}\u003c/p\u003e\n      \u003cbutton onClick={() =\u003e viewModel.increment()}\u003eIncrement\u003c/button\u003e\n      \u003cbutton onClick={() =\u003e viewModel.decrement()}\u003eDecrement\u003c/button\u003e\n    \u003c/div\u003e\n  );\n});\n\nexport default App;\n```\n\n## Comparison to LiveView\n\nWhile LiveViewModel shares similar goals with Phoenix LiveView, it takes a different approach:\n\n- LiveView manages both server logic and view presentation in Elixir, primarily for web applications.\n- LiveViewModel handles server logic in Elixir but relies on client-side code for rendering, making it adaptable for both web and mobile platforms.\n\nThis distinction allows LiveViewModel to be used in scenarios where full server-side rendering is not possible or desirable, such as in native mobile applications.\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## License\n\n[MIT License](LICENSE.md)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fguess%2Flive-view-model","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fguess%2Flive-view-model","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fguess%2Flive-view-model/lists"}