Skip to content

Smart options (they grow up so fast)#136

Merged
leastbad merged 8 commits intostimulusreflex:masterfrom
leastbad:respond_to
Jul 17, 2021
Merged

Smart options (they grow up so fast)#136
leastbad merged 8 commits intostimulusreflex:masterfrom
leastbad:respond_to

Conversation

@leastbad
Copy link
Contributor

@leastbad leastbad commented Jun 9, 2021

This was inspired by conversations with @rrosen and @jaredcwhite. Rafe talked about how when he came to Ruby, he was inspired by the idea that objects should "just know what to do". And Jared's recent #135 was - at its core - about making it possible for an object to take responsibility for its DOM selector. This PR is hopefully the natural conclusion of both thoughts.

An object should be able to provide its own options

Allows objects that conform to the to_xxx naming convention optionally provide a value to matching options.

class Death
  def to_html
    "I rock"
  end
end
death = Death.new
cable_car.inner_html(html: death) # => "innerHtml" => [{"html" => "I rock"}]

An object can provide options and a selector

class Death
  def to_html
    "I rock"
  end

  def to_dom_id
    "death"
  end
end
death = Death.new
cable_car.inner_html(death, html: death) # => "innerHtml" => [{"html" => "I rock", "selector" => "#death"}]

An object should be able to provide all options

If an object is the only parameter passed to an operation, and that object exposes a to_operation_options method, and the to_operation method returns an array of Symbols... the operation builder will build an options hash based for each matching to_xxx method that is defined.

class Death
  def to_html
    "I rock"
  end

  def to_dom_id
    "death"
  end

  def to_operation_options
    [:html, :dom_id, :spaz]
  end
end
death = Death.new
cable_car.inner_html(death) # => "innerHtml" => [{"html" => "I rock", "dom_id" => "death"}]

In the above case, dom_id is added to the options but doesn't get used as the selector because there would have to be a method on the class called to_selector. This is intentional because it means a class can be passed to an operation as a selector with other options, and have its to_dom_id method provide a selector value... or it can be the only parameter and provide a list of options to include which may or may not include selector. This ensures full flexibility in how the feature is used.

You can also return a Hash of options from to_operation_options:

class Life
  def to_operation_options
    {
      html: "You go, girl",
      dom_id: "life"
    }
  end
end
life = Life.new
cable_car.inner_html(life) # => "innerHtml" => [{"html" => "You go, girl", "dom_id" => "life"}]

@leastbad leastbad added enhancement proposal ruby Pull requests that update Ruby code labels Jun 9, 2021
@hopsoft
Copy link
Contributor

hopsoft commented Jun 9, 2021

This is some nice syntactic sugar.

I'm curious as to whether or not to_operation is necessary? Don't we have enough information if an object provides to_html and to_dom_id or to_dom_selector?

If we do need/want to keep it, I propose that we rename to to_operation_options or something a bit more pedantically explicit.

@leastbad
Copy link
Contributor Author

leastbad commented Jun 9, 2021

I'm open to and excited to see other ways of achieving the same end result, but I don't see how we can get to the same outcome through inference alone. The reason is simple: we don't know what options each operation needs; some want text and others want message. Even if we did have a mapping (which is impossible due to custom operations) providing a to_operation (or to_operation_options) gives the developer explicit control over which options are passed.

Consider this: depending on the internal state of an object, it might only want to provide a selector value in certain circumstances. The array result could be dynamically constructed to include or exclude :selector.

I'm generally not wild about making names longer than they need to be (would to_options suffice?) but there wouldn't be any argument, here: if we keep it, you'll tell me what the method name will be and I'll update all of the places. It actually started as to_cable and it's an easy change.

@jaredcwhite
Copy link
Contributor

I'm a little fuzzy on the edge cases here… What if you want to pass the object for several different types of operations? Wouldn't each operation need a different list of parameters?

So maybe do something like params_for_operation(operation_name) and you could write a case…when or whatever to handle multiple operations, whether that's :inner_html or :text_content or whatever.

@leastbad
Copy link
Contributor Author

🙀

@leastbad
Copy link
Contributor Author

Sorry to keep you on the edge of your seat, I've been on the go, today. 🚄

I really don't want to over-engineer this PR. Even to_operation (or whatever it becomes) is a rare example of maximalism in a library that has traditionally been low fat, low carb. I'm just a sucker for making things short and magical when possible.

It is true that if you want a given object to be able to be passed to multiple types of operation - something you can't even remotely do in today's CR and wasn't even on the horizon until this PR - you'll need to have to_xxx methods defined for the options you need. If this breaks down because you need an object to behave differently depending on the operation it's being passed to, then the correct thing to do is to just pass in keyword parameters, like you already do today.

After all, if we implement some kind of lookup structure eg. params_for_operation(operation_name) then the next stop on the slippery slope is going to be "but my inner_html operations need different options depending on the context in which they are called in, like different controllers or Reflexes". It seems as though the path to hell is paved with good intentions here, doesn't it?

I already think that inner_html(selector: object, html: object) is seriously cool and a huge improvement on the status quo.

If you have an object with complex utility, it seems like to_operation might just be too magical. However, many objects will fit into the patterns proposed by this PR like a hand in glove.

@jaredcwhite
Copy link
Contributor

I'd humbly suggest leaving out to_operation this go around…like you said, just being able to hand the obj off to the keyword args seems way cool. Beyond that it maybe gets more murky/confusing…

@leastbad
Copy link
Contributor Author

It's 100% opt-in - you don't have to define to_operation if you don't want this functionality.

Could you go into a bit more detail on the edge cases that you're concerned about? I'm really excited to use this...

@existentialmutt
Copy link
Contributor

@leastbad this looks really great! to_operations does take a minute to grok but is a really powerful way to give models full control of their destiny. I think it's worth going for even if not everyone feels comfortable using it. Like you said, it's optional and the keyword method will be a great choice too!

@leastbad leastbad added this to the 5.0 milestone Jun 30, 2021
@hopsoft
Copy link
Contributor

hopsoft commented Jul 17, 2021

I'd like to also support a more explicit Hash return value for to_operation as well.

I also propose renaming this method to to_operation_payload given that an "operation" is really the combination of an identifier + payload.

# lets keep this shorthand syntactic sugar
def to_html
  "<p>neato</p>"
end

def to_dom_id
  "example"
end

def to_selector
  "#" + to_dom_id
end

def to_operation_payload
  [:html, :dom_id]
end
# but also support returning an explicit hash
def to_operation_payload
  {    
    dom_id: "example",
    selector: "#example",
    html: "<p>neato</p>",
 }
end

@leastbad leastbad requested a review from hopsoft July 17, 2021 21:00
@leastbad leastbad merged commit 11e6922 into stimulusreflex:master Jul 17, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement proposal ruby Pull requests that update Ruby code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants