What's new in Surface v0.8?

by Marlus Saraiva ・

surface releases CSS contexts tailwind catalogue

Surface v0.8 is out with new features, major improvements and fixes. The full changelog can be found here.

A migration guide is also provided with more information about running mix surface.convert to help you migrate your code from v0.7 to v0.8.

Now, let's take a look at some of the new features.

Scoped CSS styles

When defining a component or LiveView, you can now declare its CSS styles directly in the template using <style> or in a colocated .css file. The Surface compiler will treat those styles as scoped styles so any CSS declaration will apply only to the related component.

Some of the benefits are:

  • Better code organization as each component style can be defined alongside its component
  • Avoid conflicts between components rules as each declaration is scoped per component
  • Prevent CSS rules from the parent components from leaking into children elements or other components.
  • Support injecting elixir expressions into the CSS declarations using s-bind(), allowing users to apply dynamic values to CSS properties based on the components' assigns. One great thing about this approach is that it keeps the CSS syntax valid so it doesn't interfere with tools that depend on valid CSS syntax like editor highlighters, linters, etc.
  • Zero configuration in app.css or any other file when importing new components, including components from dependencies. The compiler will automatically collect and process all component-related styles seamlessly.

For more info on the topic, see the Scoped CSS page.

New context API

The context API has been extended and fully redesigned to improve ergonomics and make it more friendly for diff tracking. The compiler is able now to detect many cases where the use of contexts might impact performance and suggest one or more alternative approaches to achieve the same goal.

Along with the optimizations, we introduced a set of functions that allows users to manipulate the context inside lifecycle callbacks (Liveview and LiveComponent) or in render/1 (function components). The main functions introduced are Context.put/3 and Context.get/3, which can also be used with the new context_from option in prop or data declarations.

Example

Storing the value in the context (parent Liveview):

def mount(_params, _session, socket) do
  socket = Context.put(socket, timezone: "UTC")
  {:ok, socket}
end

Retrieving the value from the context (in any component down the tree):

data timezone, :string, from_context: :timezone

def render(assigns) do
  ~F"""
  <h1>Timezone: {@timezone}<h1>
  """
end

For more info on the topic, see the Contexts page.

New render_sface/1 function

One limitation of using colocated .sface files was that there was no easy way to override render/1 to (re)calculate assigns values to be used in the template. With render_sface/1 you can now safely implement render/1, do your calculation and keep the external file.

Example

def render(assigns) do
  assigns = assign(assigns, full_name: "#{assigns.last_name}, #{assigns.first_name}")
  # Render the colocated .sface template with the given updated
  # assigns so it can render `@full_name` properly.
  render_sface(assigns)
end

New --layouts option for mix surface.init

One common request from Surface users was that they wanted to use Surface syntax everywhere, including in layouts. Although that's been possible for a while, the user needed to configure the project manually to achieve that. With the new --layouts option, you can bootstrap your project replacing the generated default .heex layouts with .sface files.

New --tailwind option for mix surface.init

According to discussions on the issue tracker and Slack, it seems most Surface users are also using TailwindCSS, including most of the core team members, so to make our lives easier, we decided to add a --tailwind option to mix surface.init that allows users to bootstrap projects fully configured and ready to build and run the project using tailwind's standalone CLI. Notice that when used together with the --catalogue, --demo and --layouts options, all the related artefacts generated will also be styled using Tailwind CSS instead of Milligram.

New catalogue_test macro

One common issue when using the Surface Catalogue is that your examples and playgrounds might get easily out-of-sync with the related component due to continuous changes in its API. With the new catalogue_test macro, you can now generate basic tests for all your examples and playgrounds with ease, minimizing the chances of facing this kind of issue.

Example

defmodule MyProject.Components.ButtonTest do
  use MyProject.ConnCase, async: true

  catalogue_test MyProject.Components.Button

  # Your other tests for <Button> here
  ...
end

In the example above, the macro will automatically generate tests for all examples/playgrounds found for MyProject.Components.Button.

Note: the generated catalogue tests should not be considered a replacement for all kinds of tests. Their main goal, as already mentioned, is to keep the examples and playgrounds in sync with the component's latest changes. More complex logic and interaction should be covered with more appropriate specific tests as usual.

Enhanced catalogue example API

We improve the Catalogue's API by adding a new module called Surface.Catalogue.Examples, which allows users to create multiple examples as function components per module. Before this addition, users needed to create one module per example using Surface.Catalogue.Example.

Another important change in the API is that we introduced a @example module attribute that also accepts a set of options to customize each example.

Example

defmodule MyProject.Components.Button.Examples do
  use Surface.Catalogue.Examples,
    subject: MyProject.Components.Button,
    title: "Examples for <Button/>"

  @example "The color property"
  def color_example(assigns) do
    ~F"""
    <Button color="info">Info</Button>
    <Button color="warning">Warning</Button>
    <Button color="danger">Danger</Button>
    """
  end

  @example [
    title: "The size property",
    assert: ["small", "medium", "large"]
  ]
  def size_example(assigns) do
    ~F"""
    <Button size="small">Small</Button>
    <Button size="medium">Medium</Button>
    <Button size="large">Large</Button>
    """
  end
end

The assert option, for instance, will instruct catalogue_test to generate extra assertions to check if the texts passed are present in the generated HTML.

Wrapping up

Along with the features above, we shipped other important updates, like the new Slot API, as well as many enhancements to improve both, ergonomics and performance. So make sure you take a look at the full changelog and feel free to report any issue you might find. You can also reach out on our #surface channel on Slack.

For the next version, we'll keep working on extracting parts of the codebase and replacing them with new LV's native counterparts that have been introduced in the past few months. The goal is to reduce friction between Surface and LV components until we make them fully compatible in the next Surface v0.9 version, which will use the upcoming LV's declarative API for components.

Happy coding!

-marlus