Dennis Hackethal’s avatar

Dennis Hackethal

@dennis.hackethal​·​Joined Jun 2024​·​2,804 ideas: 1,840 adoptable, 964 rejectable
Log in or sign up to follow Dennis or post on their wall.

Founder Veritula.
Author. Software engineer. Ex Apple. Translator of The Beginning of Infinity.
dennishackethal.com

 User
Registered their account.
 Initiator
Started their first discussion.
 Engager
Participates in three or more discussions.
 Novice
Posted their first idea.
 Beginner
Posted their 10th idea.
 Intermediate
Posted their 50th idea.
 Advanced
Posted their 100th idea.
 Critic
Criticized their first idea.
 Private
Criticized their 10th idea.
 Lieutenant
Criticized their 50th idea.
 Captain
Criticized their 100th idea.
 Defender
Addressed their first criticism.
 Shield
Addressed their 10th criticism.
 Watchman
Addressed their 50th criticism.
 Copy editor
Created their first revision.
 Assistant editor
Created their 10th revision.
 Associate editor
Created their 50th revision.
 Professional
Posted their 500th idea.
 Deputy editor
Created their 100th revision.
 Colonel
Criticized their 500th idea.
 Master
Posted their 1000th idea.
 Bulwark
Addressed their 500th criticism.
 General
Criticized their 1000th idea.
 Editor
Created their 500th revision.
 Fortress
Addressed their 1000th criticism.
  Dennis Hackethal commented on idea #5858.

Make Hiccdown reactive, like Reagent: a helper method as the first element marks a component, as in [method(:product_card), @product]. A Method knows its module and name, so a later request can call ProductsHelper#product_card again for the same records.

The server keeps track of which components each page rendered, with which records, and gives each component's element the id dom_id(record), as turbo streams do. When a record changes (after_commit, checking that it actually changed), the server tells the pages showing it, over Action Cable. Each page then requests just that component, which renders in the viewer's own request, with their user, session, locale, time zone and CSRF token, and swaps it in with a turbo stream.

Rendering the component in the request that changed the record would save that round trip, but that request has none of the viewers' context: it would render for the wrong user, or have to rebuild each viewer's context and render once per viewer.

A first pass can skip checking whether a change affects what a component shows, as when a product's name changes on a page that only shows its price.

#5858​·​Dennis HackethalOP revised 1 day ago​·​AI-assisted

Measured on Veritula locally, on a discussion of 213 ideas:

  • The whole discussion page, which a Turbo refresh re-renders: 211 ms and 506 KB, of which loading the whole tree (Idea.tree) takes 18 ms and Hiccdown.to_html 35 ms.
  • One idea's own markup, without its replies: 0.4 ms (median; 0.9 ms at the 90th percentile).
  • One idea's HTML with its replies: 3.3 KB (median).

A component request would cost the request's fixed overhead, loading that idea's part of the tree (18 ms at most) and about a millisecond of rendering: an estimated 10–30 ms and a few KB, about ten times faster and a hundred times smaller than the whole page. The whole page grows with the discussion while a component stays about the same, and the browser morphing 500 KB of DOM, which I didn't measure, comes on top.

  Dennis Hackethal revised idea #5853. The revision addresses idea #5856.

Mark components with Method objects, which later requests can call again, instead of procs


Make Hiccdown reactive, like Reagent: a proc as the first element marks a component, as in [->(product) { [:li, product.name] }, @product].

The server keeps track of which components each page rendered, with which records, and gives each component's element the id dom_id(record), as turbo streams do. When a record changes (after_commit, checking that it actually changed), the server tells the pages showing it, over Action Cable. Each page then requests just that component, which renders in the viewer's own request, with their user, session, locale, time zone and CSRF token, and swaps it in with a turbo stream.

Rendering the component in the request that changed the record would save that round trip, but that request has none of the viewers' context: it would render for the wrong user, or have to rebuild each viewer's context and render once per viewer.

A first pass can skip checking whether a change affects what a component shows, as when a product's name changes on a page that only shows its price.

Make Hiccdown reactive, like Reagent: a helper method as the first element marks a component, as in [method(:product_card), @product]. A Method knows its module and name, so a later request can call ProductsHelper#product_card again for the same records.

The server keeps track of which components each page rendered, with which records, and gives each component's element the id dom_id(record), as turbo streams do. When a record changes (after_commit, checking that it actually changed), the server tells the pages showing it, over Action Cable. Each page then requests just that component, which renders in the viewer's own request, with their user, session, locale, time zone and CSRF token, and swaps it in with a turbo stream.

Rendering the component in the request that changed the record would save that round trip, but that request has none of the viewers' context: it would render for the wrong user, or have to rebuild each viewer's context and render once per viewer.

A first pass can skip checking whether a change affects what a component shows, as when a product's name changes on a page that only shows its price.

  Dennis Hackethal addressed criticism #5855.

Turbo 8 already does most of this. With broadcasts_refreshes and morphing, each viewer's page re-renders in their own request when a record changes, keeping scroll position and form state. Components would only save rendering and sending the rest of the page.

#5855​·​Dennis HackethalOP, 1 day ago​·​AI-assisted

On large pages, like Veritula's discussions, that's too expensive: a refresh re-renders the whole page for every viewer, sends all of it, and morphs all of it, for a change to one idea. Components re-render, send and morph only what changed, so they'd give the same result as morphing on pages where morphing the whole page is too slow to use.

  Dennis Hackethal criticized idea #5853.

Make Hiccdown reactive, like Reagent: a proc as the first element marks a component, as in [->(product) { [:li, product.name] }, @product].

The server keeps track of which components each page rendered, with which records, and gives each component's element the id dom_id(record), as turbo streams do. When a record changes (after_commit, checking that it actually changed), the server tells the pages showing it, over Action Cable. Each page then requests just that component, which renders in the viewer's own request, with their user, session, locale, time zone and CSRF token, and swaps it in with a turbo stream.

Rendering the component in the request that changed the record would save that round trip, but that request has none of the viewers' context: it would render for the wrong user, or have to rebuild each viewer's context and render once per viewer.

A first pass can skip checking whether a change affects what a component shows, as when a product's name changes on a page that only shows its price.

#5853​·​Dennis HackethalOP revised 1 day ago​·​AI-assisted

Procs can't be found again in a later request. The lambda a helper built while rendering the page is gone by the time the page asks for the component, so the server has no way to call it again.

  Dennis Hackethal criticized idea #5853.

Make Hiccdown reactive, like Reagent: a proc as the first element marks a component, as in [->(product) { [:li, product.name] }, @product].

The server keeps track of which components each page rendered, with which records, and gives each component's element the id dom_id(record), as turbo streams do. When a record changes (after_commit, checking that it actually changed), the server tells the pages showing it, over Action Cable. Each page then requests just that component, which renders in the viewer's own request, with their user, session, locale, time zone and CSRF token, and swaps it in with a turbo stream.

Rendering the component in the request that changed the record would save that round trip, but that request has none of the viewers' context: it would render for the wrong user, or have to rebuild each viewer's context and render once per viewer.

A first pass can skip checking whether a change affects what a component shows, as when a product's name changes on a page that only shows its price.

#5853​·​Dennis HackethalOP revised 1 day ago​·​AI-assisted

Turbo 8 already does most of this. With broadcasts_refreshes and morphing, each viewer's page re-renders in their own request when a record changes, keeping scroll position and form state. Components would only save rendering and sending the rest of the page.

  Dennis Hackethal revised idea #4332 and marked it as AI-assisted.

Spell out how components update, and why each page fetches its own


Some Reagent-like way to make things reactive using proc as first element? And then the server keeps track of which procs have been rendered, which items have changed, and re-renders that part of the template in a turbo stream?

Make Hiccdown reactive, like Reagent: a proc as the first element marks a component, as in [->(product) { [:li, product.name] }, @product].

The server keeps track of which components each page rendered, with which records, and gives each component's element the id dom_id(record), as turbo streams do. When a record changes (after_commit, checking that it actually changed), the server tells the pages showing it, over Action Cable. Each page then requests just that component, which renders in the viewer's own request, with their user, session, locale, time zone and CSRF token, and swaps it in with a turbo stream.

Rendering the component in the request that changed the record would save that round trip, but that request has none of the viewers' context: it would render for the wrong user, or have to rebuild each viewer's context and render once per viewer.

A first pass can skip checking whether a change affects what a component shows, as when a product's name changes on a page that only shows its price.

  Dennis Hackethal archived idea #1984 along with any revisions.
  Dennis Hackethal addressed criticism #1984.

Hiccdown should have support for ids and class names in the tag symbol. Like Hiccup.

ruby
[:'div#my-id.my-class.another-class']
# => <div id="my-id" class="my-class another-class"></div>

It should also allow mixing:

ruby
[:'div#my-id.my-class.another-class', {id: 'override', class: 'additive'}]
# => <div id="override" class="my-class another-class additive"></div>

In other words, the id from the hash would override the id from the symbol, and the class from the hash would be added to the classes from the symbol.

#1984​·​Dennis HackethalOP, about 1 year ago

Done as of 3a304be, to be released in version 1.6.0, as described here. The id and classes can come in any order after the tag name, and two ids in one tag raise ArgumentError.

  Dennis Hackethal archived idea #5847 along with any revisions.
  Dennis Hackethal addressed criticism #5847.

A helper method that calls another action's helper method by name can get another helper's. Rails mixes every helper into every view, so calling index from ItemsHelper#create runs whichever helper's index Rails included last, such as PagesHelper#index:

ruby
module ItemsHelper
def index
[:ul, @items.map { |i| [:li, i.name] }]
end
def create
index # PagesHelper#index, not ItemsHelper#index
end
end

Action names like index, show and edit are in many helpers, and the Readme encourages helper methods calling one another.

#5847​·​Dennis HackethalOP, 1 day ago​·​AI-assisted

Known since #301, and accepted in #333: the alternatives, helper class methods (#1980) and separate display classes (#1982), were worse. Qualifying a helper method's name, like items_page rather than calling index, is the way around it.

  Dennis Hackethal archived idea #4331 along with any revisions.
  Dennis Hackethal commented on idea #4331.

They already do: turbo-rails gives frame requests its own minimal layout, and Hiccdown leaves it be. As of c5ce76f, to be released in version 1.6.0, a test checks that frame requests keep Turbo's frame layout even where ApplicationHelper#layout exists, and a system test (e71df95) loads a frame in a browser.

  Dennis Hackethal archived idea #4330 along with any revisions.
  Dennis Hackethal archived idea #859 along with any revisions.
  Dennis Hackethal addressed criticism #4330.

Redirects result in two additional requests, the first of which is a turbo-stream request that renders nothing, thus (presumably) prompting the browser to make another request for the same resource.

This? https://stackoverflow.com/a/74071278

#4330​·​Dennis HackethalOP, 8 months ago

Doesn't reproduce as of e71df95. In the app in test/dummy, a form whose action redirects takes one POST and one GET in a headless browser, which a system test now checks.

  Dennis Hackethal addressed criticism #859.

Could the errors around layouts be related to this?

#859​·​Dennis HackethalOP, almost 2 years ago

Probably, and they're fixed. Hiccdown used to force a layout (layout: true, then layout: !request.format.turbo_stream?), overriding the layout method turbo-rails relies on, and turbo-stream requests failed with "no default layout" (fef00cb). cf92f06, in version 1.5.2, stopped forcing a layout. As of c5ce76f and 4a087e4, to be released in version 1.6.0, tests check that frame requests keep Turbo's frame layout and that turbo streams get none, and the app in test/dummy shows it in a browser.

  Dennis Hackethal posted criticism #5847.

A helper method that calls another action's helper method by name can get another helper's. Rails mixes every helper into every view, so calling index from ItemsHelper#create runs whichever helper's index Rails included last, such as PagesHelper#index:

ruby
module ItemsHelper
def index
[:ul, @items.map { |i| [:li, i.name] }]
end
def create
index # PagesHelper#index, not ItemsHelper#index
end
end

Action names like index, show and edit are in many helpers, and the Readme encourages helper methods calling one another.

  Dennis Hackethal archived idea #5845 along with any revisions.
  Dennis Hackethal addressed criticism #5845.

On Rails 8.1, Hiccdown's capture hands keyword arguments to the block as key-value pairs rather than as keywords. capture(text: 'foo') { |text:| text } raises ArgumentError: missing keyword: :text instead of returning 'foo', and a block that takes them as a hash, { |options| … }, gets [:text, 'foo'] instead of {text: 'foo'}.

Earlier versions of Rails pass keywords on to the block as a hash, which Hiccdown leaves alone.

#5845​·​Dennis HackethalOP, 1 day ago​·​AI-assisted

Fixed as of 99b6280, to be released in version 1.6.0.

  Dennis Hackethal posted criticism #5845.

On Rails 8.1, Hiccdown's capture hands keyword arguments to the block as key-value pairs rather than as keywords. capture(text: 'foo') { |text:| text } raises ArgumentError: missing keyword: :text instead of returning 'foo', and a block that takes them as a hash, { |options| … }, gets [:text, 'foo'] instead of {text: 'foo'}.

Earlier versions of Rails pass keywords on to the block as a hash, which Hiccdown leaves alone.

  Dennis Hackethal archived idea #4329 along with any revisions.
  Dennis Hackethal commented on idea #4329.

Is there a way to teach user-built helpers how to process Hiccdown? Or maybe intercepting capture already took care of this?

#4329​·​Dennis HackethalOP, 8 months ago

Mostly, intercepting capture already took care of it: helpers of your own that pass a block on to Rails' helpers, or return Hiccdown, worked. Hiccdown given to Rails' helpers as content didn't, as in content_tag(:div, yield), tag.div([:span, 'foo']) or link_to([:span, 'foo'], path). As of 134deb9, to be released in version 1.6.0, it does.

  Dennis Hackethal archived idea #4328 along with any revisions.
  Dennis Hackethal commented on idea #4328.

Could the application layout live in ApplicationHelper#layout?

#4328​·​Dennis HackethalOP, 8 months ago

Yes. As of 4fd7a0e, to be released in version 1.6.0, the layout named X lives in XHelper#layout, just as it would in layouts/X.html.erb, so the application layout lives in ApplicationHelper#layout. yield and yield(:title) work as in an ERB layout, and layout files keep working for a gradual migration.

  Dennis Hackethal archived idea #5840 along with any revisions.