1 | # Class Components
|
2 |
|
3 | Marko makes it easy to create user interface components to use as building blocks for web pages and applications of any complexity.
|
4 |
|
5 | Marko promotes self-contained components that:
|
6 |
|
7 | - Are independently testable
|
8 | - Encapsulate the view, client-side behavior (like event handling) and styling
|
9 | - Can easily be combined to create composite UI components.
|
10 |
|
11 | Marko components compile into small, efficient JavaScript modules that hide implementation details from consumers. Components can be published to [npm](https://www.npmjs.com) for reuse across applications.
|
12 |
|
13 | ## UI component diagram
|
14 |
|
15 | ![Component diagram](./component-diagram.svg)
|
16 |
|
17 | In Marko, the DOM output of a UI component is based on _input properties_ and optional _internal state_ used to control the view.
|
18 |
|
19 | If Marko detects changes to `input` or the internal `state`, then the view (that is, the DOM) will automatically update to reflect the new input and state. Internally, Marko uses virtual DOM diffing/patching to update the view, but that’s an implementation detail that could change at any time.
|
20 |
|
21 | ## Component structure
|
22 |
|
23 | Marko makes it easy to keep your component’s class and styles next to the HTML view that they correspond to. The following are the key parts of any UI component:
|
24 |
|
25 | - **View** - The HTML template for your UI component. Receives input properties and states, and renders to either server-side HTML or browser-side virtual DOM nodes.
|
26 | - **Client-side behavior** - A JavaScript `class` with methods and properties for initialization, event handling (including DOM events, custom events and lifecycle events), and state management.
|
27 | - **Styles** - Cascading StyleSheets, including support for CSS preprocessors like [Less](http://lesscss.org/) or [Sass](https://sass-lang.com/).
|
28 |
|
29 | ## Server-side rendering
|
30 |
|
31 | A UI component can be rendered on the server or in the browser, but stateful component instances will be automatically mounted to the DOM in the browser for both. If a UI component tree is rendered on the server, then Marko will recreate the UI component tree in the browser with no extra code required. For more details, please see [Server-side rendering](/docs/server-side-rendering/).
|
32 |
|
33 | ## Single-file components
|
34 |
|
35 | Marko lets you define a `class` for a component right in the `.marko` file, and call that class’s methods with `on-*` attributes:
|
36 |
|
37 | ```marko
|
38 | class {
|
39 | onCreate() {
|
40 | this.state = {
|
41 | count: 0
|
42 | };
|
43 | }
|
44 | increment() {
|
45 | this.state.count++;
|
46 | }
|
47 | }
|
48 |
|
49 | <label>The current count is <output>${state.count}</output></label>
|
50 | <p><button on-click('increment')>+1</button></p>
|
51 | ```
|
52 |
|
53 | ### Styles
|
54 |
|
55 | Adding styles in your view is also made easy:
|
56 |
|
57 | ```marko
|
58 | style {
|
59 | .primary {
|
60 | background: #09c;
|
61 | }
|
62 | }
|
63 |
|
64 | <label>The current count is <output>${state.count}</output></label>
|
65 | <p><button.primary on-click('increment')>+1</button></p>
|
66 | ```
|
67 |
|
68 | These styles aren’t output in a `<style>` tag as inline styles usually are, but are externalized to deduplicate them across multiple component instances on a page.
|
69 |
|
70 | If you use a CSS preprocessor, you can add its file extension on `style`:
|
71 |
|
72 | ```marko
|
73 | style.less {
|
74 | .primary {
|
75 | background: @primaryColor;
|
76 | }
|
77 | }
|
78 | ```
|
79 |
|
80 | > **Note:** The code in the `style` section is processed in a context separate from the rest of the template, so you can’t use JavaScript variables inside it. If you need variables in your CSS, use a CSS preprocessor that supports them.
|
81 |
|
82 | ## Multi-file components
|
83 |
|
84 | You might prefer to keep your component’s class and styles in separate files from the view — the classical separation of HTML, CSS, and JavaScript. Marko makes this possible with a filename-based convention.
|
85 |
|
86 | > **ProTip:** If your’re moving the component’s class and styles to separate files is because the code is getting too large, consider splitting the component into smaller, more manageable components.
|
87 |
|
88 | ### Supporting files
|
89 |
|
90 | Marko discovers supporting files in the same directory as a Marko view. For example, if you have a view named `counter.marko`, Marko will automatically look for `counter.component.js` and `counter.style.css`.
|
91 |
|
92 | ```
|
93 | counter.marko
|
94 | counter.component.js
|
95 | counter.style.css
|
96 | ```
|
97 |
|
98 | Marko also handles views named `index.marko` specially. It will look for `component.js` and `style.css` in addition to `index.component.js` and `index.style.css`. This allows easily grouping component files into a directory:
|
99 |
|
100 | ```
|
101 | counter/
|
102 | index.marko
|
103 | component.js
|
104 | style.css
|
105 | ```
|
106 |
|
107 | In your `component.js` file, export the component’s class:
|
108 |
|
109 | ```js
|
110 | module.exports = class {
|
111 | onCreate() {
|
112 | this.state = {
|
113 | count: 0
|
114 | };
|
115 | }
|
116 | increment() {
|
117 | this.state.count++;
|
118 | }
|
119 | };
|
120 | ```
|
121 |
|
122 | In your `index.marko` file, you can reference methods from that class with `on-*` attributes:
|
123 |
|
124 | ```marko
|
125 | <label>The current count is <output>${state.count}</output></label>
|
126 | <p><button.primary on-click('increment')>+1</button></p>
|
127 | ```
|
128 |
|
129 | And in your `style.css`, define the styles:
|
130 |
|
131 | ```css
|
132 | .primary {
|
133 | background: #09c;
|
134 | }
|
135 | ```
|
136 |
|
137 | > **ProTip:** Marko actually looks any filenames with the pattern `[name].style.*`, so it will pick up any CSS preprocessor file extensions you use: `.less`, `.stylus`, `.scss`, etc.
|
138 |
|
139 | ### Components with plain objects
|
140 |
|
141 | If you target browsers that does not support classes, a plain object of methods can be exported:
|
142 |
|
143 | ```js
|
144 | module.exports = {
|
145 | onCreate: function() {
|
146 | this.state = {
|
147 | count: 0
|
148 | };
|
149 | },
|
150 | increment: function() {
|
151 | this.state.count++;
|
152 | }
|
153 | };
|
154 | ```
|
155 |
|
156 | ## Split components
|
157 |
|
158 | Split components optimize for when a component renders on the server, and doesn’t need to dynamically rerender in the browser. As a result, its template and logic aren’t sent to the browser, reducing load time and download size.
|
159 |
|
160 | > **Note:** If a split component is the child of a stateful component, its full rendering logic will still be sent because the parent may pass new input to the split component and rerender it.
|
161 |
|
162 | Additionally, if _all_ components rendered on a page are split components, Marko’s VDOM and rendering runtime is unnecessary, and therefore not sent to the browser.
|
163 |
|
164 | > **ProTip:** Don’t over-optimize. If your component really doesn’t need rerendering, go ahead and split, but don’t forgo stateful rerendering when it would make your code more maintainable.
|
165 |
|
166 | ### Usage
|
167 |
|
168 | Marko discovers split components similarly to how it discovers an external component class. For example, if you have a view named `button.marko`, it will automatically look for `button.component-browser.js`. If your view is named `index.marko`, it will look for `component-browser.js` in addition to `index.component-browser.js`.
|
169 |
|
170 | ```
|
171 | counter/
|
172 | index.marko
|
173 | component-browser.js
|
174 | ```
|
175 |
|
176 | A split component might need to do some setup as part of its initial render. In this case, the component may define a second component class to use the `onCreate`, `onInput`, and `onRender` [lifecycle methods](#lifecycle-events).
|
177 |
|
178 | This class can be exported from `component.js`, or defined right in the template as a single-file components. In this case, your component folder may contain a `component.js` file, and must contain a `component-browser.js`. The following [lifecycle methods](#lifecycle-events) can go inside the `component.js` file:
|
179 |
|
180 | ```
|
181 | class {
|
182 | onCreate(input, out) { }
|
183 | onInput(input, out) { }
|
184 | onRender(out) { }
|
185 | onDestroy() { }
|
186 | }
|
187 | ```
|
188 |
|
189 | And the following [lifecycle methods](#lifecycle-events) can go inside the `component-browser.js` file:
|
190 |
|
191 | ```
|
192 | class {
|
193 | onMount() { }
|
194 | onUpdate() { }
|
195 | }
|
196 | ```
|
197 |
|
198 | Any JavaScript code related to the DOM or browser should also be inside `component-browser.js`.
|
199 |
|
200 | ### Example
|
201 |
|
202 | `index.marko`
|
203 |
|
204 | ```marko
|
205 | class {
|
206 | onCreate() {
|
207 | this.number = 123;
|
208 | }
|
209 | }
|
210 |
|
211 | <button on-click('shout')>What’s my favorite number?</button>
|
212 | ```
|
213 |
|
214 | `component-browser.js`
|
215 |
|
216 | ```js
|
217 | module.exports = {
|
218 | shout() {
|
219 | alert(`My favorite number is ${this.number}!`);
|
220 | }
|
221 | };
|
222 | ```
|
223 |
|
224 | ## Event handling
|
225 |
|
226 | The `on-[event](methodName|function, ...args)` attributes allow event listeners to be attached for either:
|
227 |
|
228 | - A native DOM event, when used on a native DOM element such as a `<button>`
|
229 | - Or a UI component event, when used on a custom tag for a UI component such as `<my-component>`
|
230 |
|
231 | The `on-*` attributes are used to associate event handler methods with an event name. Event handlers may be specified by `'methodName'` — a string that matches a method on the component instance, or they may be a `function`. Attaching listeners for native DOM events and UI component custom events is explained in more detail in the sections below.
|
232 |
|
233 | You may also use the `once-[event](methodName|function, ...args)` syntax, which will listen for only the first event, and then remove the listener.
|
234 |
|
235 | ### Attaching DOM event listeners
|
236 |
|
237 | The code below illustrates how to attach an event listener for native DOM events:
|
238 |
|
239 | ```marko
|
240 | class {
|
241 | onButtonClick(name, event, el) {
|
242 | alert(`Hello ${name}!`);
|
243 | }
|
244 | }
|
245 |
|
246 | static function fadeIn(event, el) {
|
247 | el.hidden = false;
|
248 | el.style.opacity = 0;
|
249 | el.style.transition = 'opacity 1s';
|
250 | setTimeout(() => el.style.opacity = 1);
|
251 | }
|
252 |
|
253 | <button on-click('onButtonClick', 'Frank')>
|
254 | Say Hello to Frank
|
255 | </button>
|
256 |
|
257 | <button on-click('onButtonClick', 'John')>
|
258 | Say Hello to John
|
259 | </button>
|
260 |
|
261 | <img src='foo.jpg' once-load(fadeIn) hidden />
|
262 | ```
|
263 |
|
264 | The following arguments are passed to the event handler when the event occurs:
|
265 |
|
266 | 1. `...args` - Any extra bound arguments are _prepended_ to the arguments passed to the component’s handler method.
|
267 | For example: `on-click('onButtonClick', arg1, arg2)` → `onButtonClick(arg1, arg2, event, el)`
|
268 | 2. `event` - The native DOM event object.
|
269 | 3. `el` - The DOM element that the event listener was attached to.
|
270 |
|
271 | When using the `on-*` or `once-*` attributes to attach event listeners, Marko uses event delegation that is more efficient than direct attachment of `el.addEventListener()`. Please see [Why is Marko Fast? § Event delegation](/docs/why-is-marko-fast/#event-delegation) for more details.
|
272 |
|
273 | <a id="declarative-custom-events"></a>
|
274 |
|
275 | ### Attaching custom event listeners
|
276 |
|
277 | The code below illustrates how to attach an event listener for a UI component’s custom event:
|
278 |
|
279 | ```marko
|
280 | class {
|
281 | onCounterChange(newValue, el) {
|
282 | alert(`New value: ${newValue}!`);
|
283 | }
|
284 | onCounterMax(max) {
|
285 | alert(`It reached the max: ${max}!`);
|
286 | }
|
287 | }
|
288 |
|
289 | <counter on-change('onCounterChange') once-max('onCounterMax') />
|
290 | ```
|
291 |
|
292 | The following arguments are passed to the event handler when the event occurs:
|
293 |
|
294 | 1. `...args` - Any extra bound arguments are _prepended_ to the arguments passed to the component’s handler method.
|
295 | 2. `...eventArgs` - The arguments passed to `this.emit()` by the target UI component.
|
296 | 3. `component` - The component instance that the event listener was attached to.
|
297 |
|
298 | The following code illustrates how the UI component for `<counter>` might emit its `change` event:
|
299 |
|
300 | `counter/index.marko`
|
301 |
|
302 | ```marko
|
303 | class {
|
304 | onCreate() {
|
305 | this.max = 50;
|
306 | this.state = { count: 0 };
|
307 | }
|
308 | increment() {
|
309 | if (this.state.count < this.max) {
|
310 | this.emit('change', ++this.state.count);
|
311 | }
|
312 | if (this.state.count === this.max) {
|
313 | this.emit('max', this.state.count);
|
314 | }
|
315 | }
|
316 | }
|
317 |
|
318 |
|
319 | <button.example-button on-click('increment')>
|
320 | Increment
|
321 | </button>
|
322 | ```
|
323 |
|
324 | > **ProTip:** Unlike native DOM events, UI component custom events may be emitted with multiple arguments. For example:
|
325 | >
|
326 | > ```js
|
327 | > this.emit("foo", "bar", "baz");
|
328 | > ```
|
329 |
|
330 | ## Attributes
|
331 |
|
332 | ### `on-[event](methodName|function, ...args)`
|
333 |
|
334 | The `on-*` attribute syntax attaches an event listener to either a native DOM event or a UI component event. The `on-*` attribute associates an event handler method with an event name. Please see the [Event handling](#event-handling) section above for details.
|
335 |
|
336 | ### `once-[event](methodName|function, ...args)`
|
337 |
|
338 | The same as the `on-*` attribut,e except that its listener is only invoked for the first event, and then removed from memory. Please see the [Event handling](#event-handling) section above for more details.
|
339 |
|
340 | ### `key`
|
341 |
|
342 | The `key` property does 2 things in Marko:
|
343 |
|
344 | - Obtains references to nested HTML elements and nested UI components.
|
345 | - Matches corresponding elements together when DOM diffing/patching after a rerender. When updating the DOM, keyed elements/components are matched up and reused rather than discarded and recreated.
|
346 |
|
347 | Internally, Marko assigns a unique key to all HTML elements and UI components in a `.marko` file, based on the order they appear in the file. If you have repeated elements or elements that move between locations in the DOM, then you likely want to assign a custom `key` by adding a `key` attribute. The `key` attribute can be applied to both HTML elements and custom tags.
|
348 |
|
349 | #### Referencing nested HTML elements and components
|
350 |
|
351 | ```marko
|
352 | class {
|
353 | onMount() {
|
354 | const headerElement = this.getEl('header');
|
355 | const colorListItems = this.getEls('colors');
|
356 | const myFancyButton = this.getComponent('myFancyButton');
|
357 | }
|
358 | }
|
359 |
|
360 | <h1 key="header">Hello</h1>
|
361 |
|
362 | <ul>
|
363 | <for|color| of=['red', 'green', 'blue']>
|
364 | <li key="colors[]">${color}</li>
|
365 | </for>
|
366 | </ul>
|
367 |
|
368 | <fancy-button key="myFancyButton"/>
|
369 | ```
|
370 |
|
371 | > **Note:** The `[]` suffix (e.g. `key="colors[]"`) lets Marko know that the element will be repeated multiple times with the same key.
|
372 |
|
373 | #### Keyed matching
|
374 |
|
375 | The `key` attribute can pair an HTML element or UI component that moves to a new location in the DOM. For example:
|
376 |
|
377 | ```marko
|
378 | class {
|
379 | onCreate() {
|
380 | this.state = {
|
381 | swapped: false
|
382 | }
|
383 | }
|
384 | }
|
385 |
|
386 | <if(state.swapped)>
|
387 | <p key="b">B</p>
|
388 | <p key="a">A</p>
|
389 | </if>
|
390 | <else>
|
391 | <p key="a">A</p>
|
392 | <p key="b">B</p>
|
393 | </else>
|
394 | ```
|
395 |
|
396 | The `key` attribute can be used to pair HTML elements or UI components that are repeated:
|
397 |
|
398 | ```marko
|
399 | <ul>
|
400 | <for|user| of=input.users>
|
401 | <li key=user.id>${user.name}</li>
|
402 | </for>
|
403 | </ul>
|
404 | ```
|
405 |
|
406 | This way, if the order of `input.users` changes, the DOM will be rerendered more efficiently.
|
407 |
|
408 | #### `*:scoped`
|
409 |
|
410 | The `:scoped` attribute modifier results in the attribute value getting prefixed with a unique ID associated with the current UI component. `:scoped` attribute modifiers can be used to assign a globally unique attribute value from a value that only needs to be unique to the current UI component.
|
411 |
|
412 | Here’s a use-case: certain HTML attributes reference the `id` of other elements on the page. For example, the [HTML `<label>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label) `for` attribute takes an `id` as its value. Many `ARIA` attributes like [`aria-describedby`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-describedby_attribute) also take an `id` as their value.
|
413 |
|
414 | The `:scoped` modifier on an attribute allows you to reference another element without fear of duplicate `id`s, as shown in the following examples:
|
415 |
|
416 | **`for:scoped`**
|
417 |
|
418 | ```marko
|
419 | <label for:scoped="name">Name</label>
|
420 | <input id:scoped="name" value="Frank"/>
|
421 | ```
|
422 |
|
423 | The above code will output HTML similar to the following:
|
424 |
|
425 | ```html
|
426 | <label for="c0-name">Name</label> <input id="c0-name" value="Frank" />
|
427 | ```
|
428 |
|
429 | **`aria-describedby:scoped`**
|
430 |
|
431 | ```marko
|
432 | <button
|
433 | aria-describedby:scoped="closeDisclaimer"
|
434 | on-click('closeDialog')>Close</button>
|
435 |
|
436 | <p id:scoped="closeDisclaimer">
|
437 | Closing this window will discard any entered information and return you to the main page.
|
438 | </p>
|
439 | ```
|
440 |
|
441 | ```html
|
442 | <button aria-describedby="c0-closeDisclaimer">Close</button>
|
443 |
|
444 | <p id="c0-closeDisclaimer">
|
445 | Closing this window will discard any entered information and return you to the
|
446 | main page.
|
447 | </p>
|
448 | ```
|
449 |
|
450 | **`href:scoped`**
|
451 |
|
452 | ```marko
|
453 | <a href:scoped="#anchor">Jump to section</a>
|
454 | <section id:scoped="anchor"></section>
|
455 | ```
|
456 |
|
457 | ```html
|
458 | <a href="#c0-anchor">Jump to section</a>
|
459 | <section id="c0-anchor"></section>
|
460 | ```
|
461 |
|
462 | ### `no-update`
|
463 |
|
464 | Preserves the DOM subtree associated with the element or component, so it won’t be modified when rerendering.
|
465 |
|
466 | ```marko
|
467 | <!-- Never rerender this table -->
|
468 | <table no-update>
|
469 | …
|
470 | </table>
|
471 | ```
|
472 |
|
473 | ```marko
|
474 | <!-- N ever rerender this UI component -->
|
475 | <app-map no-update/>
|
476 | ```
|
477 |
|
478 | This is most useful when other JavaScript modifies the DOM tree of an element, like for embeds.
|
479 |
|
480 | ### `no-update-if`
|
481 |
|
482 | Similar to [no-update](#no-update), except that the DOM subtree is _conditionally_ preserved:
|
483 |
|
484 | ```marko
|
485 | <!-- Don’t re-render this table without table data -->
|
486 | <table no-update-if(input.tableData == null)>
|
487 | …
|
488 | </table>
|
489 | ```
|
490 |
|
491 | ### `no-update-body`
|
492 |
|
493 | Similar to [no-update](#no-update), except that only the descendant DOM nodes are preserved:
|
494 |
|
495 | ```marko
|
496 | <!-- Never rerender any nested DOM elements -->
|
497 | <div no-update-body>
|
498 | …
|
499 | </div>
|
500 | ```
|
501 |
|
502 | ### `no-update-body-if`
|
503 |
|
504 | Similar to [no-update-body](#no-update-body), except that its descendant DOM nodes are _conditionally_ preserved:
|
505 |
|
506 | ```marko
|
507 | <!-- Never rerender any nested DOM elements without table data -->
|
508 | <table no-update-body-if(input.tableData == null)>
|
509 | …
|
510 | </table>
|
511 | ```
|
512 |
|
513 | ### `:no-update`
|
514 |
|
515 | Prevents certain attributes from being modified during a rerender. The attribute(s) that should not be modified should have a `:no-update` modifier:
|
516 |
|
517 | ```marko
|
518 | <!-- Never modify the `class` attribute -->
|
519 | <div class:no-update=input.className>
|
520 | …
|
521 | </div>
|
522 | ```
|
523 |
|
524 | ## Properties
|
525 |
|
526 | ### `this.el`
|
527 |
|
528 | The root [`HTMLElement` object](https://developer.mozilla.org/en-US/docs/Web/API/element) that the component is bound to. If there are multiple roots, this is the first.
|
529 |
|
530 | ### `this.els`
|
531 |
|
532 | An array of the root [`HTMLElement` objects](https://developer.mozilla.org/en-US/docs/Web/API/element) that the component is bound to.
|
533 |
|
534 | > ⚠️ `this.el` and `this.els` are deprecated. Please use [the `this.getEl()` or `this.getEls()` methods](#getelkey-index).
|
535 |
|
536 | ### `this.id`
|
537 |
|
538 | A string identifier for the root HTML element that the component is bound to. (Not the `id` attribute.)
|
539 |
|
540 | ### `this.state`
|
541 |
|
542 | The current state for the component. Changing `this.state` or its direct properties will cause the component to rerender.
|
543 |
|
544 | Only properties that exist when `this.state` is first defined will be watched for changes. If you don’t need a property initially, you can set its value to `null`:
|
545 |
|
546 | ```marko
|
547 | class {
|
548 | onCreate() {
|
549 | this.state = {
|
550 | data: null,
|
551 | error: null
|
552 | }
|
553 | }
|
554 | getData() {
|
555 | fetch('/endpoint')
|
556 | .then(data => this.state.data = data)
|
557 | .catch(error => this.state.error = error);
|
558 | }
|
559 | }
|
560 | ```
|
561 |
|
562 | Beware: setting a `state` property only _nominates_ the component for a possible rerender, and properties are only watched one level deep. Thus, the component is only rerendered if at least one of the component state properties changed (`oldValue !== newValue`).
|
563 |
|
564 | If none of the properties changed (because the new value is identical, or no difference is detected by a shallow comparison), the assignment is considered a no-operation (great for performance).
|
565 |
|
566 | We recommend using [immutable data structures](https://wecodetheweb.com/2016/02/12/immutable-javascript-using-es6-and-beyond/), but if you want to mutate a state property (perhaps push a new item into an array), you can mark it as dirty with `setStateDirty`:
|
567 |
|
568 | ```js
|
569 | this.state.numbers.push(num);
|
570 |
|
571 | // Mark numbers as dirty, because a `push`
|
572 | // won’t be automatically detected by Marko
|
573 | this.setStateDirty("numbers");
|
574 | ```
|
575 |
|
576 | ### `this.input`
|
577 |
|
578 | The current input for the component. Setting `this.input` will rerender the component. If a `$global` property is set, `out.global` will also be updated during the rerender, otherwise the existing `$global` is used.
|
579 |
|
580 | ## Variables
|
581 |
|
582 | When a Marko component is compiled, some additional variables are available to the rendering function. These variables are described below.
|
583 |
|
584 | ### `component`
|
585 |
|
586 | The `component` variable refers to the instance of the currently rendering UI component. This variable can be used to call methods on the UI component instance:
|
587 |
|
588 | ```marko
|
589 | class {
|
590 | getFullName() {
|
591 | const { person } = this.input;
|
592 | return `${person.firstName} ${person.lastName}`;
|
593 | }
|
594 | }
|
595 |
|
596 | <h1>Hello, ${component.getFullName()}</h1>
|
597 | ```
|
598 |
|
599 | ### `input`
|
600 |
|
601 | The `input` variable refers to the `input` object, and is equivalent to `component.input`|`this.input`.
|
602 |
|
603 | ```marko
|
604 | <h1>Hello, ${input.name}</h1>
|
605 | ```
|
606 |
|
607 | ### `state`
|
608 |
|
609 | The `state` variable refers to the UI component’s `state` object, and is the _unwatched_ equivalent of `component.state`|`this.state`.
|
610 |
|
611 | ```marko
|
612 | <h1>Hello ${state.name}</h1>
|
613 | ```
|
614 |
|
615 | ## Methods
|
616 |
|
617 | ### `destroy([options])`
|
618 |
|
619 | | Option | Type | Default | Description |
|
620 | | ------------ | --------- | ------- | --------------------------------------------------------------------------------- |
|
621 | | `removeNode` | `Boolean` | `true` | `false` will keep the component in the DOM while unsubscribing all events from it |
|
622 | | `recursive` | `Boolean` | `true` | `false` will prevent child components from being destroyed |
|
623 |
|
624 | Destroys the component by unsubscribing from all listeners made using the `subscribeTo` method, and then detaching the component’s root element from the DOM. All nested components (discovered by querying the DOM) are also destroyed.
|
625 |
|
626 | ```js
|
627 | component.destroy({
|
628 | removeNode: false, // true by default
|
629 | recursive: false // true by default
|
630 | });
|
631 | ```
|
632 |
|
633 | ### `forceUpdate()`
|
634 |
|
635 | Queue the component to re-render and skip all checks to see if it actually needs it.
|
636 |
|
637 | > When using `forceUpdate()` the updating of the DOM will be queued up. If you want to immediately update the DOM
|
638 | > then call `this.update()` after calling `this.forceUpdate()`.
|
639 |
|
640 | ### `getEl([key, index])`
|
641 |
|
642 | | Signature | Type | Description |
|
643 | | ------------ | ------------- | --------------------------------------------------------------------------------- |
|
644 | | `key` | `String` | _optional_ — the scoped identifier for the element |
|
645 | | `index` | `Number` | _optional_ — the index of the component, if `key` references a repeated component |
|
646 | | return value | `HTMLElement` | The element matching the key, or `this.el` if no key is provided |
|
647 |
|
648 | Returns a nested DOM element by prefixing the provided `key` with the component’s ID. For Marko, nested DOM elements should be assigned an ID with the `key` attribute.
|
649 |
|
650 | ### `getEls(key)`
|
651 |
|
652 | | Signature | Type | Description |
|
653 | | ------------ | -------------------- | ----------------------------------------------------- |
|
654 | | `key` | `String` | The scoped identifier for the element |
|
655 | | return value | `Array<HTMLElement>` | An array of _repeated_ DOM elements for the given key |
|
656 |
|
657 | Repeated DOM elements must have a value for the `key` attribute that ends with `[]`. For example, `key="items[]"`.
|
658 |
|
659 | ### `getElId([key, index])`
|
660 |
|
661 | | Signature | Type | Description |
|
662 | | ------------ | -------- | --------------------------------------------------------------------------------- |
|
663 | | `key` | `String` | _optional_ — The scoped identifier for the element |
|
664 | | `index` | `Number` | _optional_ — The index of the component, if `key` references a repeated component |
|
665 | | return value | `String` | The element ID matching the key, or `this.el.id` if `key` is undefined |
|
666 |
|
667 | Similar to `getEl`, but only returns the String ID of the nested DOM element instead of the actual DOM element.
|
668 |
|
669 | ### `getComponent(key[, index])`
|
670 |
|
671 | | Signature | Type | Description |
|
672 | | ------------ | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
673 | | `key` | `String` | The scoped identifier for the element |
|
674 | | `index` | `Number` | _optional_ — The index of the component, if `key` references a repeated component |
|
675 | | return value | `Component` | A reference to a nested `Component` for the given key. If an `index` is provided and the target component is a repeated component (i.e. `key="items[]"`), then the component at the given index will be returned. |
|
676 |
|
677 | For example, given the following component,
|
678 |
|
679 | ```marko
|
680 | <app-main>
|
681 | <app-child key="child"/>
|
682 | </app-main>
|
683 | ```
|
684 |
|
685 | The following code can be used to get the `<app-child/>` component:
|
686 |
|
687 | ```js
|
688 | const childComponent = this.getComponent("child");
|
689 | ```
|
690 |
|
691 | ### `getComponents(key, [, index])`
|
692 |
|
693 | | Signature | Type | Description |
|
694 | | ------------ | ------------------ | --------------------------------------------------------------------------------- |
|
695 | | `key` | `String` | The scoped identifier for the element |
|
696 | | `index` | `Number` | _optional_ — The index of the component, if `key` references a repeated component |
|
697 | | return value | `Array<Component>` | An array of _repeated_ `Component` instances for the given key |
|
698 |
|
699 | Repeated components must have a value for the `key` attribute that ends with `[]`, like `key="items[]"`.
|
700 |
|
701 | ### `isDestroyed()`
|
702 |
|
703 | Returns `true` if a component has been destroyed using [`component.destroy()`](#ondestroy), otherwise `false`.
|
704 |
|
705 | ### `isDirty()`
|
706 |
|
707 | Returns `true` if the component needs a bath.
|
708 |
|
709 | ### `replaceState(newState)`
|
710 |
|
711 | | Signature | Type | Description |
|
712 | | ---------- | -------- | ------------------------------------------------ |
|
713 | | `newState` | `Object` | A new state object to replace the previous state |
|
714 |
|
715 | Replaces the state with an entirely new state. Equivalent to `this.state = newState`.
|
716 |
|
717 | > **Note:** While `setState()` is additive and will not remove properties that are in the old state but not in the new state, `replaceState()` _will_ add the new state and remove the old state properties that are not found in the new state. Thus, if `replaceState()` is used, consider possible side effects if the new state contains less or other properties than the replaced state.
|
718 |
|
719 | ### `rerender([input])`
|
720 |
|
721 | | Signature | Type | Description |
|
722 | | --------- | -------- | --------------------------------------------------- |
|
723 | | `input` | `Object` | _optional_ — New input data to use when rerendering |
|
724 |
|
725 | Rerenders the component using its `renderer`, and either supplied `input` or internal `input` and `state`.
|
726 |
|
727 | ### `setState(name, value)`
|
728 |
|
729 | | Signature | Type | Description |
|
730 | | --------- | -------- | ------------------------------------------ |
|
731 | | `name` | `String` | The name of the `state` property to update |
|
732 | | `value` | `Any` | The new value for the `state` property |
|
733 |
|
734 | Changes the value of a single `state` property. Equivalent to `this.state[name] = value`, except it will also work for adding new properties to the component state.
|
735 |
|
736 | ```js
|
737 | this.setState("disabled", true);
|
738 | ```
|
739 |
|
740 | ### `setState(newState)`
|
741 |
|
742 | | Signature | Type | Description |
|
743 | | ---------- | -------- | --------------------------------------------------- |
|
744 | | `newState` | `Object` | A new state object to merge into the previous state |
|
745 |
|
746 | Changes the value of multiple state properties:
|
747 |
|
748 | ```js
|
749 | this.setState({
|
750 | disabled: true,
|
751 | size: "large"
|
752 | });
|
753 | ```
|
754 |
|
755 | ### `setStateDirty(name[, value])`
|
756 |
|
757 | | Signature | Type | Description |
|
758 | | --------- | -------- | ------------------------------------------------- |
|
759 | | `name` | `String` | The name of the `state` property to mark as dirty |
|
760 | | `value` | `Any` | _optional_ — A new value for the `state` property |
|
761 |
|
762 | Forces a state property change, even if the value is equal to the old value. This is helpful in cases where a change occurs to a complex object that would not be detected by a shallow compare. Invoking this function completely circumvents all property equality checks (shallow compares) and always rerenders the component.
|
763 |
|
764 | #### More details
|
765 |
|
766 | The first parameter, `name`, is used to allow update handlers (e.g. `update_foo(newValue)`) to handle the state transition for the specific state property that was marked dirty.
|
767 |
|
768 | The second parameter, `value`, is used as the new value that is given to update handlers. Because `setStateDirty()` always bypasses all property equality checks, this parameter is optional. If not given or equal to the old value, the old value will be used for the update handler.
|
769 |
|
770 | Important: the given parameters do not affect how or if `setStateDirty()` rerenders a component; they are only considered as additional information to update handlers.
|
771 |
|
772 | ```js
|
773 | // Because this does not create a new array, the change
|
774 | // would not be detected by a shallow property comparison
|
775 | this.state.colors.push("red");
|
776 |
|
777 | // Force that particular state property to be considered dirty so
|
778 | // that it will trigger the component's view to be updated
|
779 | this.setStateDirty("colors");
|
780 | ```
|
781 |
|
782 | ### `subscribeTo(emitter)`
|
783 |
|
784 | | Signature | Description |
|
785 | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
786 | | `emitter` | A [Node.js `EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter) or DOM object that emits events (`window`, `document`, etc.) |
|
787 | | return value | A tracked subscription |
|
788 |
|
789 | When a component is destroyed, it is necessary to remove any listeners that were attached by the component to prevent memory leaks. By using `subscribeTo`, Marko will automatically track and remove any listeners you attach when the component is destroyed.
|
790 |
|
791 | Marko uses [`listener-tracker`](https://github.com/patrick-steele-idem/listener-tracker) to provide this feature.
|
792 |
|
793 | ```js
|
794 | this.subscribeTo(window).on("scroll", () =>
|
795 | console.log("The user scrolled the window!")
|
796 | );
|
797 | ```
|
798 |
|
799 | ### `update()`
|
800 |
|
801 | Immediately executes any pending updates to the DOM, rather than following the normal queued update mechanism for rendering.
|
802 |
|
803 | ```js
|
804 | this.setState("foo", "bar");
|
805 | this.update(); // Force the DOM to update
|
806 | this.setState("hello", "world");
|
807 | this.update(); // Force the DOM to update
|
808 | ```
|
809 |
|
810 | ## Event methods
|
811 |
|
812 | Marko components inherit from [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter). Below are a few commonly used methods — view the Node.js docs for the full list.
|
813 |
|
814 | ### `emit(eventName, ...args)`
|
815 |
|
816 | | Signature | Type | Description |
|
817 | | ----------- | -------- | ----------------------------------------------------- |
|
818 | | `eventName` | `String` | Name of the event |
|
819 | | `...args` | `Any` | All subsequent parameters are passed to the listeners |
|
820 |
|
821 | Emits a UI component custom event. If a UI component attached a listener with the matching `eventName`, then the corresponding event listener method will be invoked. Event listeners can be attached using either the [`on-[event](methodName|function, ...args)`](#declarative-custom-events) attribute syntax, or `targetComponent.on()`.
|
822 |
|
823 | ### `on(eventName, handler)`
|
824 |
|
825 | | Signature | Type | Description |
|
826 | | ----------- | ---------- | ----------------------------------------- |
|
827 | | `eventName` | `String` | Name of the event to listen for |
|
828 | | `handler` | `Function` | The function to call when the event fires |
|
829 |
|
830 | Adds the listener function to the end of the listeners array for the `eventName` event. Does not check to see if the listener has already been added. Multiple calls passing the same combination of `eventName` and `handler` will result in the listener being added and called multiple times.
|
831 |
|
832 | ### `once(eventName, handler)`
|
833 |
|
834 | | Signature | Type | Description |
|
835 | | ----------- | ---------- | ------------------------------------------ |
|
836 | | `eventName` | `String` | Name of the event to listen for |
|
837 | | `handler` | `Function` | Tthe function to call when the event fires |
|
838 |
|
839 | Adds a one-time listener function for the `eventName` event. The next time `eventName` triggers, this listener is removed and then invoked.
|
840 |
|
841 | ## Lifecycle events
|
842 |
|
843 | Marko defines six lifecycle events:
|
844 |
|
845 | - `create`
|
846 | - `input`
|
847 | - `render`
|
848 | - `mount`
|
849 | - `update`
|
850 | - `destroy`
|
851 |
|
852 | These events are emitted at specific points over the lifecycle of a component, as shown below:
|
853 |
|
854 | **First render**
|
855 |
|
856 | ```js
|
857 | emit('create') → emit('input') → emit('render') → emit('mount')
|
858 | ```
|
859 |
|
860 | **New input**
|
861 |
|
862 | ```js
|
863 | emit('input') → emit('render') → emit('update')
|
864 | ```
|
865 |
|
866 | **Internal state change**
|
867 |
|
868 | ```js
|
869 | emit('render') → emit('update')
|
870 | ```
|
871 |
|
872 | **Destroy**
|
873 |
|
874 | ```js
|
875 | emit("destroy");
|
876 | ```
|
877 |
|
878 | ### Lifecycle event methods
|
879 |
|
880 | Each lifecycle event has a corresponding component lifecycle method that can listen for the event:
|
881 |
|
882 | ```js
|
883 | class {
|
884 | onCreate(input, out) { }
|
885 | onInput(input, out) { }
|
886 | onRender(out) { }
|
887 | onMount() { }
|
888 | onUpdate() { }
|
889 | onDestroy() { }
|
890 | }
|
891 | ```
|
892 |
|
893 | > **ProTip:** When a lifecycle event occurs in the browser, the corresponding event is emitted on the component instance. A parent component, or other code that has access to the component instance, can listen for these events. For example:
|
894 | >
|
895 | > ```js
|
896 | > component.on("input", function(input, out) {
|
897 | > // The component received an input
|
898 | > });
|
899 | > ```
|
900 |
|
901 | ### `onCreate(input, out)`
|
902 |
|
903 | | Signature | Description |
|
904 | | --------- | --------------------------------------------------------------- |
|
905 | | `input` | The input data used to render the component for the first time |
|
906 | | `out` | The async `out` used to render the component for the first time |
|
907 |
|
908 | The `create` event is emitted (and `onCreate` is called) when the component is first created.
|
909 |
|
910 | `onCreate` is typically used to set the initial state for stateful components:
|
911 |
|
912 | ```marko
|
913 | class {
|
914 | onCreate(input) {
|
915 | this.state = { count: input.initialCount };
|
916 | }
|
917 | }
|
918 | ```
|
919 |
|
920 | ### `onInput(input, out)`
|
921 |
|
922 | | Signature | Description |
|
923 | | --------- | ------------------ |
|
924 | | `input` | The new input data |
|
925 |
|
926 | The `input` event is emitted (and `onInput` is called) when the component receives input: both the initial input, and for any subsequent updates to its input.
|
927 |
|
928 | ### `onRender(out)`
|
929 |
|
930 | | Signature | Description |
|
931 | | --------- | -------------------------------------- |
|
932 | | `out` | The async `out` for the current render |
|
933 |
|
934 | The `render` event is emitted (and `onRender` is called) when the component is about to render or rerender.
|
935 |
|
936 | ### `onMount()`
|
937 |
|
938 | The `mount` event is emitted (and `onMount` is called) when the component is first mounted to the DOM. For server-rendered components, this is the first event that is emitted only in the browser.
|
939 |
|
940 | This is the first point at which `this.el` and `this.els` are defined. `onMount` is commonly used to attach third-party JavaScript to the newly-mounted DOM.
|
941 |
|
942 | For example, attaching a library that monitors if the component is in the viewport:
|
943 |
|
944 | ```marko
|
945 | import scrollmonitor from 'scrollmonitor';
|
946 |
|
947 | class {
|
948 | onMount() {
|
949 | this.watcher = scrollmonitor.create(this.el);
|
950 | this.watcher.enterViewport(() => console.log('I have entered the viewport'));
|
951 | this.watcher.exitViewport(() => console.log('I have left the viewport'));
|
952 | }
|
953 | }
|
954 | ```
|
955 |
|
956 | ### `onUpdate()`
|
957 |
|
958 | The `update` event is emitted (and `onUpdate` is called) when the component is called after a component rerenders and the DOM has been updated. If a rerender does not update the DOM (nothing changed), this event will not fire.
|
959 |
|
960 | ### `onDestroy()`
|
961 |
|
962 | The `destroy` event is emitted (and `onDestroy` is called) when the component is about to unmount from the DOM and cleaned up. `onDestroy` should be used to do any additional cleanup beyond what Marko handles itself.
|
963 |
|
964 | For example, cleaning up from our `scrollmonitor` example in [`onMount`](#onmount):
|
965 |
|
966 | ```marko
|
967 | import scrollmonitor from 'scrollmonitor';
|
968 |
|
969 | class {
|
970 | onMount() {
|
971 | this.watcher = scrollmonitor.create(this.el);
|
972 | this.watcher.enterViewport(() => console.log('Entered the viewport'));
|
973 | this.watcher.exitViewport(() => console.log('Left the viewport'));
|
974 | }
|
975 | onDestroy() {
|
976 | this.watcher.destroy();
|
977 | }
|
978 | }
|
979 | ```
|
980 |
|
981 | ## DOM manipulation methods
|
982 |
|
983 | The following methods move the component’s root DOM node(s) from the current parent element to a new parent element (or out of the DOM in the case of `detach`).
|
984 |
|
985 | ### `appendTo(targetEl)`
|
986 |
|
987 | Moves the UI component’s DOM elements into the position after the target element’s last child.
|
988 |
|
989 | ```js
|
990 | this.appendTo(document.body);
|
991 | ```
|
992 |
|
993 | ### `insertAfter(targetEl)`
|
994 |
|
995 | Moves the UI component’s DOM elements into the position after the target DOM element.
|
996 |
|
997 | ### `insertBefore(targetEl)`
|
998 |
|
999 | Moves the UI component’s DOM elements into the position before the target DOM element.
|
1000 |
|
1001 | ### `prependTo(targetEl)`
|
1002 |
|
1003 | Moves the UI component’s DOM elements into the position before the target element’s first child.
|
1004 |
|
1005 | ### `replace(targetEl)`
|
1006 |
|
1007 | Replaces the target element with the UI component’s DOM elements.
|
1008 |
|
1009 | ### `replaceChildrenOf(targetEl)`
|
1010 |
|
1011 | Replaces the target element’s children with the UI component’s DOM elements.
|