751 lines
32 KiB
Markdown
751 lines
32 KiB
Markdown
# AGENTS.md
|
||
|
||
## Project Overview
|
||
|
||
Music Library is an Elixir/Phoenix application for managing a personal music collection. It allows users to:
|
||
|
||
- Add records from MusicBrainz, with optional override of specific pieces of data
|
||
- Manage a collection and a wishlist of records, with ways to quickly search and filter based on records' metadata
|
||
- Integration with Last.fm:
|
||
- display latest scrobbles, and where possible connect them with records in the collection or wishlist
|
||
- scrobble a record
|
||
- store a local copy of the complete scrobble history, and setup rules to fix its data as needed
|
||
- Barcode scanning functionality for adding records
|
||
- Artist information management with automatic fetching and caching
|
||
- Online store templates for quick external lookups
|
||
- Notes system for tracking custom information about records
|
||
- Assets management with image transformation and caching
|
||
- Color extraction from album artwork
|
||
- Some basic stats (top albums, top artists, collection insights)
|
||
- Universal search across collection, wishlist, and artists
|
||
- Similar records discovery using AI-powered embeddings (using SQLite vector extension)
|
||
- All data stored in a single SQLite database for portability and ease of backup/restore
|
||
|
||
## Development Setup
|
||
|
||
### Prerequisites
|
||
|
||
- [mise-en-place](https://mise.jdx.dev) is used for environment management
|
||
- Requires Erlang, Elixir, and Node.js (managed by mise)
|
||
- Uses [Fluxon UI](https://fluxonui.com/) - requires valid credentials
|
||
|
||
### Environment Configuration
|
||
|
||
Required environment variables:
|
||
|
||
- `LAST_FM_USER`: Last.fm username used to populate the Scrobble Activity
|
||
- `LAST_FM_API_KEY`: Last.fm API key used to fetch the Scrobble Activity (secret)
|
||
- `OPENAI_KEY`: OpenAI API key used to populate genres (secret)
|
||
- `FLUXON_KEY_FINGERPRINT`: Fluxon license fingerprint
|
||
- `FLUXON_LICENSE_KEY`: Fluxon license key (secret)
|
||
- `LOGIN_PASSWORD`: Password for accessing the application (in production, secret)
|
||
|
||
In development, the default password is `change me`.
|
||
|
||
Create a `mise.local.toml` with required variables (samples in `mise.toml`).
|
||
|
||
### Initial Setup
|
||
|
||
```sh
|
||
# Install required tools (Erlang, Elixir, Node.js)
|
||
mise install
|
||
|
||
# Setup dependencies and database
|
||
mise run dev:setup
|
||
```
|
||
|
||
## Common Commands
|
||
|
||
### Development
|
||
|
||
IF YOU CAN CONNECT TO THE MCP SERVER, THEN THE APPLICATION IS ALREADY RUNNING.
|
||
|
||
Otherwise you can run:
|
||
|
||
```sh
|
||
# Run the Phoenix server with IEx console
|
||
mise run dev:console
|
||
|
||
# Run static checks (format, credo, gettext)
|
||
mise run dev:lint
|
||
|
||
# Show outdated dependencies (errors at first outdated)
|
||
mise run dev:outdated
|
||
|
||
# Update dependencies (errors at first outdated)
|
||
mise run dev:update
|
||
|
||
# Fix translation file conflicts
|
||
mise run dev:fix-translations
|
||
|
||
# Render README.md with GitHub Flavored Markdown preview
|
||
mise run dev:readme
|
||
```
|
||
|
||
### Testing
|
||
|
||
```sh
|
||
# Run all tests
|
||
mix test
|
||
|
||
# Run tests via mise (alternative)
|
||
mise run dev:test
|
||
|
||
# Run a specific test file
|
||
mix test test/path/to/test_file.exs
|
||
|
||
# Run a specific test (line number)
|
||
mix test test/path/to/test_file.exs:42
|
||
|
||
# Run previously failed tests
|
||
mix test --failed
|
||
```
|
||
|
||
### Database
|
||
|
||
```sh
|
||
# Setup database (create and migrate)
|
||
mix ecto.setup
|
||
|
||
# Reset database (drop, create, and migrate)
|
||
mix ecto.reset
|
||
|
||
# Run migrations
|
||
mix ecto.migrate
|
||
|
||
# Create a new migration
|
||
mix ecto.gen.migration migration_name
|
||
|
||
# Rollback last migration
|
||
mix ecto.rollback
|
||
```
|
||
|
||
### Production & CI
|
||
|
||
YOU'RE NOT ALLOWED TO TOUCH PRODUCTION IN ANY POSSIBLE WAY.
|
||
|
||
Available commands for monitoring (view only):
|
||
|
||
- `mise run prod:backup` - Backup the production database to local dev env
|
||
- `mise run prod:prune-backups` - Remove locally downloaded production database backups
|
||
- `mise run prod:test` - Run HTTP tests against production
|
||
- `mise run ci:watch` - Watch CI running
|
||
|
||
Deployment is handled via Coolify using a Docker Compose strategy.
|
||
|
||
## Architecture
|
||
|
||
### Database Structure
|
||
|
||
The application uses SQLite with two databases:
|
||
|
||
- Main database (`Repo`): Primary data storage
|
||
- Background database (`BackgroundRepo`): For background jobs and workers
|
||
|
||
Key tables and schemas:
|
||
|
||
- `records` table stores all record data with embedded JSON for artists
|
||
- `artist_infos` table stores additional artist metadata (bio, images, etc.)
|
||
- `artist_records` view provides normalized artist-record relationships
|
||
- `records_search_index` virtual FTS5 table for efficient full-text searching
|
||
- `record_embeddings` table stores AI-generated embeddings for similarity search (uses sqlite-vec extension)
|
||
- `scrobble_rules` table for managing Last.fm scrobble data corrections
|
||
- `online_store_templates` table for external store URL templates
|
||
- `notes` table for custom record annotations
|
||
- `assets` table for cached images with transformations
|
||
- `secrets` table for encrypted sensitive data (uses Cloak)
|
||
- Collection vs. wishlist differentiated by `purchased_at` field (NULL = wishlist)
|
||
|
||
Tables are synchronized with triggers, and multiple indices exist for performance.
|
||
|
||
### Code Organization
|
||
|
||
The application follows standard Phoenix/Elixir structure:
|
||
|
||
- `lib/music_library`: Core application logic
|
||
- `records/`: Record and collection management (includes batch operations, search parsing, embeddings, similarity)
|
||
- `artists/`: Artist data handling and artist info management
|
||
- `collection.ex`: Collection-specific functionality
|
||
- `wishlist.ex`: Wishlist-specific functionality
|
||
- `search.ex`: Universal search across collection, wishlist, and artists
|
||
- `scrobble_activity.ex`: Last.fm scrobble tracking and activity
|
||
- `scrobble_rules/`: Rules for correcting scrobble data
|
||
- `barcode_scan/`: Barcode scanning features
|
||
- `assets/`: Asset caching, image transformation
|
||
- `colors/`: Extract colors from images (album artworks)
|
||
- `notes/`: Note management for records
|
||
- `online_store_templates/`: External store URL templates
|
||
- `secrets/`: Encrypted secrets management (uses Cloak/Vault)
|
||
- `worker/`: Background job workers (Oban)
|
||
- `fetch_artist_image.ex`, `fetch_artist_info.ex`, `prune_artist_info.ex`
|
||
- `record_refresh_music_brainz_data.ex`, `refresh_cover.ex`
|
||
- `extract_colors.ex`, `populate_genres.ex`
|
||
- `generate_record_embedding.ex`: Generate AI embeddings for similarity search
|
||
- `apply_scrobble_rules.ex`, `prune_asset_cache.ex`
|
||
|
||
- External API integrations:
|
||
- `lib/music_brainz`: MusicBrainz API client (primary metadata source)
|
||
- `lib/discogs`: Discogs API client
|
||
- `lib/last_fm`: Last.fm API client (scrobbles, artist info)
|
||
- `lib/open_ai`: OpenAI API client (genre population, embeddings generation)
|
||
|
||
- `lib/music_library_web`: Web interface (Phoenix)
|
||
- `live/`: LiveView implementations
|
||
- `collection_live/`, `wishlist_live/`: Collection and wishlist views
|
||
- `artist_live/`: Artist detail pages
|
||
- `scrobble_live/`, `scrobble_rules_live/`, `scrobbled_tracks_live/`: Scrobble management
|
||
- `stats_live/`: Statistics (top albums, top artists)
|
||
- `universal_search_live/`: Universal search interface
|
||
- `online_store_template_live/`: Store template management
|
||
- `components/`: UI components (Fluxon-based)
|
||
- `record_components.ex`, `artist_components.ex`, `scrobble_components.ex`
|
||
- `search_components.ex`, `stats_components.ex`, `chart_components.ex`
|
||
- `barcode_scanner.ex`, `notes.ex`, `pagination.ex`
|
||
- `record_form.ex`, `add_record.ex`, `release.ex`
|
||
- `controllers/`: Traditional Phoenix controllers
|
||
- `collection_controller.ex`: Collection export endpoints
|
||
- `asset_controller.ex`: Asset serving
|
||
- `archive_controller.ex`: Archive management
|
||
- `health_controller.ex`: Health checks
|
||
- `last_fm_controller.ex`: Last.fm OAuth callback
|
||
- `session_controller.ex`: Authentication
|
||
- `hooks/`: LiveView hooks (`get_timezone.ex`, `static_assets.ex`)
|
||
|
||
## Git Workflow
|
||
|
||
Run the following before commits to ensure code quality:
|
||
|
||
```sh
|
||
# Run static checks to ensure code quality
|
||
mise run dev:precommit
|
||
```
|
||
|
||
To set up a pre-commit hook:
|
||
|
||
```sh
|
||
mise generate git-pre-commit --write --task=dev:precommit
|
||
```
|
||
|
||
### Guidelines
|
||
|
||
You are an expert in Elixir, Phoenix, Sqlite, LiveView, and Tailwind CSS.
|
||
|
||
Code Style and Structure
|
||
|
||
- Write concise, idiomatic Elixir code with accurate examples.
|
||
- Follow Phoenix conventions and best practices.
|
||
- Use functional programming patterns and leverage immutability.
|
||
- Prefer higher-order functions and recursion over imperative loops.
|
||
- Use descriptive variable and function names (e.g., user_signed_in?, calculate_total).
|
||
- Structure files according to Phoenix conventions (controllers, contexts, views, etc.).
|
||
- Where possible use Fluxon components instead of rolling your own
|
||
- Use Oban for background job processing (workers in `lib/music_library/worker/`)
|
||
|
||
Database design
|
||
|
||
- Do not add columns for properties that are not specified in the requirements. Keep database changes to a minimum.
|
||
|
||
Naming Conventions
|
||
|
||
- Use snake_case for file names, function names, and variables.
|
||
- Use PascalCase for module names.
|
||
- Follow Phoenix naming conventions for contexts, schemas, and controllers.
|
||
|
||
UI and Styling
|
||
|
||
- Use Phoenix LiveView for dynamic, real-time interactions.
|
||
- Implement responsive design with Tailwind CSS. When screen space is limited,
|
||
prefer content-focused flexible layouts over rigid tabular structures.
|
||
Reorganize information hierarchically within each item, grouping related data
|
||
visually while maintaining scanability and preserving all functionality.
|
||
- Use Phoenix view helpers and templates to keep views DRY.
|
||
- Use minimal markup and avoid nesting DIVs unnecessarily.
|
||
- Always use `gettext/1` for user-visible text instead of hardcoded strings
|
||
- Apply translations consistently across all UI text, error messages, page titles, and button labels
|
||
- Use `:if` attributes over `<%= if %>` blocks in HEEx templates for cleaner, more readable code
|
||
- Leverage built-in Phoenix LiveView features like `phx-disable-with` for loading states instead of manual state tracking
|
||
|
||
Performance Optimization
|
||
|
||
- Use database indexing effectively.
|
||
- Implement caching strategies (ETS, Redis).
|
||
- Use Ecto's preload to avoid N+1 queries.
|
||
- Optimize database queries using preload, joins, or select.
|
||
|
||
Key Conventions
|
||
|
||
- Follow RESTful routing conventions.
|
||
- Use contexts for organizing related functionality.
|
||
- Implement GenServers for stateful processes and background jobs.
|
||
- Use Tasks for concurrent, isolated jobs.
|
||
|
||
Testing
|
||
|
||
- Write comprehensive tests using ExUnit.
|
||
- Follow TDD practices.
|
||
- Run tests with `mix test` or `mise run dev:test`
|
||
- Use `mix test --failed` to rerun failed tests
|
||
|
||
Security
|
||
|
||
- Authentication handled via session-based login (see `MusicLibraryWeb.Auth`)
|
||
- Use strong parameters in controllers (params validation).
|
||
- Protect against common web vulnerabilities (XSS, CSRF, SQL injection).
|
||
- Sensitive data encrypted using Cloak (see `MusicLibrary.Vault` and `MusicLibrary.Secrets`)
|
||
|
||
Background Jobs
|
||
|
||
- Use Oban workers for all background tasks
|
||
- Workers are in `lib/music_library/worker/`
|
||
- Common worker patterns: fetching external data, pruning caches, applying rules
|
||
|
||
Follow the official Phoenix guides for best practices in routing, controllers, contexts, views, and other Phoenix components.
|
||
|
||
<!-- usage-rules-start -->
|
||
<!-- usage-rules-header -->
|
||
|
||
# Usage Rules
|
||
|
||
**IMPORTANT**: Consult these usage rules early and often when working with the packages listed below.
|
||
Before attempting to use any of these packages or to discover if you should use them, review their
|
||
usage rules to understand the correct patterns, conventions, and best practices.
|
||
|
||
<!-- usage-rules-header-end -->
|
||
|
||
<!-- igniter-start -->
|
||
## igniter usage
|
||
_A code generation and project patching framework_
|
||
|
||
[igniter usage rules](deps/igniter/usage-rules.md)
|
||
<!-- igniter-end -->
|
||
<!-- usage_rules-start -->
|
||
## usage_rules usage
|
||
_A dev tool for Elixir projects to gather LLM usage rules from dependencies_
|
||
|
||
## Using Usage Rules
|
||
|
||
Many packages have usage rules, which you should *thoroughly* consult before taking any
|
||
action. These usage rules contain guidelines and rules *directly from the package authors*.
|
||
They are your best source of knowledge for making decisions.
|
||
|
||
## Modules & functions in the current app and dependencies
|
||
|
||
When looking for docs for modules & functions that are dependencies of the current project,
|
||
or for Elixir itself, use `mix usage_rules.docs`
|
||
|
||
```
|
||
# Search a whole module
|
||
mix usage_rules.docs Enum
|
||
|
||
# Search a specific function
|
||
mix usage_rules.docs Enum.zip
|
||
|
||
# Search a specific function & arity
|
||
mix usage_rules.docs Enum.zip/1
|
||
```
|
||
|
||
|
||
## Searching Documentation
|
||
|
||
You should also consult the documentation of any tools you are using, early and often. The best
|
||
way to accomplish this is to use the `usage_rules.search_docs` mix task. Once you have
|
||
found what you are looking for, use the links in the search results to get more detail. For example:
|
||
|
||
```
|
||
# Search docs for all packages in the current application, including Elixir
|
||
mix usage_rules.search_docs Enum.zip
|
||
|
||
# Search docs for specific packages
|
||
mix usage_rules.search_docs Req.get -p req
|
||
|
||
# Search docs for multi-word queries
|
||
mix usage_rules.search_docs "making requests" -p req
|
||
|
||
# Search only in titles (useful for finding specific functions/modules)
|
||
mix usage_rules.search_docs "Enum.zip" --query-by title
|
||
```
|
||
|
||
|
||
<!-- usage_rules-end -->
|
||
<!-- usage_rules:elixir-start -->
|
||
## usage_rules:elixir usage
|
||
# Elixir Core Usage Rules
|
||
|
||
## Pattern Matching
|
||
- Use pattern matching over conditional logic when possible
|
||
- Prefer to match on function heads instead of using `if`/`else` or `case` in function bodies
|
||
- `%{}` matches ANY map, not just empty maps. Use `map_size(map) == 0` guard to check for truly empty maps
|
||
|
||
## Error Handling
|
||
- Use `{:ok, result}` and `{:error, reason}` tuples for operations that can fail
|
||
- Avoid raising exceptions for control flow
|
||
- Use `with` for chaining operations that return `{:ok, _}` or `{:error, _}`
|
||
|
||
## Common Mistakes to Avoid
|
||
- Elixir has no `return` statement, nor early returns. The last expression in a block is always returned.
|
||
- Don't use `Enum` functions on large collections when `Stream` is more appropriate
|
||
- Avoid nested `case` statements - refactor to a single `case`, `with` or separate functions
|
||
- Don't use `String.to_atom/1` on user input (memory leak risk)
|
||
- Lists and enumerables cannot be indexed with brackets. Use pattern matching or `Enum` functions
|
||
- Prefer `Enum` functions like `Enum.reduce` over recursion
|
||
- When recursion is necessary, prefer to use pattern matching in function heads for base case detection
|
||
- Using the process dictionary is typically a sign of unidiomatic code
|
||
- Only use macros if explicitly requested
|
||
- There are many useful standard library functions, prefer to use them where possible
|
||
|
||
## Function Design
|
||
- Use guard clauses: `when is_binary(name) and byte_size(name) > 0`
|
||
- Prefer multiple function clauses over complex conditional logic
|
||
- Name functions descriptively: `calculate_total_price/2` not `calc/2`
|
||
- Predicate function names should not start with `is` and should end in a question mark.
|
||
- Names like `is_thing` should be reserved for guards
|
||
|
||
## Data Structures
|
||
- Use structs over maps when the shape is known: `defstruct [:name, :age]`
|
||
- Prefer keyword lists for options: `[timeout: 5000, retries: 3]`
|
||
- Use maps for dynamic key-value data
|
||
- Prefer to prepend to lists `[new | list]` not `list ++ [new]`
|
||
|
||
## Mix Tasks
|
||
|
||
- Use `mix help` to list available mix tasks
|
||
- Use `mix help task_name` to get docs for an individual task
|
||
- Read the docs and options fully before using tasks
|
||
|
||
## Testing
|
||
- Run tests in a specific file with `mix test test/my_test.exs` and a specific test with the line number `mix test path/to/test.exs:123`
|
||
- Limit the number of failed tests with `mix test --max-failures n`
|
||
- Use `@tag` to tag specific tests, and `mix test --only tag` to run only those tests
|
||
- Use `assert_raise` for testing expected exceptions: `assert_raise ArgumentError, fn -> invalid_function() end`
|
||
- Use `mix help test` to for full documentation on running tests
|
||
|
||
## Debugging
|
||
|
||
- Use `dbg/1` to print values while debugging. This will display the formatted value and other relevant information in the console.
|
||
|
||
<!-- usage_rules:elixir-end -->
|
||
<!-- usage_rules:otp-start -->
|
||
## usage_rules:otp usage
|
||
# OTP Usage Rules
|
||
|
||
## GenServer Best Practices
|
||
- Keep state simple and serializable
|
||
- Handle all expected messages explicitly
|
||
- Use `handle_continue/2` for post-init work
|
||
- Implement proper cleanup in `terminate/2` when necessary
|
||
|
||
## Process Communication
|
||
- Use `GenServer.call/3` for synchronous requests expecting replies
|
||
- Use `GenServer.cast/2` for fire-and-forget messages.
|
||
- When in doubt, use `call` over `cast`, to ensure back-pressure
|
||
- Set appropriate timeouts for `call/3` operations
|
||
|
||
## Fault Tolerance
|
||
- Set up processes such that they can handle crashing and being restarted by supervisors
|
||
- Use `:max_restarts` and `:max_seconds` to prevent restart loops
|
||
|
||
## Task and Async
|
||
- Use `Task.Supervisor` for better fault tolerance
|
||
- Handle task failures with `Task.yield/2` or `Task.shutdown/2`
|
||
- Set appropriate task timeouts
|
||
- Use `Task.async_stream/3` for concurrent enumeration with back-pressure
|
||
|
||
<!-- usage_rules:otp-end -->
|
||
<!-- phoenix:ecto-start -->
|
||
## phoenix:ecto usage
|
||
## Ecto Guidelines
|
||
|
||
- **Always** preload Ecto associations in queries when they'll be accessed in templates, ie a message that needs to reference the `message.user.email`
|
||
- Remember `import Ecto.Query` and other supporting modules when you write `seeds.exs`
|
||
- `Ecto.Schema` fields always use the `:string` type, even for `:text`, columns, ie: `field :name, :string`
|
||
- `Ecto.Changeset.validate_number/2` **DOES NOT SUPPORT the `:allow_nil` option**. By default, Ecto validations only run if a change for the given field exists and the change value is not nil, so such as option is never needed
|
||
- You **must** use `Ecto.Changeset.get_field(changeset, :field)` to access changeset fields
|
||
- Fields which are set programatically, such as `user_id`, must not be listed in `cast` calls or similar for security purposes. Instead they must be explicitly set when creating the struct
|
||
|
||
<!-- phoenix:ecto-end -->
|
||
<!-- phoenix:elixir-start -->
|
||
## phoenix:elixir usage
|
||
## Elixir guidelines
|
||
|
||
- Elixir lists **do not support index based access via the access syntax**
|
||
|
||
**Never do this (invalid)**:
|
||
|
||
i = 0
|
||
mylist = ["blue", "green"]
|
||
mylist[i]
|
||
|
||
Instead, **always** use `Enum.at`, pattern matching, or `List` for index based list access, ie:
|
||
|
||
i = 0
|
||
mylist = ["blue", "green"]
|
||
Enum.at(mylist, i)
|
||
|
||
- Elixir variables are immutable, but can be rebound, so for block expressions like `if`, `case`, `cond`, etc
|
||
you *must* bind the result of the expression to a variable if you want to use it and you CANNOT rebind the result inside the expression, ie:
|
||
|
||
# INVALID: we are rebinding inside the `if` and the result never gets assigned
|
||
if connected?(socket) do
|
||
socket = assign(socket, :val, val)
|
||
end
|
||
|
||
# VALID: we rebind the result of the `if` to a new variable
|
||
socket =
|
||
if connected?(socket) do
|
||
assign(socket, :val, val)
|
||
end
|
||
|
||
- **Never** nest multiple modules in the same file as it can cause cyclic dependencies and compilation errors
|
||
- **Never** use map access syntax (`changeset[:field]`) on structs as they do not implement the Access behaviour by default. For regular structs, you **must** access the fields directly, such as `my_struct.field` or use higher level APIs that are available on the struct if they exist, `Ecto.Changeset.get_field/2` for changesets
|
||
- Elixir's standard library has everything necessary for date and time manipulation. Familiarize yourself with the common `Time`, `Date`, `DateTime`, and `Calendar` interfaces by accessing their documentation as necessary. **Never** install additional dependencies unless asked or for date/time parsing (which you can use the `date_time_parser` package)
|
||
- Don't use `String.to_atom/1` on user input (memory leak risk)
|
||
- Predicate function names should not start with `is_` and should end in a question mark. Names like `is_thing` should be reserved for guards
|
||
- Elixir's builtin OTP primitives like `DynamicSupervisor` and `Registry`, require names in the child spec, such as `{DynamicSupervisor, name: MyApp.MyDynamicSup}`, then you can use `DynamicSupervisor.start_child(MyApp.MyDynamicSup, child_spec)`
|
||
- Use `Task.async_stream(collection, callback, options)` for concurrent enumeration with back-pressure. The majority of times you will want to pass `timeout: :infinity` as option
|
||
|
||
## Mix guidelines
|
||
|
||
- Read the docs and options before using tasks (by using `mix help task_name`)
|
||
- To debug test failures, run tests in a specific file with `mix test test/my_test.exs` or run all previously failed tests with `mix test --failed`
|
||
- `mix deps.clean --all` is **almost never needed**. **Avoid** using it unless you have good reason
|
||
|
||
<!-- phoenix:elixir-end -->
|
||
<!-- phoenix:html-start -->
|
||
## phoenix:html usage
|
||
## Phoenix HTML guidelines
|
||
|
||
- Phoenix templates **always** use `~H` or .html.heex files (known as HEEx), **never** use `~E`
|
||
- **Always** use the imported `Phoenix.Component.form/1` and `Phoenix.Component.inputs_for/1` function to build forms. **Never** use `Phoenix.HTML.form_for` or `Phoenix.HTML.inputs_for` as they are outdated
|
||
- When building forms **always** use the already imported `Phoenix.Component.to_form/2` (`assign(socket, form: to_form(...))` and `<.form for={@form} id="msg-form">`), then access those forms in the template via `@form[:field]`
|
||
- **Always** add unique DOM IDs to key elements (like forms, buttons, etc) when writing templates, these IDs can later be used in tests (`<.form for={@form} id="product-form">`)
|
||
- For "app wide" template imports, you can import/alias into the `my_app_web.ex`'s `html_helpers` block, so they will be available to all LiveViews, LiveComponent's, and all modules that do `use MyAppWeb, :html` (replace "my_app" by the actual app name)
|
||
|
||
- Elixir supports `if/else` but **does NOT support `if/else if` or `if/elsif`. **Never use `else if` or `elseif` in Elixir**, **always** use `cond` or `case` for multiple conditionals.
|
||
|
||
**Never do this (invalid)**:
|
||
|
||
<%= if condition do %>
|
||
...
|
||
<% else if other_condition %>
|
||
...
|
||
<% end %>
|
||
|
||
Instead **always** do this:
|
||
|
||
<%= cond do %>
|
||
<% condition -> %>
|
||
...
|
||
<% condition2 -> %>
|
||
...
|
||
<% true -> %>
|
||
...
|
||
<% end %>
|
||
|
||
- HEEx require special tag annotation if you want to insert literal curly's like `{` or `}`. If you want to show a textual code snippet on the page in a `<pre>` or `<code>` block you *must* annotate the parent tag with `phx-no-curly-interpolation`:
|
||
|
||
<code phx-no-curly-interpolation>
|
||
let obj = {key: "val"}
|
||
</code>
|
||
|
||
Within `phx-no-curly-interpolation` annotated tags, you can use `{` and `}` without escaping them, and dynamic Elixir expressions can still be used with `<%= ... %>` syntax
|
||
|
||
- HEEx class attrs support lists, but you must **always** use list `[...]` syntax. You can use the class list syntax to conditionally add classes, **always do this for multiple class values**:
|
||
|
||
<a class={[
|
||
"px-2 text-white",
|
||
@some_flag && "py-5",
|
||
if(@other_condition, do: "border-red-500", else: "border-blue-100"),
|
||
...
|
||
]}>Text</a>
|
||
|
||
and **always** wrap `if`'s inside `{...}` expressions with parens, like done above (`if(@other_condition, do: "...", else: "...")`)
|
||
|
||
and **never** do this, since it's invalid (note the missing `[` and `]`):
|
||
|
||
<a class={
|
||
"px-2 text-white",
|
||
@some_flag && "py-5"
|
||
}> ...
|
||
=> Raises compile syntax error on invalid HEEx attr syntax
|
||
|
||
- **Never** use `<% Enum.each %>` or non-for comprehensions for generating template content, instead **always** use `<%= for item <- @collection do %>`
|
||
- HEEx HTML comments use `<%!-- comment --%>`. **Always** use the HEEx HTML comment syntax for template comments (`<%!-- comment --%>`)
|
||
- HEEx allows interpolation via `{...}` and `<%= ... %>`, but the `<%= %>` **only** works within tag bodies. **Always** use the `{...}` syntax for interpolation within tag attributes, and for interpolation of values within tag bodies. **Always** interpolate block constructs (if, cond, case, for) within tag bodies using `<%= ... %>`.
|
||
|
||
**Always** do this:
|
||
|
||
<div id={@id}>
|
||
{@my_assign}
|
||
<%= if @some_block_condition do %>
|
||
{@another_assign}
|
||
<% end %>
|
||
</div>
|
||
|
||
and **Never** do this – the program will terminate with a syntax error:
|
||
|
||
<%!-- THIS IS INVALID NEVER EVER DO THIS --%>
|
||
<div id="<%= @invalid_interpolation %>">
|
||
{if @invalid_block_construct do}
|
||
{end}
|
||
</div>
|
||
|
||
<!-- phoenix:html-end -->
|
||
<!-- phoenix:liveview-start -->
|
||
## phoenix:liveview usage
|
||
## Phoenix LiveView guidelines
|
||
|
||
- **Never** use the deprecated `live_redirect` and `live_patch` functions, instead **always** use the `<.link navigate={href}>` and `<.link patch={href}>` in templates, and `push_navigate` and `push_patch` functions LiveViews
|
||
- **Avoid LiveComponent's** unless you have a strong, specific need for them
|
||
- LiveViews should be named like `AppWeb.WeatherLive`, with a `Live` suffix. When you go to add LiveView routes to the router, the default `:browser` scope is **already aliased** with the `AppWeb` module, so you can just do `live "/weather", WeatherLive`
|
||
- Remember anytime you use `phx-hook="MyHook"` and that js hook manages its own DOM, you **must** also set the `phx-update="ignore"` attribute
|
||
- **Never** write embedded `<script>` tags in HEEx. Instead always write your scripts and hooks in the `assets/js` directory and integrate them with the `assets/js/app.js` file
|
||
|
||
### LiveView streams
|
||
|
||
- **Always** use LiveView streams for collections for assigning regular lists to avoid memory ballooning and runtime termination with the following operations:
|
||
- basic append of N items - `stream(socket, :messages, [new_msg])`
|
||
- resetting stream with new items - `stream(socket, :messages, [new_msg], reset: true)` (e.g. for filtering items)
|
||
- prepend to stream - `stream(socket, :messages, [new_msg], at: -1)`
|
||
- deleting items - `stream_delete(socket, :messages, msg)`
|
||
|
||
- When using the `stream/3` interfaces in the LiveView, the LiveView template must 1) always set `phx-update="stream"` on the parent element, with a DOM id on the parent element like `id="messages"` and 2) consume the `@streams.stream_name` collection and use the id as the DOM id for each child. For a call like `stream(socket, :messages, [new_msg])` in the LiveView, the template would be:
|
||
|
||
<div id="messages" phx-update="stream">
|
||
<div :for={{id, msg} <- @streams.messages} id={id}>
|
||
{msg.text}
|
||
</div>
|
||
</div>
|
||
|
||
- LiveView streams are *not* enumerable, so you cannot use `Enum.filter/2` or `Enum.reject/2` on them. Instead, if you want to filter, prune, or refresh a list of items on the UI, you **must refetch the data and re-stream the entire stream collection, passing reset: true**:
|
||
|
||
def handle_event("filter", %{"filter" => filter}, socket) do
|
||
# re-fetch the messages based on the filter
|
||
messages = list_messages(filter)
|
||
|
||
{:noreply,
|
||
socket
|
||
|> assign(:messages_empty?, messages == [])
|
||
# reset the stream with the new messages
|
||
|> stream(:messages, messages, reset: true)}
|
||
end
|
||
|
||
- LiveView streams *do not support counting or empty states*. If you need to display a count, you must track it using a separate assign. For empty states, you can use Tailwind classes:
|
||
|
||
<div id="tasks" phx-update="stream">
|
||
<div class="hidden only:block">No tasks yet</div>
|
||
<div :for={{id, task} <- @stream.tasks} id={id}>
|
||
{task.name}
|
||
</div>
|
||
</div>
|
||
|
||
The above only works if the empty state is the only HTML block alongside the stream for-comprehension.
|
||
|
||
- **Never** use the deprecated `phx-update="append"` or `phx-update="prepend"` for collections
|
||
|
||
### LiveView tests
|
||
|
||
- `Phoenix.LiveViewTest` module and `LazyHTML` (included) for making your assertions
|
||
- Form tests are driven by `Phoenix.LiveViewTest`'s `render_submit/2` and `render_change/2` functions
|
||
- Come up with a step-by-step test plan that splits major test cases into small, isolated files. You may start with simpler tests that verify content exists, gradually add interaction tests
|
||
- **Always reference the key element IDs you added in the LiveView templates in your tests** for `Phoenix.LiveViewTest` functions like `element/2`, `has_element/2`, selectors, etc
|
||
- **Never** tests again raw HTML, **always** use `element/2`, `has_element/2`, and similar: `assert has_element?(view, "#my-form")`
|
||
- Instead of relying on testing text content, which can change, favor testing for the presence of key elements
|
||
- Focus on testing outcomes rather than implementation details
|
||
- Be aware that `Phoenix.Component` functions like `<.form>` might produce different HTML than expected. Test against the output HTML structure, not your mental model of what you expect it to be
|
||
- When facing test failures with element selectors, add debug statements to print the actual HTML, but use `LazyHTML` selectors to limit the output, ie:
|
||
|
||
html = render(view)
|
||
document = LazyHTML.from_fragment(html)
|
||
matches = LazyHTML.filter(document, "your-complex-selector")
|
||
IO.inspect(matches, label: "Matches")
|
||
|
||
### Form handling
|
||
|
||
#### Creating a form from params
|
||
|
||
If you want to create a form based on `handle_event` params:
|
||
|
||
def handle_event("submitted", params, socket) do
|
||
{:noreply, assign(socket, form: to_form(params))}
|
||
end
|
||
|
||
When you pass a map to `to_form/1`, it assumes said map contains the form params, which are expected to have string keys.
|
||
|
||
You can also specify a name to nest the params:
|
||
|
||
def handle_event("submitted", %{"user" => user_params}, socket) do
|
||
{:noreply, assign(socket, form: to_form(user_params, as: :user))}
|
||
end
|
||
|
||
#### Creating a form from changesets
|
||
|
||
When using changesets, the underlying data, form params, and errors are retrieved from it. The `:as` option is automatically computed too. E.g. if you have a user schema:
|
||
|
||
defmodule MyApp.Users.User do
|
||
use Ecto.Schema
|
||
...
|
||
end
|
||
|
||
And then you create a changeset that you pass to `to_form`:
|
||
|
||
%MyApp.Users.User{}
|
||
|> Ecto.Changeset.change()
|
||
|> to_form()
|
||
|
||
Once the form is submitted, the params will be available under `%{"user" => user_params}`.
|
||
|
||
In the template, the form form assign can be passed to the `<.form>` function component:
|
||
|
||
<.form for={@form} id="todo-form" phx-change="validate" phx-submit="save">
|
||
<.input field={@form[:field]} type="text" />
|
||
</.form>
|
||
|
||
Always give the form an explicit, unique DOM ID, like `id="todo-form"`.
|
||
|
||
#### Avoiding form errors
|
||
|
||
**Always** use a form assigned via `to_form/2` in the LiveView, and the `<.input>` component in the template. In the template **always access forms this**:
|
||
|
||
<%!-- ALWAYS do this (valid) --%>
|
||
<.form for={@form} id="my-form">
|
||
<.input field={@form[:field]} type="text" />
|
||
</.form>
|
||
|
||
And **never** do this:
|
||
|
||
<%!-- NEVER do this (invalid) --%>
|
||
<.form for={@changeset} id="my-form">
|
||
<.input field={@changeset[:field]} type="text" />
|
||
</.form>
|
||
|
||
- You are FORBIDDEN from accessing the changeset in the template as it will cause errors
|
||
- **Never** use `<.form let={f} ...>` in the template, instead **always use `<.form for={@form} ...>`**, then drive all form references from the form assign as in `@form[:field]`. The UI should **always** be driven by a `to_form/2` assigned in the LiveView module that is derived from a changeset
|
||
|
||
<!-- phoenix:liveview-end -->
|
||
<!-- phoenix:phoenix-start -->
|
||
## phoenix:phoenix usage
|
||
## Phoenix guidelines
|
||
|
||
- Remember Phoenix router `scope` blocks include an optional alias which is prefixed for all routes within the scope. **Always** be mindful of this when creating routes within a scope to avoid duplicate module prefixes.
|
||
|
||
- You **never** need to create your own `alias` for route definitions! The `scope` provides the alias, ie:
|
||
|
||
scope "/admin", AppWeb.Admin do
|
||
pipe_through :browser
|
||
|
||
live "/users", UserLive, :index
|
||
end
|
||
|
||
the UserLive route would point to the `AppWeb.Admin.UserLive` module
|
||
|
||
- `Phoenix.View` no longer is needed or included with Phoenix, don't use it
|
||
|
||
<!-- phoenix:phoenix-end -->
|
||
<!-- fluxon-start -->
|
||
## fluxon usage
|
||
_Fluxon UI Components_
|
||
|
||
[fluxon usage rules](deps/fluxon/usage-rules.md)
|
||
<!-- fluxon-end -->
|
||
<!-- usage-rules-end -->
|