Skip to content

Implementing issue 153 #154

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Sep 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/concepts/matestack/ui/core/app/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def components(&block)
@nodes = Matestack::Ui::Core::AppNode.build(self, &block)

@nodes.each do |key, node|
@cells[key] = to_cell(key, node["component_name"], node["config"], node["argument"], node["components"], nil)
@cells[key] = to_cell(key, node["component_name"], node["config"], node["argument"], node["components"], nil, node["cached_params"])
end
end

Expand Down
9 changes: 5 additions & 4 deletions app/concepts/matestack/ui/core/component/dynamic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,14 @@ def initialize(model=nil, options={})
@nodes = {}
@cells = {}
@included_config = options[:included_config]
@cached_params = options[:cached_params]
@rerender = false
@options = options
set_tag_attributes
setup
generate_component_name
generate_children_cells
set_tag_attributes
validate_options
setup
end

def validate_options
Expand Down Expand Up @@ -157,7 +158,7 @@ def components(&block)
@nodes = Matestack::Ui::Core::ComponentNode.build(self, nil, &block)

@nodes.each do |key, node|
@cells[key] = to_cell(key, node["component_name"], node["config"], node["argument"], node["components"], node["included_config"])
@cells[key] = to_cell(key, node["component_name"], node["config"], node["argument"], node["components"], node["included_config"], node["cached_params"])
end
end

Expand Down Expand Up @@ -197,7 +198,7 @@ def generate_children_cells
#needs refactoring --> in some cases, :component_key, :children, :origin_url, :url_params, :included_config get passed into options[:children] which causes errors
#quickfix: except them from iteration
options[:children].except(:component_key, :children, :origin_url, :url_params, :included_config).each do |key, node|
@children_cells[key] = to_cell("#{@component_key}__#{key}", node["component_name"], node["config"], node["argument"], node["components"], node["included_config"])
@children_cells[key] = to_cell("#{@component_key}__#{key}", node["component_name"], node["config"], node["argument"], node["components"], node["included_config"], node["cached_params"])
end
end
end
Expand Down
2 changes: 2 additions & 0 deletions app/concepts/matestack/ui/core/isolate/isolate.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- @children_cells.each do |key, cell|
= cell.call(:show)
11 changes: 11 additions & 0 deletions app/concepts/matestack/ui/core/isolate/isolate.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module Matestack::Ui::Core::Isolate
class Isolate < Matestack::Ui::Core::Component::Static

def setup
@component_key = @component_key + "__" + @argument.to_s
@component_key = @component_key + "(" + @cached_params.to_json + ")" if @cached_params.present?
@component_config[:component_key] = @component_key
end

end
end
34 changes: 31 additions & 3 deletions app/concepts/matestack/ui/core/page/page.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def components(&block)

def nodes_to_cell
@nodes.each do |key, node|
@cells[key] = to_cell(key, node["component_name"], node["config"], node["argument"], node["components"], node["included_config"])
@cells[key] = to_cell(key, node["component_name"], node["config"], node["argument"], node["components"], node["included_config"], node["cached_params"])
end
end

Expand All @@ -65,8 +65,15 @@ def slot(&block)
Matestack::Ui::Core::PageNode.build(self, nil, context[:params], &block)
end

def isolate(&block)
return block
end


def show(component_key=nil, only_page=false)
prepare
return resolve_isolated_component(component_key) if !component_key.nil? && component_key.include?("isolate")

response

render_mode = nil
Expand All @@ -91,11 +98,11 @@ def show(component_key=nil, only_page=false)
keys_array = keys_array.drop(keys_array.find_index(page_content_keys[0])+2)
end
node = @nodes.dig(*keys_array)
cell = to_cell(component_key, node["component_name"], node["config"], node["argument"], node["components"], node["included_config"])
cell = to_cell(component_key, node["component_name"], node["config"], node["argument"], node["components"], node["included_config"], node["cached_params"])
return cell.render_content
else
node = @nodes[component_key]
cell = to_cell(component_key, node["component_name"], node["config"], node["argument"], node["components"], node["included_config"])
cell = to_cell(component_key, node["component_name"], node["config"], node["argument"], node["components"], node["included_config"], node["cached_params"])
return cell.render_content
end
rescue
Expand All @@ -111,6 +118,27 @@ def page_id

private

def resolve_isolated_component component_key
keys_array = component_key.gsub("__", "__components__").split("__").map {|k| k.to_s}
isolate_keys = keys_array.select{|key| key.match(/^isolate_/)}
keys_array = keys_array.drop(keys_array.find_index(isolate_keys[0])+2)
isolated_scope_method = keys_array[0]
if isolated_scope_method.include?("(")
isolated_scope_method_name = isolated_scope_method.split("(").first
isolated_scope_method_argument = isolated_scope_method.split("(").last.split(")").first
isolated_scope_method_argument = JSON.parse(isolated_scope_method_argument)
isolated_block = self.send(isolated_scope_method_name, isolated_scope_method_argument.with_indifferent_access)
else
isolated_block = self.send(isolated_scope_method)
end
nodes = Matestack::Ui::Core::PageNode.build(
self, nil, context[:params], &isolated_block
)
node = nodes.dig(*keys_array.drop(2))
cell = to_cell(component_key, node["component_name"], node["config"], node["argument"], node["components"], node["included_config"], node["cached_params"])
return cell.render_content
end

def generate_page_name
name_parts = self.class.name.split("::").map { |name| name.underscore }
@page_id = name_parts.join("_")
Expand Down
6 changes: 5 additions & 1 deletion app/lib/matestack/ui/core/component_node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,18 @@ def method_missing meth, *args, &block
@hash[current_node]["argument"] = nil
@hash[current_node]["included_config"] = @included_config

if meth == :isolate
raise("isolate > only works on page level currently. component support is comming soon!")
end

if meth == :partial
@hash[current_node]["components"] = @component_instance.send(args.first, *args.drop(1))
elsif meth == :yield_components
@hash[current_node]["component_name"] = "partial"
@hash[current_node]["components"] = @component_instance.send(:get_children)
else
if args.first.is_a?(Hash)
@hash[current_node]["config"] = args.first
@hash[current_node]["config"] = args.first unless meth == :slot
else
@hash[current_node]["argument"] = args.first
end
Expand Down
21 changes: 21 additions & 0 deletions app/lib/matestack/ui/core/page_node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,27 @@ def method_missing meth, *args, &block
end
end

if meth == :isolate
if args.second.present?
if args.second.is_a? Hash
if args.second[:cached_params].nil?
raise "isolate > you need to pass params within a hash called 'cached_params'"
else
isolated_block = @page_instance.send(args.first, args.second[:cached_params])
end
else
raise "isolate > you need to pass params within a hash called 'cached_params'"
end
else
isolated_block = @page_instance.send(args.first)
end
@hash[current_node]["components"] = PageNode.build(
@page_instance, nil, @url_params, &isolated_block
)
@hash[current_node]["argument"] = args.first
@hash[current_node]["cached_params"] = args.second[:cached_params] if args.second.present?
end

if meth == :partial
partial_block = @page_instance.send(args.first, *args.drop(1))
@hash[current_node]["components"] = PageNode.build(
Expand Down
4 changes: 3 additions & 1 deletion app/lib/matestack/ui/core/to_cell.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module Matestack::Ui::Core::ToCell

def to_cell(key, component_name, config, argument, children, included_config)
def to_cell(key, component_name, config, argument, children, included_config, cached_params=nil)
# p cached_params
request_uri = context[:request].env["REQUEST_URI"]
query_string = context[:request].env["QUERY_STRING"]

Expand All @@ -9,6 +10,7 @@ def to_cell(key, component_name, config, argument, children, included_config)
config.merge!(origin_url: request_uri.gsub("?" + query_string, ""))
config.merge!(url_params: context[:params])
config.merge!(included_config: included_config)
config.merge!(cached_params: cached_params)

if component_name.start_with?("custom")
return resolve_custom_component(component_name, argument, config)
Expand Down
106 changes: 106 additions & 0 deletions docs/components/isolate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# matestack core component: Isolate

Show [specs](/spec/usage/components/isolate_spec.rb)

The `isolate` component allows you to create isolated "scopes", which can be rendered without calling the main response method of a `page`. Used with `async`, only specific isolated scopes are resolved on the serverside rather than resolving the whole UI when asynchronously rerendering parts of the UI. Usage seems to be similar to `partial`, but there are important differences! Read below!

**the isolate/async components currently only work on page-level --> we're working on it in order support the usage of async/isolate within a component [#75](https://github.com/basemate/matestack-ui-core/issues/75)**

## Parameters

The isolate core component accepts the following parameters:

### isolated method (as a symbol) (mandatory)

Just like a partial, using a symbol, a specific part of the UI can be referenced:

```ruby
def response
components{
#a lot of other UI components you want to bypass if you rerender the isolated scope

isolate :my_isolated_scope

#a lot of other UI components you want to bypass if you rerender the isolated scope

#for emphasizing the difference, a similar partial would work like so:
partial :my_partial_scope
}
end

def my_isolated_scope
@some_data = SomeModel.find(42)

isolate{
async rerender_on: "some_event" do
div do
plain @some_data.some_attribute
end
end
}
end

def my_partial_scope
@some_data = SomeModel.find(42)

partial{
async rerender_on: "some_other_event" do
div do
plain @some_data.some_attribute
end
end
}
end
```

As mentioned in the example code above: The `async` requests a new version of `my_isolated_scope` after the event `some_event` is received. But instead of resolving the whole UI and then returning only the desired part, the `isolate` component takes care of bypassing all irrelevant parts of the UI. The `async` rerender action inside the `partial` in contrast would call the whole `response` method of the page, which can lead to significant higher response times.


### Cached Params (Clientside params --> public!)

As the isolated scope is truly encapsulated from the main `response` method while rerendering, simply passing in dynamic params from within the `response` method can not work. As it is often necessary, to render "partials" dynamically, we can use `cached_params` for isolated scopes. These params are stored on the clientside after initial page and then used when performing the rerender action. Using this approach, we can still bypass the main `response` method while using dynamic params.

**Just put simple data into cached_params (simple integers or strings, no hashes etc). Never put sensible data into cached_params as they are visible on the clientside!**

```ruby
def response
components{
#a lot of other UI components you want to bypass if you rerender the isolated scope

[1, 2, 3].each do |id|
isolate :my_isolated_scope, cached_params: { id: id }
end

#a lot of other UI components you want to bypass if you rerender the isolated scope

#for emphasizing the difference, a similar partial would work like so:
[1, 2, 3].each do |id|
partial :my_partial_scope, id
end
}
end

def my_isolated_scope cached_params
@some_data = SomeModel.find(cached_params[:id])

isolate{
async rerender_on: "isolated_rerender_#{cached_params[:id]}" do
div do
plain @some_data.some_attribute
end
end
}
end

def my_partial_scope id
@some_data = SomeModel.find(id)

partial{
async rerender_on: "partial_rerender_#{id}" do
div do
plain @some_data.some_attribute
end
end
}
end
```
6 changes: 3 additions & 3 deletions spec/usage/base/page_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ def my_action
it "can fill slots of components with access to page instance scope" do

module Matestack::Ui::Core::Example end

module Matestack::Ui::Core::Example::Component
class Component < Matestack::Ui::StaticComponent

Expand All @@ -299,8 +299,8 @@ def prepare
def response
components {
div id: "my-component" do
slot options[:my_first_slot]
slot options[:my_second_slot]
slot @options[:my_first_slot]
slot @options[:my_second_slot]
end
}
end
Expand Down
Loading