How to write an action to dynamically sort a resource based on user input?

I wanna write an action to dynamically sort a resource based on user input. It’s going to be used for rendering dynamic tables on Live View. The user can click on any column header to sort the table by that column (:asc or :desc). How can I achieve that? Thanks.

If you aren’t doing this for learning purposes and just wanna get 'er done, check out Flop and its companion, Flop Phoenix

Thanks, @sodapopcan. I’ll take a look. Is Flop compatible with Ash?

Oh I’m sorry, I totally missed that this was posted in Ash Forum. I have to pay better attention to that.

I’m not qualified to answer as I haven’t used Ash yet. I’m pretty sure Ash has its own solution to this, or at least partial solution, and Flop is likely not a good fit.

No problem.

Yes, Ash is ready for pagination (offset and keyset), but the examples in the docs don’t show how to do it dynamically.

BTW, I highly recommend you to take a look at Ash. It is not easy to get started (I’m on this endeavour for about 3 weeks now), but as soon as you get used to it (at least with the basic functionalities), you start to speed up the development performance.

Oh ya, I’ve looked at Ash and will be trying it out sooner or later. I just don’t have the cycles right now. Once it has solid sqlite support I will likely pull it into my personal project.

1 Like

There are a few different options. You can build a sort i.e [{:field, :asc}, {:field, :desc}] and pass it to Ash.Query.sort. We also have a tool for parsing a sort as a string, which is often best because you want to serialize a sort as a query parameter so it is remembered. For that, you’d use Ash.Sort.parse_input/3

So you might have something like this:

# params = %{"sort" => "name,-age"}

results = 
  case Ash.Sort.parse_input(Resource, params["sort"]}) do
    {:ok, sort} ->
       Resource |> Ash.Query.for_read(:read_action) |> Ash.Query.sort(sort) |> Api.read!()
    _ ->
      Resource |> Ash.Query.for_read(:read_action)
  end

If you are using a code_interface you can pass the query option, i.e

  case Ash.Sort.parse_input(Resource, params["sort"]}) do
    {:ok, sort} ->
       Resource.some_read_interface!(query: Ash.Query.sort(Resource, sort))
    _ ->
      Resource.some_read_interface!()
  end

EDIT: If you build a sort up as a list, just make sure you also pass that to Ash.Sort.parse_input/2 to make sure it is validated against user input. This protects against things like [{:private_field, :ask}]. Its okay for you to sort on private fields, but from user input, it typically isn’t allowed so parse_input/2 validates it.

3 Likes

Excellent, @zachdaniel. This will help me a lot.
Thank you very much.

1 Like

You can possibly specify the sort order for a known attribute quick and dirty as follows (caveat, not at my computer, typing on my phone so it’s not tested(:

# in the resource

code_interface do
  define_for Helpdesk.Support

  define :top_ten, args: [:sort_dir]
end

read :top_ten do
  argument :sort_dir, :atom do
    constraints [one_of: [:asc, :desc]]
    default :asc
  end

  prepare build(top: 10, sort: [opened_at: arg(:sort_dir)])
end

Using Ash query directly is another way of specifying sort order

MyApp.Post
|> Ash.Query.filter(likes > 10)
|> Ash.Query.sort([:title])
|> MyApp.Api.read!()

MyApp.Author
|> Ash.Query.aggregate(:published_post_count, :posts, query: [filter: [published: true]])
|> Ash.Query.sort(published_post_count: :desc)
|> Ash.Query.limit(10)
|> MyApp.Api.read!()

MyApp.Author
|> Ash.Query.load([:post_count, :comment_count])
|> Ash.Query.load(posts: [:comments])
|> MyApp.Api.read!()
1 Like