{"id":30376595,"url":"https://github.com/pringels/sharedassigns","last_synced_at":"2025-10-10T15:33:29.341Z","repository":{"id":303476883,"uuid":"1015639508","full_name":"Pringels/SharedAssigns","owner":"Pringels","description":"React Context-like library for Phoenix LiveView","archived":false,"fork":false,"pushed_at":"2025-07-09T07:35:45.000Z","size":16972,"stargazers_count":0,"open_issues_count":3,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-10-10T10:57:33.255Z","etag":null,"topics":["elixir","elixir-library","elixir-phoenix","liveview","phoenix","phoenix-framework","phoenix-liveview"],"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/Pringels.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,"zenodo":null}},"created_at":"2025-07-07T20:11:48.000Z","updated_at":"2025-07-10T13:53:51.000Z","dependencies_parsed_at":"2025-07-07T22:52:49.222Z","dependency_job_id":null,"html_url":"https://github.com/Pringels/SharedAssigns","commit_stats":null,"previous_names":["pringels/liveview_shared_assigns","pringels/sharedassigns"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/Pringels/SharedAssigns","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Pringels%2FSharedAssigns","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Pringels%2FSharedAssigns/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Pringels%2FSharedAssigns/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Pringels%2FSharedAssigns/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Pringels","download_url":"https://codeload.github.com/Pringels/SharedAssigns/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Pringels%2FSharedAssigns/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279004574,"owners_count":26083736,"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","status":"online","status_checked_at":"2025-10-10T02:00:06.843Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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","elixir-library","elixir-phoenix","liveview","phoenix","phoenix-framework","phoenix-liveview"],"created_at":"2025-08-20T15:01:16.434Z","updated_at":"2025-10-10T15:33:29.336Z","avatar_url":"https://github.com/Pringels.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# !!! WIP - DO NOT USE IN PRODUCTION !!! #\n\n\n![Gemini_Generated_Image_1pq9o41pq9o41pq9](https://github.com/user-attachments/assets/b843a628-e3a9-4b70-949f-6ecd8e2e257b)\n\n\n\n# SharedAssigns\n\nA React Context-like library for Phoenix LiveView that eliminates prop drilling by allowing components to subscribe to specific context values and automatically re-render when those contexts change.\n\n## Features\n\n- 🚀 **Zero boilerplate** - Declarative API with simple macros\n- ⚡ **Explicit assigns-based** - Pure explicit assigns, no process dictionary\n- 🔄 **Reactive components** - Automatic `send_update/3` when contexts change\n- 🎯 **Granular subscriptions** - Components subscribe only to needed contexts\n- 📦 **Automatic context injection** - No manual prop drilling required\n- ✨ **Seamless integration** - Works naturally with Phoenix LiveView\n- 🧪 **Fully tested** - Comprehensive test suite included\n\n## Quick Start Demo\n\n```bash\n./start_demo.sh\n# Opens http://localhost:4000 - Live browser demo!\n```\n\n## Installation\n\nAdd `shared_assigns` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:shared_assigns, \"~\u003e 0.1.0\"}\n  ]\nend\n```\n\n## Basic Usage\n\n### 1. Define a Provider (LiveView)\n\n```elixir\ndefmodule MyAppWeb.PageLive do\n  use MyAppWeb, :live_view\n  use SharedAssigns.Provider,\n    contexts: [\n      theme: \"light\",\n      user_role: \"guest\",\n      notifications: []\n    ]\n\n  def handle_event(\"toggle_theme\", _params, socket) do\n    new_theme = if get_context(socket, :theme) == \"light\", do: \"dark\", else: \"light\"\n    {:noreply, put_context(socket, :theme, new_theme)}\n  end\n\n  def render(assigns) do\n    ~H\"\"\"\n    \u003cdiv\u003e\n      \u003c!-- Automatic context injection as explicit assigns --\u003e\n      \u003c.sa_live_component module={MyAppWeb.HeaderComponent} id=\"header\" /\u003e\n      \n      \u003cbutton phx-click=\"toggle_theme\"\u003e\n        Toggle Theme (Current: \u003c%= @theme %\u003e)\n      \u003c/button\u003e\n    \u003c/div\u003e\n    \"\"\"\n  end\nend\n```\n\n### 2. Create Consumer Components\n\n```elixir\ndefmodule MyAppWeb.HeaderComponent do\n  use MyAppWeb, :live_component\n\n  # Declare which contexts this component subscribes to\n  def subscribed_contexts, do: [:theme]\n\n  def render(assigns) do\n    ~H\"\"\"\n    \u003cheader class={[\"header\", @theme == \"dark\" \u0026\u0026 \"dark-theme\"]}\u003e\n      \u003ch1\u003eMy App\u003c/h1\u003e\n      \u003cp\u003eCurrent theme: \u003c%= @theme %\u003e\u003c/p\u003e\n    \u003c/header\u003e\n    \"\"\"\n  end\n\n  def update(assigns, socket) do\n    # Receives context updates via send_update/3\n    {:ok, assign(socket, assigns)}\n  end\nend\n```\n\n### 3. Alternative Helper Function Usage\n\nFor programmatic usage, you can use the `sa_component/2` helper:\n\n```elixir\ndef render(assigns) do\n  ~H\"\"\"\n  \u003c.live_component {sa_component(assigns, module: MyComponent, id: \"my-id\")} /\u003e\n  \"\"\"\nend\n```\n\n## Architecture: Explicit Assigns + send_update/3\n\nSharedAssigns uses a pure explicit assigns approach with reactive updates:\n\n1. **Context Storage**: Contexts stored in `socket.assigns[:__shared_assigns_contexts__]`\n2. **Version Tracking**: Each context has a version number in `socket.assigns[:__shared_assigns_versions__]`\n3. **Context Updates**: When contexts change, Provider calls `send_update/3` to notify subscribing components\n4. **Explicit Assignment**: Context values injected as explicit assigns (e.g., `@theme`, `@user_role`)\n5. **Component Reactivity**: Components automatically update via their `update/2` callback\n\n```\nProvider (LiveView)\n├── Context Storage: socket.assigns[:__shared_assigns_contexts__]\n├── Version Tracking: socket.assigns[:__shared_assigns_versions__]\n├── Context Updates: Trigger send_update/3 to subscribing components\n└── Components receive contexts as explicit assigns\n```\n\n**No process dictionary usage anywhere!**\n\n## API Reference\n\n### Provider Functions\n\nWhen you `use SharedAssigns.Provider`, your LiveView gets these helper functions:\n\n#### `put_context(socket, key, value)`\nSets a context value and automatically triggers `send_update/3` for all consuming components.\n\n```elixir\nsocket = put_context(socket, :theme, \"dark\")\n# This automatically calls send_update/3 for all components that subscribe to :theme\n```\n\n#### `update_context(socket, key, function)`\nUpdates a context value using a function and triggers component updates.\n\n```elixir\nsocket = update_context(socket, :count, \u0026(\u00261 + 1))\n# This increments the count and notifies consuming components via send_update/3\n```\n\n#### `get_context(socket, key)`\nGets the current value of a context.\n\n```elixir\ntheme = get_context(socket, :theme)\n```\n\n#### `context_keys()`\nReturns all available context keys for this provider.\n\n```elixir\nkeys = MyLive.context_keys()  # [:theme, :user_role, :notifications]\n```\n\n### Helper Functions\n\n#### `sa_live_component(opts)`\nThe main macro for creating context-aware LiveComponents. Automatically injects context values as explicit assigns.\n\n```heex\n\u003c.sa_live_component module={MyComponent} id=\"my-id\" /\u003e\n\u003c.sa_live_component module={MyComponent} id=\"my-id\" class=\"custom-class\" /\u003e\n```\n\n#### `sa_component(parent_assigns, opts)`\nHelper function for programmatic context injection.\n\n```elixir\ncomponent_assigns = sa_component(assigns, module: MyComponent, id: \"my-id\")\n```\n\n#### `sa_live_session(id, module, assigns, custom_session \\\\ %{})`\nPrepares LiveView session data with context values for nested LiveViews.\n\n```heex\n\u003c%= live_render(@socket, MyChildLive, sa_live_session(\"child\", MyChildLive, assigns)) %\u003e\n```\n\n## Component Subscription Pattern\n\nComponents declare their context dependencies using the `subscribed_contexts/0` function:\n\n```elixir\ndefmodule MyComponent do\n  use Phoenix.LiveComponent\n\n  # Declare which contexts this component subscribes to\n  def subscribed_contexts, do: [:theme, :user_role]\n\n  def render(assigns) do\n    ~H\"\"\"\n    \u003cdiv class={@theme}\u003eUser: \u003c%= @user_role %\u003e\u003c/div\u003e\n    \"\"\"\n  end\n\n  def update(assigns, socket) do\n    # Contexts are received as explicit assigns\n    {:ok, assign(socket, assigns)}\n  end\nend\n```\n\n## PubSub for Nested LiveViews\n\nFor cross-LiveView context synchronization, add PubSub:\n\n```elixir\ndefmodule MyAppWeb.ParentLive do\n  use SharedAssigns.Provider,\n    contexts: [theme: \"light\", user: %{}],\n    pubsub: MyApp.PubSub  # Enables cross-LiveView sync\nend\n```\n\n## Best Practices\n\n### 1. Keep Contexts Focused\nCreate specific contexts rather than one large context:\n\n```elixir\n# ✅ Good - Focused contexts\nuse SharedAssigns.Provider,\n  contexts: [\n    theme: \"light\",\n    user_role: \"guest\",\n    notifications: []\n  ]\n\n# ❌ Avoid - Monolithic context\nuse SharedAssigns.Provider,\n  contexts: [\n    app_state: %{theme: \"light\", user: %{}, notifications: [], ...}\n  ]\n```\n\n### 2. Subscribe Only to Needed Contexts\nComponents should only subscribe to contexts they actually use:\n\n```elixir\n# ✅ Good - Only subscribes to needed context\ndef subscribed_contexts, do: [:theme]\n\n# ❌ Avoid - Subscribes to unused contexts\ndef subscribed_contexts, do: [:theme, :user_role, :notifications]\n```\n\n### 3. Use Semantic Context Names\nChoose clear, descriptive names for your contexts:\n\n```elixir\n# ✅ Good\ncontexts: [\n  theme: \"light\",\n  user_role: \"guest\",\n  sidebar_collapsed: false\n]\n\n# ❌ Avoid\ncontexts: [\n  mode: \"light\",\n  state: \"guest\",\n  flag: false\n]\n```\n\n## Testing\n\nSharedAssigns components can be easily tested by providing context values directly as assigns:\n\n```elixir\ndefmodule MyAppWeb.HeaderComponentTest do\n  use MyAppWeb.ConnCase\n  import Phoenix.LiveViewTest\n\n  test \"renders with theme context\" do\n    {:ok, view, _html} = live_isolated(build_conn(), MyAppWeb.HeaderComponent,\n      theme: \"dark\",\n      __sa_version_theme: 1\n    )\n\n    assert has_element?(view, \".header.dark-theme\")\n  end\n\n  test \"renders with different theme\" do\n    {:ok, view, _html} = live_isolated(build_conn(), MyAppWeb.HeaderComponent,\n      theme: \"light\",\n      __sa_version_theme: 1\n    )\n\n    refute has_element?(view, \".header.dark-theme\")\n  end\nend\n```\n\n## Performance\n\nSharedAssigns is designed for optimal performance:\n\n- **Minimal overhead**: Version checking is O(1) per context\n- **Selective updates**: Only components subscribed to changed contexts receive `send_update/3`\n- **Explicit assigns**: Context values stored as regular socket assigns for fast access\n- **Efficient targeting**: Provider knows exactly which components to update\n- **Zero JavaScript**: Pure Elixir implementation\n\n## Demo Application\n\nThe repository includes a complete demo application showcasing all features:\n\n```bash\ncd demo\nmix deps.get\nmix phx.server\n# Visit http://localhost:4000\n```\n\nFeatures demonstrated:\n- Theme switching with instant UI updates\n- User role management with conditional content\n- Counter state with reactive updates\n- Beautiful Tailwind styling\n- Nested LiveView synchronization via PubSub\n\n## Contributing\n\n1. Fork the repository\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Add tests for your changes\n4. Ensure all tests pass (`mix test`)\n5. Commit your changes (`git commit -am 'Add some feature'`)\n6. Push to the branch (`git push origin my-new-feature`)\n7. Create a Pull Request\n\n## License\n\nThis project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.\n\n## Changelog\n\nSee [CHANGELOG.md](CHANGELOG.md) for a detailed history of changes.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpringels%2Fsharedassigns","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpringels%2Fsharedassigns","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpringels%2Fsharedassigns/lists"}