marko/docs/widgets/overview.md
Charlie Duong fc82ef0cf0 Deprecated w-on* attributes (#434)
Added support for on*() attributes.
2016-11-16 10:53:51 -08:00

412 lines
11 KiB
Markdown

Overview
========
Marko Widgets extends the [Marko templating engine](https://github.com/marko-js/marko) to provide a simple and efficient mechanism for binding behavior to UI components rendered on either the server or in the browser. In addition, changing a widgets state or properties will result in the DOM automatically being updated without writing extra code. Marko Widgets has adopted many of the good design principles promoted by the [React](https://facebook.github.io/react/index.html) team, but aims to be much lighter and often faster (especially on the server). When updating the view for a widget, Marko Widgets uses DOM diffing to make the minimum number of changes to the DOM through the use of the [morphdom](https://github.com/patrick-steele-idem/morphdom) module.
<a href="http://markojs.com/marko-widgets/try-online/" target="_blank">Try Marko Widgets Online!</a>
# Features
- Simple
- Clean JavaScript syntax for defining widgets
- Utilizes [Marko templates](https://github.com/marko-js/marko) (an HTML-based templating language) for the view
- Supports stateful and stateless widgets
- No complex class hierarchy
- Simple, declarative event binding for both native DOM events and custom events
- Lifecycle management for widgets (easily destroy and create widgets)
- Events bubble up and view state changes trickle down
- Only need to understand a few concepts to get started
- High performance
- Lightning fast performance on the server and in the browser (see [Marko vs React: Performance Benchmark](https://github.com/patrick-steele-idem/marko-vs-react))
- Supports streaming and asynchronous rendering
- Efficient binding of behavior of UI components rendered on the server and in the browser
- Efficient updating of the DOM via the following tricks:
- DOM diffing is used to make the minimum number of changes to the DOM using the [morphdom](https://github.com/patrick-steele-idem/morphdom) module.
- Batched updates
- When re-rendering a widget, nested widgets are reused
- Only widgets whose state changed are re-rendered
- Full re-rendering of a widget can be short circuited if state transition handlers are provided
- For container components, nested body DOM nodes are automatically preserved
- Entire DOM subtrees can be preserved between rendering
- Smart template compilers to offload as much work to compile time
- Very efficient event delegation
- Fast serialization of state from the server to the browser using [warp10](https://github.com/patrick-steele-idem/warp10)
- Lightweight
- Extremely small JavaScript runtime (~6.3 KB gzipped)
- No dependencies on any other JavaScript library such as jQuery
- Focused exclusively on the UI view (easily mix and match with other libraries/frameworks)
# Design Philosophy
- A UI component should encapsulate view, behavior and styling
- A complex page should be decomposed into modular UI components
- UI components should be used as building blocks
- A component's view should be driven by a pure function that accepts an input state and produces output HTML
- A UI component should be independently testable
- A UI component should not leak its internal implementation
- A UI component should be installable via npm
- A UI component should play nice with other frameworks and libraries
- UI components should be easily composable
- Developers should not need to manually manipulate the DOM
# Sample Code
Marko Widgets allows you to declaratively bind behavior to an HTML element inside a Marko template. The widget provides the client-side behavior for your UI component.
## Stateless Widget
__src/components/app-hello/template.marko__
```xml
<div w-bind>
Hello ${data.name}!
</div>
```
__src/components/app-hello/index.js__
```javascript
module.exports = require('marko-widgets').defineComponent({
template: require('./template.marko'),
getTemplateData: function(state, input) {
return {
name: input.name
};
},
init: function() {
var el = this.el; // The root DOM element that the widget is bound to
console.log('Initializing widget: ' + el.id);
}
});
```
Congratulations, you just built a reusable UI component! Your UI component can be embedded in other Marko template files:
```xml
<div>
<app-hello name="Frank"/>
</div>
```
In addition, your UI can be rendered and added to the DOM using the JavaScript API:
```javascript
var widget = require('./app-hello')
.render({
name: 'John'
})
.appendTo(document.body)
.getWidget();
// Changing the props will trigger the widget to re-render
// with the new props and for the DOM to be updated:
widget.setProps({
name: 'Jane'
});
```
## Stateless Widget with Behavior
__src/components/app-hello/template.marko__
```xml
<div w-bind
onClick("handleClick")>
Hello ${data.name}!
</div>
```
__src/components/app-hello/index.js__
```javascript
module.exports = require('marko-widgets').defineComponent({
template: require('./template.marko'),
getTemplateData: function(state, input) {
return {
name: input.name
};
},
handleClick: function() {
this.setSelected(true);
},
setSelected: function(selected) {
if (selected) {
this.el.style.backgroundColor = 'yellow';
} else {
this.el.style.backgroundColor = null;
}
}
});
```
## Stateful Widget
Let's create a stateful widget that changes to yellow when you click on it:
__src/components/app-hello/template.marko__
```xml
<div w-bind
onClick("handleClick")
style="background-color: ${data.color}">
Hello ${data.name}!
</div>
```
__src/components/app-hello/index.js__
```javascript
module.exports = require('marko-widgets').defineComponent({
template: require('./template.marko'),
getInitialState: function(input) {
return {
name: input.name,
selected: input.selected || false;
}
},
getTemplateData: function(state, input) {
var style = ;
return {
name: state.name,
color: state.selected ? 'yellow' : 'transparent'
};
},
handleClick: function() {
this.setState('selected', true);
},
isSelected: function() {
return this.state.selected;
}
});
```
## Stateful Widget with Update Handlers
If you want to avoid re-rendering a widget for a particular state property change then simply provide your own method to handle the state change as shown below:
__src/components/app-hello/template.marko__
```xml
<div w-bind
onClick("handleClick")
style="background-color: ${data.color}">
Hello ${data.name}!
</div>
```
__src/components/app-hello/index.js__
```javascript
module.exports = require('marko-widgets').defineComponent({
template: require('./template.marko'),
getInitialState: function(input) {
return {
name: input.name,
selected: input.selected || false;
}
},
getTemplateData: function(state, input) {
var style = ;
return {
name: state.name,
color: state.selected ? 'yellow' : 'transparent'
};
},
handleClick: function() {
this.setState('selected', true);
},
isSelected: function() {
return this.state.selected;
},
update_selected: function(newSelected) {
// Manually update the DOM to reflect the new "selected"
// state" to avoid re-rendering the entire widget.
if (newSelected) {
this.el.style.backgroundColor = 'yellow';
} else {
this.el.style.backgroundColor = null;
}
}
});
```
## Complex Widget
```xml
<div w-bind>
<app-overlay title="My Overlay"
ref="overlay"
onBeforeHide("handleOverlayBeforeHide")>
Body content for overlay.
</app-overlay>
<button type="button"
onClick("handleShowButtonClick")>
Show Overlay
</button>
<button type="button"
onClick("handleHideButtonClick")>
Hide Overlay
</button>
</div>
```
Below is the content of `index.js` where the widget type is defined:
```javascript
module.exports = require('marko-widgets').defineComponent({
template: require('./template.marko'),
init: function() {
// this.el will be the raw DOM element the widget instance
// is bound to:
var el = this.el;
},
handleShowButtonClick: function(event) {
console.log('Showing overlay...');
this.getWidget('overlay').show();
},
handleHideButtonClick: function(event) {
console.log('Hiding overlay...');
this.getWidget('overlay').hide();
},
handleOverlayBeforeHide: function(event) {
console.log('The overlay is about to be hidden!');
}
})
```
## Container Widget
A container widget supports nested content. When the container widget is re-rendered, the nested content is automatically preserved.
__src/components/app-alert/template.marko__
```xml
<div class="alert alert-${data.type}" w-bind>
<i class="alert-icon"/>
<span w-body></span>
</div>
```
__src/components/app-alert/index.js__
```javascript
module.exports = require('marko-widgets').defineComponent({
template: require('./template.marko'),
init: function() {
// this.el will be the raw DOM element the widget instance
// is bound to:
var el = this.el;
},
getInitialState: function(input) {
return {
type: input.type || 'success'
}
},
getTemplateData: function(state, input) {
return {
type: state.type
};
},
getInitialBody: function(input) {
return input.message || input.renderBody;
},
setType: function(type) {
this.setState('type', type);
}
})
```
The widget can then be used as shown below:
```xml
<app-alert message="This is a success alert"/>
<app-alert>
This is a success alert
</app-alert>
<app-alert message="This is a failure alert" type="failure"/>
<app-alert type="failure">
This is a failure alert
</app-alert>
```
## Preserving DOM Nodes during Re-render
Sometimes it is important to _not_ re-render a DOM subtree. This may due to either of the following reasons:
- Improved performance
- DOM nodes contains externally provided content
- DOM nodes have internal state that needs to be maintained
Marko Widgets allows DOM nodes to be preserved by putting a special `no-update`, `no-update-if(<condition>)`, `no-update-body` or `no-update-body-if(<condition>)` attribute on the HTML tags that should be preserved. Preserved DOM nodes will be reused and re-inserted into a widget's newly rendered DOM automatically.
```xml
<div w-bind>
<span no-update>
<p>
The root span and all its children will never
be re-rendered.
</p>
<p>
Rendered at ${Date.now()}.
</p>
</span>
<div no-update-body>
Only the children of the div will preserved and
the outer HTML div tag will be re-rendered.
</div>
Don't rerender the search results if no search results
are provided.
<app-search-results items="data.searchResults"
no-update-if(data.searchResults == null)/>
</div>
```
## Preserving DOM Attributes during Re-render
Similar to preserving DOM nodes, Marko Widgets also makes it possible to preserve specific attributes on a DOM node. This can be helpful if a separately library is modifying DOM attributes and those changes should be preserved during a rerender. This is mostly the case with `class` and `style` attributes when using a animation/tweening engines such as [Velocity.js](http://julian.com/research/velocity/) or [GSAP](http://greensock.com/gsap).
The `w-preserve-attrs` attribute can be applied to any DOM element and it expects a comma-separated list of attribute names as shown below:
```xml
<div w-preserve-attrs="class,style">
...
</div>
```