README.md

# X.Component

Component-based HTML templates for Elixir/Phoenix, inspired by Vue.<br/>
Zero-dependency. Framework/library agnostic. Optimized for [Phoenix](#phoenix-integration) and Gettext.

![x_component](https://raw.githubusercontent.com/omohokcoj/x_component/master/examples/example.gif)

## Installation

```elixir
def deps do
  [
    {:x_component, "~> 0.1.0"}
  ]
end
```

## Features

[↳](#template-syntax) Declarative HTML template syntax close to Vue.<br/>
[↳](https://github.com/omohokcoj/x_component/blob/master/test/x_test.exs#L16) Compile time errors and warnings.<br/>
[↳](#assigns) Type checks with dialyzer specs.<br/>
[↳](#template-formatter) Template code formatter.<br/>
[↳](#inline-compilation) Inline, context-aware components.<br/>
[↳](#smart-attributes-merge) Smart attributes merge.<br/>
[↳](#decorator-components) Decorator components.<br/>
[↳](#performance-and-benchmarks) Fast compilation and rendering.<br/>
[↳](#phoenix-integration) Optimized for Gettext/Phoenix/ElixirLS.<br/>
[↳](#generator) Component generator task.

## Template Syntax

See more examples [here](https://github.com/omohokcoj/x_component/tree/master/examples/lib).

```vue
~X"""
<body>
  <!-- Body -->
  <div class="container">
    <Breadcrumbs
      :crumbs=[
        %{to: :root, params: [], title: "Home", active: false},
        %{to: :form, params: [], title: "Form", active: true}
      ]
      data-breadcrumbs
    />
    <Form :action='"/book/" <> to_string(book.id)'>
      {{ @message }}
      <FormInput
        :label='"Title"'
        :name=":title"
        :record="book"
      />
      <FormInput
        :name=":body"
        :record="book"
        :type=":textarea"
      />
      <RadioGroup
        :name=":type"
        :options=["fiction", "bussines", "tech"]
        :record="book"
      />
    </Form>
  </div>
</body>
"""
```

### Tags

#### Static

```vue
<div>
  <meta item="example">
  <span />
</div>
```

#### Dynamic

```vue
<X :is="tag_name" />
```

### Interpolations

#### Safe

```vue
<div>{{ message }}</div>
```

#### Unsafe

```vue
<div>{{= html_string }}</div>
```

### Attributes

#### Static

```vue
<button class="d-flex" data-item="1" />
```

#### Dynamic

```vue
<input :class="[active: item.active]" class="form" data-item="1">
```

```vue
<input :class="item.classes" class="form" data-item="1">
```

```vue
<input :attrs=%{"class" => %{"active" => item.classes, "form" => true}, "data-item" => 1}>
```

### Directives

#### x-for

`x-for` is compiled into Elixir `for` list comprehensions.

```vue
<ul>
  <li x-for="i <- [1, 2, 3, 4], i > 2">{{ i }}</li>
</ul>
```

#### x-if, x-else, x-else-if, x-unless

```vue
<div x-unless="is_nil(day)">
  <span x-if="day == 1">Today</span>
  <span x-else-if="day == 2">Tomorrow</span>
  <span x-else>In the future</span>
<div>
```

### Comments

```vue
<!-- Example -->
```

## Components

### Assigns

Assigns can be defined using Elixir typespecs syntax:

```elixir
defmodule Example do
  use X.Component,
    assigns: %{
      :conn => Conn.Plug.t(),
      required(:book) => map(),
      optional(:label) => nil | false | String.t(),
    },
    template: ~X"""
    <div....
```

By default all assigns are required. Optional assigns can be defined with `optional`
map key typespec.

`:asng="expr"` dynamic attribute syntax is used to pass assigns to the component:

```vue
<Example :conn="conn" :book="book" />
```

Also, assigns can be passed as a `map` via the `:assigns` dynamic attr:

```vue
<Example :assigns=%{conn: conn, book: book} />
```

Assigns can be invoked on the template as local variables:

```vue
<div>{{ book.title }}</div>
```

All assigns can be fetched on the template via `@assigns` macro syntax.

```vue
<div>{{ inspect(@assigns) }}</div>
```

### Dynamic components

Component can be rendered dynamicaly from Elixir expresion using special `X` tag with `:component` attribute:

```vue
<X :component="component_module" />
```

### Decorator components

A simple decorator component would look like:

```elixir
defmodule Form do
  use X.Component,
    assigns: %{
      :action => String.t(),
      :method => String.t() | atom()
    },
    template: ~X"""
    <form
      :attrs="@attrs"
      :action="action"
      :method="method"
      class="base-form"
    > {{= yield }}
    </form>
    """
end
```

`:attrs="@attrs"` is used to specify which HTML tag should be decorated (in Vue it's set to the root tag implicitly).

```elixir
defmodule Index do
  use X.Component,
    template: ~X"""
    <Form
      :action='"/books"'
      :method='"get"'
      class="example-class"
    >
      <label>Title</label>
      <input name="title">
    </Form>
    """
end
```

Nested nodes are passed to the `yield` variable of the child component.<br/>
It's important to use the unsafe (`{{=`) interpolation with `yield` to avoid HTML escaping.

Decorator components are fast due to the inline compilation.

### Inline compilation

By default, all components are rendered using the `inline` method.
It means that instead of rendering nested components with a render function it inserts
nested components AST into the parent component AST.
This approach allows to optimize parent component AST for faster rendering.
Decorator component example from the previous paragraph will be compiled entirely into Elixir
string in compile time:

```elixir
iex> Index.template_ast()
"<form action=\"/books\" method=\"get\" class=\"base-form example-class\"> <label>Title</label> <input name=\"title\"> </form>"
```

Also, makes it possible to fetch parent component assigns from the child component
via `@var` syntax, without passing the assigns explicitly.

```vue
  <a
    :href="router(@conn, to, params)"
  > {{ yield }}
  </a>
```

Inline compilation method is not supported by dynamic components.

Compilation method can be adjusted via the application configs:

```elixir
config :x_component,
  compile_inline: true
```

### Smart attributes merge

X template compiler uses special rules for `style` and `class` attributes. Instead of overriding
values it merges them into a list of classes and styles:

```elixir
defmodule Button do
  use X.Component,
    assigns: %{
      optional(:submit) => nil | boolean()
    },
    template: ~X"""
    <button :attrs="@attrs" :class=[submit: submit] class="btn">Submit</button>
    """
end
```

```vue
~X"""
<Button
  :submit="true"
  :class=[{"btn-default", true}]
  class="btn-lg"
/>
"""
```

```html
<button class="btn submit btn-lg btn-default">Submit</button>
```

Style or class can be removed by passing `false` to the child component:

```vue
~X"""
<Button
  :submit="true"
  :class=[{"btn", false}, {"x-btn", true}]
  class="btn-lg"
/>
"""
```

```html
<button class="submit btn-lg x-btn">Submit</button>
```

## Template formatter

Formatter task uses settings from `.formatter.exs` by default.
All project files can be formatted with:

```elixir
mix x.format
```

Also, formatter task can be used to format a specific file:

```elixir
mix x.format path/to/file.ex
```

## Generator

New component files can be generated with:

```elixir
mix x.gen Users.Show
```

Generator settings can be adjusted via `:x_component` application configs:

```elixir
config :x_component,
  root_path: "lib/app_web/components",
  root_module: "AppWeb.Components",
  generator_template: """
    use X.Template
  """
```

## Phoenix integration

* Remove `:phoenix_html` library *(optional)*.
* Add `:x_component` application configs to the `config/config.exs`:

```elixir
config :x_component,
  json_library: Jason,
  root_module: AppWeb.Components,
  root_path: "lib/app_web/components"
```

* Disable html `format_encoders` in `configs.exs`:

```elixir
config :phoenix, :format_encoders, html: false
```

* Create application layout module:

```elixir
defmodule MyApp.Components.Layouts.App do
  use Uncovered.Web, :component

  def render(_, assigns) do
    ~X"""
    <!DOCTYPE html>
    <html lang="en">
      <head>
        ...
      </head>
      <body>
        <X
          :assigns="@assigns"
          :component="@component"
        />
      </body>
    </html>
    """
  end
end
```

* Set layout (in the `router.ex` or in the controller):

```elixir
  pipeline :browser do
    plug :put_layout, {MyApp.Components.Layouts.App, :default}
    ...
  end
```

* Add `use Phoenix.Controller.Components` to your controller or to all controllers
via the macro in `my_app_web.ex`:

```elixir
  def controller do
    quote do
      use Phoenix.Controller, namespace: Uncovered
      use Phoenix.Controller.Components
      ...
    end
  end
```

* Specify components root module for the controller *(optional)*:

```elixir
defmodule MyAppWeb.HomeController do
  use MyAppWeb, :controller

  plug :put_components_module, MyApp.Components.Root
```

* Specify page components for the controller action *(optional)*:

```elixir
defmodule MyApp.ChatController do
  use MyAppWeb, :controller

  def index(conn, _params) do
    conn
    |> put_component(MyApp.Components.Chat)
    |> render()
  end
end
```

`put_components_module` and `put_component` are optional because `Phoenix.Controller.Components`
uses controller and action names to find a component:

```elixir
MyAppWeb.UserController.show => MyAppWeb.Components.Users.Show
MyAppWeb.HomeController.index => MyAppWeb.Components.Homes.Index
```

## Performance and Benchmarks

### Rendering

X templates HTML rendering shows slightly better results than EEx with `Phoenix.HTML.Engine`.
It was achieved due to safe/unsafe interpolation syntax (instead of `{:safe, ...}` tuples) and due to more compact HTML output with trimmed whitespaces (example [here](https://raw.githubusercontent.com/omohokcoj/x_component/master/examples/index.html)).
However, X templates show a significantly faster rendering of nested components
(templates in case of EEx) due to the inline components compilation:

```
Comparison:
X inline (iodata)          20.38 K
X inline (string)          14.48 K - 1.41x slower +19.99 μs
Phoenix EEx (iodata)        7.52 K - 2.71x slower +83.99 μs
Phoenix EEx (string)        6.43 K - 3.17x slower +106.39 μs
```

### Compilation

X templates compile ~2 times slower than EEx templates because it requires to parse the whole
HTML into the template AST (see `X.Ast`) and compile it back to Elixir AST.
However, X templates are much faster than other Elixir HTML template implementations:

```
Comparison:
Floki/Mochi (html parser)        385.79
X (parser)                       357.78 - 1.08x slower +0.20 ms
EEx (html)                       314.95 - 1.22x slower +0.58 ms
X (compiler)                     152.93 - 2.52x slower +3.95 ms
Calliope (haml)                   23.83 - 16.19x slower +39.37 ms
Slime (slim)                       2.27 - 170.23x slower +438.65 ms
Expug (pug)                      0.0836 - 4614.95x slower +11959.75 ms
```

See all benchmarks [here](https://github.com/omohokcoj/x_component/tree/master/bench).

## TODO

- [ ] Live view integration
- [ ] Components cache
- [ ] Syntax highlight plugins

### Vim hack

Syntax highlight via Vue plugin can be enabled by adding the following line to the `vim-elixir/syntax/elixir.vim`:

```vim
syntax include @VUE syntax/vue.vim
syntax region elixirXTemplateSigil matchgroup=elixirSigilDelimiter keepend start=+\~X\z("""\)+ end=+^\s*\z1+ skip=+\\"+ contains=@VUE fold
```

## Issue/Pull Request?

Yes/Please

## License

MIT