1 | Composi
|
2 | =======
|
3 |
|
4 | Contents
|
5 | --------
|
6 | - [Installation](../README.md)
|
7 | - [JSX](./jsx.md)
|
8 | - [Hyperx](./hyperx.md)
|
9 | - [Hyperscript](./hyperscript.md)
|
10 | - [Functional Components](./functional-components.md)
|
11 | - [Mount, Render and Unmount](./render.md)
|
12 | - Components
|
13 | - [Class Component](#Class-Component)
|
14 | - [Exteding the Class Component](#Extending-the-Class-Component)
|
15 | - [State](#State)
|
16 | - [Events](#Events)
|
17 | - [Styles](#Styles)
|
18 | - [Lifecylce Methods](#Lifecylce-Methods)
|
19 | - [Trigger Initial Render With State](#Trigger-Initial-Render-with-State)
|
20 | - [Querying the DOM](#Querying-the-DOM)
|
21 | - [SSR & Hydration](#SSR-&-Hydration)
|
22 | - [State](./state.md)
|
23 | - [Lifecycle Methods](./lifecycle.md)
|
24 | - [Events](./events.md)
|
25 | - [Styles](./styles.md)
|
26 | - [State Management with DataStore](./data-store.md)
|
27 | - [Unmount](./unmount.md)
|
28 | - [Third Party Libraries](./third-party.md)
|
29 | - [Deployment](./deployment.md)
|
30 | - [Differences with React](./composi-react.md)
|
31 |
|
32 | ## Class Component
|
33 |
|
34 | Composi is a library that provides a rich and powerful solution for components. This is provided by the Component class. To use it, you'll first need to create a project. After creating a project (see [Create a New Project](../README.md)), import both `h` and `Component` into your `app.js` file:
|
35 |
|
36 | ```javascript
|
37 | import {h, Component} from 'composi'
|
38 | ```
|
39 |
|
40 | You create a new or custom component by extending the Component class. Components have a number of properties that you can use to make your component useful. Below is a general list of properties. There are differences in how an instance of the Component class, or an extension of it, use these.
|
41 |
|
42 | 1. container - the element in the DOM in which the component will be rendered. Multiple components can share the same container. In such a case they will be appended to the container one after the other in the order you first render them or set their initial state.
|
43 | 2. render - a function that returns markup for the element. This function may optionally take some data that it uses to create dynamic content for the component. You may want to use inline events in your markup to capture user interactions. Read the [documentation for events](./events.md) for more information.
|
44 | 3. state - some data that the component will use when rendering. This could be primitive types, or an object or array.
|
45 | 4. Lifecycle methods - these allow you to do things during the lifecycle of the component.
|
46 |
|
47 | ## Extending the Class Component
|
48 |
|
49 | By extending Component, you can create a custom, reusable component. This is how you would be able to create multiple versions of the same component on the page. You would need to provide only the container element and data to render. If you want to use inline events, you would want to create and extension of Component because then you can define all your actions directly on the component. To learn more about adding events to components, read the [documentation](./events.md).
|
50 |
|
51 | Below we are going to create a list component to create multiple lists with different datasets:
|
52 |
|
53 | ```javascript
|
54 | import {h, Component} from 'composi'
|
55 |
|
56 | // Three arrays of data for three different components:
|
57 | const fruits = [
|
58 | {
|
59 | name: 'Apple',
|
60 | quantity: 2
|
61 | },
|
62 | {
|
63 | name: 'Orange',
|
64 | quantity: 3
|
65 | },
|
66 | {
|
67 | name: 'Banana',
|
68 | quantity: 1
|
69 | }
|
70 | ]
|
71 |
|
72 | const pets = [
|
73 | {
|
74 | name: 'cat',
|
75 | quantity: 1
|
76 | },
|
77 | {
|
78 | name: 'dog',
|
79 | quantity: 1
|
80 | },
|
81 | {
|
82 | name: 'turtle',
|
83 | quantity: 0
|
84 | }
|
85 | ]
|
86 |
|
87 | const tools = [
|
88 | {
|
89 | name: 'hammer',
|
90 | quantity: 1
|
91 | },
|
92 | {
|
93 | name: 'nails',
|
94 | quantity: 100
|
95 | },
|
96 | {
|
97 | name: 'screw driver',
|
98 | quantity: 1
|
99 | }
|
100 | ]
|
101 |
|
102 | // Extend Component:
|
103 | class List extends Component {
|
104 | constructor(opts) {
|
105 | super(opts)
|
106 | // Render all instances in the body:
|
107 | this.container = 'body'
|
108 | }
|
109 |
|
110 | render(data) {
|
111 | return (
|
112 | <ul>
|
113 | {
|
114 | data.map(item => (
|
115 | <li>
|
116 | <div>Name: {item.name}</div>
|
117 | <div>Quanity: {item.quantity}</div>
|
118 | </li>))
|
119 | }
|
120 | </ul>
|
121 | )
|
122 | }
|
123 | }
|
124 |
|
125 | // Create three instances of List.
|
126 | // Then assign them state to render them.
|
127 | const fruitsList = new List()
|
128 | fruitsList.state = fruits
|
129 |
|
130 | const petsList = new List()
|
131 | petsList.state = pets
|
132 |
|
133 | const toolsList = new List()
|
134 | toolsList.state = tools
|
135 | ```
|
136 |
|
137 | The above code would render three lists in the document body.
|
138 |
|
139 | ## State
|
140 |
|
141 | In the above examples our components were stateless. Whatever data they needed, we passed it to them using their `update` methods. But you can define a component with state. The advantage is that when you change the state, the component automatically updates. Learn more about state by reading the [documentation](./state.md).
|
142 |
|
143 | ## Events
|
144 |
|
145 | Components can handle events in two ways, inline or as event listeners. Learn more about adding events to component by reading the [documentation](./events.md).
|
146 |
|
147 | ## Styles
|
148 |
|
149 | There are two ways to define styles for a component: a component style tag or a virtual stylesheet scoped to a component. This makes the component easier to reuse. Learn more about defining styles for components by reading the [documentation](./styles.md).
|
150 |
|
151 | ## Lifecylce Methods
|
152 |
|
153 | Components have five lifecycle methods:
|
154 |
|
155 | 1. componentWillMount
|
156 | 2. componentDidMount
|
157 | 3. componentWillUpdate
|
158 | 4. componentDidUpdate
|
159 | 5. componentWillUnmount
|
160 |
|
161 | Learn more about using lifecycle methods with components by reading the [documentation](./lifecycle.md).
|
162 |
|
163 | About Component Instantiation
|
164 | -----------------------------
|
165 |
|
166 | When you create a stateless component, you'll need to pass it some data with its `update` method to get it to render in the DOM. Of course, you counld make a component that does not need any data passed to it because it is not rendering any data. Below we show both approaches:
|
167 |
|
168 | ```javascript
|
169 | // Render title component, no data needed:
|
170 | const title = new Component({
|
171 | container: 'header',
|
172 | // Define render function that returns state markup:
|
173 | render: () => <h1>This is a Title!</h1>
|
174 | })
|
175 | // Render component without data:
|
176 | titleComponent.update()
|
177 |
|
178 | // Render the fruitList component with fruits data:
|
179 | const fruitList = new Component({
|
180 | container: 'body',
|
181 | // Define render function that consumes fruit data:
|
182 | render: (fruits) => (
|
183 | <ul class='list'>
|
184 | {
|
185 | fruits.map(fruit => <li>{fruit}</li>)
|
186 | }
|
187 | </ul>
|
188 | )
|
189 | })
|
190 | // Render component with fruit data:
|
191 | fruitList.update(fruits)
|
192 | ```
|
193 | ### Creating a Stateful Component
|
194 | When you create a class by extending Component you can set default state in the constructor. When you create a new instance of such a class, the state gets set. This triggers the creation of a virtual DOM, Composi runs a diff, and then patches the DOM. This results in the automatic rendering and insertion of the component in the DOM without having to run `update` on the component instance. Examine the example below:
|
195 |
|
196 | ```javascript
|
197 | import {h, Component} from 'composi'
|
198 |
|
199 | class Clock extends Component {
|
200 | constructor(opts) {
|
201 | super(opts)
|
202 | this.state = {time: Date.now()}
|
203 | this.styles = {
|
204 | 'p > span': {
|
205 | fontFamily: 'Monospace'
|
206 | }
|
207 | }
|
208 | }
|
209 | render() {
|
210 | let time = this.state.time
|
211 | const angle = (time) => 2 * Math.PI * time / 60000
|
212 | return (
|
213 | <li>
|
214 | <div>
|
215 | <svg viewBox="0 0 100 100" width="150px">
|
216 | <circle cx="50" cy="50" r="45" fill="blue" />
|
217 | <line
|
218 | stroke-width="2"
|
219 | x1="50"
|
220 | y1="50"
|
221 | x2={ 50 + 40 * Math.cos(angle(time)) }
|
222 | y2={ 50 + 40 * Math.sin(angle(time)) }
|
223 | stroke="white"
|
224 | />
|
225 | </svg>
|
226 | <p>The time: <span>{ new Date(time).getHours() > 12 ? new Date(time).getHours() -12 : new Date(time).getHours() }:{ new Date(time).getMinutes() }:{ new Date(time).getSeconds() < 10 ? '0' + new Date(time).getSeconds() : new Date(time).getSeconds()} { new Date(time).getHours() > 12 ? 'PM' : 'AM' }</span></p>
|
227 | </div>
|
228 | </li>
|
229 | )
|
230 | }
|
231 |
|
232 | tick() {
|
233 | this.setState({time: new Date()})
|
234 | }
|
235 |
|
236 | componentDidMount() {
|
237 | this.timeID = setInterval(() => { this.tick() }, 1000)
|
238 | }
|
239 | }
|
240 | const clock = new Clock({
|
241 | container: '#clock'
|
242 | })
|
243 | ```
|
244 |
|
245 | ## Trigger Initial Render With State
|
246 |
|
247 | When you create a component instance, you can trigger its render by setting state on the instance. When you do, no need to use `update` on it:
|
248 |
|
249 | ```javascript
|
250 | // Define component:
|
251 | const hello = new Component({
|
252 | container: 'body',
|
253 | render: (name) => <h1>Hello, {name}!</h1>
|
254 | })
|
255 |
|
256 | // Setting state on the Component instance will cause the component to render to the DOM into the body tag:
|
257 | hello.state = 'World'
|
258 | ```
|
259 |
|
260 | ## Querying the DOM
|
261 |
|
262 | ### this.element
|
263 |
|
264 | Composi does not have a `ref` property like React. However, every component has an `element` property, which is the base element you define in the component's `render` function. Let's look at the following example:
|
265 |
|
266 | ```javascript
|
267 | const List extends Component {
|
268 | constructor(props) {
|
269 | super(props)
|
270 | this.container = 'body',
|
271 | }
|
272 | render(data) {
|
273 | return (
|
274 | <ul class='list'>
|
275 | {
|
276 | data.map(item => <li>{item}</li>)
|
277 | }
|
278 | </ul>
|
279 | )
|
280 | }
|
281 | }
|
282 | const list = new List()
|
283 | // Render list with data:
|
284 | list.update(['One','Two','Three'])
|
285 | ```
|
286 |
|
287 | The above component instance `list` has a property `element`. In this case its value will be `<ul class="list"></ul`> We can search for any of the list's child elements using the `element` property as the starting point. This gives you more precise, scoped queries:
|
288 |
|
289 | ```javascript
|
290 | // Get the text of the second list item:
|
291 | const text = list.element.children[1].textContent
|
292 |
|
293 | // Get the last list item,
|
294 | // then set an event on it:
|
295 | const lastItem = list.element.querySelector('li:last-of-type')
|
296 | lastItem.addEventListener('click', (e) => alert(e.textContent.trim()))
|
297 | ```
|
298 | For components with complex descendent structure you can use the `element` property for access. You can also access the `element` property from the Component extension's `this` keyword. Notice how we do this in the `update` method below:
|
299 |
|
300 | ```javascript
|
301 | import {h, Component} from 'composi'
|
302 |
|
303 | class Person extends Component {
|
304 | constructor(props) {
|
305 | super(props)
|
306 | this.container = 'section'
|
307 | }
|
308 | render(person) {
|
309 | return (
|
310 | <div class='person-component'>
|
311 | <h3>{person.name}</h3>
|
312 | <p>
|
313 | <label for="name">New Name:</label>
|
314 | <input type="text" />
|
315 | <button onclick={() => this.updateName()}>Update</button>
|
316 | </p>
|
317 | </div>
|
318 | )
|
319 | }
|
320 | updateName() {
|
321 | // Get input through 'element' property:
|
322 | const input = this.element.querySelector('input')
|
323 | const name = input.value || 'unknown'
|
324 | this.setState({name})
|
325 | }
|
326 | }
|
327 | const person = new Person()
|
328 | person.state = {name: 'Joe'}
|
329 | ```
|
330 |
|
331 | ## SSR & Hydration
|
332 |
|
333 | You can use whatever server-side solution to pre-render the html for your document. Then after page loads, you can let Composi take over parts of the document as class components. To do this you need to tell the component what element in the DOM to hydrate. You do that with the `hydrate` property.
|
334 |
|
335 | Let's take a look at how we might do this. Suppose on the server we output some markup as follows:
|
336 |
|
337 | ```html
|
338 | <body>
|
339 | <article>
|
340 | <ul id="specialList">
|
341 | <li>Apples</li>
|
342 | <li>Oranges</li>
|
343 | <li>Bananas</li>
|
344 | </ul>
|
345 | </article>
|
346 | </body>
|
347 | ```
|
348 |
|
349 | When the page first loads this will be the default content. In fact, if the JavaScript did not load or failed with an exception, the user would see this content. If we want to replace the static content with a dynamic list, we can define the list component like this:
|
350 |
|
351 | ```javascript
|
352 | class Liste extends Component {
|
353 | render(data) {
|
354 | return (
|
355 | <ul id="specialList">
|
356 | {
|
357 | fruits.map(fruit => <li>{fruit}</li>)
|
358 | }
|
359 | </ul>
|
360 | )
|
361 | }
|
362 | }
|
363 |
|
364 | // Create instance of List:
|
365 | const list = new List({
|
366 | // Designate container to render in:
|
367 | container: 'article',
|
368 | // Tell Composi to hydrate the list with an id of 'specialList':
|
369 | hydrate: 'specialList'
|
370 | // Set state, render the component and replace state nodes:
|
371 | state: ['Stawberries', 'Peaches', 'Blueberries']
|
372 | })
|
373 | ```
|
374 |
|
375 | With the above code, even though the server sent a static list to the browser, at load time Composi will replace it with the dynamic component of the same id in the same container element.
|
376 |
|
377 | **Note:** When implementing serve-side rendering and component hydration, it's best to use ids for the parts you want to hydrate. That way it's easier for Composi to identify unique parts of the static HTML to take over.
|
378 |
|