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

Form filling in and updating #37

Open
catmando opened this issue Aug 2, 2019 · 8 comments
Open

Form filling in and updating #37

catmando opened this issue Aug 2, 2019 · 8 comments

Comments

@catmando
Copy link
Contributor

catmando commented Aug 2, 2019

No description provided.

@explainer
Copy link

I can build a SignUp-Login form on W3 Schools and get the HTML, with no behavior.

HTML Forms

First name:

Last name:

email

Password:


If you click the "Submit" button, the form data will be sent to a page called "/action_page.php".

What I need to understand first is how to convert that HTML to a Component. Once I understand that then I need to get the data committed to an ActiveRecord instance.

I don't currently grok how components map to AR fields, and I don't know if the form is a single FORM component, or if each field is a separate component. I am confused.

@catmando
Copy link
Contributor Author

catmando commented Aug 3, 2019

Lets break it down:

  1. Mapping an HTML form and tags to a components
  2. Mounting our component
  3. What is the equivalent to submitting a form but in an SPA?
  4. What is the relationship between components and ActiveRecord

I'll add a comment for each these below.

@catmando
Copy link
Contributor Author

catmando commented Aug 3, 2019

Mapping an HTML form and tags to a component

Here is a typical HTML login form:

stolen shamelessly from https://www.w3schools.com/howto/howto_css_signup_form.asp

<form action="action_page.php" style="border:1px solid #ccc">
  <div class="container">
    <h1>Sign Up</h1>
    <p>Please fill in this form to create an account.</p>
    <hr>

    <label for="email"><b>Email</b></label>
    <input type="text" placeholder="Enter Email" name="email" required>

    <label for="psw"><b>Password</b></label>
    <input type="password" placeholder="Enter Password" name="psw" required>

    <label for="psw-repeat"><b>Repeat Password</b></label>
    <input type="password" placeholder="Repeat Password" name="psw-repeat" required>

    <label>
      <input type="checkbox" checked="checked" name="remember" style="margin-bottom:15px"> Remember me
    </label>

    <p>By creating an account you agree to our <a href="#" style="color:dodgerblue">Terms & Privacy</a>.</p>

    <div class="clearfix">
      <button type="button" class="cancelbtn">Cancel</button>
      <button type="submit" class="signupbtn">Sign Up</button>
    </div>
  </div>
</form>

To begin our conversion we need some place to put our code. In Hyperstack you break the UI up into components. You could of course build the entire UI as one giant component, but that would be hard to maintain, so we want to break up our app into smaller chunks of code that represent a smaller, testable portion of the UI. The above form makes a perfect sized component and we will call it LoginForm and it will look like this:

class LoginForm < HyperComponent
  render do 
    # our form goes here
  end
end

Each component is defined as a Ruby class that inherits from the applications base component class called HyperComponent. A component class (like any class) defines how instances of the class will behave. To actually display a component you need to create an instance by mounting it someplace on the UI. More on that later.

Converting our form to Ruby syntax is straight forward. HTML tags become method calls, and are written ALLCAPS. Attributes to the HTML tags are sent as a hash data. Any nested tags are placed in the methods block. For example:

<div class="clearfix">
    <button type="button" class="cancelbtn">Cancel</button>
    <button type="submit" class="signupbtn">Sign Up</button>
</div>

becomes

DIV(class: :clearfix) do 
  BUTTON(type: :button, class: :cancelbtn) { 'Cancel' }
  BUTTON(type: :submit, class: :signupbtn) { 'Sign Up' }
end

Note that Ruby allows us to define blocks either as do...end or { ... }. Ruby style convention suggests that { ... } be used for single line blocks, and do...end be used for multiple line blocks, but otherwise they are equivalent.

Also note that in Hyperstack strings and symbols are also equivalent, so that writing class: :clearfix is the same as class: 'clearfix'

Styles are translated to a hash, and when text is mixed with tags, the text needs to be wrapped in span tags. So

<p>
  By creating an account you agree to our 
   <a href="#" style="color:dodgerblue">Terms & Privacy</a>.
</p>

becomes

P do 
  SPAN { 'By creating an account you agree to our ' } 
  A(href: '#', style: {color: :dogerblue}) { 'Terms & Privacy' }
  SPAN { '.' }
end

Okay with this in mind we can convert the form to a complete Hyperstack component:

class LoginForm < HyperComponent
  render do 
    FORM(action: "action_page.php", style: {border: 1 solid: '#ccc'}) do
      DIV class: :container
        H1 { 'Sign Up' }
      P { 'Please fill in this form to create an account.' }
      HR()  # if there are no params and no block you must include empty ()

      LABEL(for: :email) { B { 'Email' } }

      # the following is the same as saying INPUT(required: true, ...)
      INPUT(:required, type: :text, placeholder: "Enter Email", name: email)

      LABEL(for: :psw) { B { 'Password' } }
      INPUT(:required, type: :password, placeholder: "Enter Password", name: :psw)

      LABEL(for: :psw_repeat) { B { 'Repeat Password' } }
      INPUT(:required, type: :password, placeholder: "Repeat Password", name: :psw_repeat)

      LABEL do
        # note that the style property margin-bottom becomes marginBottom
        INPUT(type: :checkbox, checked: :checked, name: :remember, style: {marginBottom: 15})
        SPAN { 'Remember me' }
      end

      P do
        SPAN { 'By creating an account you agree to our ' }
        A(href: "#", style: {color: :dodgerblue}) { 'Terms & Privacy' }
        SPAN { '.' }
      end

      DIV(class: :clearfix)  do
        BUTTON(type: :button, class: :cancelbtn) { 'Cancel' }
        BUTTON(type: :submit, class: :signupbtn) { 'Sign Up' }
      end
    end
  end
end

@explainer
Copy link

explainer commented Aug 3, 2019 via email

@catmando
Copy link
Contributor Author

catmando commented Aug 3, 2019

Mounting our component

The LoginForm defines our components behavior, but to actually display the component we will have to mount it someplace in our UI. This will be dependent on how we want our overall app to behave but for now lets continue to emulate https://www.w3schools.com/howto/howto_css_signup_form.asp and create a simple wrapper and button that brings up our LoginForm in a modal dialog when a "Signup" button is clicked.

As this will represent our top level app we will call it simply App

class App < HyperComponent
  render do 
    if @show_signup 
      DIV(class: :modal) do 
        SPAN(class: close, title: "Close Modal") { '&times;' }
        .on(:click) { toggle :show_signup }
        LoginForm()
      end
    else
      H2 { 'Modal Signup Form' }
      BUTTON { 'Sign Up' }
      .on(:click) { toggle :show_signup }
    end
  end
end

If you look at https://www.w3schools.com/howto/tryit.asp?filename=tryhow_css_signup_form_modal you will see that our implementation of the "wrapper" is different. In the w3 schools example they directly use the onclick event to modify the style of the modal div making it show and hide.

While this is possible in Hyperstack we would much prefer to use states to control how things look. So we have a single state variable @show_signup and toggle it from true to false, when either the signup button or the the close modal device is clicked.

Depending on the state we either show the "Sign Up" button or show the LoginForm component.

The final step here is to "mount" this top level App component in a test framework. The easiest way to do this is to create a new Hyperstack rails app following these instructions: https://github.com/hyperstack-org/hyperstack/blob/edge/install/readme.md

Once you have built a new blank app, replace the contents of app/hyperstack/components/app.rb with the above code, and add the InputForm component to a file named app/hyperstack/components/input_form.rb

Finally copy the following style sheet (from the W3 example) to the app/assets/stylesheets/application.css file:

body {font-family: Arial, Helvetica, sans-serif;}
* {box-sizing: border-box;}

/* Full-width input fields */
input[type=text], input[type=password] {
  width: 100%;
  padding: 15px;
  margin: 5px 0 22px 0;
  display: inline-block;
  border: none;
  background: #f1f1f1;
}

/* Add a background color when the inputs get focus */
input[type=text]:focus, input[type=password]:focus {
  background-color: #ddd;
  outline: none;
}

/* Set a style for all buttons */
button {
  background-color: #4CAF50;
  color: white;
  padding: 14px 20px;
  margin: 8px 0;
  border: none;
  cursor: pointer;
  width: 100%;
  opacity: 0.9;
}

button:hover {
  opacity:1;
}

/* Extra styles for the cancel button */
.cancelbtn {
  padding: 14px 20px;
  background-color: #f44336;
}

/* Float cancel and signup buttons and add an equal width */
.cancelbtn, .signupbtn {
  float: left;
  width: 50%;
}

/* Add padding to container elements */
.container {
  padding: 16px;
}

/* The Modal (background) */
.modal {
  display: none; /* Hidden by default */
  position: fixed; /* Stay in place */
  z-index: 1; /* Sit on top */
  left: 0;
  top: 0;
  width: 100%; /* Full width */
  height: 100%; /* Full height */
  overflow: auto; /* Enable scroll if needed */
  background-color: #474e5d;
  padding-top: 50px;
}

/* Modal Content/Box */
.modal-content {
  background-color: #fefefe;
  margin: 5% auto 15% auto; /* 5% from the top, 15% from the bottom and centered */
  border: 1px solid #888;
  width: 80%; /* Could be more or less, depending on screen size */
}

/* Style the horizontal ruler */
hr {
  border: 1px solid #f1f1f1;
  margin-bottom: 25px;
}
 
/* The Close Button (x) */
.close {
  position: absolute;
  right: 35px;
  top: 15px;
  font-size: 40px;
  font-weight: bold;
  color: #f1f1f1;
}

.close:hover,
.close:focus {
  color: #f44336;
  cursor: pointer;
}

/* Clear floats */
.clearfix::after {
  content: "";
  clear: both;
  display: table;
}

/* Change styles for cancel button and signup button on extra small screens */
@media screen and (max-width: 300px) {
  .cancelbtn, .signupbtn {
     width: 100%;
  }
}

Your UI of your signup form is now complete.

@Tim-Blokdijk
Copy link

I'm interested in:
3. What is the equivalent to submitting a form but in an SPA?
4. What is the relationship between components and ActiveRecord

I'm trying to build a page where multiple users can update ActiveRecord objects trough form inputs in real time. So no "save" button. Each change to the form is saved and synced. When multiple users mutate the same ActiveRecord object at the same time it should keep the data synced across users, overwriting local data as needed.

I have also been thinking about how to implement some sort of locking feature. The idea is that if one user has his cursor in a textarea, this texterea gets disabled (grayed out) on other clients. Preferably with some sort of indication which user has a lock on it.

@catmando
Copy link
Contributor Author

catmando commented Aug 6, 2019

DRAFT DRAFT DRAFT - not complete

What is the equivalent to submitting a form but in an SPA? - part 1 - a login form

This is where things get a bit tricky. The above code "works" - sort of. When you click to signup nothing happens, because the action on form submit is to post to action_page.php which does not exist in our app.

Before going further let's talk about what a form submit does: It takes all the input fields inside the form and packages them up as a set of parameters that are part of the post request. So when the form is submitted all the input fields arrive at the server neatly packaged. If you are using rails, then Rails takes care of unpacking the fields and assigning them to the controllers param object.

The Rails controller then processes the request, and typically would redirect to a new page where the new user is logged in.

So to get this work we could simply add a signup end point to our rails app by adding a controller, and a route to that controller, and then change the name action_page.php to whatever our url was.

This is not a bad way to do it, especially if you have an existing Rails app that you are converting to Hyperstack. However Hyperstack provides an alternative mechanism that avoids a lot of the excess baggage called Operations. We can define a signup operation like this:

class SignupOp < Hyperstack::ControllerOp 
  param :email 
  inbound :password  
  # here you can validate the params 
  step { User.create(email: params.email, password: params.password).id } 
end

@explainer
Copy link

explainer commented Aug 6, 2019 via email

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

No branches or pull requests

3 participants