Current section

46 Versions

Jump to

Compare versions

149 files changed
+6228 additions
-2529 deletions
  @@ -6,7 +6,10 @@ locals_without_parens = [
6 6 data: 3,
7 7 data: 2,
8 8 slot: 1,
9 - slot: 2
9 + slot: 2,
10 + catalogue_test: 1,
11 + catalogue_test: 2,
12 + load_asset: 2
10 13 ]
11 14
12 15 [
  @@ -1,8 +1,55 @@
1 1 # Changelog
2 2
3 - ## v0.7.6 (2022-09-05)
3 + ## v0.8.0
4 4
5 5 * Support Elixir `v1.14`
6 + * Support scoped CSS styles for components using either inline `<style>` tags or colocated
7 + `.css` files (#621)
8 + * Add `render_sface/1` to allow overriding `render/1` and compute/update assigns when using
9 + external `.sface` files
10 + * Add `--tailwind` option to `mix surface.init` to bootstrap a project with TailwindCSS support
11 + * Add `--layouts` option to `mix surface.init` to replace `.heex` layout files with corresponding
12 + `.sface` files
13 + * Add `--web-module` option to `mix surface.init` to override the default web module (#638)
14 + * Support using the `:hook` directive to point to the `default` hook exported in the `.hooks.js` file
15 + * Add option `from_context` to `prop` and `data` to allow setting values directly from the context
16 + * Add `Context.put/3` and `Context.get/2` to allow manipulating the context inside
17 + lifecycle callbacks and `render/1`
18 + * Add prop `context_put` to `<#slot>` to pass context values directly to a slot without propagating
19 + context values to other components
20 + * Add config `:propagate_context_to_slots` to restrict context propagatiion, optimizing diff tracking
21 + * Add functions `Context.copy_assign/3`, `Context.maybe_copy_assign/3` and `Context.maybe_copy_assign!/3`
22 + * Add `catalogue_test/1` macro to generate basic tests for catalogue examples and playgrounds
23 + * Add module `Surface.Catalogue.Examples` to allow defining multiple stateless examples
24 + in a single module
25 + * Support editing slot values as text in playgrounds (Catalogue)
26 + * Fix context propagation in dynamic components
27 + * Fix context propagation in recursive components
28 + * New API for Slot arguments and generator (#613)
29 +
30 + ### Deprecations
31 +
32 + * Deprecate `<InputContext>` in favor of declarative option `from_context`
33 + * Slots (#613)
34 + * Option `:args` has been deprecated, use
35 + * `slot arg: :string` instead of `slot args: [:name]`
36 + * `slot arg: %{name: :string, age: number}` instead of `slot args: [:name, :age]`
37 + * Attribute `for` has been deprecated, use `<#slot {@header}>` instead of`<#slot for={@header}>`
38 + * Attributes `name` and `index` have been deprecated, use `<#slot {col}>` instead of`<#slot name={"col"} index={index}>`
39 + * Directive `:args` has been deprecated, use
40 + * `<#slot {@default, name}>` instead of `<#slot :args={name: name}>`
41 + * `<#slot {@default, name: name, age: age}>` instead of `<#slot :args={name: name, age: age}>`
42 +
43 + ### Breaking Changes
44 +
45 + * Drop support for Elixir < `v1.12`
46 + * Context values are no longer automatically propagated through slots. Components that need to
47 + pass values to the parent scope via slots must explicitly set `propagate_context_to_slots: true`
48 + in their configuration
49 + * Slots (#613)
50 + * New option `:generator_prop` use `slot default, generator_prop: :items` instead of `slot default, args: [item: ^items]`, associated prop `:items` must be of type `:generator`
51 + * New attribute `generator_value` use `<#slot generator_value={item} />` instead of `<#slot :args={item: item} />`
52 + * `<#template slot="slot_name">` has been removed in favor of `<:slot_name>` (#575)
6 53
7 54 ## v0.7.5 (2022-07-21)
  @@ -76,7 +76,7 @@ Add `surface` to the list of dependencies in `mix.exs`:
76 76 ```elixir
77 77 def deps do
78 78 [
79 - {:surface, "~> 0.7.5"}
79 + {:surface, "~> 0.8.0"}
80 80 ]
81 81 end
82 82 ```
  @@ -116,20 +116,7 @@ mix help surface.init
116 116 For further information regarding installation, including how to install Surface manually,
117 117 please visit the [Getting Started](https://surface-ui.org/getting_started) guide.
118 118
119 - ## Migrating from `v0.5.x` to `v0.6.x`
120 -
121 - Surface `v0.6.x` relies on the Liveview features available since `v0.16`. The main change
122 - from the user perspective is that the stateless `Surface.Component` now is built on top of
123 - `Phoenix.Component` instead of `Phoenix.LiveComponent`. This means the `mount/1`, `preload/1`
124 - and `update/2` callbacks are no longer available. If you initialize any assign or compute
125 - any value using those callbacks, you need to replace them with one of the new
126 - [assign helpers](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#module-assigns).
127 -
128 - ## Migrating from `v0.4.x` to `v0.5.x`
129 -
130 - Surface `v0.5.0` introduces a new syntax which requires migrating components written in previous versions.
131 - In order to make the migration process as smooth as possible, Surface `v0.5.x` ships with a converter that
132 - can automatically translate the old syntax into the new one.
119 + ## Migrating from previous versions
133 120
134 121 Please see the [Migration Guide](MIGRATING.md) for details.
  @@ -1,7 +1,7 @@
1 1 {<<"app">>,<<"surface">>}.
2 2 {<<"build_tools">>,[<<"mix">>]}.
3 3 {<<"description">>,<<"A component based library for Phoenix LiveView">>}.
4 - {<<"elixir">>,<<"~> 1.11">>}.
4 + {<<"elixir">>,<<"~> 1.12">>}.
5 5 {<<"files">>,
6 6 [<<"lib">>,<<"lib/surface">>,<<"lib/surface/renderer.ex">>,
7 7 <<"lib/surface/formatter.ex">>,<<"lib/surface/compiler.ex">>,
  @@ -70,23 +70,27 @@
70 70 <<"lib/surface/directive/show.ex">>,<<"lib/surface/directive/values.ex">>,
71 71 <<"lib/surface/directive/component_props.ex">>,
72 72 <<"lib/surface/directive/for.ex">>,<<"lib/surface/directive/if.ex">>,
73 - <<"lib/surface/directive/slot_props.ex">>,
74 73 <<"lib/surface/directive/let.ex">>,<<"lib/surface/directive/debug.ex">>,
75 74 <<"lib/surface/live_view_test.ex">>,<<"lib/surface/type_handler.ex">>,
76 75 <<"lib/surface/catalogue.ex">>,<<"lib/surface/api.ex">>,
77 76 <<"lib/surface/catalogue">>,<<"lib/surface/catalogue/playground.ex">>,
78 77 <<"lib/surface/catalogue/data.ex">>,<<"lib/surface/catalogue/example.ex">>,
79 - <<"lib/surface/view.ex">>,<<"lib/surface/compiler">>,
78 + <<"lib/surface/catalogue/examples.ex">>,<<"lib/surface/view.ex">>,
79 + <<"lib/surface/compiler">>,<<"lib/surface/compiler/converter_0_8.ex">>,
80 80 <<"lib/surface/compiler/converter.ex">>,
81 81 <<"lib/surface/compiler/parse_tree_translator.ex">>,
82 + <<"lib/surface/compiler/css_tokenizer.ex">>,
82 83 <<"lib/surface/compiler/node_translator.ex">>,
84 + <<"lib/surface/compiler/css_parser_error.ex">>,
83 85 <<"lib/surface/compiler/parse_error.ex">>,
84 - <<"lib/surface/compiler/converter_0_5.ex">>,
85 86 <<"lib/surface/compiler/helpers.ex">>,
86 87 <<"lib/surface/compiler/eex_engine.ex">>,
87 88 <<"lib/surface/compiler/tokenizer.ex">>,
88 - <<"lib/surface/compiler/parser.ex">>,<<"lib/surface/live_component.ex">>,
89 - <<"lib/surface/directive.ex">>,<<"lib/surface/type_handler">>,
89 + <<"lib/surface/compiler/css_translator.ex">>,
90 + <<"lib/surface/compiler/parser.ex">>,
91 + <<"lib/surface/compiler/css_parser.ex">>,
92 + <<"lib/surface/live_component.ex">>,<<"lib/surface/directive.ex">>,
93 + <<"lib/surface/type_handler">>,
90 94 <<"lib/surface/type_handler/static_list.ex">>,
91 95 <<"lib/surface/type_handler/context_get.ex">>,
92 96 <<"lib/surface/type_handler/hook.ex">>,
  @@ -95,44 +99,76 @@
95 99 <<"lib/surface/type_handler/event.ex">>,
96 100 <<"lib/surface/type_handler/list.ex">>,
97 101 <<"lib/surface/type_handler/style.ex">>,
98 - <<"lib/surface/type_handler/explict_keyword.ex">>,
102 + <<"lib/surface/type_handler/render_slot.ex">>,
99 103 <<"lib/surface/type_handler/phx_event.ex">>,
100 104 <<"lib/surface/type_handler/atom.ex">>,
101 105 <<"lib/surface/type_handler/default.ex">>,
102 106 <<"lib/surface/type_handler/dynamic.ex">>,
107 + <<"lib/surface/type_handler/let_arg.ex">>,
103 108 <<"lib/surface/type_handler/map.ex">>,
104 109 <<"lib/surface/type_handler/keyword.ex">>,
105 110 <<"lib/surface/type_handler/generator.ex">>,
106 111 <<"lib/surface/type_handler/form.ex">>,
107 112 <<"lib/surface/type_handler/context_put.ex">>,
108 - <<"lib/surface/type_handler/css_class.ex">>,
109 - <<"lib/surface/type_handler/bindings.ex">>,<<"lib/surface/ast.ex">>,
113 + <<"lib/surface/type_handler/css_class.ex">>,<<"lib/surface/ast.ex">>,
110 114 <<"lib/mix">>,<<"lib/mix/tasks">>,<<"lib/mix/tasks/surface">>,
111 115 <<"lib/mix/tasks/surface/surface.format.ex">>,
112 116 <<"lib/mix/tasks/surface/surface.init">>,
117 + <<"lib/mix/tasks/surface/surface.init/file_patchers">>,
118 + <<"lib/mix/tasks/surface/surface.init/file_patchers/js.ex">>,
119 + <<"lib/mix/tasks/surface/surface.init/file_patchers/formatter.ex">>,
120 + <<"lib/mix/tasks/surface/surface.init/file_patchers/config.ex">>,
121 + <<"lib/mix/tasks/surface/surface.init/file_patchers/component.ex">>,
122 + <<"lib/mix/tasks/surface/surface.init/file_patchers/phoenix.ex">>,
123 + <<"lib/mix/tasks/surface/surface.init/file_patchers/text.ex">>,
124 + <<"lib/mix/tasks/surface/surface.init/file_patchers/mix_exs.ex">>,
113 125 <<"lib/mix/tasks/surface/surface.init/ex_patcher.ex">>,
114 - <<"lib/mix/tasks/surface/surface.init/patchers">>,
115 - <<"lib/mix/tasks/surface/surface.init/patchers/js.ex">>,
116 - <<"lib/mix/tasks/surface/surface.init/patchers/formatter.ex">>,
117 - <<"lib/mix/tasks/surface/surface.init/patchers/component.ex">>,
118 - <<"lib/mix/tasks/surface/surface.init/patchers/phoenix.ex">>,
119 - <<"lib/mix/tasks/surface/surface.init/patchers/text.ex">>,
120 - <<"lib/mix/tasks/surface/surface.init/patchers/mix_exs.ex">>,
121 126 <<"lib/mix/tasks/surface/surface.init/ex_patcher_move.ex">>,
122 - <<"lib/mix/tasks/surface/surface.init/patches.ex">>,
127 + <<"lib/mix/tasks/surface/surface.init/project_patcher.ex">>,
123 128 <<"lib/mix/tasks/surface/surface.init/patcher.ex">>,
129 + <<"lib/mix/tasks/surface/surface.init/project_patchers">>,
130 + <<"lib/mix/tasks/surface/surface.init/project_patchers/js_hooks.ex">>,
131 + <<"lib/mix/tasks/surface/surface.init/project_patchers/formatter.ex">>,
132 + <<"lib/mix/tasks/surface/surface.init/project_patchers/layouts.ex">>,
133 + <<"lib/mix/tasks/surface/surface.init/project_patchers/demo.ex">>,
134 + <<"lib/mix/tasks/surface/surface.init/project_patchers/catalogue.ex">>,
135 + <<"lib/mix/tasks/surface/surface.init/project_patchers/scoped_css.ex">>,
136 + <<"lib/mix/tasks/surface/surface.init/project_patchers/tailwind.ex">>,
137 + <<"lib/mix/tasks/surface/surface.init/project_patchers/common.ex">>,
138 + <<"lib/mix/tasks/surface/surface.init/project_patchers/error_tag.ex">>,
124 139 <<"lib/mix/tasks/surface/surface.init.ex">>,
125 140 <<"lib/mix/tasks/surface/surface.convert.ex">>,<<"lib/mix/tasks/compile">>,
141 + <<"lib/mix/tasks/compile/surface">>,
142 + <<"lib/mix/tasks/compile/surface/validate_components.ex">>,
126 143 <<"lib/mix/tasks/compile/surface.ex">>,
127 144 <<"lib/mix/tasks/compile/surface.asset_generator.ex">>,<<"lib/surface.ex">>,
128 145 <<"priv">>,<<"priv/templates">>,<<"priv/templates/surface.init">>,
129 146 <<"priv/templates/surface.init/demo">>,
130 - <<"priv/templates/surface.init/demo/example02.ex">>,
131 - <<"priv/templates/surface.init/demo/playground.ex">>,
132 - <<"priv/templates/surface.init/demo/demo.ex">>,
133 - <<"priv/templates/surface.init/demo/hero.ex">>,
134 - <<"priv/templates/surface.init/demo/example01.ex">>,<<"priv/catalogue">>,
135 - <<"priv/catalogue/surface">>,<<"priv/catalogue/surface/components">>,
147 + <<"priv/templates/surface.init/demo/card_playground.ex">>,
148 + <<"priv/templates/surface.init/demo/default">>,
149 + <<"priv/templates/surface.init/demo/default/card_examples.ex">>,
150 + <<"priv/templates/surface.init/demo/default/demo.ex">>,
151 + <<"priv/templates/surface.init/demo/default/card.ex">>,
152 + <<"priv/templates/surface.init/demo/tailwind">>,
153 + <<"priv/templates/surface.init/demo/tailwind/card_examples.ex">>,
154 + <<"priv/templates/surface.init/demo/tailwind/demo.ex">>,
155 + <<"priv/templates/surface.init/demo/tailwind/card.ex">>,
156 + <<"priv/templates/surface.init/demo/card_test.exs">>,
157 + <<"priv/templates/surface.init/tailwind">>,
158 + <<"priv/templates/surface.init/tailwind/tailwind.config.js">>,
159 + <<"priv/templates/surface.init/layouts">>,
160 + <<"priv/templates/surface.init/layouts/default">>,
161 + <<"priv/templates/surface.init/layouts/default/index.sface">>,
162 + <<"priv/templates/surface.init/layouts/default/root.sface">>,
163 + <<"priv/templates/surface.init/layouts/default/live.sface">>,
164 + <<"priv/templates/surface.init/layouts/default/app.sface">>,
165 + <<"priv/templates/surface.init/layouts/tailwind">>,
166 + <<"priv/templates/surface.init/layouts/tailwind/index.sface">>,
167 + <<"priv/templates/surface.init/layouts/tailwind/root.sface">>,
168 + <<"priv/templates/surface.init/layouts/tailwind/live.sface">>,
169 + <<"priv/templates/surface.init/layouts/tailwind/app.sface">>,
170 + <<"priv/catalogue">>,<<"priv/catalogue/surface">>,
171 + <<"priv/catalogue/surface/components">>,
136 172 <<"priv/catalogue/surface/components/form">>,
137 173 <<"priv/catalogue/surface/components/form/textarea">>,
138 174 <<"priv/catalogue/surface/components/form/textarea/playground.ex">>,
  @@ -156,10 +192,10 @@
156 192 {<<"name">>,<<"phoenix_live_view">>},
157 193 {<<"optional">>,false},
158 194 {<<"repository">>,<<"hexpm">>},
159 - {<<"requirement">>,<<"~> 0.17.4">>}],
195 + {<<"requirement">>,<<"~> 0.17.6">>}],
160 196 [{<<"app">>,<<"sourceror">>},
161 197 {<<"name">>,<<"sourceror">>},
162 198 {<<"optional">>,false},
163 199 {<<"repository">>,<<"hexpm">>},
164 200 {<<"requirement">>,<<"~> 0.9">>}]]}.
165 - {<<"version">>,<<"0.7.6">>}.
201 + {<<"version">>,<<"0.8.0">>}.
  @@ -4,20 +4,57 @@ defmodule Mix.Tasks.Compile.Surface.AssetGenerator do
4 4 alias Mix.Task.Compiler.Diagnostic
5 5
6 6 @default_hooks_output_dir "assets/js/_hooks"
7 + @default_css_output_file "assets/css/_components.css"
7 8 @supported_hooks_extensions ~W"js jsx ts tsx" |> Enum.join(",")
8 9 @hooks_tag ".hooks"
9 10 @hooks_extension "#{@hooks_tag}.{#{@supported_hooks_extensions}}"
10 11
11 - def run() do
12 - {js_files, _css_files, diagnostics} = get_colocated_assets()
13 - generate_files(js_files)
12 + def run(components, opts \\ []) do
13 + hooks_output_dir = Keyword.get(opts, :hooks_output_dir, @default_hooks_output_dir)
14 + css_output_file = Keyword.get(opts, :css_output_file, @default_css_output_file)
15 + env = Keyword.get(opts, :env, Mix.env())
16 + {js_files, js_diagnostics} = get_colocated_js_files(components)
17 + generate_js_files(js_files, hooks_output_dir)
18 + css_diagnostics = generate_css_file(components, css_output_file, env)
19 + diagnostics = js_diagnostics ++ css_diagnostics
14 20 diagnostics |> Enum.reject(&is_nil/1)
15 21 end
16 22
17 - def generate_files(js_files) do
18 - opts = Application.get_env(:surface, :compiler, [])
23 + defp generate_css_file(components, css_output_file, env) do
24 + dest_file = Path.join([File.cwd!(), css_output_file])
19 25
20 - hooks_output_dir = Keyword.get(opts, :hooks_output_dir, @default_hooks_output_dir)
26 + dest_file_content =
27 + case File.read(dest_file) do
28 + {:ok, content} -> content
29 + _ -> nil
30 + end
31 +
32 + {content, _, diagnostics} =
33 + for mod <- Enum.sort(components, :desc),
34 + function_exported?(mod, :__style__, 0),
35 + {func, %{css: css, scope_id: scope_id, vars: vars}} = func_style <- mod.__style__(),
36 + reduce: {"", nil, []} do
37 + {content, last_mod_func_style, diagnostics} ->
38 + css = String.trim_leading(css, "\n")
39 + component_header = [inspect(mod), ".", to_string(func), "/1 (", scope_id, ")"]
40 +
41 + {
42 + ["\n/* ", component_header, " */\n\n", vars_comment(vars, env), css | content],
43 + {mod, func_style},
44 + validate_multiple_styles({mod, func_style}, last_mod_func_style) ++ diagnostics
45 + }
46 + end
47 +
48 + content = to_string([header(), "\n" | content])
49 +
50 + if content != dest_file_content do
51 + File.write!(dest_file, content)
52 + end
53 +
54 + diagnostics
55 + end
56 +
57 + defp generate_js_files(js_files, hooks_output_dir) do
21 58 js_output_dir = Path.join([File.cwd!(), hooks_output_dir])
22 59 index_file = Path.join([js_output_dir, "index.js"])
23 60
  @@ -48,9 +85,9 @@ defmodule Mix.Tasks.Compile.Surface.AssetGenerator do
48 85 end
49 86 end
50 87
51 - defp get_colocated_assets() do
52 - for mod <- components(), module_loaded?(mod), reduce: {[], [], []} do
53 - {js_files, css_files, diagnostics} ->
88 + defp get_colocated_js_files(components) do
89 + for mod <- components, module_loaded?(mod), reduce: {[], []} do
90 + {js_files, diagnostics} ->
54 91 component_file = mod.module_info() |> get_in([:compile, :source]) |> to_string()
55 92 base_file = component_file |> Path.rootname()
56 93 base_name = inspect(mod)
  @@ -65,12 +102,7 @@ defmodule Mix.Tasks.Compile.Surface.AssetGenerator do
65 102 js_files
66 103 end
67 104
68 - css_file = "#{base_file}.css"
69 - dest_css_file = "#{base_name}.css"
70 -
71 - css_files = if File.exists?(css_file), do: [{css_file, dest_css_file} | css_files], else: css_files
72 -
73 - {js_files, css_files, [new_diagnostic | diagnostics]}
105 + {js_files, [new_diagnostic | diagnostics]}
74 106 end
75 107 end
76 108
  @@ -153,55 +185,6 @@ defmodule Mix.Tasks.Compile.Surface.AssetGenerator do
153 185 unsused_files
154 186 end
155 187
156 - defp components() do
157 - project_app = Mix.Project.config()[:app]
158 - :ok = Application.ensure_loaded(project_app)
159 - project_deps_apps = Application.spec(project_app, :applications) || []
160 - opts = Application.get_env(:surface, :compiler, [])
161 - only_web_namespace = Keyword.get(opts, :only_web_namespace, false)
162 -
163 - for app <- [project_app | project_deps_apps],
164 - deps_apps = Application.spec(app)[:applications] || [],
165 - app in [:surface, project_app] or :surface in deps_apps,
166 - prefix = app_beams_prefix(app, project_app, only_web_namespace),
167 - {dir, files} = app_beams_dir_and_files(app),
168 - file <- files,
169 - List.starts_with?(file, prefix) do
170 - :filename.join(dir, file)
171 - end
172 - |> Enum.chunk_every(50)
173 - |> Task.async_stream(fn files ->
174 - for file <- files,
175 - {:ok, {_, [{_, chunk} | _]}} = :beam_lib.chunks(file, ['Attr']),
176 - chunk |> :erlang.binary_to_term() |> Keyword.get(:component_type) do
177 - file |> Path.basename(".beam") |> String.to_atom()
178 - end
179 - end)
180 - |> Enum.flat_map(fn {:ok, result} -> result end)
181 - end
182 -
183 - defp app_beams_prefix(app, project_app, only_web_namespace) do
184 - if only_web_namespace and app == project_app do
185 - Mix.Phoenix.base()
186 - |> Mix.Phoenix.web_module()
187 - |> Module.concat(".")
188 - |> to_charlist()
189 - else
190 - 'Elixir.'
191 - end
192 - end
193 -
194 - defp app_beams_dir_and_files(app) do
195 - dir =
196 - app
197 - |> Application.app_dir()
198 - |> Path.join("ebin")
199 - |> String.to_charlist()
200 -
201 - {:ok, files} = :file.list_dir(dir)
202 - {dir, files}
203 - end
204 -
205 188 defp module_loaded?(module) do
206 189 match?({:module, _mod}, Code.ensure_compiled(module))
207 190 end
  @@ -224,4 +207,35 @@ defmodule Mix.Tasks.Compile.Surface.AssetGenerator do
224 207 compiler_name: "Surface"
225 208 }
226 209 end
210 +
211 + defp validate_multiple_styles({mod, {func, style}}, {mod, {func, other_style}}) do
212 + position = "#{Path.relative_to_cwd(style.file)}:#{style.line}"
213 +
214 + message = """
215 + scoped CSS style already defined for #{inspect(mod)}.#{func}/1 at #{position}. \
216 + Scoped styles must be defined either as the first <style> node in \
217 + the template or in a colocated .css file.
218 + """
219 +
220 + [warning(message, other_style.file, other_style.line)]
221 + end
222 +
223 + defp validate_multiple_styles(_, _) do
224 + []
225 + end
226 +
227 + defp vars_comment(vars, env) do
228 + comment =
229 + vars
230 + |> Enum.reverse()
231 + |> Enum.reduce([], fn {var, {expr, _meta}}, acc ->
232 + [" #{var}: `#{expr}`\n" | acc]
233 + end)
234 +
235 + if comment == [] or env == :prod do
236 + ""
237 + else
238 + ["/* vars:\n", comment, "*/\n\n"]
239 + end
240 + end
227 241 end
Loading more files…