8000 GitHub - andresgutgon/wayfinder_ex: Wayfinder connects your Phoenix backend and TypeScript frontend by generating TypeScript functions that correspond to your controllers. These functions are fully typed and ready to import into your frontend code.
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Wayfinder connects your Phoenix backend and TypeScript frontend by generating TypeScript functions that correspond to your controllers. These functions are fully typed and ready to import into your frontend code.

License

Notifications You must be signed in to change notification settings

andresgutgon/wayfinder_ex

Repository files navigation

CI

Introduction

Phoenix Wayfinder seamlessly connects your Phoenix backend with your TypeScript frontend. It automatically generates fully-typed, importable TypeScript functions for your controllers, allowing you to call your Phoenix endpoints directly from your client code as if they were regular functions. No more hardcoded URLs, guessing route parameters, or manually syncing backend changes.

Important

Wayfinder is currently in Beta. The API may change before the v1.0.0 release. All notable changes will be documented in the changelog.

Table of Contents

Installation

To get started, install Wayfinder using the Composer package manager:

defp deps do
  [
    {:wayfinder_ex, "~> 0.1.0"}
  ]
end

Then, run the following command to fetch the dependencies:

mix deps.get

Configuration

Configure Wayfinder in your config/config.exs file. This configuration specifies which OTP app Wayfinder belongs to and which router to use for generating the TypeScript functions.

# config/config.exs
config :wayfinder_ex,
  otp_app: :my_app,
  router: MyApp.Router,

  # Optional: Specify paths to ignore when generating routes
  # ignore_paths: ["^/backoffice"]
# lib/my_app_web/router.ex
defmodule MyApp.Router do
  use MyApp, :router
  +  use Wayfinder.PhoenixRouter

  # ...rest of your router code
end

Development Setup

For development, you can enable the Wayfinder.RoutesWatcher to automatically reload routes when they change. This is useful during development to avoid restarting the server every time you modify your routes.

defmodule MyApp.Application do
  def start(_type, _args) do
    children = [
      MyApp.Telemetry,
      MyApp.Repo,
      {DNSCluster, query: Application.get_env(:my_app, :dns_cluster_query) || :ignore},
      {Phoenix.PubSub, name: MyApp.PubSub},
      # ... other children
    ]

    children =
      if Mix.env() == :dev do
+        children ++ [{Wayfinder.RoutesWatcher, []}]
      else
        children
      end

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end

  # ...rest of your application code
end

Production Setup

In production, you need to run the Wayfinder mix task to generate fresh TypeScript functions.

# mix.exs
defmodule MyApp.MixProject do
  use Mix.Project

  defp aliases do
    [
      # ...other aliases
+      "assets.build": ["wayfinder.generate", "cmd pnpm --dir assets run build"],
      "assets.deploy": ["assets.build", "phx.digest"]
    ]
  end
end

TypeScript Setup

It is recommended to use aliases in your project so you can reference Wayfinder helpers and actions without using relative paths.

{
  "compilerOptions": {
    "paths": {
      "@/*": ["./js/*"]
    }
  }
}

Vite Users

If you are using Vite, you can set the same alias as follows:

import { resolve } from 'node:path'
import { defineConfig } from 'vite'

export default defineConfig({
  resolve: {
    alias: {
      '@': resolve(__dirname, './js'),
    },
  },
})

Once this is set up, you can import the generated TypeScript functions in your client code like this:

import UsersController from '@/actions/UsersController'

// Or import only one action (recommended for better tree-shaking)
import { create } from '@/actions/UsersController'

You can see a full example in this example project.

Ignoring Generated Files

It is recommended to configure your project to ignore generated files.

ESLint Configuration

Generated TypeScript from Wayfinder might not follow your project's ESLint rules. It is recommended to ignore the generated files in your assets/eslint.config.js file:

import tseslint from 'typescript-eslint'

export default tseslint.config(
  { ignores: ['dist', 'js/actions/**', 'js/wayfinder/**'] },
  {
    // ... your other ESLint rules
  },
)

Prettier Configuration

Similarly, you may want to ignore the generated files in your assets/.prettierignore file:

js/actions/**/*
js/wayfinder/**/*

Generated Files

When Wayfinder is set up and you change your routes or run mix wayfinder.generate, it will generate two new directories in your assets/js folder:

assets/
  └── js/
      ├── wayfinder/                  # Shared helper functions (e.g., .url(), .visit(), etc.)
      └── actions/                    # Auto-generated route handlers
          ├── UsersController/
          │   └── index.ts            # Handles routes for UsersController#index
          ├── HomeController/
          │   └── index.ts            # Handles routes for HomeController#index
          └── Admin/
              └── TasksController/
                  └── index.ts        # Handles routes for Admin::TasksController#index

Usage

Now that Wayfinder is set up and you know how to import the generated TypeScript functions, let's see how to use them in your client code. Generated functions are typed with the parameters you defined in your Phoenix router. For example:

defmodule MyApp.Router do
  use MyApp, :router
+  use Wayfinder.PhoenixRouter

  resources("/users", UsersController)
end

We defined a full CRUD resource for users, so Wayfinder will generate the following TypeScript functions:

// assets/js/actions/UsersController/index.ts
const UsersController = {
  create,
  delete: deleteMethod,
  index,
  show,
  update,
  edit,
  new: newMethod,
}

export default UsersController

Suppose you want to edit a user. You can use the edit function like this with Inertia's useForm:

import { edit } from '@/actions/UsersController'

function EditUser({ name, id }: { id: number; name: string; }) {
  const { data, setData, post, processing, errors } = useForm({
    id,
    name,
  })

  function submit(e) {
    e.preventDefault()
    // .url is typed. You can only pass an `id` here. It can be a string or number
+    post(edit.url({ id: data.id }))
    // Alternatively, you can pass just the id. This is equivalent to the above
    // post(edit.url(data.id))
  }

  return (
    <form onSubmit={submit}>
      <input type="text" value={data.name} onChange={e => setData('name', e.target.value)} />
      <button type="submit" disabled={processing}>Login</button>
    </form>
  )
}

Important

We have tried to support all ways of defining routes in Phoenix, including glob routes like get "/something/*path", SomethingController, :index.

Important

Phoenix does not support optional parameters, but if you define the same route with and without a parameter, Wayfinder will generate both functions for you. For example:

defmodule MyApp.Router do
  use MyApp, :router
  use Wayfinder.PhoenixRouter

  get "/something", SomethingController, :show
  get "/something/:my_parameter", SomethingController, :show
end

Generated TypeScript functions will be:

import { show } from '@/actions/SomethingController'

// This is valid
show.url() + // No parameters
  // This is also valid
  show.url({ my_parameter: 'value' }) // With parameter

Checking Current URL

If you need to know which page you are on, Inertia's usePage hook can help. It also works for SSR-rendered pages.

import { usePage } from '@inertiajs/react'
import { Menu } from '@/components/Menu'
import { home } from '@/actions/HomeController'
import { organizations } from '@/actions/OrganizationsController'
import { contacts } from '@/actions/ContactsController'
import { reports } from '@/actions/ReportsController'

function MyMenu() {
  const { url: currentPath } = usePage()
  return (
    <div className={className}>
      <MenuItem
        text='Dashboard'
        link={home.url({ currentPath, exactMatch: true })}
      />
      <MenuItem
        text='Organizations'
        link={organizations.url({ currentPath })}
        icon={<Building size={20} />}
      />
      <MenuItem
        text='Contacts'
        link={contacts.url({ currentPath })}
        icon={<Users size={20} />}
      />
      <MenuItem
        text='Reports'
        link={reports.url({ currentPath })}
        icon={<Printer size={20} />}
      />
    </div>
  )
}

Important

The currentPath parameter is optional, but it helps Wayfinder generate the correct URL for the current page. If you don't pass it, Wayfinder will generate a URL without the current path.

Important

You can pass exactMatch: true to the home.url() function. This will generate a URL that matches the current path exactly, so you can use it to highlight the current page in your menu. The home menu item will only be selected if the current path is exactly /.

Why the Name "Wayfinder"?

You may notice that this README is very similar to Laravel's version of Wayfinder. That is intentional! The goal is to provide a consistent experience across frameworks. This package adapts the great ideas from the Laravel version to the Phoenix ecosystem, so you can enjoy the same benefits in your Phoenix applications.

Thank you to the Laravel team for the inspiration! 🙌

Contributing

Thank you for considering contributing to Elixir Wayfinder! You can read the contribution guide here.

License

Wayfinder is open-source software licensed under the MIT license.

About

Wayfinder connects your Phoenix backend and TypeScript frontend by generating TypeScript functions that correspond to your controllers. These functions are fully typed and ready to import into your frontend code.

Resources

License

Stars

Watchers

Forks

Packages

No packages published
0