Iliad examples explained part II

Tagged:

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!

What about adding a #path or something like that method to BlogPost:

path [
    ^self title, self hash printString
]

and then use it in #postContentsFor: and #post?

Also, say I wanted to have view/edit switching only in the single post view. Could that work given that the same widgets are used in the default view too? What is exactly the state that the widgets have to hold?

Thanks!

Yes, the path could be in the blog post directly. It would be better.

If you want to have the switch in the single post view only, you have to think differently, yes, as the same widgets are used in both. But this is only a simple example, this blog is not intended to be actually used.

You could for instance have another view method #edit to edit posts separately. The switch may not be appropriate in this case, if you want a different bahaviour.

Nico

Exactly what kind of state is stored there?

Magritte widgets store domain object mementos, which are to be committed to the object, possible errors, object descriptions, and validation, form, switch, ... decorators.

User login