Iliad examples explained part II
In the previous part, I explained the basic concepts behind Iliad Applications and Widgets, through the simple counter example. This part will show how to use Magritte to automatically build views and editors with data validation.
For those who don't know Magritte yet, you should read the Magritte tutorial and Magritte exercises first, as my goal here is to show how Magritte can be used with Iliad, not how Magritte itself works.
How I learned to stop worrying and love Magritte
Magritte is a wonderful piece of software. It's a meta description framework useful for tons of things. The basic idea is to describe your domain objects once, then Magritte will provide views, editors, or reports for Morphic, Seaside, and now Iliad. There are also interesting add-ons for mapping objects into relational databases, etc.
Let's take a look at the simple blog example included in Iliad in More/Examples/.
The blog application uses a BlogRepository singleton class to store blog posts. This class is completely uninteresting, so I'll just skip it. A more interesting class is BlogPost, where you can see some Magritte class side descriptions:
Object subclass: BlogPost [ | title body timestamp | BlogPost class [ descriptionBody [ ^Magritte.MAMemoDescription new priority: 3; autoAccessor: #body; label: 'Body'; beRequired; yourself ] descriptionTimestamp [ ^Magritte.MATimeStampDescription new priority: 2; autoAccessor: #timestamp; label: 'Publication date'; beRequired; yourself ] descriptionTitle [ ^Magritte.MAStringDescription new priority: 1; autoAccessor: #title; label: 'Title'; beRequired; yourself ] ] body [^body] body: aString [body := aString] title [^title] title: aString [title := aString] timestamp [^timestamp] timestamp: aTimestamp [timestamp := aTimestamp] ]
The BlogPost has three class side Magritte descriptions, one for each instance variable. Each desciption is set as required, so Magritte editors will require them while validating data. As we will use magritte to build widgets for our posts, all we need now is an application:
Application subclass: BlogApplication [ BlogApplication class >> path [ ^'/blog' ] initialize [ super initialize. model := BlogRepository default ] index [ <category: 'views'> ^self posts ] posts [ <category: 'views'> ^[:e | e h1: 'Blog Example'. e anchor action: [self addPost]; text: 'Add a new post'. (self model posts asSortedCollection: [:a :b | a timestamp > b timestamp]) do: [:each | e build: (self postContentsFor: each)]] ] ]
Each application can have a model. This is not required, but it can be handy to use it. Also, note that the #index method just answers the #posts view method.
In the #posts view, we set an achor to add a new post, and then show each post, sorted by timestamp. The #postContentsFor: method is missing, but before adding it, we have to deal with post widgets. Each post will have its own widget.
To create a widget for a given domain object with Magritte, just call #asWidget on the object. What? That's it? Yes, that's it. All we have to do now it to store these widgets, to keep their state between requests.
postsWidgets [ ^postsWidgets ifNil: [postsWidgets := Dictionary new] ] postWidgetFor: aPost [ ^self postsWidgets at: aPost ifAbsentPut: [ aPost asWidget yourself] ]
Now the #postContentsFor: method
postContentsFor: aPost [ ^[:e | e h2: aPost title; build: (self postWidgetFor: aPost)] ]
Our blog application is almost working, but we still miss the #addPost action method.
addPost [ self show: ( (BlogPost new asWidget) addValidatedForm; addMessage: 'Add new post') onAnswer: [:post | post ifNotNil: [self model posts add: post]] ]
The #addValidatedForm method is needed to build an editor, with data validation. In fact it adds two decorators to the basic widget: one for building the form, and another for the validation. We could also have used #addForm and #addValidation.
Ok, now we can add blog posts to our application, but it would be nice to be able to edit existing posts. Magritte provides another nice decorator to swtich between the viewer and the editor.
postWidgetFor: aPost [ ^self postsWidgets at: aPost ifAbsentPut: [ aPost asWidget addValidatedSwitch; yourself] ]
More about dispatching
We saw in the previous part how view methods are used to dispatch a request inside an application. Now we would like to have an url for each post. This is a bit more complicated (nothing big, really), so we need to know more about how Iliad dispatches a request.
To dispatch a request, Iliad uses a Route object, representing the path of the url, and its current position within the application.
For example, if the url is http://example.com/blog/index, the corresponding route path will be blog/index. The path of a route can be streamed forward with usual #next and #peek methods in Iliad.Route:
st> route := Iliad.Route from: (Iliad.Url absolute: '/blog/index') st> route path OrderedCollection ('blog' 'index' ) st> route next 'blog' st> route peek 'index' st> route next 'index' st> route next nil
Now that we know that, we can easily modify our #postContentsFor: method to add a link to each post's url.
postContentsFor: aPost [ ^[:e | e h2 anchor text: aPost title; action: [self redirectToLocal: 'post/', aPost title, aPost hash printString]. e build: (self postWidgetFor: aPost)] ]
We also need to add a #post view method that actually uses the route to find the blog post:
post [ "View for a specific post" <category: 'views'> | post path | path := self route next. post := self model posts detect: [:each | (each title , each hash printString) = path] ifNone: [self redirectToIndex]. ^self postContentsFor: post ]
The route path is always streamed as Iliad dispatches the request, so "self route next" will answer the next path fragment after the current one, 'post', the view we are within.
That's all for now :)
Happy Iliad Hacking!