From 6fd0b58fe842605aaa27f43d18f3b3e58114624d Mon Sep 17 00:00:00 2001 From: Valian Date: Wed, 4 Dec 2024 23:22:09 +0100 Subject: [PATCH] WIP guides --- guides/advanced_features.md | 166 +++++++++++++++++++++++++++++++ guides/basic_usage.md | 193 ++++++++++++++++++++++++++++++++++++ guides/deployment.md | 164 ++++++++++++++++++++++++++++++ guides/faq.md | 158 +++++++++++++++++++++++++++++ guides/getting_started.md | 115 +++++++++++++++++++++ guides/installation.md | 147 +++++++++++++++++++++++++++ guides/testing.md | 189 +++++++++++++++++++++++++++++++++++ lib/live_vue.ex | 90 +++++++++-------- mix.exs | 13 ++- 9 files changed, 1192 insertions(+), 43 deletions(-) create mode 100644 guides/advanced_features.md create mode 100644 guides/basic_usage.md create mode 100644 guides/deployment.md create mode 100644 guides/faq.md create mode 100644 guides/getting_started.md create mode 100644 guides/installation.md create mode 100644 guides/testing.md diff --git a/guides/advanced_features.md b/guides/advanced_features.md new file mode 100644 index 0000000..180e15c --- /dev/null +++ b/guides/advanced_features.md @@ -0,0 +1,166 @@ +# Advanced Features + +This guide covers advanced LiveVue features and customization options. + +## Using ~V Sigil + +The `~V` sigil provides an alternative to the standard LiveView DSL, allowing you to write Vue components directly in your LiveView: + +```elixir +defmodule MyAppWeb.CounterLive do + use MyAppWeb, :live_view + + def render(assigns) do + ~V""" + + + + """ + end + + def mount(_params, _session, socket) do + {:ok, assign(socket, count: 0)} + end + + def handle_event("inc", %{"diff" => diff}, socket) do + {:noreply, update(socket, :count, &(&1 + String.to_integer(diff)))} + end +end +``` + +## Lazy Loading Components + +Enable lazy loading by returning a function that returns a promise in your components configuration: + +```javascript +// assets/vue/index.js +const components = { + Counter: () => import("./Counter.vue"), + Modal: () => import("./Modal.vue") +} + +// Using Vite's glob import +const components = import.meta.glob( + './components/*.vue', + { eager: false, import: 'default' } +) +``` + +When SSR is enabled, related JS and CSS files will be automatically preloaded in HTML. + +## Customizing Vue App Instance + +You can customize the Vue app instance in `assets/vue/index.js`: + +```javascript +import { createPinia } from "pinia" +const pinia = createPinia() + +export default createLiveVue({ + setup: ({ createApp, component, props, slots, plugin, el, ssr }) => { + const app = createApp({ render: () => h(component, props, slots) }) + app.use(plugin) + app.use(pinia) // Add your plugins + + if (ssr) { + // SSR-specific initialization + } + + app.mount(el) + return app + } +}) +``` + +Available setup options: + +| Property | Description | +|------------|------------------------------------------------| +| createApp | Vue's createApp or createSSRApp function | +| component | The Vue component to render | +| props | Props passed to the component | +| slots | Slots passed to the component | +| plugin | LiveVue plugin for useLiveVue functionality | +| el | Mount target element | +| ssr | Boolean indicating SSR context | + +## Server-Side Rendering (SSR) + +LiveVue provides two SSR strategies: + +### Development (ViteJS) +```elixir +# config/dev.exs +config :live_vue, + ssr_module: LiveVue.SSR.ViteJS +``` +Uses Vite's ssrLoadModule for efficient development compilation. + +### Production (NodeJS) +```elixir +# config/prod.exs +config :live_vue, + ssr_module: LiveVue.SSR.NodeJS +``` +Uses elixir-nodejs for optimized production SSR with an in-memory server bundle. + +### SSR Performance + +Vue SSR is compiled into string concatenation for optimal performance. The SSR step: +- Only runs during "dead" renders +- Skips during live navigation +- Can be disabled per-component with `v-ssr={false}` + +## Client-Side Hooks + +Access Phoenix hooks from Vue components using `useLiveVue`: + +```vue + +``` + +## TypeScript Support + +LiveVue provides full TypeScript support: + +1. Use the example tsconfig.json from the example project +2. Check `example_project/assets/ts_config_example` for TypeScript versions of: + - LiveVue entrypoint file + - Tailwind configuration + - Vite configuration + +For app.js TypeScript support: +```javascript +// app.js +import {initApp} from './app.ts' +initApp() +``` + +## Next Steps + +- Check out the [FAQ](faq.html) for implementation details and optimization tips +- Visit the [Deployment Guide](deployment.html) for production setup +- Join our [GitHub Discussions](https://github.com/Valian/live_vue/discussions) for questions and ideas \ No newline at end of file diff --git a/guides/basic_usage.md b/guides/basic_usage.md new file mode 100644 index 0000000..628d532 --- /dev/null +++ b/guides/basic_usage.md @@ -0,0 +1,193 @@ +# Basic Usage + +This guide covers the fundamental patterns for using Vue components within LiveView. + +## Component Organization + +By default, Vue components should be placed in either: +- `assets/vue` directory +- Colocated with your LiveView files in `lib/my_app_web` + +You can configure these paths by: +1. Modifying `assets/vue/index.js` +2. Adjusting the LiveVue.Components configuration: +```elixir +use LiveVue.Components, vue_root: ["your/vue/dir"] +``` + +## Rendering Components + +### Basic Syntax + +To render a Vue component from HEEX, use the `<.vue>` function: + +```elixir +<.vue + v-component="Counter" + v-socket={@socket} + count={@count} +/> +``` + +### Required Attributes + +| Attribute | Example | Required | Description | +|--------------|------------------------|-----------------|------------------------------------------------| +| v-component | `v-component="Counter"`| Yes | Component name or path relative to vue_root | +| v-socket | `v-socket={@socket}` | Yes in LiveView| Required for SSR and reactivity | + +### Optional Attributes + +| Attribute | Example | Description | +|--------------|----------------------|------------------------------------------------| +| v-ssr | `v-ssr={true}` | Override default SSR setting | +| v-on:event | `v-on:inc={JS.push("inc")}` | Handle Vue component events | +| prop={@value}| `count={@count}` | Pass props to the component | + +### Component Shortcut + +Instead of writing `<.vue v-component="Counter">`, you can use the shortcut syntax: + +```elixir +<.Counter + count={@count} + v-socket={@socket} +/> +``` + +Function names are generated based on `.vue` file names. For files with identical names, use the full path: +```elixir +<.vue v-component="helpers/nested/Modal" /> +``` + +## Passing Props + +Props can be passed in three equivalent ways: + +```elixir +# Individual props +<.vue + count={@count} + name={@name} + v-component="Counter" + v-socket={@socket} +/> + +# Map spread +<.vue + v-component="Counter" + v-socket={@socket} + {%{count: @count, name: @name}} +/> + +# Using shortcut +<.Counter + count={@count} + name={@name} + v-socket={@socket} +/> +``` + +## Handling Events + +### Phoenix Events + +All standard Phoenix event handlers work inside Vue components: +- `phx-click` +- `phx-change` +- `phx-submit` +- etc. + +### Vue Events + +For Vue-specific events, use the `v-on:` syntax: + +```elixir +<.vue + v-on:submit={JS.push("submit")} + v-on:close={JS.hide()} + v-component="Form" + v-socket={@socket} +/> +``` + +Special case: When using `JS.push()` without a value, it automatically uses the emit payload: +```elixir +# In Vue +emit('inc', {value: 5}) + +# In LiveView +<.vue v-on:inc={JS.push("inc")} /> +# Equivalent to: JS.push("inc", value: 5) +``` + +## Slots Support + +Vue components can receive slots from LiveView templates: + +```elixir +<.Card title="Example Card" v-socket={@socket}> +

This is the default slot content!

+

Phoenix components work too: <.icon name="hero-info" />

+ + <:footer> + This is a named slot + + +``` + +```vue + +``` + +Important notes about slots: +- Each slot is wrapped in a div (technical limitation) +- Slots are passed as raw HTML +- Phoenix hooks in slots won't work +- Slots stay reactive and update when their content changes + +## Dead Views vs Live Views + +Components can be used in both contexts: +- Live Views: Full reactivity with WebSocket updates +- Dead Views: Static rendering, no reactivity + - `v-socket={@socket}` not required + - SSR still works for initial render + +## Client-Side Hooks + +Access Phoenix hooks from Vue components using `useLiveVue`: + +```vue + +``` + +The hook provides all methods from [Phoenix.LiveView JS Interop](https://hexdocs.pm/phoenix_live_view/js-interop.html#client-hooks-via-phx-hook). + +## Next Steps + +Now that you understand the basics, you might want to explore: + +- [Advanced Features](advanced_features.html) to learn about: + - Using the `~V` sigil for inline Vue components + - Lazy loading components + - Customizing the Vue app instance + - SSR configuration and optimization +- [FAQ](faq.html) for: + - Understanding how LiveVue works under the hood + - Performance optimizations + - TypeScript setup + - Comparison with LiveSvelte \ No newline at end of file diff --git a/guides/deployment.md b/guides/deployment.md new file mode 100644 index 0000000..baf0766 --- /dev/null +++ b/guides/deployment.md @@ -0,0 +1,164 @@ +# Deployment + +Deploying a LiveVue app is similar to deploying a regular Phoenix app, with one key requirement: **Node.js version 19 or later must be installed** in your production environment. + +## General Requirements + +1. Node.js 19+ installed in production +2. Standard Phoenix deployment requirements +3. Build assets before deployment + +## Fly.io Deployment Guide + +Here's a detailed guide for deploying to [Fly.io](https://fly.io/). Similar principles apply to other hosting providers. + +### 1. Generate Dockerfile + +First, generate a Phoenix release Dockerfile: + +```bash +mix phx.gen.release --docker +``` + +### 2. Modify Dockerfile + +Update the generated Dockerfile to include Node.js: + +```dockerfile +# Build Stage +FROM hexpm/elixir:1.14.4-erlang-25.3.2-debian-bullseye-20230227-slim AS builder + +# Set environment variables +...(about 15 lines omitted)... + +# Install build dependencies +RUN apt-get update -y && apt-get install -y build-essential git curl \ + && apt-get clean && rm -f /var/lib/apt/lists/*_* + +# Install Node.js for build stage +RUN curl -fsSL https://deb.nodesource.com/setup_19.x | bash - && apt-get install -y nodejs + +# Copy application code +COPY . . + +# Install npm dependencies +RUN cd /app/assets && npm install + +...(about 20 lines omitted)... + +# Production Stage +FROM ${RUNNER_IMAGE} + +RUN apt-get update -y && \ + apt-get install -y libstdc++6 openssl libncurses5 locales ca-certificates curl \ + && apt-get clean && rm -f /var/lib/apt/lists/*_* + +# Install Node.js for production +RUN curl -fsSL https://deb.nodesource.com/setup_19.x | bash - && apt-get install -y nodejs + +...(remaining dockerfile content)... +``` + +Key changes: +- Add `curl` to build dependencies +- Install Node.js in both build and production stages +- Add npm install step for assets + +### 3. Launch on Fly.io + +1. Initialize your app: +```bash +fly launch +``` + +2. Configure database when prompted: +```bash +? Do you want to tweak these settings before proceeding? (y/N) y +``` + +3. In the configuration window: + - Choose "Fly Postgres" for database + - Name your database + - Consider development configuration for cost savings + - Review other settings as needed + +4. After deployment completes, open your app: +```bash +fly apps open +``` + +## Other Deployment Options + +### Heroku + +For Heroku deployment: +1. Use the [Phoenix buildpack](https://hexdocs.pm/phoenix/heroku.html) +2. Add Node.js buildpack: +```bash +heroku buildpacks:add --index 1 heroku/nodejs +``` + +### Docker + +If using your own Docker setup: +1. Ensure Node.js 19+ is installed +2. Follow standard Phoenix deployment practices +3. Configure SSR in production: +```elixir +# config/prod.exs +config :live_vue, + ssr_module: LiveVue.SSR.NodeJS, + ssr: true +``` + +### Custom Server + +For bare metal or VM deployments: +1. Install Node.js 19+: +```bash +curl -fsSL https://deb.nodesource.com/setup_19.x | bash - +apt-get install -y nodejs +``` + +2. Follow standard [Phoenix deployment guide](https://hexdocs.pm/phoenix/deployment.html) + +## Production Checklist + +- [ ] Node.js 19+ installed +- [ ] Assets built (`mix assets.build`) +- [ ] SSR configured properly +- [ ] Database configured +- [ ] Environment variables set +- [ ] SSL certificates configured (if needed) +- [ ] Production secrets generated +- [ ] Release configuration tested + +## Troubleshooting + +### Common Issues + +1. **SSR Not Working** + - Verify Node.js installation + - Check SSR configuration + - Ensure server bundle exists in `priv/vue/server.js` + +2. **Asset Loading Issues** + - Verify assets were built + - Check digest configuration + - Inspect network requests + +3. **Performance Issues** + - Consider adjusting NodeJS pool size: +```elixir +# in your application.ex +children = [ + {NodeJS.Supervisor, [path: LiveVue.SSR.NodeJS.server_path(), pool_size: 4]}, + # other children... +] +``` + +## Next Steps + +- Review [FAQ](faq.html) for common questions +- Join our [GitHub Discussions](https://github.com/Valian/live_vue/discussions) for help +- Consider contributing to [LiveVue](https://github.com/Valian/live_vue) \ No newline at end of file diff --git a/guides/faq.md b/guides/faq.md new file mode 100644 index 0000000..df150b6 --- /dev/null +++ b/guides/faq.md @@ -0,0 +1,158 @@ +# Frequently Asked Questions + +## General Questions + +### Why LiveVue? + +Phoenix LiveView makes it possible to create rich, interactive web apps without writing JS. However, when you need complex client-side functionality, you might end up writing lots of imperative, hard-to-maintain hooks. + +LiveVue allows you to create hybrid apps where: +- Server maintains the session state +- Vue handles complex client-side interactions +- Both sides communicate seamlessly + +Common use cases: +- Your hooks are starting to look like jQuery +- You have complex local state to manage +- You want to use the Vue ecosystem (transitions, graphs, etc.) +- You need advanced client-side features +- You simply like Vue 😉 + +### What's with the Name? + +Yes, "LiveVue" sounds exactly like "LiveView" - we noticed slightly too late to change! Some helpful Reddit users pointed it out 😉 + +We suggest referring to it as "LiveVuejs" in speech to avoid confusion. + +## Technical Details + +### How Does LiveVue Work? + +The implementation is straightforward: + +1. **Rendering**: Phoenix [renders](https://github.com/Valian/live_vue/blob/main/lib/live_vue.ex) a `div` with: + - Props as data attributes + - Slots as child elements + - Event handlers configured + - SSR content (when enabled) + +2. **Initialization**: The [LiveVue hook](https://github.com/Valian/live_vue/blob/main/assets/js/live_vue/hooks.js): + - Mounts on element creation + - Sets up event handlers + - Injects the hook for `useLiveVue` + - Mounts the Vue component + +3. **Updates**: + - Phoenix updates only changed data attributes + - Hook updates component props accordingly + +4. **Cleanup**: + - Vue component unmounts on destroy + - Garbage collection handles cleanup + +Note: Hooks fire only after `app.js` loads, which may cause slight delays in initial render. + +### What Optimizations Does LiveVue Use? + +LiveVue implements several performance optimizations: + +1. **Selective Updates**: + - Only changed props/handlers/slots are sent to client + - Achieved through careful `__changed__` assign modifications + +2. **Efficient Props Handling**: + ```elixir + data-props={"#{@props |> Jason.encode()}"} + ``` + String interpolation prevents sending `data-props=` on each update + +3. **Coming Soon**: + - Sending only updated props + - Deep-diff of props (similar to LiveJson) + +### Why is SSR Useful? + +SSR (Server-Side Rendering) provides several benefits: + +1. **Initial Render**: Components appear immediately, before JS loads +2. **SEO**: Search engines see complete content +3. **Performance**: Reduces client-side computation + +Important notes: +- SSR runs only during "dead" renders (no socket) +- Not needed during live navigation +- Can be disabled per-component with `v-ssr={false}` + +## Development + +### How Do I Use TypeScript? + +LiveVue provides full TypeScript support: + +1. Use the example `tsconfig.json` +2. Check `example_project/assets/ts_config_example` for: + - LiveVue entrypoint + - Tailwind setup + - Vite config + +For `app.js`, since it's harder to convert directly: +```javascript +// Write your code in TypeScript +// app.ts +export const initApp = () => { /* ... */ } + +// Import in app.js +import {initApp} from './app.ts' +initApp() +``` + +### Where Should I Put Vue Files? + +Vue files in LiveVue are similar to HEEX templates. You have two main options: + +1. **Default Location**: `assets/vue` directory +2. **Colocated**: Next to your LiveViews in `lib/my_app_web` + +Colocating provides better DX by: +- Keeping related code together +- Making relationships clearer +- Simplifying maintenance + +No configuration needed - just place `.vue` files in `lib/my_app_web` and reference them by name or path. + +## Comparison with Other Solutions + +### How Does LiveVue Compare to LiveSvelte? + +Both serve similar purposes with similar implementations, but have key differences: + +**Technical Differences**: +- Vue uses virtual DOM, Svelte doesn't +- Vue bundle is slightly larger due to runtime +- Performance is comparable + +**Reactivity Approach**: +- Svelte: Compilation-based, concise but with [some limitations](https://thoughtspile.github.io/2023/04/22/svelte-state/) +- Vue: Proxy-based, more verbose but more predictable + +**Future Developments**: +- Vue is working on [Vapor mode](https://github.com/vuejs/core-vapor) (no virtual DOM) +- Svelte 5 Runes will be similar to Vue `ref` + +**Ecosystem**: +- Vue has a larger ecosystem +- More third-party components available +- Larger community + +Choose based on: +- Your team's familiarity +- Ecosystem requirements +- Syntax preferences +- Bundle size concerns + +## Additional Resources + +- [GitHub Discussions](https://github.com/Valian/live_vue/discussions) +- [Example Project](https://github.com/Valian/live_vue/tree/main/example_project) +- [Vue Documentation](https://vuejs.org/) +- [Phoenix LiveView Documentation](https://hexdocs.pm/phoenix_live_view) \ No newline at end of file diff --git a/guides/getting_started.md b/guides/getting_started.md new file mode 100644 index 0000000..ebfaa0c --- /dev/null +++ b/guides/getting_started.md @@ -0,0 +1,115 @@ +# Getting Started + +Now that you have LiveVue installed, let's create your first Vue component and integrate it with LiveView. + +## Creating Your First Component + +Let's create a simple counter component that demonstrates the reactivity between Vue and LiveView. + +1. Create `assets/vue/Counter.vue`: + +```vue + + + +``` + +2. Create a LiveView to handle the counter state (`lib/my_app_web/live/counter_live.ex`): + +```elixir +defmodule MyAppWeb.CounterLive do + use MyAppWeb, :live_view + + def mount(_params, _session, socket) do + {:ok, assign(socket, count: 0)} + end + + def render(assigns) do + ~H""" + <.vue + count={@count} + v-component="Counter" + v-socket={@socket} + v-on:inc={JS.push("inc")} + /> + """ + end + + def handle_event("inc", %{"value" => value}, socket) do + {:noreply, update(socket, :count, &(&1 + value))} + end +end +``` + +3. Add the route in your `router.ex`: + +```elixir +live "/counter", CounterLive +``` + +This example demonstrates several key LiveVue features: +- Props passing (`count={@count}`) +- Event handling (`v-on:inc={JS.push("inc")}`) +- Two-way reactivity between Vue and LiveView +- TypeScript support +- Automatic Tailwind integration + +## Understanding the Integration + +Let's break down how LiveVue connects Vue and LiveView: + +1. **Props Flow**: LiveView assigns are passed as props to Vue components + +```elixir +count={@count} # LiveView assign becomes Vue prop +``` + +2. **Events Flow**: Vue emits are handled by LiveView + +```elixir +# In Vue +emit('inc', {value: parseInt(diff)}) + +# In LiveView +def handle_event("inc", %{"value" => value}, socket) do +``` + +3. **State Management**: LiveView manages the source of truth, Vue handles local UI state + +## Next Steps + +Now that you have your first component working, explore: +- [Basic Usage Guide](basic_usage.html) for more component patterns +- [Advanced Features](advanced_features.html) for SSR, slots, and Vue customization +- [FAQ](faq.html) for common questions and troubleshooting + +## Tips + +- Use the Vue DevTools browser extension for debugging +- Enable [Hot Module Replacement](https://vitejs.dev/guide/features.html#hot-module-replacement) in Vite for better development experience +- Consider colocating Vue components with your LiveViews in `lib/my_app_web/` \ No newline at end of file diff --git a/guides/installation.md b/guides/installation.md new file mode 100644 index 0000000..555a3e0 --- /dev/null +++ b/guides/installation.md @@ -0,0 +1,147 @@ +# Installation + +LiveVue replaces `esbuild` with [Vite](https://vitejs.dev/) for both client side code and SSR to achieve an amazing development experience. + +## Why Vite? + +- Vite provides a best-in-class Hot-Reload functionality and offers [many benefits](https://vitejs.dev/guide/why#why-vite) +- `esbuild` package doesn't support plugins, so we would need to setup it anyway +- In production, we'll use [elixir-nodejs](https://github.com/revelrylabs/elixir-nodejs) for SSR + +## Prerequisites + +- Node.js installed (version 19 or later recommended) +- Phoenix 1.7+ project +- Elixir 1.13+ + +## Setup Steps + +1. Add LiveVue to your dependencies: + +```elixir +def deps do + [ + {:live_vue, "~> 0.5"} + ] +end +``` + +2. Configure environments: + +```elixir +# in config/dev.exs +config :live_vue, + vite_host: "http://localhost:5173", + ssr_module: LiveVue.SSR.ViteJS + +# in config/prod.exs +config :live_vue, + ssr_module: LiveVue.SSR.NodeJS, + ssr: true +``` + +3. Add LiveVue to your `html_helpers` in `lib/my_app_web.ex`: + +```elixir +defp html_helpers do + quote do + # ... existing code ... + use LiveVue + use LiveVue.Components, vue_root: ["./assets/vue", "./lib/my_app_web"] + end +end +``` + +4. Run the setup command to generate required files: + +```bash +mix deps.get +mix live_vue.setup +cd assets && npm install +``` + +5. Update your JavaScript configuration: + +```javascript +// app.js +import {getHooks} from "live_vue" +import liveVueApp from "../vue" + +let liveSocket = new LiveSocket("/live", Socket, { + hooks: getHooks(liveVueApp), +}) +``` + +6. Configure Tailwind to include Vue files: + +```javascript +// tailwind.config.js +module.exports = { + content: [ + // ... existing patterns + "./vue/**/*.vue", + "../lib/**/*.vue", + ], +} +``` + +7. Update root.html.heex for Vite: + +```heex + + + + +``` + +8. Configure watchers in `config/dev.exs`: + +```elixir +config :my_app, MyAppWeb.Endpoint, + watchers: [ + npm: ["--silent", "run", "dev", cd: Path.expand("../assets", __DIR__)] + ] +``` + +9. Setup SSR for production in `application.ex`: + +```elixir +children = [ + {NodeJS.Supervisor, [path: LiveVue.SSR.NodeJS.server_path(), pool_size: 4]}, + # ... other children +] +``` + +10. (Optional) Enable [stateful hot reload](https://twitter.com/jskalc/status/1788308446007132509): + +```elixir +# config/dev.exs +config :my_app, MyAppWeb.Endpoint, + live_reload: [ + notify: [ + live_view: [ + ~r"lib/my_app_web/core_components.ex$", + ~r"lib/my_app_web/(live|components)/.*(ex|heex)$" + ] + ], + patterns: [ + ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$", + ~r"lib/my_app_web/controllers/.*(ex|heex)$" + ] + ] +``` + +## Troubleshooting + +### TypeScript Compatibility + +If you encounter TypeScript errors, you may need to downgrade typescript and vue-tsc: + +```bash +npm install typescript@5.5.4 vue-tsc@2.10.0 +``` + +## Next Steps + +See our [Getting Started Guide](getting_started.html) for creating your first Vue component! \ No newline at end of file diff --git a/guides/testing.md b/guides/testing.md new file mode 100644 index 0000000..86e4de4 --- /dev/null +++ b/guides/testing.md @@ -0,0 +1,189 @@ +# Testing Guide + +LiveVue provides a robust testing module `LiveVue.Test` that makes it easy to test Vue components within your Phoenix LiveView tests. + +## Overview + +Testing LiveVue components differs from traditional Phoenix LiveView testing in a key way: +- Traditional LiveView testing uses `render_component/2` to get final HTML +- LiveVue testing provides helpers to inspect the Vue component configuration before client-side rendering + +## Basic Component Testing + +Let's start with a simple component test: + +```elixir +defmodule MyAppWeb.CounterTest do + use ExUnit.Case + import Phoenix.LiveViewTest + alias LiveVue.Test + + test "renders counter component with initial props" do + {:ok, view, _html} = live(conn, "/counter") + vue = Test.get_vue(view) + + assert vue.component == "Counter" + assert vue.props == %{"count" => 0} + end +end +``` + +The `get_vue/2` function returns a map containing: +- `:component` - Vue component name +- `:id` - Unique component identifier +- `:props` - Decoded props +- `:handlers` - Event handlers and operations +- `:slots` - Slot content +- `:ssr` - SSR status +- `:class` - CSS classes + +## Testing Multiple Components + +When your view contains multiple Vue components, you can specify which one to test: + +```elixir +# Find by component name +vue = Test.get_vue(view, name: "UserProfile") + +# Find by ID +vue = Test.get_vue(view, id: "profile-1") +``` + +Example with multiple components: + +```elixir +def render(assigns) do + ~H""" +
+ <.vue id="profile-1" name="John" v-component="UserProfile" /> + <.vue id="card-1" name="Jane" v-component="UserCard" /> +
+ """ +end + +test "finds specific component" do + html = render_component(&my_component/1) + + # Get UserCard component + vue = Test.get_vue(html, name: "UserCard") + assert vue.props == %{"name" => "Jane"} + + # Get by ID + vue = Test.get_vue(html, id: "profile-1") + assert vue.component == "UserProfile" +end +``` + +## Testing Event Handlers + +You can verify event handlers are properly configured: + +```elixir +test "component has correct event handlers" do + vue = Test.get_vue(render_component(&my_component/1)) + + assert vue.handlers == %{ + "click" => JS.push("click", value: %{"abc" => "def"}), + "submit" => JS.push("submit") + } +end +``` + +## Testing Slots + +LiveVue provides tools to test both default and named slots: + +```elixir +def component_with_slots(assigns) do + ~H""" + <.vue v-component="WithSlots"> + Default content + <:header>Header content + <:footer>Footer content + + """ +end + +test "renders slots correctly" do + vue = Test.get_vue(render_component(&component_with_slots/1)) + + assert vue.slots == %{ + "default" => "Default content", + "header" => "Header content", + "footer" => "Footer content" + } +end +``` + +Important notes about slots: +- Use `<:inner_block>` instead of `<:default>` for default content +- Slots are automatically Base64 encoded in the HTML +- The test helper decodes them for easier assertions + +## Testing SSR Configuration + +Verify Server-Side Rendering settings: + +```elixir +test "respects SSR configuration" do + vue = Test.get_vue(render_component(&my_component/1)) + assert vue.ssr == true + + # Or with SSR disabled + vue = Test.get_vue(render_component(&ssr_disabled_component/1)) + assert vue.ssr == false +end +``` + +## Testing CSS Classes + +Check applied styling: + +```elixir +test "applies correct CSS classes" do + vue = Test.get_vue(render_component(&my_component/1)) + assert vue.class == "bg-blue-500 rounded" +end +``` + +## Integration Testing + +For full integration tests, combine LiveVue testing with LiveView test helpers: + +```elixir +test "counter increments correctly", %{conn: conn} do + {:ok, view, _html} = live(conn, "/counter") + + # Verify initial state + vue = Test.get_vue(view) + assert vue.props == %{"count" => 0} + + # Simulate increment event + view |> element("button") |> render_click() + + # Verify updated state + vue = Test.get_vue(view) + assert vue.props == %{"count" => 1} +end +``` + +## Best Practices + +1. **Component Isolation** + - Test Vue components in isolation when possible + - Use `render_component/1` for focused tests + +2. **Clear Assertions** + - Test one aspect per test + - Use descriptive test names + - Assert specific properties rather than entire component structure + +3. **Integration Testing** + - Test full component interaction in LiveView context + - Verify both server and client-side behavior + - Test error cases and edge conditions + +4. **Maintainable Tests** + - Use helper functions for common assertions + - Keep test setup minimal and clear + - Document complex test scenarios \ No newline at end of file diff --git a/lib/live_vue.ex b/lib/live_vue.ex index d0de4d0..62e088a 100644 --- a/lib/live_vue.ex +++ b/lib/live_vue.ex @@ -1,6 +1,34 @@ defmodule LiveVue do @moduledoc """ + LiveVue provides seamless integration between Phoenix LiveView and Vue.js components. + + ## Installation and Configuration + See README.md for installation instructions and usage. + + ## Component Options + + When using the `vue/1` component or `~V` sigil, the following options are supported: + + ### Required Attributes + * `v-component` (string) - Name of the Vue component (e.g., "YourComponent", "directory/Example") + + ### Optional Attributes + * `id` (string) - Explicit ID of the wrapper component. If not provided, a random one will be + generated. Useful to keep ID consistent in development (e.g., "vue-1") + * `class` (string) - CSS class(es) to apply to the Vue component wrapper + (e.g., "my-class" or "my-class another-class") + * `v-ssr` (boolean) - Whether to render the component on the server. Defaults to the value set + in config (default: true) + * `v-socket` (LiveView.Socket) - LiveView socket, should be provided when rendering inside LiveView + + ### Event Handlers + * `v-on:*` - Vue event handlers can be attached using the `v-on:` prefix + (e.g., `v-on:click`, `v-on:input`) + + ### Props and Slots + * All other attributes are passed as props to the Vue component + * Slots can be passed as regular Phoenix slots """ use Phoenix.Component @@ -20,49 +48,27 @@ defmodule LiveVue do end end - # TODO - commented out because it's impossible to make :rest accept all attrs without a warning - # attr( - # :"v-component", - # :string, - # required: true, - # doc: "Name of the Vue component", - # examples: ["YourComponent", "directory/Example"] - # ) - - # attr( - # :id, - # :string, - # default: nil, - # doc: - # "Explicit id of a wrapper component. If not provided, a random one will be generated. Useful to keep ID consistent in development.", - # examples: ["vue-1"] - # ) - - # attr( - # :class, - # :string, - # default: nil, - # doc: "Class to apply to the Vue component", - # examples: ["my-class", "my-class another-class"] - # ) - - # attr( - # :"v-ssr", - # :boolean, - # default: Application.compile_env(:live_vue, :ssr, true), - # doc: "Whether to render the component on the server", - # examples: [true, false] - # ) - - # attr( - # :"v-socket", - # :map, - # default: nil, - # doc: "LiveView socket, should be provided when rendering inside LiveView" - # ) - - # attr :rest, :global + @doc """ + Renders a Vue component within Phoenix LiveView. + + ## Examples + <.vue + v-component="MyComponent" + message="Hello" + v-on:click="handleClick" + class="my-component" + /> + + <.vue + v-component="nested/Component" + v-ssr={false} + items={@items} + > + <:default>Default slot content + <:named>Named slot content + + """ def vue(assigns) do init = assigns.__changed__ == nil dead = assigns[:"v-socket"] == nil or not LiveView.connected?(assigns[:"v-socket"]) diff --git a/mix.exs b/mix.exs index b1ace4d..3ef1dc1 100644 --- a/mix.exs +++ b/mix.exs @@ -34,9 +34,20 @@ defmodule LiveVue.MixProject do main: "readme", extras: [ "README.md": [title: "LiveVue"], - "INSTALLATION.md": [title: "Installation"], + "guides/installation.md": [title: "Installation"], + "guides/getting_started.md": [title: "Getting Started"], + "guides/basic_usage.md": [title: "Basic Usage"], + "guides/advanced_features.md": [title: "Advanced Features"], + "guides/deployment.md": [title: "Deployment"], + "guides/testing.md": [title: "Testing Guide"], + "guides/faq.md": [title: "FAQ"], "CHANGELOG.md": [title: "Changelog"] ], + extra_section: "GUIDES", + groups_for_extras: [ + Introduction: ["README.md", "guides/installation.md"], + Guides: Path.wildcard("guides/*.md") |> Enum.reject(&(&1 == "guides/faq.md")) + ], links: %{ "GitHub" => @repo_url }