Skip to content
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

Opal 0.8 - WIP noise reducing syntax changes #32

Open
wants to merge 52 commits into
base: opal-0.8
Choose a base branch
from

Conversation

catmando
Copy link
Collaborator

This is WIP, just to see what you think of the changes.
For now its using my own copy of opal that better source mapping than the current version 0.7...

Summary of changes for discussion:

Any class with a render method can be used in the dsl by using the class name.

class MyComponent
  ...
end

class ParentComponent
  def render
    div do 
     MyComponent param1: 'foo' # works same as presents MyComponent ...
    end
  end
end

Component classes can also be scoped normally, so in the above example MyComponent could be a child, sibling, etc of ParentComponent. So if ParentComponents full name was Components::Admin::ParentComponent, then MyComponent could be a child of Components, Components::Admin, or Components::Admin::ParentComponent.

In the case where MyComponent was somewhere else in the module hierarchy the name can be qualified as needed. i.e. Shared::MyComponent

params can be specified as one a liner (similar to state, and rails attributes etc.)

required_param :foo   
optional_param :bar, default: "xyz"

params get their own read accessors just like state, so you can say

puts foo
puts bar

params with type Proc, can be called inside the component without using call.

required_param :inform_of_update, type: Proc
...
   inform_of_update "new text string"

state can now be updated by using the ! methods.

some_state!  12 # state now updated to 12, returns the previous value
some_state!   # without a param returns an object that will observe any changes to some_state
some_state! << 12 << 13 # assuming some_state is an array for example

the object returned by some_state! will also respond to :call, this allows
state to be easily passed as a Proc to a child param. For example

  my_component inform_of_update: my_state!   
  # compare to the equivalent
  present MyComponent, inform_of_update: -> (x) { self.my_state = x }

params can be of type React::Observable. Params of this type act like a Proc with an initial value. They can be read in the usual way, and can be update by calling the ! version. So they work like react linkages. State variables with the ! on the end are actually Observables, so you can now simply write:

  class MyInput

     required_param :value, type: React::Observable
     def render
       input(value: value).on(:change) { |new_value| value! new_value }
     end
   end

  class Parent
     define_state(:my_state) { "initial value"}
     def render
        MyInput value! # value will be updated by MyInput
     end
  end

If needed you can create an observable on the fly by calling watch(initial_value) { | updated_value | ... }

A copy of the current state is kept, so it can be read during events.
ReactJS does not guarantee the value of state being stable until after
the event completes.

So now its safe to write

   my_state! get_new_value
   my_state.each do { ... } #etc
# instead of
   temp = get_new_value
   temp.each do { ... } #etc

The React.render method can be passed a Opal Element directly (no need to do a get(0).

  React.render(element, Element['#todoapp'])
# works the same as
  React.render(element, Element['#todoapp'].get(0)) # this still works

There is a React::TopLevelComponent class. This provides several features:

  1. ability to create multiple react components mounted in various places on a single page and communicating. Very handy for evolving from other frameworks, as it allows subsections of the page to be redone as components.
  2. automatically mounts the components on DOM ready
  3. provides a safe mechanism to communicate from outside the React structure
class App < React::TopLevelComponent

  define_state :text do "I have never been clicked" end

  mount_component FriendsContainer, 'div#placeholder'
  mount_component JustSomeText, 'div#placeholder2' do {text: text} end

  external_update :update_text do |new_text| text! new_text end
end

elsewhere in old js code you can say

 $(document).ready(function() {
  var n = 0;
  $('#jquery-button').click(function() {    
    Opal.App.$update_text("I have been clicked "+n+"times")
  }}

Any calls to external updates will be queued until the top level component is mounted

The TopLevelComponent class can be defined across multiple files. This means it can be used to control the app layout across multiple pages, with each page adding its own "top level" behavior inside the page.

Well I think that's it... interested in what you think... I am using all these changes for my main company app, and should be deploying soon, so you can see it live in action.

I am happy to write the docs, update the samples, and of course write tests for all these.

@zetachang
Copy link
Owner

Hi, I've go through the features you proposed, for

A copy of the current state is kept, so it can be read during events.
ReactJS does not guarantee the value of state being stable until after
the event completes.
So now its safe to write

I didn't get it, would you mind elaborate more with some examples of the problem you are faced?

@catmando
Copy link
Collaborator Author

Sure in react when you update a state, you cannot immediately read the state and be guaranteed to get the value back till after the render.

This makes sense in react.js as the syntax is very cumbersome anyway. but in ruby its nice to be able to do this:

self.my_state = "foo"
# later the same render cycle
if my_state == "foo" 

The thing is this works sometimes without out any changes but sometimes it doesn't. I got really confused debugging some code, and then I checked the reactjs docs and discovered that setting a state does not guarantee that you can immediately read the state!!!

So I just copy the state to a Ruby hash that gets merged with the ReactJs hash.

@catmando
Copy link
Collaborator Author

FYI... I am working on changing the way the dsl references Opal components:

right now if the component is class FooBar then the dsl references it as foo_bar

The ReactJS convention seems to be the capitalize component names in JSX so I think FooBar should be used in the dsl as well. That way its easy to see which are client supplied components.

So you would write

def render
  div do
    FooBar title: "its a foo bar"
  end
end

I would also like to be able to write

def render
  div do
    SharedComponents::FooBar title: "its a foo bar"
  end
end

but this is going to take a little more effort, but I think I can get it to work.

@zetachang
Copy link
Owner

I think caching state for getter is a great idea, would you mind make a separate request for this feature?

The ReactJS convention seems to be the capitalize component names in JSX so I think FooBar should be used in the dsl as well. That way its easy to see which are client supplied components.

👍 on this, maybe some const missing magic?

@catmando
Copy link
Collaborator Author

That's exactly where I am heafing

@catmando
Copy link
Collaborator Author

I tried out using a const name (i.e. FooBar instead of foo_bar) for client defined components.

It works nice, unless the component has no block and no parameters (I think that is a real tails case). For such a component to work you need to include at least an empty block so that ruby interprets the expression as a method call and not just a constant.

For example if you say FooBar title: "my foobar" then all is well as Ruby interprets FooBar as a method. But if there are no parameters, then Ruby interprets FooBar as the class, but FooBar {} works okay.

I have one more thing to try to get around this, we will see...

@catmando
Copy link
Collaborator Author

Okay!

Got component classes to name space correctly, and things read quite nicely... in addition I setup a full two linkage (based on React Linkages) that allows a single param (prop) to have 2 way communication with the parent. Like React Linkages its just syntax sugar to make things easier to ready.

I am going to update my initial comment with the pull request so all the info is one place.

@catmando
Copy link
Collaborator Author

Just added some more syntax sugar and noise reduction.

you can now say

define_state :foo, :bar, baz: 12, blog: 17

foo and bar will be initialized to nil. baz will get 12, and blog 17.

You can also say

define_state :state1, :state2 { calculate_a_value }

the result of the block becomes the default initializer for any states not having their own initial values.

So everything works per the current syntax.

After reading through the react documents I don't even think its a good idea to have the initializer execute in the context creating a new instance of the component, so I left the code just like it is now.

Also made it so any param (whether declared in the params validation or not) gets responds as a method. So you can say count in the render method assuming count was passed as a parameter.

Finally I did add (at least to try it out) having Strings respond to a display method. This will just put the string into the render stream. Not sure if its really needed or not but it is allowed by React

@wied03
Copy link
Contributor

wied03 commented Jun 6, 2015

@catmando @zetachang:

I like the define_state initial value shortcuts and params shortcuts.

For reading state, is it really a good idea to cache state or encourage people to read state like this? I'm no expert, but I think React does this for a reason and that is it bunches up state updates so that multiple renders aren't needed. I realize this PR may not be fundamentally changing that, If we start presenting different behavior with this Ruby layer on top, people might become a bit too distant from what's going on underneath.

@catmando
Copy link
Collaborator Author

catmando commented Jun 8, 2015

@weid03

The intention is to make the reading/writing state syntax simple, without changing the semantics.

As far as writing goes, the implementation always writes through to the react hash, so what ever optimizations react has should be in effect.

There is one "semantic" difference, that is intentional. React does not guarantee the that if write to a state, you can immediately read the same value. I do guarantee it hence the cache. I don't think this changes anything because it just puts a handy wrapper around writing to state that anybody could do themselves. I think the only reason is does not work this way in react is because in JS you couldn't really make it read nicely anyway.

To put it another way:

The opal-react "state" is a wrapper around the the react-js state. The react-js state is still there.

can you think of a case where something bad might happen using the higher level opal-react state representation?

@ajjahn
Copy link
Collaborator

ajjahn commented Oct 30, 2015

@catmando merging the reactive-ruby branch into master closes this entire PR. Correct?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants