Skip to content

Commit

Permalink
Create Komponent::ComponentRenderer
Browse files Browse the repository at this point in the history
  • Loading branch information
florentferry committed Mar 29, 2018
1 parent 551603a commit f6ce86a
Show file tree
Hide file tree
Showing 26 changed files with 201 additions and 66 deletions.
7 changes: 7 additions & 0 deletions .simplecov
@@ -0,0 +1,7 @@
SimpleCov.minimum_coverage 90

SimpleCov.start do
add_filter "/fixtures/"
add_filter "/test/"
add_filter "/features/"
end
3 changes: 1 addition & 2 deletions .travis.yml
Expand Up @@ -29,5 +29,4 @@ install:
- yarn

script:
- bundle exec rubocop
- bundle exec rake test
- bundle exec rake test:all_with_coverage
2 changes: 2 additions & 0 deletions Gemfile
Expand Up @@ -14,4 +14,6 @@ gem "rubocop", require: false
group :test do
gem "aruba"
gem "cucumber"
gem "simplecov", require: false
gem "coveralls", require: false
end
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -2,6 +2,7 @@
[![Build Status](https://travis-ci.org/komposable/komponent.svg?branch=master)](https://travis-ci.org/komposable/komponent)
[![Gem](https://img.shields.io/gem/v/komponent.svg)](https://github.com/komposable/komponent)
[![Dependency Status](https://beta.gemnasium.com/badges/github.com/komposable/komponent.svg)](https://beta.gemnasium.com/projects/github.com/komposable/komponent)
[![Coverage Status](https://coveralls.io/repos/github/komposable/komponent/badge.svg?branch=master)](https://coveralls.io/github/komposable/komponent?branch=master)

**Komponent** implements an opinionated way of organizing front-end code in Ruby on Rails, based on _components_.

Expand Down
16 changes: 12 additions & 4 deletions Rakefile
@@ -1,25 +1,33 @@
# frozen_string_literal: true

require "bundler/gem_tasks"
require "rubocop/rake_task"
require "rake/testtask"
require 'cucumber'
require 'cucumber/rake/task'
require "cucumber"
require "cucumber/rake/task"
require "coveralls/rake/task"

namespace :test do
task all: [:unit, :cucumber]
task all: [:rubocop, :unit, :cucumber]
task all_with_coverage: [:all, "coveralls:push"]

RuboCop::RakeTask.new

Rake::TestTask.new(:unit) do |t|
t.libs << "test"
t.libs << "lib"
t.test_files = FileList["test/**/*_test.rb"]
t.verbose = true
t.warning = false
end

Cucumber::Rake::Task.new(:cucumber) do |t|
t.cucumber_opts = "--format pretty"
end

Coveralls::RakeTask.new
end

task test: 'test:all'
task test: "test:all"

task default: :test
3 changes: 3 additions & 0 deletions features/support/env.rb
@@ -0,0 +1,3 @@
# frozen_string_literal: true

require 'simplecov'
1 change: 1 addition & 0 deletions fixtures/my_app/config/application.rb
Expand Up @@ -12,5 +12,6 @@

module MyApp
class Application < Rails::Application
config.i18n.load_path += Dir[config.root.join('frontend/components/**/*.yml')]
end
end
1 change: 1 addition & 0 deletions fixtures/my_app/frontend/components/foo/_foo.html.erb
@@ -0,0 +1 @@
<div class="foo"><%= properties[:bar] %></div>
1 change: 1 addition & 0 deletions fixtures/my_app/frontend/components/foo/foo.js
@@ -0,0 +1 @@
import "./foo.scss";
2 changes: 2 additions & 0 deletions fixtures/my_app/frontend/components/foo/foo.scss
@@ -0,0 +1,2 @@
.foo {
}
5 changes: 5 additions & 0 deletions fixtures/my_app/frontend/components/foo/foo_component.rb
@@ -0,0 +1,5 @@
module FooComponent
extend ComponentHelper

property :bar, default: "Foobar"
end
1 change: 1 addition & 0 deletions fixtures/my_app/frontend/components/hello/_hello.html.erb
@@ -0,0 +1 @@
<div class="hello"><%= t(".hello") %></div>
3 changes: 3 additions & 0 deletions fixtures/my_app/frontend/components/hello/hello.en.yml
@@ -0,0 +1,3 @@
en:
hello_component:
hello: "Hello"
3 changes: 3 additions & 0 deletions fixtures/my_app/frontend/components/hello/hello.fr.yml
@@ -0,0 +1,3 @@
fr:
hello_component:
hello: "Bonjour"
1 change: 1 addition & 0 deletions fixtures/my_app/frontend/components/hello/hello.js
@@ -0,0 +1 @@
import "./hello.scss";
2 changes: 2 additions & 0 deletions fixtures/my_app/frontend/components/hello/hello.scss
@@ -0,0 +1,2 @@
.hello {
}
3 changes: 3 additions & 0 deletions fixtures/my_app/frontend/components/hello/hello_component.rb
@@ -0,0 +1,3 @@
module HelloComponent
extend ComponentHelper
end
Expand Up @@ -9,10 +9,5 @@ def property(name, options = {})

def self.extended(component)
component.properties = {}
component.class_eval do
def block_given_to_component?
@block_given_to_component.present?
end
end
end
end
File renamed without changes.
67 changes: 67 additions & 0 deletions lib/komponent/component_renderer.rb
@@ -0,0 +1,67 @@
# frozen_string_literal: true

module Komponent
class ComponentRenderer
attr_reader :context

def initialize(controller)
@context = controller.view_context.dup
@view_renderer = @context.view_renderer = @context.view_renderer.dup
@lookup_context = @view_renderer.lookup_context = @view_renderer.lookup_context.dup
end

def render(component, locals = {}, &block)
parts = component.split("/")
component_name = parts.join("_")

component_module_path = resolved_component_path(component)
.join("#{component_name}_component")
require_dependency(component_module_path)
component_module = "#{component_name}_component".camelize.constantize

@context.class_eval { prepend component_module }
@context.class_eval { prepend Komponent::Translation }

@lookup_context.prefixes = ["components/#{component}"]

capture_block = proc { capture(&block) } if block

@context.instance_eval do
if component_module.respond_to?(:properties)
locals = locals.dup
component_module.properties.each do |name, options|
unless locals.has_key?(name)
if options.has_key?(:default)
locals[name] = options[:default]
elsif options[:required]
raise "Missing required component parameter: #{name}"
end
end
end
end

locals.each do |name, value|
instance_variable_set(:"@#{name}", locals[name])
end

instance_variable_set(:"@block_given_to_component", capture_block)

define_singleton_method(:properties) { locals }
define_singleton_method(:block_given_to_component?) { !!block }
end

begin
@context.render("components/#{component}/#{parts.join('_')}", &capture_block)
rescue ActionView::MissingTemplate
warn "[DEPRECATION WARNING] `#{parts.last}` filename in namespace is deprecated in favor of `#{parts.join('_')}`."
@context.render("components/#{component}/#{parts.last}", &capture_block)
end
end

private

def resolved_component_path(component)
Komponent::ComponentPathResolver.new.resolve(component)
end
end
end
60 changes: 9 additions & 51 deletions lib/komponent/komponent_helper.rb
Expand Up @@ -2,60 +2,17 @@

module KomponentHelper
def component(component, locals = {}, &block)
component_path = Komponent::ComponentPathResolver.new.resolve(component)

parts = component.split("/")
component_name = parts.join("_")
component_path = component_path.join("#{component_name}_component")

require_dependency(component_path)

component_module = "#{component_name}_component".camelize.constantize
context = controller.view_context.dup

context.view_flow = view_flow

view_renderer = context.view_renderer = context.view_renderer.dup
lookup_context = view_renderer.lookup_context = view_renderer.lookup_context.dup
lookup_context.prefixes = ["components/#{component}"]

context.class_eval { prepend component_module }
context.class_eval { prepend Komponent::Translation }

capture_block = proc { capture(&block) } if block

context.instance_eval do
if component_module.respond_to?(:properties)
locals = locals.dup
component_module.properties.each do |name, options|
unless locals.has_key?(name)
if options.has_key?(:default)
locals[name] = options[:default]
elsif options[:required]
raise "Missing required component parameter: #{name}"
end
end
end
end

locals.each do |name, value|
instance_variable_set(:"@#{name}", locals[name])
end

define_singleton_method(:properties) { locals }

instance_variable_set(:"@block_given_to_component", block)
end

begin
context.render("components/#{component}/#{parts.join('_')}", &capture_block)
rescue ActionView::MissingTemplate
warn "[DEPRECATION WARNING] `#{parts.last}` filename in namespace is deprecated in favor of `#{parts.join('_')}`."
context.render("components/#{component}/#{parts.last}", &capture_block)
end
Komponent::ComponentRenderer.new(
controller,
).render(
component,
locals,
&block
)
end
alias :c :component

# :nocov:
def render_partial(partial_name, locals = {}, &block)
warn "[DEPRECATION WARNING] `render_partial` is deprecated. Please use default `render` instead."

Expand All @@ -79,4 +36,5 @@ def render_partial(partial_name, locals = {}, &block)

rendered_partial
end
# :nocov:
end
7 changes: 4 additions & 3 deletions lib/komponent/railtie.rb
@@ -1,9 +1,10 @@
# frozen_string_literal: true

require 'webpacker'
require 'komponent/core/component_helper'
require 'komponent/core/translation'
require 'komponent/core/component_path_resolver'
require 'komponent/component_helper'
require 'komponent/component_path_resolver'
require 'komponent/component_renderer'
require 'komponent/translation'

module Komponent
class Railtie < Rails::Railtie
Expand Down
File renamed without changes.
43 changes: 43 additions & 0 deletions test/komponent/component_renderer_test.rb
@@ -0,0 +1,43 @@
# frozen_string_literal: true

require 'test_helper'

class FakeController < ApplicationController
def initialize(method_name = nil, &method_body)
if method_name and block_given?
self.class.send(:define_method, method_name, method_body)
Rails.application.routes.draw do
get method_name, to: "fake##{method_name}"
end
end
end
end

class ComponentRendererTest < ActionController::TestCase
def test_methods_are_accessible_in_context
@controller = FakeController.new

renderer = Komponent::ComponentRenderer.new(@controller)
renderer.render('all', text: 'hello world')
@context = renderer.context

assert_respond_to @context, :block_given_to_component?
assert_respond_to @context, :properties
assert_respond_to @context, :translate
assert_equal @context.instance_variable_get(:'@text'), 'hello world'
assert_nil @context.instance_variable_get(:'@block_given_to_component')
end

def test_block
@controller = FakeController.new

renderer = Komponent::ComponentRenderer.new(@controller)
renderer.render('all') do
"<p>HELLO</p>"
end
@context = renderer.context

assert_equal @context.block_given_to_component?, true
assert_not_nil @context.instance_variable_get(:'@block_given_to_component')
end
end
28 changes: 27 additions & 1 deletion test/komponent/komponent_helper_test.rb
Expand Up @@ -3,13 +3,39 @@
require 'test_helper'

class KomponentHelperTest < ActionView::TestCase
def test_helper_raises_component_missing_error
assert_raise Komponent::ComponentPathResolver::MissingComponentError do
component('missing')
end
end

def test_helper_renders_makes_locals_available_as_instance_variables
assert_equal %(<div class="world">🌎</div>), component('world', world: "🌎").chomp
assert_equal \
%(<div class="world">🌎</div>),
component('world', world: "🌎").chomp
end

def test_helper_makes_all_properties_accessible
assert_equal \
%(<div class="all">🌎 😎</div>),
component('all', world: "🌎", sunglasses: "😎").chomp
end

def test_helper_renders_localized_keys
I18n.locale = :en
assert_equal \
%(<div class="hello">Hello</div>),
component('hello').chomp

I18n.locale = :fr
assert_equal \
%(<div class="hello">Bonjour</div>),
component('hello').chomp
end

def test_helper_renders_default_property
assert_equal \
%(<div class="foo">Foobar</div>),
component('foo').chomp
end
end
2 changes: 2 additions & 0 deletions test/test_helper.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require 'simplecov'

ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../fixtures/my_app/config/environment', __FILE__)
require 'rails/test_help'

0 comments on commit f6ce86a

Please sign in to comment.