[BLOSSOM-173] How do I develop Webflow components that are editable in the Magnolia Author Created: 05/Mar/14 Updated: 07/Nov/14 Resolved: 12/Mar/14 |
|
| Status: | Closed |
| Project: | Blossom |
| Component/s: | None |
| Affects Version/s: | None |
| Fix Version/s: | None |
| Type: | Task | Priority: | Blocker |
| Reporter: | Anthony Vieitez | Assignee: | Tobias Mattsson |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Remaining Estimate: | 0d | ||
| Time Spent: | 1h | ||
| Original Estimate: | Not Specified | ||
| Template: |
|
| Acceptance criteria: |
Empty
|
| Task DoR: |
Empty
|
| Date of First Response: |
| Description |
|
Firstly, I will describe a little about the current implementation of our website in Magnolia. Then I'll describe the problem that I am facing. We have a website that contains a booking flow. The flow contains a number of pages - search, results, itinerary, payment, confirmation. At this current moment, the booking flow has been developed as a Blossom Webflow component. This means that the component can be dropped into an area inside a magnolia template. The problem is this - the business wants to be able to control the booking flow pages from within the author. For example: The search page is a JSP inside the web application. It is mapped within the webflow view state and resolved by a Spring view resolver. But the business wants to author this page - the business wants to add/remove components to the search page. Also the business want to do the same for other booking flow pages from within the author. How can we achieve this? Is there a way of defining the pages inside magnolia author and for the Webflow view states to map to them? We've looked into a couple of potential solutions - the best looking solution involves redirects, e.g. <view-state id="results" view="externalRedirect:contextRelative:results.html?flowExecutionUrl=# {flowExecutionUrl}"> This 'works' (there are a few quirks) up until we start to use the Webflow asynchronous support which should return a fragment from the page. Redirecting won't be compatible with this solution Do you have a magnolia specific solution (or in fact any solution) to this problem? Hopefully this describes the problems we're facing - its sometimes difficult to explain more complex problems outside of a conversation so please let me know if any of this is unclear. Cheers, Tony |
| Comments |
| Comment by Tobias Mattsson [ 07/Mar/14 ] |
|
Hi Anthony, You need to have your views resolved using info.magnolia.module.blossom.view.TemplateViewResolver, it will then delegate the rendering to either a JSP or Freemarker renderer. The same kind of renderer that renderers templates in Magnolia. The renderers will provide all the attributes you need to access the content and render content within your component. <bean id="mvcViewFactoryCreator" class="org.springframework.webflow.mvc.builder.MvcViewFactoryCreator"> <property name="viewResolvers"> <list> <bean class="info.magnolia.module.blossom.view.TemplateViewResolver"> <property name="order" value="2"/> <property name="prefix" value="/templates/blossomSampleModule/"/> <property name="viewNames" value="*.jsp"/> <property name="viewRenderer"> <bean class="info.magnolia.module.blossom.view.JspTemplateViewRenderer" /> </property> </bean> </list> </property> <property name="useSpringBeanBinding" value="true" /> </bean> In your scenario you'd want to have areas within the component that are displayed depending on which view-state you're in. This can be achieved by using the <cms:area> tag in your views. However, this means that your editors will have to interact with the flow to reach the step where the area they want to change is displayed. That could be very time consuming and its not a great user experience. A better solution would be if the editors could manage content separately from the flow. This can be done by creating a separate page with areas and then from the views in the flow you render the components from those areas. This snippet renders components from an area "sidebar" on page "/itinerary" <c:forEach items="${cmsfn:children(cmsfn:asContentMap(cmsfn:content('/home/about/promos', 'website')), '')}" var="component"> <cms:component content="${component}" /> </c:forEach> Hope that helps, Tobias |
| Comment by Anthony Vieitez [ 07/Mar/14 ] |
|
Great, thanks for that Tobias, I'm going to start implementing this on Monday Just two more questions - Will your solution still allow for asynchronous refreshes of parts of the booking flow pages (let me know if you need more information about what we are trying to do)? Will your solution work with Thymeleaf (couldn't see a Magnolia ThymeleafTemplateViewRenderer)? Cheers, Tony |
| Comment by Tobias Mattsson [ 08/Mar/14 ] |
|
Hi Anthony, It should work with ajax requests, don't see any reason it shouldn't. As long as the request goes through the magnolia filter chain, which requests for rendering components does, everything should be prepared to render other components. Thymeleaf isn't one of the templating languages we support out of the box so some degree of coding will be required. Either by implementing a ThymeleafTemplateViewRenderer which would be a full integration, or by implementing only this function and making it available in Thymeleaf as a variable. Cheers, Tobias |
| Comment by Anthony Vieitez [ 18/Mar/14 ] |
|
Hi Tobias, Gib (the front end developer for our team) has hit another problem related to your solution - it's about accessing page properties (I'll let him explain - he sent me an email since I'm the magnolia account holder): We've got a couple of problems with webflow and accessing page properties set within magnolia. First case:
<div th:each="component :
${cmsfn.children(cmsfn.nodeByPath('/test/main'))}">
<div cms:component="${component}"></div>
</div>
This pulls in the main components from the page with the path /test but obviously doesn't bring anything else with it. If I attempt to access the page properties, e.g:
${cmsfn.asContentMap(content)[bodyClass]}
then nothing is returned. I would expect it to at least pull in the properties for the original page where the webflow component is inserted. Secondly Let me know if you need any more information Cheers, Tony |
| Comment by Tobias Mattsson [ 26/Mar/14 ] |
|
Hi Anthony, Within the div where you're iterating the components in /test/main you have access to their nodes as a variable 'component'. As that component is rendered, by using cms:component, you have access to the same node as a variable 'content'. The loop does not access the page node or the area node. If you need to access to those you'll need to explicitly query for it using methods in the cmsfn object. |
| Comment by Anthony Vieitez [ 24/Apr/14 ] |
|
Hi Tobias, I have another question in regards to this issue: We have webflow page (customerDetails.html). All it does is reference another page defined in Magnolia (as per your above suggestion) called formDetails The referenced page that's defined in Magnolia contains form fields. These are fields that the customer enters data into The problem is this - when the customer submits the data back to the server, Spring cannot bind the data to the form object. The error is: java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'booking' available as request attribute Now, I believe this is because Spring needs access to the bindings that are defined in the formDetails file, e.g. Spring needs access to:
<input type="text" th:field="*{leadPassenger.personName.middleName}" />
But these bindings are not available to Spring as they are inside the file defined in Magnolia In order to add support for this theory, I overwrote the contents of customerDetails with the contents of formDetails and all works correctly. Also, object references inside formDetails, e.g.
<form th:action="${flowExecutionUrl}" th:object="${booking}" method="post">
resolve correctly - so it's not just a case that objects are out of scope Let me know if any of this is not clear or you need any more information Cheers, Tony |
| Comment by Tobias Mattsson [ 25/Apr/14 ] |
|
Hi Tony, Could you share the code for the controller? If you want to keep it private you can file a SUPPORT ticket instead. Also, could you check the page source to see that the include rendering works as intended and includes the output of 'formDetails' in the right place. // Tobias |
| Comment by Anthony Vieitez [ 30/Apr/14 ] |
|
Hi Tobias, The controller is very simple - it's just there to enable the html page to be added as a component:
@Controller
@Main
@Template(id = "virgin-spark:components/guestDetails", title = "Guest Details")
public class GuestDetails {
@Area("gdRightArea")
@Controller
@AvailableComponentClasses()
public static class RightArea {
@RequestMapping("/guestDetails/gdRightArea")
public String render() {
return "html/areas/area.html :: mainArea";
}
}
@RequestMapping("/guestDetails")
public String render(){
return "html/components/guestDetails.html :: guestDetails";
}
}
Also, I can confirm that the formDetails (or guestDetails as we're calling it) is being pulled through and is being displayed |
| Comment by Tobias Mattsson [ 01/May/14 ] |
|
Hi Anthony, Looks like you need to have model attributes on the render method. Add a form backing object / command object and use it to represent the data in your form. Here's how this is done in ContactFormComponent in the Blossom sample. It has two methods that are called based on the request method. Both use the form backing object ContactForm. @Controller @Template(title = "Contact Form", id = "blossomSampleModule:components/contactForm") @TemplateDescription("A contact form where visitors can get in contact with a sales person by filling in a form") public class ContactFormComponent { @RequestMapping(value = "/contact", method = RequestMethod.GET) public String viewForm(@ModelAttribute ContactForm contactForm) { return "components/contactForm.ftl"; } @RequestMapping(value = "/contact", method = RequestMethod.POST) public String handleSubmit(@ModelAttribute ContactForm contactForm, BindingResult result, Node content) throws RepositoryException { new ContactFormValidator().validate(contactForm, result); if (result.hasErrors()) { return "components/contactForm.ftl"; } return "website:" + content.getProperty("successPage").getString(); } @TabFactory("Content") public void contentTab(UiConfig cfg, TabBuilder tab) { tab.fields( cfg.fields.pageLink("successPage").label("Success page") ); } } |
| Comment by Anthony Vieitez [ 09/May/14 ] |
|
Hi Tobias, Thanks for the response, but I'm not sure it's what we're after. The problem is that webflow is acting as the controller for the booking flow (and this page is included in the booking flow). Basically, on the previous page to this one, a Booking object is created by a webflow view-state. This object is placed into conversation scope and is then used as the model in the guest details page, e.g. <view-state id="guestDetails" model="booking" view="guestDetails.html"> ... </view-state> When the guestDetails.html file contains the model bindings, all works fine. But when the guestDetails.html page references a component that contains the bindings, we get the exception described above. This is how guestDetails.html references a component that has been defined in the editor (this is the solution you proposed when you were over here) <div th:each="component : ${#cmsfn.children(#cmsfn.nodeByPath('customerDetails/main'))}">
<div cms:component="${component}"></div>
</div>
The above piece of code pulls in all the components defined in the main area form the customerDetails page that was created via the editor. The solution I'm looking for would allow us to keep all the binding functionality local to webflow I'd like to hear your thoughts on this - whether it's possible (and is the correct approach), as well as any alternative solutions you may have. What we're trying to achieveThe key requirement is that we use webflow for our booking flow but allow the business to edit components on the page - the business wont be able to edit the guest details form though - just components that sit alongside this form. One solution we are considering is to define the form in the guestDetails.html view (the view that webflow points at) and only define and pull in the editable components from the Magnolia page, i.e like this: 0000000000000000000000000000000000000000000000000000000000000 0 0000000000000000000000000000 0000000000000000000000000000 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 form fields - html 0 0 promo pod - editable 0 0 0 0 page defined in 0 0 component pulled in 0 0 0 0 webflow 0 0 from Magnolia 0 0 0 0 0 0 0 0 0 0000000000000000000000000000 0000000000000000000000000000 0 0000000000000000000000000000000000000000000000000000000000000 Would this be the best approach? Hope this makes sense, let me know if any part of it doesn't Cheers, Tony |
| Comment by Tobias Mattsson [ 09/May/14 ] |
|
Hi Anthony, I'm struggling a bit to figure out why it doesn't work. Your approach looks valid to me and should work. The exception is thrown when the view is rendered, since you're seeing it after submitting it must be occurring when the resulting view is rendered. I would expect it to be available at this stage based on your view-state definition. To move forward I would suggest adding the form in guestDetails.html since that works and you're saying the business won't be editing it anyway. Having it on the separate page might give the business a better understanding of the step they're editing but as the form needs information from the flow, the Booking object, it might end up not being viewable outside the flow. To figure out exactly what causes the problem I'd look at the full stack trace of the exception to see if it is indeed failing to render the resulting form view and debug from there to see why the model attribute it needs isn't available. Hope that helps, Tobias |