|Version 1 (modified by tstuij, 8 years ago)|
Nested components (:|,) containers and tabbed panes
And Here is the code for this chapter (and all the others). I don't really know why I keep on announcing this in every chapter.
Nested components are components that occupy the slot of another component. They are not that different from regular slot values except for that you want them to inherit the proper values from past frames. To set these values, nested components use :component as their initialization keyword when placed in a component's slot. In day to day use they behave just as other slot values. If you want to use them to nest html layout, define a render method on them and invoke it in their parents render method.
Containers are used as a storehouse for components. Their main use is to be able to select one of those components as the component to be rendered to the user. The components in the container are stored as a list and can be referenced by key or index. This is done through several methods associated with containers to manipulate their contents.
Containers come in three flavors: container, switching-container and list-container. A bog-standard container is a component with three extra slots:
- contents - An alist of (key . component) holding the components - accessor = container.contents
- key-generator - A lambda that generates the keys from a component when they are not specified. Defaults to identity - accessor = container.key-generator
- key-test - A function used to compare two keys which defaults to eql - accessor = container.key-test
Containers are very nice for letting people navigate a site. You nest one inside a window-component with :component and present the user with a list of navigation links to which you attach a switch-component action (already provided by ucw) which accepts a so called switching-container and a key name as an argument. The action sets the key of the component which is associated by key as the current-component-key.
The switching-container component class is generally used as the super class for navigatation components and tabbed-pane like components. it's like the regular container but serves to manage a set of components which share the same place in the user interface. It provides an implementation of render which simply renders its current component. It adds one extra slot called current-component-key, which is referenced to render the appropriate component.
A list-container is a child of container-component as well as widget-component. It's render method renders the components in the container as an ordered list. A list container has a custom initialize-instance method defined on it, which accepts an additional orientation keyword which can be set to either :horizontal or :vertical. If one of these keywords is supplied, it is added to that component's css class list which it inherited from the widget-component class.
The container-manipulating methods are in order of appearance:
- clear-container - accepts a container - clears contents of container
- find-component - accepts a container and a key - returns the component and t if the component is present. Otherwise nil and nil.
- (setf find-component) - (setf (container key) component) - sets a component at the position of key. If no such key exists, the key and component are added at the end of the contents list.
- add-component - accepts a container a component and an optional symbol as key name - adds a component to the end of the contents list. If key is supplied, key is used as key for component, otherwise the index of the component in the contents list is used.
- component-at - accepts a container and an index. If a component is set at the index of integer in the contents list, the component is returned, together with t, otherwise nil and nil are returned.
- (setf component-at) - (setf (container index optional-key) component) - Overrides the current component in list if it's available. In which case key isn't used. If the provided index is one past the last component, component is set at end of list with key as key if provided, otherwise index is used as key. If the index given is two or more past the last item in the list, a recoverable error is signaled.
- container.current-component - accepts a container - returns component with key of current-component-name as with find-component
- ensure-valid-component-key - accepts a container and a key - Returns t if key names one of the components in container, otherwise a restartable error is signaled.
- map-contents - accepts a function and a container - applies a two argument function to the key and the component of every key-component pair in contents of container.
The following example demonstrates the use of a simple-container, nested in a window-component. It's a simple mock tragic adventure fragment. When all drawers are checked, a new component is shoved onto the containers contents list. The flet at the end and the simple-container initialization in the room class are the most interesting. The switching-container mechanics code is mostly taken from the example application, bundled with ucw. Start it up to see a more traditional use of containers.
First add this to your ucw-intro.asd file:
(:file "containers" :depends-on ("config"))
Add this to the list of dispatchers in your config.lisp file:
;; containers (regexp-dispatcher "^(containers.ucw|)$" (call 'office))
And then define containers.lisp like so:
(defcomponent timeline (widget-component) ((history :initarg :history :accessor history)) (:render () (<:as-html (history timeline))))
(defcomponent drawer (widget-component) ((contents :initarg :contents :accessor contents)) (:render () (<:as-html (contents drawer))))
(defcomponent office (simple-window-component) ((what-happened :initform '(0 0 0) :accessor what-happened) (closet :component (switching-container :current-component-key 'in-office :contents `((in-office . ,(make-instance 'timeline :history "You stand alone in the office of your evil boss. In front of you you see a closet in which you are sure you will find valuables with which you can pay off your insurmountable debts.")) (top-drawer . ,(make-instance 'drawer :contents "you find memoirs of a broken soul")) (middle-drawer . ,(make-instance 'drawer :contents "you find an agenda of grinding daily chores")) (bottom-drawer . ,(make-instance 'drawer :contents "you find an ointment to take the edge off")))) :accessor closet)) (:default-initargs :stylesheet "sheet.css") (:render () (with-slots (what-happened closet) office (unless (find-component closet 'leave) (case (container.current-component-key closet) ('top-drawer (setf (car what-happened) 1)) ('bottom-drawer (setf (cadr what-happened) 1)) ('middle-drawer (setf (caddr what-happened) 1))) (when (and (= (car what-happened) 1) (= (cadr what-happened) 1) (= (caddr what-happened) 1)) (add-component closet (make-instance 'timeline :history "You leave the office ashamed and angry. You know now your evil boss is also a human being and you will have to feel compassion for him forever.") 'leave))) (render closet) (<:br) (<:as-html "What will you do next?") (<:br) (if (eql (container.current-component-key closet) 'leave) (flet ((drawer-link (name text) (<ucw:a :action (switch-component (closet office) name) (<:as-html text)))) (<:ul (<:li (drawer-link 'top-drawer "inspect top drawer")) (<:li (drawer-link 'middle-drawer "inspect middle drawer")) (<:li (drawer-link 'bottom-drawer "inspect bottom drawer")) (<:li (drawer-link 'in-office "recall what your doing here")) (when (find-component closet 'leave) (<:li (drawer-link 'leave "leave"))))) (<:ai "<br/>Nothing, just stand here a bit")))))
UCW also harbors a convenience class to quickly render different components/pages in a tabbed-pane, which builds upon switching-container. It basically spits out a main window and a row/column of choosable components, all wrapped in divs, so you can style them every way you want. An example that wraps all the components we made thus far, and the tal components later in this chapter in a tabbed pane:
(defcomponent pane (tabbed-pane simple-window-component) () (:default-initargs :title "pane" :stylesheet "sheet.css" :contents `(("hello world" . ,(make-instance 'hello-world)) ("basics" . ,(make-instance 'basics)) ("tal" . ,(make-instance 'tal-component)) ("more tal" . ,(make-instance 'more-tal))) :current-component-key "hello world") (:render () (call-next-method)))
There you go. We also let our component inherit from the simple-window-component so it will render a whole page and not just a div. The initialization part is the same as a normal switching-container, but it has a render-method defined on it that wraps the divs, and assigns actions. So we just call the next method in our render method. Use the sheet.css in the supplied sources to conjure up a nicely formatted tabbed-pane. This example is a bit wasteful cause now you're wrapping whole html pages in divs, but you get the picture.