Skip to content

Commit

Permalink
Merge pull request #9 from cblavier/better-extend-class
Browse files Browse the repository at this point in the history
Better extend_class/3
  • Loading branch information
cblavier authored Jul 11, 2022
2 parents 6884f14 + 670f8af commit 9f7dec7
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 24 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# 1.1.0

- `extend_class/3` behavior has been updated and will soon no longer replace default css
classes based on their prefix (this behavior is still working but deprecated). To switch to
the new behavior and suppress warning messages, pass the `prefix_replace: false` option and
use the new `!` based syntax to explicitly remove default CSS classes. (ex: `!border-* border-red-500`)

# 1.0.3

- another fix on `validate_required_attributes` not handling well `false` values
Expand All @@ -9,7 +16,7 @@

# 1.0.1

- fixed a nasty bug where `extend_class/1` was not updating assigns `__changed__` key
- fixed a nasty bug where `extend_class/3` was not updating assigns `__changed__` key

# 1.0.0

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ Add the following to your `mix.exs`.
```elixir
def deps do
[
{:phx_component_helpers, "~> 1.0.0"},
{:phx_component_helpers, "~> 1.1.0"},
{:jason, "~> 1.0"} # only required if you want to use json encoding options
]
end
Expand Down
26 changes: 22 additions & 4 deletions lib/phx_component_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,23 @@ defmodule PhxComponentHelpers do
end

@doc ~S"""
Set assigns with class attributes.
Provides default css classes and extend them from assigns.
The class attribute will take provided `default_classes` as a default value and will
be extended, on a class-by-class basis, by your assigns.
extend them, on a class-by-class basis, with your assigns.
Any CSS class provided in the assigns (by default under the `:class` attribute) will be
added to the `default_classes`. You can also remove classes from the `default_classes`
by using the `!` prefix.
- `"!bg-gray-400 bg-blue-200"` will remove `"bg-gray-400"` from default component classes
and replace it with `"bg-blue-200"`
- `"!block flex"` will replace `"block"` by `"flex"` layout in your component classes.
- `"!border* border-2 border-red-400"` will replace all border classes by
`"border-2 border-red-400"`.
> #### Deprecation notice {: .warning}
>
> Following behavior will be deprecated from 1.2, no implicit class replacement will be performed.
This function will identify default classes to be replaced by assigns on a prefix basis:
- "bg-gray-200" will be overwritten by "bg-blue-500" because they share the same "bg-" prefix
Expand All @@ -157,6 +170,8 @@ defmodule PhxComponentHelpers do
## Options
* `:attribute` - read & write css classes from & into this key
* `:prefix_replace` - when set to false, disable the prefix based class replacement.
From 1.2, `prefix_replace: false` will be the default.
## Example
```
Expand All @@ -171,14 +186,17 @@ defmodule PhxComponentHelpers do
`assigns` now contains `@heex_class` and `@heex_wrapper_class`.
If your input assigns were `%{class: "mt-2", wrapper_class: "divide-none"}` then:
If your input assigns were `%{class: "!mt-8 mt-2", wrapper_class: "!divide* divide-none"}` then:
* `@heex_class` would contain `"bg-blue-500 mt-2"`
* `@heex_wrapper_class` would contain `"py-4 px-2 divide-none"`
"""
def extend_class(assigns, default_classes, opts \\ []) do
class_attribute_name = Keyword.get(opts, :attribute, :class)
prefix_replace = Keyword.get(opts, :prefix_replace, true)
warn_for_deprecated_prefix_replace(prefix_replace)

new_class = do_css_extend_class(assigns, default_classes, class_attribute_name)
new_class =
do_css_extend_class(assigns, default_classes, class_attribute_name, prefix_replace)

assigns
|> assign(:"#{class_attribute_name}", new_class)
Expand Down
57 changes: 48 additions & 9 deletions lib/phx_component_helpers/css.ex
Original file line number Diff line number Diff line change
@@ -1,19 +1,37 @@
defmodule PhxComponentHelpers.CSS do
@moduledoc false

require Logger

@doc false
def do_css_extend_class(assigns, default_classes, class_attribute_name, prefix_replace \\ false)

@doc false
def do_css_extend_class(assigns, default_classes, class_attribute_name) when is_map(assigns) do
def do_css_extend_class(assigns, default_classes, class_attribute_name, prefix_replace)
when is_map(assigns) do
input_class = Map.get(assigns, class_attribute_name) || ""
do_extend_class(assigns, input_class, default_classes)
do_extend_class(assigns, input_class, default_classes, prefix_replace)
end

@doc false
def do_css_extend_class(options, default_classes, class_attribute_name) when is_list(options) do
def do_css_extend_class(options, default_classes, class_attribute_name, prefix_replace)
when is_list(options) do
input_class = Keyword.get(options, class_attribute_name) || ""
do_extend_class(options, input_class, default_classes)
do_extend_class(options, input_class, default_classes, prefix_replace)
end

@doc false
def warn_for_deprecated_prefix_replace(false), do: :ok

@doc false
def warn_for_deprecated_prefix_replace(true) do
Logger.warn("""
Prefix based class replacement in extend_class/3 will soon be deprecated.
Use prefix_replace: false to disable this behavior and suppress this warning message.
""")
end

defp do_extend_class(assigns_or_options, input_class, default_classes) do
defp do_extend_class(assigns_or_options, input_class, default_classes, prefix_replace) do
default_classes =
case default_classes do
_ when is_function(default_classes) -> default_classes.(assigns_or_options)
Expand All @@ -22,13 +40,12 @@ defmodule PhxComponentHelpers.CSS do

default_classes = String.split(default_classes, [" ", "\n"], trim: true)
extend_classes = String.split(input_class, [" ", "\n"], trim: true)
target_classes = Enum.reject(extend_classes, &String.starts_with?(&1, "!"))

classes =
for class <- default_classes, reduce: extend_classes do
for class <- Enum.reverse(default_classes), reduce: target_classes do
acc ->
[class_prefix | _] = String.split(class, "-")

if Enum.any?(extend_classes, &String.starts_with?(&1, "#{class_prefix}-")) do
if class_should_be_removed?(class, extend_classes, prefix_replace) do
acc
else
[class | acc]
Expand All @@ -37,4 +54,26 @@ defmodule PhxComponentHelpers.CSS do

Enum.join(classes, " ")
end

defp class_should_be_removed?(class, extend_classes, prefix_replace) do
Enum.any?(extend_classes, fn
"!" <> ^class ->
true

"!" <> pattern ->
if String.ends_with?(pattern, "*") do
pattern = String.slice(pattern, 0..-2)
String.starts_with?(class, pattern)
else
false
end

extend_class when prefix_replace ->
[class_prefix | _] = String.split(class, "-")
String.starts_with?(extend_class, "#{class_prefix}-")

_ ->
false
end)
end
end
4 changes: 2 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule PhxComponentHelpers.MixProject do
def project do
[
app: :phx_component_helpers,
version: "1.0.3",
version: "1.1.0",
elixir: "~> 1.7",
start_permanent: Mix.env() == :prod,
deps: deps(),
Expand Down Expand Up @@ -38,7 +38,7 @@ defmodule PhxComponentHelpers.MixProject do
[
{:credo, "~> 1.5", only: [:dev, :test], runtime: false},
{:ex_doc, "~> 0.24", only: :dev, runtime: false},
{:excoveralls, "~> 0.10", only: :test},
{:excoveralls, "~> 0.14", only: :test},
{:jason, "~> 1.0", optional: true},
{:mix_test_watch, "~> 1.0", only: :dev, runtime: false},
{:phoenix_html, ">= 3.0.0"},
Expand Down
105 changes: 103 additions & 2 deletions test/phx_component_helpers_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ defmodule PhxComponentHelpersTest do

alias Phoenix.HTML.Form

import ExUnit.CaptureLog

defp assigns(input), do: Map.merge(input, %{__changed__: %{}})

describe "set_attributes" do
Expand All @@ -20,6 +22,12 @@ defmodule PhxComponentHelpersTest do
assert %{heex_foo: [foo: "foo"]} = Helpers.set_attributes(assigns, [:foo])
end

test "with liveview assigns" do
assigns = assigns(%{__changed__: [], foo: "foo", bar: "bar"})

assert %{heex_foo: [foo: "foo"]} = Helpers.set_attributes(assigns, [:foo])
end

test "absent assigns are set as empty attributes" do
assigns = assigns(%{foo: "foo", bar: "bar"})

Expand Down Expand Up @@ -277,16 +285,18 @@ defmodule PhxComponentHelpersTest do
end

describe "extend_class" do
@tag :capture_log
test "without class keeps the default class attribute" do
assigns = %{c: "foo", bar: "bar"}
new_assigns = Helpers.extend_class(assigns, "bg-blue-500 mt-8")

assert new_assigns ==
assigns
|> Map.put(:class, "mt-8 bg-blue-500")
|> Map.put(:heex_class, class: "mt-8 bg-blue-500")
|> Map.put(:class, "bg-blue-500 mt-8")
|> Map.put(:heex_class, class: "bg-blue-500 mt-8")
end

@tag :capture_log
test "with class extends the default class attribute" do
assigns = %{class: "mt-2"}
new_assigns = Helpers.extend_class(assigns, "bg-blue-500 mt-8 ")
Expand All @@ -298,6 +308,7 @@ defmodule PhxComponentHelpersTest do
}
end

@tag :capture_log
test "can extend other class attribute" do
assigns = %{wrapper_class: "mt-2"}
new_assigns = Helpers.extend_class(assigns, "bg-blue-500 mt-8 ", attribute: :wrapper_class)
Expand All @@ -309,6 +320,7 @@ defmodule PhxComponentHelpersTest do
}
end

@tag :capture_log
test "default classes can be a function" do
assigns = %{class: "mt-2", active: true}

Expand All @@ -324,6 +336,7 @@ defmodule PhxComponentHelpersTest do
|> Map.put(:heex_class, class: "bg-blue-500 mt-2")
end

@tag :capture_log
test "does not extend with error_class when a form field is not faulty" do
assigns = %{
class: "mt-2",
Expand All @@ -345,6 +358,94 @@ defmodule PhxComponentHelpersTest do
class: "bg-blue-500 mt-2"
)
end

@warning_msg "Prefix based class replacement in extend_class/3 will soon be deprecated."
test "without prefix_replace: false produces a warning log message" do
{_result, log} =
with_log([level: :warning], fn ->
Helpers.extend_class(%{class: "mt-2"}, "bg-blue-500 mt-8")
end)

assert log =~ @warning_msg

{_result, log} =
with_log([level: :warning], fn ->
Helpers.extend_class(%{class: "mt-2"}, "bg-blue-500 mt-8", prefix_replace: true)
end)

assert log =~ @warning_msg
end

test "with prefix_replace: false does not produce a warning log message" do
{_result, log} =
with_log([level: :warning], fn ->
Helpers.extend_class(%{class: "mt-2"}, "bg-blue-500 mt-8", prefix_replace: false)
end)

refute log =~ @warning_msg
end

test "with prefix_replace: false does not replace existing class" do
assigns = %{class: "mt-2"}
new_assigns = Helpers.extend_class(assigns, "bg-blue-500 mt-8", prefix_replace: false)

assert new_assigns ==
%{
class: "bg-blue-500 mt-8 mt-2",
heex_class: [class: "bg-blue-500 mt-8 mt-2"]
}
end

test "removes classes prefixed by !" do
assigns = %{class: "!mt-8 mt-2"}
new_assigns = Helpers.extend_class(assigns, "bg-blue-500 mt-8", prefix_replace: false)

assert new_assigns ==
%{
class: "bg-blue-500 mt-2",
heex_class: [class: "bg-blue-500 mt-2"]
}
end

test "removes classes prefixed by ! with * patterns" do
assigns = %{class: "!border* mt-2"}

new_assigns =
Helpers.extend_class(assigns, "border-2 border-gray-400", prefix_replace: false)

assert new_assigns ==
%{
class: "mt-2",
heex_class: [class: "mt-2"]
}
end

@tag :capture_log
test "mix prefix based replacement ! * patterns" do
assigns = %{class: "!border* mt-2"}

new_assigns =
Helpers.extend_class(assigns, "border border-2 border-gray-400 mt-4", prefix_replace: true)

assert new_assigns ==
%{
class: "mt-2",
heex_class: [class: "mt-2"]
}
end

test "removes everything with !* " do
assigns = %{class: "!* mt-2"}

new_assigns =
Helpers.extend_class(assigns, "border-2 border-gray-400", prefix_replace: false)

assert new_assigns ==
%{
class: "mt-2",
heex_class: [class: "mt-2"]
}
end
end

describe "set_form_attributes" do
Expand Down
10 changes: 5 additions & 5 deletions test/phx_view_helpers_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@ defmodule PhxViewHelpersTest do
test "without class keeps the default class attribute" do
opts = [c: "foo", bar: "bar"]
new_opts = Helpers.extend_form_class(opts, "bg-blue-500 mt-8")
assert new_opts == Keyword.put(opts, :class, "mt-8 bg-blue-500")
assert new_opts == Keyword.put(opts, :class, "bg-blue-500 mt-8")
end

test "with class extends the default class attribute" do
opts = [class: "mt-2"]
new_opts = Helpers.extend_form_class(opts, "bg-blue-500 mt-8 ")
opts = [class: "!mt-8 mt-2"]
new_opts = Helpers.extend_form_class(opts, "bg-blue-500 mt-8")
assert new_opts == Keyword.put(opts, :class, "bg-blue-500 mt-2")
end

test "with class extends the default class attribute as map" do
assigns = %{class: "mt-2"}
new_assigns = Helpers.extend_form_class(assigns, "bg-blue-500 mt-8 ")
assigns = %{class: "!mt-8 mt-2"}
new_assigns = Helpers.extend_form_class(assigns, "bg-blue-500 mt-8")
assert new_assigns == Map.put(assigns, :class, "bg-blue-500 mt-2")
end
end
Expand Down

0 comments on commit 9f7dec7

Please sign in to comment.