1. Trang chủ
  2. » Công Nghệ Thông Tin

The definitive guide to grails second edition - phần 5 ppsx

58 372 0

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 58
Dung lượng 604,72 KB

Nội dung

212 CHAPTER 9 ■ CREATING WEB FLOWS Figure 9-2. Screenshot of the updates _album.gsp template Defining the Flow In the previous section, you created a <g:link> tag that referenced an action called buy. As you might have guessed, buy is going to be the name of the flow. Open grails-app/controllers/ StoreController and define a new flow called buyFlow, as shown in Listing 9-17. Listing 9-17. Defining the buyFlow def buyFlow { } Adding a Start State Now let’s consider the start state. Here’s a logical point to start: After a user clicks on the “Buy” button, the application should ask him whether he’d like to receive a CD version of the album. But before you can do that, you should validate whether he is logged in; if he is, you should place him into flow scope. To achieve this, you can make the first state of the flow an action state. Listing 9-18 shows an action state, called start, that checks if the user exists in the session object and triggers a login() event if not. Listing 9-18. Checking Login Details with an Action State 1 start { 2 action { 3 // check login status 4 if(session.user) { CHAPTER 9 ■ CREATING WEB FLOWS 213 5 flow.user = User.get(session.user.id) 6 return success() 7 } 8 login() 9 } 10 on('success') { 11 if(!flow.albumPayments) flow.albumPayments = [] 12 def album = Album.get(params.id) 13 14 if(!flow.albumPayments.album.find { it?.id == album.id }) { 15 flow.lastAlbum = new AlbumPayment(album:album) 16 flow.albumPayments << flow.lastAlbum 17 } 18 }.to 'requireHardCopy' 19 on('login') { 20 flash.album = Album.get(params.id) 21 flash.message = "user.not.logged.in" 22 }.to 'requiresLogin' 23 } The login event handler contains a transition action that places the Album instance into flash scope along with a message code (you’ll understand why shortly). The event then causes a transition to a state called requiresLogin, which is the first example of a redirect state. Listing 9-19 shows the requiresLogin state using the objects that were placed into flash scope to perform a redirect back to the display action of the AlbumController. Listing 9-19. Using a Redirect Action to Exit the Flow requiresLogin { redirect(controller:"album", action:"display", id: flash.album.id, params:[message:flash.message]) } Hold on a moment; the display action of the AlbumController doesn’t return a full HTML page! In the previous chapter, you designed the code to handle Ajax requests and return only partial responses. Luckily, Grails makes it possible to modify this action to deal with both Ajax and regular requests using the xhr property of the request object, which returns true if the request is an Ajax request. Listing 9-20 shows the changes made to the display action in bold. Listing 9-20. Adapting the display Action to Handle Regular Requests def display = { def album = Album.get(params.id) if(album) { def artist = album.artist 214 CHAPTER 9 ■ CREATING WEB FLOWS if(request.xhr) { render(template:"album", model:[artist:artist, album:album]) } else { render(view:"show", model:[artist:artist, album:album]) } } else { response.sendError 404 } } The code highlighted in bold changes the action to render a view called grails-app/views/ album/show.gsp if the request is a non-Ajax request. Of course, the shop.gsp view in question doesn’t exist yet, and at this point you can consider refactoring some of the view code devel- oped in the previous section. There is a lot of commonality not only for shop.gsp, but also for the pages of the buy flow. Currently the instant-search box and the top-five-songs panel are hard-coded into the grails-app/views/store/shop.gsp view, so start by extracting those into templates called _searchbox.gsp and _top5panel.gsp, respectively. Listing 9-21 shows the updated shop.gsp view with the extracted code replaced by templates highlighted in bold. Listing 9-21. Extracting Common GSP Code into Templates <html> <body id="body"> <h1>Online Store</h1> <p>Browse or search the categories below:</p> <g:render template="/store/searchbox" /> <g:render template="/store/top5panel" model="${pageScope.variables}" /> <div id="musicPanel"> </div> </body> </html> Notice how in Listing 9-21 you can pass the current pages’ model to the template using the expression pageScope.variables. With that done, you’re going to take advantage of the knowledge you gained about SiteMesh layouts in Chapter 5. Using the magic of SiteMesh, you can make the layout currently embedded into shop.gsp truly reusable. Cut and paste the code within shop.gsp into a new layout called grails-app/views/layouts/storeLayout.gsp, adding the <g:layoutBody /> tag into the “musicPanel” <div>. Listing 9-22 shows the new storeLayout.gsp file. CHAPTER 9 ■ CREATING WEB FLOWS 215 Listing 9-22. Creating a New storeLayout <html> <head> <meta http-equiv="Content-type" content="text/html; charset=utf-8"> <meta name="layout" content="main"> <title>gTunes Store</title> </head> <body id="body"> <h1>Online Store</h1> <p>Browse or search the categories below:</p> <g:render template="/store/searchbox" /> <g:render template="/store/top5panel" model="${pageScope.variables}" /> <div id="musicPanel"> <g:layoutBody /> </div> </body> </html> Notice how you can still supply the HTML <meta> tag that ensures the main.gsp layout is applied to pages rendered with this layout. In other words, you can use layouts within layouts! Now that you’ve cut and pasted the contents of shop.gsp into the storeLayout.gsp file, shop.gsp has effectively been rendered useless. You can fix that using the <g:applyLayout> tag: <g:applyLayout name="storeLayout" /> With one line of code, you have restored order; shop.gsp is rendering exactly the same con- tent as before. So what have you gained? Remember that when you started this journey, the aim was to create a grails-app/views/album/show.gsp file that the non-Ajax display action can use to render an Album instance. With a defined layout in storeLayout, creating this view is sim- ple (see Listing 9-23). Listing 9-23. Reusing the storeLayout in show.gsp <g:applyLayout name="storeLayout"> <g:if test="${params.message}"> <div class="message"> <g:message code="${params.message}"></g:message> </div> </g:if> <g:render template="album" model="[album:album]"></g:render> </g:applyLayout> Using the <g:applyLayout> tag again, you can apply the layout to the body of the <g:applyLayout> tag. When you do this in conjunction with rendering the _album.gsp tem- plate, it takes little code to render a pretty rich view. We’ll be using the storeLayout.gsp repeatedly throughout the creation of the rest of the flow, so stay tuned. 216 CHAPTER 9 ■ CREATING WEB FLOWS Returning to the start state of the flow from Listing 9-18, you’ll notice that the success event executes a transition action. When the transition action is triggered, it first creates an empty list of AlbumPayment instances in flow scope if the list doesn’t already exist: 11 if(!flow.albumPayments) flow.albumPayments = [] Then it obtains a reference to the Album the user wants to buy using the id obtained from the params object on line 12: 12 def album = Album.get(params.id) With the album in hand, the code on line 14 then checks if an AlbumPayment already exists in the list by executing a nifty GPath expression in combination with Groovy’s find method: 14 if(!flow.albumPayments.album.find { it?.id == album.id }) This one expression really reflects the power of Groovy. If you recall that the variable flow.albumPayments is actually a java.util.List, how can it possibly have a property called album? Through a bit of magic affectionately known as GPath, Groovy will resolve the expres- sion flow.albumPayments.album to a new List that contains the values of the album property of each element in the albumPayments List. With this new List in hand, the code then executes the find method and passes it a closure that will be invoked on each element in the List until the closure returns true. The final bit of magic utilized in this expression is the usage of the “Groovy Truth” (http://docs.codehaus.org/ display/GROOVY/Groovy+Truth). Essentially, unlike Java where only the boolean type can be used to represent true or false, Groovy defines a whole range of other truths. For example, null resolves to false in an if statement, so if the preceding find method doesn’t find anything, null will be returned and the if block will never be entered. Assuming find does resolve to null, the expression is then negated and the if block is entered on line 15. This brings us to the next snippet of code to consider: 15 flow.lastAlbum = new AlbumPayment(album:album) 16 flow.albumPayments << flow.lastAlbum This snippet of code creates a new AlbumPayment instance and places it into flow scope using the key lastAlbum. Line 15 then adds the AlbumPayment to the list of albumPayments held in flow scope using the Groovy left shift operator << — a neat shortcut to append an element to the end of a List. Finally, with the transition action complete, the flow then transitions to a new state called requireHardCopy on line 18: 18 }.to 'requireHardCopy' Implementing the First View State So after adding a start state that can deal with users who have not yet logged in, you’ve finally arrived at this flow’s first view state. The requireHardCopy view state pauses to ask the user CHAPTER 9 ■ CREATING WEB FLOWS 217 whether she requires a CD of the purchase sent to her or a friend as a gift. Listing 9-24 shows the code for the requireHardCopy view state. Listing 9-24. The requireHardCopy View State requireHardCopy { on('yes') { if(!flow.shippingAddresses) flow.shippingAddress = new Address() }.to 'enterShipping' on('no') { flow.shippingAddress = null }.to 'loadRecommendations' } Notice that the requireHardCopy state specifies two event handlers called yes and no reflect- ing the potential answers to the question. Let’s see how you can define a view that triggers these events. First create a GSP file called grails-app/views/store/buy/requireHardCopy.gsp. Remember that the requireHardCopy.gsp file name should match the state name, and that the file should reside within a directory that matches the flow id—in this case, grails-app/ views/store/buy. You will need to use the <g:link> tag’s event attribute to trigger the events in the requireHardCopy state, as discussed previously in the section on triggering events from the view. Listing 9-25 shows the code to implement the requireHardCopy view state. Listing 9-25. The requireHardCopy.gsp View <g:applyLayout name="storeLayout"> <div id="shoppingCart" class="shoppingCart"> <h2>Would you like a CD edition of the album sent to you or a friend as a gift?</h2> <div class="choiceButtons"> <g:link controller="store" action="buy" event="yes"> <img r:'images',file:'yes-button.gif')}" border="0"/> </g:link> <g:link controller="store" action="buy" event="no"> <img src="${createLinkTo(dir:'images',file:'no-button.gif')}" border="0"/> </g:link> </div> </div> </g:applyLayout> Notice how you can leverage the storeLayout once again to make sure the user interface remains consistent. Each <g:link> tag uses the event attribute to specify the event to trigger. Figure 9-3 shows what the dialog looks like. 218 CHAPTER 9 ■ CREATING WEB FLOWS Figure 9-3. Choosing whether you want a CD hard copy As you can see from the requireHardCopy state’s code in Listing 9-24, if a yes event is triggered, the flow will transition to the enterShipping state; otherwise it will head off to the loadRecommendations state. Each of these states will help you learn a little more about how flows work. Let’s look at the enterShipping state, which presents a good example of doing data binding and validation. Data Binding and Validation in Action The enterShipping state is the first view state that asks the user to do some form of free-text entry. As soon as you start to accept input of this nature from a user, the requirement to vali- date input increases. Luckily, you’ve already specified the necessary validation constraints on the Address class in Listing 9-13. Now it’s just a matter of putting those constraints to work. Look at the implementation of the enterShipping state in Listing 9-26. As you can see, it defines two event handlers called next and back. Listing 9-26. The enterShipping State 1 enterShipping { 2 on('next') { 3 def address = flow.shippingAddress 4 address.properties = params 5 if(address.validate()) { 6 flow.lastAlbum.shippingAddress = address 7 return success() 8 } 9 return error() 10 }.to 'loadRecommendations' 11 on('back') { 12 flow.shippingAddress.properties = params 13 }.to 'requireHardCopy' 14 } We’ll revisit the transition actions defined for the next and back events shortly. For the moment, let’s develop the view that will render the enterShipping state and trigger each event. Create a GSP at the location grails-app/views/store/buy/enterShipping.gsp. Again, you can use the storeLayout to ensure the layout remains consistent. Listing 9-27 shows a shortened CHAPTER 9 ■ CREATING WEB FLOWS 219 version of the code because the same <g:textField> tag is used for each property of the Address class. Listing 9-27. The enterShipping.gsp View 1 <g:applyLayout name="storeLayout"> 2 <div id="shoppingCart" class="shoppingCart"> 3 <h2>Enter your shipping details below:</h2> 4 <div id="shippingForm" class="formDialog"> 5 <g:hasErrors bean="${shippingAddress}"> 6 <div class="errors"> 7 <g:renderErrors bean="${shippingAddress}"></g:renderErrors> 8 </div> 9 </g:hasErrors> 10 11 <g:form name="shippingForm" url="[controller:'store',action:'buy']"> 12 <div class="formFields"> 13 <div> 14 <label for="number">House Name/Number:</label><br> 15 <g:textField name="number" 16 value="${fieldValue(bean:shippingAddress, 17 field:'number')}" /> 18 </div> 19 <div> 20 <label for="street">Street:</label><br> 21 <g:textField name="street" 22 value="${fieldValue(bean:shippingAddress, 23 field:'street')}" /> 24 </div> 25 </div> 26 27 <div class="formButtons"> 28 <g:submitButton type="image" 29 reateLinkTo(dir:'images', 30 file:'back-button.gif')}" 31 name="back" 32 value="Back"></g:submitButton> 33 <g:submitButton type="image" 34 src="${createLinkTo(dir:'images', 35 file:'next-button.gif')}" 36 name="next" 37 value="Next"></g:submitButton> 38 </div> 39 40 41 </g:form> 220 CHAPTER 9 ■ CREATING WEB FLOWS 42 </div> 43 </div> 44 </g:applyLayout> After creating fields for each property in the Address class, you should end up with some- thing that looks like the screenshot in Figure 9-4. Figure 9-4. Entering shipping details As discussed in the previous section on triggering events from the view, the name of the event to trigger is established from the name attribute of each <g:submitButton>. For example, the following snippet taken from Listing 9-27 will trigger the next event: 33 <g:submitButton type="image" 34 src="${createLinkTo(dir:'images', 35 file:'next-button.gif')}" 36 name="next" 37 value="Next"></g:submitButton> Another important part of the code in Listing 9-27 is the usage of <g:hasErrors> and <g:renderErrors> to deal with errors that occur when validating the Address: 5 <g:hasErrors bean="${shippingAddress}"> 6 <div class="errors"> 7 <g:renderErrors bean="${shippingAddress}"></g:renderErrors> 8 </div> 9 </g:hasErrors> CHAPTER 9 ■ CREATING WEB FLOWS 221 This code works in partnership with the transition action to ensure that the Address is val- idated before the user continues to the next part of the flow. You can see the transition action’s code in the following snippet, taken from Listing 9-26: 2 on('next') { 3 def address = flow.shippingAddress 4 address.properties = params 5 if(address.validate()) { 6 flow.lastAlbum.shippingAddress = address 7 return success() 8 } 9 return error() 10 }.to 'loadRecommendations' Let’s step through this code line by line to better understand what it’s doing. First, on line 3 the shippingAddress is obtained from flow scope: 3 def address = flow.shippingAddress If you recall from Listing 9-24, in the requireHardCopy state you created a new instance of the Address class and stored it in a variable called shippingAddress in flow scope when the user specified that she required a CD version of the Album. Here, the code obtains the shippingAddress variable using the expression flow.shippingAddress. Next, the params object is used to bind incoming request parameters to the properties of the Address object on line 4: 4 address.properties = params This will ensure the form fields that the user entered are bound to each property in the Address object. With that done, the Address object is validated through a call to its validate() method. If validation passes, the Address instance is applied to the shippingAddress property of the lastAlbum object stored in flow scope. The success event is then triggered by a call to the success() method. Lines 5 through 8 show this in action: 5 if(address.validate()) { 6 flow.lastAlbum.shippingAddress = address 7 return success() 8 } Finally, if the Address object does not validate because the user entered data that doesn’t adhere to one of the constraints defined in Listing 9-15, the validate() method will return false, causing the code to fall through and return an error event: 9 return error() When an error event is triggered, the transition action will halt the transition to the loadRecommendations state, returning the user to the enterShipping state. The view will then render any errors that occurred so the user can correct her mistakes (see Figure 9-5). [...]... "enterShipping" } The example in Listing 9 -5 2 sets the currentState to requireHardCopy The next trick is to use the signalEvent(String) method to transition from one state to the next Notice how the code in Listing 9 -5 2 uses signalEvent(String) followed by assertCurrentStateEquals(String) to assert that the web flow is transitioning states as expected Now note one of the other aspects of the enterShipping... Listing 9-2 6: it presents an example of using data binding to populate the Address class In Listing 9 -5 2, when you try to trigger the next event, the flow transitions back to the enterShipping state because the Address object doesn’t validate Listing 9 -5 3 shows how to build on testEnterShippingAddress to test whether a valid Address object is provided Listing 9 -5 3 Testing Data Binding in Flows 1 2 3 4 5 6... flow to another state In this case, we want to produce two types of recommendations: • Genre recommendations: We show recent additions to the store that share the same genre (rock, pop, alternative, etc.) as the album(s) the user is about to purchase • “Other users purchased” recommendations: If another user has purchased the same Album the current user is about to purchase, then we show some of the other... are, there is a problem with the code in the flow prior to the showConfirmation view state As for the rest of the code in Listing 9-4 6, on lines 5 through 7 a new Payment instance is created, placed in the flow, and assigned a generated invoice number: 5 6 7 def p = new Payment(user:user) flow.payment = p p.invoiceNumber = "INV-${user.id }-$ {System.currentTimeMillis()}" Then on line 10, there is a to- do”... So you need to create an integration test by running the create-integration-test command: $ grails create-integration-test com.g2one.gtunes.StoreBuyFlow You’ll end up with a new test suite in the test/integration/com/g2one/gtunes directory called StoreBuyFlowTests.groovy Currently, the StoreBuyFlowTests suite extends the vanilla GroovyTestCase superclass You’ll need to change it to extend the WebFlowTestCase... any empty List resolves to false If there are no results in either the userRecommendations or the genreRecommendations list, the code in Listing 9-3 1 triggers the execution of the error event, which results in skipping the recommendations page altogether That’s it! You’re done The loadRecommendations state is complete Listing 9-3 2 shows the full code in action Listing 9-3 2 The Completed loadRecommendations... Also, if the user wanted the Album to be shipped as a CD, then the previous screen was actually the enterShipping view state! In this scenario, you need a way to dynamically specify the state to transition to, and luckily Grails Web Flow support allows dynamic transitions by using a closure as an argument to the to method Listing 9-4 4 presents an example of a dynamic transition that checks whether there... cards to get past Grails credit-card number validator! CHAPTER 9 ■ CREATING WEB FLOWS Figure 9-8 Validating credit card details Dynamic Transitions Before we move on from the enterCardDetails view state, you need to implement the back event that allows the user to return to the previous screen Using a static event name to transition back to the showRecommendations event doesn’t make sense because there... class StoreBuyFlowTests extends WebFlowTestCase { def controller = new StoreController() def getFlow() { controller.buyFlow } } Now it’s time to consider the first test to implement Recall from Listing 9-1 8 that if the user is not logged in, the flow ends by sending a redirect in the requiresLogin end state Listing 9 -5 1 shows how to test whether a user is logged in Listing 9 -5 1 Testing if the User... requireHardCopy state to inquire if the user wants a CD version of the newly added Album The next event allows the user to bypass the option of buying any of the recommendations and go directly to entering her credit-card details in the enterCardDetails state Finally, the back event triggers the first example of a dynamic transition, a topic that we’ll cover later in the chapter Now all you need to do is provide . instant-search box and the top-five-songs panel are hard-coded into the grails- app/views/store/shop.gsp view, so start by extracting those into templates called _searchbox.gsp and _top5panel.gsp, respectively selected Album to the list of albums the user wishes to purchase. It then transitions back to the requireHardCopy state to inquire if the user wants a CD version of the newly added Album. The next. reside within a directory that matches the flow id—in this case, grails- app/ views/store/buy. You will need to use the <g:link> tag’s event attribute to trigger the events in the requireHardCopy

Ngày đăng: 13/08/2014, 08:21