Skip to content

Routes for non lite apps

Sachin edited this page Nov 29, 2016 · 41 revisions

The advantage of routes

Instead of letting a web server like Apache decide which files to serve based on the provided URL, the whole work can be done by a Mojolicious application.

For example, a URL like

http://www.example.com/cities/paris.html

can provide content that has been retrieved from a database, or also content that has been fetched from another website in "real-time", or even continuously updating content retrieved from other users sitting in front of their computers.

So, Mojolicious allows you to display dynamic content in a search engine friendly way!

In order to achieve this, Mojolicious decides on it's own how to handle URLs and what to deliver as a result.

This is where routes come into play. Routes are kind of "rules" how to handle URLs. Mojolicious checks whether a specific URL matches a certain pattern (as defined in the route), and determines what happens if a match occurs (as also defined in the route).

For example, you can define Apache to handle all URLs, except requests for http://yourdomain.com/myapp, so that URLs like

http://yourdomain.com/myapp/users.html
http://yourdomain.com/myapp/data.html

are handled by Mojolicious.

Routes themselves are very dynamic (think of them as kind of simplified regular expressions), so another advantage of routes is that an infinite amount of URLs can be handled, without the need to place a file for each URL on your server .

For example, a route in the form

/:cities/

could be responsible to deliver content if a user requests

http://yourdomain.com/myapp/new_york.html
http://yourdomain.com/myapp/paris.html or
http://yourdomain.com/myapp/any_other_city.html

Finally, routes are reversible. Instead of hard copying URLs in your templates, you can use route names in templates, forms and redirects. If you decide to relocate the content (provide content under a different URL), you just need to modifiy the route, not the templates, forms etc.

The following guide is a step by step introduction to Mojolicious not-lite apps, with an emphasis on routes. It is recommended that you understand Mojolicious lite-applications first, before starting with non-lite Mojolicious applications.

For a more complete guide that demonstrates the full power of routes, also read the official Mojolicious Routing Guide.

Getting started with the built-in example

When you create a mojolicious (non lite) app through

mojo generate app Hello

a Hello.pm file is created in the /hello/lib folder.

The Hello.pm file contains the following route

$r->route('/:controller/:action/:id')->to('example#welcome', id => 1);

which acts as kind of a fallback default route.

You can now start the Morbo development server

morbo hello/script/hello

Server available at http://*:3000

and start playing around. Morbo notices changes in your files, eliminating the need to restart the app manually.

Enter

http://localhost:3000/welcome

in your browser and a "Welcome to the Mojolicious Web Framework!!" message should appear on your screen!

Calling this URL actually calls the welcome method of the example controller which is located at

hello/lib/Hello/Example.pm

Now lets change the URL a bit

http://localhost:3000/test/me

and a website not found (404) error should appear.

In order to get some content being displayed, we have to create a "Test" controller that contains the "me" method.

So create a file

hello/lib/Hello/Test.pm

which should look like this:

package Hello::Test;
use base 'Mojolicious::Controller';

sub me {
    my $self = shift;

    $self->render(text => 'The me method from controller test is called!');
}

1;

Hit the reload button or enter

http://localhost:3000/test/me

again and you should see the message "The me method from controller test is called!".

Let's look at the route again:

$r->route('/:controller/:action/:id')->to('example#welcome', id => 1);

Using routes that way, when you add a new controller "Foo" with an action "bar" it will always be available as /foo/bar right away.

Also check out the ->to part. to is just a set of default values.

So when you enter

http://localhost:3000/example/

the default "welcome" method (as defined in to) will be called.

"example#welcome" is just a shortcut for

controller=>'example', action=>'welcome'

so one could also write

$r->route('/:controller/:action/:id')
  ->to(controller=>'example', action=>'welcome', id => 1);

Or enter

http://localhost:3000/

and the default "welcome" method (as defined in to) of the default controller "example" (as also defined in to) will be called.

Now you can start to define more specific routes ahead of the existing route, e.g.

$r->route('/test/you/')->to('example#welcome');
$r->route('/:controller/:action/:id')->to('example#welcome', id => 1);

The request for

http://localhost:3000/test/you

would always use the same controller (example) and action (welcome).

Finally lets look at the "id" part of the route:

$r->route('/:controller/:action/:id')->to('example#welcome', id => 1);

Including an "id" in the URL has no effect on which controller or action is finally called. However, the "id" value is saved in the so called "stash".

To make things a bit more clear, we change the Test.pm controller a bit:

sub me {
    my $self = shift;

    $self->render(text => 
        'The me method from controller test is called, '.
        'the passed id is:'.$self->stash('id')
    );
}

Now http://localhost:3000/test/me

should output

"The me method from controller test is called, the passed id is:1"

As we haven't passed an id value, the default as defined in the "to" part of the route is used (value: 1).

http://localhost:3000/test/me/2

results in

"The me method from controller test is called, the passed id is:2"

Routes and file extensions

Let's create an app that provides you with information on the worlds biggest cities.

First, create a route

$r->route('/cities/:id/')->to('cities#show');

As explained in the previous example, routes for the "Hello" sample app are defined in

/hello/lib/Hello.pm

Also delete the routes created in the previous example, to avoid any conflicts, as we start with a fresh new example!

Now add a controller that can handle the route (at /hello/lib/Hello/Cities.pm):

package Hello::Cities;
use base 'Mojolicious::Controller';

sub show {
    my $self = shift;

    my $city_info = $self->stash('id');

    $self->render(text => $city_info);
}

1;

Note: make sure that the second part of the package name starts with a capital letter:

package Hello::Cities
not: package Hello::cities.

as the controller file name "Cities.pm" also starts with a capital letter.

Now enter

http://localhost:3000/cities/paris

and the word "paris" should appear on your screen!

The route

$r->route('/cities/:id/')->to('cities#show');

dispatches all requests to the "show" method in the "Cities" controller as defined in the "to" part of the route.

In contrast to :controller and :action (used earlier directly in the route), :id is NOT a reserved word and as a result, has no effect on which action or controller is called and also has no other side effects.

The :id placeholder in the route is saved in the so called stash and can be used in the "show" method of the "Cities" controller:

my $city_info = $self->stash('id');

$self->render(text => $city_info);

In our example, the value contained in :id is just saved in $city_info and than printed via render with the text stash value.

Enter

http://localhost:3000/cities/newyork

and the string "newyork" is delivered to your browser!

Lets modify the request a bit, add .html (dot html) to the end of the URL.

Reload or enter

http://localhost:3000/cities/newyork.html

again.

Surprise, the request still works and "newyork" is printed on the screen again.

Mojolicious is able to detect file extensions like .html and .txt at the end of a route automatically. Even better, the file extension is also saved in the stash and can be accessed in the controller methods.

For testing purposes, we modify the "show" method in the "Cities" controller

my $city_info = $self->stash('id');
my $format    = $self->stash('format') || 'no special format';
$self->render(text => $city_info.' format:'.$format);

Now the name of the city and the format should be displayed on your screen:

http://localhost:3000/cities/newyork.html

outputs: newyork format:html

http://localhost:3000/cities/newyork.txt

outputs: newyork format:txt

http://localhost:3000/cities/berlin

outputs: berlin format:no special format

Now we want to display information on each city. As we do not want to deal with real databases like MySQL at this point, we save all data in a Perl hash. Our controller should look like this now:

package Hello::Cities;
use base 'Mojolicious::Controller';

my %cities = (
    new_york => 'New York is famous for its Statue of Liberty.',
    paris    => 'Paris is famous for its Eiffel Tower.'
);

sub show {
    my $self = shift;

    my $city_info = $cities{ $self->stash('id') };

    if ( $city_info ){
        $self->render(text => $city_info);
    }
    else {
        $self->render(text => 'City not found!');
    }
}

1;

A bit off-topic:

What are we doing here? We just use $self->stash('id') (e.g. paris) as the hash key to get a corresponding hash value from the %cities hash. If the hash key exists and contains a true value, the hash value with information on the city is returned and rendered. It only works if the cities hash is defined as a "class hash". So after the "Cities" controller has been compiled and as long as the "Cities" controller exists in memory, the data is save. All this won't work if you define the cities hash directly in one of the methods (e.g. the "show" method), as the hash would only be alive for the time the method is executed. In real live, you should use a database, of course, to save data permanently!

Enter:

http://localhost:3000/cities/new_york.html

and "New York is famous for its Statue of Liberty." should appear on your screen!

Since simply rendering text is really "boring", we will set up templates first, before we learn more about routes.

We will modify our "Cities" controller file once again, now it looks like this:

package Hello::Cities;
use base 'Mojolicious::Controller';

my %cities = (
    new_york => 'New York is famous for its Statue of Liberty.',
    paris    => 'Paris is famous for its Eiffel Tower.'
);

sub show {
    my $self = shift;

    my $city_info = $cities{ $self->stash('id') };

    $self->render(text => 'City not found') unless $city_info;

    $self->stash( our_data => $city_info );

}

1;

Instead of renderring text (in case that city information exists), we now just put our $city_info as a hash into the stash (hash key is "our_data", hash value is $city_info). Do not try to load the URL right now! To get the whole thing working, we first have to create a template file.

So switch to the /hello/templates folder and create a new sub folder called "cities" (the name of our controller, but all lowercase).

Now create a template file called

show.html.ep

Full path is /hello/templates/cities/show.html.ep

"show" is the name of our action, "html" is the desired format, and "ep" is the templating language, in this case embedded perl (mojolious default templating language).

The template file should look like this:

<%= $our_data %>
(Called from our HTML template!)

That's it! Now reload or enter

http://localhost:3000/cities/new_york.html

You should see: "New York is famous for its Statue of Liberty. (Called from our HTML template!)"

Earlier, we have saved $cities_info to the stash, using "our_data" as the hash key. Now we can access this data from our templates via

$our_data

We put this variable between <%= %> tags to tell Mojolicious to print this perl var to the screen!

But Mojolicous is smart. We now add .txt instead of .html to the end!

http://localhost:3000/cities/new_york.txt

Loading the page results in a 404 error. This is because we have only created a template for the "html" format, but not for the "text" format!

Create a template for "txt" format, full path is

/hello/templates/cities/show.txt.ep

with similar content:

<%= $our_data %>
(Called from our TXT template!)

Enter

http://localhost:3000/cities/new_york.txt

again and the displayed message should look like this:

New York is famous for its Statue of Liberty.
(Called from our TXT template!)

You can enter a URL with a specific file extension (like txt or html) and the format is automatically detected! Even more important, the template name is automatically detected as well, based on the names of the controller, the action and the format! If you add the "html" extension, an html template is delivered (if it exists), and if you add the "txt" extension, than the text template is delivered, if it exists, and this also works with all kind of file extensions, like xml, jpeg and so on!

Mojolicious is smart: depending on what extension you request, it renders the correct template file!

Finally, let's add another route:

$r->route('/cities')->to('cities#index');

as well as a new "Cities" controller method called "index"

sub index {
    my $self = shift;

    $self->stash(cities => \%cities );
}

and another template:

/hello/templates/cities/index.html.ep

with the content:

List of all city IDs (names)<br />
% foreach my $key (keys %$cities) {
  %= link_to $key => "/cities/$key"
  <br />
% }

Now enter

http://localhost:3000/cities

or

http://localhost:3000/cities.html

and you should see a list of all cities with links to detailed information on the city.

Routes and request methods (via)

We will now not only display existing data, but create new data, of course with a tight focus on routes.

As we not only want to display information, but also create some, we create another route:

$r->route('/cities/new')->to('cities#new_form');

This route has to appear before

$r->route('/cities/:id/')->to('cities#show');

otherwise it would never match. The URL

http://localhost:3000/cities/new/

would (if done in wrong order) look for a city with the name "new", which is not what we want! So the order of routes is extremely important.

Now lets define a "new_form" method in our "Cities" controller:

sub new_form {
    my $self = shift;
}

and a template file, full path is:

/hello/templates/cities/new_form.html.ep

Put

Create a new city:

in this file.

Enter

http://localhost:3000/cities/new

in your browser and "Create a new city:" should appear on your screen.

Actually, the new_form method does nothing! So the question is: does all this still works if we remove the "new_form" method completely from our "Cities" controller? Let's do that. Remove the "new_form" method!

Enter

http://localhost:3000/cities/new

in your browser and an exception is thrown! Oh no.

So it doesn't work? It works. However, because of certain Perl limitations, the --reload option of our build-in server does not support this.

Second trial: restart the development server manually, that means stop the server via CTRL+C, then

perl hello/script/hello daemon --reload

again.

Now

http://localhost:3000/cities/new

and "Create a new city:" should appear on your screen again.

So basically, it's enough to define a route and a template, a controller method is only required if you want to change something!

Our goal is to display a form that allows us to enter the name of a new city as well as additional information on the city.

The data of the new city than should be send back to the path

http://localhost:3000/cities

and saved to our database (in our case, actually to our hash, see chapter before)!

However, there is a problem: the path

http://localhost:3000/cities

is already used to show a list of all cities (dispatched to the "index" method in our Cities controller)!

So how is it possible to use the same URL for just showing a list of cities and for adding a new city at the same time?

Answer: by implementing different behaviour based on the request method!

If you enter

http://localhost:3000/cities

a so called "get request" is processed. In this case, we just want to display a list of cities. Get requests are useful if you do NOT want to change the state of your data (RESTful behaviour).

So let's modify the route:

$r->route('/cities')->via('get')->to('cities#index');

The route will only match if http://localhost:3000/cities is requested via GET, e.g. by just typing in your browser:

http://localhost:3000/cities

The route will not match, if http://localhost:3000/cities is requested via POST (which will be the case when we submit our form data).

Try it out, everything should still work.

Now let's change the "new_form" template a bit. We want to add two input fields and a submit button, and in order to do this, we use the Mojolicious tag helper. The new_form template located at

/hello/templates/cities/new_form.html.ep

should now look like this:

Create a new city:
<%= form_for '/cities' => (method => 'post') => begin %>
    Name: <%= text_field 'name' %>
    Info: <%= text_field 'info' %>
    <%= submit_button 'Submit' %>
<% end %>

"form_for" creates a HTML form tag, submitted data is send to 'http://localhost:3000/cities', and the information is send via the "POST" method.

Enter

http://localhost:3000/cities/new

and you should see two input fields and a submit button!

Press the submit button and you will see a 404 not found error, even though the URL

http://localhost:3000/cities

is requested by your browser.

This is because so far we do not have a route that matches our POST request for http://localhost:3000/cities correctly (just for get requests).

So let's create a fourth route:

$r->route('/cities')->via('post')->to('cities#create');

From now on, all post requests are dispatched to controller "Cities" and method "create".

Now, we finally need a "create" method in the "Cities" controller, otherwise we would have a route, but still get a 404 not found error:

sub create {
    my $self = shift;

    my $new_city_name = $self->param('name');
    my $new_city_info = $self->param('info');

    $cities{$new_city_name} = $new_city_info;

    $self->redirect_to('/cities/'.$self->param('name'));

}

In order to make the following example work, our daemon script needs to be restarted without the --reload option, temporarily, since this setting doesn't allow persistent data.

Again: restart the development server manually, that means stop the server via CTRL+C, then

perl hello/script/hello daemon

same as before, but without --reload option. Please note that this will stop later changes to apply instantly and you might have to restart the daemon script before each further test.

Then enter

http://localhost:3000/cities/new

and enter

"berlin" (in the name field),

"Berlin is famous for its Brandenburg Gate" (in the info field),

and press the "Submit" button!

You should then be redirected to

http://localhost:3000/cities/berlin

and "Berlin is famous for its Brandenburg Gate" should appear on your screen!

In the create method, we use the submitted value from the first form field "name" as the hash key ("berlin"), and the value from the second form field "info" ("Berlin is famous for its Brandenburg Gate") as the hash value for the %cities hash. That way, new data is added. Finally, a user is redirected to http://localhost:3000/cities/berlin after the data has been saved.

So, first of all, information on berlin has been saved to our %cities hash, the user is than redirected and a second request prints this information to our screen!

The value "berlin" has been saved into the cities hash successfully. However, it will only be there as long as your server is running and as long as files are not reloaded. If you restart your server, the cities hash will be set to it's initial state (containing just paris and new_york)! If you modify the "Cities" controller and the mojolicious daemon is started with the --reload option, the data is lost as well. Use a real database to save data permanently.

The via method allows to dispatch requests based on the request method. "GET" and "POST" are most common and supported by virtually all browsers. Use GET, if you just want to retrieve data (without changing it on the server side), use POST if you want to add data on the server (to your database).

Routes and names

Each route can be assigned a name via the "name" method.

Our route file contains the following routes:

$r->route('/cities')->via('get')->to('cities#index');
$r->route('/cities')->via('post')->to('cities#create');
$r->route('/cities/new')->to('cities#new_form');
$r->route('/cities/:id/')->to('cities#show');

Now, we give every route a name:

$r->route('/cities')->name('cities_index')
  ->via('get')->to('cities#index');

$r->route('/cities')->name('cities_create')
  ->via('post')->to('cities#create');

$r->route('/cities/new')->name('cities_new_form')
  ->to('cities#new_form');

$r->route('/cities/:id/')->name('cities_show')
  ->to('cities#show');

One of the main advantages of routes is that they are easily reversible, so with the help of a route, placeholders can be turned back into a path at any time.

Let's look at our new_form template:

Create a new city:
<%= form_for '/cities' => (method => 'post') => begin %>
    Name: <%= text_field 'name' %>
    Info: <%= text_field 'info' %>
    <%= submit_button 'Submit' %>
<% end %>

Now that we have a name for our route (that matches the path "/cities"), we include it in the form_for helper (replacing the path /cities/create ) and our template file looks like this:

Create a new city:
<%= form_for 'cities_create' => (method => 'post') => begin %>
    Name: <%= text_field 'name' %>
    Info: <%= text_field 'info' %>
    <%= submit_button 'Submit' %>
<% end %>

Also look at the "create" method in the "Cities" controller:

sub create {
    my $self = shift;

    my $new_city_name = $self->param('name');
    my $new_city_info = $self->param('info');

    $cities{$new_city_name} = $new_city_info;

    $self->redirect_to('/cities/'.$self->param('name'));

}

The redirect_to method makes sure that we are redirected to "/cities/new_town" after the data has been saved.

Lets modify the method a bit:

$self->redirect_to('cities_show', id => $self->param('name') );

So instead of passing a path, we now pass the name of the route along with a value for the :id placeholder (that is part of that route).

Finally, lets look at the index.html.ep template:

List of all city IDs (names)<br />
% foreach my $key (keys %$cities) {
  %= link_to $key => "/cities/$key"
  <br />
% }

Just replace the part after the link_to helper:

List of all city IDs (names)<br />
% foreach my $key (keys %$cities) {
    %= link_to $key => "cities_show", { id => $key }
    <br />
% }

Still everything works fine! Try it!

Suddenly, you decide that you no longer want your data to be located under the "/cities" path, but under "/towns" !

Now, change all the routes:

$r->route('/towns')->name('cities_index')
  ->via('get')->to('cities#index');

$r->route('/towns')->name('cities_create')
  ->via('post')->to('cities#create');

$r->route('/towns/new')->name('cities_new_form')
  ->to('cities#new_form');

$r->route('/towns/:id/')->name('cities_show')
  ->to('cities#show');

and check out if everything still works fine, e.g. enter

http://localhost:3000/towns/new

and save some data, e.g. name: "hamburg", info: "Hamburg is famous for its harbour promenade!" Press "Submit" and watch at the output and the paths...

Everything still works fine! You should get redirected to

http://localhost:3000/towns/hamburg

So by just changing the routes, the whole app is now available under http://localhost:3000/towns.

Without changing the new_form template, data is send to the correct path.

Without changing the "create" method in our "Cities" controller, we are still redirected to the correct path (/towns/new_city).

So avoiding absolute paths, and giving your routes names, makes a lot of sense!

But be careful: route names have to be unique (per application).

So if you give names to routes, they have to be individual enough to not get in conflict with route names within the same application that might be added in the future.

E.g. if you start to give names to routes, "new_form" could be too general as there might be lots of forms in the future that allow to create something, so cities_new_form might be more appropriate!

Also keep in mind: routes don't have anything to do with controllers per se. For example, one route can have multiple controllers (when using :controller as a placeholder).

ResourcefulRoutes Plugin

In many cases, you have to create the same route structures again and again. Referring to the "cities" example above, you need routes

- to list all cities,

- to display information on a single city,

- to display forms that allow you to create, update and delete cities and routes to
   
- create,

- update and

- delete cities. This is a total of 8 routes.

Now lets say you also want to create a database of users. That would require the creation of even more routes, which would be very similar to the "cities" routes created earlier.

So to simplify the annoying task of route creation, we use the ResourcefulRoutes plugin, which is part of MojoX-Scaffold.

The ResourcefulRoutes plugin allows you to create a bunch of routes by just passing the name of a resource.

Staying with our example, the resource would be called "cities".

So first of all, install the ResourcefulRoutes plugin http://github.com/forwardever/mojox-scaffold.

After that, remove all the routes from

/hello/lib/Hello.pm

and as a replacement, just add these 2 lines to the "startup" method:

$self->plugin('resourceful_routes');
$self->resources('cities');

Our file should look like this now:

package Hello;

use strict;
use warnings;

use base 'Mojolicious';

# This method will run once at server start
sub startup {
    my $self = shift;

    $self->plugin('resourceful_routes');
    $self->resources('cities');

}

1;

Now check if everything still works a expected, e.g. enter

http://localhost:3000/cities

to get a list of cities, or enter

http://localhost:3000/cities/new

to create a new city.

Everything still works fine! Our plugin has created a bunch of routes using certain naming conventions. The following routes are generated by $self->resources('cities'); :

# GET /cities/new - form to create a city
$r->route('/cities/new')
  ->via('get')
  ->to(controller => 'cities', action => 'new_form')
  ->name('cities_new_form');

# GET /cities/123 - show city with id 123
$r->route('/cities/:id')
  ->via('get')
  ->to(controller => 'cities', action => 'show')
  ->name('cities_show');

# GET /cities/123/edit - form to update a city
$r->route('/cities/:id/edit')
  ->via('get')
  ->to(controller => 'cities', action => 'edit_form')
  ->name('cities_edit_form');

# GET /cities - list of all cities
$r->route('/cities')
  ->via('get')
  ->to(controller => 'cities', action => 'index')
  ->name('cities_index');

# POST /cities - create new city
$r->route('/cities')
  ->via('post')
  ->to(controller => 'cities', action => 'create')
  ->name('cities_create');

# PUT /cities/123 - update a city
$r->route('/cities/:id')
  ->via('put')
  ->to(controller => 'cities', action => 'update')
  ->name('cities_update');

# DELETE /cities/123 - delete a city
$r->route('/cities/:id')
  ->via('delete')
  ->to(controller => 'cities', action => 'delete')
  ->name('cities_delete');

As you can see, several assumptions are made.

All requests, e.g.

http://localhost:3000/cities or

http://localhost:3000/cities/paris

are dispatched to a controller called "Cities".

Furthermore, each request is dispatched to a certain method in the "Cities" controller. For example,

http://localhost:3000/cities/paris

is dispatched to the Cities controller's "show" method. As a result, the content is finally displayed in the template with the name: show.html.ep

In order to make things work, you have to create a template following the naming convention so that the route works correctly (and in addition to that, a show "method" in the "Cities" controller if you want to change data before it is passed to the template). All this has already be done in previous examples.

Here is an overview:

HTTP     URL (not including      Controller  Method          Route Name
request  http://localhost:3000)
method          

GET      /cities/new             cities      new_form        cities_new_form
GET      /cities/paris           cities      show            cities_show
GET      /cities/paris/edit      cities      edit_form       cities_edit_form
GET      /cities/paris/delete    cities      delete_form     cities_delete_form
GET      /cities                 cities      index           cities_index
POST     /cities                 cities      create          cities_create
PUT      /cities/paris           cities      update          cities_update
DELETE   /cities/paris           cities      delete          cities_delete

Now, you don't have to make the same decisions again and again when it comes to defining routes. With a simple command like $self->resources('cities'); you can create a bunch of routes, making the creation of routes fun. All routes are RESTful, by the way, meaning that e.g. updates or done via PUT requests only, while the creation of new requests is done via POST.

As you can see above, the "/cities" path behaves differently depending on the http request method. E.g., if you request "/cities" via GET, the "index" method is called (which usually will show you a list of all cities). If you request "/cities" via POST, the create method is called, which is responsible to create a new city (the missing data like the name of the missing city has to be submitted by a http request parameter, which has to be included in the html form).

There are also two request methods (PUT and DELETE) which are not supported by the old HTML standard. In order to make these methods work, a workaround is necessary! You have to include a "_method" parameter in the HTML form. As a result, data is actually send via POST, but internally transformed to PUT and DELETE requests by the ResourcefulRoutes plugin.

To make things a bit clearer, we now finalize our "Hello" application.

The routes are already working, so let's create an "edit_form" method in our "Cities" controller to read the existing data from our database (actually from our cities hash) and save that data in the stash to make it accessible from our template:

sub edit_form {
    my $self = shift;
    my $city_info = $cities{ $self->stash('id') };
    $self->stash(info => $city_info);
}

Now we still need a template to display a form that allows us to edit the data of a city. The template should be located at

/hello/templates/cities/edit_form.html.ep

and contain:

Update city:
<%= form_for 'cities_update', {id => $id}, method => 'post' => begin %>
    Name: <%= text_field 'name', value => $id %>
    Info: <%= text_field 'info', value => $info %>
    <%= hidden_field '_method' => 'put' %>
    <%= submit_button 'Submit' %>
<% end %>

As you can see, we still submit data via POST (instead of PUT), however, there is a hidden form parameter that allows our plugin to transform the POST request to a PUT request before the data is finally dispatched by Mojolicious!

In order to remain "RESTful", updates have to be done via PUT.

Now enter

http://localhost:3000/cities/paris/edit

and you should see "Update city:", two input forms with the existing data on Paris and a submit button.

Before hitting the submit button, we also have to create an "update" method in our "Cities" controller, so that our app knows what to do!

sub update {
    my $self = shift;

    my $new_city_name = $self->param('name');
    my $new_city_info = $self->param('info');

    if ( $cities{$new_city_name} ){
        $cities{$new_city_name} = $new_city_info;

        $self->redirect_to('cities_show', id => $self->param('name') );
    }
    else {
        $self->render(text => 'city not found');
    }
}

So in case that our city already exists in our city hash, we just update the value, otherwise, a "city not found" message should be displayed!

Now enter

http://localhost:3000/cities/paris/edit

and put some stuff in the second form field (info), e.g. "Paris is cool!").

Press the submit button, and you should be redirected to

http://localhost:3000/cities/paris

and see "Paris is cool! (Called from our HTML template!)" on your screen. The content has been updated.

Clone this wiki locally