defmodule SnapFramework.Component do
alias Scenic.Graph
alias Scenic.Primitive
require SnapFramework.Macros
require Logger
@moduledoc """
## Overview
SnapFramework.Component is nearly identical to a Scene. The main different is the addition of the defcomponent macro,
as well as the addition of the scenic opts key.
defcomponent build out your scenic validate function and helper functions, automatically so you don't have to.
``` elixir
defmodule Example.Component.MyComponent do
use SnapFramework.Component,
template: "lib/scenes/my_component.eex",
controller: :none,
assigns: []
defcomponent :my_component, :tuple
end
```
The above example defines a component that takes in a tuple for data. and build your helper function defined as ```my_component/3```
## Templates
Component templates also have an additional feature that primitives or scene templates do not have. You can inject children into them.
Lets write a basic icon button component that takes an icon child.
``` elixir
# template
<%= graph font_size: 20 %>
<%= primitive Scenic.Primitive.Circle,
15,
id: :bg,
translate: {23, 23}
%>
@children
# component module
defmodule Example.Component.IconButton do
use SnapFramework.Component,
name: :icon_button,
template: "lib/icons/icon_button/icon_button.eex",
controller: :none,
assigns: [],
opts: []
defcomponent :icon_button, :any
end
```
Now lets see how to use this component with children in a scene. This assumes we've already made an icon component.
``` elixir
<%= graph font_size: 20 %>
<%= component Example.Component.IconButton,
nil,
id: :icon_button
do %>
<%= component Example.Component.Icon,
@icon,
id: :icon
%>
<% end %>
```
That's all there is to putting children in components!
"""
@opts_schema [
name: [required: false, type: :atom],
template: [required: true, type: :string],
controller: [required: true, type: :any],
assigns: [required: true, type: :any],
opts: [required: false, type: :any]
]
defmacro __using__(opts) do
case NimbleOptions.validate(opts, @opts_schema) do
{:ok, opts} ->
quote do
use SnapFramework.Scene,
template: unquote(opts[:template]),
controller: unquote(opts[:controller]),
assigns: unquote(opts[:assigns]),
opts: unquote(opts[:opts]),
type: :component
import SnapFramework.Component
alias Scenic.Primitives
require SnapFramework.Macros
require EEx
require Logger
Module.register_attribute(__MODULE__, :assigns, persist: true)
Module.register_attribute(__MODULE__, :preload, persist: true)
end
{:error, error} ->
raise Exception.message(error)
end
end
defmacro defcomponent(name, data_type) do
quote do
case unquote(data_type) do
:string ->
def validate(data) when is_bitstring(data), do: {:ok, data}
:number ->
def validate(data) when is_number(data), do: {:ok, data}
:list ->
def validate(data) when is_list(data), do: {:ok, data}
:map ->
def validate(data) when is_map(data), do: {:ok, data}
:atom ->
def validate(data) when is_atom(data), do: {:ok, data}
:tuple ->
def validate(data) when is_tuple(data), do: {:ok, data}
:any ->
def validate(data), do: {:ok, data}
_ ->
def validate(data), do: {:ok, data}
end
def validate(data) do
{
:error,
"""
#{IO.ANSI.red()}Invalid #{__MODULE__} specification
Received: #{inspect(data)}
#{IO.ANSI.yellow()}
The data for a #{__MODULE__} is just the #{inspect(unquote(data_type))} string to be displayed in the button.#{IO.ANSI.default_color()}
"""
}
end
def unquote(name)(graph, data, options \\ [])
def unquote(name)(%Graph{} = g, data, options) do
add_to_graph(g, data, options)
end
def unquote(name)(
%Scenic.Primitive{module: Scenic.Primitive.Component, data: {mod, _, id}} = p,
data,
options
) do
data =
case mod.validate(data) do
{:ok, data} -> data
{:error, msg} -> raise msg
end
Primitive.put(p, {__MODULE__, data, id}, options)
end
def unquote(name)(%Scenic.Primitive{module: mod} = p, data, opts) do
data =
case mod.validate(data) do
{:ok, data} -> data
{:error, error} -> raise Exception.message(error)
end
Primitive.put(p, data, opts)
end
end
end
end