UNPKG

38.1 kBMarkdownView Raw
1# Class Components
2
3Marko makes it easy to create user interface components to use as building blocks for web pages and applications of any complexity.
4
5Marko 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
11Marko 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
17In Marko, the DOM output of a UI component is based on _input properties_ and optional _internal state_ used to control the view.
18
19If 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
23Marko 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
31A 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
35Marko 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
38class {
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
55Adding styles in your view is also made easy:
56
57```marko
58style {
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
68These 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
70If you use a CSS preprocessor, you can add its file extension on `style`:
71
72```marko
73style.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
84You 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
90Marko 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```
93counter.marko
94counter.component.js
95counter.style.css
96```
97
98Marko 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```
101counter/
102 index.marko
103 component.js
104 style.css
105```
106
107In your `component.js` file, export the component’s class:
108
109```js
110module.exports = class {
111 onCreate() {
112 this.state = {
113 count: 0
114 };
115 }
116 increment() {
117 this.state.count++;
118 }
119};
120```
121
122In 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
129And 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
141If you target browsers that does not support classes, a plain object of methods can be exported:
142
143```js
144module.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
158Split 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
162Additionally, 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
168Marko 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```
171counter/
172 index.marko
173 component-browser.js
174```
175
176A 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
178This 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```
181class {
182 onCreate(input, out) { }
183 onInput(input, out) { }
184 onRender(out) { }
185 onDestroy() { }
186}
187```
188
189And the following [lifecycle methods](#lifecycle-events) can go inside the `component-browser.js` file:
190
191```
192class {
193 onMount() { }
194 onUpdate() { }
195}
196```
197
198Any 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
205class {
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
217module.exports = {
218 shout() {
219 alert(`My favorite number is ${this.number}!`);
220 }
221};
222```
223
224## Event handling
225
226The `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
231The `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
233You 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
237The code below illustrates how to attach an event listener for native DOM events:
238
239```marko
240class {
241 onButtonClick(name, event, el) {
242 alert(`Hello ${name}!`);
243 }
244}
245
246static 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
264The following arguments are passed to the event handler when the event occurs:
265
2661. `...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)`
2682. `event` - The native DOM event object.
2693. `el` - The DOM element that the event listener was attached to.
270
271When 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
277The code below illustrates how to attach an event listener for a UI component’s custom event:
278
279```marko
280class {
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
292The following arguments are passed to the event handler when the event occurs:
293
2941. `...args` - Any extra bound arguments are _prepended_ to the arguments passed to the component’s handler method.
2952. `...eventArgs` - The arguments passed to `this.emit()` by the target UI component.
2963. `component` - The component instance that the event listener was attached to.
297
298The following code illustrates how the UI component for `<counter>` might emit its `change` event:
299
300`counter/index.marko`
301
302```marko
303class {
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
334The `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
338The 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
342The `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
347Internally, 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
352class {
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
375The `key` attribute can pair an HTML element or UI component that moves to a new location in the DOM. For example:
376
377```marko
378class {
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
396The `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
406This way, if the order of `input.users` changes, the DOM will be rerendered more efficiently.
407
408#### `*:scoped`
409
410The `: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
412Here’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
414The `: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
423The 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
464Preserves 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
478This is most useful when other JavaScript modifies the DOM tree of an element, like for embeds.
479
480### `no-update-if`
481
482Similar 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
493Similar 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
504Similar 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
515Prevents 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
528The 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
532An 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
538A string identifier for the root HTML element that the component is bound to. (Not the `id` attribute.)
539
540### `this.state`
541
542The current state for the component. Changing `this.state` or its direct properties will cause the component to rerender.
543
544Only 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
547class {
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
562Beware: 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
564If 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
566We 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
569this.state.numbers.push(num);
570
571// Mark numbers as dirty, because a `push`
572// won’t be automatically detected by Marko
573this.setStateDirty("numbers");
574```
575
576### `this.input`
577
578The 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
582When a Marko component is compiled, some additional variables are available to the rendering function. These variables are described below.
583
584### `component`
585
586The `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
589class {
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
601The `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
609The `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
624Destroys 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
627component.destroy({
628 removeNode: false, // true by default
629 recursive: false // true by default
630});
631```
632
633### `forceUpdate()`
634
635Queue 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
648Returns 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
657Repeated 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
667Similar 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
677For example, given the following component,
678
679```marko
680<app-main>
681 <app-child key="child"/>
682</app-main>
683```
684
685The following code can be used to get the `<app-child/>` component:
686
687```js
688const 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
699Repeated components must have a value for the `key` attribute that ends with `[]`, like `key="items[]"`.
700
701### `isDestroyed()`
702
703Returns `true` if a component has been destroyed using [`component.destroy()`](#ondestroy), otherwise `false`.
704
705### `isDirty()`
706
707Returns `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
715Replaces 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
725Rerenders 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
734Changes 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
737this.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
746Changes the value of multiple state properties:
747
748```js
749this.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
762Forces 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
766The 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
768The 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
770Important: 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
775this.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
779this.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
789When 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
791Marko uses [`listener-tracker`](https://github.com/patrick-steele-idem/listener-tracker) to provide this feature.
792
793```js
794this.subscribeTo(window).on("scroll", () =>
795 console.log("The user scrolled the window!")
796);
797```
798
799### `update()`
800
801Immediately executes any pending updates to the DOM, rather than following the normal queued update mechanism for rendering.
802
803```js
804this.setState("foo", "bar");
805this.update(); // Force the DOM to update
806this.setState("hello", "world");
807this.update(); // Force the DOM to update
808```
809
810## Event methods
811
812Marko 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
821Emits 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
830Adds 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
839Adds 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
843Marko defines six lifecycle events:
844
845- `create`
846- `input`
847- `render`
848- `mount`
849- `update`
850- `destroy`
851
852These events are emitted at specific points over the lifecycle of a component, as shown below:
853
854**First render**
855
856```js
857emit('create') → emit('input') → emit('render') → emit('mount')
858```
859
860**New input**
861
862```js
863emit('input') → emit('render') → emit('update')
864```
865
866**Internal state change**
867
868```js
869emit('render') → emit('update')
870```
871
872**Destroy**
873
874```js
875emit("destroy");
876```
877
878### Lifecycle event methods
879
880Each lifecycle event has a corresponding component lifecycle method that can listen for the event:
881
882```js
883class {
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
908The `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
913class {
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
926The `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
934The `render` event is emitted (and `onRender` is called) when the component is about to render or rerender.
935
936### `onMount()`
937
938The `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
940This 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
942For example, attaching a library that monitors if the component is in the viewport:
943
944```marko
945import scrollmonitor from 'scrollmonitor';
946
947class {
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
958The `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
962The `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
964For example, cleaning up from our `scrollmonitor` example in [`onMount`](#onmount):
965
966```marko
967import scrollmonitor from 'scrollmonitor';
968
969class {
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
983The 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
987Moves the UI component’s DOM elements into the position after the target element’s last child.
988
989```js
990this.appendTo(document.body);
991```
992
993### `insertAfter(targetEl)`
994
995Moves the UI component’s DOM elements into the position after the target DOM element.
996
997### `insertBefore(targetEl)`
998
999Moves the UI component’s DOM elements into the position before the target DOM element.
1000
1001### `prependTo(targetEl)`
1002
1003Moves the UI component’s DOM elements into the position before the target element’s first child.
1004
1005### `replace(targetEl)`
1006
1007Replaces the target element with the UI component’s DOM elements.
1008
1009### `replaceChildrenOf(targetEl)`
1010
1011Replaces the target element’s children with the UI component’s DOM elements.