UNPKG

18.2 kBMarkdownView Raw
1Composi
2=======
3
4Contents
5--------
6- Components
7- [JSX](./jsx.md)
8- [Hyperx](./hyperx.md)
9- [Hyperscript](./hyperscript.md)
10- [Mount and Render](./render.md)
11- [State](./state.md)
12- [Lifecycle Methods](./lifecycle.md)
13- [Events](./events.md)
14- [Styles](./styles.md)
15- [Unmount](./unmount.md)
16- [Installation](../README.md)
17- [Third Party Libraries](./third-party.md)
18- [Functional Components](./functional-components.md)
19- [Deployment](./deployment.md)
20
21Component
22---------
23
24Composi 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:
25
26```javascript
27import {h, Component} from 'composi'
28```
29
30With these imported, you have two options for creating components.
31
321. Create an instance of the Component class
332. Extend Component to create a new component class
34
35Regardless of which approach you take, 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.
36
371. 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.
382. 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.
393. state - some data that the component will use when rendering. This could be primitive types, or an object or array.
404. Lifecycle methods - these allow you to do things during the lifecycle of the component.
41
42Creating an Instance of Component
43---------------------------------
44
45Let's look at the first option, creating an instance of `Component`. When creating a component, you need to provide at least two arguments: the container, which is the element into which the component will be rendered and a render function. The container could just be the document body. Or you could have a basic html shell with predefined containers into which you will render your components. Using a shell means your document reaches first render quicker.
46
47The component's render function is used to define markup that will get converted into elements and inserted into the DOM. The render function is also used every time the component is updated. Rendering the component with new data or changing the state of a stateful component causes the component use this render function to create a new virtual dom. Composi compares the component's new virtual dom with its previous one. If they do not match, the new one is used to patch and update the DOM. This results in fast and efficient updating of the DOM based on current state.
48
49By default Composi uses JSX for markdown. You can learn more about JSX in the [documentation](./jsx.md). If you prefer, you can use the hyperscript function [h](./hyperscript.md) to define your markup. For the purpose of this tutorial we're going to use JSX for simplicity's sake.
50
51Component Instance
52------------------
53
54When you create a new Component instance, you initialize it by passing an object of options to the constructor. In this case, the options will be `container` and `render`:
55
56```javascript
57import {h, Component} from 'composi'
58
59const hello = new Component({
60 container: '#helloMessage',
61 render: (name) => <p>Hello, {name}!</p>
62})
63
64// Render the component to the DOM by passing data to the component's update method:
65hello.update('World') // Returns <p>Hello, World!</p>
66```
67
68We can also design a component that uses a complex object as its source of data:
69
70```javascript
71import {h, Component} from 'composi'
72
73// A person object:
74const person = {
75 name: {
76 firstName: 'Joe',
77 lastName: 'Bodoni'
78 },
79 job: 'Mechanic',
80 age: 23
81}
82
83// Define a component instance:
84const user = new Component({
85 container: '#userOutput',
86 render: (person) => (
87 <div>
88 <p>Name: {person.name.first} {person.name.last}</p>
89 <p>Job: {person.job}</p>
90 </div>)
91})
92
93// Render the component with the person object:
94user.update(person)
95```
96
97You can also use an array as the source of data for a component. This can be an array of simple types or of objects. In order to output the contents of an array you'll need to use the `map` function on the array and return the markup for each array loop instance:
98
99```javascript
100import {h, Component} from 'composi'
101
102const fruits = ['Apples', 'Oranges', 'Bananas']
103
104const fruitList = new Component({
105 container: '#fruits',
106 // Loop over the fruits array with "map":
107 render: (fruits) => (
108 <ul>
109 {
110 fruits.map(fruit => <li>{fruit}</li>)
111 }
112 </ul>)
113})
114// Render the list of fruits:
115fruitList.update(fruits)
116```
117
118Using this same pattern, we can output an array of objects:
119
120```javascript
121import {h, Component} from 'composi'
122
123const people = [
124 {
125 firstName: 'Joe',
126 lastName: 'Bodoni',
127 job: 'Mechanic'
128 },
129 {
130 firstName: 'Ellen',
131 lastName: 'Vanderbilt',
132 job: 'Lab Technician'
133 },
134 {
135 firstName: 'Sam',
136 lastName: 'Anderson',
137 job: 'Developer'
138 }
139]
140
141const peopleList = new Component({
142 container: '#people',
143 render: (people) => (
144 <ul>
145 {
146 people.map(person => <li>
147 <div>Name: {person.firstName} {person.lastName}</div>
148 <div>Job: {person.job}</div>
149 </li>)
150 }
151 </ul>)
152})
153
154// Render the peopleList component:
155peopleList.update(people)
156```
157
158Understanding Component Instances
159-------------------------------------
160When you create an instance of Component with the `new` keyword, you do so by passing in a object literal of properties and values. Because of this, the scope of those properties is not the component but the object itself. This means that one property does not have access to another, even though they are defined in the same object literal. The only way to access these is from the instance itself. For example, suppose we create a new component instance with state. If we want to access that state inside an event, we would need to do so through the variable we used to create the instance:
161
162```javascript
163const person = new Component({
164 container: '#person',
165 state: personObj,
166 render: (person) => (
167 <div>
168 <h3>{person.firstName} {person.lastName}</h3>
169 <h4>{person.job}</h4>
170 </div>
171 )
172})
173```
174
175### Anti-Patterns
176Although it is possible to access a Component instance's properties as we've shown above, this is not ideal. Component instances are best used when the purpose is simple and straightforward. If you have need to directly access properties of a component or to have one with custom properties, then you want to instead extend the Component class. This is explained next.
177
178Extending Component
179-------------------
180
181Many times it makes more sense to extend the Component class rather than to use an instance of it. 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).
182
183Below we are going to create a list component to create multiple lists with different datasets:
184
185```javascript
186import {h, Component} from 'composi'
187
188// Three arrays of data for three different components:
189const fruits = [
190 {
191 name: 'Apple',
192 quantity: 2
193 },
194 {
195 name: 'Orange',
196 quantity: 3
197 },
198 {
199 name: 'Banana',
200 quantity: 1
201 }
202]
203
204const pets = [
205 {
206 name: 'cat',
207 quantity: 1
208 },
209 {
210 name: 'dog',
211 quantity: 1
212 },
213 {
214 name: 'turtle',
215 quantity: 0
216 }
217]
218
219const tools = [
220 {
221 name: 'hammer',
222 quantity: 1
223 },
224 {
225 name: 'nails',
226 quantity: 100
227 },
228 {
229 name: 'screw driver',
230 quantity: 1
231 }
232]
233
234// Extend Component:
235class List extends Component {
236 constructor(opts) {
237 super(opts)
238 // Render all instances in the body:
239 this.container = 'body'
240 }
241
242 render(data) {
243 return (
244 <ul>
245 {
246 data.map(item => (
247 <li>
248 <div>Name: {item.name}</div>
249 <div>Quanity: {item.quantity}</div>
250 </li>))
251 }
252 </ul>
253 )
254 }
255}
256
257// Create three instances of List.
258// Then assign them state to render them.
259const fruitsList = new List()
260fruitsList.state = fruits
261
262const petsList = new List()
263petsList.state = pets
264
265const toolsList = new List()
266toolsList.state = tools
267```
268
269The above code would render three lists in the document body.
270
271Component Instance or Extend
272----------------------------
273Decisions, decisions. Ultimately it's up to you to decide whether to create a Component instance or to extend it. Each has benefits and drawbacks. If you just want to define a render function, set up state and inject your component in the DOM, creating a Component instance is enough. If you want a component with special methods and lifecycle events, or you want a reusable component with multiple instances, you'll want to extend the Component class. Understanding what you need to accomplish will determine which approach you chose.
274
275In fact, you might start of with a simple component as an Component instance. But later you need to add more functionality and it starts getting messy. It that happens, consider refactoring the component as an extension instead of an instance of Component.
276
277State
278-----
279In 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).
280
281Events
282------
283Components can handle events in two ways, inline or as event listeners. Learn more about adding events to component by reading the [documentation](./events.md).
284
285Styles
286------
287There 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).
288
289Lifecylce Methods
290-----------------
291Components have four lifecycle methods:
292
2931. componentWasCreated
2942. componentWillUpdate
2953. componentDidUpdate
2964. componentWillUnmount
297
298Learn more about using lifecycle methods with components by reading the [documentation](./lifecycle.md).
299
300About Component Instantiation
301-----------------------------
302
303When 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:
304
305```javascript
306// Render title component, no data needed:
307const title = new Component({
308 container: 'header',
309 // Define render function that returns state markup:
310 render: () => <h1>This is a Title!</h1>
311})
312// Render component without data:
313titleComponent.update()
314
315// Render the fruitList component with fruits data:
316const fruitList = new Component({
317 container: 'body',
318 // Define render function that consumes fruit data:
319 render: (fruits) => (
320 <ul class='list'>
321 {
322 fruits.map(fruit => <li>{fruit}</li>)
323 }
324 </ul>
325 )
326})
327// Render component with fruit data:
328fruitList.update(fruits)
329```
330### Creating a Stateful Component
331When 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:
332
333```javascript
334import {h, Component} from 'composi'
335
336class Clock extends Component {
337 constructor(opts) {
338 super(opts)
339 this.state = {time: Date.now()}
340 this.styles = {
341 'p > span': {
342 fontFamily: 'Monospace'
343 }
344 }
345 }
346 render() {
347 let time = this.state.time
348 const angle = (time) => 2 * Math.PI * time / 60000
349 return (
350 <li>
351 <div>
352 <svg viewBox="0 0 100 100" width="150px">
353 <circle cx="50" cy="50" r="45" fill="blue" />
354 <line
355 stroke-width="2"
356 x1="50"
357 y1="50"
358 x2={ 50 + 40 * Math.cos(angle(time)) }
359 y2={ 50 + 40 * Math.sin(angle(time)) }
360 stroke="white"
361 />
362 </svg>
363 <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>
364 </div>
365 </li>
366 )
367 }
368
369 tick() {
370 this.setState({time: new Date()})
371 }
372
373 componentWasCreated() {
374 this.timeID = setInterval(() => { this.tick() }, 1000)
375 }
376}
377const clock = new Clock({
378 container: '#clock'
379})
380```
381
382Trigger Initial Render With State
383---------------------------------
384When 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:
385
386```javascript
387// Define component:
388const hello = new Component({
389 container: 'body',
390 render: (name) => <h1>Hello, {name}!</h1>
391})
392
393// Setting state on the Component instance will cause the component to render to the DOM into the body tag:
394hello.state = 'World'
395```
396
397Querying the DOM
398----------------
399### this.element
400
401Composi 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:
402
403```javascript
404const List extends Component {
405 constructor(props) {
406 super(props)
407 this.container = 'body',
408 }
409 render(data) {
410 return (
411 <ul class='list'>
412 {
413 data.map(item => <li>{item}</li>)
414 }
415 </ul>
416 )
417 }
418}
419const list = new List()
420// Render list with data:
421list.update(['One','Two','Three'])
422```
423
424The 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:
425
426```javascript
427// Get the text of the second list item:
428const text = list.element.children[1].textContent
429
430// Get the last list item,
431// then set an event on it:
432const lastItem = list.element.querySelector('li:last-of-type')
433lastItem.addEventListener('click', (e) => alert(e.textContent.trim()))
434```
435For 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:
436
437```javascript
438import {h, Component} from 'composi'
439
440class Person extends Component {
441 constructor(props) {
442 super(props)
443 this.container = 'section'
444 }
445 render(person) {
446 return (
447 <div class='person-component'>
448 <h3>{person.name}</h3>
449 <p>
450 <label for="name">New Name:</label>
451 <input type="text" />
452 <button onclick={() => this.updateName()}>Update</button>
453 </p>
454 </div>
455 )
456 }
457 updateName() {
458 // Get input through 'element' property:
459 const input = this.element.querySelector('input')
460 const name = input.value || 'unknown'
461 this.setState({name})
462 }
463}
464const person = new Person()
465person.state = {name: 'Joe'}
466```
467
468SSR & Hydration
469---------------
470
471You 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 components. To do this you need to follow a simple rule:
472
473 Give your component's main element a unique id that matches the id of an element in the rendered document. This needs to be in the same element as the component's container.
474
475Let's take a look at how we might do this. Suppose on the server we output some markup as follows:
476
477```html
478<body>
479 <article>
480 <ul id="specialList">
481 <li>Apples</li>
482 <li>Oranges</li>
483 <li>Bananas</li>
484 </ul>
485 </article>
486</body>
487```
488
489When 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:
490
491```javascript
492const list = new Component({
493 // Give the component the same container as the list "specialList" above:
494 container: 'article',
495 // Define list with same id as list in server-side markup:
496 render: (fruits) => (
497 <ul id="specialList">
498 {
499 fruits.map(fruit => <li>{fruit}</li>)
500 }
501 </ul>
502 )
503})
504// Set state, render the component and replace state nodes:
505list.state = ['Stawberries', 'Peaches', 'Blueberries']
506```
507
508With the above code, even though the server sent a static list to the browser, at laod time Composi will replace it with the dynamic component of the same id in the same container element.
509
510**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.
511