Packages
phoenix_kit
1.7.199
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
221 Versions
Jump to
Current section
221 Versions
Compare versions
6
files changed
+92
additions
-26
deletions
unknownCHANGELOG.md
File is too large to be displayed (100 KB limit).
| @@ -1,6 +1,6 @@ | |
| 1 1 | {<<"links">>,[{<<"GitHub">>,<<"https://github.com/BeamLabEU/phoenix_kit">>}]}. |
| 2 2 | {<<"name">>,<<"phoenix_kit">>}. |
| 3 | - {<<"version">>,<<"1.7.198">>}. |
| 3 | + {<<"version">>,<<"1.7.199">>}. |
| 4 4 | {<<"description">>, |
| 5 5 | <<65,32,102,111,117,110,100,97,116,105,111,110,32,102,111,114,32,98,117,105, |
| 6 6 | 108,100,105,110,103,32,69,108,105,120,105,114,32,80,104,111,101,110,105, |
| @@ -69,12 +69,18 @@ defmodule PhoenixKitWeb.Components.Core.NavTabs do | |
| 69 69 | </span> |
| 70 70 | </.link> |
| 71 71 | <% else %> |
| 72 | + <%!-- While the switch round-trips, LiveView tags the clicked tab |
| 73 | + with phx-click-loading — pulse it so a switch whose content |
| 74 | + needs server work still gives instant feedback. --%> |
| 72 75 | <button |
| 73 76 | type="button" |
| 74 77 | role="tab" |
| 75 78 | phx-click={@on_change} |
| 76 79 | phx-value-tab={tab.id} |
| 77 | - class={["tab gap-2", tab.id == @active_tab && "tab-active"]} |
| 80 | + class={[ |
| 81 | + "tab gap-2 [&.phx-click-loading]:animate-pulse", |
| 82 | + tab.id == @active_tab && "tab-active" |
| 83 | + ]} |
| 78 84 | > |
| 79 85 | <.icon :if={Map.has_key?(tab, :icon)} name={tab.icon} class="w-4 h-4" /> |
| 80 86 | {tab.label} |
| @@ -74,6 +74,14 @@ defmodule PhoenixKitWeb.Components.Core.SearchPicker do | |
| 74 74 | The selector must actually match an element or every push is dropped |
| 75 75 | with a console error. |
| 76 76 | - `mode` — `"multi"` (default) or `"single"` (see moduledoc) |
| 77 | + - `search_on_focus` — when `true`, clicking/focusing the EMPTY input |
| 78 | + already opens the dropdown in browse mode (the hook searches with an |
| 79 | + empty query, so the server should answer it with a first page — the |
| 80 | + workspace picker rule: offer options before any typing). Off by |
| 81 | + default; without it the dropdown only opens while typing, which also |
| 82 | + means a multi-mode picker won't reopen after a pick clears the input. |
| 83 | + (Previously only reachable as a raw `data-search-on-focus` rest attr — |
| 84 | + still honored for existing call sites.) |
| 77 85 | - `direction` — `"down"` (default) or `"up"`. Use `"up"` when the input |
| 78 86 | sits near the BOTTOM of a scrollable container (e.g. the last field |
| 79 87 | of a modal): a downward dropdown there extends the container's |
| @@ -90,6 +98,7 @@ defmodule PhoenixKitWeb.Components.Core.SearchPicker do | |
| 90 98 | attr :dropdown_id, :string, required: true |
| 91 99 | attr :target, :any, default: nil |
| 92 100 | attr :mode, :string, default: "multi", values: ["multi", "single"] |
| 101 | + attr :search_on_focus, :boolean, default: false |
| 93 102 | attr :direction, :string, default: "down", values: ["down", "up"] |
| 94 103 | attr :name, :string, default: nil |
| 95 104 | attr :value, :string, default: nil |
| @@ -121,6 +130,7 @@ defmodule PhoenixKitWeb.Components.Core.SearchPicker do | |
| 121 130 | data-target={@target} |
| 122 131 | data-dropdown={@dropdown_id} |
| 123 132 | data-mode={@mode} |
| 133 | + data-search-on-focus={@search_on_focus || nil} |
| 124 134 | data-search-event={@search_event} |
| 125 135 | data-results-event={@results_event} |
| 126 136 | data-pick-event={@pick_event} |
| @@ -29,9 +29,9 @@ defmodule PhoenixKitWeb.Components.MultilangForm do | |
| 29 29 | |
| 30 30 | ### Events |
| 31 31 | |
| 32 | - def handle_event("switch_language", %{"lang" => lang_code}, socket) do |
| 33 | - {:noreply, handle_switch_language(socket, lang_code)} |
| 34 | - end |
| 32 | + `mount_multilang/2` auto-handles the `"switch_language"` event, so you do |
| 33 | + NOT need a `handle_event("switch_language", …)` clause — you only wire your |
| 34 | + own form events: |
| 35 35 | |
| 36 36 | def handle_event("validate", %{"record" => params}, socket) do |
| 37 37 | params = merge_translatable_params(params, socket, ["name", "description"], |
| @@ -40,6 +40,18 @@ defmodule PhoenixKitWeb.Components.MultilangForm do | |
| 40 40 | {:noreply, assign(socket, :changeset, changeset)} |
| 41 41 | end |
| 42 42 | |
| 43 | + ## Storage caveat — the helper OWNS the `data` column |
| 44 | + |
| 45 | + `merge_translatable_params/4` (and `put_language_data/3`) store translations |
| 46 | + by RESTRUCTURING the schema's `data` JSONB into a multilang shape |
| 47 | + (`%{"_primary_language" => "en", "en" => %{…}, "et" => %{…}}`). On a schema |
| 48 | + whose `data` column already holds unrelated keys (e.g. per-record settings), |
| 49 | + this MOVES those keys into the primary-language sub-map and every top-level |
| 50 | + reader breaks. In that case do NOT point `merge_translatable_params` at |
| 51 | + `data`: keep translations in an isolated sub-key and use `translatable_field` |
| 52 | + in its settings-translations mode (`secondary_name` + `lang_data_key`) with |
| 53 | + your own small merge. See the `translatable_field` docs. |
| 54 | + |
| 43 55 | ### Template — whole-form translation |
| 44 56 | |
| 45 57 | <.multilang_tabs |
| @@ -105,10 +117,25 @@ defmodule PhoenixKitWeb.Components.MultilangForm do | |
| 105 117 | Adds: `:multilang_enabled`, `:primary_language`, `:current_lang`, |
| 106 118 | `:language_tabs`, `:show_multilang_tabs`, `:switching_lang` (no-op |
| 107 119 | compat assign — kept because consumer templates pass it through to |
| 108 | - the wrapper). Also attaches an internal `:handle_info` hook that |
| 109 | - receives the debounced language-switch timer message. |
| 120 | + the wrapper). |
| 121 | + |
| 122 | + Also attaches two internal LiveView hooks so consumers don't have to |
| 123 | + wire the language-switching protocol by hand: |
| 124 | + |
| 125 | + * a `:handle_info` hook for the debounced language-switch timer, and |
| 126 | + * a `:handle_event` hook for the `"switch_language"` event that |
| 127 | + `<.multilang_tabs>` pushes — it calls `handle_switch_language/2` |
| 128 | + and halts, so **you no longer need to add** |
| 129 | + `def handle_event("switch_language", …)` yourself. Forgetting that |
| 130 | + clause used to crash the LiveView on the first tab click. |
| 131 | + |
| 132 | + ## Options |
| 133 | + |
| 134 | + * `:auto_switch_language` — when `false`, skips the `"switch_language"` |
| 135 | + event hook so you can handle the event yourself (e.g. to switch |
| 136 | + language immediately without the debounce). Default `true`. |
| 110 137 | """ |
| 111 | - def mount_multilang(socket) do |
| 138 | + def mount_multilang(socket, opts \\ []) do |
| 112 139 | multilang_enabled = multilang_enabled?() |
| 113 140 | primary_language = if multilang_enabled, do: safe_primary_language(), else: nil |
| 114 141 | |
| @@ -126,21 +153,27 @@ defmodule PhoenixKitWeb.Components.MultilangForm do | |
| 126 153 | # The wrapper no longer reads it — visibility is client-side JS. |
| 127 154 | switching_lang: false |
| 128 155 | ) |
| 129 | - |> attach_multilang_hook() |
| 156 | + |> attach_multilang_hooks(opts) |
| 130 157 | end |
| 131 158 | |
| 132 | - # Attaches a `:handle_info` hook that intercepts the internal |
| 133 | - # `{:__multilang_apply_lang__, lang}` message. The hook lives on the |
| 134 | - # consumer's socket but is invisible to them — no `handle_info/2` |
| 135 | - # clause needed. Returning `{:halt, socket}` prevents the message |
| 136 | - # from reaching the consumer's own handle_info (and suppresses the |
| 137 | - # "unhandled message" warning). Other messages pass through with |
| 138 | - # `{:cont, socket}`. |
| 139 | - # |
| 140 | - # `attach_hook/4` is idempotent-by-name within a process — re-running |
| 141 | - # `mount_multilang/1` (e.g. across reconnects) with the same hook id |
| 142 | - # simply replaces the prior callback, so we don't need a guard. |
| 143 | - defp attach_multilang_hook(socket) do |
| 159 | + # Attaches the internal hooks. Both are idempotent-by-name within a |
| 160 | + # process — re-running `mount_multilang/2` (e.g. across reconnects) with |
| 161 | + # the same hook ids simply replaces the prior callbacks, so no guard is |
| 162 | + # needed. `attach_hook/4` only works on LiveView sockets (not |
| 163 | + # LiveComponents), so each attach is guarded — a LiveComponent consumer |
| 164 | + # would need to wire the handlers itself. |
| 165 | + defp attach_multilang_hooks(socket, opts) do |
| 166 | + socket = attach_apply_lang_hook(socket) |
| 167 | + |
| 168 | + if Keyword.get(opts, :auto_switch_language, true), |
| 169 | + do: attach_switch_language_hook(socket), |
| 170 | + else: socket |
| 171 | + end |
| 172 | + |
| 173 | + # Intercepts the debounced `{:__multilang_apply_lang__, lang}` message so |
| 174 | + # the consumer needs no `handle_info/2` clause. Other messages pass |
| 175 | + # through with `{:cont, socket}`. |
| 176 | + defp attach_apply_lang_hook(socket) do |
| 144 177 | Phoenix.LiveView.attach_hook( |
| 145 178 | socket, |
| 146 179 | :__phoenix_kit_multilang_apply_lang__, |
| @@ -154,10 +187,27 @@ defmodule PhoenixKitWeb.Components.MultilangForm do | |
| 154 187 | end |
| 155 188 | ) |
| 156 189 | rescue |
| 157 | - # `attach_hook/4` only works on LiveView sockets, not LiveComponent |
| 158 | - # sockets. Multilang consumers are all `Phoenix.LiveView` today, but |
| 159 | - # if someone wires it into a component in the future, fall back |
| 160 | - # silently — they'll need to add the `handle_info` themselves. |
| 190 | + ArgumentError -> socket |
| 191 | + end |
| 192 | + |
| 193 | + # Intercepts the `"switch_language"` event that `<.multilang_tabs>` |
| 194 | + # pushes, so the consumer needs no `handle_event/3` clause. Halting stops |
| 195 | + # propagation to the consumer's own callbacks; every other event passes |
| 196 | + # through with `{:cont, socket}`. |
| 197 | + defp attach_switch_language_hook(socket) do |
| 198 | + Phoenix.LiveView.attach_hook( |
| 199 | + socket, |
| 200 | + :__phoenix_kit_multilang_switch_language__, |
| 201 | + :handle_event, |
| 202 | + fn |
| 203 | + "switch_language", %{"lang" => lang_code}, socket -> |
| 204 | + {:halt, handle_switch_language(socket, lang_code)} |
| 205 | + |
| 206 | + _event, _params, socket -> |
| 207 | + {:cont, socket} |
| 208 | + end |
| 209 | + ) |
| 210 | + rescue |
| 161 211 | ArgumentError -> socket |
| 162 212 | end |
Loading more files…