mirror of
https://github.com/marko-js/marko.git
synced 2025-12-08 19:26:05 +00:00
409 lines
11 KiB
Markdown
409 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>
|
|
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 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
|
|
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
|
|
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>
|
|
<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}">
|
|
<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>
|
|
|
|
<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 `:no-update` attribute can be applied to any DOM element and it expects a single attribute name to the left of the colon as shown below:
|
|
|
|
```xml
|
|
<div class:no-update=data.className style:no-update=data.styles>
|
|
...
|
|
</div>
|
|
``` |