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

Forms API #1476

Closed
wants to merge 33 commits into from
Closed

Forms API #1476

wants to merge 33 commits into from

Conversation

dktapps
Copy link
Member

@dktapps dktapps commented Oct 17, 2017

No description provided.

@dktapps dktapps added Category: API Related to the plugin API PR: Contribution Status: Incomplete Work in progress labels Oct 17, 2017
@SOF3 SOF3 self-requested a review October 18, 2017 05:04
SOF3
SOF3 previously requested changes Oct 18, 2017
Copy link
Member

@SOF3 SOF3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There seems to be an inconsistency, where calling getters of form input without form submission sometimes gets a null but sometimes gets an InvalidStateException.
Throwing an exception may also result in some dependence (the assumption that "if I don't get this exception, I am OK"), which does not work as expected when someone tries to get the value from a form they resent but is not yet resubmitted.

// I have not reviewed the code under pocketmine\form\element yet.

}

/**
* Returns the index of the option selected by the user. Pass this to {@link getButton} to get the button object
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead link to getButton. I believe this should be Pass this to {@link MenuForm::getOption} to get the MenuOption object.

public function getSelectedOption() : MenuOption{
$index = $this->getSelectedOptionIndex();
if($index === null){
throw new \InvalidStateException("No option selected, maybe the form hasn't been submitted yet");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider changing the message to Form is closed or has not been submitted yet to be more precise.

* Sets the selected option to the specified index or null. null = no selection.
* @param int $option
*/
public function setSelectedOptionIndex(int $option) : void{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a practical reason to expose this method publicly?

*
* @param bool $choice
*/
public function setChoice(bool $choice) : void{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to expose this method publicly?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plugins may want to modify form result data when it's submitted (there will be events for this)

/**
* Clears response data from a form, useful if you want to reuse the same form object several times.
*/
public function clearResponseData() : void{
Copy link
Member

@SOF3 SOF3 Oct 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is very confusing. It is not called by the server, but relies on the developer to call it.

This leads to inconsistency:

  1. Is a Form object expected to be reused for a single player?
  2. Is a Form object expected to be reused for multiple players?
  3. Can the developer use the form values outside onSubmit()?

The answer is:

  • If the same Form instance is only sent once, you can use the form values outside onSubmit() and hold the Form instance forever.
  • If the same Form instance is sent more than once (same player or not), it would be a very bad practice to depend on the value held in the Form instance outside onSubmit(), even in the same tick.

It is particularly tempting to hold a CustomForm instance and use the values there later, since the data cannot be easily extracted. I can foresee this is going to cause a lot of concurrency bugs that can be very difficult to identify. Therefore, it is suggested that, starting from the core, one of either approach ("resend same instance" or "new instance each resend") be adopted to avoid confusion.

  • Adopting the "resend same instance" approach should automatically call the clearResponseData() method after each onSubmit() call.
  • Adopting the "new instance each resend" approach should remove this method and add a flag to identify a Form as sent and throw an exception when attempting to use sendForm() with it.

Deciding which approach to adopt should require extensive consultation.

return $json;
}

}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about also adding an empty public function onSelected(), which is called before/after MenuForm::onSubmit() is called? This can allow developers to write their handlers in the menu options instead of from the big MenuForm object, resulting in extra routing work. Without adding this in the core, developers have to add a few lines of routing code + assertion that the MenuOption::onSelected() method exists in order to put the handler code with the MenuOption.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

before, or after? If a developer does it themselves, they are explicit about when they want it to be called, which can be any time - if the core does it, then developers have to get used to BEFORE, or AFTER, no in-betweens.

* Represents a custom form which can be shown in the Settings menu on the client. This is exactly the same as a regular
* CustomForm, except that this type can also have an icon which can be shown on the settings section button.
*/
class ServerSettingsForm extends CustomForm{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All other Form classes are abstract. Why is this suddenly non-abstract?

* @param string[] $steps
* @param int $defaultStepIndex
*/
public function __construct(string $text, array $steps, int $defaultStepIndex = 0){
Copy link
Contributor

@inxomnyaa inxomnyaa Oct 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling it Steps makes it confusing - If possible, rename to $options. A dropdown is a list of options that you can choose from, not a slider

@@ -3279,6 +3279,11 @@ public function sendWhisper(string $sender, string $message){
}

public function sendForm(Form $form) : void{
if($form->hasBeenSent()){
throw new \InvalidArgumentException("Cannot send the same form more than once, create a new one instead");
Copy link
Contributor

@inxomnyaa inxomnyaa Oct 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But: Why? So i can not cache a Form and send it to a player? So it has to be recreated (rebuildt in the core) every time? Isn't that just a waste of calculation if i have a form with never changing contents?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The core code has no foolproof way to tell if your form is immutable (so to speak). You can clone the form object if you're concerned about the overhead.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have already explained in my previous comment:

It is particularly tempting to hold a CustomForm instance and use the values there later, since the data cannot be easily extracted. I can foresee this is going to cause a lot of concurrency bugs that can be very difficult to identify. Therefore, it is suggested that, starting from the core, one of either approach ("resend same instance" or "new instance each resend") be adopted to avoid confusion.

  • Adopting the "resend same instance" approach should automatically call the clearResponseData() method after each onSubmit() call.
  • Adopting the "new instance each resend" approach should remove this method and add a flag to identify a Form as sent and throw an exception when attempting to use sendForm() with it.

As it has been noticed that resetting contents each time makes it very inconvenient to retain data (especially inconvenient for custom form data), the "new instance each resend" approach is considered more appropriate.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the overhead of form creation overhead, perhaps you should compare it against encoding the form, sending it to the client, making the client display it and sending it back. As one wouldn't reasonably send forms very frequently, the impact on performance is negligible, so trying to cache forms for the sake of performance would be premature optimization, and therefore your comment is 4√(all evil), and so you are 8√(all evil).

@inxomnyaa
Copy link
Contributor

"Offtoppic" question: Could you merge the changes from master into the branch / update it?

abstract class ModalForm extends Form{

/** @var string */
private $content;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please concider adding a function to get/set the content

@AyyItzJ4y
Copy link

how do you make a player open a form when they hold click a specific block

@inxomnyaa
Copy link
Contributor

Please, for questions head over to the Forums. This is a place to discuss the changes and issues, not some random guy's issues about not reading the docs / commits

@YaBoiLewis
Copy link

YaBoiLewis commented Oct 22, 2017

I believe image handling on UI has been added, correct me if I'm wrong. If so, I suggest adding an image class. No unnecessary replies required. A yes or no would suffice.

@dktapps
Copy link
Member Author

dktapps commented Oct 22, 2017

Have eyes been added yet, if so, they should be used.

@dktapps
Copy link
Member Author

dktapps commented Oct 22, 2017

wishes github had a facepalm reaction

@dktapps
Copy link
Member Author

dktapps commented Dec 7, 2017

Aside from the bugs, some things we're not happy with yet:

  • We don't allow sending the same form instance twice. I don't think this is reasonable, a better restriction would be to only allow sending to one player at a time (makes more sense).
  • Leading on from the above point, the disposability of forms leads to the problem that a form cannot be easily copied, i.e. duplicating the form and erasing the data for reuse. This requires, then, that a form object must always be recreated. This is a little inconvenient.
  • Plugins wanting to access their owners inside a form currently have to implement their own methods for getting the plugin's PluginBase instance. For example, the following code:
$player->sendForm(new class($this, $username, $error) extends ModalForm{
	/** @var Main */
	private $plugin;

	public function __construct(Main $plugin, string $username, string $error){
		$this->plugin = $plugin;
		parent::__construct("Account transfer failed!", "Failed to transfer \"$username\" account to yours.\nError: $error\n\nWould you like to try again?");
	}

	public function onSubmit(Player $player) : ?Form{
		if($this->getChoice()){
			$this->plugin->requestAccountDetails($player);
		}else{
			$this->plugin->warnCancelTransfer($player);
		}
		return null;
	}
});

requires that the form being sent must declare a $plugin property and assign it in the constructor, in order to access it in onSubmit(). This leads to boilerplate code. However, I don't want to restrict forms to being a plugin-only thing - the server should be able to use them as well.

@inxomnyaa
Copy link
Contributor

Plugins wanting to access their owners inside a form currently have to implement their own methods for getting the plugin's PluginBase instance. For example, the following code:

Actually that applies to any parameter. Passing a variable that a player input into a form and displaying it in another form requires constructor rewrites. I suggest an additional parameter array $arguments = []

@SOF3
Copy link
Member

SOF3 commented Dec 13, 2017

@thebigsmileXD Explicitly requiring passing the Plugin also has the advantage that it's possible to know the scope of the form and manage it with plugin enable/disable stuff.

@NyaomiDEV
Copy link

NyaomiDEV commented Dec 15, 2017

Why don't make some events like PlayerFormSubmitEvent and PlayerFormCloseEvent to handle form submitting and closing? It would be a lot simple, especially if forms' classes are not abstract so we can make Form objects right out of the box and we can send them easily, without managing them by extending them with our own classes in plugins. For example, this piece of code in a class that extends Listener:

public function onSubmit(PlayerFormSubmitEvent $e){
    // assuming we are handling a Toggle element in index 0 that enables something
    /** @var Toggle $toggle */
    $toggle = $e->getForm()->getElement(0);
    $this->doSomethingToPlayer($e->getPlayer(), $toggle->getValue());
    $e->getPlayer()->sendForm(new CustomForm("Test Form", array(
        new Toggle("Test toggle 1", false),
        new Toggle("Test toggle 2", true)
    )));
}

will be MUCH simpler to implement for plugin developers as we don't have to pass the owner to our form to do things on onSubmit().
Also, if you think of a way to make forms instantiable out of the box then when we are sending a form to a player it would be a good idea to have a custom internal form name or ID to keep track of what form is being sent to the player; for example this piece of code:

public function onSubmit(PlayerFormSubmitEvent $e){
    // first we want to make sure the form we are receiving is a "TestToggleForm1" form
    if($e->getForm()->getInternalName() !== "TestToggleForm1") return;
    // then we manage to do something...

    // assuming we are handling a Toggle element in index 0 that enables something
    /** @var Toggle $toggle */
    $toggle = $e->getForm()->getElement(0);
    $this->doSomethingToPlayer($e->getPlayer(), $toggle->getValue());
    // maybe the constructor for a CustomForm object can be
    // "string $internalName, string $title, array $elements"
    $e->getPlayer()->sendForm(new CustomForm("TestToggleForm2", "Test Form 2", array(
        new Toggle("Test toggle 1", false),
        new Toggle("Test toggle 2", true)
    )));
}

Obviously this is more straight-forward but it will certainly have problems in more complicated plugins that use forms to do complicated things but I wrote this suggestion in a couple of minutes so it definitely needs some refining here and there but the main idea is there.

@SOF3
Copy link
Member

SOF3 commented Dec 16, 2017

@AryToNeX This will certainly make it much harder to pass extra data through the form.

@inxomnyaa
Copy link
Contributor

@AryToNeX I highly say NO THANKS to events. customui was discontinued just because events created plugin-crossover calls, and every plugin reacted to the event.

@dktapps
Copy link
Member Author

dktapps commented Dec 16, 2017

Events are a reasonable ask (since they are just, well, events) but definitely a bad idea to make them the primary way to handle a form.

public function onSubmit(PlayerFormSubmitEvent $e){
    // first we want to make sure the form we are receiving is a "TestToggleForm1" form
    if($e->getForm()->getInternalName() !== "TestToggleForm1") return;
    ...

here's the problem already.

@pranavbhuv
Copy link

Not meant to be rude, but, can we get this merged by New Year?

@xhaeffer
Copy link

Not meant to be rude, but this PR make teaspoon plugin error

@Sandertv
Copy link
Contributor

Plugins should adapt to the core, not the other way around. Furthermore, this is a development branch.

@dktapps
Copy link
Member Author

dktapps commented Dec 29, 2017

@WaldoFS that's entirely irrelevant to the discussion. TeaSpoon is its own project.

@Thunder33345
Copy link
Contributor

@WaldoFS Not meant to be rude, plugin are suppose to make themself work with the core, not the other way around

@xhaeffer
Copy link

Ok, thanks all 😘

@SOF3 SOF3 mentioned this pull request Jan 4, 2018
@GooglePlugin
Copy link

GooglePlugin commented Jan 18, 2018

I tested @thebigsmileXD 's WarpUI with this branch, but it didn't work properly.
Here are some bugs I found:

  • Forms doesn't appear after sending a form.
  • Form::onSubmit is not called after submitting a form.

@dktapps This has been W.I.P For almost THREE months. When will it be done?

@robske110
Copy link
Contributor

robske110 commented Jan 18, 2018

@GooglePlugin When somebody got time for it. Also, i think the problems you mentioned are probably WarpUI's problems.

@eqMFqfFd
Copy link
Contributor

How do you change the ‘Submit’ button to other text?

@Thunder33345
Copy link
Contributor

This is not a plugin support forum
please use forums.pmmp.io

@dktapps
Copy link
Member Author

dktapps commented Jan 18, 2018

When will it be done?

you must be new round here...

@GooglePlugin
Copy link

I haven't fully understand the rule here. Not meant to be rude, but I really look forward to this ;)

@SOF3
Copy link
Member

SOF3 commented Jan 19, 2018

It appears no constructive discussion is going on in this pull request, and therefore I am locking. If you have any further comments, please create a thread on the forums.

@pmmp pmmp locked as too heated and limited conversation to collaborators Jan 19, 2018
@dktapps
Copy link
Member Author

dktapps commented Apr 14, 2018

I'm going to close this since there hasn't been any movement on it recently. A new PR will be created when we have something worth shipping.

@dktapps dktapps closed this Apr 14, 2018
@dktapps dktapps added Resolution: Stale and removed Status: Incomplete Work in progress labels Apr 14, 2018
@dktapps dktapps added Type: Enhancement Contributes features or other improvements to PocketMine-MP and removed Type: Contribution labels Nov 26, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Category: API Related to the plugin API Resolution: Stale Type: Enhancement Contributes features or other improvements to PocketMine-MP
Projects
None yet
Development

Successfully merging this pull request may close these issues.