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
|
11 | - [Mount and Render](./render.md)
|
12 | - [Components](./components.md)
|
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 |
|
21 | Functional Components
|
22 | ---------------------
|
23 | The component architecture is based on classes. If you favor functional programing over OOP, you might prefer to use functional components. Functional Components are always stateless, so you will want to use them with some kind of state management, such as Redux, Mobx, etc.
|
24 |
|
25 | Virtual Dom
|
26 | -----------
|
27 |
|
28 | When you create components with the Component class, you have a reference for the component's properties, methods, etc. This also provides a point of reference for the virtual dom. In contrast, functional components do not have this reference for their virtual dom. Instead, with functional components, the scope in which the function is invoked becomes the scope for the virtual dom. This means as your project grows and code gets out over many files, the scope of a functional component can be spread across several files. For components this is not a problem. For functional components this means the first time it gets invoked in a new scope, the previous virtual dom will be replaced by a new one. Unless you are creating a list with 10,000 or more items, you won't notice any inefficencies. However, it does result in more layout thrashing than when creating class-based components.
|
29 |
|
30 | Creating Functional Components
|
31 | ------------------------------
|
32 |
|
33 | Functional components use [JSX](./jsx.md) to create markup, but technically you could also use [Hyperx](./hyperx.md) with ES6 template literals. Here we're going to look at using JSX.
|
34 |
|
35 | To create a functional component, you start with a function, surprise! The function could accept some data, or not. It depends on what the function needs to return. If it is returning static content, no need for a parameter of data. If you want the function component to consume some data, then pass it in as a parameter. And of course you'll need to return some markup. In the example below we have a simple, function component that creates a header:
|
36 |
|
37 | ```javascript
|
38 | // title.js:
|
39 | // We need to import the "h" function:
|
40 | import {h} from 'composi'
|
41 |
|
42 | // Define function that takes props for data:
|
43 | export function Title(props) {
|
44 | // Return the header with name from props:
|
45 | return (
|
46 | <nav>
|
47 | <h1>Hello, {props.name}!</h1>
|
48 | </nav>
|
49 | )
|
50 | }
|
51 | ```
|
52 |
|
53 | If we were to do this with the Composi `h` function, it would look like this:
|
54 |
|
55 | ```javascript
|
56 | // app.js:
|
57 | import {h} from 'composi'
|
58 |
|
59 | // Define function that takes props for data:
|
60 | export function Title(name) {
|
61 | return h(
|
62 | 'nav', {}, h(
|
63 | 'h1', {}, name
|
64 | )
|
65 | )
|
66 | }
|
67 | ```
|
68 |
|
69 | Both examples above create virtual nodes, so we will need a way to get them into the DOM. Composi provides the `mount` function for that purpose. It works similar to React's `ReactDOM.render` function, but is specifically for mounting a functional component. It takes two arguments: a tag/vnode and a selector/element in which to insert the markup.
|
70 |
|
71 | In our `app.js` file we import `h` and `render` and then the `Title` functional component and render it to the DOM:
|
72 |
|
73 | ```javascript
|
74 | // app.js:
|
75 | import {h, mount} from 'composi'
|
76 | import {Title} from './components/title'
|
77 |
|
78 | // Define data for component:
|
79 | const name = 'World'
|
80 |
|
81 | // Render the component into header tag:
|
82 | mount(<Title {...{name}}/>, 'header')
|
83 | ```
|
84 |
|
85 | This will convert the functional component into a virtual node (vnode) and then convert that into actual nodes inside the header tag. Because a virtual node was created, we can re-render this later with new data and Composi will patch the DOM efficiently for us. In this case that would involve patching the text node of the `h1` tag.
|
86 |
|
87 | Using Mount with Render
|
88 | -----------------------
|
89 | The `mount` function returns a reference to the DOM tree that was created. This is useful for when you need to update a component. You can capture that value in a variable and then pass it to Composi's `render` funcion to update a functional component. Notice in the following example how we use `mount` to get a reference to the component and then pass it to the `render` function in a `setTimeout` to update it:
|
90 |
|
91 | ```javascript
|
92 | // Remember to import both mount and render:
|
93 | import {h, mount, render} from 'composi'
|
94 | function Clock({date}) {
|
95 | return (
|
96 | <div>
|
97 | <h3>The Current Time</h3>
|
98 | <p>It is {date.toLocaleTimeString()}.</p>
|
99 | </div>
|
100 | )
|
101 | }
|
102 |
|
103 | const clock = mount(<Clock date={new Date()} />, 'section')
|
104 |
|
105 | setInterval(
|
106 | () => {
|
107 | // Pass the clock reference to render function to update it:
|
108 | render(<Clock date={new Date()}/>, clock)
|
109 | },
|
110 | 1000
|
111 | )
|
112 | ```
|
113 |
|
114 | List with Map
|
115 | -------------
|
116 |
|
117 | Now lets create a functional component that takes an array and outputs a list of items. Notice that we are using [BEM](https://css-tricks.com/bem-101/) notation for class names to make styling easier.
|
118 |
|
119 | ```javascript
|
120 | // list.js:
|
121 | import {h} from 'composi'
|
122 |
|
123 | export function List(props) {
|
124 | return (
|
125 | <div class='container--list'>
|
126 | <ul class='list--fruits'>
|
127 | {props.items.map(item => <li class='list--fruits__item'>{props.item}</li>)}
|
128 | </ul>
|
129 | </div>
|
130 | )
|
131 | }
|
132 | ```
|
133 |
|
134 | This list is consuming an `items` array:
|
135 |
|
136 | ```javascript
|
137 | // items.js:
|
138 | export const items = ['Apples', 'Oranges', 'Bananas']
|
139 | ```
|
140 |
|
141 | In our `app.js` file we put this all together:
|
142 |
|
143 | ```javascript
|
144 | // app.js:
|
145 | import {h, mount} from 'composi'
|
146 | import {Title} from './components/title'
|
147 | import {List} from './components/list'
|
148 | import {items} from './items'
|
149 |
|
150 | // Define data for Title component:
|
151 | const name = 'World'
|
152 |
|
153 | // Mount component into header:
|
154 | mount(<Title {...{name}}/>, 'header')
|
155 |
|
156 | // Mount list component into section with items data:
|
157 | mount(<List {...{items}}/>, 'section')
|
158 | ```
|
159 |
|
160 | Custom Tags
|
161 | -----------
|
162 |
|
163 | We can break this list down a bit using a custom tag for the list item. We will need to pass the data down to the child component through its props:
|
164 |
|
165 |
|
166 | ```javascript
|
167 | // list.js:
|
168 | import {h} from 'composi'
|
169 |
|
170 | function ListItem(props) {
|
171 | return (
|
172 | <li class='list--fruits__item'>{props.item}</li>
|
173 | )
|
174 | }
|
175 |
|
176 | export function List(props) {
|
177 | return (
|
178 | <div class='container--list'>
|
179 | <ul class='list--fruits'>
|
180 | {props.items.map(item => <ListItem {...{item}}/>)}
|
181 | </ul>
|
182 | </div>
|
183 | )
|
184 | }
|
185 | ```
|
186 |
|
187 | Data Flows Down
|
188 | ---------------
|
189 |
|
190 | When using custom tags inside a functional component, the data flows down, from the parent to the children. There is no two-way data binding. This makes it easy to reason about. If you need a child component to communicate some kind of data change to its parent, you can use events or any pubsub solution from NPM. We have a tiny and efficient pubsub module on NPM called [pubber](https://www.npmjs.com/package/pubber) that is perfect for these situations.
|
191 |
|
192 | Events
|
193 | ------
|
194 |
|
195 | What if we wanted to make this list dynamic by allowing the user to add items? For that we need events. We'll add a text input and button so the user can enter an item. Since this doesn't involve any data consuming any data, their function just needs to return the markup for the nodes to be created. We'll call this function component `ListForm`:
|
196 |
|
197 | ```javascript
|
198 | import {h} from 'composi'
|
199 |
|
200 | function ListForm() {
|
201 | return (
|
202 | <p>
|
203 | <input class='list--fruits__input--add' placeholder='Enter Item...' type="text"/>
|
204 | <button class='list--fruits__button--add'>Add Item</button>
|
205 | </p>
|
206 | )
|
207 | }
|
208 | function ListItem(props) {
|
209 | return (
|
210 | <li>{props.item}</li>
|
211 | )
|
212 | }
|
213 |
|
214 | export function List(props) {
|
215 | return (
|
216 | <div class='container--list'>
|
217 | <ListForm />
|
218 | <ul>
|
219 | {props.items.map(item => <ListItem {...{item}}/>)}
|
220 | </ul>
|
221 | </div>
|
222 | )
|
223 | }
|
224 | ```
|
225 |
|
226 | handleEvent Interface
|
227 | ---------------------
|
228 |
|
229 | We are going to use the `handleEvent` interface to wire up events for the component. We'll do this in a separate file and import it into our `app.js` file. To use `handleEvent` with a functional component we'll need to create an object with the `handleEvent` function on it. Then we'll use a standar event listener and pass the that object instead of a callback.
|
230 |
|
231 | Since we want to be able to add a new fruit to the list, we need to have access to the data and also the function component that mounts the list. Therefore we import them into the `events.js` file.
|
232 |
|
233 | ```javascript
|
234 | // app.js:
|
235 | import {h, mount, render} from 'composi'
|
236 | import {items} from './items'
|
237 | import {List} from './components/list'
|
238 |
|
239 | // Mount the component and capture reference in "list" variable:
|
240 | const list = mount(<List {...{items}}/>, 'section')
|
241 |
|
242 | export const events = {
|
243 | addItem(e) {
|
244 | const input = document.querySelector('.list__input--add')
|
245 | const value = input.value
|
246 | if (value) {
|
247 | items.push(value)
|
248 | // Pass list variable render function to update component:
|
249 | render(<List {...{items}}/>, list)
|
250 | input.value = ''
|
251 | }
|
252 | },
|
253 | handleEvent(e) {
|
254 | e.target.className === 'list__button--add' && this.addItem(e)
|
255 | }
|
256 | }
|
257 |
|
258 | // Attach event listener for list and pass the events object instead of a callback:
|
259 | document.querySelector('.container-list').addEventListener('click', events)
|
260 | ```
|
261 |
|
262 | Notice that we first mount the list. This gives us a reference to the mounted component. We use this reference in our event handler to update the list in place. This allows Composi to use its virtual dom to efficiently update the DOM nodes with the least layout thrashing as possible.
|
263 |
|
264 | And that's it. With that we now have an interactive functional component that updates in real time with a virtual dom.
|
265 |
|
266 | Component Class Advantages
|
267 | --------------------------
|
268 |
|
269 | Although you can build a complex app using nothing but functional components, there are certain conveniences when using the Component class. First and foremost is the fact that classes allow you to encapsulate functionality using properties and methods defined directly on the component. The second is the component itself enables more efficient use of the virtual dom because it keeps track of that internally.
|
270 |
|