diff --git a/README.md b/README.md
index 327588c..49299eb 100644
--- a/README.md
+++ b/README.md
@@ -69,7 +69,7 @@ config.use_local_editor = false # or true
## Installation
> [!NOTE]
-> This guide assumes that you use ferbe on a standard rails 8.1 app that already has stimulus and importmap set up.
+> This guide assumes that you use ferbe on a standard rails 8.0/8.1 app that already has stimulus and importmap set up.
Add this line to your application's Gemfile:
@@ -91,35 +91,13 @@ $ rails generate ferbe:install
### Layout
-In your layout, add these in the head section:
+Now, in your layout, add these in the head section:
```erb
<%= ferbe_styles_tag %>
<%= ferbe_javascript_tag %>
```
-Add the `ferbe_editor_tag` and wrap everything in an element with the `ferbe__page` class.
-
-```erb
-
-
- <%= yield %>
-
-
-<%= ferbe_editor_tag %>
-
-```
-
-### Add the Stimulus controller
-
-The final step is to add the provided Stimulus controller to your `javascript/controllers/index.js`:
-
-```js
-import FerbeEditorController from "ferbe/controllers/ferbe_editor_controller";
-// ...
-application.register("ferbe-editor", FerbeEditorController);
-```
-
---
Copyright 2026 by Renuo AG
diff --git a/app/assets/javascripts/ferbe/application.js b/app/assets/javascripts/ferbe/application.js
index 33219c8..2bd1387 100644
--- a/app/assets/javascripts/ferbe/application.js
+++ b/app/assets/javascripts/ferbe/application.js
@@ -1,3 +1,4 @@
-import FerbeTemplate from "ferbe/elements/ferbe_template";
+import { application } from "controllers/application";
+import FerbeEditorController from "ferbe/controllers/ferbe_editor_controller";
-customElements.define("ferbe-template", FerbeTemplate);
+application.register("ferbe-editor", FerbeEditorController);
diff --git a/app/assets/javascripts/ferbe/controllers/ferbe_editor_controller.js b/app/assets/javascripts/ferbe/controllers/ferbe_editor_controller.js
index 1030b64..517f76d 100644
--- a/app/assets/javascripts/ferbe/controllers/ferbe_editor_controller.js
+++ b/app/assets/javascripts/ferbe/controllers/ferbe_editor_controller.js
@@ -1,3 +1,5 @@
+import editorUrl from "ferbe/utils/editor_url";
+
import { Controller } from "@hotwired/stimulus";
import { CodeJar } from "codejar";
import hljs from "highlight.js/lib/core";
@@ -6,23 +8,41 @@ import xml from "highlight.js/lib/languages/xml";
import ruby from "highlight.js/lib/languages/ruby";
export default class FerbeEditorController extends Controller {
- static targets = ["editor", "form", "input", "errorContainer"];
+ static targets = ["editor", "form", "input"];
connect() {
this.#setupHighlighting();
this.#highlight(this.editorTarget);
this.#preventUnsavedClosing();
- this.#setupErrorHandling();
}
disconnect() {
this.jar.destroy();
window.onbeforeunload = null;
- this.errorContainerTarget.innerHTML = "";
}
- close() {
- this.element.remove();
+ open(event) {
+ const template = event.detail;
+
+ const currentParams = new URLSearchParams(document.location.search);
+ const url = currentParams.get("url");
+
+ const params = new URLSearchParams();
+ params.append("url", url);
+ params.append("template[path]", template.filePath);
+ template.renderPath.forEach((p) =>
+ params.append("template[render_path][]", p),
+ );
+
+ fetch(`/ferbe/template/edit?${params.toString()}`, {
+ headers: { Accept: "text/vnd.turbo-stream.html" },
+ })
+ .then((r) => r.text())
+ .then((html) => {
+ Turbo.renderStreamMessage(html);
+ window.history.pushState({}, "", editorUrl(template));
+ })
+ .catch((err) => console.error("Failed to open editor:", err));
}
save() {
@@ -58,24 +78,4 @@ export default class FerbeEditorController extends Controller {
return "There are unsaved changes in the ferbe editor.";
};
}
-
- #setupErrorHandling() {
- addEventListener("turbo:before-fetch-response", (event) => {
- const response = event.detail.fetchResponse;
- if (response.statusCode !== 500) return;
-
- event.preventDefault();
- document.documentElement.removeAttribute("aria-busy");
- this.#displayError({
- message: `${response.statusCode} ${response.response.statusText}`,
- url: response.response.url,
- });
- });
- }
-
- #displayError({ message, url }) {
- this.errorContainerTarget.innerHTML = `There is an error that was likely caused by your edit:
-${message}
-Open in new tab`;
- }
}
diff --git a/app/assets/javascripts/ferbe/elements/ferbe_template.js b/app/assets/javascripts/ferbe/elements/ferbe_template.js
index 0a957e8..6579848 100644
--- a/app/assets/javascripts/ferbe/elements/ferbe_template.js
+++ b/app/assets/javascripts/ferbe/elements/ferbe_template.js
@@ -43,19 +43,15 @@ export default class FerbeTemplate extends HTMLElement {
}
#openInEditor() {
- const path = this.getAttribute("path");
- const renderPath = this.#getRenderPath();
-
- const params = new URLSearchParams();
- params.append("template[path]", path);
- renderPath.forEach((p) => params.append("template[render_path][]", p));
-
- fetch(`/ferbe/template/edit?${params.toString()}`, {
- headers: { Accept: "text/vnd.turbo-stream.html" },
- })
- .then((r) => r.text())
- .then((html) => Turbo.renderStreamMessage(html))
- .catch((err) => console.error("Failed to open editor:", err));
+ window.top.dispatchEvent(
+ new CustomEvent("ferbe:open-editor", {
+ detail: {
+ url: window.location.toString(),
+ filePath: this.getAttribute("path"),
+ renderPath: this.#getRenderPath(),
+ },
+ }),
+ );
}
#getRenderPath() {
diff --git a/app/assets/javascripts/ferbe/host.js b/app/assets/javascripts/ferbe/host.js
new file mode 100644
index 0000000..999115c
--- /dev/null
+++ b/app/assets/javascripts/ferbe/host.js
@@ -0,0 +1,12 @@
+import FerbeTemplate from "ferbe/elements/ferbe_template";
+import editorUrl from "ferbe/utils/editor_url";
+
+customElements.define("ferbe-template", FerbeTemplate);
+
+addEventListener("ferbe:open-editor", (event) => {
+ const editor = document.getElementById("ferbe-editor");
+ if (editor) return;
+
+ const template = event.detail;
+ window.location.href = editorUrl(template);
+});
diff --git a/app/assets/javascripts/ferbe/utils/editor_url.js b/app/assets/javascripts/ferbe/utils/editor_url.js
new file mode 100644
index 0000000..609a052
--- /dev/null
+++ b/app/assets/javascripts/ferbe/utils/editor_url.js
@@ -0,0 +1,11 @@
+export default function editorUrl(template) {
+ const params = new URLSearchParams();
+
+ params.append("url", template.url);
+ params.append("template[path]", template.filePath);
+ template.renderPath.forEach((p) =>
+ params.append("template[render_path][]", p),
+ );
+
+ return `/ferbe/template/edit?${params.toString()}`;
+}
diff --git a/app/assets/stylesheets/ferbe/application.css b/app/assets/stylesheets/ferbe/application.css
index d7ddf6c..08743f8 100644
--- a/app/assets/stylesheets/ferbe/application.css
+++ b/app/assets/stylesheets/ferbe/application.css
@@ -2,23 +2,22 @@
--ferbe-color-light-grey: #fafafa;
--ferbe-color-grey: #dedede;
--ferbe-color-blue: #0033b3;
- --ferbe-color-red: #f6817f;
- --ferbe-color-light-red: #ff9b99;
--ferbe-border: 2px solid var(--ferbe-color-grey);
}
+body {
+ margin: 0;
+}
+
.ferbe__page {
display: flex;
- height: 100vh;
+ min-height: 100vh;
- & > main {
+ iframe {
+ all: unset;
flex-grow: 1;
}
- .ferbe__editor * {
- all: revert;
- }
-
.ferbe__editor > div {
width: min(50vw, 60rem);
padding: 1rem;
@@ -55,6 +54,12 @@
}
}
+ a.ferbe__button {
+ box-sizing: border-box;
+ text-decoration: none;
+ text-align: center;
+ }
+
.ferbe__header {
display: flex;
align-items: baseline;
@@ -83,14 +88,5 @@
}
}
}
-
- .ferbe__error-container {
- border-color: var(--ferbe-color-red);
- background-color: var(--ferbe-color-light-red);
-
- &:empty {
- display: none;
- }
- }
}
}
diff --git a/app/assets/stylesheets/ferbe/host.css b/app/assets/stylesheets/ferbe/host.css
new file mode 100644
index 0000000..30b746d
--- /dev/null
+++ b/app/assets/stylesheets/ferbe/host.css
@@ -0,0 +1,3 @@
+ferbe-template {
+ display: contents;
+}
diff --git a/app/controllers/ferbe/templates_controller.rb b/app/controllers/ferbe/templates_controller.rb
index 59be321..47ea579 100644
--- a/app/controllers/ferbe/templates_controller.rb
+++ b/app/controllers/ferbe/templates_controller.rb
@@ -1,19 +1,16 @@
module Ferbe
class TemplatesController < ApplicationController
def edit
- template_params = params.require(:template).permit(:path, render_path: [])
- return head :bad_request unless valid_path? template_params[:path]
+ template = edit_params[:template]
+ return head :bad_request unless valid_path? template[:path]
@template = {
- content: File.read(template_params[:path]),
- path: template_params[:path],
- render_path: template_params[:render_path]
+ content: File.read(template[:path]),
+ path: template[:path],
+ render_path: template[:render_path]
}
- respond_to do |format|
- format.turbo_stream
- format.html { head :no_content }
- end
+ @url = edit_params[:url]
end
# :nocov: -> covered by manual system tests
@@ -40,6 +37,13 @@ def update
private
+ def edit_params
+ {
+ template: params.require(:template).permit(:path, render_path: []),
+ url: params.require(:url)
+ }
+ end
+
def valid_path?(path)
expanded_path = File.expand_path path
diff --git a/app/views/ferbe/templates/_editor.html.erb b/app/views/ferbe/templates/_editor.html.erb
new file mode 100644
index 0000000..6d268fa
--- /dev/null
+++ b/app/views/ferbe/templates/_editor.html.erb
@@ -0,0 +1,33 @@
+<%# locals: (url:, template:) %>
+
+
+
+
+<% template[:render_path].each_with_index do |path, level| %>
+<%= " " * level %>> <%= link_to(Pathname.new(path).relative_path_from(Rails.root),
+ edit_template_url(url: ,
+ template: {
+ path: path,
+ render_path: template[:render_path]
+ }),
+ data: { turbo_stream: true }) %>
+<% end %>
+
+ <%= form_with url: template_url,
+ method: :patch,
+ scope: :template,
+ data: { ferbe_editor_target: "form" } do |form| %>
+ <%= form.hidden_field :path, value: template[:path] %>
+ <%= form.hidden_field :content,
+ value: template[:content],
+ data: { ferbe_editor_target: "input" } %>
+
<%= template[:content] %>
+ <%= form.submit "💾", class: "ferbe__button ferbe__save-button" %>
+ <% end %>
+
diff --git a/app/views/ferbe/templates/edit.html.erb b/app/views/ferbe/templates/edit.html.erb
new file mode 100644
index 0000000..adb1b6a
--- /dev/null
+++ b/app/views/ferbe/templates/edit.html.erb
@@ -0,0 +1,11 @@
+
+
+
+ <%= tag.div class: "ferbe__editor", id: "ferbe-editor", data: {
+ modifier_key: Ferbe.configuration.modifier_key,
+ use_local_editor: Ferbe.configuration.use_local_editor,
+ turbo_permanent: true
+ } do %>
+ <%= render partial: "editor", locals: { url: @url, template: @template } %>
+ <% end %>
+
diff --git a/app/views/ferbe/templates/edit.turbo_stream.erb b/app/views/ferbe/templates/edit.turbo_stream.erb
index 8a57d47..7072e79 100644
--- a/app/views/ferbe/templates/edit.turbo_stream.erb
+++ b/app/views/ferbe/templates/edit.turbo_stream.erb
@@ -1,32 +1,3 @@
<%= turbo_stream.update "ferbe-editor" do %>
-
-
-
-<% @template[:render_path].each_with_index do |path, level| %>
-<%= " " * level %>> <%= link_to( Pathname.new(path).relative_path_from(Rails.root),
- edit_template_url(template: {
- path: path,
- render_path: @template[:render_path]
- }),
- data: { turbo_stream: true }) %>
-<% end %>
-
- <%= form_with url: template_url,
- method: :patch,
- scope: :template,
- data: { ferbe_editor_target: "form" } do |form| %>
- <%= form.hidden_field :path, value: @template[:path] %>
- <%= form.hidden_field :content,
- value: @template[:content],
- data: { ferbe_editor_target: "input" } %>
-
<%= @template[:content] %>
- <%= form.submit "💾", class: "ferbe__button ferbe__save-button" %>
- <% end %>
-
-
+ <%= render partial: "editor", locals: { url: @url, template: @template } %>
<% end %>
diff --git a/app/views/layouts/ferbe/application.html.erb b/app/views/layouts/ferbe/application.html.erb
new file mode 100644
index 0000000..6d862db
--- /dev/null
+++ b/app/views/layouts/ferbe/application.html.erb
@@ -0,0 +1,21 @@
+
+
+
+ Ferbe Editor
+ <%= csrf_meta_tags %>
+ <%= csp_meta_tag %>
+
+ <%= yield :head %>
+
+ <%= javascript_importmap_tags %>
+ <%= javascript_import_module_tag "ferbe/application" %>
+
+ <%= stylesheet_link_tag "ferbe/highlight", media: "all" %>
+ <%= stylesheet_link_tag "ferbe/application", media: "all" %>
+
+
+
+<%= yield %>
+
+
+
diff --git a/config/ferbe_importmap.rb b/config/ferbe_importmap.rb
index b130484..30e4723 100644
--- a/config/ferbe_importmap.rb
+++ b/config/ferbe_importmap.rb
@@ -1,8 +1,7 @@
+pin_all_from Ferbe::Engine.root.join("app/assets/javascripts/ferbe"), under: "ferbe"
+
pin "highlight.js/lib/core", to: "https://ga.jspm.io/npm:highlight.js@11.11.1/es/core.js"
pin "highlight.js/lib/languages/erb", to: "https://ga.jspm.io/npm:highlight.js@11.11.1/es/languages/erb.js"
pin "highlight.js/lib/languages/xml", to: "https://ga.jspm.io/npm:highlight.js@11.11.1/es/languages/xml.js"
pin "highlight.js/lib/languages/ruby", to: "https://ga.jspm.io/npm:highlight.js@11.11.1/es/languages/ruby.js"
pin "codejar", to: "https://ga.jspm.io/npm:codejar@4.2.0/dist/codejar.js"
-
-pin "ferbe", to: "ferbe/application.js"
-pin_all_from Ferbe::Engine.root.join("app/assets/javascripts/ferbe"), under: "ferbe"
diff --git a/lib/ferbe/helper.rb b/lib/ferbe/helper.rb
index ad6771f..13d788a 100644
--- a/lib/ferbe/helper.rb
+++ b/lib/ferbe/helper.rb
@@ -4,26 +4,13 @@ module Helper
def ferbe_styles_tag
return unless Ferbe.configuration.enabled
- capture do
- concat stylesheet_link_tag "ferbe/highlight", media: "all"
- concat stylesheet_link_tag "ferbe/application", media: "all"
- end
+ stylesheet_link_tag "ferbe/host", media: "all"
end
def ferbe_javascript_tag
return unless Ferbe.configuration.enabled
- javascript_import_module_tag "ferbe"
- end
-
- def ferbe_editor_tag
- return unless Ferbe.configuration.enabled
-
- tag.div class: "ferbe__editor", id: "ferbe-editor", data: {
- modifier_key: Ferbe.configuration.modifier_key,
- use_local_editor: Ferbe.configuration.use_local_editor,
- turbo_permanent: true
- }
+ javascript_import_module_tag "ferbe/host"
end
#:nocov:
end
diff --git a/test/controllers/ferbe/templates_controller_test.rb b/test/controllers/ferbe/templates_controller_test.rb
index 1a26a86..66a15df 100644
--- a/test/controllers/ferbe/templates_controller_test.rb
+++ b/test/controllers/ferbe/templates_controller_test.rb
@@ -45,17 +45,24 @@ class TemplatesControllerTest < ActionDispatch::IntegrationTest
end
end
- test "edit as html returns no content" do
+ test "edit is available as turbo stream and html" do
Tempfile.create(["test", ".html.erb"], Rails.root.join("app/views")) do |file|
file.write "I am a template!"
- get edit_template_url, params: {template: {path: file.path}}
- assert_response :no_content
+ params = {template: {path: file.path, render_path: [file.path]}, url: "http://example.com"}
+
+ get(edit_template_url, params:)
+ assert_response :success
+
+ get(edit_template_url, params:, as: :turbo_stream)
+ assert_response :success
end
end
- test "reject editing of invalid template" do
- get edit_template_url, params: {template: {path: "some/invalid/file.txt"}}
+ test "rejection of editor for invalid template" do
+ invalid_path = "some/invalid/file.txt"
+ get edit_template_url,
+ params: {template: {path: invalid_path, render_path: [invalid_path]}, url: "http://example.com"}
assert_response :bad_request
end
end
diff --git a/test/dummy/app/assets/stylesheets/application.css b/test/dummy/app/assets/stylesheets/application.css
index 1a1bd79..0556e93 100644
--- a/test/dummy/app/assets/stylesheets/application.css
+++ b/test/dummy/app/assets/stylesheets/application.css
@@ -7,6 +7,10 @@ main {
margin-block: 1rem;
padding: 1rem;
min-height: 3rem;
+
+ &.is-outlined {
+ border: 0.25rem solid;
+ }
}
.red {
@@ -24,19 +28,22 @@ main {
opacity: 0.5;
}
-.traffic-light {
- border: 0.25rem solid;
+.brown {
+ background-color: var(--primary);
+ opacity: 0.5;
+}
- &.is-horizontal {
- display: flex;
- gap: 1rem;
+.is-horizontal {
+ display: flex;
+ gap: 1rem;
- * {
- flex-grow: 1;
- }
+ * {
+ flex-grow: 1;
}
}
-.tower {
+.grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
border: 0.25rem solid;
}
diff --git a/test/dummy/app/javascript/controllers/index.js b/test/dummy/app/javascript/controllers/index.js
index f913136..625b998 100644
--- a/test/dummy/app/javascript/controllers/index.js
+++ b/test/dummy/app/javascript/controllers/index.js
@@ -1,7 +1,5 @@
// Import and register all your controllers from the importmap via controllers/**/*_controller
import { application } from "controllers/application";
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading";
-import FerbeEditorController from "ferbe/controllers/ferbe_editor_controller";
eagerLoadControllersFrom("controllers", application);
-application.register("ferbe-editor", FerbeEditorController);
diff --git a/test/dummy/app/views/home/index.html.erb b/test/dummy/app/views/home/index.html.erb
index 9986507..636f278 100644
--- a/test/dummy/app/views/home/index.html.erb
+++ b/test/dummy/app/views/home/index.html.erb
@@ -1,5 +1,7 @@
Welcome to the Homepage
+<%= render partial: "shared/grid" %>
+
<%= render partial: "shared/horizontal_traffic_light" %>
<%= render partial: "shared/traffic_light" %>
diff --git a/test/dummy/app/views/layouts/application.html.erb b/test/dummy/app/views/layouts/application.html.erb
index 74a52bd..d6ada37 100644
--- a/test/dummy/app/views/layouts/application.html.erb
+++ b/test/dummy/app/views/layouts/application.html.erb
@@ -20,17 +20,14 @@
<%= stylesheet_link_tag :app %>
<%= ferbe_styles_tag %>
+
<%= javascript_importmap_tags %>
<%= ferbe_javascript_tag %>
-
<%= yield %>
-
- <%= ferbe_editor_tag %>
-