surface-ui / surface Goto Github PK
View Code? Open in Web Editor NEWA server-side rendering component library for Phoenix
Home Page: https://surface-ui.org
License: MIT License
A server-side rendering component library for Phoenix
Home Page: https://surface-ui.org
License: MIT License
I would assume that this would work:
alias Surface.Components.Link
<Link to="#" :on-phx-click={{ "ok", target: "#comp" }}>OK</Link>
But it fails to compile and emits a warning.
Unknown property ":on-phx-click" for component <Link>
How can I make it work?
Hi @msaraiva, this is an awesome library! I def plan on contributing to this project.
One initial question I have looking over the docs, is why handle_event
methods on Surface.{LiveComponent, LiveView}
dont use bracket notation the same way Surface.Component
does?
In this documentation, for a Surface.Component
a click handler is set like this:
<Button click={{ @hideEvent }}>Ok</Button>
Where as on a Surface.{LiveComponent, LiveView}
it looks like this:
<Button click="hide">Ok</Button>
I personally find this pretty confusing, as well as harder to parse what is markup versus executed code (I realize it all is actually a sigil that is intepreted and executed, but I hope you get the gist of what I am getting at).
Why not just have all Surface modules use bracket syntax for consistency?
Here's a weird one that took me some time to isolate.
defmodule Empty do
use Surface.LiveComponent
require Logger
def render(assigns) do
~H"""
<div class="blank">
</div>
"""
end
def handle_event(event, _data, socket) do
Logger.warn("Unhandled event #{event}")
{:noreply, socket}
end
end
This results in the following 2 errors: (I edited the error text for file paths)
warning: this clause cannot match because a previous clause at line 1 always matches
lib/empty.ex:1
This puts a compiler warning on the defmodule
line. It appears to swallow the actual line with the issue.
If you really don't want to allow a "catch-all" handle_event
, then perhaps give a more specific error.
The documentation (which is rendered by Surface) shows an example of using a Button.
alias Surface.Components.Button
With usage like this:
<Button click="show_dialog" kind="is-info">Click to open</Button>
The surface repo doesn't declare it in the components
directory.
Add a surface_component/3
function to enable loading components inside *.leex
files
If you define a component that renders inner content, e.g:
defmodule Card do
use Surface.Component
def render(assigns) do
~H"""
<div class="card>
{{ @inner_content.() }}
</div>
"""
end
end
but then use it without defining any wrapped content, e.g:
defmodule User do
use Surface.Component
def render(assigns) do
~H"""
<Card />
"""
end
end
The above will generate a KeyError in content_handler/join_contents/3
, which assumes that an :inner_content
assign exists.
The reason I added this feature was to be able to work nicely with css libs like bulma
which has naming conventions for classes mostly as is-*
and has-*
and since atom keys cannot contain -
without wrapping it in "..."
, it felt like a good addition at the time. The rationale behind it was that if you don't want the auto-conversion, you could just use keys as strings instead of atoms:
<!-- Using atoms: `isDanger` is auto-converted to "is-danger" -->
<div class={{ isDanger: true }}>
<!-- Using strings: This is NOT auto-converted. It keeps whatever value the key has -->
<div class={{ "isDanger" => true }}>
However, I'm afraid no matter how good this feature is documented (currently is not), people will still get confused, especially newcomers. So in order to avoid confusion, I think we could just drop this feature and always use the original value. WDTY?
BTW, as far as I can remember some JS frameworks have similar auto-conversion feature.
@wrren, @brainlid, @mazz-seven or anyone interested. I'd love to hear your thoughts about this one.
Original discussion can be found here.
This may be an odd edge case. But check this out...
~H"""
<div style="height: {{ @dragging_height }}px;">
</div>
"""
This results the following compilation error.
== Compilation error in file lib/fe_web/views/editor/item.ex ==
** (TokenMissingError) lib/fe_web/views/editor/item.ex:74: missing terminator: " (for string starting at line 74)
(eex 1.10.1) lib/eex/compiler.ex:45: EEx.Compiler.generate_buffer/4
expanding macro: Surface.sigil_H/2
lib/fe_web/views/editor/item.ex:73: FeWeb.Editor.Item.render/1
(elixir 1.10.1) expanding macro: Kernel.if/2
lib/fe_web/views/editor/item.ex:72: FeWeb.Editor.Item.render/1
Rewriting it this way fixed the issue.
style_text = "height: #{assigns.dragging_height}px;"
~H"""
<div style={{style_text}}>
</div>
"""
The issue appears to be when putting {{ @stuff }}
inside a string it confuses the parser.
Similar to :if
but the component will always be rendered and remain in the DOM, even when it's false
(it just sets display: none
)
The first thing we need is to find a way to parse/change css. The only lib I could find was this one https://github.com/acammack-r7/erlang-css, but I'm afraid it hasn't been maintained for a while. So we either find an alternative or we need to roll our own parser. Maybe using nimble_parsec
again?
Read the scoped
attribute from <style scoped>...</style>
, if it's set, add a unique component hash id to selectors and translate the code accordingly. Example:
<style scoped>
.example {
color: red;
}
</style>
<div class="example">
...
</div>
becomes:
<style scoped>
.example[data-sface-d4f9a23] {
color: red;
}
</style>
<div data-sface-d4f9a23>
...
</div>
Keep track of styles definitions in order to render only one definition for each type of component. Currently, if we render more than one instance of a component, the style is also rendered, causing unnecessary duplication.
Similar to mix phx.new
. Probably with the same options.
First thanks for the awesome work on this library ๐
I would like to use Alpine.js for some very simple interactions like toggling a dropdown. However some Alpine attributes like @click
conflict with Surface.
How can I deal with this?
Example
defmodule MyAppWeb.TestComponent do
use Surface.Component
def render(assigns) do
~H"""
<div @click.away="open = false" x-data="{ open: false }">
<div @click="open = !open">My profile</div>
<div x-show="open">
<a href="#">Log out</a>
</div>
</div>
"""
end
end
** (Surface.Translator.ParseError) lib/my_app_web/live/test_component.ex:6: expected opening HTML tag
lib/surface/translator.ex:44: Surface.Translator.run/4
expanding macro: Surface.sigil_H/2
lib/my_app_web/live/test_component.ex:5: MyAppWeb.LayoutLive.TestLayout.render/1
I foolishly attempted to use <#Raw></#Raw>
without first aliasing Surface.Components.Raw
. As you might expect, this caused the component to break. Unfortunately, I was not able to recover from this error unless I stopped the server, ran mix.clean
, and then started the server up again.
To reproduce, simply use Raw
without the alias. Then, attempt to fix the issue by adding in the missing alias. The error should still exist.
I don't see this behavior when other errors occur. For example, I can attempt to use a component without first aliasing it. But, once I fix the alias, the error goes away and the component renders. It seems to only be happening with the Raw
component.
When building a project that contains any Surface component (i.e. any module using Component, LiveView, LiveComponent or MacroComponent) with @moduledoc false
, compilation fails with the following error:
The project should compile successfully.
Compilation fails.
Adding the following arm to the case
statement of Surface.API.generate_docs/1 appears to fix the issue:
{_line, false} ->
{env.line, props.doc}
This solution returns the same doc as if the @moduledoc
attribute was not specified at all.
using Surface.Component
and @moduledoc false
mix compile
Elixir version: 1.10.3-otp-22
Surface version: Latest from master
If this an actual issue and the proposed solution is acceptable I would be happy to submit a PR to fix this!
I am unable to nest a stateful component inside a Form component. Perhaps there is something additional needed in the setup of the Form?
It results in this error:
[error] GenServer #PID<0.1987.0> terminating
** (ArgumentError) cannot convert component FeWeb.Editor.Inspectors.Components.LiveOptions with id "live-options" to HTML. A component must always be returned directly as part of a LiveView template
Here is the context that they are being rendered in:
defmodule FeWeb.Editor.InspectorPane do
use Surface.LiveComponent
alias FeWeb.Editor.Inspectors.Components.Form
alias FeWeb.Editor.Inspectors.Components.LiveOptions
def render(assigns) do
~H"""
<div id="inspector_pane">
<Form for={{ @changeset }} id="inspector_form" as="inspector_form" change="validate_inspector" autocomplete="off">
<LiveOptions id="live-options" />
</Form>
</div>
"""
end
In the above example, moving the "LiveOptions" component outside of the Form works.
Below is the code for the "stateful" item being nested. It manages no state, but it is a "Surface.LiveComponent". Changing it to a "Surface.Component" works fine.
defmodule FeWeb.Editor.Inspectors.Components.LiveOptions do
use Surface.LiveComponent
@doc """
Render the options live component for the inspector.
"""
def render(assigns) do
IO.puts "11111111111111111111111111111111111"
IO.puts "11111111111111111111111111111111111"
IO.puts "11111111111111111111111111111111111"
~H"""
<p>Testing</p>
"""
end
end
The "Form" component is heavily based on yours. For completeness the code follows. I assume that the Form component needs to be stateful. It appears that in order for a stateful component to be used, the entire hierarchy above it must be stateful as well. Is that correct?
defmodule FeWeb.Editor.Inspectors.Components.Form do
@moduledoc """
A surface "Form" component used for the inspector.
"""
use Surface.LiveComponent
import Phoenix.HTML.Form
property for, :string, required: true
property as, :string
property change, :event
property submit, :event
property autocomplete, :string
def begin_context(props) do
form =
form_for(props.for, "#",
as: props.as,
novalidate: true,
phx_submit: props.submit,
phx_change: props.change,
autocomplete: props.autocomplete
)
Map.put(props.context, :form, form)
end
def render(assigns) do
~H"""
{{ @context.form }}
{{ @inner_content.() }}
{{ raw("</form>") }}
"""
end
def end_context(props) do
Map.delete(props.context, :form)
end
end
Hi ๐
Is there some way of rendering a component dynamically? Like if I had an array with a Button
and a Link
component and I want to render them in a for loop:
def render(assigns)
content = [Button, Link]
~H"""
<div :for={{component <- content}}>
???
</div>
"""
end
I tried already some things like rendering with live_component()
like :
{{ live_component, @socket, component }}
That works on an older state, but errors with the current master: BadMapError: expected a map, got: nil
in Surface.ContentHandler.init_contents/1
.
Also trying to interpolate the component somehow does not work, but would probably be a nice syntax:
<{{component}} />
Do you have any plans for this feature already (or am I missing something).
Since this is easily possible in plain LiveView it would sad if we couldn't also do this with surface.
I also imagine having the interpolation on a html element level like in react would be handy:
<h{{level}}>A headline</h{{level}}>
Also I would like to thank you for creating this project since I am already a huge fan of the idea ๐.
I would also volunteer to look into this and maybe create a PR if you give me some directions :)
Cheers
Andi
A tag like the following (which I wanted to use as first information in a table where the first visible header row does not have all the columns)
<col :for={{ c <- @cols }}>
fails to be parsed.
Full context:
<table>
<col :for={{ _ <- @cols }}>
<thead>
<tr><th colspan="99">This row only has one cell, so table layout would fail without the COL tags.</th></tr>
<tr><th :for={{ c <- @cols }}>{{ c.title }}</th></tr>
</thead>
....
</table>
We are unable to pattern match in the function declaration and use the resulting assignments in a guard.
For example, this code:
def handle_event(event, %{"id" => id}, socket) when id in ["foo"] do
{:noreply, socket}
end
...raises the following compiler error:
== Compilation error in file lib/fe_web/live/editor.ex ==
** (CompileError) lib/fe_web/live/editor.ex:765: cannot find or invoke local id/0 inside guard. Only macros can be invoked in a guard and they must be defined before their invocation. Called as: id()
(stdlib 3.11) lists.erl:1354: :lists.mapfoldl/3
(elixir 1.10.1) expanding macro: Kernel.in/2
(fe_web 0.1.0) lib/fe_web/live/editor.ex:765: FeWeb.Editor.__has_event_handler__?/1
(surface 0.1.0) lib/editor.ex:1: Surface.EventValidator.__before_compile__/1
(elixir 1.10.1) lib/kernel/parallel_compiler.ex:233: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/7
Guards still work if you are not pattern matching and assigning variables from inside of a map. For example, this still works:
def handle_event(event, _data, socket) when event in ["foo"] do
{:noreply, socket}
end
Reproduction Steps
property click, :event
def render(assigns) do
~H"""
<div class="card" :on-phx-click={{ @click }}>
{{ @inner_content.() }}
</div>
"""
end
In practice, using the above component without specifying a click
event will cause a runtime error:
(RuntimeError) invalid value for ":on-phx-click". Expected a :string or :event, got: nil
Expected Behaviour
No phx-*
markup should be emitted when the input event is nil.
Versions
1.10.2
msaraiva/surface@master
How to reproduce
defmodule MyAppWeb.Components.Hello do
use Surface.Component
def render(assigns) do
~H"""
<input type="text" placeholder="รฉ" />
"""
end
end
Stacktrace
== Compilation error in file lib/my_app_web/components/hello.ex ==
** (UnicodeConversionError) invalid encoding starting at <<233, 34, 41, 32, 37, 62, 34, 32, 62, 10>>
(elixir 1.10.2) lib/string.ex:2223: String.to_charlist/1
(eex 1.10.2) lib/eex/tokenizer.ex:33: EEx.Tokenizer.tokenize/3
(eex 1.10.2) lib/eex/compiler.ex:18: EEx.Compiler.compile/2
(surface 0.1.0-alpha.1) expanding macro: Surface.sigil_H/2
lib/my_app_web/components/hello.ex:5: MyApp.Components.Hello.render/1
(elixir 1.10.2) lib/kernel/parallel_compiler.ex:304: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/7
Hey @msaraiva!
I've been noticing this same issue pop up lately. When we use the "modal pattern" of maintaining an open/close state in a stateful component, we're unable to called send_update
. (we're borrowing this idea from your docs)
For example, to open a modal you might call Modal.open("id-here")
. That function will then call send_update
:
def open(id) do
send_update(__MODULE__, id: id, open: true)
end
The problem is this line https://github.com/msaraiva/surface/blob/3d17ed8f3c21d2ea8e48d8b4208dbfc68405bcda/lib/surface/live_component.ex#L85 assumes that __surface__
already exists, but it doesn't. It does exist on the component, but isn't added until we merge in the existing assigns with the new incoming assigns.
Cannot use Phoenix HTML helpers with do blocks with {{ }}
.
defmodule Foo do
use Surface.Component
import Phoenix.HTML.Link
property links, :list, default: ["foo", "bar", "baz"]
def render(assigns) do
~H"""
<div>
{{ for l <- @links do }}
{{ link to: "#" do }}
<span>{{ l }}</span>
{{ end }}
{{ end }}
</div>
"""
end
end
With this we get a compiler warning and error
Compiling 1 file (.ex)
warning: unexpected beginning of EEx tag "<%=" on end of expression "<%= end %>", please remove "=" accordingly
lib/foo.ex:14
== Compilation error in file lib/foo.ex ==
** (SyntaxError) lib/foo.ex:14: unexpected token: end
lib/eex/compiler.ex:101: EEx.Compiler.generate_buffer/4
lib/eex/compiler.ex:54: EEx.Compiler.generate_buffer/4
expanding macro: Surface.sigil_H/2
lib/foo.ex:9: Parent.render/1
In this contrived example (intentionally not using the :for
directive) we can move past the error by manually creating the anchor tag, but is this desired behavior?
If we remove the do
block for link/2
we lose the error but keep the warning.
Sorry @msaraiva, I don't mean to pummel you with issues. This is great package and I can't wait to see it reach v1.0.0!
Create built-in context friendly form components around Phoenix helpers.
<Inputs for={{...}} .../>
)I'm not sure yet if it makes sense to create wrappers for all of them. Please let me know if you believe we should add/remove any helper to/from this list.
Similar to property
, I want to add data
and context
functions so we can benefit from static checking and better tooling. Proposal:
data show, :boolean, default: false
The :default
option could minimise the need to implement mount/3
.
defmodule Form do
...
context set: :form
end
defmodule TextInput do
...
context get: :form, from: Form
end
Both data
and context
would be merged into the assigns. So instead of @context.form
we could use @form
directly.
We can also have a :as
option for cases when we want to change the name of the assign:
defmodule TextInput do
...
context get: :form, from: Form, as: :whatever
end
I've enjoyed working with surface so far; it has worked really well, and feels familiar coming from Vue/Angular/React type frameworks. Thank you for the work you've put into this!
One thing that feels a little odd is using the live_redirect
and live_patch
functions. It's fine when the link has a simple string as the contents (as that can just be passed as the first argument); however, when there's more complicated inner content I've been having to fall back to <#Raw><% end %></#Raw>
. With standard leex it looks something like:
<%= live_redirect to: "something", class: "..." do %>
<!-- inner contents -->
<% end %>
Which I'd expect to convert into something like:
{{ live_redirect to: "something", class: "..." do }}
<!-- inner contents -->
{{ end }}
however, the compiler complains as {{ end }}
gets translated into <%= end %>
.
Is there a better way to handle that? Or is using <#Raw><% end %></#Raw>
the way to go right now?
After phoenixframework/phoenix_live_view#551, when dispatching events in live components, the default target is now the parent live view, not the component anymore. This can be non-intuitive, especially if you're coming from an existing component-based library like React
where events are usually handled locally. I have a feeling that this will be a frequent source of confusion as you can already see here. If you need more details you can see a broader discussion in the PR.
Just to make it clear, this was not the initially desired behaviour as explained by Josรฉ Valim in this comment.
So, in order to implement the most common case (which is using an id) and have all events handled locally, you need to do something like:
id
attribute of the component's root elementphx-*
events on the elements you need (e.g, phx-click
)phx-target
on any of those elements that you want to handle the events locallyExample:
defmodule MyComponent do
use Surface.LiveComponent
property id, :integer
def render(assigns) do
~H"""
<div id={{ @id }}>
<button phx-click="ok" phx-target="#{{ @id }}">OK</button>
<button phx-click="cancel" phx-target="#{{ @id }}">Cancel</button>
</div>
"""
end
def handle_event("ok") do
...
end
def handle_event("cancel") do
...
end
end
This might get a little verbose especially when you start splitting your view into smaller components since you need to always pass both, the event and the target using properties as well.
With Surface, however, we have the chance to restore the originally desired behaviour and also add some syntactic sugar when defining events. We could then:
id
for any Surface.LiveComponent
id
if the user passes a custom identifierid
to the root elementExample:
Assuming we define a component <Button>
with a property "click" of type :event
. We could write just:
defmodule MyComponent do
use Surface.LiveComponent
def render(assigns) do
~H"""
<div>
<Button click="ok">OK</Button>
<Button click="cancel">Cancel</Button>
</div>
"""
end
def handle_event("ok") do
...
end
def handle_event("cancel") do
...
end
end
and then if we need to tell that one of the events should be handled by the parent live view, we could do something like:
<Button click="^ok">OK</button>
Internally, we can represent an event always as a tuple like {selector, event}
. For the first example click="ok"
, assuming the auto-generated id was "MyComponent_1", the event would be {"#MyComponent_1", "ok"}
and for the second one click="^ok"
the internal representation could be {:parent, "ok"}
.
By always passing the events in the above format, Surface can generate the code accordingly for each case.
This markup was copied from an EEx template into a ~H
one.
The problem is the <hr>
. The compiler error is:
** (Surface.Translator.Parser.ParseError) /my_app/views/editor/placeholder.ex:14: expected end of string
(surface) lib/surface/translator/parser.ex:222: Surface.Translator.Parser.parse/3
(surface) lib/surface/translator.ex:23: Surface.Translator.run/4
(surface) expanding macro: Surface.sigil_H/2
(fe_web) lib/fe_web/views/editor/placeholder.ex:13: FeWeb.Editor.Placeholder.render/1
(elixir) lib/kernel/parallel_compiler.ex:229: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/7
After trial and error I realized it was the <hr>
. When I changed it to <hr />
then it worked fine.
The solution is either support markup like the non-xml valid <hr>
or improve the error message to help identify what the problem is.
Generate the AST directly instead of an EEx template
It would be nice to be able to only set an attribute if the
property is non empty. This is particularly interesting for non
required properties. For example, if no class attribute is passed,
it still setsclass=""
.
We do this already for boolean attributes. We should extend this behaviour when working with regular HTML tags as none of them accepts nil
anyway.
A callback to process props/param inputs into the a Surface.LiveComponent
's state would be helpful.
The mount/1
callback doesn't have access to the props yet. The render/1
function doesn't feel like the right place.
Here's an example. Let me know if you see a better way to solve this.
<MyOptions item={{ item }} />
An item may look like this:
%{
name: "Thing",
options: [{"display text", "value"}]
}
I want to edit the "options" value. I need to transform it into a structure I can work with more easily. Additionally, it would be helpful if it had an ID for handling edits and deletes. The ID can be generated using Ecto.UUID.generate()
. The desired structure might look like this...
%{
id: "c9215b61-e5af-410a-a84e-fa4195931eb6",
display: "display text",
value: "value"
}
There currently doesn't appear to be a good place to perform this transformation from a prop value into the state I want to manage internally.
This could be a problem on our end but we're stumped. We're getting this warning:
warning: unexpected beginning of EEx tag "<%=" on end of expression "<%= end %>", please remove "=" accordingly
[reference to{{ end }}
line below]
with code like the following:
def render(assigns) do
~H"""
{{form = form_for @changeset, Routes.user_path(@socket, :create), [phx_change: "update", class: "form"] }}
{{ inputs_for form, :emails, fn fp -> }}
{{ email_input fp, :address }}
{{ end }}
<#Raw></form></#Raw>
"""
end
Could this be an issue with the Eex translator? I'm able to reproduce this with current master.
Thanks for your work on this, it looks amazing!
I'm experimenting with Surface on a project, and I ran into an issue with the example TextInput
component on http://surface-demo.msaraiva.io/getting_started. With the code as is, I get the error:
[error] GenServer #PID<0.1207.0> terminating
** (RuntimeError) expected TextInput.render/1 to return a %Phoenix.LiveView.Rendered{} struct
Ensure your render function uses ~L, or your eex template uses the .leex extension.
I was able to work around it by wrapping the phoenix form text_input
call with an H-sigil like:
~H"""
{{ text_input(...) }}
"""
I'm not sure if this is the best solution, and I did not see the repo for the demo site. If this is a good fix, I'd be happy to submit a PR if that repo is public.
Thanks!
First thank you for this library, I really like the abstraction and its elegance! Reading the documentation I noticed the (currently empty) UI Components entry, can you anticipate us some informations on what it will implement? It will be a separate library with a collection of independent components? It will be a sort of UI framework?
If you are already ready to give some details and guidelines, the community that is being formed around Surface could start to working on it (and I'd like to contribute, for what I can), instead of developing, each one independently, similar components.
I'm considering using this in my application; however can't find any documentation on how phx-hook interacts with the generated code. I assume that this would just work correctly as long as the attribute were added to the actual html element (i.e. not a surface component) i.e.:
defmodule Hello do
use Surface.Component
def render(assigns) do
~H"""
<span phx-hook="HelloHook">Hello, I'm a component!</span>
"""
end
end
is that accurate?
Thank you for the work you've put into this; it looks really solid!
I'm loving Surface so far, but in order to use it in my projects more reliably I think it'll need to move to Hex and concrete releases. The current approach of importing through Git means that any breakages in master
will affect everyone who runs mix deps.get
after that point. If you'd like any help with the release generation and Hex publishing process then I'd be happy to help.
When a component fails to render, a red box is rendered in it's place in a similar style as:
Error: cannot render <MyComponent> (module MyComponent could not be loaded)
However, there is no other feedback as to what is wrong. Nothing in the logs, nothing in the page, no JS console errors, nothing.
I struggled with this because the page I was working on was long and the layout was such that the component with the issue was just "not there". There were no errors, added IO.inspect
calls and the render wasn't getting hit. Wasn't until I scrolled to the bottom of the page that I saw the read box. Still, as of this time I don't know why it has a problem. Working on that.
It would be helpful if it logged out some information about:
If there is a reason to not want to generate log messages with that, then perhaps in a "dev mode only" have it log?
LiveView 0.6+ comes with support for Live Layouts; markup that can be used to wrap whatever is rendered by a LiveView. While we can currently use mount
to specify which Live Layout to use in a Surface.LiveView
, what's missing is the ability to specify the Live Layout through the using
macro as it's possible to do in Phoenix LiveView.
This pattern is useful if a project has its own using
directive for Live Views and wants to use the same Live Layout most of the time without having to specify it for every LiveView.
Similar to React but with valid Elixir syntax:
<Component {{..., props}} foo="override" />
Implement a default render/1
that automatically loads files with the same base name + .sface
and .css
.
Example:
live/
โโโ components/
โ โโโ card.css
โ โโโ card.ex
โ โโโ card.sface
โ โโโ hero.css
โ โโโ hero.ex
โ โโโ hero.sface
โ
...
For *.sface
files, the implementation is straightforward, however, for *.css
we should probably wait until we have scoped styles (#1).
I'm using a form component with context, following the Getting Started docs.
<!-- ... -->
<Form for={{ @changeset }} change="validate" autocomplete="on">
<div class="columns is-multiline">
<Field field="surname" required>
<TextInput />
</Field>
<Field field="given_name" required>
<TextInput/>
</Field>
<Field field="email" required>
<TextInput placeholder="[email protected]"/>
</Field>
</div>
</Form>
<!-- ... -->
Validations work when I arrive at the page via live_link
. However, using live_redirect
causes two weird behaviours:
live_redirect
: The form "validate" event fires, but navigating back using the browser doesn't work (I'm still on /form).live_redirect
to /form again: The form "validate" event no longer fires.I suspected that it was due to the LV Component limitations, but I'm not sure. Is it the case?
Thanks.
Hey,
I'm playing with Surface and I wonder if I can translate a string after loading it from a file?
Say I loaded this string.
"Hello World"
How can I use Surface to translate it and resolve the custom component?
Thanks
โ Mazz
Dialyzer gives The created fun has no local return
when using slots.
A page with sections:
defmodule Page do
use Surface.Component
def render(assigns) do
~H"""
<div>
<Section>
<template slot="header">
<h1>The Header</h1>
</template>
<p>The body</p>
<template slot="footer">
<h1>The Footer</h1>
</template>
</Section>
</div>
"""
end
end
defmodule Section do
use Surface.Component
slot header
slot default
slot footer
def render(assigns) do
~H"""
<div>
<div class="header"><slot name="header" /></div>
<div class="body"><slot /></div>
<div class="footer"><slot name="footer" /></div>
</div>
"""
end
end
With this setup using slots, dialyzer shows:
Total errors: 1, Skipped: 0, Unnecessary Skips: 0
done in 0m49.14s
lib/page.ex:5:no_return
The created anonymous function has no local return.
________________________________________________________________________________
done (warnings were emitted)
Halting VM with exit status 2
Since dialyzer often has esoteric and cryptic errors I'm not really sure where this is coming from. Any guidance would be greatly appreciated.
Similar to property
and data
. The goal is to add a new context
function so we can provide a safer way to use contexts. The new API should:
defmodule Form do
...
@doc "The form data"
property for, :any, required: true
@doc "The Form struct created by the <Form/> component"
context :set, form
def begin_context(props, context) do
form = form_for(props.for, ...)
Map.put(context, :form, form)
end
def render(assigns) do
~H"""
{{ @form }}
...
"""
end
...
end
Pay attention that form
was automatically merged into the assigns. The @context.form
is no longer available. Anything available to the user must be in the assigns
and must have been explicitly declared using the API. Also notice that the begin_context
now receives the context
as an extra argument since props.context
will no longer be available.
defmodule Field do
...
### Public
@doc "The field name"
property field, :string, required: true
@doc "The field name specified in the <Field/> component"
context :set, field, :string, scope: :children
### Private
context :get, form, from: Form
def begin_context(props, context) do
field = String.to_atom(props.field)
Map.put(context, :field, field)
end
def render(assigns) do
~H"""
<div class="field">
{{ label(@form, @field, class: "label") }}
...
</div>
"""
end
...
end
The scope: :children
option informs that the context variable field
will only be available to its children, otherwise it would override the existing field
property. The static checker will not allow overriding existing properties or data and should raise an error. The goal is to force the user to be explicit about the origin of anything present in the assigns.
defmodule TextInput do
...
context :get, form, from: Form
context :get, field, from: Field
def render(assigns) do
~H"""
{{ text_input(@form, @field, ...) }}
"""
end
end
Feel free to send any comment or suggestion you might have.
@brainlid since you already got your hands dirty with contexts before, I'd love to hear your thoughts on this one :)
Cheers.
<div class={{ "m-1.5": true }}>
</div>
Currently generates the following html:
<div class="m-1/5">
</div>
However, I'd expect for it to generate the following html:
<div class="m-1.5">
</div>
The cause is this function in surface.ex (line 301):
# TODO: Find a better way to do this
defp to_kebab_case(value) do
value
|> to_string()
|> Macro.underscore()
|> String.replace("_", "-")
end
Something like the following might give similar functionality without relying on Macro.underscore()
:
defp to_kebab_case(value) do
value
|> to_string()
|> String.replace(~r/([a-z])([A-Z0-9])/, "\\1-\\2")
|> String.downcase()
end
and should fix this specific issue; however I'm not sure if any code is relying on anything else that Macro.underscore()
does as I'm not familiar with that.
For more context on why this is an issue for me, I'm using tailwind css which has class names that include periods.
The child does not fetch the context if rendered separately from the context provider:
defmodule Parent do
use Surface.Component
slot default
def render(assigns) do
~H"""
<div>
<Parent.ContextProvider foo="bar">
<slot />
</Parent.ContextProvider>
</div>
"""
end
end
defmodule Parent.ContextProvider do
use Surface.Component
property foo, :string
context set foo, :string, scope: :only_children
slot default
# The foo prop is passed here and so we can use it
def init_context(assigns) do
{:ok, foo: assigns.foo}
end
def render(assigns) do
~H"""
<slot />
"""
end
end
defmodule Child do
use Surface.Component
context get foo, from: Parent.ContextProvider
def render(assigns) do
# @foo is nil here
~H"""
<div>{{ @foo }}</div>
"""
end
end
defmodule ExampleWeb.ContextLive do
use Surface.LiveView
def render(assigns) do
~H"""
<Parent>
<Child/>
</Parent>
"""
end
end
If we render it all at once in either the live view or a component, the context works as expected. It's only once they are broken apart this issue arises.
Child components can consume context from a parent rendered elsewhere.
We get a BadArityError
when a parent component uses :props={{ assigns }}
on the slot or calls @inner_content.(assigns)
in its render function and the live view does not use :let
on the parent.
I think this is related to #2 in that I am trying to pass on props from a parent to a child. The only way I can seem to get it to work is if the live view author knows about these props in advance and uses the :let
directive on the parent. This is problematic since some props are optional and requires the live view author to have intimate knowledge of some implementation details of each component. I'm trying to avoid this since I am working on creating a stylized component library with built-in components.
Parent component passing props on:
defmodule Parent do
use Surface.Component
slot default, required: true, props: [:foo]
def render(assigns) do
~H"""
<div>
<slot :props={{ foo: "bar" }} />
</div>
"""
end
end
Child component with a prop:
defmodule Child do
use Surface.Component
property foo, :string
def render(assigns) do
~H"""
<div>{{ @foo }}</div>
"""
end
end
Live view without using let
:
defmodule ExampleWeb.PropsLive do
use Surface.LiveView
def render(assigns) do
~H"""
<Parent>
<Child />
</Parent>
"""
end
end
The error we get:
** (exit) an exception was raised:
** (BadArityError) #Function<2.26325794/0 in Surface.ContentHandler.default_content_fun/3> with arity 0 called with 1 argument ([foo: "bar"])
(surface_antd 0.1.0) lib/parent.ex:9: Parent."render (overridable 1)"/1
(phoenix_live_view 0.8.1) lib/phoenix_live_view/utils.ex:332: Phoenix.LiveView.Utils.render_view/2
(phoenix_live_view 0.8.1) lib/phoenix_live_view/utils.ex:91: Phoenix.LiveView.Utils.to_rendered/2
(phoenix_live_view 0.8.1) lib/phoenix_live_view/diff.ex:233: Phoenix.LiveView.Diff.traverse/5
(phoenix_live_view 0.8.1) lib/phoenix_live_view/diff.ex:288: anonymous fn/3 in Phoenix.LiveView.Diff.traverse_dynamic/5
(elixir 1.10.2) lib/enum.ex:2111: Enum."-reduce/3-lists^foldl/2-0-"/3
(phoenix_live_view 0.8.1) lib/phoenix_live_view/diff.ex:221: Phoenix.LiveView.Diff.traverse/5
(phoenix_live_view 0.8.1) lib/phoenix_live_view/diff.ex:92: Phoenix.LiveView.Diff.render/3
(phoenix_live_view 0.8.1) lib/phoenix_live_view/static.ex:275: Phoenix.LiveView.Static.to_rendered_content_tag/4
(phoenix_live_view 0.8.1) lib/phoenix_live_view/static.ex:137: Phoenix.LiveView.Static.render/3
(phoenix_live_view 0.8.1) lib/phoenix_live_view/controller.ex:35: Phoenix.LiveView.Controller.live_render/3
(phoenix 1.4.14) lib/phoenix/router.ex:288: Phoenix.Router.__call__/2
(example 0.1.0) lib/example_web/endpoint.ex:1: ExampleWeb.Endpoint.plug_builder_call/2
(example 0.1.0) lib/plug/debugger.ex:130: ExampleWeb.Endpoint."call (overridable 3)"/2
(example 0.1.0) lib/example_web/endpoint.ex:1: ExampleWeb.Endpoint.call/2
(phoenix 1.4.14) lib/phoenix/endpoint/cowboy2_handler.ex:42: Phoenix.Endpoint.Cowboy2Handler.init/4
(cowboy 2.7.0) /Users/mathew/example/deps/cowboy/src/cowboy_handler.erl:41: :cowboy_handler.execute/2
(cowboy 2.7.0) /Users/mathew/example/deps/cowboy/src/cowboy_stream_h.erl:320: :cowboy_stream_h.execute/3
(cowboy 2.7.0) /Users/mathew/example/deps/cowboy/src/cowboy_stream_h.erl:302: :cowboy_stream_h.request_process/3
(stdlib 3.12.1) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
Live view using :let
directive on the parent and passing prop to child:
defmodule ExampleWeb.PropsLive do
use Surface.LiveView
def render(assigns) do
~H"""
<Parent :let={{ foo: foo }}>
<Child foo={{ foo }} />
</Parent>
"""
end
end
The above example works but what I'm really trying to do, though, and why I ran into this is to have dynamic props. The dream is to have the child's properties initialized by the parent passing them on and using :let
be optional since some or all may not need to be set by the user. I realize that this maybe a slightly different issue and that dynamic props as a feature are still a work in progress.
With guidance, I could take a crack at a PR to either make using :let
optional or even dynamic props since I think the dynamic props implementation may also address this issue.
After upgrading to commit bb7d1e9 from previous commit, tests that use render_component/2
started failing for components with use Surface.Component
or use Surface.LiveComponent
, but work if that line is changed to use Phoenix.LiveComponent
.
defmodule SomeComponent do
use Surface.Component
def render(assigns) do
~L"""
<div>Hello</div>
"""
end
end
render_component(SomeComponent, %{
__surface__: %{groups: %{__default__: %{binding: false, size: 0}}}
})
Test output:
** (BadMapError) expected a map, got: nil
code: render_component(SomeComponent, %{
stacktrace:
(elixir 1.10.1) lib/map.ex:764: Map.split(nil, [:__default__])
(surface 0.1.0-alpha.0) lib/surface/content_handler.ex:23: Surface.ContentHandler.init_contents/1
test/test_project_web/live/user/some_test.exs:1: SomeComponent.render/1
(phoenix_live_view 0.11.0) lib/phoenix_live_view/utils.ex:101: Phoenix.LiveView.Utils.to_rendered/2
(phoenix_live_view 0.11.0) lib/phoenix_live_view/test/live_view_test.ex:393: Phoenix.LiveViewTest.__render_component__/3
Possibly related: #21
Hi @msaraiva , I updated the latest commit from master and got the following error.
** (BadMapError) expected a map, got: nil
Just tried it in data_component_test.exs
defmodule StatefulComponent do
use Surface.LiveComponent
def update(assigns, socket) do
{:ok, assign(socket, test: "test")}
end
def render(assigns) do
~H"""
<div>Stateful</div>
"""
end
end
and running the test will output
** (BadMapError) expected a map, got: nil
code: {:ok, _view, html} = live_isolated(build_conn(), ViewWithNoBindings)
stacktrace:
(elixir) lib/map.ex:744: Map.split(nil, [:__default__])
(surface) lib/surface/content_handler.ex:21: Surface.ContentHandler.init_contents/1
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.