Conversation
|
sounds very cool |
|
Let me know if you want to pair on this, I could use an introduction to the API. |
|
I'm not so sure we need the last few checkbox items right now, to be honest. Rerendering the entire app every time hasn't been a bottleneck in anything I've built yet, so I'd like to go ahead and talk about merging this in. Controllers/views and the old HTML-based components are goneThe concepts of components and controller/view don't seem to mesh really well, though I admit I haven't spent much time trying to make that happen. I'm happy to be wrong, but I can also say with reasonable confidence that HTML and the virtual DOM definitely do not mix. They're two different representations and you can't really combine them well. Components are now the routing targets:require 'clearwater/router'
router = Clearwater::Router.new do
route 'blog' => BlogComponent.new
endComponent#outletThe components have an Permanent vs ephemeral stateThe main difference is that, while routed components are persisted (they're instantiated with the router), subcomponents instantiated by the routed components are not, so you can store state on routed components but you'll either need to pass that state down to instantiated subcomponents or store those components, as well: require 'clearwater/component'
class BlogComponent
include Clearwater::Component
def initialize
@title = 'My Blog'
end
def render
div({id: 'blog'}, [
blog_header,
ArticleIndex.new,
])
end
def blog_header
@blog_header ||= BlogHeader.new(@title)
end
endEvery instance variable stored on this component will stick around for the life of the app. Notice how we store the blog's header component inside the Event handlingIn the old views and HTML-based components, we used class ArticleIndex
include Clearwater::Component
attributes :active
def render
ul({id: 'article-index'}, articles.map { |article| ArticleIndexItem.new(article, index: self) })
end
end
class ArticleIndexItem
include Clearwater::Component
def initialize(article, index:)
@article = article
@index = index
end
def render
li({ onclick: method(:set_active) }, [
h4(nil, article.title),
])
end
def set_active
@index.active = article.id
end
endOne primary difference is that React fires an Event handlersThe value you pass as the event handler ( However, we aren't limited to the usual proc/lambda-type things. Event handlers can be any object that responds to class NewUserRegistration
attr_reader :form
def initialize(form)
@form = form
end
def call
# Data stores aren't a thing yet, but clap your hands if you believe.
UserStore.create!(email: form.email, password: form.password)
end
end
class RegistrationForm
include Clearwater::Component
attr_reader :email, :password
def render
form(({onsubmit: NewUserRegistration.new(self)}, [
input(type: 'email', onkeyup: method(:set_email), placeholder: 'me@example.com'),
input(type: 'password', onkeyup: method(:set_password), placeholder: 'password'),
input(type: 'submit', value: 'Register'),
])
end
def set_email(event)
@email = event.target.value
end
def set_password(event)
@password = event.target.value
end
endI'm using bound methods here to set ivars on the form whenever the form inputs are updated, but the submit handler for the form is a service object that responds to As you might have noticed from the bound-method handlers, the AttributesBuilding on the model/view bindings that this PR deletes, components have the concept of "attributes". You define them in the component class body: class ArticleIndex
include Clearwater::Component
attributes :active
endWhenever you change this with Render throttlingBecause rerendering constantly can absolutely wreck performance and you may need to modify multiple attributes sequentially, changing several attributes rapid-fire will actually throttle rendering to at most once per 1/60th of a second. Changing the first attribute will eagerly fire a rerender, but subsequent attribute modifications will schedule at most one more to happen in 1/60th of a second or so (or whenever the current queue of callbacks is finished). This means that even if you change 10 attributes in a single callback, you will rerender at most twice. Default renderingIf you don't specify a The time this helps the most is when you're getting your routes setup. You have to provide routing targets. router = Clearwater::Router.new do
route 'blog' => Blog.new do
route ':id' => ArticleDisplay.new
end
route 'about' => About.new
route 'contact' => ContactPage.new
endBut those all have to be valid Clearwater components, so we can stub them out: class Blog
include Clearwater::Component
end
class ArticleDisplay
include Clearwater::Component
end
class About
include Clearwater::Component
end
class ContactPage
include Clearwater::Component
endThe Link componentThis is one of my favorite things. With this PR, we no longer trap all link clicks and use some arcane incantation to determine whether or not to handle it in the framework or let it pass to the browser. Instead, we provide a I came across this as a problem in the original link-trapping implementation because the app was only for the admin UI, but links to the homepage, which was not a Clearwater app, were still getting trapped. Using The class ArticleIndexItem
include Clearwater::Component
def render
li(nil,
Link.new({href: "/blog/#{article.id}"}, article.title)
)
end
end |
|
If routers target instantiated components, how do components get request payloads? To clarify: Wouldn't it be more flexible to allow passing request payloads: Clearwater::Router.new do
route 'blog' => Blog
end
class Blog
def initialize(payload)
@id = payload["id"]
end
end |
|
Oh, a few more things I wanted to talk about: React-style … er, styleYou can specify the styles for your components programmatically, a la React: class Foo
include Clearwater::Component
def render
div({ style: random_background_color }, 'Hello, World!')
end
# Transition the background color each time the app gets rerendered.
def random_background_color
{
background_color: "##{rand(16 ** 6).to_s(16)}",
transition: 'all 500ms',
}
end
endI'd kinda like to build some sort of Notice the style attributes like Components as gemsWith programmatic styles, you can gemify your components more easily because users of your components won't need to include your stylesheets in their apps. I've built a couple I'd like to use as examples. |
|
If I end up making a CSS gem like I did the HTML gem this will fit in very well. |
They don't. Why do you ask? |
Shouldn't they? Since we don't have the concept of a Control anymore, how do we pass state between routes? |
|
Oh, do you mean how controllers had URL-based I thought you meant like HTTP POST payloads. |
|
To be fair I do mean like HTTP POST payloads. I've been playing around with the idea that Earhart merges all data structures in an HTTP request: URL path pattern, Query, Headers, and Body. Merged right of course. So basically the component is acting as presenter and the router is acting as router and control? |
|
Well, HTTP POST should probably only be happening as XHR. If a server is serving up the Clearwater app as the response for a POST request, I'd posit that that app is doing the wrong thing since the POST would be re-sent if the user refreshes the browser. The idea is that the URL determines the app state, similar to how Ember works. Barring any permissions discrepancies, if I give you a link to any point inside a Clearwater app, it should show you what it's showing me. Any other data from the request would need to be passed from the server to Clearwater, probably via |
|
This probably bears further discussion, but frankly I'm just excited to use On Sun, Apr 26, 2015 at 5:12 PM Jamie Gaskins notifications@github.com
|
|
I created an example app by porting a React tutorial's app over to Clearwater's virtual DOM: Demo: http://clearwater-mailbox.herokuapp.com Since I've been able to at the very least duplicate the capability of what's on |
The virtual DOM is a pretty hot topic. I've found Matt Esch's
virtual-domlibrary and wrapped it in some Opal bindings to let us re-render the app when needed. It supplies four main functions to do this:h()creates the virtual nodes. They have a similar JS API to DOM elements.createElement()turns virtual DOM nodes into actual DOM nodes.diff()compares two trees and generates a set of patches.patch()applies the patch set from thediff()function to a tree.virtual-domlibrary with Opalvirtual-dom'sh(),createElement()Clearwater::Applicationto understandvirtual-domcomponentsvirtual-domwrapper for stateful componentsdata-react-id="0.0.1.2.$key"maybe?)The reason I want to remove controllers and views is so that we don't have to support two different development styles. They don't seem to be compatible — in the old style, the
Clearwater::Applicationhad to set up event handlers on allaelements, whereas with the virtual DOM that is handled by theLinkcomponent.I'll update the checklist above as I think of more things that are needed.