I call the functions from the service layer (that use models) that return :ok or error tuples, same as you do in controllers.
To avoid having too much dom manipulation I render templates to strings (same partials that are used on render) and pass them to frontend, so channels work much like controllers.
On the front end you might get away with jQuery (I did in the latest app), since there’s not much JS this way. For a proper SPA I’d go for Vue or React or something.
It’s not easy to deliver an example without over-simplifying or polluting it with non relevant parts, I’ll do my best though, hope it still paints a picture
// some_socket.js
// import base socket stuff and create a socket / channel vars
const getItems = (opts, onSuccess, onError) => {
base.pushMessage(channel, "get-items", opts, onSuccess, onError);
};
export {someSocket as socket};
export {getItems as getItems};
// some_template_partial.html.eex
let someSocket = require('app/some_socket');
someSocket.socket.connect();
let onSuccess = function(response) {
$("#partial-id").replaceWith(response.some_html_string);
// render info notification
}
let onError = function(response) {
// render error notification
}
$someElement.click(function(e){
e.preventDefault();
// get relevant data from dom
someSocket.getItems(data, onSuccess, onError);
});
// some_channel.ex
def handle_in("get-items", payload, socket) do
case SomeService.get_items(payload) do
{:ok, items} ->
html = render_to_string(LayoutView, "partials/some.html", [socket: socket, items: items])
{:reply, {:ok, %{some_html_string: html}}, socket}
{:error, reason} -> {:reply, {:error, %{reason: reason}}, socket}
end
end
Also check out Drab and Unpoly as suggested by @OvermindDL1 - they use similar idea but provide more abstractions.
What I find nice about this approach - it’s pretty clean and organized (despite all the love jQuery front ends get), very fast (often feels like working offline) and you get less rituals with all the actions, api routes etc that you get when working with AJAX.