Skip to content

Commit

Permalink
feat: implements component editing
Browse files Browse the repository at this point in the history
- adds VS Code's Monaco Editor to the page
- allows for saving your code in Rails.env.development?
  • Loading branch information
jrogers-hedgeye committed Sep 10, 2024
1 parent 0bcfd41 commit 3c3b203
Show file tree
Hide file tree
Showing 11 changed files with 162 additions and 9 deletions.
32 changes: 32 additions & 0 deletions app/controllers/phlex_storybook/components_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

module PhlexStorybook
class ComponentsController < ApplicationController
def update
respond_to do |format|
format.turbo_stream do
case story_component.update_source(params[:ruby])
in {success: true}
story_id = params[:story_id]
redirect_to story_path(story_component, story_id: story_id)
in {success: false, error:}
render turbo_stream: [
turbo_stream.replace(
"saving",
"error: #{error.message}",
),
]
else
# huh?!?!
end
end
end
end

private

def story_component
PhlexStorybook.configuration.components.detect { |_k, e| e.name == params[:id] }&.last
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Controller } from "@hotwired/stimulus"
import * as monaco from "monaco-editor"

export default class extends Controller {
static targets = ["ruby"];

connect() {
monaco.editor.create(this.element, {value: this.source(), language: "ruby", theme: "vs-dark"});
}

source() {
return this.rubyTarget.textContent;
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,50 @@
import { Controller } from "@hotwired/stimulus";
import * as monaco from "monaco-editor"

export default class extends Controller {
static targets = ["previewBtn", "codeBtn", "sourceBtn", "preview", "code", "source"];
static targets = [
"code",
"codeBtn",
"editor",
"preview",
"previewBtn",
"ruby",
"source",
"sourceBtn",
];

getComponentSource() {
return this.rubyTarget.textContent;
}

saveComponentSource() {
this.rubyTarget.textContent = this.editor.getValue();
}

showCode() {
this.activate({displayTarget: this.codeTarget, buttonTarget: this.codeBtnTarget});
this.deactivate({displayTarget: this.previewTarget, buttonTarget: this.previewBtnTarget});
this.deactivate({displayTarget: this.sourceTarget, buttonTarget: this.sourceBtnTarget});
this.editor = null;
}

showComponentSource() {
this.activate({displayTarget: this.sourceTarget, buttonTarget: this.sourceBtnTarget});
this.deactivate({displayTarget: this.codeTarget, buttonTarget: this.codeBtnTarget});
this.deactivate({displayTarget: this.previewTarget, buttonTarget: this.previewBtnTarget});
this.editorTarget.style.height = `${this.element.clientHeight * 0.66}px`;
this.editor = monaco.editor.create(this.editorTarget, {
value: this.getComponentSource(),
language: "ruby",
theme: "vs-dark",
});
}

showPreview() {
this.activate({displayTarget: this.previewTarget, buttonTarget: this.previewBtnTarget});
this.deactivate({displayTarget: this.codeTarget, buttonTarget: this.codeBtnTarget});
this.deactivate({displayTarget: this.sourceTarget, buttonTarget: this.sourceBtnTarget});
this.editor = null;
}

activate({displayTarget, buttonTarget}) {
Expand Down
2 changes: 2 additions & 0 deletions config/importmap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
pin "turbo_power", to: "https://ga.jspm.io/npm:[email protected]/dist/index.js"
pin 'monaco-editor', to: 'https://cdn.jsdelivr.net/npm/[email protected]/+esm'

pin_all_from PhlexStorybook::Engine.root.join("app/javascript/phlex_storybook/controllers"), under: "controllers", to: "phlex_storybook/controllers"
5 changes: 5 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
PhlexStorybook::Engine.routes.draw do
root to: 'stories#index'

resources :components, only: [:update]

resources :stories, only: [:index, :show, :update] do
collection do
get :all

get 'codicon.ttf', to: redirect(
'https://cdn.jsdelivr.net/npm/[email protected]/esm/vs/base/browser/ui/codicons/codicon/codicon.ttf')
end
end
end
19 changes: 19 additions & 0 deletions lib/phlex_storybook/assets/phlex_storybook_application.css
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,11 @@ fieldset{
border-color: rgb(51 65 85 / var(--tw-border-opacity));
}

.bg-blue-500{
--tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
}

.bg-gray-100{
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
Expand Down Expand Up @@ -1100,6 +1105,11 @@ fieldset{
padding-right: 0.75rem;
}

.px-4{
padding-left: 1rem;
padding-right: 1rem;
}

.py-1{
padding-top: 0.25rem;
padding-bottom: 0.25rem;
Expand Down Expand Up @@ -1128,6 +1138,10 @@ fieldset{
line-height: 1rem;
}

.font-bold{
font-weight: 700;
}

.font-semibold{
font-weight: 600;
}
Expand Down Expand Up @@ -1213,6 +1227,11 @@ li.story-button-active{
color: rgb(75 85 99 / var(--tw-text-opacity));
}

.hover\:bg-blue-700:hover{
--tw-bg-opacity: 1;
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
}

.hover\:ring-1:hover{
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def view_template
li do
label(class: 'grid grid-cols-1 w-full') do
div { prop.label }
render prop.clone_with_value(@props&.fetch(prop.position, @props&.fetch(prop.key, nil)))
render prop.clone_from(@props)
end
end
end
Expand Down
36 changes: 30 additions & 6 deletions lib/phlex_storybook/components/stories/component_rendering.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,41 @@ def initialize(story_component:, story_id: nil, **props)
end

def view_template
initializer = PhlexStorybook::Props::ComponentInitializer.create(@story_component, **@props)
component_instance, component_source = @story_component.evaluate!(**@props)

turbo_frame_tag("story-rendering") do
div(data: { story_display_target: "preview" }) do
render initializer.initialize_component(@story_component.component)
render component_instance
end

render_ruby_code "code", component_source

div(class: "hidden mt-4 w-full", data: { story_display_target: "source" }) do
if PhlexStorybook.configuration.editable?
action = helpers.component_path(@story_component, story_id: @story_id)
form(action: action, method: "PUT") do
render_component_source

div(class: "hidden") { @story_component.props.each { |prop| render prop.clone_from(@props) } }

button(
class: "mt-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded",
data: { action: "click->story-display#saveComponentSource" },
) { "Save" }
end
else
render_component_source
end
end
end
end

args = @props.blank? ? "" : "(#{initializer.parameters_as_string}\n)"
source = "render #{@story_component.component.name}.new#{args}".strip
private

render_ruby_code "code", source
render_ruby_code "source", @story_component.source
def render_component_source
div id: "source", class: "p-2 overflow-auto scroll", data: { story_display_target: "editor" }
textarea(class: "hidden", data: { story_display_target: "ruby" }, name: "ruby") do
@story_component.source
end
end

Expand Down
4 changes: 4 additions & 0 deletions lib/phlex_storybook/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ def initialize
@components = {}
end

def editable?
Rails.env.development?
end

def register(component, location)
@components[component] = StoryComponent.new(component, location: location)
end
Expand Down
4 changes: 4 additions & 0 deletions lib/phlex_storybook/props/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ def add_to(initializer)
end
end

def clone_from(hash)
clone_with_value(hash&.fetch(position, hash&.fetch(key, nil)))
end

def clone_with_value(v)
p = dup
p.instance_variable_set(:@value, v)
Expand Down
25 changes: 24 additions & 1 deletion lib/phlex_storybook/story_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class StoryComponent
def initialize(component, location: nil)
@component = component
@location = location || nil #component.source_location
@filename = nil
@name = component.name
@category = "Uncategorized"
@description = "No description provided"
Expand All @@ -21,12 +22,19 @@ def default_story
.to_h
end

def evaluate!(**props)
initializer = PhlexStorybook::Props::ComponentInitializer.create(self, **props)
args = props.blank? ? "" : "(#{initializer.parameters_as_string}\n)"
source = "render #{component.name}.new#{args}".strip
[initializer.initialize_component(component), source]
end

def id_for(title)
Digest::MD5.hexdigest(title)
end

def source
File.read component.instance_method(:view_template).source_location.first
File.read filename
end

def story_for(id)
Expand All @@ -40,5 +48,20 @@ def transform_props(user_data)
h[k] = props.detect { |prop| prop.prop_for?(k) }&.transform(v)
end.compact
end

def update_source(new_source)
# component.class_eval new_source
bytes = File.write(filename, new_source)
load filename
{success: true, bytes: bytes}
rescue StandardError => e
{success: false, error: e}
end

private

def filename
@filename = component.instance_method(:view_template).source_location.first
end
end
end

0 comments on commit 3c3b203

Please sign in to comment.