UNPKG

18.2 kBMarkdownView Raw
1Composi
2=======
3
4Contents
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 and Render](./render.md)
12- Components
13- [State](./state.md)
14- [Lifecycle Methods](./lifecycle.md)
15- [Events](./events.md)
16- [Styles](./styles.md)
17- [Unmount](./unmount.md)
18- [Third Party Libraries](./third-party.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 five lifecycle methods:
292
2931. componentWillMount
2942. componentDidMount
2953. componentWillUpdate
2964. componentDidUpdate
2975. componentWillUnmount
298
299Learn more about using lifecycle methods with components by reading the [documentation](./lifecycle.md).
300
301About Component Instantiation
302-----------------------------
303
304When 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:
305
306```javascript
307// Render title component, no data needed:
308const title = new Component({
309 container: 'header',
310 // Define render function that returns state markup:
311 render: () => <h1>This is a Title!</h1>
312})
313// Render component without data:
314titleComponent.update()
315
316// Render the fruitList component with fruits data:
317const fruitList = new Component({
318 container: 'body',
319 // Define render function that consumes fruit data:
320 render: (fruits) => (
321 <ul class='list'>
322 {
323 fruits.map(fruit => <li>{fruit}</li>)
324 }
325 </ul>
326 )
327})
328// Render component with fruit data:
329fruitList.update(fruits)
330```
331### Creating a Stateful Component
332When 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:
333
334```javascript
335import {h, Component} from 'composi'
336
337class Clock extends Component {
338 constructor(opts) {
339 super(opts)
340 this.state = {time: Date.now()}
341 this.styles = {
342 'p > span': {
343 fontFamily: 'Monospace'
344 }
345 }
346 }
347 render() {
348 let time = this.state.time
349 const angle = (time) => 2 * Math.PI * time / 60000
350 return (
351 <li>
352 <div>
353 <svg viewBox="0 0 100 100" width="150px">
354 <circle cx="50" cy="50" r="45" fill="blue" />
355 <line
356 stroke-width="2"
357 x1="50"
358 y1="50"
359 x2={ 50 + 40 * Math.cos(angle(time)) }
360 y2={ 50 + 40 * Math.sin(angle(time)) }
361 stroke="white"
362 />
363 </svg>
364 <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>
365 </div>
366 </li>
367 )
368 }
369
370 tick() {
371 this.setState({time: new Date()})
372 }
373
374 componentWasCreated() {
375 this.timeID = setInterval(() => { this.tick() }, 1000)
376 }
377}
378const clock = new Clock({
379 container: '#clock'
380})
381```
382
383Trigger Initial Render With State
384---------------------------------
385When 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:
386
387```javascript
388// Define component:
389const hello = new Component({
390 container: 'body',
391 render: (name) => <h1>Hello, {name}!</h1>
392})
393
394// Setting state on the Component instance will cause the component to render to the DOM into the body tag:
395hello.state = 'World'
396```
397
398Querying the DOM
399----------------
400### this.element
401
402Composi 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:
403
404```javascript
405const List extends Component {
406 constructor(props) {
407 super(props)
408 this.container = 'body',
409 }
410 render(data) {
411 return (
412 <ul class='list'>
413 {
414 data.map(item => <li>{item}</li>)
415 }
416 </ul>
417 )
418 }
419}
420const list = new List()
421// Render list with data:
422list.update(['One','Two','Three'])
423```
424
425The 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:
426
427```javascript
428// Get the text of the second list item:
429const text = list.element.children[1].textContent
430
431// Get the last list item,
432// then set an event on it:
433const lastItem = list.element.querySelector('li:last-of-type')
434lastItem.addEventListener('click', (e) => alert(e.textContent.trim()))
435```
436For 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:
437
438```javascript
439import {h, Component} from 'composi'
440
441class Person extends Component {
442 constructor(props) {
443 super(props)
444 this.container = 'section'
445 }
446 render(person) {
447 return (
448 <div class='person-component'>
449 <h3>{person.name}</h3>
450 <p>
451 <label for="name">New Name:</label>
452 <input type="text" />
453 <button onclick={() => this.updateName()}>Update</button>
454 </p>
455 </div>
456 )
457 }
458 updateName() {
459 // Get input through 'element' property:
460 const input = this.element.querySelector('input')
461 const name = input.value || 'unknown'
462 this.setState({name})
463 }
464}
465const person = new Person()
466person.state = {name: 'Joe'}
467```
468
469SSR & Hydration
470---------------
471
472You 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:
473
474 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.
475
476Let's take a look at how we might do this. Suppose on the server we output some markup as follows:
477
478```html
479<body>
480 <article>
481 <ul id="specialList">
482 <li>Apples</li>
483 <li>Oranges</li>
484 <li>Bananas</li>
485 </ul>
486 </article>
487</body>
488```
489
490When 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:
491
492```javascript
493const list = new Component({
494 // Give the component the same container as the list "specialList" above:
495 container: 'article',
496 // Define list with same id as list in server-side markup:
497 render: (fruits) => (
498 <ul id="specialList">
499 {
500 fruits.map(fruit => <li>{fruit}</li>)
501 }
502 </ul>
503 )
504})
505// Set state, render the component and replace state nodes:
506list.state = ['Stawberries', 'Peaches', 'Blueberries']
507```
508
509With 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.
510
511**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.
512