Coder Social home page Coder Social logo

surface's People

Contributors

adrianomitre avatar alexandrubagu avatar brainlid avatar cvkmohan avatar davydog187 avatar edgarmiadzieles avatar guisehn avatar harmon25 avatar herminiotorres avatar joshprice avatar lnr0626 avatar maennchen avatar malian avatar mathewdgardner avatar mazz-seven avatar mhanberg avatar miguel-s avatar msaraiva avatar oskarkook avatar ouven avatar paulstatezny avatar rubysolo avatar rudolfman avatar samuelpordeus avatar sekunho avatar shritesh avatar tiagoefmoraes avatar tschmidleithner avatar wrren avatar zamith avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

surface's Issues

Event directives on built in components

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?

LiveComponent/LiveView API Design Question

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?

Compiler warning on defmodule line with catch-all handle_event

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.

Documentation shows usage of "Button" but doesn't exist

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.

Components that render `@inner_content` generate a KeyError when no inner content is specified.

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.

Drop camelCase to kebab-case auto-conversion for CSS classes

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.

Template parsing issue with handlebars in a string

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.

Add directive :show

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)

Scoped CSS style

  1. 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?

  2. 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>
  3. 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.

Attributes starting with "@" conflict with Surface

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

Using Raw without alias requires a mix.clean to recover

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.

`ArgumentError` in `Surface.API.generate_docs` when compiling a component with `@moduledoc false`

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:
image

Expected Behavior

The project should compile successfully.

Current Behavior

Compilation fails.

Possible Solution

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 @moduledocattribute was not specified at all.

Steps to Reproduce

  1. Create a mix project
  2. Add Surface as a dependency
  3. Create a module with using Surface.Component and @moduledoc false
  4. Run mix compile

Context (Environment)

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!

Unable to nest a stateful component inside a Form

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

Rendering dynamic component

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

Non-closing tags with directive fail.

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>

Unable to use guard in `handle_event/3`

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

Events passed as non-required properties cause a runtime exception when used but unset.

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.

Unicode conversion error on attribute value

Versions

  • elixir: 1.10.2
  • surface: 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

Unable to call `send_update`

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 some Phoenix helpers within curly braces

Cannot use Phoenix HTML helpers with do blocks with {{ }}.

Environment

  • Elixir version: 1.10.2
  • OTP version: 22.3.2
  • Phoenix version: 1.4.16
  • Phoenix LiveView version: 0.11.0
  • Operating system: macOS Catalina 10.15.4

Contrived Example

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!

Add built-in form components

Create built-in context friendly form components around Phoenix helpers.

List of Phoenix built-in form helpers

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.

Add data to the API

Similar to property, I want to add data and context functions so we can benefit from static checking and better tooling. Proposal:

Data (same concept of Vue.js)

data show, :boolean, default: false

The :default option could minimise the need to implement mount/3.

Context (same existing concept, but more restrict and explicit)

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

Question: is there an equivalent for <% 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?

Define new API for events to handle phx-target

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:

  1. define a root element for the component
  2. set the id attribute of the component's root element
  3. set the phx-* events on the elements you need (e.g, phx-click)
  4. set the phx-target on any of those elements that you want to handle the events locally

Example:

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:

  1. automatically define a property id for any Surface.LiveComponent
  2. override the id if the user passes a custom identifier
  3. automatically add the id to the root element
  4. define a marker that indicates that the event should be handled by the parent live view or by any other component

Example:

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.

Un-terminated markup codes like <hr> cause issues

This markup was copied from an EEx template into a ~H one.

image

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.

Do not generate attributes for HTML tags if the values are `nil`

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 sets class="".

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.

ability to transform a prop into desired state structure?

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.

Eex translation issue with `inputs_for`

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.

Error with TextInput on surface-demo / getting started

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!

UI Components (WIP)

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.

phx-hook question

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!

Migrate to Hex and Releases

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.

red box of "cannot render" isn't helpful and may hide issues

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:

  • there being a problem in the first place
  • any indication as to the nature of the problem

If there is a reason to not want to generate log messages with that, then perhaps in a "dev mode only" have it log?

Support for Live Layouts

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.

Dynamic props

Similar to React but with valid Elixir syntax:

<Component {{..., props}} foo="override" />

Support loading external templates and css files

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).

Form component does not trigger phx-change when navigated to from live_redirect

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:

  1. Navigating from /home to /form via live_redirect: The form "validate" event fires, but navigating back using the browser doesn't work (I'm still on /form).
  2. Full page refresh on /form, navigating back to /home, then clicking on 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.

Question: can I translate a string?

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 no local return when using slots

Dialyzer gives The created fun has no local return when using slots.

Environment

  • Elixir version: 1.10.2
  • OTP version: 22.3.2
  • Phoenix version: 1.4.16
  • Phoenix LiveView version: 0.11.0
  • Operating system: macOS Catalina 10.15.4

Example

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.

New context API [Proposal]

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:

  1. Classify added variables by parent component, so children cannot accidentally override their values
  2. Allow child components to specify just the variables they need
  3. Merge context variables directly into the assigns
  4. Allow static checking
  5. Consider an added context variable part of the public interface, forcing the author to treat it as such (encourage him to provide documentation for it, etc.)
  6. Export all information regarding contexts for later introspection. This way tools like ElixirSense can provide features like autocomplete, Go-to-definition and documentation preview for various editors.

Example

Form

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.

Field

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.

TextInput

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.

css_class transforms periods in class names into slashes

<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.

Allow passing context assigns to slot content

Current behavior

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.

Expected behavior

Child components can consume context from a parent rendered elsewhere.

Cannot use slot props without using let in the live view

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.

Surface.Component raises (BadMapError) expected a map, got: nil in render_component

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

Surface.LiveComponent - Raise (BadMapError) expected a map, got: nil if a component defined update/2 callback

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

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.