Skip to content

Commit 93651a7

Browse files
committed
Merge branch 'next-release' of github.com:matestack/matestack-ui-core into next-release
2 parents 4f7d903 + 72f4ebe commit 93651a7

File tree

15 files changed

+505
-464
lines changed

15 files changed

+505
-464
lines changed

app/concepts/matestack/ui/core/component/base.rb

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -223,34 +223,33 @@ def get_children
223223
# rendering.
224224
def add_child(child_class, *args, &block)
225225

226-
# when the child is an async component like shown below, only render its wrapper
226+
# when the child is an async or isolate component like shown below, only render its wrapper
227227
# and skip its content on normal page rendering call indicated by @matestack_skip_defer == true
228228
# Example: async defer: 1000 { plain "I should be deferred" }
229229
# the component will triger a subsequent component rendering call after 1000ms
230230
# the main renderer will then set @matestack_skip_defer to false which allows processing
231231
# the childs content in order to respond to the subsequent component rendering call with
232232
# the childs content. In this case: "I should be deferred"
233233
skip_deferred_child_response = false
234-
if child_class == Matestack::Ui::Core::Async::Async
234+
if child_class <= Matestack::Ui::Core::Async::Async || child_class < Matestack::Ui::Core::Isolate::Isolate
235235
if args.any? { |arg| arg[:defer].present? } && @matestack_skip_defer == true
236236
skip_deferred_child_response = true
237237
end
238238
end
239-
# same applies for all isolated components, no extra defer option needed
239+
240+
# check only allowed keys are passed to isolated components
240241
if child_class < Matestack::Ui::Core::Isolate::Isolate
241-
skip_deferred_child_response = true
242-
unless args.empty? || args[0].keys.all? { |key| key == :defer || key == :public_options || key == :rerender_on || key == :init_on || key == :rerender_delay }
242+
unless args.empty? || args[0].keys.all? { |key| [:defer, :public_options, :rerender_on, :init_on, :rerender_delay].include? key }
243243
raise "isolated components can only take params in a public_options hash, which will be exposed to the client side in order to perform an async request with these params."
244244
end
245+
if args.any? { |arg| arg[:init_on].present? } && @matestack_skip_defer == true
246+
skip_deferred_child_response = true
247+
end
245248
end
246249

247250
if self.class < Matestack::Ui::Core::Isolate::Isolate
248-
parent_context_included_config = @current_parent_context.get_included_config
249-
if parent_context_included_config.nil?
250-
parent_context_included_config = { isolated_parent_class: self.class.name }
251-
else
252-
parent_context_included_config.merge!({ isolated_parent_class: self.class.name })
253-
end
251+
parent_context_included_config = @current_parent_context.get_included_config || {}
252+
parent_context_included_config.merge!({ isolated_parent_class: self.class.name })
254253
args_with_context = add_context_to_options(args,parent_context_included_config)
255254
else
256255
args_with_context = add_context_to_options(args, @current_parent_context.get_included_config)

app/concepts/matestack/ui/core/component/dynamic.rb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,15 @@ def dynamic_tag_attributes
2424
end
2525

2626
def get_vue_js_name
27-
if @vue_js_component_name.present?
28-
@vue_js_component_name
29-
else
30-
self.class.vue_js_name
31-
end
27+
self.class.vue_js_name
3228
end
3329

3430
class << self
31+
32+
def inherited(subclass)
33+
subclass.vue_js_component_name vue_js_name unless self == Matestack::Ui::Core::Component::Dynamic
34+
end
35+
3536
def vue_js_component_name(name)
3637
@vue_js_name = name.to_s
3738
end
Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
%component{dynamic_tag_attributes}
22
%div{loading_classes.merge(class: "matestack-isolated-component-container")}
3-
%div{loading_classes.merge(class: "loading-state-element-wrapper")}
4-
=loading_state_element
5-
%div{loading_classes.merge(class: "matestack-isolated-component-wrapper")}
3+
- if loading_state_element.present?
4+
%div{loading_classes.merge(class: "loading-state-element-wrapper")}
5+
=loading_state_element
6+
- unless options[:defer] || options[:init_on]
7+
%div{class: "matestack-isolated-component-wrapper", "v-if": "isolatedTemplate == null", "v-bind:class": "{ 'loading': loading === true }"}
8+
= render_isolated_content
9+
%div{class: "matestack-isolated-component-wrapper", "v-if": "isolatedTemplate != null", "v-bind:class": "{ 'loading': loading === true }"}
610
%v-runtime-template{":template":"isolatedTemplate"}

app/concepts/matestack/ui/core/isolate/isolate.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,20 @@ const componentDef = {
7171
},
7272
mounted: function (){
7373
const self = this
74-
if(this.componentConfig["init_on"] === undefined){
75-
if(self.componentConfig["defer"] != undefined){
74+
console.log('mounted isolated component')
75+
if(this.componentConfig["init_on"] === undefined || this.componentConfig["init_on"] === null){
76+
console.log('Its me')
77+
if(self.componentConfig["defer"] == true || Number.isInteger(self.componentConfig["defer"])){
78+
console.log('I should render deferred')
7679
if(!isNaN(self.componentConfig["defer"])){
7780
self.startDefer()
7881
}
79-
}else{
80-
self.renderIsolatedContent();
82+
else{
83+
self.renderIsolatedContent();
84+
}
85+
}
86+
else {
87+
console.log('I should NOT render deferred')
8188
}
8289
}else{
8390
if(self.componentConfig["defer"] != undefined){

app/concepts/matestack/ui/core/isolate/isolate.rb

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
module Matestack::Ui::Core::Isolate
22
class Isolate < Matestack::Ui::Core::Component::Dynamic
3-
43
vue_js_component_name "matestack-ui-core-isolate"
54

65
def initialize(*args)
76
super
87
@public_options = args.map { |hash| hash[:public_options] }[0]
9-
# using this instance var here as users inherit from this class and would need
10-
# to use `vue_js_component_name "matestack-ui-core-isolate"` in their components
11-
# which is not convinient
12-
@vue_js_component_name = "matestack-ui-core-isolate"
138
end
149

1510
def public_options
@@ -26,6 +21,7 @@ def setup
2621
@component_config[:defer] = @options[:defer]
2722
@component_config[:rerender_on] = @options[:rerender_on]
2823
@component_config[:rerender_delay] = @options[:rerender_delay]
24+
@component_config[:init_on] = @options[:init_on]
2925
end
3026

3127
def loading_classes
@@ -44,7 +40,7 @@ def show
4440

4541
# this method gets called when the isolate vuejs component requests isolated content
4642
def render_isolated_content
47-
render :children
43+
render :children if authorized?
4844
end
4945

5046
def authorized?

app/lib/matestack/ui/core/rendering/main_renderer.rb

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,7 @@ def render(controller_instance, page_class, options)
2828
# isolated component rendering
2929
component_class = params[:component_class]
3030
page_instance = page_class.new(controller_instance: controller_instance, context: context)
31-
if params[:public_options].present?
32-
public_options = JSON.parse(params[:public_options]).with_indifferent_access
33-
else
34-
public_options = nil
35-
end
31+
public_options = JSON.parse(params[:public_options]).with_indifferent_access rescue nil
3632
render_isolated_component(component_class, page_instance, controller_instance, context, public_options)
3733
elsif (params[:component_class].present? && params[:component_key].present?)
3834
# async component rerendering from isolated context

docs/api/base/backend/isolate.md

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
# Matestack Core Component: Isolate
2+
3+
Feel free to check out the [component specs](/spec/usage/components/dynamic/isolate).
4+
5+
The isolate component allows you to create components, which can be rendered independently. It means that isolate components are rendered without calling the response method of your page, which gives you the possibility to rerender components dynamically without rerendering the whole UI.
6+
7+
In addition it is possible to combine isolate components and async components. If an async component inside an isolate component gets rerendered the server only needs to resolve the isolate scope instead of the whole UI.
8+
9+
If not configured, the isolate component gets rendered on initial pageload. You can prevent this by passing a `defer` or `init_on` option. See below for further details.
10+
11+
## Parameters
12+
13+
The isolate core component accepts the following parameters:
14+
15+
### defer
16+
17+
The option defer lets you delay the initial component rendering. If you set defer to a positive integer or `true` the isolate component will not be rendered on initial page load. Instead it will be rendered with an asynchronous request only resolving the isolate component.
18+
19+
If `defer` is set to `true` the asynchronous requests gets triggered as soon as the initial page is loaded.
20+
21+
If `defer` is set to a positive integer (including zero) the asynchronous request is delayed by the given amount in ms.
22+
23+
### rerender_on
24+
25+
The `rerender_on` options lets you define events on which the component will be rerenderd asynchronously. Events on which the component should be rerendered are specified via a comma seperated string, for example `rerender_on: 'event_one, event_two`.
26+
27+
### rerender_delay
28+
29+
The `rerender_delay` option lets you specify a delay in ms after which the asynchronous request is emitted to rerender the component. It can for example be used to smooth out loading animations, preventing flickering in the UI for fast responses.
30+
31+
### init_on
32+
33+
With `init_on` you can specify events on which the isolate components gets initialized. Specify events on which the component should be initially rendered via a comma seperated string. When receiving a matching event the isolate component is rendered asynchronously. If you also specified the `defer` option the asynchronous rerendering call will be delayed by the given time in ms of the defer option. If `defer` is set to `true` the rendering will not be delayed.
34+
35+
### public_options
36+
37+
You can pass data as a hash to your custom isolate component with the `public_options` option. This data is inside the isolate component accessible via a hash with indifferent access, for example `public_options[:item_id]`. All data contained in the `public_options` will be passed as json to the corresponding vue component, which means this data is visible on the client side as it is rendered in the vue component config. So be careful what data you pass into `public_options`!
38+
39+
Due to the isolation of the component the data needs to be stored on the client side as to encapsulate the component from the rest of the UI.
40+
For example: You want to render a collection of models in single components which should be able to rerender asynchronously without rerendering the whole UI. Since we do not rerender the whole UI there is no way the component can know which of the models it should rerender. Therefore passing for example the id in the public_options hash gives you the possibility to access the id in an async request and fetch the model again for rerendering. See below for examples.
41+
42+
## Loading State and animations
43+
44+
TODO
45+
46+
## Examples
47+
48+
### Example 1 - Simple Isolate
49+
50+
Create a custom component inheriting from the isolate component
51+
52+
```ruby
53+
class MyIsolated < Matestack::Ui::Core::IsolatedComponent
54+
def response
55+
div id: 'my-isolated-wrapper' do
56+
plain I18n.l(DateTime.now)
57+
end
58+
end
59+
end
60+
```
61+
Register your custom component
62+
```ruby
63+
module ComponentsRegistry
64+
Matestack::Ui::Core::Component::Registry.register_components(
65+
my_isolated: MyIsolated
66+
)
67+
```
68+
And use it on your page
69+
```ruby
70+
class Home < Matestack::Ui::Page
71+
def response
72+
heading size: 1, text: 'Welcome'
73+
my_isolated
74+
end
75+
end
76+
```
77+
78+
This will render a h1 with the content welcome and the localized current datetime inside the isolated component. The isolated component gets rendered with the initial page load, because the defer options is not set.
79+
80+
### Example 2 - Simple Deferred Isolated
81+
```ruby
82+
class Home < Matestack::Ui::Page
83+
def response
84+
heading size: 1, text: 'Welcome'
85+
my_isolated defer: true,
86+
my_isolated defer: 2000
87+
end
88+
end
89+
```
90+
91+
By specifying the `defer` option both calls to the custom isolated components will not get rendered on initial page load. Instead the component with `defer: true` will get rendered as soon as the initial page load is done and the component with `defer: 2000` will be rendered 2000ms after the initial page load is done. Which means that the second my_isolated component will show the datetime with 2s more on the clock then the first one.
92+
93+
### Example 3 - Rerender On Isolate Component
94+
95+
```ruby
96+
class Home < Matestack::Ui::Page
97+
def response
98+
heading size: 1, text: 'Welcome'
99+
my_isolated rerender_on: 'update_time'
100+
onclick emit: 'update_time' do
101+
button 'Update Time!'
102+
end
103+
end
104+
end
105+
```
106+
107+
`rerender_on: 'update_time'` tells the custom isolated component to rerender its content asynchronously whenever the event `update_time` is emitted. In this case every time the button is pressed the event is emitted and the isolated component gets rerendered, showing the new timestamp afterwards. In contrast to async components only the `MyIsolated` component is rendered on the server side instead of the whole UI.
108+
109+
### Example 4 - Rerender Isolated Component with a delay
110+
111+
```ruby
112+
class Home < Matestack::Ui::Page
113+
def response
114+
heading size: 1, text: 'Welcome'
115+
my_isolated rerender_on: 'update_time', rerender_delay: 300
116+
onclick emit: 'update_time' do
117+
button 'Update Time!'
118+
end
119+
end
120+
end
121+
```
122+
123+
The my_isolated component will be rerendered 300ms after the `update_time` event is emitted
124+
125+
### Example 5 - Initialize isolated component on a event
126+
127+
```ruby
128+
class Home < Matestack::Ui::Page
129+
def response
130+
heading size: 1, text: 'Welcome'
131+
my_isolated init_on: 'init_time'
132+
onclick emit: 'init_time' do
133+
button 'Init Time!'
134+
end
135+
end
136+
end
137+
```
138+
139+
With `init_on: 'init_time'` you can specify an event on which the isolated component should be initialized. When you click the button the event `init_time` is emitted and the isolated component asynchronously requests its content.
140+
141+
### Example 6 - Use custom data in isolated components
142+
143+
Like described above it is possible to use custom data in your isolated components. Just pass them as a hash to `public_options` and use them in your isolated component. Be careful, because `public_options` are visible in the raw html response from the server as they get passed to a vue component.
144+
145+
Lets render a collection of models and each of them should rerender when a user clicks a corresponding refresh button. Our model is called `Match`, representing a soccer match. It has an attribute called score with the current match score.
146+
147+
At first we create a custom isolated component.
148+
```ruby
149+
class Components::Match::IsolatedScore < Matestack::Ui::IsolatedComponent
150+
151+
def prepare
152+
@match = Match.find_by(public_options[:id])
153+
end
154+
155+
def response
156+
div class: 'score' do
157+
plain @match.score
158+
end
159+
onclick emit: "update_match_#{@match.id}" do
160+
button 'Refresh'
161+
end
162+
end
163+
164+
end
165+
```
166+
After that we register our new custom component.
167+
```ruby
168+
module ComponentsRegistry
169+
Matestack::Ui::Core::Component::Registry.register_components(
170+
match_isolated_score: Components::Match::IsolatedScore
171+
)
172+
```
173+
Make sure your registry is loaded in your controller. In our case we include our registry in the `ApplicationController`.
174+
```ruby
175+
class ApplicationController < ActionController::Base
176+
include Matestack::Ui::Core::ApplicationHelper
177+
include Components::Registry
178+
end
179+
```
180+
Now we create our page which will render a list of matches.
181+
```ruby
182+
class Match::Pages::Index < Matestack::Ui::Page
183+
def response
184+
Match.all.each do |match|
185+
match_isolated_score public_options: { id: match.id }, rerender_on: "update_match_#{match.id}"
186+
end
187+
end
188+
end
189+
```
190+
191+
This page will render a match_isolated_score component for each match.
192+
If one of the isolated components gets rerendered we need the id in order to fetch the correct match. Because the server only resolves the isolated component instead of the whole UI it does not know which match exactly is requested unless the client requests a rerender with the match id. This is why `public_options` options are passed to the client side vue component.
193+
So if match two should be rerendered the client requests the match_isolated_score component with `public_options: { id: 2 }`. With this information our isolated component can fetch the match and rerender itself.

0 commit comments

Comments
 (0)