]>
Commit | Line | Data |
---|---|---|
24c7594d BB |
1 | # SpacePen [![Build Status](https://travis-ci.org/atom/space-pen.svg?branch=master)](https://travis-ci.org/atom/space-pen) |
2 | ||
3 | **Version 5.x of SpacePen is intended to be included as a direct dependency of 1.0-compatible Atom packages. If you're looking for SpacePen 3.x, used in [Atom Core](https://github.com/atom/atom), check out the [3.x branch](https://github.com/atom/space-pen/tree/3.x).** | |
4 | ||
5 | ## Write markup on the final frontier | |
6 | ||
7 | SpacePen is a powerful but minimalistic client-side view framework for | |
8 | CoffeeScript. It combines the "view" and "controller" into a single jQuery | |
9 | object, whose markup is expressed with an embedded DSL similar to Markaby for | |
10 | Ruby. | |
11 | ||
12 | ## Changes In Version 4 | |
13 | ||
14 | This version of SpacePen depends on HTML 5 custom elements to support lifecycle | |
15 | hooks that previously depended on all DOM manipulation being performed via | |
16 | jQuery. The `afterAttach` and `beforeRemove` hooks have been replaced with | |
17 | `attached` and `detached` and their semantics have been altered. | |
18 | ||
19 | If you need to use SpacePen in an environment that doesn't support custom | |
20 | elements, consider using the previous major version or switching frameworks. | |
21 | ||
22 | ## Basics | |
23 | ||
24 | View objects extend from the View class and have a @content class method where | |
25 | you express their HTML contents with an embedded markup DSL: | |
26 | ||
27 | ```coffeescript | |
28 | class Spacecraft extends View | |
29 | @content: -> | |
30 | @div => | |
31 | @h1 "Spacecraft" | |
32 | @ol => | |
33 | @li "Apollo" | |
34 | @li "Soyuz" | |
35 | @li "Space Shuttle" | |
36 | ``` | |
37 | ||
38 | Views descend from jQuery's prototype, so when you construct one you can call | |
39 | jQuery methods on it just as you would a DOM fragment created with `$(...)`. | |
40 | ||
41 | ```coffeescript | |
42 | view = new Spacecraft | |
43 | view.find('ol').append('<li>Star Destroyer</li>') | |
44 | ||
45 | view.on 'click', 'li', -> | |
46 | alert "They clicked on #{$(this).text()}" | |
47 | ``` | |
48 | ||
49 | But SpacePen views are more powerful than normal jQuery fragments because they | |
50 | let you define custom methods: | |
51 | ||
52 | ```coffeescript | |
53 | class Spacecraft extends View | |
54 | @content: -> ... | |
55 | ||
56 | addSpacecraft: (name) -> | |
57 | @find('ol').append "<li>#{name}</li>" | |
58 | ||
59 | ||
60 | view = new Spacecraft | |
61 | view.addSpacecraft "Enterprise" | |
62 | ``` | |
63 | ||
64 | You can also pass arguments on construction, which get passed to both the | |
65 | `@content` method and the view's constructor. | |
66 | ||
67 | ```coffeescript | |
68 | class Spacecraft extends View | |
69 | @content: (params) -> | |
70 | @div => | |
71 | @h1 params.title | |
72 | @ol => | |
73 | @li name for name in params.spacecraft | |
74 | ||
75 | view = new Spacecraft(title: "Space Weapons", spacecraft: ["TIE Fighter", "Death Star", "Warbird"]) | |
76 | ``` | |
77 | ||
78 | Methods from the jQuery prototype can be gracefully overridden using `super`: | |
79 | ||
80 | ```coffeescript | |
81 | class Spacecraft extends View | |
82 | @content: -> ... | |
83 | ||
84 | hide: -> | |
85 | console.log "Hiding Spacecraft List" | |
86 | super() | |
87 | ``` | |
88 | ||
89 | If you override the View class's constructor, ensure you call `super`. | |
90 | Alternatively, you can define an `initialize` method, which the constructor will | |
91 | call for you automatically with the constructor's arguments. | |
92 | ||
93 | ```coffeescript | |
94 | class Spacecraft extends View | |
95 | @content: -> ... | |
96 | ||
97 | initialize: (params) -> | |
98 | @title = params.title | |
99 | ``` | |
100 | ||
101 | ## Outlets and Events | |
102 | ||
103 | SpacePen will automatically create named reference for any element with an | |
104 | `outlet` attribute. For example, if the `ol` element has an attribute | |
105 | `outlet=list`, the view object will have a `list` entry pointing to a jQuery | |
106 | wrapper for the `ol` element. | |
107 | ||
108 | ```coffeescript | |
109 | class Spacecraft extends View | |
110 | @content: -> | |
111 | @div => | |
112 | @h1 "Spacecraft" | |
113 | @ol outlet: "list", => | |
114 | @li "Apollo" | |
115 | @li "Soyuz" | |
116 | @li "Space Shuttle" | |
117 | ||
118 | addSpacecraft: (name) -> | |
119 | @list.append("<li>#{name}</li>") | |
120 | ``` | |
121 | ||
122 | Elements can also have event name attributes whose value references a custom | |
123 | method. For example, if a `button` element has an attribute | |
124 | `click=launchSpacecraft`, then SpacePen will invoke the `launchSpacecraft` | |
125 | method on the button's parent view when it is clicked: | |
126 | ||
127 | ```coffeescript | |
128 | class Spacecraft extends View | |
129 | @content: -> | |
130 | @div => | |
131 | @h1 "Spacecraft" | |
132 | @ol => | |
133 | @li click: 'launchSpacecraft', "Saturn V" | |
134 | ||
135 | launchSpacecraft: (event, element) -> | |
136 | console.log "Preparing #{element.name} for launch!" | |
137 | ``` | |
138 | ## Markup DSL Details | |
139 | ||
140 | ### Tag Methods (`@div`, `@h1`, etc.) | |
141 | ||
142 | As you've seen so far, the markup DSL is pretty straightforward. From the | |
143 | `@content` class method or any method it calls, just invoke instance methods | |
144 | named for the HTML tags you want to generate. There are 3 types of arguments you | |
145 | can pass to a tag method: | |
146 | ||
147 | * *Strings*: The string will be HTML-escaped and used as the text contents of the generated tag. | |
148 | ||
149 | * *Hashes*: The key-value pairs will be used as the attributes of the generated tag. | |
150 | ||
151 | * *Functions* (bound with `=>`): The function will be invoked in-between the open and closing tag to produce the HTML element's contents. | |
152 | ||
153 | If you need to emit a non-standard tag, you can use the `@tag(name, args...)` | |
154 | method to name the tag with a string: | |
155 | ||
156 | ```coffeescript | |
157 | @tag 'bubble', type: "speech", => ... | |
158 | ``` | |
159 | ||
160 | ### Text Methods | |
161 | ||
162 | * `@text(string)`: Emits the HTML-escaped string as text wherever it is called. | |
163 | ||
164 | * `@raw(string)`: Passes the given string through unescaped. Use this when you need to emit markup directly that was generated beforehand. | |
165 | ||
166 | ## Subviews | |
167 | ||
168 | Subviews are a great way to make your view code more modular. The | |
169 | `@subview(name, view)` method takes a name and another view object. The view | |
170 | object will be inserted at the location of the call, and a reference with the | |
171 | given name will be wired to it from the parent view. A `parentView` reference | |
172 | will be created on the subview pointing at the parent. | |
173 | ||
174 | ```coffeescript | |
175 | class Spacecraft extends View | |
176 | @content: (params) -> | |
177 | @div => | |
178 | @subview 'launchController', new LaunchController(countdown: params.countdown) | |
179 | @h1 "Spacecraft" | |
180 | ... | |
181 | ``` | |
182 | ||
183 | ## Freeform Markup Generation | |
184 | ||
185 | You don't need a View class to use the SpacePen markup DSL. Call `View.render` | |
186 | with an unbound function (`->`, not `=>`) that calls tag methods, and it will | |
187 | return a document fragment for ad-hoc use. This method is also assigned to the | |
188 | `$$` global variable for convenience. | |
189 | ||
190 | ```coffeescript | |
191 | view.list.append $$ -> | |
192 | @li => | |
193 | @text "Starship" | |
194 | @em "Enterprise" | |
195 | ``` | |
196 | ||
197 | ## jQuery extensions | |
198 | ||
199 | ### $.fn.view | |
200 | You can retrieve the view object for any DOM element by calling `view()` on it. | |
201 | This usually shouldn't be necessary, as most DOM manipulation will take place | |
202 | within the view itself using outlet references, but is occasionally helpful. | |
203 | ||
204 | ```coffeescript | |
205 | view = new Spacecraft | |
206 | $('body').append(view) | |
207 | ||
208 | # assuming no other li elements on the DOM, for example purposes, | |
209 | # the following expression should be true | |
210 | $('li').view() == view | |
211 | ``` | |
212 | ||
213 | ### Attached/Detached Hooks | |
214 | The `initialize` method is always called when the view is still a detached DOM | |
215 | fragment, before it is appended to the DOM. This is usually okay, but | |
216 | occasionally you'll have some initialization logic that depends on the view | |
217 | actually being on the DOM. For example, you may depend on applying a CSS rule | |
218 | before measuring an element's height. | |
219 | ||
220 | For these situations, use the `attached` hook. It will be called whenever your | |
221 | element is actually attached to the DOM. Past versions of SpacePen would also | |
222 | call this hook when your element was attached to another detached node, but that | |
223 | behavior is no longer supported. | |
224 | ||
225 | To be notified when your element is detached from the DOM, implement the | |
226 | `detached` hook. | |
227 | ||
228 | ```coffeescript | |
229 | class Spacecraft extends View | |
230 | @content: -> ... | |
231 | ||
232 | attached: -> | |
233 | console.log "With CSS applied, my height is", @height() | |
234 | ||
235 | detached: -> | |
236 | console.log "I have been detached." | |
237 | ``` | |
238 | ||
239 | ## Hacking on SpacePen | |
240 | ||
241 | ```sh | |
242 | git clone https://github.com/atom/space-pen.git | |
243 | cd space-pen | |
244 | npm install | |
245 | npm start | |
246 | ``` | |
247 | ||
248 | * Open http://localhost:1337 to run the specs | |
249 | * Open http://localhost:1337/benchmark to run the benchmarks | |
250 | * Open http://localhost:1337/examples to browse the examples |