Skip to content

Commit

Permalink
Add helper function to generate js to execute at root item
Browse files Browse the repository at this point in the history
  • Loading branch information
bluzky committed Nov 14, 2024
1 parent 9a86d60 commit 04d9cba
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 36 deletions.
1 change: 1 addition & 0 deletions lib/salad_ui.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ defmodule SaladUI do

defmacro __using__(_) do
quote do
import SaladUI.Helpers
import SaladUI.Alert
import SaladUI.Avatar
import SaladUI.Badge
Expand Down
18 changes: 9 additions & 9 deletions lib/salad_ui/collapsible.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,40 +22,40 @@ defmodule SaladUI.Collapsible do
required: true,
doc: "Id to identify collapsible component, collapsible_trigger uses this id to toggle content visibility"

attr :open, :boolean, default: false, doc: "Initial state of collapsible content"
attr :open, :boolean, default: true, doc: "Initial state of collapsible content"
attr :class, :string, default: nil
slot(:inner_block, required: true)

def collapsible(assigns) do
assigns =
assigns
|> assign(:builder, %{open: assigns[:open], id: assigns[:id]})
|> assign(:open, normalize_boolean(assigns[:open]))

~H"""
<div
phx-toggle-collapsible={toggle_collapsible(@builder)}
phx-toggle-collapsible={toggle_collapsible(@id)}
phx-mounted={@open && JS.exec("phx-toggle-collapsible", to: "##{@id}")}
class={classes(["inline-block relative", @class])}
class={classes(["inline-block relative collapsible-root", @class])}
id={@id}
>
<%= render_slot(@inner_block, @builder) %>
<%= render_slot(@inner_block) %>
</div>
"""
end

@doc """
Render trigger for collapsible component.
"""
attr :builder, :map, required: true, doc: "Builder instance for collapsible component"
attr(:class, :string, default: nil)
attr :as, :string, default: "button"
slot(:inner_block, required: true)

def collapsible_trigger(assigns) do
~H"""
<div phx-click={JS.exec("phx-toggle-collapsible", to: "#" <> @builder.id)} class={@class}>
<.dynamic_tag name={@as}
onclick={exec_closest("phx-toggle-collapsible", ".collapsible-root")} class={@class}>
<%= render_slot(@inner_block) %>
</div>
</.dynamic_tag>
"""
end

Expand Down Expand Up @@ -85,7 +85,7 @@ defmodule SaladUI.Collapsible do
@doc """
Show collapsible content.
"""
def toggle_collapsible(js \\ %JS{}, %{id: id} = _builder) do
def toggle_collapsible(js \\ %JS{}, id) do
JS.toggle(js,
to: "##{id} .collapsible-content",
in: {"ease-out duration-200", "opacity-0", "opacity-100"},
Expand Down
28 changes: 28 additions & 0 deletions lib/salad_ui/helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ defmodule SaladUI.Helpers do
end
end

@doc """
Normalize id to be used in HTML id attribute
It will replace all non-alphanumeric characters with `-` and downcase the string
"""
def id(id) do
id
|> String.replace(~r/[^a-zA-Z0-9]/, "-")
|> String.downcase()
end

@doc """
Variant helper for generating classes based on side and align
"""
Expand Down Expand Up @@ -197,6 +207,24 @@ defmodule SaladUI.Helpers do
Enum.map_join(css_map, "; ", fn {k, v} -> "#{k}: #{v}" end) <> ";"
end

@doc """
This function build js script to invoke JS stored in given attribute.
Similar to JS.exec/2 but this function target the nearest ancestor element.
## Examples
```heex
<button click={exec_closest("phx-hide-sheet", ".ancestor_class")}>
Close
</button>
```
"""
def exec_closest(attribute, ancestor_selector) do
"""
var el = this.closest("#{ancestor_selector}"); liveSocket.execJS(el, el.getAttribute("#{attribute}"));
"""
end

# Translate error message
# borrowed from https://github.com/petalframework/petal_components/blob/main/lib/petal_components/field.ex#L414
defp translate_error({msg, opts}) do
Expand Down
75 changes: 48 additions & 27 deletions lib/salad_ui/sidebar.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,28 @@ defmodule SaladUI.Sidebar do
@sidebar_width_mobile "18rem"
@sidebar_width_icon "3rem"


@doc """
@doc """
Render
"""
"""
attr(:class, :string, default: nil)
attr(:rest, :global)
slot(:inner_block, required: true)

def sidebar_provider(assigns) do
assigns = assign(assigns, %{sidebar_width: @sidebar_width, sidebar_width_icon: @sidebar_width_icon})

~H"""
<div
style={
style(%{
"--sidebar-width": @sidebar_width,
"--sidebar-width-icon": @sidebar_width_icon
})
}
style(%{
"--sidebar-width": @sidebar_width,
"--sidebar-width-icon": @sidebar_width_icon
})
}
class={
classes([
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
@class
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
@class
])
}
{@rest}
Expand All @@ -48,11 +48,12 @@ defmodule SaladUI.Sidebar do
Render
"""

attr :id, :string, required: true, doc: "The id of the sidebar, used for the trigger to identify the target sidebar"
attr :side, :string, values: ~w(left right), default: "left"
attr :variant, :string, values: ~w(sidebar floating inset), default: "sidebar"
attr :collapsible, :string, values: ~w(offcanvas icon none), default: "offcanvas"
attr :is_mobile, :boolean, default: false
attr :state, :string, default: "expanded"
attr :state, :string, values: ~w(expanded collapsed), default: "expanded"
attr(:class, :string, default: nil)
attr(:rest, :global)
slot(:inner_block, required: true)
Expand Down Expand Up @@ -98,11 +99,13 @@ defmodule SaladUI.Sidebar do
def sidebar(assigns) do
~H"""
<div
class="group peer hidden md:block text-sidebar-foreground"
class="group peer hidden md:block text-sidebar-foreground sidebar-root"
data-state={@state}
data-collapsible={(@state == "collapsed" && @collapsible) || ""}
data-collapsible={(@state == "collapsed" && @collapsible) || "none"}
data-variant={@variant}
data-side={@side}
id={@id}
phx-toggle-sidebar={toggle_sidebar({"none", @collapsible})}
>
<div class={
classes([
Expand Down Expand Up @@ -144,8 +147,9 @@ defmodule SaladUI.Sidebar do
Render
"""
attr(:class, :string, default: nil)
attr :target, :string, required: true, doc: "The id of the target sidebar"
attr(:rest, :global)
# slot(:inner_block, required: true)
slot(:inner_block, required: true)

def sidebar_trigger(assigns) do
~H"""
Expand All @@ -154,10 +158,11 @@ defmodule SaladUI.Sidebar do
variant="ghost"
size="icon"
class={classes(["h-7 w-7", @class])}
onclick="TODO togleSidebar()"
phx-click={JS.exec("phx-toggle-sidebar", to: "#" <> @target)}
{@rest}
>
Close <span class="sr-only">Toggle Sidebar</span>
<%= render_slot(@inner_block) %>
<span class="sr-only">Toggle Sidebar</span>
</.button>
"""
end
Expand All @@ -174,7 +179,7 @@ defmodule SaladUI.Sidebar do
data-sidebar="rail"
aria-label="Toggle Sidebar"
tab-index={-1}
onclick="toggleSidebar() //TODO"
onclick={exec_closest("phx-toggle-sidebar", ".sidebar-root")}
title="Toggle Sidebar"
class={
classes([
Expand All @@ -197,6 +202,7 @@ defmodule SaladUI.Sidebar do
"""
attr(:class, :string, default: nil)
attr(:rest, :global)
slot :inner_block, required: true

def sidebar_inset(assigns) do
~H"""
Expand All @@ -209,7 +215,9 @@ defmodule SaladUI.Sidebar do
])
}
{@rest}
/>
>
<%= render_slot(@inner_block) %>
</main>
"""
end

Expand Down Expand Up @@ -349,24 +357,28 @@ defmodule SaladUI.Sidebar do
TODO: class merge not work well here
"""
attr(:class, :string, default: nil)
attr :as, :string, default: "div"
attr(:rest, :global)
slot(:inner_block, required: true)

def sidebar_group_label(assigns) do
~H"""
<div
<.dynamic_tag name={@as}
data-sidebar="group-label"
class={
Enum.join([
"duration-200 flex h-8 shrink-0 items-center rounded-md px-2 font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0 text-xs",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
@class
], " ")
Enum.join(
[
"duration-200 flex h-8 shrink-0 items-center rounded-md px-2 font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0 text-xs",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
@class
],
" "
)
}
{@rest}
>
<%= render_slot(@inner_block) %>
</div>
</.dynamic_tag>
"""
end

Expand Down Expand Up @@ -406,10 +418,10 @@ defmodule SaladUI.Sidebar do
def sidebar_group_content(assigns) do
~H"""
<div
data-sidebar="menu"
data-sidebar="group-content"
class={
classes([
"flex w-full min-w-0 flex-col gap-1",
"w-full text-sm",
@class
])
}
Expand Down Expand Up @@ -707,4 +719,13 @@ defmodule SaladUI.Sidebar do
" " <>
variant_class(@variant_config, input)
end

@doc """
Toggle sidebar between collapsed and expanded state.
"""
def toggle_sidebar({state1, state2} = _collapsible_states) do
{"data-state", "collapsed", "expanded"}
|> JS.toggle_attribute()
|> JS.toggle_attribute({"data-collapsible", state1, state2})
end
end

0 comments on commit 04d9cba

Please sign in to comment.