-
Notifications
You must be signed in to change notification settings - Fork 0
π¦βπ₯ Elixir & Phoenix Crash Course Notes π¦βπ₯
1. values from do block of comprehension get packaged into a list, so have to use a fancy :into
this gives a list
> for {key, val} <- style, do: {String.to_atom(key), val}
[border: "2px", height: 20, width: 10]
But you can use the :into option to package the returned values into any Collectable. And a map is a collectable, so we can do this:
> for {key, val} <- style, into: %{}, do: {String.to_atom(key), val}
%{border: "2px", height: 20, width: 10}
2. Elixir's match function can have even more general DSL.
so, this would be a set of macros (DSL) that eventually will get replaced with actual pipe |>
operators and all. These macros result in Pheonix's match functions.
![image](https://private-user-images.githubusercontent.com/38996397/284026796-41ea06ec-da4d-49a5-8905-e2d800299f6d.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzkzNDcxMzIsIm5iZiI6MTczOTM0NjgzMiwicGF0aCI6Ii8zODk5NjM5Ny8yODQwMjY3OTYtNDFlYTA2ZWMtZGE0ZC00OWE1LTg5MDUtZTJkODAwMjk5ZjZkLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMTIlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjEyVDA3NTM1MlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTZmNDA1MjE4NTZjZDE5NDg0NGJkZjE2NTAyZDM4ODRhNjYwZDQ2YjAwYmM3YzAxOGIyMmM5MjczYjNkMzFiYTcmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.EYwJfG81yoCzFRThg7ai0Tu64LCZML4-QRiblsHlTZk)
- random reminder that the controller functions in Phoenix are called actions.
4. Testing notes:
* doctests exist, that is really useful5. Usign an erlang lib requires transcoding from erlang to elixir
common steps: 1. update fn defn to elixir syntax 2. update atoms (erlang atoms are in lowercase) to be colon-prefixed (i.e. ok --> :ok) * atoms are definit 3. update erlang vars with uppercase to lowercase 4. update subroutine calls by colon-prefixing 5. ...Erlang |> Elixir
Here's a summary of things to keep in mind when transcoding Erlang to Elixir:
-
Erlang atoms start with a lowercase letter, whereas Elixir atoms start with a colon (:). For example, ok in Erlang becomes :ok in Elixir.
-
Erlang variables start with an uppercase letter, whereas Elixir variables start with a lowercase letter. For example, Socket in Erlang becomes socket in Elixir.
-
Erlang modules are always referenced as atoms. For example, gen_tcp in Erlang becomes :gen_tcp in Elixir.
-
Function calls in Erlang use a colon (:) whereas function calls in Elixir always us a dot (.). For example, gen_tcp:listen in Erlang becomes :gen_tcp.listen in Elixir. (Replace the colon with a dot.)
-
Last, but by no means least, it's important to note that Erlang strings aren't the same as Elixir strings. In Erlang, a double-quoted string is a list of characters whereas in Elixir a double-quoted string is a sequence of bytes (a binary). Thus, double-quoted Erlang and Elixir strings aren't compatible. So if an Erlang function takes a string argument, you can't pass it an Elixir string. Instead, Elixir has a character list which you can create using single-quotes rather than double-quotes. For example, 'hello' is a list of characters that's compatible with the Erlang string "hello".
- Stateful Servers in Elixir: (see the exercises within unit 24 of the basic course for guided examples)
- Basic logic required would be a start routine (init) and a loop routine that keeps looping back so that the server waits for msgs. The start routine is where a new proc (for the stateful server) is init and the proc's PID is registered so that it can be read again.
- There's a common pattern where the same module will have both Client Interface and Server Interface
- Client Interface:
- actions that other clients can use to interact with this stateful server
- start function
- Server Interface: listen loops and message receiving logic from senders to the stateful server
- GenServer reminders:
- GenServer can be described as a behaviour for handling stateful processes (other example being things like Supervisor...)
- some of the function calls get automatically called (e.g. init() where the state init logic is placed) and they can be blocking ( init is sync)
- So in total, for stateful procs, we can use Task, Agent or GenServer.
Some heuristics
- the default callbacks in GenServer includes a
terminate()
function which is similar to a fini() function that allows us to do cleanups on killing of the server, but it may not always be called and this is where a Supervisor has a use-case. - for the catchall case, instead of using the
unexpected
clause, thehandle_info()
callback is what is used for GenServer. If this function is overridden, then it will no longer handle the catchall case, so we would have to implement our own catch-all version of thehandle_info()
callback fn.- in the example, they used
handle_info()
callback that implemented a background scheduled task, which continually calls the same timeout so that the callback gets triggered over a defined interval.
- in the example, they used
- check out Erlang's sys module to get system related information about processes.
- Linking processes:
- if it's an OTP-compliant server (GenServer) then we can use OTP's Supervisor behaviour. If not, then wrap a custom server process with a GenServer process.
- Options: Link two processes (bi-directional) OR make one process monitor the other (unidirectional nav)
- using a two processes are linked via Process.* then it's a bi-directional link (that's why terminations should be trapped and handled explicitly so that one dying doesn't kill the other)
- By default, if the exit signal indicates that the process terminated abnormally, the linked process terminates with the same reason unless the linked process is trapping exits. If it's trapping exits, then the linked process does not terminate. Instead, the exit signal is converted to a message that's sent to the mailbox of the linked process
- Supervisors:
- children can be GenServers or other Supervisors
- child_spec() shows a server's spec from the POV of a child proc. Shows how it's started, restarted (in a MFA pattern)
- a top level supervisor as a root can help to create a supervisor tree which shall help respawn things when children or parents are killed
- the analogy of process spawning model in Elixir to organisms, cell death and all that. Understanding the overall aggregate behaviours by mapping it to the body itself.
- Task mechanisms ==> good for deferred computations, where we don't need any blocking and we don't care about the response. Can do tasks async and spin it off to childprocs so that the msg queue doesn't get blocked. GenServer can end up blocking things (e.g. when the msg queue bloats up).
- rmb use of pin operator:
- rmb that overriding a var is actually creating a new variable.
pid = pid + 1
. This is still immutable actually. So the pin operator's purpose is to prevent the "mutation" and just to get a reference to value of the var. - Good for db queries where there's a ref to a val as opposed to a var (as opposed to not using the ^ operator where it would have matched the variable instead of the value)
Notes :
-
db migration files can be directly modified to effect the required db related changes. Once migrated, the db is updated. Updating the schema file will allow us to conform to the new structure of the db. These are the two main parts.
-
changes functions are used under the hood for form components. They are interoperable.
-
Examples:
- command we ran to generate the database migration, Boat Ecto schema module, and Boats context module:
mix phx.gen.context Boats Boat boats model:string type:string price:string image:string
- The command takes the name of the context module (Boats) followed by the Ecto schema module (Boat) and its lowercase plural name used as the database table name (boats), and then any attributes and their types. We used a string type for the price attribute since prices are represented as a series of dollar signs ("$$$").
-
Liveview Module:
-
Stored in
/lib/live
, has 3 callback functions (mount, render, handle_event
) as its interface. -
handing proc state:
- Proc state found within the socket parameter to
mount()
-
assigns()
to assign state upon mount. it handles the LiveView state.- assigns happens in sequence and can be blocking. to make it non-blocking, send internal msgs.
- Proc state found within the socket parameter to
-
handling html rendering:
- render accepts the socket's assigns map param, which holds the LiveView state. it shall return heex templates, with state variables accessed via
@
syntax. Can be externalised to a separate heex file. The rendering renders the Live View's current state. - the
~H"""[...]"""
H sigil indicates that it's a heex template - careful about the common gotchas for heex templates:
- don't use
do..end
blocks for anything other than the primitives (if, case, for) else the change tracking won't work because heex won't be able to handle the change tracking on custom user-defined functions and such. use the given conveniences instead. - don't use variables (var definition, or variable related logic) in the same scope as the heex template. e.g. don't do variable arithmetic, just use a provided convenience function for this.
- don't use
- heex does html validation by default, so when dynamically using state from the assigns map, we surrounded it by curly
{}
braces containing elixir expressions. Elsewhere, we use the usual eex tags. - within the heex is where inbound event triggers (e.g.
phx-click
) are indicated, which will get emitteddispatchedwhen that action is done (e.g. button clicked). The event handling logic is implemented within its correspondinghandle_event()
callback for this LiveView module.
- render accepts the socket's assigns map param, which holds the LiveView state. it shall return heex templates, with state variables accessed via
-
handling inbound events within the LiveView module:
- handle_event() is event-specific, overloaded
- supports an
update()
function, shorthand toassign()
-
-
LiveView lifecycle:
- there's a layout tree that starts from a root.html.heex that handles the html layouts and such. root -> app -> inner content slots. The pattern is basically using template slots.
- Key events in the lifecycle:
- establishing initial HTTP connect
- app.js setup of websocket connection
- does the necessary js based logic to setup LiveView and establishes the websocket
- result: LiveView joins a phx channel and a stateful process is spawned.
- the LiveView js takes care of patching the DOM diffs for every outgoing-incoming msg over the websocket. See diagrams for LiveView lifecycle.
- Dynamic Forms:
- represents a pattern of receiving external messages by the browser
- Phoenix has form events that can correspond to interactions with forms:
-
phx-change
for value changes -
phx-submit
for submit events
-
- event handler for user-generated events (external) are handled by the
handle_event()
callback, internal events emitted by the LiveView module are handled byhandle_info()
- internal events:
- only way to update a LiveView state is to emit events; same goes for internal events (e.g. ticks)
- LiveView:: mount():
- 2 invocations @ beginning: once when at the initial http req and then again when the websocket has been established
- Some useful events to emit:
- phx-change <-- essentially the "onChange" event
- phx-debounce <-- LiveView natively will support debouncing by just passing that as a eex attribute...
- Database Interactions
- db access and context functions shall be abstracted away from LiveView components so that the LiveView component is agnostic to the data source and all that. Context functions are where we put the crud actions for the db.
- Context modules are where we'd put other functions relevant to what is being stored e.g. get_all_X()
- LiveView has a
temporary_assigns()
- assets can be stored temporarily until the rendering is done and then the memory is free-ed up since the rendering has already happened. This improves overall memory performance esp in the LiveView component.
- NOTE: shouldn't be used if other persistent things (e.g. CSS classes that rely on some state var) exist
-
Summary: to support a particular db schema the following get done: (e.g. add db persistence for volunteers)
- gen the db migration file:
mix phx.gen.context Volunteers Volunteer volunteers name:string phone:string checked_out:boolean
- update the Ecto schema module (Volunteer):
- this is where change sets are defined
- update the context module (Volunteers) to add in crud actions and helpers. The LiveView module shall use the context module to interface with anything db-related
- This is where change sets can be interacted with (e.g.
change_volunteer()
that gets the Changeset for the volunteer and uses it)
- This is where change sets can be interacted with (e.g.
- Update the LiveView module to use the context module.
- gen the db migration file:
- Component Flavours:
- Function Components for reusability:
- returns a heex template, uses the Phoenix.Component behaviour rather than the Liveview behaviour. changes in attr will re-render the function component
- calling a function component is dot-accessed if it's in the same LiveView module.
- Component slots: a way to render children component slots
-
How a slot is defined:
-
defaults slot: for rendering, accessed from within the function component's heex via
render_slot(@inner_block)
. Anything that is outside of a named slot will end up -
named slot(s): for rendering, accessed via a named access like
render_slot(@legal)
-
- Passing values: passed as html props e.g.
<: legal myVal={"nice"}>
. These values are accessed via the assigns map of the function component. - slot and attribute declarations:
- @ compile time checks can be added by adding slot & attribute definitions e.g.
slot :legal, required: true
. If at least one definition is done like that, then all the slots and the attributes must be defined . These just use theattr
and slotmacros
. The scope of these macro definitions are the function components that immediately follow the macros, not all of them
- @ compile time checks can be added by adding slot & attribute definitions e.g.
- It's possible to assign new variables to the assigns map from within the function component. this can be used to either transform an assignment that is incoming, or it can add values if not passed (like default values)
- template embedding is supported, can use global attributes, similar to how we use
rest
in JS
- Function Components for reusability:
- Navigation Routing:
summary:
* can use
<. link>
function component instead of the pure html one * multiple navigation options:- to refresh the page and navigate to a new page: use the
href
attribute - to patch dom diffs by patching the changes to the current LiveView component, uses
patch
attribute - to navigate to a different LiveView component, use the
navigate
attribute, which essentially dismounts the current LiveView process and mounts a new LiveView process.
- to refresh the page and navigate to a new page: use the
- href can be validated paths by using the
~p""
sigil so that the path matching check is done @ compile time and the compiler shall complain if there's a mismatch. - can use live view's default
.link
function component which gives a regular HTML link (it will cause the page to refresh when its clicked on. If we want patching instead of a navigation (and hence refreshing), we want apatch
instead of ahref
. - the
patch
attribute will make it such that the same proc is used (so same PID) and hence the state from before will persist. The HTML diffs are pushed back to the browser over the websocket. The JS library will update the url in the browser and updates the browser's history, all without reloading the page. The pattern here is that it's "patching" the state of the current LiveView process. - adding logic that is params-specific is done by
handle_params()
which is invoked after themount
callback and before therender
callback. Remember to add catch-all handle_params - Alternatively, to navigate to a different LiveView while also killing the existing LiveView process, then use the
navigate
attribute within a<. link>
component. If wepatch
across different LiveViews, then it automatically becomes a redirect. - NOTE: handle all url-related state assignments in the
handle_params()
callback, the rest of the state assignments naturally should be handled in themount()
callback - for such navigation, we can have both client-side initiated navigation (e.g. click link) or server-side initiated navigation (e.g. user chose a drop down for how many items per page).
* client-side: use the function component and attach a patch/navigate attribute
* server-side: use
push_patch()
. Okay seems like push_patch is deprecated and we should use[push_navigate()](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#push_navigate/2)
instead
- Ecto forms and lists:
-
Ecto changesets are the key here. They are used to render the form and they are also used to insert into the repo. I think it's a misconception to view it as a data object, should probably be seen as a deferred function that can effect a change within the db or any other state-keeper -- therefore it gets used to create a form (temp state management) or passed to Ecto Repo (actual persistence)(?). Also probably should link it to the concept of form bindings to the repo.
- prior to effecting that change in states, Changeset's validation logic shall get fired again.
- To render a form that eventually effects a changes via Ecto within the db, we need to render and initial form, which requires an initial Changeset. So the LiveView page's
mount()
function should create the necessary state to create the form. Changesets can get converted to forms via the default function[to_form()](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#to_form/1)
. From the LiveView component, this gets accessed by the assigns map, and we render all these by using live view's default function components (<.form/> <.input/>
and the like...). The input fn component automatically handles the display of validation errors within the Changeset. - for form events that effect changes on a particular struct (e.g. Volunteer), the form params are keyed by that struct name ("volunteer").
- for live validations:
- an on change event needs to be emitted by the form, where a new to_form() gets called using the current form params and naturally updates the error within the changeset
- to display the errors, the actions field of the changeset needs to be set! Can be anything as long as it's non-nil but generally the action ends up being
validate
- remember to add debouncing to the fields so that the validation event doesn't emit too early.
- to create a form not bound to Ecto changesets, we can bind it to a map of attributes instead.
- it's the same thing as the Ecto changset version, just that in the
to_form()
, we pass in a map of attributes instead of passing in a changeset. From withinrender()
, can still access form fields via @form. For handling events, can still match form params by matching the map's keys. Reminder: we can effect navigation changes (e.g. patching of urls) from the server interface usingpush_patch()
- embedding actions within the url:
- via the controller, we can invoke an action which renders a page that is specific to the sub-url. This is done when defining the live routes within the controller itself. Actions will be keyed via
:live_action
atom for this since this action is emitted fro the controller itself.
Diagrams for LiveView Lifecycle
- Streams: shows a way to handle large data volumes without relying on limited in-memory stores
- The goal is to do crud actions to large collections of state without needing to refetch the items from the database after the initial render AND ALSO the LiveView doesn't carry around large collections of items in its state.
- sockets will have collections and state changes would be streaming new entries to the named-socket in order to update that state held within a stream
- Some guidelines about using stream:
- the browser tracks stream items via a
dom-id
, which is what thestreams
map (a parallel to the assigns map for in-mem store). This computed dom-id (which is the first value in the return tuple from the streams map) needs to be used to id the dom element that renders the stream item. - Also, stream items need to be a parent dom container that wraps them. The container will have a unique id (can be anything) and have
phx-update="stream"
as it's binding. The binding helps show where new DOM elements (corresponding to stream items) should be appended / prepended - for in-place updates to state (e.g. toggling state):
- allow the event to be emitted as usual and add a
handle_event
as usual. note that we can add value related bindings for events viaphx-value-*
which can allow us to nest values (e.g. identifier num [id]) that can be referenced to via the params in the event handler. this is how things are added to the event payload. - update the db: use the context module's functions from within the event handler (in the lifeview component). This shouldn't fail, and should update the database.
- handle the re-rendering for the updated stream-item. To do this, we want to change the stream in-place via the
stream_insert()
fn. Since that stream item already exists in the dom with that unique dom-id, then that dom element gets updated.[1]
- allow the event to be emitted as usual and add a
- the browser tracks stream items via a
- performance benefit: after rendering, the stream item isn't kept in memory anymore [ "automatically kicked out of the stream"] (since it's already in the dom, with unique ids and they are contained in the server)
- this behaviour is similar to
temporary_assigns()
, but the key difference is whether the data is dynamic or static:- Use temporary_assigns for static data that only ever needs to be rendered. Once it's rendered, it can safely be kicked out of the LiveView's state to conserve server-side memory.
- Use streams for collections of items that you don't want to hold in memory, but you need to update the collection after it's been initially rendered for example to append, prepend, update, or delete items.
- this behaviour is similar to
[1] Here's an example event handler that handles state update of a stream item in-place
![image](https://private-user-images.githubusercontent.com/38996397/287113529-bbfab056-9820-4704-833f-78ebb9928b34.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzkzNDcxMzIsIm5iZiI6MTczOTM0NjgzMiwicGF0aCI6Ii8zODk5NjM5Ny8yODcxMTM1MjktYmJmYWIwNTYtOTgyMC00NzA0LTgzM2YtNzhlYmI5OTI4YjM0LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMTIlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjEyVDA3NTM1MlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWZhYTZhNWMzMDhmYzkwMTg5MDFiNzM0ZDZhYWE2YjM1OTUwMDNkNDc5M2EzMzA5ODQ1NjBmMzU5OTY1Y2I2NTcmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.gcjEjX1w5dbQVlv0yVCVYkSjtf260aHIUdg0DeiEfLg)
- Live Components: a way to reuse the markup and its event handlers when attempting to externalise a piece of live view code. It's neatly self-contained around a discrete responsibility -- holds its own local state structs, renders markup, emits and handles events (where it either notifies the parent for things it can't do [via msg passing] or updates its own state)
- without it, we'd need to duplicate both the markup (the heex template part) and the event handlers everywhere we reuse the LiveView components.
- rules:
- Live View components must be defined in their own module
- needs to use the
:live_component
behaviour - since the purpose is to render a heex component (with event handlers), the live view component also has a
render()
function. - gotchas:
- live components must have a single static html tag at their root (e.g. a
<div> </div>
tag that encapsulates it) - any events emitted by a live component get send to the parent LiveView. Hence, there's a need to target the events emitted to go to the children (the live component) --
phx-target={@myself}
which is a binding to the<.form/>
from within the live component whereby the@myself
gets resolved to the current component instance which in this case is the child (live component).
- live components must have a single static html tag at their root (e.g. a
- calling that live component:
<.live_component module={<LiveViewComponentName>} id={<anythinghere e.g. :new>} />
- each live component on a page needs to be uniquely identifiable -- need to pass it an id attribute.
- has to be unique amongst that particular type of live component. different types of live components can have colliding IDs and they won't conflict.
- A live component can keep its own state as well, via the same callback functions (
mount()
,render()
, handlers...)- but event-emitting needs to target the child live component explicitly via a phx-target (if not it defaults to looking into the parent)
- but the child live component won't have access to the streams map since the streams map will be within the parent component. Therefore, the child needs to notify the parent component that the streams map state needs to be updated.
- Any live-component rendered by a LiveView runs in the same process as its parent's LiveView --> the child live component can notify the parent live-view by sending a message to self() (which returns the PID of the parent LiveView). So this requires the parent (LiveView component) to have a handle_info() that handles the message passed by the child (live component), and the logic is the same state update logic.
- State passing from parent to child is default-handled.
- Parent state that is held by the
assigns
map gets merged with thesocket
assigns map that is used in the live component. This merging happens after the child is mounted. - The
update()
callback (unique to the lifecycle of a live component) happens between when the child'smount()
is called and when therender()
is called. The default implementation ofupdate(assigns, socket)
takes the assigns (passed from parent to the child within this assigns map), then merges (viaassign()
) the socket assigns with the assigns map passed from the parent and returns the socket assigns map like so:{:ok, socket}
.- we can define custom update() that modifies how the merging of parent state with child state happens.
- mount is only called once (when the live component is initially rendered), else it will lose track of its internal state. update gets fired multiple
- Parent state that is held by the
- PubSub!
- it's at the application-level. every application will have a PubSub service (which is a GenServer). Procs within that application can sub to topics and publish to topics. For subbed topics (the subbing happens
@mount-time
) , the handling of a broadcasted message (by the topic) gets done by a matchinghandle_info
callback that might update the liveview's state and hence a re-render. - the logic for subbing/pubbing can be put within the context module so that all the live-views (of the same module) can use that topic.
- since pubbing to a topic will make that message get broadcasted, we can use PubSub service as an intermediary to effect changes within its parent -- there's no real need for a child component send messages to the parent
- protips:
- since the topic names have to be unique, use a stringified module name for convention sake:
@topic inspect(__MODULE__)
- also use theme of the pubsub server via a module attribute to make things easy:
@pubsub LiveViewStudio.PubSub
to get things like:
Phoenix.PubSub.subscribe(@pubsub, @topic) Phoenix.PubSub.broadcast(@pubsub, @topic, message)
- since the topic names have to be unique, use a stringified module name for convention sake:
- Authenticating LiveViews
- auth code is kickstarted by Phoenix out of the box and will work. The generator is like so:
mix phx.gen.auth Accounts User users
where theAccounts
context module uses theUser
schema module and theusers
db table. - random reminders:
- LiveViews can't set cookies in the browser because LiveView events are pushed over a websocket connection and for security reasons, cookies can't be set over a websocket.
- that's why session controllers are bare Phoenix controllers and not live views themselves.
- how to set cookies in browser via a LiveView component:
- this is similar to how the generated auth code works, in that there 's a
UserRegistrationLive
which is a LiveView module and it relies onUserSessionController
to set cookies in the browser. - when form is saved with valid
needs further reading.phx-trigger-action={@trigger_submit} action={~p"/users/log_in?_action=registered"} method="post"
- this is similar to how the generated auth code works, in that there 's a
- Some nomenclature:
-
pipeline
: a named sequence of plugs that run to-to-bottom. -
plug
: a function that takes a Phx connection (a struct containing data for the current HTTP request) and returns an updated connection -- it's a transformer of the connection struct -- central to how Phoenix handles HTTP requests. -
live
: a scope sub-definition whereby the appropriate pipeline is invoked. And only after the connection struct gets passed from the start to the end of the pipeline, will the struct be passed to the LiveView component. Note that adding a keyword list for the scope'spipe_through
would mean that the connection struct traverses the pipelines in order of that keyword list. plugs can also be passed in and it would be equivalent to a pipeline.
-
- the router is where the flow of routes get defined:
- requests get plugged into live views via plugs (e.g.
:browser
and:require_authenticated_user
).
- LiveViews can't set cookies in the browser because LiveView events are pushed over a websocket connection and for security reasons, cookies can't be set over a websocket.
- so for auth, the flow goes like this:
- gotcha: , if after running the phx.gen.auth generator you don't see the
:fetch_current_user
plug in router.ex then you'll need to manually add it after the:put_secure_browser_headers
plugpipeline :browser do ... plug :put_secure_browser_headers plug :fetch_current_user end
-
Live sessions: a way to group routes that have the same authentication strategy (instead of adding the on_mount() callbacks on a per-module basis)
- Redirects b/w live views within the same
live_session
use the same websocket ==> avoids extra HTTP requests - Navigating between LiveViews in different live sessions always forces a full page reload, and establishes a new websocket connection. So there's always an initial HTTP request for the disconnected mount, which means all the plugs in the HTTP pipeline are run.
- Redirects b/w live views within the same
-
key takeaway: any auth checks must be done in 2 places:
- 1: when a user browses, directly via LiveView, via HTTP
- 2: in the liveview's
mount()
lifecycle to account for live re-directs (via theon_mount()
hook from within the LiveView module)
- Presence Tracking via the Phoenix.Presence Behaviour
- Presence tracker module can be generated.. uses the Presence behaviour, uses PubServer behind the scenes.
- there's a 1:1 correspondence b/w a LiveView process and a user, so tracking a process allows us to track a user.
- a user can have multiple presences (multiple browsers for example), but since the presence tracking can be aggregated (e.g. by user.id) then the presences can be aggregated into a collection (list).
- Rough steps to add presence tracking:
- use phx generator to generate presence tracker module
- add the presence tracker module to application supervisor
- Add login to make the presence tracker track current user's PID and metadata on_mount
- add in metadata changes based on events are required. Use the presence tracker to track meta changes
- use pubsub to send messages to others
TODO: revisit examples in the JS commands section.
- JS commands -- a way to add in client-side effects & prevent websocket based comms for simple client side interactions.
- rather than emitting event to the server, we call a JS function (via the JS module), where we pass in a dom element target.
- JS.show() isn't showing the dom element when the expression is evaluated when the template renders. Instead, the show function just returns a command. This command gets executed by LiveView's javascript when that event happens and hence the JS is executed on the client-side.
- JS commands returns a JS struct -- hence the js commands are composable into a pipe...
- JS commands are Dom-patch-aware ==> dom changes made by JS will preserve across re-renders
- It's not possible to just add JS event listeners as a workaround, because on Dom-patching (to get the re-render), the listener would have been lost.
- There's a JS.push() command that pushes an event to the server
- common client side functions supported (JS commands)
- JS Hooks -- allowing JS-libs to communicate with LiveView and vice-versa
- when using a hook, we can hook into the lifecycle of any html element and run custom javascript code. use the
phx-hook
binding for this. (NB: any element with a hook must have a unique id attribute). The JS hook gets bound to an element. - hook-definitions get passed in the app.js-definition itself, where the live socket is initialised -- hooks get added to the
hooks
options within the connectionparams
for that initialisation. - Reminders:
- The JS hooks rely on JS objects, so we'd have to do JSON.parse!() on it
- The LiveView module relies on Elixir Structs so we'd have to parse things accordingly.
- There are 2 main ways to pass information to/from the JS Hook:
- data attributes:
- from the live module, within the heex template -- e.g. json string passed via a html binding e.g.
data-*
- from the js hook, receiving: update the options(?)
- from the js hook, emit by using
pushEvent
. Note: thethis
in the screenshot is referring to the hook itself. - from the LiveView module, we'd just have to add an event handler (
handle_event
) for that event edited by the JS hook. - Instead of data attributes, ask the LiveView @ mount-time
- So from the JS to the Server, we could send events using 2 ways:
-
- using the
pushEvent
function from within the JS hook.
- using the
-
- in LiveView, running on the server using a
push_event
- in LiveView, running on the server using a
-
- NOTE: you'd have to contain the JS-hook containing dom element with a wrapper and mark it as ignored for the dom patching, else the hook will get lost during a Dom patch. Anytime you have custom JS controlling part of the DOM, you'll need to tell LiveView to ignore it when patching the DOM.
- Key Events
- key (keyboard) related event listeners automatically supported by LiveView
- TIPS:
- better to use key up events than key down, unless need more specific behaviour
- if it's a keydown event, should do some throttling
- remember to add default catchalls for the keypress handling
Other Resources / Reading List:
- figure out how to do the live websocket debugging that's in chapter 4 exercises.
- Syntax question: what's the meaning of this ellipsis here?
![image](https://private-user-images.githubusercontent.com/38996397/285656105-af5aded1-f58c-4d37-8827-c5fde869dd87.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzkzNDcxMzIsIm5iZiI6MTczOTM0NjgzMiwicGF0aCI6Ii8zODk5NjM5Ny8yODU2NTYxMDUtYWY1YWRlZDEtZjU4Yy00ZDM3LTg4MjctYzVmZGU4NjlkZDg3LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMTIlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjEyVDA3NTM1MlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWY1YzIwZDFlZDcyYzA5MzZlNjNlM2E0N2VmNjY2ZjViMGMxMDZjMWQwYTBiZWM5ZDQwNzY5ZWM3NDFkZjZmNDQmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.wGxRxzoLFBdPuv2_hxUoHlzJZA-B8B_SQGbMJ9QYEYg)
-
QQ: based on the function components, there's sort of a parent child relationship. Is it possible to access state values in the grandfather's assigns map from a child's context?
-
QQs: for live components, it's said that the parent and child are in the same process. Then what's a memory model that gives an idea of how this works? is it an elixir version of multiple threads within the same proc? oh or perhaps the terminology it's using is sockets since there's a socket assigns map...
-
QQ: Should revisit the hooks part with Bala -- just a sequential injection of code prior to the running of the process upon a particular event right e.g. mount event