In attempt to build a functioning app with ratchet, a number of edge cases have been encountered. This leads me to believe that we may need to rethink it's design.
Overview
Fundamentally ratchet implements a protocol for binding data to HTML elements. There are only two places where data may be stored in an HTML document:
- element attributes
- element content
Protocol
Previously, we had the concept of scopes and properties. A scope represented an area of concern, or domain, in the view. A scope might have a number of properties which data would be bound to. I propose we simplify this to just properties. Scopes are more or less a semantic which could be introduced later as a synonym for properties.
<!-- Previously -->
<article data-scope="post">
<p data-prop="body"></p>
</article>
<!-- Now -->
<article data-prop="post">
<p data-prop="body"></p>
</article>
Edges
The following edge cases were encoutered while trying to translate existing apps with ratchet. These scenarios cannot be solved with the current version of ratchet (0.2.0).
Case # 1
Updating only the attributes of an element. For e.g., you have an anchor tag with static text content. Only the href
needs be bound when rendered.
<!-- Template -->
<a>Click here!</a>
<!-- Rendered -->
<a href="https://iamvery.com" rel="nofollow">Click here!</div>
Case # 2
Updating attributes of an element as well as properties within its content. For e.g., you have a form tag that must have data bound to properties in its content as well as action
and method
attributes when rendered.
<!-- Template -->
<form>
<input type="text">
<button type="submit">Submit</button>
</form>
<!-- Rendered -->
<form action="..." method="post">
<input type="text" name="full_name">
<button type="submit">Submit</button>
</form>
Case # 3
Currently there is no way to compose views from other files. For e.g., the application layout is in one file and the view specific to the particular page is in another file to be included. I'm on the fence whether this is really a responsibility of ratchet. On one hand, a claim of the lib is writing plain HTML views so that designers can easy prototype things. On the other hand, any including of external HTML breaks that whole idea anyway.
At this time, I'm thinking we just shell out to EEx and call render
.
Case # 4
There may be a case where it is desirable to prepend/append content on an element rather than overwriting it. For e.g. a designer would have no reason to implement hidden form elements such as CSRF token as they do not affect the view.
<!-- Template -->
<form>
<input type="text">
<button type="submit">Submit</button>
</form>
<!-- Rendered -->
<form>
<input type="hidden" name="csrf" value="...">
<input type="text">
<button type="submit">Submit</button>
</form>
However, I don't think it's the responsibility of ratchet to generate to HTML content. IMO the developer should be able to retroactively add elements to the view and bind data to them as expected.
Transformations
There are 5 transformations which may take place in the view.
- Element content
- Element attributes
- Element content and attributes
- Multiple elements (recursively applying 1-3 above)
- Attributes and nested elements
Element content
<!-- Template -->
<p data-prop="body"></p>
<!-- Rendered -->
<p data-prop="body">Content has been inserted</p>
Element attributes
<!-- Template -->
<a data-prop="link">Click here!</a>
<!-- Rendered -->
<a href="https://iamvery.com" rel="nofollow" data-prop="link">Click here!</div>
Element content and attributes
<!-- Template -->
<a data-prop="link"></div>
<!-- Rendered -->
<a href="https://iamvery.com" rel="nofollow" data-prop="link">iamvery</div>
Multiple elements
<!-- Template -->
<article data-prop="post"></article>
<!-- Rendered -->
<article data-prop="post">...</article>
<article data-prop="post">...</article>
<article data-prop="post">...</article>
Attributes and nested elements
<!-- Template -->
<form data-prop="new_comment">
<input data-prop="body">
</form>
<!-- Rendered -->
<form action="/comments" method="post" data-prop="new_comment">
<input name="comment[body]" data-prop="body">
</form>
Data
As previously stated, there are two places for data in the view: content and attributes. Therefore there are two basic types of data: strings and keyword lists. These basic types may be combined into complex data structures to support complex views.
Consider the following prototype template for some posts with comments:
<article data-prop="posts">
<h2>
<a href="#" data-prop="title">Post Title</a>
</h2>
<p data-prop="body">Thoughts and opinions.</p>
<a href="#" data-prop="permalink">Permalink</a>
<ul>
<li data-prop="comments">
<span data-prop="user">Jay</span>
<span data-prop="body">Yes and no</span>
</li>
</ul>
<form data-prop="new_comment">
<input data-prop="body">
<button type="submit">New comment</button>
</form>
</article>
When rendered with data, the final view would look like the following:
<article data-prop="posts">
<h2><a href="/posts/1" data-prop="title">A good post</a></h2>
<p data-prop="body">This post is about things.</p>
<a href="/posts/1" data-prop="permalink">Permalink</a>
<ul>
<li data-prop="comments">
<span data-prop="user">Jon</span>
<span data-prop="body">Good read.</span>
</li>
<li data-prop="comments">
<span data-prop="user">Les</span>
<span data-prop="body">Can you even?</span>
</li>
</ul>
<form action="/comments" method="post" data-prop="new_comment">
<input name="comment[body]" data-prop="body">
<button type="submit">New comment</button>
</form>
</article>
The data:
data = [
posts: [
title: {"A good post", href: "/posts/1"},
body: "This post is about things.",
permalink: [href: "/posts/1"],
comments: [
[user: "Jon", body: "Good read."],
[user: "Les", body: "Can you even?"],
],
new_comment: {
[body: [name: "comment[body]"]],
action: "/comments", method: "post",
}
]
]
Let's talk about the data...
String, e.g. post body
The body of the post is plain text content.
Data:
Applied:
<p data-prop="body">This is the post body</p>
Keyword List, e.g. post permalink
Only attributes need be bound on the permalink element. The keyword list has the general form [attribute: value]
.
Data:
Applied:
<a href="/posts/1" data-prop="permalink">Permalink</a>
Tuple, e.g. post title
Both the content and attributes of the title are updated. The tuple has the general form {content, attributes}
.
Data:
{"A good post", href: "/posts/1"}
Applied:
<a href="/posts/1" data-prop="title">A good post</a>
List, e.g. comments
A regular list is used as a collection of property data. The general form is [data, ...]
Data:
Applied:
<li data-prop="comments">...</li>
<li data-prop="comments">...</li>
Map, e.g. a post
Maps are used as a collection of properties to their data. They have the general form %{property: data, ...}
Data:
%{
body: "...",
permalink: [...],
comments: [...],
new_comment: {...},
}
Applied:
<article data-prop="post">...</article>
Combined concepts, e.g. new comment form
Both the content and attributes of the form must be updated, so a tuple is used. In this particular case, the content is recursively updated with additional properties.
Data:
{%{body: [name: "comment[body]"]}, action: "/comments", method: "post"}
Applied:
<form action="/comments" method="post" data-prop="new_comment">
<input name="comment[body]" data-prop="body">
</form>
EEx
An implementation detail of Ratchet is that it uses EEx as an intermediary format. Provided for example is the above template compiled to EEx:
<%= for post <- List.wrap(data.posts) do %>
<article <%= Ratchet.Data.attributes(post, [{"data-prop", "posts"}]) %>>
<%= if Ratchet.Data.content?(post) do %>
<%= Ratchet.Data.content(post) %>
<% else %>
<h2>
<%= for title <- List.wrap(post.title) do %>
<a <%= Ratchet.Data.attributes(title, [{"href", "#"}, {"data-prop", "title"}]) %>>
<%= if Ratchet.Data.content?(title) do %>
<%= Ratchet.Data.content(title) %>
<% else %>
Post Title
<% end %>
</a>
<% end %>
</h2>
<%= for body <- List.wrap(post.body) do %>
<p <%= Ratchet.Data.attributes(body, [{"data-prop", "body"}]) %>>
<%= if Ratchet.Data.content?(body) do %>
<%= Ratchet.Data.content(body) %>
<% else %>
Thoughts and opinions.
<% end %>
</p>
<% end %>
<%= for permalink <- List.wrap(post.permalink) do %>
<a <%= Ratchet.Data.attributes(permalink, [{"data-prop", "permalink"}]) %>>
<%= if Ratchet.Data.content?(permalink) do %>
<%= Ratchet.Data.content(permalink) %>
<% else %>
Permalink
<% end %>
</a>
<% end %>
<ul>
<%= for comments <- List.wrap(post.comments) do %>
<li <%= Ratchet.Data.attributes(comments, [{"data-prop", "comments"}]) %>>
<%= if Ratchet.Data.content?(comments) do %>
<%= Ratchet.Data.content(comments) %>
<% else %>
<%= for user <- List.wrap(comments.user) do %>
<span <%= Ratchet.Data.attributes(user, [{"data-prop", "user"}]) %>>
<%= if Ratchet.Data.content?(user) do %>
<%= Ratchet.Data.content(user) %>
<% else %>
Jay
<% end %>
</span>
<% end %>
<%= for body <- List.wrap(comments.body) do %>
<span <%= Ratchet.Data.attributes(body, [{"data-prop", "body"}]) %>>
<%= if Ratchet.Data.content?(body) do %>
<%= Ratchet.Data.content(body) %>
<% else %>
Yes and no
<% end %>
</span>
<% end %>
<% end %>
</li>
<% end %>
</ul>
<%= for new_comment <- List.wrap(post.new_comment) do %>
<form <%= Ratchet.Data.attributes(new_comment, [{"data-prop", "new_comment"}]) %>>
<%= if Ratchet.Data.content?(new_comment) do %>
<%= Ratchet.Data.content(new_comment) %>
<% else %>
<%= for body <- List.wrap(new_comment.body) do %>
<input <%= Ratchet.Data.attributes(body, [{"data-prop", "body"}]) %>>
<% end %>
<button type="submit">New comment</button>
<% end %>
</form>
<% end %>
<% end %>
</article>
<%= end %>
(๐ฑ WHAT HAVE I DONE)