rails-hyperstack
'gem' into the system.config/initializers/hyperstack.rb
file, and make sure that this line is not commented out:Ignore any comments saying that it should be commented out, this is a typo in the current installer
todo-demo
directory to execute various commands.todo-demo
directory. You will see folders like app
, bin
, config
and db
. These have all been preinitialized by Rails and Hyperstack gems.app/hyperstack/components/app.rb
file. It looks like this:"Todo App Coming Soon"
. You will see the display instantly change when you save the file.The Hyperstack UI is built from components. Each component is defined by a subclass of HyperComponent. In some cases there will only be one instance of the class displayed, and as we will see at other times the class is reused to display multiple components. If you are familiar with Rails or the MVC structure then you can think of Components as views that continuously update as the state of the application changes.
A Rails ActiveRecord Model is a Ruby class that is backed by a database table. In this example we will have one model class calledTodo
. When manipulating models, Rails automatically generates the necessary SQL code for you. So whenTodo.all
is evaluated Rails generates the appropriate SQL and turns the result of the query into appropriate Ruby data structures.
Todo.all
can be evaluated on the server or the client.completed
is treated as a real boolean, and will avoid having to check between false
and null
later on.models/todo.rb
to hyperstack/models
app/policies/application_policy
if you are interested.App
component's render method to:app.rb
with:app/hyperstack/components
folder:Hyperstack uses the following conventions to easily distinguish between HTML tags, application defined components and other helper methods:
HTML tags are in all caps Application components are CamelCased other helper methods are snake_cased
Rails uses the terminology params (short for parameters) which have a similar purpose to React props, so to make the transition more natural for Rails programmers Hyperstack uses params, rather than props.
param
macro which creates an accessor method of the same name within the component.Index
component mounts a new TodoItem
with each Todo
record and passes the Todo
to the TodoItem
component as the parameter.create
like you did before. You will see the new Todo is added to the list.INPUT
html tag to your TodoItem component like this:completed
checkbox changing state.event handler
for the change event:change
event update the todo, setting the completed attribute to the opposite of its current value. The rest of coordination between the database and the display is taken care of for you by the Hyperstack.completed
state of each Todo, and check on the rails console (say by checking Todo.last.completed
) and you will see that the value has been persisted to the database. You can also demonstrate this by refreshing the page.state
of some objects. External events, such as mouse clicks, the arrival of new data from the server, and even timers update the state
. Hyperstack recomputes whatever portion of the display depends on the state
so that the display is always in sync with the state
. In our case the objects are the Todo model and its associated records, which have a number of associated internal states
./all
will display all todos, /completed
will display the completed Todos, and of course /active
will display only active (or incomplete) Todos. We would also like the root url /
to be treated as /all
Todo.all
, Todo.completed
, and Todo.active
, and get the desired subset of Todos. You might want to try it now in the rails console.
Note: you will have to do a reload!
to load the changes to the Model./all
we want the Todo.all scope to be run;/completed
we want the Todo.completed scope to be run;/active
we want the Todo.active scope to be run;/
(by itself) then we should redirect to /all
.App
to look like this:Index
component to look like this:Header
components as before./
and if it does, redirect to /all
.Index
component, we route to it based on the URL. In this case if the url must look like /xxx
.Index
now includes (mixes-in) the Hyperstack::Router::Helpers
module which has methods like match
.:scope
. Route('/:scope', mounts: Index)
and match.params[:scope]
:Route
is checked. If it matches then the indicated component is mounted, and the match parameters are saved for that component to use./all
, to /completed
, to /active
, and see a different set of Todos. For example if you are displaying the /active
Todos, you will only see the Todos that are not complete. If you check one of these it will disappear from the list.Rails also has the concept of routing, so how do the Rails and Hyperstack routers interact? Have a look at the config/routes.rb file. You will see a line like this:get '/(*other)', to: 'hyperstack#app'
This is telling Rails to accept all requests and to process them using theHyperstack
controller, which will attempt to mount a component namedApp
in response to the request. The mounted App component is then responsible for further processing the URL.For more complex scenarios Hyperstack provides Rails helper methods that can be used to mount components from your controllers, layouts, and views.
link_item
method is just a helper method to save us some typing.link_item
method uses the path
argument to construct an HTML Anchor tag.camelize
.margin-right
is written marginRight
, and that 10px
can be expressed as the integer 10.HyperRouter::ComponentMethods
inside of class.NavLink
component:NavLink
component reacts to a click just like an anchor tag, but instead of changing the window's URL directly, it updates the HTML5 history object. Associated with this history is (hope you guessed it) state. So when the history changes it causes any components depending on the state of the URL to be re-rendered.EditItem
component so we can change the Todo title.todo
param which will be edited by the user;title
of the todo is displayed as the initial value of the input;todo
is updated.TodoItem
component replacingTodoItem
. We will call our state editing
. If editing
is true, then we will display the title in a EditItem
component, otherwise we will display it in a LABEL
tag. The user will change the state to editing
by double clicking on the label. When the user saves the Todo, we will change the state of editing
back to false. Finally we will let the user cancel the edit by moving the focus away (the blur
event) from the EditItem
. To summarize:true
.false
.blur
) from the Todo being edited: editing changes to false
.EditItem
component is going to communicate to its parent via two application defined events - saved
and cancel
.EditItem
component like this:EditItem
component after it is mounted. The jQ
method is Hyperstack's jQuery wrapper, and dom_node
is the method that returns the actual dom node where this instance of the component is mounted. This is the INPUT
html element as defined in the render method.saved!
line will fire the saved event in the parent component. Notice that the method to fire a custom event is the name of the event followed by a bang (!).blur
event handler and fire our cancel
event.TodoItem
component to react to three events: double_click
, saved
and cancel
.@editing
ivar.@editing
.TodoItem
component the value of @editing
controls whether to render the EditItem
or the INPUT, LABEL, and Anchor tags.@editing
(like all ivars) starts off as nil, when the TodoItem
first mounts, it renders the INPUT, LABEL, and Anchor tags. Attached to the label tag is a double_click
handler which does one thing: mutates the component's state setting @editing
to true. This then causes the component to re-render, and now instead of the three tags, we will render the EditItem
component.EditItem
component is the saved
and cancel
handler (which is shared between the two events) that mutates the component's state, setting @editing
back to false.mutate
method to signal Hyperstack that that state is changing.EditItem
component has a good robust interface. It takes a Todo, and lets the user edit the title, and then either save or cancel, using two custom events to communicate back outwards.EditItem
to create new Todos. Not only does this save us time, but it also insures that the user interface acts consistently.Header
component to use EditItem
like this:@new_todo
to a new unsaved Todo
item in the before_mount
lifecycle method.@new_todo
to EditItem, and when it is saved, we generate another new Todo and save it in the new_todo
state variable.Header
's state is mutated, it will cause a re-render of the Header, which will then pass the new value of @new_todo
, to EditItem
, causing that component to also re-render.:cancel
event handler.INPUT
tag's defaultValue
specially. It is only read when the INPUT
is first mounted, so it does not react to changes like normal parameters. Our Header
component does pass in new Todo records, but even though they are changing React does not update the INPUT.key
. React uses this to uniquely identify mounted components. It's used to keep track of lists of components, in this case it can also be used to indicate that the component needs to be remounted when the value of key
is changed.to_key
method which will return a suitable unique key id, so all we have to do is pass todo
as the key param, this will insure that as todo
changes, we will re-initialize the INPUT
tag.todo.css
in the app/assets/stylesheets/
directory.class
parameter.App
component. With styling it will look like this:Footer
component needs to have a UL
added to hold the links nicely, and we can also use the NavLinks
active_class
param to highlight the link that is currently active:main
and todo-list
classes.class
along to the INPUT tag. We do this by adding the special other
param that will collect any extra params, we then pass it along in to the INPUT tag. Hyperstack will take care of merging all the params together sensibly.EditItem
component:EditItem
component. While we are at it we will add the H1 { 'todos' }
hero unit.Footer
component:EditItem
should display a meaningful placeholder hint if the title is blank:App
component add a guard so that we won't show the Footer if there are no Todos: