Skip to content

Multi screen Applications

Ryan Neufeld edited this page Jul 9, 2013 · 7 revisions

To follow along with this section, start with tag v2.1.2.

Creating a single-page application with only one screen is hard enough. Creating one with multiple screens is much more difficult. In addition to managing application state you now also have to know which screen is active and which changes to state effect the active screen.

Pedestal-app was designed to create applications with multiple screens without making it harder to manage state. The feature in pedestal-app which supports this is called Focus.

In this section you will add a login screen to the application which allows a player to enter their name before a game starts. While making this change, you will see how to use Focus to control which part of the application model is visible and will report changes. You will also see how pedestal-app's workflow allows you to incrementally make this change, doing exactly one thing at a time.

Before making these changes, start up the Data UI. You can use this to watch and interact with the changes as they are made.

Enter and store a name

This new login feature will interact with the application by sending a message which contains the player's name.

{msg/topic [:login :name] :value "Feanor"}

The name will be stored at [:login :name]. Notice that a new top-level key has been introduced into the data model. The subtree under :login will contain all of the data which corresponds to the login screen. To allow for the login message above to be sent, a transform-enable delta should be emitted. This is exactly like what was done in the Increment the Counter section when the init-main emitter was created. Similar to init-main, create an init-login to initialize the login portion of the data model.

(defn init-login [_]
  [[:node-create [:login] :map]
   [:node-create [:login :name] :map]
   [:transform-enable [:login :name]
    :login [{msg/type :swap msg/topic [:login :name] (msg/param :value) {}}]]])

When creating a lot of structure like this, it can be easier to write it as nested maps. The version below will have exactly the same result.

(defn init-login [_]
  [{:login
     {:name
       {:transforms
         {:login [{msg/type :swap msg/topic [:login :name] (msg/param :value) {}}]}}}}])

Message parameters are used to indicate that the :value key should be supplied when the message is sent.

Configure the init-login emitter and allow changes at [:login :*] to be reported. Add the following to the top of the :emit section of the dataflow definition.

{:init init-login}
[#{[:login :*]} (app/default-emitter [])]

With these changes in place, the Data UI will now allow you to enter and store a player name.

Make this its own page

Ideally, a login form would appear on the page by itself and then, after submitting the form, the game would start. It would be nice to have two screens, one for login and another for the game.

To achieve this in pedestal-app, you use something called Focus. Focus allows you to view only part of the application model tree and then use set-focus to view another part of the tree. You can assign names to subtrees using the :focus key in the dataflow definition.

Add the configuration below to the dataflow definition.

:focus {:login [[:login]]
        :game  [[:main] [:pedestal]]
        :default :login}

The application model tree currently has three top-level nodes: :login, :game and :pedestal. This configuration associates the tree under :login with the name :login and the trees under :main and :pedestal with the name :game. These names can now be used to the set focus. The default focus will be set to :login. When the application starts you will only see changes to the tree under the [:login] node.

To change focus, send a special message with a topic of msg/app-model and type of :set-focus.

{msg/topic msg/app-model msg/type :set-focus :name :game}

The message above will change the focus to :game (which will now only show changes to the trees under the [:main] and [:pedestal] nodes).

Since messages are not currently being validated, this message should be sent at the same time that a player submits their name. To achieve this, add the message above to the vector of messages in init-login.

(defn init-login [_]
  [{:login
    {:name
     {:transforms
      {:login [{msg/type :swap msg/topic [:login :name] (msg/param :value) {}}
               {msg/type :set-focus msg/topic msg/app-model :name :game}]}}}}])

Try out these changes in the Data UI. You should now only see the login portion of the tree when the app begins. When you click on :login, a dialog will open asking for a user name. Enter a name and click continue to display the game screen.

One interesting thing about focus is that it allows for overlap. Two different screens can view the same subtree. Transitions between two screens that share a common subtree will not cause the common areas to be redrawn.

Using the player name

Now that the player name exists in the data model, it should be used instead of "Me". To do this, supply the name to the merge-counters function

(defn merge-counters [_ {:keys [me others login-name]}]
  (assoc others login-name me))

and update the config for merge-counters.

[{[:my-counter] :me [:other-counters] :others [:login :name] :login-name} [:counters]
 merge-counters :map]

Try this in the Data UI. When you login the name that you entered should now appear in the counter list instead of "Me".

Sharing names with other players

Having collected a player name and used it locally, it can now be sent to other players as well. In the tutorial-client.behavior namespace, the publish-counter function sends the message

{msg/type :swap msg/topic [:other-counters] :value count}

and the server supplies the session id to complete the path for another player. To distribute names you will now fill in the complete path when publishing the counter and remove the code from the service which adds the session id. This will mean that the game can't have two players with the same name, that feature is left as an exercise for the reader.

Update publish-counter to use the name in the path.

(defn publish-counter [{:keys [count name]}]
  [{msg/type :swap msg/topic [:other-counters name] :value count}])

Update the config for publish-counter to add the name to the input map.

:effect #{[{[:my-counter] :count [:login :name] :name} publish-counter :map]}

The Data UI can now be used to confirm that the correct message will be sent to the service when the local counter is updated.

Updating the service

In the tutorial-service project, update the tutorial-service.service namespace, removing the code from the publish function which adds the session id to the message topic. The new version of publish is shown below.

(defn publish
  "Publish a message to all other connected clients."
  [{msg-data :edn-params :as request}]
  (log/info :message "received message"
            :request request
            :msg-data msg-data)
  (let [session-id (or (session-from-request request)
                       (session-id))]
    (notify-all-others session-id
                       "msg"
                       (pr-str msg-data)))
  (ring-resp/response ""))

You can use curl to confirm that the server works correctly.

Next steps

It is not yet possible to test this new feature because the renderer for this application does not yet render the login screen. In the next two sections, you will create the login template and then render the login screen.

However, if you are so inclined, you could create a new aspect which combines the Data UI and the real back-end service. This would allow you to test this kind of feature addition without having to update the renderer.

The tag for this section is v2.1.3.

Home | Login Template | Rendering the Login Screen