Surface Boxicon - a component library that wraps the boxicons library (My first library :))

Hey everyone!

For a personal project I’m working, I need to use boxicons in surface templates. Since the package didn’t exist, I decided to take the matter into my own hands. The results are here:

https://github.com/fceruti/surface-boxicon

I feel like a leveled up or something.

PS: The library is very small, so I’d very happy if you could check it out and point out things you would have don’t better. Anything. Even style pointers are most welcomed.

11 Likes

Very nice and congrats! Just a small thing but it sticks out immediately: could you give the background of the search area a softer white/grey colour so the contrast isn’t as overwhelming? :+1:

I’m sorry, I’m not following you. What search area are you talking about?

Sorry, I thought you were also behind the boxicons.com page. I meant the search area on that page.

1 Like

Hi @fceruti. Nice work!

One thing to keep in mind is that defining modules is more expensive than defining multiple function clauses so since all components have the same props, I believe you would have a more consistent API, along with faster compilation, if you define a single component with name and maybe also a type prop to differentiate each icon. Something like:

def render(%{name: "video-plus", type="solid"} = assigns) do
  ~F[<svg ... width={@size} height={@size} class={@class} .../></svg>]
end

def render(%{name: "video-plus", type="regular"} = assigns) do
  ~F[<svg ... width={@size} height={@size} class={@class} .../></svg>]
end

def render(%{name: "bell", type="solid"} = assigns) do
  ~F[<svg ... width={@size} height={@size} class={@class} .../></svg>]
end

def render(%{name: "bell", type="regular"} = assigns) do
  ~F[<svg ... width={@size} height={@size} class={@class} .../></svg>]
end
...

Then you could use it like:

<BoxIcon name="bell" type="solid"/>
6 Likes

That’s great! Actually compilation time was something bothering me. Thanks!

Turns out, with this approach, compilation times went bananas! I’m compiling that single module in about 60 seconds, while it used to take around 10-15 seconds to compile all the 1500 modules.

I do appreciate how the API is much better now thou.

Anyone got any ideas on how to improve things? The file in question is this: surface-boxicon/lib/boxicon.ex at main · fceruti/surface-boxicon · GitHub

Large numbers of function clauses does eventually create exponentially slower compilation times. I have a similar issue in ex_CLDR and I know the gettext team went with splitting into modules as an option for the same reason.

I haven’t dug far enough to know where performance gets pathologically bad but ‘hundreds’ of clauses seems to exhibit this behaviour.

Is it because the 1500 modules are compiled in parallel, whereas the one giant module can only be compiled in one thread?
Can you compare the CPU time. I am curious whether too many function clauses have non linear performance penalty, on top of the lack of parallelization?

I’m not sure, but it makes perfect sense, since I saw a ~6X increase in time and I’m running on a 6 core machine.

How can I get the CPU time used in compilation?

use time(1). such as:

derek@roastidious:~/projects/roastidious$ time mix compile
Compiling 39 files (.ex)
Generated roastidious app

real	0m2.150s
user	0m3.608s
sys	0m1.017s

real is the wall time, user+sys is the cpu time spent; please note cpu time is usually larger than wall time.

1 Like

OK, here are the results (I little different than what I experienced before for some reason)

1 module version:

❯ time mix compile
Compiling 2 files (.ex)
Compiling lib/boxicon.ex (it's taking more than 10s)
Generated surface_boxicon app
mix compile  27.49s user 2.24s system 106% cpu 27.818 total

1527 modules version

❯ time mix compile
Compiling 1527 files (.ex)
Generated surface_boxicon app
mix compile  71.68s user 8.96s system 437% cpu 18.427 total

Looks great! Maybe you could make the included icons and types configurable. Then you can reduce the number of function heads to generate at compile time. Most projects would only be using a small subset.

Edit: didn’t look at the implementation first. I was assuming you were using a macro to generate the function heads at compile time.

This sounds neat! Can you point me to a project that does this? I’m still a n00b and talking about macros goes over my head at the moment.

No more of noob than me. I can’t think of a project off the top of my head. This (nice and short) book is a great way to learn more about macros though: Metaprogramming Elixir: Write Less Code, Get More Done (and Have Fun!) by Chris McCord

Hopefully someone with a bit more experience can point you in the right direction, or clarify if this is a good use case for macros at all!

You wouldn’t necessarily need to implement any macros, but meta programming would still be useful. Something like:

defmodule Boxicon do
  
  @doc "Type of the icon"
  prop type, :string, values!: ["solid", "regular", "logos"]

  @doc "Name of the icon"
  prop name, :string, required: true

  @doc "Width & height of the icon"
  prop size, :integer, default: 24

  @doc "CSS classes for the wrapping svg element"
  prop class, :string, default: "icon"

  @icons "populate icons from static data at compile time"

  for {type, name, path} <- @icons do
    def render(%{name: unquote(name), type: unquote(type)} = assigns) do
      ~F[<svg xmlns="http://www.w3.org/2000/svg" width={"#{@size}"} height={"#{@size}"} class={"#{@class}"} viewBox="0 0 24 24"><path d=unquote(path)/></svg>]
    end
  end
end

Happy to help with how to structure the data to populate @icons at compile time if you need.

3 Likes

@fceruti here is a fork where I was playing with something along the lines of what @kip suggested. It’s definitely not complete (or probably the most idiomatic Elixir) but it might be helpful. GitHub - joshdcuneo/surface-boxicon: Surface component library that wraps the amazing boxicons library.

The compile times were a lot faster when I just specified the “logos” type to be included so if you allowed configuring the included icons it should be pretty quick.

There seem to be categories as well as different cuts for boxicons, so there are already some bags to split into different modules by.

To build on top of @Kip’s, you can share the common parts and leave only the different bits for meta programming:

  def render(assigns) do
    ~F[<svg xmlns="http://www.w3.org/2000/svg" width={"#{@size}"} height={"#{@size}"} class={"#{@class}"} viewBox="0 0 24 24"><path d={path(@name, @type)} /></svg>]
  end

  for {type, name, path} <- @icons do
    defp path(unquote(name), unquote(type)), do: unquote(path)
  end
1 Like

I haven’t looked too deep but are there any Surface specific things in this library? I wonder if it would be more reusable to use Phoenix’s component & functional component API? (Reusable across non-Surface LiveView apps and normal non-LiveView Phoenix apps)

I’m thinking something like:

# usage
<%= component(&BoxIcon.bell/1, []) %>

# definition
defmodule BoxIcon do
  def bell(assigns) do
    ~H[<svg>...</svg>]
  end
end

Here’s some resource on the upcoming changes Surface and Phoenix LiveView - what comes next? - Dashbit Blog (it links to different PRs there as well)

Just a thought! No clue if it’d work at all