Merge marko-widgets into marko

This commit is contained in:
Patrick Steele-Idem 2016-11-01 17:07:48 -06:00
commit 1b8770df35
570 changed files with 14587 additions and 0 deletions

17
widgets/.gitignore vendored Normal file
View File

@ -0,0 +1,17 @@
/work
/build
/.idea/
/npm-debug.log
/node_modules
/test/node_modules
/*.sublime-workspace
*.orig
.DS_Store
coverage
*.actual.js
*.actual.html
*.marko.js
/.cache
.cache
/test/generated/
.marko-devtools/

30
widgets/.jshintrc Normal file
View File

@ -0,0 +1,30 @@
{
"node": true,
"browser": true,
"esnext": true,
"boss": false,
"curly": false,
"debug": false,
"devel": false,
"eqeqeq": true,
"evil": true,
"forin": false,
"immed": true,
"laxbreak": false,
"newcap": true,
"noarg": true,
"noempty": false,
"nonew": true,
"nomen": false,
"onevar": false,
"plusplus": false,
"regexp": false,
"undef": true,
"sub": false,
"white": false,
"eqeqeq": false,
"latedef": "func",
"unused": "vars",
"strict": false,
"eqnull": true
}

10
widgets/.travis.yml Normal file
View File

@ -0,0 +1,10 @@
sudo: false
node_js:
- "4"
- "5"
- "6"
- "7"
language: node_js
# Use latest version of npm
before_install: npm install -g npm@*
script: "npm test"

364
widgets/CHANGELOG.md Normal file
View File

@ -0,0 +1,364 @@
CHANGELOG
=========
# 6.x
## 6.3.x
### v6.3.5
- Fixed [#160](https://github.com/marko-js/marko-widgets/issues/160) - Listeners for event delegation do not work if `document.body` is not available
### v6.3.4
- Fixed [#154](https://github.com/marko-js/marko-widgets/issues/154) - Listeners for event delegation do not work if `document.body` is not available
### v6.3.3
- Fixed [#153](https://github.com/marko-js/marko-widgets/issues/153) - Bubbling event listeners used by event delegation system are attached too late
### v6.3.2
- Upgraded to `raptor-util@2`
### v6.3.1
- Fixed regression where `code` was note being returned from `getInitWidgetsCode`.
### v6.3.0
- Fixed [#150](https://github.com/marko-js/marko-widgets/issues/150) - Integrated [warp10](https://github.com/patrick-steele-idem/warp10) for high performance state/config serialization. Circular dependencies are now supported and duplicate objects are only serialized once.
## 6.2.x
### v6.2.2
- Revert back to `morphdom@^1` until more testing can be done
### v6.2.1
- Fixes #147 - Immediate widget initialization not working correctly with client reordering enabled
### v6.2.0
- Throw an error if a widget fails to initialize because DOM el is missing
- Upgrade to `morphdom@2`
## 6.1.x
### v6.1.3
- Fixed #144 - ready callback invoked multiple times when using jQuery proxy
- Refactored testing harness
### v6.1.2
- Fixed #141 - w-preserve-if/w-preserve-body-if broken
### v6.1.1
- Fixed #137 - A widget should not update the DOM if it is destroyed (fixed by [@avigy](https://github.com/avigy))
### v6.1.0
- Use [lasso-modules-client](https://github.com/lasso-js/lasso-modules-client) for generating client-side widget module paths
## 6.0.x
### v6.0.2
- Upgraded test dependencies
### v6.0.1
- Improve how client-side module paths are generated when lasso is available
### v6.0.0
- Marko v3 compatibility
# 5.x
## 5.3.x
### v5.3.0
- Fixes [#121](https://github.com/marko-js/marko-widgets/issues/121) - Allow for w-preserve-attrs to enable preservation of attributes:
```html
<div w-preserve-attrs="style,class"></div>
```
## 5.2.x
### v5.2.2
- The function for generating unique widget IDs on the client-side is not
exported as a global on the `window` object (fixes #118).
### 5.2.0
- Fixes #116 - Bug in browserify breaks browserify compatibility (workaround required)
- Use `<noscript>` tag for delivering widget IDs to the browser
## 5.1.x
### 5.1.0
- Fixes #112 DOM document should be bound to widget for purpose of using correct document.getElementById(...)
### 5.0.9
## 5.0.x
### 5.0.9
- Fixes #111 - Make registerWidget a no-op on the server
### 5.0.8
- Fixes #109 - checks addEventListener instead of attachEvent
### 5.0.8
- Fixes #109 - checks addEventListener instead of attachEvent
### 5.0.7
- Improvement to docs
### 5.0.6
- Fixes #106 - Fix immediate widget initialization
### 5.0.5
- Fixes #102 Fix how require path to marko-widgets is generated in compiled templates
### 5.0.4
- Fixes #101 - Widget binding broken when UI component is npm linked in
### 5.0.3
- Documentation fixes
### 5.0.2
- Fix for #98 - Properly preserve widget stack when beginning async rendering
### 5.0.1
- Fixes #92 - w-preserve* does not work on widget root node
### 5.0.0
- Integrated [morphdom](https://github.com/patrick-steele-idem/morphdom) to more efficiently transform the existing DOM instead of replacing it entirely
- Significant performance improvements
- Code cleanup
- Breaking changes:
- The `onAfterUpdate` lifecycle event was renamed to `onUpdate`
- Reusing DOM nodes might change application behavior (in rare cases)
- The `init()` constructor for a widget is only called _once_ even if a widget is rerendered
- Dust-related code has been removed
- New lifecycle event: 'onRender'
# 4.x
## 4.3.x
### 4.3.0
- Fixed #41 - Initialize widgets in nested batched updates first
- Fixed #54 - Allow event handler method name to be dynamic
## 4.2.x
### 4.2.3
- Internal code improvements
### 4.2.2
- Fixed #49 - Allow `w-id` to be used in an intermediate template
### 4.2.1
- Fixed #48 - Improved logic for generating repeated widget IDs to handle gaps
### 4.2.0
- Fixed #47 - Switched from `optimizer` to [lasso](https://github.com/lasso-js/lasso) for tests
- Fixed #46 - Allow `w-id` attr with `<invoke>`
## 4.1.x
### 4.1.0
- Added support `w-preserve-if` and `w-preserve-body-if`. These new attributes allow DOM elements to be conditionally preserved. The right-hand side of the attribute should a JavaScript expression. If the expression evaluates to `false` then the elements will _not_ be preserved. Example usage:
```xml
<div w-bind>
<!-- Don't rerender the search results if no search results are provided -->
<app-search-results items="data.searchResults"
w-preserve-if="data.searchResults == null"/>
</div>
```
## 4.0.x
### 4.0.1
- Stateful widgets
- Batched updates to the DOM
- New methods exported by marko-widgets:
- `defineComponent(def)`
- `defineWidget(def)`
- `defineRenderer(def)`
- New custom taglib attributes: `w-preserve`, `w-preserve-body` and `w-body`
- New Widget methods:
- `setState(name, value)`
- `setState(newState)`
- `replaceState(newState)`
- `setProps(newProps)`
- `setStateDirty(name)`
- `onBeforeDestroy()`
- `onDestroy()`
- `onBeforeUpdate()`
- `onAfterUpdate()`
- New rendering properties:
- `template` (`String` path to a Marko template)
- `getInitialProps(input)`
- `getInitialState(input)`
- `getTemplateData(state, input)`
- `getWidgetConfig(input)`
- `getInitialBody(input)`
- General code cleanup and performance improvements
- :exclamation: Removed support for `Widget.prototype.render = function(input, out) { ... }`
# 3.x
## 3.0.x
### 3.0.0
- :exclamation: Removed support for `this.widgets.foo` (use `this.getWidget('foo')` instead)
- Added support for repeated DOM elements and repeated widgets:
```html
<div class="my-component" w-bind="./widget">
<ul>
<li w-id="todoListeItems[]" for="todoItem in data.todoItems">
<app-todo-item w-id="todoItems[]" todo-item="todoItem"/>
</li>
</ul>
</div>
```
```javascript
var todoListeItems = this.getEls('todoListeItems');
var todoItemWidgets = this.getWidgets('todoItems');
```
- Added new methods:
- `getEls(id)`
- `getWidgets(id)`
# 2.x
## 2.0.x
### 2.0.12
- Deprecate `w-el-id` in favor of `w-id`
### 2.0.11
- Internal code cleanup
### 2.0.10
- Fixed #19 - Allow `w-on` for custom widget events
### 2.0.9
- Fixed #18 - Widgets in async blocks now initialize in the correct order
### 2.0.8
- Fixed #17 - Allow `w-config` with `w-extend`
### 2.0.7
- Internal: use 'renderBody(out)' instead of `invokeBody()`
### 2.0.6
- Introduced `require('marko-widgets').render(renderer, input)`
### 2.0.5
- Replaced `require('marko-widgets').renderFunc(renderer)` with `require('marko-widgets').renderable(exports, renderer)`
### 2.0.4
- Improved documentation
### 2.0.3
- Added support for using `exports.extendWidget = function(widget, widgetConfig) { ... }`
- Allow `w-extend` attribute to be empty
### 2.0.2
- Allow empty value for `w-bind` (e.g. `<div w-bind>...</div>`) and default to `./widget` or `./`
- Export new method for creating a client-side render function:
```javascript
function renderer(input, out) {
out.write('Hello ' + input.name + '!');
}
exports.render = require('marko-widgets').renderFunc(renderer);
```
### 2.0.1
- Added new sub-module, `require('marko-widgets/dom')`, that exports [raptor-dom](https://github.com/raptorjs/raptor-dom)
Example:
```javascript
require('marko-widgets/dom').removeChildren(document.body);
```
- Added new sub-module, `require('marko-widgets/renderer')`, that exports [raptor-renderer](https://github.com/raptorjs/raptor-renderer)
Example:
```javascript
require('marko-widgets/renderer').renderFunc(renderer);
```
### 2.0.0
- Improved mechanism for registering and loading widget types.
- Use `exports.Widget` instead of `module.exports = Widget`
# 1.x
## 1.5.x
### 1.5.2
- Fixes #14 - Making adding event listeners more robust by allowing initialization even if document is not ready
### 1.5.1
- Changes to prevent unoptimized code in V8
### 1.5.0
- Fixes #11 Added support for w-on* attributes. Other cleanup and tests

35
widgets/README.md Normal file
View File

@ -0,0 +1,35 @@
Marko Widgets
=============
[![Build Status](https://travis-ci.org/marko-js/marko-widgets.svg?branch=master)](https://travis-ci.org/marko-js/marko-widgets)
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/marko-js/marko-widgets)
[![NPM](https://img.shields.io/npm/v/marko-widgets.svg)](https://www.npmjs.com/package/marko-widgets)
[![Downloads](https://img.shields.io/npm/dm/marko-widgets.svg)](http://npm-stat.com/charts.html?package=marko-widgets)
![Marko Logo](https://raw.githubusercontent.com/marko-js/branding/master/marko-logo-small.png)
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. Learn more at [http://markojs.com/docs/marko-widgets/](http://markojs.com/docs/marko-widgets/).
# Changelog
See [CHANGELOG.md](CHANGELOG.md)
# Discuss
Chat channel: [![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/marko-js/marko-widgets?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Questions or comments can also be posted on the [Marko Widgets Github issues](https://github.com/marko-js/marko-widgets/issues) page.
# Maintainers
* [Patrick Steele-Idem](https://github.com/patrick-steele-idem) (Twitter: [@psteeleidem](http://twitter.com/psteeleidem))
* [Phillip Gates-Idem](https://github.com/philidem/) (Twitter: [@philidem](https://twitter.com/philidem))
* [Martin Aberer](https://github.com/tindli) (Twitter: [@metaCoffee](https://twitter.com/metaCoffee))
# Contribute
Pull Requests welcome. Please submit Github issues for any feature enhancements, bugs or documentation problems.
# License
Apache License v2.0

View File

@ -0,0 +1,6 @@
{
"dependencies": [
"require: ./taglib/*.js",
"require: ./taglib/TransformHelper/*.js"
]
}

View File

@ -0,0 +1,7 @@
Additional Resources
====================
# Sample Apps
* UI Components Playground [source](https://github.com/marko-js-samples/ui-components-playground) | [demo](https://ui-components-playground.herokuapp.com/)
* More sample apps: [https://github.com/marko-js-samples](https://github.com/marko-js-samples)

View File

@ -0,0 +1,123 @@
Component Lifecycle
===================
<!--{TOC}-->
Rendering a UI component will cause the top-level UI component to be rendered and all nested UI components to be rendered. The output of rendering will be an HTML string that contains the output of all rendered UI components. When the HTML string is added to the DOM any widgets associated with the rendered UI components will be initialized. Nested widgets will be initialized before their parents.
# Component Rendering
When a widget is rendered the following optional functions are called in the order shown below:
1. `getInitialProps(input)`
2. `getInitialState(input)`
3. `getTemplateData(state, input)`
4. `getInitialBody(input)`
5. `getWidgetConfig(input)`
Each of the rendering methods is described in the sections below.
## Rendering Methods
`this` should _not_ be used in these methods because a widget instance has not yet been created during rendering.
### getInitialProps(input, out)
This optional method is used to normalize the input properties during the rendering of a UI component. If implemented, this method should return the input properties to use based on the provided `input` and `out` arguments.
```javascript
{
getInitialProps: function(input, out) {
return {
name: input.name.toUpperCase()
}
},
...
}
```
### getInitialState(input, out)
This optional method is used to determine the initial state for a newly rendered UI component.
```javascript
{
getInitialState: function(input, out) {
return {
counter: input.counter == null ? 0 : input.counter
}
},
...
}
```
### getTemplateData(state, input, out)
This optional method is used to determine what data will be passed to the Marko template that is used to render the UI component.
### getWidgetConfig(input, out)
This optional method is used to determine is passed to the widget constructor when the widget is initialized in the browser. If the UI component is rendered on the server then the widget config data will be serialized to a JSON-like data structure and stored in a special `data-w-config` attribute in the DOM.
### getInitialBody(input, out)
This optional method is used to determine the nested external content that is to be injected into the body of the UI component (to support transclusion). The actual injection point is determined by the `w-body` attribute.
# Widget Lifecycle
After a UI component's DOM nodes have been added to the DOM a widget instance will be created and bounded to the corresponding DOM node.
## Widget Lifecycle Methods
`this` can be used in these methods as the widget instance. Widget lifecycle methods are optional methods, that if implemented will be invoked by the Marko Widgets runtime in response to widget lifecycle events. For example:
```javascript
module.exports = require('marko-widgets').defineComponent({
// ...
//
init: function() {
console.log('The UI component has been mounted to the DOM.', this.id);
},
onBeforeUpdate: function() {
console.log('The DOM is about to be updated...', this.id);
},
onUpdate: function() {
console.log('The DOM has been updated.', this.id);
},
onDestroy: function() {
console.log('The UI component is being removed from the DOM :(', this.id);
}
});
```
### init(widgetConfig)
The `init(widgetConfig)` constructor method is called once in the browser when the widget is first created and after the widget has been mounted in the DOM. The `init(widgetConfig)` method is only called once for a given widget.
### onBeforeUpdate()
The `onBeforeUpdate()` method is called when a widget's view is about to be updated due to either new properties or a state change.
### onUpdate()
The `onUpdate()` method is called when a widget's view has been updated due to either new properties or a state change. The DOM nodes have been updated accordingly by time this method has been called.
### onRender(event)
Called when the widget has been rendered (or rerendered) and is mounted to the DOM. The `event` argument will be an object. If the event is being fired for the first render then the `event` argument will have the `firstRender` property set to `true`.
### onBeforeDestroy()
The `onBeforeDestroy()` method is called when a widget is about to be destroyed due to it being fromed from the DOM.
### onDestroy()
The `onDestroy()` method is called after a widget has been destroyed and removed from the DOM.
### shouldUpdate(newProps, newState)
The `shouldUpdate(newProps, newState)` method is called when a widget's view is about to be updated. Returning `false` will prevent the widget's view from being updated.

98
widgets/docs/faq.md Normal file
View File

@ -0,0 +1,98 @@
FAQ
===================
<!--{TOC}-->
# What are the difficulties involved in creating „reusable“ widgets when building an app?
As a developer one needs to make the choice between building a "reusable" UI component versus a widget that is tightly coupled with an app - there most certainly is a tradeoff between reusability and simplicity.
A reusable UI widget would need to emit generic events while a tightly coupled widget would directly talk to the app instance to control the app. Therefore, a reusable widget introduces complexity because it then becomes the responsibility of the parent widget to handle the events. A tightly coupled widget, in comparison, avoids the middleman and goes straight to the app.
As the decision is centered around individual situations, an application developer has to decide if a certain component would benefit to be reusable.
# Is there a difference between reuse and preserve in the context of Marko Widgets?
DOM nodes can be preserved during a rerender and widget instances can be reused during a rerender (to have the same widget instance even after a rerender). A preserved DOM node is left completely in its previous state (other than being reinserted into the DOM with a new parent).
A preserved DOM nodes is detached from the DOM and reinserted into the updated DOM in its proper place.
Reusing the same widget instance ensures that any references to the old widget instance will still be correct.
# How far should a UI be "componentized"?
There is no right answer for how far a page should be decomposed into individual UI components. The goal should be for each UI component to be Focused, Independent, Reusable, Small & Testable ([FIRST](http://addyosmani.com/first/)). If you feel like a UI component does not meet these requirements then break it up into smaller UI components.
# When should the body of a custom component be used?
There are multiple ways to express the same thing using HTML. For example, an HTML button can be defined in two different ways:
```html
<input type="button" value="My Button">
<button type="button">
My Button
</button>
```
In the above example, the exact same button is produced, but when using the `<input>` tag the label of the button is provided in the `value` _attribute_ and when using the `<button>` tag, the label of the button is provided in the nested body content. This is important because an HTML attribute does _not_ allow HTML content, while HTML content can be provided in the body of an HTML element. When designing a UI component, if it may be necessary to provide input to a component that includes markup then it should be possible to provide that markup as part of the body content.
# Which component functions are invoked at what time - what's the order of invocation?
Please see [Component Lifecycle](./component-lifecycle.md).
# Marko Widgets supports the batching of DOM updates, but what does this mean to the developer?
DOM updates to widgets are batched to prevent DOM updates from happening after every state change. If a widget's DOM needs to be update due to either `setState()` or `setProps()` the widget will be queued for update with the next batch (a widget will only be queued up once).
For example, given the following code that repeatedly sets the same state property to a new value:
```javascript
this.setState('name', 'Frank');
this.setState('name', 'Jane');
this.setState('name', 'John');
```
The DOM will only be updated once for the widget and it will be based on the final value of the `name` state property.
Marko Widgets starts a batch when handling a bubbling a DOM event that came off of the event loop. That is, the DOM will be updated once after all code has had a chance to respond to the DOM event. If a widget is queued for update and no batch has been started then a new batch will automatically be started and the update will be scheduled using `process.nextTick()`.
# How do widgets communicate?
Every widget is an [EventEmitter](https://nodejs.org/api/events.html#events_class_events_eventemitter) instance and widgets typically communicate by emitting custom events that can then be handled by the direct parent of the widget. The parent widget can choose to handle the event or emit another custom event that bubbles up to its parent. A widget should only communicate directly with nested widgets that it "owns" (i.e., nested widgets that were introduced in the containing widget's template). A widget can get a direct reference to a nested widget using the `this.getWidget(nestedWidgetId)` method (where `nestedWidgetId` is the ID assigned using the `w-id` attribute).
In some situations, it may be helpful to communicate an event on a global pub/sub channel. Pub/sub can be helpful in situations where it would overkill for an event to have to bubble up a complex widget hierarchy in order for it to be handled. The [raptor-pubsub](https://github.com/raptorjs/raptor-pubsub) module provides a very simple pub/sub implementation based on the [EventEmitter](https://nodejs.org/api/events.html#events_class_events_eventemitter) API.
# Why is state so important for Marko Widgets?
The concept of stateful widgets was introduced into Marko Widgets after evaluating some the great ideas introduced with [React](https://facebook.github.io/react/). By making widgets stateful and tracking changes to state, the Marko Widgets runtime can minimize updates to the DOM. Both Marko Widgets and React want to allow developers to create more easily maintainable applications by promoting rerendering over writing code that manually manipulates the DOM. Marko Widgets still allows developers to manually update the DOM if performance is a concern, but that should not be the norm.
A stateful widget's view will only be updated if any of its state properties have been changed. In addition, when rerendering a tree of widgets, only the specific widgets that need to be updated will be updated and the other widgets will continue to be used. While rerendering a widget with nested widgets, if Marko Widgets encounters a previously rendered widget in the DOM with the same ID then the previous widget will be reused to avoid rerendering and entire subtree of widgets. Marko Widgets also has the concept of container components that accept external nested content. If the state of the container component changes, only the outer "shell" will be rerendered and not the nested content.
For performance reasons, only a shallow compare is done when checking if the value of a state property has changed. This means that complex objects that exist in the state should be treated as immutable or, alternatively, Marko Widgets needs to be told when a state property has changed using `setStateDirty(name)`. Both supported solutions are compared below:
___Immutable objects and copy-on-write:___
```javascript
function addColor(newColor) {
// Create a new Array with the new color added:
var newColors = this.state.colors.concat([newColor]);
// Set the colors state with the new Array:
this.setState('colors', newColors);
}
```
___Using setStateDirty:___
```javascript
function addColor(newColor) {
// Modify the existing colors array
this.state.colors.push(newColor);
// Let Marko Widgets know that the colors Array was modified:
this.setStateDirty('colors');
}
```
Unlike React, Marko Widgets does not try to maintain a virtual DOM tree. This allows the Marko Widgets runtime to be much smaller. Marko Widgets makes the assumption that rerendering directly to the DOM is usually fast enough. In the cases where performance is a concern, developers have the option to provide custom state update handlers to manually update the DOM. In addition, Marko Widgets allows developers to mark entire subtrees of the DOM as "preserved" so that portions of the DOM are never rerendered and will continue be used across rerendering.

772
widgets/docs/get-started.md Normal file
View File

@ -0,0 +1,772 @@
Get Started
===========
<!--{TOC}-->
# Installation
```bash
npm install marko-widgets --save
```
# Glossary
A few definitions before you get started:
* A "widget" is the "client-side behavior" of a UI component
* A widget instance has the following characteristics
* All widget instances are bound to a DOM element
* All widgets are [event emitters](http://nodejs.org/api/events.html)
* Client-side behavior includes the following:
* Attaching DOM event listeners (mouse click, keyboard press, etc.)
* Attaching listeners to other widgets
* Manipulating the DOM
* Publishing client-side events
* etc.
# Usage
## Binding Behavior
Using the bindings for Marko, you can bind a widget to a rendered DOM element using the custom `w-bind` attribute as shown in the following sample template:
```xml
<div class="my-component" w-bind="./widget">
<div>Click Me</div>
</div>
```
You can also choose to leave the value of the `w-bind` attribute empty. If the value of `w-bind` is empty then `marko-widgets` will search for a widget module by first checking to see if `widget.js` exists and then `index.js`. Example:
```xml
<div class="my-component" w-bind>
<div>Click Me</div>
</div>
```
The widget bound to the `<div>` should then be implemented as a CommonJS module that exports a widget type as shown in the following JavaScript code:
__src/pages/index/widget.js:__
```javascript
module.exports = require('marko-widgets').defineComponent({
init: function() {
var rootEl = this.el; // this.el returns the root element that the widget is bound to
var self = this;
rootEl.addEventListener('click', function() {
self.addText('You clicked on the root element!');
});
},
addText: function(text) {
this.el.appendChild(document.createTextNode(text));
}
})
```
## Widget Props
When a widget is initially rendered, it is passed in an initial set of properties. For example:
```javascript
require('fancy-checkbox').render({
checked: true,
label: 'Foo'
});
```
If a widget is stateful, then the state should be derived from the input properties and the template data should then be derived from the state. If a widget is not stateful, then the template data should be derived directly from the input properties. If you need to normalize the input properties then you can implement the `getInitialProps(input, out)` method as shown below:
```javascript
module.exports = require('marko-widgets').defineComponent({
template: require('./template.marko'),
getInitialProps: function(input, out) {
return {
size: input.size ? input.size.toLowerCase() : 'normal'
};
},
getTemplateData: function(state, input) {
// input will be the value returned by getInitialProps()
// ...
}
// ...
});
```
## Widget Template
Every widget should have an associated Marko template that will be used to render the widget. A widget is associated with a template using the `template` property as shown below:
```javascript
module.exports = require('marko-widgets').defineComponent({
template: require('./template.marko'),
getTemplateData: function(state, input, out) {
return {
name: input.name
};
},
...
});
```
The `getTemplateData(state, input, out)` method is used to build the view model that gets passed to the template based on the state and/or input. If a widget is stateful then the template data should be derived only from the `state`. If a widget is stateless then the template data should be derived only from the `input`. If a stateful widget is being re-rendered then the `input` argument will always be `null`. For a stateless widget, the `state` argument will be `null`.
## Widget State
A stateful widget will maintain state as part of the widget that instance. If the state of the widget changes then the widget will be queued to be updated in the next batch. The initial state should be provided using the `getInitialState(input)` method. All state changes should go through the `setState(name, value)` or `setState(newState)` methods. For example:
```javascript
module.exports = require('marko-widgets').defineComponent({
template: require('./template.marko'),
getInitialState: function(input, out) {
return {
name: input.name,
selected: input.selected || false;
}
},
getTemplateData: function(state, input) {
return {
name: state.name,
color: state.selected ? 'yellow' : 'transparent'
};
},
handleClick: function() {
this.setState('selected', true);
},
isSelected: function() {
return this.state.selected;
}
});
```
The current state of the widget can always be read using the `this.state` property. For example:
```javascript
var isSelected = this.state.selected === true;
```
When state is modified using either the `setState(name, value)` or `setState(newState)` method, only a shallow compare is done to see if the state has changed. Therefore, if a complex object is part of the state then it should be treated as immutable.
## Widget Config
Arbitrary widget configuration data determined at render time can be provided to the constructor of a widget by implementing the `getWidgetConfig(input, out)` method as shown below:
```javascript
module.exports = require('marko-widgets').defineComponent({
template: require('./template.marko'),
getWidgetConfig: function(input, out) {
return {
foo: 'bar'
}
},
init: function(widgetConfig) {
var foo = widgetConfig.foo; // foo === 'bar'
},
...
});
```
## Referencing Nested Widgets
The `marko-widgets` taglib also provides support for allowing a widget to communicate directly with nested widgets. A nested widget can be assigned a widget ID (only needs to be unique within the scope of the containing widget) and the containing widget can then reference the nested widget by the assigned widget ID using the `this.getWidget(id)` method.
The following HTML template fragment contains a widget that has three nested [sample-button](https://github.com/marko-js-samples/marko-sample-components/tree/master/components/sample-button) widgets. Each nested [sample-button](https://github.com/marko-js-samples/marko-sample-components/tree/master/components/sample-button) is assigned an ID (i.e. `primaryButton`, `successButton` and `dangerButton`).
```xml
<div class="my-component" w-bind="./widget">
<div class="btn-group">
<sample-button label="Click Me" variant="primary" w-id="primaryButton"/>
<sample-button label="Click Me" variant="success" w-id="successButton"/>
<sample-button label="Click Me" variant="danger" w-id="dangerButton"/>
</div>
...
</div>
```
The containing widget can then reference a particular nested widget as shown in the following sample JavaScript code:
```javascript
this.getWidget('dangerButton').on('click', function() {
alert('You clicked on the danger button!');
});
```
Marko Widgets also supports referencing _repeated_ nested widgets as shown below:
```xml
<div class="my-component" w-bind="./widget">
<ul>
<li for="todoItem in data.todoItems">
<app-todo-item w-id="todoItems[]" todo-item="todoItem"/>
</li>
</ul>
</div>
```
The containing widget can then reference the repeated todo item widgets using the `this.getWidgets(id)` method as shown below:
```javascript
var todoItemWidgets = this.getWidgets('todoItems');
// todoItemWidgets will be an Array of todo item widgets
```
To try out and experiment with this code please see the documentation and source code for the [widget-communication](https://github.com/marko-js-samples/widget-communication) sample app.
## Referencing Nested DOM Elements
DOM elements nested within a widget can be given unique IDs based on the containing widget's ID. These DOM elements can then be efficiently looked up by the containing widget using methods provided. The `w-id` custom attribute can be used to assign DOM element IDs to HTML elements that are prefixed with the widget's ID. For example, given the following HTML template fragment:
```xml
<form w-bind="./widget">
...
<button type="submit" w-id="submitButton">Submit</button>
<button type="button" w-id="cancelButton">Cancel</button>
</form>
```
Assuming the unique ID assigned to the widget is `w123`, the following would be the HTML output:
```xml
<form id="w123">
...
<button type="submit" id="w123-submitButton">Submit</button>
<button type="button" id="w123-cancelButton">Cancel</button>
</form>
```
Finally, to reference a widget's nested DOM element's the following code can be used in the containing widget:
```javascript
var submitButton = this.getEl('submitButton'); // submitButton.id === 'w123-submitButton'
var cancelButton = this.getEl('cancelButton'); // cancelButton.id === 'w123-cancelButton'
submitButton.style.border = '1px solid red';
```
The object returned by `this.getEl(id)` will be a raw [HTML element](https://developer.mozilla.org/en-US/docs/Web/API/element). If you want a jQuery wrapped element you can do either of the following:
Option 1) Use jQuery directly:
```javascript
var $submitButton = $(this.getEl('submitButton'));
```
Option 2) Use the `this.$()` method:
```javascript
var $submitButton = this.$('#submitButton');
```
Marko Widgets also supports referencing _repeated_ nested DOM elements as shown below:
```xml
<ul>
<li for="color in ['red', 'green', 'blue']"
w-id="colorListItems[]">
$color
</li>
</ul>
```
The containing widget can then reference the repeated DOM elements using the `this.getEls(id)` method as shown below:
```javascript
var colorListItems = this.getEls('colorListItems');
// colorListItems will be an Array of raw DOM <li> elements
```
## Adding Event Listeners
Marko Widgets supports attaching event listeners to nested DOM elements and nested widgets. Event listeners can either be registered declaratively in the Marko template or in JavaScript code.
### Adding DOM Event Listeners
A widget can subscribe to events on a nested DOM element.
Listeners can be attached declaratively as shown in the following sample code:
```xml
<div w-bind>
<form w-onsubmit="handleFormSubmit">
<input type="text" value="email" w-onchange="handleEmailChange">
<button>Submit</button>
</form>
</div>
```
And then in the widget:
```javascript
module.exports = require('marko-widgets').defineComponent({
// ...
handleFormSubmit: function(event, el) {
event.preventDefault();
// ...
},
handleEmailChange: function(event, el) {
var email = el.value;
this.validateEmail(email);
// ...
},
validateEmail: function(email) {
// ...
}
});
```
NOTE: Event handler methods will be invoked with `this` being the widget instance and the following two arguments will be provided to the handler method:
1. `event` - The raw DOM event object (e.g. `event.target`, `event.clientX`, etc.)
2. `el` - The element that the listener was attached to (which can be different from `event.target` due to bubbling)
For performance reasons, Marko Widgets only adds one event listener to the root `document.body` element for each event type that bubbles. When Marko Widgets captures an event on `document.body` it will internally delegate the event to the appropriate widgets. For DOM events that do not bubble, Marko Widgets will automatically add DOM event listeners to each of the DOM nodes. If a widget is destroyed, Marko Widgets will automatically do the appropriate cleanup to remove DOM event listeners.
You can also choose to add listeners in JavaScript code by assigning an "element id" to the nested DOM element (only needs to be unique within the scope of the containing widget) so that the nested DOM element can be referenced by the containing widget. The scoped widget element ID should be assigned using the `w-id="<id>"` attribute. For example, in the template:
```xml
<div w-bind>
<form w-id="form">
<input type="text" value="email" w-id="email">
<button>Submit</button>
</form>
</div>
```
And then in the widget:
```javascript
module.exports = require('marko-widgets').defineComponent({
// ...
init: function() {
var self = this;
var formEl = this.getEl('form');
formEl.addEventListener('submit', function(event) {
self.handleFormSubmit(event, formEl)
});
// Or use jQuery if that is loaded on your page:
var emailEl = this.getEl('email');
$(emailEl).on('change', function(event) {
self.handleEmailChange(event, emailEl)
});
},
handleFormSubmit: function(event, el) {
event.preventDefault();
// ...
},
handleEmailChange: function(event, el) {
var email = el.value;
this.validateEmail(email);
// ...
},
validateEmail: function(email) {
// ...
}
});
```
### Adding Custom Event Listeners
A widget can subscribe to events on nested widgets. Every widget extends [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter) and this allows each widget to emit events.
Listeners can be attached declaratively as shown in the following sample code:
```xml
<div w-bind="./widget">
<app-overlay title="My Overlay"
w-onBeforeHide="handleOverlayBeforeHide">
Content for overlay
</app-overlay>
</div>
```
And then in the widget:
```javascript
module.exports = require('marko-widgets').defineComponent({
// ...
handleOverlayBeforeHide: function(event) {
console.log('The overlay is about to be hidden!');
}
});
```
You can also choose to add listeners in JavaScript code by assigning an "id" to the nested widget (only needs to be unique within the scope of the containing widget) so that the nested widget can be referenced by the containing widget. The scoped widget ID should be assigned using the `w-id="<id>"` attribute. For example, in the template:
```xml
<div w-bind="./widget">
<app-overlay title="My Overlay"
w-id="myOverlay">
Content for overlay
</app-overlay>
</div>
```
And then in the widget:
```javascript
module.exports = require('marko-widgets').defineComponent({
// ...
init: function() {
var self = this;
var myOverlay = this.getWidget('myOverlay');
this.subscribeTo(myOverlay)
.on('beforeHide', function(event) {
self.handleOverlayBeforeHide(event);
});
},
handleOverlayBeforeHide: function(event) {
console.log('The overlay is about to be hidden!');
}
});
```
NOTE: `subscribeTo(eventEmitter)` is used to ensure proper cleanup if the subscribing widget is destroyed.
## Lifecycle Methods
### Rendering Methods
`this` should _not_ be used in these methods because a widget instance has not yet been created during rendering.
#### getInitialProps(input, out)
This optional method is used to normalize the input properties during the rendering of a UI component. If implemented, this method should return the input properties to use based on the provided `input` and `out` arguments.
```javascript
{
getInitialProps: function(input, out) {
return {
name: input.name.toUpperCase()
}
},
...
}
```
#### getInitialState(input, out)
This optional method is used to determine the initial state for a newly rendered UI component.
```javascript
{
getInitialState: function(input, out) {
return {
counter: input.counter == null ? 0 : input.counter
}
},
...
}
```
#### getTemplateData(state, input, out)
This optional method is used to determine what data will be passed to the Marko template that is used to render the UI component.
#### getWidgetConfig(input, out)
This optional method is used to determine is passed to the widget constructor when the widget is initialized in the browser. If the UI component is rendered on the server then the widget config data will be serialized to a JSON-like data structure and stored in a special `data-w-config` attribute in the DOM.
#### getInitialBody(input, out)
This optional method is used to determine the nested external content that is to be injected into the body of the UI component (to support transclusion). The actual injection point is determined by the `w-body` attribute.
### Widget Methods
`this` can be used in these methods as the widget instance.
#### init(widgetConfig)
The `init(widgetConfig)` constructor method is called once in the browser when the widget is first created and after the widget has been mounted in the DOM. The `init(widgetConfig)` method is only called once for a given widget.
#### onBeforeUpdate()
The `onBeforeUpdate()` method is called when a widget's view is about to be updated due to either new properties or a state change.
#### onUpdate()
The `onUpdate()` method is called when a widget's view has been updated due to either new properties or a state change. The DOM nodes have been updated accordingly by time this method has been called.
#### onBeforeDestroy()
The `onBeforeDestroy()` method is called when a widget is about to be destroyed due to it being fromed from the DOM.
#### onDestroy()
The `onDestroy()` method is called after a widget has been destroyed and removed from the DOM.
#### shouldUpdate(newProps, newState)
The `shouldUpdate(newProps, newState)` method is called when a widget's view is about to be updated. Returning `false` will prevent the widget's view from being updated.
## Client-side Rendering
Every widget defined using `defineComponent(...)` exports a `render(input)` method that can be used to render the widget in the browser as shown below:
```javascript
var widget = require('fancy-checkbox').render({
checked: true,
label: 'Foo'
})
.appendTo(document.body)
.getWidget();
widget.setChecked(false);
widget.setLabel('Bar');
```
The `appendTo(targetEl)` method is only one of the methods that can be used to insert the widget into the DOM. All of the methods are listed below:
- `appendTo(targetEl)`
- `insertAfter(targetEl)`
- `insertBefore(targetEl)`
- `prependTo(targetEl)`
- `replace(targetEl)`
## Server-side Rendering
In order for everything to work on the client-side we need to include the code for the `marko-widgets` module and the `./widget.js` module as part of the client bundle and we also need to use the custom `<init-widgets>` tag to let the client know which widgets rendered on the server need to be initialized on the client. To include the client-side dependencies will be using the [lasso](https://github.com/lasso-js/lasso) module and the taglib that it provides. Our final page template is shown below:
__src/pages/index/template.marko:__
```xml
<lasso-page name="index" package-path="./browser.json" />
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Marko Widgets: Bind</title>
<lasso-head/>
</head>
<body>
<div>Marko Widgets: Bind</div>
<div class="my-component" w-bind="./widget">
<div>Click Me</div>
</div>
<lasso-body/>
<init-widgets/>
</body>
</html>
```
The `browser.json` that includes the required client-side code is shown below:
__src/pages/index/browser.json:__
```javascript
{
"dependencies": [
"require: marko-widgets",
"require: ./widget"
]
}
```
In the above example, the final HTML will be similar to the following:
```xml
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Widgets Demo</title>
</head>
<body>
<div>Marko Widgets: Bind</div>
<div class="my-component" id="w0" data-widget="/src/pages/index/widget">
<div>Click Me</div>
</div>
<script src="static/index-8947595a.js" type="text/javascript"></script>
<span style="display:none;" data-ids="w0" id="rwidgets"></span>
</body>
</html>
```
To try out and experiment with this code please see the documentation and source code for the [widget-bind](https://github.com/marko-js-samples/widget-bind) sample app.
### Manually Initializing Server-side Rendered Widgets
It's also possible to manually initialize rendered widgets as shown in the following code:
```javascript
var markoWidgets = require('marko-widgets');
var template = require('./template.marko');
module.exports = function(req, res) {
template.render(viewModel, function(err, html, out) {
var renderedWidgets = markoWidgets.getRenderedWidgets(out);
// Serialize the HTML and the widget IDs to the browser
res.json({
html: html,
renderedWidgets: renderedWidgets
});
});
}
```
And then, in the browser, the following code can be used to initialize the widgets:
```javascript
var result = JSON.parse(response.body);
var html = result.html
var renderedWidgets = result.renderedWidgets;
document.body.innerHTML = html; // Add the HTML to the DOM
// Initialize the widgets to bind behavior!
require('marko-widgets').initWidgets(renderedWidgets);
```
NOTE: the server side example above renders the template directly and therefore circumvents the
`index.js` file (neither `getInitialState()` nor `getTemplateData()` are executed).
To render the complete widget, use the code below instead
(the browser side is not affected; the same code snipped can be used):
```javascript
var markoWidgets = require('marko-widgets');
var helloComponent = require('src/components/app-hello');
module.exports = function(req, res) {
var renderResult = helloComponent.render(viewModel);
var renderedWidgets = markoWidgets.getRenderedWidgets(renderResult.out);
// Serialize the HTML and the widget IDs to the browser
res.json({
html: renderResult.html,
renderedWidgets: renderedWidgets
});
}
```
## Split Renderer and Widget
For UI components that will only be rendered on the server it may be desirable to split the renderer (i.e. rendering logic and template) from the client-side behavior (i.e. widget). This can be done by using `defineRenderer(def)` and `defineWidget(def)` instead of `defineComponent(def)`. An example of a combined and split UI component is shown below.
### Combined Renderer and Widget
```
src/components/app-hello/
├── index.js
└── template.marko
```
___src/components/app-hello/template.marko:___
```xml
<div w-bind
w-on-click="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.el.style.backgroundColor = 'yellow';
}
});
```
### Split Renderer and Widget
```
src/components/app-hello/
├── index.js
├── renderer.js
├── template.marko
└── widget.js
```
___src/components/app-hello/template.marko:___
```xml
<div w-bind="./widget"
w-on-click="handleClick">
Hello ${data.name}!
</div>
```
___src/components/app-hello/renderer.js:___
```javascript
module.exports = require('marko-widgets').defineRenderer({
template: require('./template.marko'),
getTemplateData: function(state, input) {
return {
name: input.name
};
}
});
```
___src/components/app-hello/widget.js:___
```javascript
module.exports = require('marko-widgets').defineWidget({
handleClick: function() {
this.el.style.backgroundColor = 'yellow';
}
});
```
___src/components/app-hello/index.js:___
```javascript
exports.render = require('./renderer').render;
```

View File

@ -0,0 +1,350 @@
JavaScript API
==============
# marko-widgets exports
## defineComponent(def)
Used to define a UI component that includes both the renderer *and* the widget (i.e., the client-side behavior). If a UI component is to only be rendered on the server then you might benefit from defining the renderer independently of the widget using the `defineRenderer(def)` and `defineWidget(def)` functions, respectively.
The return value of `defineComponent(def)` will be a `Widget` constructor function with static `renderer(input, out)` and `render(input)` methods.
Example usage for defining a stateless UI component:
```javascript
module.exports = require('marko-widgets').defineComponent({
template: require('./template.marko'),
getTemplateData: function(state, input) {
return {
name: input.name
};
},
handleClick: function() {
this.el.style.backgroundColor = 'yellow';
}
});
```
## defineRenderer(def)
The `defineRenderer(def)` function can be used to define a UI component renderer independently from an associated widget. This can be beneficial when a UI component needs to only be rendered on the server and it is desirable to avoid sending down the template and rendering logic to the browser. For UI components that are only rendered on the server, only the client-side behavior really needs to be be sent to the browser.
The return value of `defineRenderer(def)` will be a `renderer(input, out)` function with a static `render(input)` method.
## defineWidget(def)
The `defineWidget(def)` function can be used to define a UI component's client-side behavior independent of the code to render the UI component. This can be beneficial when a UI component needs to only be rendered on the server and it is desirable to avoid sending down the template and rendering logic to the browser. For UI components that are only rendered on the server, only the client-side behavior really needs to be be sent to the browser.
The return value of `defineWidget(def)` will be a widget constructor function that is used to instantiate new widget instances.
## getRenderedWidgets(out)
_NOTE: Available server-side only_
Used to support initializing widgets bound to to UI components rendered on the server and sent down to the browser via an AJAX request:
```javascript
var markoWidgets = require('marko-widgets');
var template = require('./template.marko');
module.exports = function(req, res) {
template.render(viewModel, function(err, html, out) {
var renderedWidgets = markoWidgets.getRenderedWidgets(out);
// Serialize the HTML and the widget IDs to the browser
res.json({
html: html,
renderedWidgets: renderedWidgets
});
});
}
```
And then, in the browser, the following code can be used to initialize the widgets:
```javascript
var result = JSON.parse(response.body);
var html = result.html
var renderedWidgets = result.renderedWidgets;
document.body.innerHTML = html; // Add the HTML to the DOM
// Initialize the widgets to bind behavior!
require('marko-widgets').initWidgets(renderedWidgets);
```
## getWidgetForEl(el)
The `getWidgetForEl(el)` function can be used to retrieve a widget object outside of its nested context.
```javascript
var myToggle = require('marko-widgets').getWidgetForEl('w0-myToggle');
myToggle.setSelected(true);
```
It is also possible to get a widget handle using the widget el:
```javascript
var el = document.getElementById('w0-myToggle');
var myToggle = require('marko-widgets').getWidgetForEl(el);
myToggle.setSelected(true);
```
# Widget
## Methods
### $(querySelector)
This is a convenience method for accessing a widget's DOM elements when jQuery is available. This mixin method serves as a proxy to jQuery to ease building queries based on widget element IDs.
Internally, this jQuery proxy method will resolve widget element IDs to their actual DOM element ID by prefixing widget element IDs with the widget ID. For example, where this is a widget with an ID of `w123`:
```javascript
this.$() ➡ $("#w123")
this.$("#myEl") ➡ $("#w123-myEl")
```
The usage of this mixin method is described below:
__`$()`__
Convenience usage to access the root widget DOM element wrapped as a jQuery object. All of the following are equivalent:
```javascript
this.$()
$(this.el)
$("#" + this.id)
```
__`$('#<widget-el-id>')`__
Convenience usage to access a nested widget DOM element wrapped as a jQuery object. All of the following are equivalent:
```javascript
this.$("#myEl")
$(this.getEl("myEl"))
$("#" + this.getElId("myEl"))
```
__`$('<selector>')`__
Convenience usage to query nested DOM elements scoped to the root widget DOM element. All of the following are equivalent:
```javascript
this.$("ul > li")
$("ul > li", this.el)
$("#" + this.id + " ul > li")
```
__`$('<selector>', '<widget-el-id>')`__
Convenience usage to query nested DOM elements scoped to a nested widget DOM element. All of the following are equivalent:
```javascript
this.$("li.color", "colorsUL")
this.$("#colorsUL li.color")
$("li.color", this.getEl("colorsUL"))
$("#" + this.getElId("colorsUL") + " li.color")
```
__`$('#<widget-el-id> <selector>')`__
Convenience usage to query nested DOM elements scoped to a nested widget DOM element. All of the following are equivalent:
```javascript
this.$("#colorsUL li.color")
this.$("li.color", "colorsUL")
$("li.color", this.getEl("colorsUL"))
$("#" + this.getElId("colorsUL") + " li.color")
```
__`$(callbackFunction)`__
Convenience usage to add a listener for the "on DOM ready" event and have the this object for the provided callback function be the current widget instance. All of the following are equivalent:
```javascript
this.$(function() { /*...*/ });
$(function() { /*...*/ }.bind(this)); // Using Function.prototype.bind
$($.proxy(function() { /*...*/ }, this));
```
### addEventListener(eventType, listener)
### appendTo(targetEl)
Moves the widget's root DOM node from the current parent element to a new parent element. For example:
```javascript
this.appendTo(document.body);
```
### destroy()
Destroys the widget by unsubscribing from all listeners made using the `subscribeTo` method and then detaching the widget's root element from the DOM. All nested widgets (discovered by querying the DOM) are also destroyed.
Destroy takes 2 optional parameters:
```javascript
widget.destroy({
removeNode: true, //true by default
recursive: true //true by default
})
```
Setting `removeNode` parameter to `false` will keep the widget on the DOM while still unsubscribing all events from it.
Setting `recursive` to `false` will prevent children widgets from being destroyed.
### detach()
Detaches the widget's root element from the DOM by removing the node from its parent node.
### doUpdate(stateChanges, oldState)
### emit(eventType, arg1, arg2, ...)
Emits an event. This method is inherited from EventEmitter (see [Node.js Events: EventsEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter)
### getBodyEl()
### getEl(widgetElId)
Returns a nested DOM element by prefixing the provided `widgetElId` with the widget's ID. For Marko, nested DOM elements should be assigned an ID using the `w-id` custom attribute. Returns `this.el` if no `widgetElId` is provided.
### getEls(id)
Returns an Array of _repeated_ `DOM` elements for the given ID. Repeated DOM elements must have a value for the `w-id` attribute that ends with `[]` (e.g., `w-id="myDivs[]"`)
### getElId(widgetElId)
Similar to `getEl`, but only returns the String ID of the nested DOM element instead of the actual DOM element.
### getWidget(id[, index])
Returns a reference to a nested `Widget` for the given ID. If an `index` is provided and the target widget is a repeated widget (e.g. `w-id="myWidget[]"`) then the widget at the given index will be returned.
### getWidgets(id)
Returns an Array of _repeated_ `Widget` instances for the given ID. Repeated widgets must have a value for the `w-id` attribute that ends with `[]` (e.g., `w-id="myWidget[]"`)
### insertAfter(targetEl)
### insertBefore(targetEl)
### isDestroyed()
### isDirty()
### on(eventType, listener)
### prependTo(targetEl)
### ready(callback)
### replace(targetEl)
### replaceChildrenOf(targetEl)
### replaceState(newState)
Replaces the state with an entirely new state. If any of the state properties changed, the widget's view will automatically be updated.
Important to know:
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. State or template data values that are derived from state properties that are not part of the new state, are `undefined`. Thus, if `replaceState()` is used, one must consider possible side effects if the new state contains less or other properties than the replaced state.
### rerender(data)
Rerenders the widget using its `renderer` and either supplied `data` or internal `state`.
### setState(name, value)
Used to change the value of a single state property. For example:
```javascript
this.setState('disabled', true);
```
Be aware, that `setState()` only nominates the component for a possible rerender. Thus, the component is only rerendered, if at least one of the component state properties changed (`oldValue !== newValue`). If none of the properties changed (because identical or not detected by a shallow comparision), invoking `setState()` is a no operation. (great for performance).
Nice to know:
Compared to `setState()`, `setStateDirty()` does not nominate a component for rerendering but instead always rerenderes the component independently from its state property values (even if they did not change).
### setState(newState)
Used to change the value of multiple state properties. For example:
```javascript
this.setState({
disabled: true,
size: 'large'
});
```
### setStateDirty(name, value)
Force a state property to be changed 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.
Additional information:
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 as dirty. 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.
It is important to know, that the given parameters do not affect how or if `setStateDirty()` rerenderes a component; they are only considered as additional information to update handlers.
Example:
```javascript
// Add a new item to an array without going through `this.setState(...)` - because this
// does not create a new array, the change would not be detected by a shallow property comparison
this.state.colors.push('red');
// Force that particular state property to be considered dirty so
// that it will trigger the widget's view to be updated
this.setStateDirty('colors');
```
### setProps(newProps)
For stateless widgets, setting a widgets properties will result in the widget being re-rendered using the new input. For stateful widgets, setting a widgets properties will result in `getInitialState(newProps)` being called again to determine the new state and the widget state will be updated to use the new state.
### subscribeTo(targetEventEmitter)
### update()
Force the DOM to update immediately, rather than following the normal queued update mechanism for rendering.
```js
this.setState('foo', 'bar');
this.update(); // Force the DOM to update
this.setState('hello', 'world');
this.update(); // Force the DOM to update
```
## Properties
### this.el
The root [HTML element](https://developer.mozilla.org/en-US/docs/Web/API/element) that the widget is bound to.
### this.id
The String ID of the root [HTML element](https://developer.mozilla.org/en-US/docs/Web/API/element) that the widget is bound to.
### this.state
The current state for the widget. For example:
```javascript
module.exports = require('marko-widgets').defineComponent({
template: require('./template.marko'),
getInitialState: function(input) {
return {
disabled: false
}
},
init: function() {
console.log(this.state.disabled); // Output: false
this.setState('disabled', true);
console.log(this.state.disabled); // Output: true
}
});
```

412
widgets/docs/overview.md Normal file
View File

@ -0,0 +1,412 @@
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
w-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
w-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
w-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"
w-id="overlay"
w-onBeforeHide="handleOverlayBeforeHide">
Body content for overlay.
</app-overlay>
<button type="button"
w-onClick="handleShowButtonClick">
Show Overlay
</button>
<button type="button"
w-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 `w-preserve`, `w-preserve-if(<condition>)`, `w-preserve-body` or `w-preserve-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 w-preserve>
<p>
The root span and all its children will never
be re-rendered.
</p>
<p>
Rendered at ${Date.now()}.
</p>
</span>
<div w-preserve-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"
w-preserve-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>
```

289
widgets/docs/taglib-api.md Normal file
View File

@ -0,0 +1,289 @@
Marko Widgets Taglib
====================
# Custom attributes
## w-bind
This attribute is used to bind a widget to a DOM element.
### Examples
Bind to a JavaScript module named `./widget.js` that exports the widget definition:
```xml
<div w-bind="./widget">...</div>
```
Bind to a JavaScript module named `./widget.js` or `./index.js` (searched for in that order) that exports the widget definition:
```xml
<div w-bind>...</div>
```
## w-id
Used to assign a _scoped_ ID to a nested widget or a nested DOM element. The ID will be a concatenation of the parent widget ID with the provided value of the `w-id`.
### Examples
#### Using `w-id` with an HTML element
```xml
<div w-bind="./widget">
<button w-id="myButton" type="button">My Button</button>
</div>
```
This will produce output code similar to the following:
```html
<div>
<button id="w0-myButton" type="button">My Button</button>
</div>
```
The containing widget can reference the nested DOM element using the following code:
```javascript
var myButton = this.getEl('myButton');
```
#### Using `w-id` with a nested widget
```xml
<div w-bind="./widget">
<app-button w-id="myButton" label="My Button" />
</div>
```
This will produce output code similar to the following:
```html
<div>
<button id="w0-myButton" type="button">My Button</button>
</div>
```
The containing widget can reference the nested widget using the following code:
```javascript
var myButton = this.getWidget('myButton');
```
## w-on*
The `w-on*` can be used to declaratively bind event listeners to a DOM element or widget.
NOTE: For DOM events that bubble, efficient DOM event delegation will automatically be used to avoid attaching direct event listeners for performance reasons.
### Examples
#### Using `w-on*` with a nested HTML element
```xml
<div w-bind="./widget">
<button w-onclick="handleMyButtonClick" type="button">My Button</button>
</div>
```
When the button HTML element is clicked, the `handleMyButtonClick` method of the widget will be invoked:
```javascript
module.exports = require('marko-widgets').defineComponent({
// ...
handleMyButtonClick: function(event, el) {
// event will be the native DOM event
// el will be the native DOM element
}
})
```
The containing widget can reference the nested DOM element using the following code:
```javascript
var myButton = this.getEl('myButton');
```
#### Using `w-on*` with a nested widget
```xml
<div w-bind="./widget">
<app-button w-onSomeCustomEvent="handleSomeCustomEvent" label="My Button" />
</div>
```
For the example above it is assumed that the nested widget will emit the custom event using code similar to the following:
```javascript
this.emit('handleSomeCustomEvent', { foo: bar });
```
<a name="w-preserve"></a>
## w-preserve
Preserves the DOM subtree associated with the DOM element or widget such that it won't be modified or rerendered when rerendering the UI component.
Example:
```xml
<div>
<table w-preserve> <!-- Don't ever rerender this table -->
...
</table>
</div>
```
```xml
<div>
<app-map w-preserve/> <!-- Don't ever rerender this UI component -->
</div>
```
## w-preserve-if
Similar to [w-preserve](#w-preserve) except that the DOM subtree is conditionally preserved:
```xml
<div>
<table w-preserve-if(data.tableData == null)>
...
</table>
</div>
```
## w-preserve-body
Similar to [w-preserve](#w-preserve) except that only the child DOM nodes are preserved:
```xml
<div w-preserve-body> <!-- Don't ever rerender any nested DOM elements -->
...
</div>
```
## w-preserve-body-if
Similar to [w-preserve-if](#w-preserve) except that only the child DOM nodes are preserved:
```xml
<div>
<table w-preserve-if(data.tableData == null)>
...
</table>
</div>
```
## w-preserve-attrs
This custom attribute is used to prevent select DOM elements from being modified during a rerender:
```xml
<div w-preserve-attrs="class,style">
...
</div>
```
#### w-for
The `w-for` attribute is used to render a `for` attribute that references a scoped widget element:
```xml
<form>
<label w-for="yes">Yes</label>
<input type="radio" w-id="yes" value="yes">
<label w-for="no">No</label>
<input type="radio" w-id="no" value="no">
</form>
```
This will produce code similar to the following:
```html
<form>
<label for="w0-yes">Yes</label>
<input type="radio" w-id="w0-yes" value="yes">
<label for="w0-no">No</label>
<input type="radio" id="w0-no" value="no">
</form>
```
# Custom tags
## `<init-widgets>`
Generates the necessary code to initialize widgets associated with UI components rendered on the _server_.
Supported attributes:
- __`immediate`__ - If true then a `<script>` tag will be generated that _immediately_ initializes all widgets instead of waiting for the "dom ready" event. For async fragments, a `<script>` will be inserted at the end of each async fragment.
### Examples:
### Non-immediate widget initialization
If the `immediate` attribute is not provided or set to `false` then widgets will initialize during the "dom ready" event. For example, given the following Marko code:
```xml
<init-widgets/>
```
This will produce output HTML code similar to the following:
```html
<noscript id="markoWidgets" data-ids="w0,w1,w2"></noscript>
```
The `<noscript>` HTML tag is simply a container to keep an "index" of all of the IDs associated with UI components that have a widget that needs to be initialized. When the `marko-widgets` module initializes in the browser it will query for the `#markoWidgets` to discover all of the widget IDs.
### Immediate widget initialization
If the `immediate` attribute is provided or set to `true` then widgets will initialize via inline JavaScript code added to the output HTML. For example, given the following Marko code:
```xml
<init-widgets immediate/>
```
This will produce output HTML code similar to the following:
```html
<script>
/* REMOVED: serialized widget state and config */
$markoWidgets("w0,w1,w2")
</script>
```
When immediate widget initialization is enabled, widgets will be initialized before the DOM ready event. In addition, inline widget initialization code will be appended to each async fragment.
## `<widget-types>`
Used to conditionally bind a widget:
```xml
<widget-types default="./widget" mobile="./widget-mobile"/>
<div w-bind=(data.isMobile ? 'default' : 'mobile')>
...
</div>
```
The `<widget-types>` can also be used to disabling binding of a widget:
```xml
<widget-types default="./"/>
<div w-bind=(data.includeWidget ? 'default' : null)>
</div>
```

View File

@ -0,0 +1,179 @@
Upgrade Guide
=============
## v4 to v5
### Breaking changes
- The `onAfterUpdate` lifecycle event was renamed to `onUpdate`
- The `init()` constructor for a widget is only called _once_ even if a widget is rerendered
- When a nested widget is rerendered, it is no longer reinitialized.
- Everything related to Dust has been removed.
### New features
- Integrated [morphdom](https://github.com/patrick-steele-idem/morphdom) to more efficiently transform the existing DOM instead of replacing it entirely
- Significant performance improvements
- Code cleanup
- Stable IDs for widgets that are not assigned a `w-id`
- New lifecycle event: 'onRender'
## v3 to v4
### Breaking changes
- `Widget.prototype.render = function(input, out) { ... }` is no longer allowed. Use `Widget.prototype.renderer = function(input, out) { ... }` or `defineComponent(...)` instead (recommended).
- NOTE: `this.widgets.foo` has been removed since `marko-widgets@^3`
### New features
- Simpler syntax for defining widget types:
___Old:___
```javascript
function Widget() {
this.doSomething();
}
Widget.prototype = {
doSomething: function() {
}
};
module.exports = Widget;
```
___New:___
```javascript
module.exports = require('marko-widgets').defineWidget({
init: function() {
this.doSomething();
},
doSomething: function() {
}
});
```
- Ability to define the _widget_ and the _renderer_ in the same JavaScript file:
```javascript
module.exports = require('marko-widgets').defineComponent({
template: require.resolve('./template.marko'),
getTemplateData: function(state, input) {
return {
hello: 'world'
};
},
init: function() {
this.doSomething();
},
doSomething: function() {
}
});
```
- Support for stateful widgets (changing widget state causes a widget to be rerendered)
```javascript
module.exports = require('marko-widgets').defineComponent({
template: require.resolve('./template.marko'),
getInitialState: function(input) {
return {
size: input.size || 'normal'
};
},
getInitialBody: function(input) {
return input.label || input.renderBody;
},
getTemplateData: function(state, input) {
var rootAttrs = {};
var classParts = ['app-button'];
var type = 'button';
var size = state.size;
if (size !== 'normal') {
classParts.push('app-button-' + size);
}
rootAttrs['class'] = classParts.join(' ');
return {
type: type,
rootAttrs: rootAttrs
};
},
setSize: function(size) {
this.setState('size', size);
},
getSize: function() {
return this.state.size;
}
});
```
## v2 to v3
With marko-widgets, `this.widgets.foo` is no longer support and `this.widgets` will be `null`. Instead, you should `this.getWidget('foo')`.
## v1 to v2
marko-widgets v2 introduced the potential for a circular dependency. To avoid problems, you should no longer use `module.exports` in your widget JavaScript module as shown below:
_Old widget.js:_
```javascript
function Widget() {
// ...
}
Widget.prototype = {
// ...
};
module.exports = Widget;
```
_New widget.js:_
```javascript
function Widget() {
}
Widget.prototype = {
};
exports.Widget = Widget;
```
You should also do the same for your UI component renderer:
_Old renderer.js:_
```javascript
module.exports = function render(input, out) {
// ...
}
```
_New renderer.js:_
```javascript
exports.renderer = function(input, out) {
// ...
}
```

1
widgets/dom.js Normal file
View File

@ -0,0 +1 @@
module.exports = require('marko-dom');

166
widgets/marko.json Normal file
View File

@ -0,0 +1,166 @@
{
"<*>": {
"@w-bind": {
"type": "string",
"preserve-name": true,
"autocomplete": [
{
"descriptionMoreURL": "http://markojs.com/docs/marko-widgets/get-started/#binding-behavior"
},
{
"displayText": "w-bind=\"<widgetPath>\"",
"snippet": "w-bind=\"${1:./widget}\"",
"descriptionMoreURL": "http://markojs.com/docs/marko-widgets/get-started/#binding-behavior"
}
]
},
"@w-scope": {
"type": "expression",
"preserve-name": true,
"autocomplete": []
},
"@w-extend": {
"type": "custom",
"preserve-name": true
},
"@w-config": {
"type": "expression",
"preserve-name": true
},
"@w-for": {
"type": "string",
"preserve-name": true
},
"@w-id": {
"type": "string",
"preserve-name": true,
"autocomplete": [
{
"displayText": "w-id=\"<method>\"",
"snippet": "w-id=\"${1:method}\"",
"descriptionMoreURL": "http://markojs.com/docs/marko-widgets/get-started/#referencing-nested-widgets"
},
{
"descriptionMoreURL": "http://markojs.com/docs/marko-widgets/get-started/#referencing-nested-widgets"
}
]
},
"@w-on*": {
"pattern": true,
"type": "string",
"allow-expressions": true,
"preserve-name": true,
"set-flag": "hasWidgetEvents",
"autocomplete": [
{
"displayText": "w-on<event>=\"<method>\"",
"snippet": "w-on${1:Click}=\"handle${2:Button}${1:Click}\"",
"descriptionMoreURL": "http://markojs.com/docs/marko-widgets/get-started/#adding-dom-event-listeners"
},
{
"text": "w-on",
"snippet": "w-on",
"descriptionMoreURL": "http://markojs.com/docs/marko-widgets/get-started/#adding-dom-event-listeners"
}
]
},
"@w-body": {
"preserve-name": true,
"autocomplete": [
{
"openTagOnly": true,
"descriptionMoreURL": "http://markojs.com/docs/marko-widgets/get-started/#adding-dom-event-listeners"
}
]
},
"@w-preserve": {
"type": "flag",
"preserve-name": true,
"autocomplete": [
{
"descriptionMoreURL": "http://markojs.com/docs/marko-widgets/#preserving-dom-nodes-during-re-render"
}
]
},
"@w-preserve-body": {
"type": "flag",
"preserve-name": true,
"autocomplete": [
{
"descriptionMoreURL": "http://markojs.com/docs/marko-widgets/#preserving-dom-nodes-during-re-render"
}
]
},
"@w-preserve-if": {
"preserve-name": true,
"autocomplete": [
{
"snippet": "w-preserve-if(${1:condition})",
"descriptionMoreURL": "http://markojs.com/docs/marko-widgets/#preserving-dom-nodes-during-re-render"
}
]
},
"@w-preserve-body-if": {
"preserve-name": true,
"autocomplete": [
{
"snippet": "w-preserve-body-if(${1:condition})",
"descriptionMoreURL": "http://markojs.com/docs/marko-widgets/#preserving-dom-nodes-during-re-render"
}
]
},
"@w-preserve-attrs": {
"type": "string",
"preserve-name": true,
"autocomplete": [
{
"displayText": "w-preserve-attrs=\"attr1,attr2\"",
"snippet": "w-preserve-attrs=\"${1:class,style}\"",
"descriptionMoreURL": "http://markojs.com/docs/marko-widgets/#preserving-dom-attributes-during-re-render"
}
]
},
"transformer": "./taglib/widgets-transformer.js"
},
"<w-widget>": {
"renderer": "./taglib/widget-tag.js",
"@type": "object",
"@config": "object",
"@id": "string",
"@hasDomEvents": "number",
"@body": "string",
"var": "widget",
"import-var": {
"_cfg": "data.widgetConfig",
"_state": "data.widgetState",
"_props": "data.widgetProps",
"_body": "data.widgetBody"
},
"autocomplete": []
},
"<init-widgets>": {
"renderer": "./taglib/init-widgets-tag.js",
"@immediate": "boolean"
},
"<w-preserve>": {
"renderer": "./taglib/preserve-tag.js",
"@id": "string",
"@if": "expression",
"@body-only": "expression",
"autocomplete": []
},
"<widget-types>": {
"code-generator": "./taglib/widget-types-tag.js",
"@*": "string",
"autocomplete": [
{
"displayText": "widget-types default=\"<pat1h>\" alt=\"<path2>\"",
"snippet": "widget-types default=\"${1:./widget}\"",
"openTagOnly": true
}
]
}
}

74
widgets/package.json Normal file
View File

@ -0,0 +1,74 @@
{
"name": "marko-widgets",
"description": "Module to support binding of behavior to rendered UI components rendered on the server or client",
"repository": {
"type": "git",
"url": "https://github.com/marko-js/marko-widgets.git"
},
"scripts": {
"test": "npm run test-server -s && npm run test-browser -s && npm run jshint --silent",
"test-server": "mocha --ui bdd --reporter spec ./test/",
"test-browser": "node test/browser-tests-runner/cli.js test/browser-tests.js --automated && npm run test-browser-pages",
"test-browser-pages": "node test/browser-tests-runner/cli.js --pages --automated",
"test-browser-dev": "browser-refresh test/browser-tests-runner/cli.js test/browser-tests.js --server",
"test-page": "browser-refresh test/browser-tests-runner/cli.js test/browser-tests.js --server --page",
"jshint": "jshint src/ taglib/ *.js"
},
"author": "Patrick Steele-Idem <pnidem@gmail.com>",
"maintainers": "Patrick Steele-Idem <pnidem@gmail.com>",
"dependencies": {
"events": "^1.0.2",
"lasso-modules-client": "^1.0.0",
"listener-tracker": "^1.0.2",
"marko-dom": "^1.1.1",
"morphdom": "^2.2.0",
"raptor-async": "^1.1.2",
"raptor-json": "^1.0.1",
"raptor-logging": "^1.0.1",
"raptor-polyfill": "^1.0.0",
"raptor-pubsub": "^1.0.2",
"raptor-util": "^3.0.0",
"resolve-from": "^1.0.1",
"try-require": "^1.2.1",
"warp10": "^1.0.0"
},
"peerDependencies": {
"marko": "^4.0.0-beta.0"
},
"devDependencies": {
"app-module-path": "^1.0.1",
"argly": "^1.0.0",
"async": "^0.9.0",
"browser-refresh": "^1.6.0",
"browser-refresh-taglib": "^1.1.0",
"chai": "^3.5.0",
"child-process-promise": "^2.0.3",
"express": "^4.14.0",
"ignoring-watcher": "^1.0.2",
"jquery": "^2.1.3",
"jshint": "^2.9.1",
"lasso": "^2.4.1",
"lasso-marko": "^2.0.4",
"marko": "^4.0.0-beta.2",
"mkdirp": "^0.5.1",
"mocha": "^2.4.5",
"mocha-phantomjs": "^4.1.0",
"phantomjs-prebuilt": "^2.1.13",
"raptor-args": "^1.0.2",
"raptor-strings": "^1.0.0",
"require-self-ref": "^2.0.1"
},
"license": "Apache-2.0",
"bin": {},
"main": "src/index.js",
"publishConfig": {
"registry": "https://registry.npmjs.org/"
},
"browser": {
"./src/index.js": "./src/index-browser.js",
"./src/uniqueId.js": "./src/uniqueId-browser.js",
"./src/init-widgets.js": "./src/init-widgets-browser.js",
"./src/defineWidget.js": "./src/defineWidget-browser.js"
},
"version": "6.3.7"
}

1
widgets/renderer.js Normal file
View File

@ -0,0 +1 @@
module.exports = require('raptor-renderer');

727
widgets/src/Widget.js Normal file
View File

@ -0,0 +1,727 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var inherit = require('raptor-util/inherit');
var dom = require('marko-dom');
var markoWidgets = require('./');
var EventEmitter = require('events').EventEmitter;
var listenerTracker = require('listener-tracker');
var arrayFromArguments = require('raptor-util/arrayFromArguments');
var extend = require('raptor-util/extend');
var updateManager = require('./update-manager');
var morphdom = require('morphdom');
var marko = require('marko');
var MORPHDOM_SKIP = false;
var WIDGET_SUBSCRIBE_TO_OPTIONS = null;
var NON_WIDGET_SUBSCRIBE_TO_OPTIONS = {
addDestroyListener: false
};
var emit = EventEmitter.prototype.emit;
var idRegExp = /^\#(\w+)( .*)?/;
var lifecycleEventMethods = {
'beforeDestroy': 'onBeforeDestroy',
'destroy': 'onDestroy',
'beforeUpdate': 'onBeforeUpdate',
'update': 'onUpdate',
'render': 'onRender',
'beforeInit': 'onBeforeInit',
'afterInit': 'onAfterInit'
};
function removeListener(eventListenerHandle) {
eventListenerHandle.remove();
}
function destroyRecursive(el) {
dom.forEachChildEl(el, function (childEl) {
var descendentWidget = childEl.__widget;
if (descendentWidget) {
destroy(descendentWidget, false, false);
}
destroyRecursive(childEl);
});
}
/**
* This method handles invoking a widget's event handler method
* (if present) while also emitting the event through
* the standard EventEmitter.prototype.emit method.
*
* Special events and their corresponding handler methods
* include the following:
*
* beforeDestroy --> onBeforeDestroy
* destroy --> onDestroy
* beforeUpdate --> onBeforeUpdate
* update --> onUpdate
* render --> onRender
*/
function emitLifecycleEvent(widget, eventType, eventArg) {
var listenerMethod = widget[lifecycleEventMethods[eventType]];
if (listenerMethod) {
listenerMethod.call(widget, eventArg);
}
widget.emit(eventType, eventArg);
}
function removeDOMEventListeners(widget) {
var eventListenerHandles = widget.__evHandles;
if (eventListenerHandles) {
eventListenerHandles.forEach(removeListener);
widget.__evHandles = null;
}
}
function destroy(widget, removeNode, recursive) {
if (widget.isDestroyed()) {
return;
}
var rootEl = widget.getEl();
emitLifecycleEvent(widget, 'beforeDestroy');
widget.__lifecycleState = 'destroyed';
if (rootEl) {
if (recursive) {
destroyRecursive(rootEl);
}
if (removeNode && rootEl.parentNode) {
//Remove the widget's DOM nodes from the DOM tree if the root element is known
rootEl.parentNode.removeChild(rootEl);
}
rootEl.__widget = null;
}
// Unsubscribe from all DOM events
removeDOMEventListeners(widget);
if (widget.__subscriptions) {
widget.__subscriptions.removeAllListeners();
widget.__subscriptions = null;
}
emitLifecycleEvent(widget, 'destroy');
}
function setState(widget, name, value, forceDirty, noQueue) {
if (typeof value === 'function') {
return;
}
if (value === null) {
// Treat null as undefined to simplify our comparison logic
value = undefined;
}
if (forceDirty) {
var dirtyState = widget.__dirtyState || (widget.__dirtyState = {});
dirtyState[name] = true;
} else if (widget.state[name] === value) {
return;
}
var clean = !widget.__dirty;
if (clean) {
// This is the first time we are modifying the widget state
// so introduce some properties to do some tracking of
// changes to the state
var currentState = widget.state;
widget.__dirty = true; // Mark the widget state as dirty (i.e. modified)
widget.__oldState = currentState;
widget.state = extend({}, currentState);
widget.__stateChanges = {};
}
widget.__stateChanges[name] = value;
if (value == null) {
// Don't store state properties with an undefined or null value
delete widget.state[name];
} else {
// Otherwise, store the new value in the widget state
widget.state[name] = value;
}
if (clean && noQueue !== true) {
// If we were clean before then we are now dirty so queue
// up the widget for update
updateManager.queueWidgetUpdate(widget);
}
}
function replaceState(widget, newState, noQueue) {
var k;
for (k in widget.state) {
if (widget.state.hasOwnProperty(k) && !newState.hasOwnProperty(k)) {
setState(widget, k, undefined, false, noQueue);
}
}
for (k in newState) {
if (newState.hasOwnProperty(k)) {
setState(widget, k, newState[k], false, noQueue);
}
}
}
function resetWidget(widget) {
widget.__oldState = null;
widget.__dirty = false;
widget.__stateChanges = null;
widget.__newProps = null;
widget.__dirtyState = null;
}
function hasCompatibleWidget(widgetsContext, existingWidget) {
var id = existingWidget.id;
var newWidgetDef = widgetsContext.getWidget(id);
if (!newWidgetDef) {
return false;
}
return existingWidget.__type === newWidgetDef.type;
}
function handleCustomEventWithMethodListener(widget, targetMethodName, args) {
// Remove the "eventType" argument
args = arrayFromArguments(args, 1);
args.push(widget);
var targetWidget = markoWidgets.getWidgetForEl(widget.__scope);
var targetMethod = targetWidget[targetMethodName];
if (!targetMethod) {
throw new Error('Method not found for widget ' + targetWidget.id + ': ' + targetMethodName);
}
targetMethod.apply(targetWidget, args);
}
var widgetProto;
/**
* Base widget type.
*
* NOTE: Any methods that are prefixed with an underscore should be considered private!
*/
function Widget(id, document) {
EventEmitter.call(this);
this.id = id;
this.el = null;
this.bodyEl = null;
this.state = null;
this.__subscriptions = null;
this.__evHandles = null;
this.__lifecycleState = null;
this.__customEvents = null;
this.__scope = null;
this.__dirty = false;
this.__oldState = null;
this.__stateChanges = null;
this.__updateQueued = false;
this.__dirtyState = null;
this.__document = document;
}
Widget.prototype = widgetProto = {
_isWidget: true,
subscribeTo: function(target) {
if (!target) {
throw new Error('target is required');
}
var tracker = this.__subscriptions;
if (!tracker) {
this.__subscriptions = tracker = listenerTracker.createTracker();
}
var subscribeToOptions = target._isWidget ?
WIDGET_SUBSCRIBE_TO_OPTIONS :
NON_WIDGET_SUBSCRIBE_TO_OPTIONS;
return tracker.subscribeTo(target, subscribeToOptions);
},
emit: function(eventType) {
var customEvents = this.__customEvents;
var targetMethodName;
if (customEvents && (targetMethodName = customEvents[eventType])) {
var len = arguments.length;
var args = new Array(len-1);
for (var i = 1; i < len; i++) {
args[i] = arguments[i];
}
handleCustomEventWithMethodListener(this, targetMethodName, args);
}
return emit.apply(this, arguments);
},
getElId: function (widgetElId, index) {
var elId = widgetElId != null ? this.id + '-' + widgetElId : this.id;
if (index != null) {
elId += '[' + index + ']';
}
return elId;
},
getEl: function (widgetElId, index) {
if (widgetElId != null) {
return this.__document.getElementById(this.getElId(widgetElId, index));
} else {
return this.el || this.__document.getElementById(this.getElId());
}
},
getEls: function(id) {
var els = [];
var i=0;
while(true) {
var el = this.getEl(id, i);
if (!el) {
break;
}
els.push(el);
i++;
}
return els;
},
getWidget: function(id, index) {
var targetWidgetId = this.getElId(id, index);
return markoWidgets.getWidgetForEl(targetWidgetId, this.__document);
},
getWidgets: function(id) {
var widgets = [];
var i=0;
while(true) {
var widget = this.getWidget(id, i);
if (!widget) {
break;
}
widgets.push(widget);
i++;
}
return widgets;
},
destroy: function (options) {
options = options || {};
destroy(this, options.removeNode !== false, options.recursive !== false);
},
isDestroyed: function () {
return this.__lifecycleState === 'destroyed';
},
getBodyEl: function() {
return this.bodyEl;
},
setState: function(name, value) {
if (typeof name === 'object') {
// Merge in the new state with the old state
var newState = name;
for (var k in newState) {
if (newState.hasOwnProperty(k)) {
setState(this, k, newState[k]);
}
}
return;
}
setState(this, name, value);
},
setStateDirty: function(name, value) {
if (arguments.length === 1) {
value = this.state[name];
}
setState(this, name, value, true /* forceDirty */);
},
_replaceState: function(newState) {
replaceState(this, newState, true /* do not queue an update */ );
},
_removeDOMEventListeners: function() {
removeDOMEventListeners(this);
},
replaceState: function(newState) {
replaceState(this, newState);
},
/**
* Recalculate the new state from the given props using the widget's
* getInitialState(props) method. If the widget does not have a
* getInitialState(props) then it is re-rendered with the new props
* as input.
*
* @param {Object} props The widget's new props
*/
setProps: function(newProps) {
if (this.getInitialState) {
if (this.getInitialProps) {
newProps = this.getInitialProps(newProps) || {};
}
var newState = this.getInitialState(newProps);
this.replaceState(newState);
return;
}
if (!this.__newProps) {
updateManager.queueWidgetUpdate(this);
}
this.__newProps = newProps;
},
update: function() {
if (this.isDestroyed()) {
return;
}
var newProps = this.__newProps;
if (this.shouldUpdate(newProps, this.state) === false) {
resetWidget(this);
return;
}
if (newProps) {
resetWidget(this);
this.rerender(newProps);
return;
}
if (!this.__dirty) {
// Don't even bother trying to update this widget since it is
// not marked as dirty.
return;
}
if (!this._processUpdateHandlers()) {
this.doUpdate(this.__stateChanges, this.__oldState);
}
// Reset all internal properties for tracking state changes, etc.
resetWidget(this);
},
isDirty: function() {
return this.__dirty;
},
_reset: function() {
resetWidget(this);
},
/**
* This method is used to process "update_<stateName>" handler functions.
* If all of the modified state properties have a user provided update handler
* then a rerender will be bypassed and, instead, the DOM will be updated
* looping over and invoking the custom update handlers.
* @return {boolean} Returns true if if the DOM was updated. False, otherwise.
*/
_processUpdateHandlers: function() {
var stateChanges = this.__stateChanges;
var oldState = this.__oldState;
var handlerMethod;
var handlers = [];
var newValue;
var oldValue;
for (var propName in stateChanges) {
if (stateChanges.hasOwnProperty(propName)) {
newValue = stateChanges[propName];
oldValue = oldState[propName];
if (oldValue === newValue) {
// Only do an update for this state property if it is actually
// different from the old state or if it was forced to be dirty
// using setStateDirty(propName)
var dirtyState = this.__dirtyState;
if (dirtyState == null || !dirtyState.hasOwnProperty(propName)) {
continue;
}
}
var handlerMethodName = 'update_' + propName;
handlerMethod = this[handlerMethodName];
if (handlerMethod) {
handlers.push([propName, handlerMethod]);
} else {
// This state change does not have a state handler so return false
// to force a rerender
return false;
}
}
}
// If we got here then all of the changed state properties have
// an update handler or there are no state properties that actually
// changed.
if (!handlers.length) {
return true;
}
// Otherwise, there are handlers for all of the changed properties
// so apply the updates using those handlers
emitLifecycleEvent(this, 'beforeUpdate');
for (var i=0, len=handlers.length; i<len; i++) {
var handler = handlers[i];
var propertyName = handler[0];
handlerMethod = handler[1];
newValue = stateChanges[propertyName];
oldValue = oldState[propertyName];
handlerMethod.call(this, newValue, oldValue);
}
emitLifecycleEvent(this, 'update');
resetWidget(this);
return true;
},
shouldUpdate: function(newState, newProps) {
return true;
},
doUpdate: function (stateChanges, oldState) {
this.rerender();
},
_emitLifecycleEvent: function(eventType, eventArg) {
emitLifecycleEvent(this, eventType, eventArg);
},
rerender: function(props) {
var self = this;
if (!self.renderer) {
throw new Error('Widget does not have a "renderer" property');
}
var elToReplace = this.__document.getElementById(self.id);
var renderer = self.renderer;
self.__lifecycleState = 'rerender';
var templateData = extend({}, props || self.state);
var globalData = {};
globalData.__rerenderWidget = self;
globalData.__rerenderEl = self.el;
globalData.__rerender = true;
if (!props) {
globalData.__rerenderState = props ? null : self.state;
}
updateManager.batchUpdate(function() {
var createOut = renderer.createOut || marko.createOut;
var out = createOut(globalData);
renderer(templateData, out);
var targetNode;
if (out.isVDOM) {
targetNode = out.getOutput().firstChild;
} else {
targetNode = out.getNode(self.__document);
}
var widgetsContext = out.global.widgets;
function onNodeDiscarded(node) {
var widget = node.__widget;
if (widget) {
destroy(widget, false, false);
}
}
function onBeforeElUpdated(fromEl, toEl) {
var id = fromEl.id;
var existingWidget;
var preservedAttrs = !out.isVDOM && toEl.getAttribute('data-preserve-attrs');
if (preservedAttrs) {
preservedAttrs = preservedAttrs.split(/\s*[,]\s*/);
for (var i=0; i<preservedAttrs.length; i++) {
var preservedAttrName = preservedAttrs[i];
var preservedAttrValue = fromEl.getAttribute(preservedAttrName);
if (preservedAttrValue == null) {
toEl.removeAttribute(preservedAttrName);
} else {
toEl.setAttribute(preservedAttrName, preservedAttrValue);
}
}
}
if (widgetsContext && id) {
if (widgetsContext.isPreservedEl(id)) {
if (widgetsContext.hasUnpreservedBody(id)) {
existingWidget = fromEl.__widget;
morphdom(existingWidget.bodyEl, toEl, {
childrenOnly: true,
onNodeDiscarded: onNodeDiscarded,
onBeforeElUpdated: onBeforeElUpdated,
onBeforeElChildrenUpdated: onBeforeElChildrenUpdated
});
}
// Don't morph elements that are associated with widgets that are being
// reused or elements that are being preserved. For widgets being reused,
// the morphing will take place when the reused widget updates.
return MORPHDOM_SKIP;
} else {
existingWidget = fromEl.__widget;
if (existingWidget && !hasCompatibleWidget(widgetsContext, existingWidget)) {
// We found a widget in an old DOM node that does not have
// a compatible widget that was rendered so we need to
// destroy the old widget
destroy(existingWidget, false, false);
}
}
}
}
function onBeforeElChildrenUpdated(el) {
if (widgetsContext && el.id) {
if (widgetsContext.isPreservedBodyEl(el.id)) {
// Don't morph the children since they are preserved
return MORPHDOM_SKIP;
}
}
}
morphdom(elToReplace, targetNode, {
onNodeDiscarded: onNodeDiscarded,
onBeforeElUpdated: onBeforeElUpdated,
onBeforeElChildrenUpdated: onBeforeElChildrenUpdated
});
// Trigger any 'onUpdate' events for all of the rendered widgets
out.afterInsert(elToReplace);
self.__lifecycleState = null;
if (!props) {
// We have re-rendered with the new state so our state
// is no longer dirty. Before updating a widget
// we check if a widget is dirty. If a widget is not
// dirty then we abort the update. Therefore, if the
// widget was queued for update and the re-rendered
// before the update occurred then nothing will happen
// at the time of the update.
resetWidget(self);
}
});
},
detach: function () {
dom.detach(this.el);
},
appendTo: function (targetEl) {
dom.appendTo(this.el, targetEl);
},
replace: function (targetEl) {
dom.replace(this.el, targetEl);
},
replaceChildrenOf: function (targetEl) {
dom.replaceChildrenOf(this.el, targetEl);
},
insertBefore: function (targetEl) {
dom.insertBefore(this.el, targetEl);
},
insertAfter: function (targetEl) {
dom.insertAfter(this.el, targetEl);
},
prependTo: function (targetEl) {
dom.prependTo(this.el, targetEl);
},
ready: function (callback) {
var document = this.el.ownerDocument;
markoWidgets.ready(callback, this, document);
},
$: function (arg) {
var jquery = markoWidgets.$;
var args = arguments;
if (args.length === 1) {
//Handle an "ondomready" callback function
if (typeof arg === 'function') {
var _this = this;
return _this.ready(function() {
arg.call(_this);
});
} else if (typeof arg === 'string') {
var match = idRegExp.exec(arg);
//Reset the search to 0 so the next call to exec will start from the beginning for the new string
if (match != null) {
var widgetElId = match[1];
if (match[2] == null) {
return jquery(this.getEl(widgetElId));
} else {
return jquery('#' + this.getElId(widgetElId) + match[2]);
}
} else {
var rootEl = this.getEl();
if (!rootEl) {
throw new Error('Root element is not defined for widget');
}
if (rootEl) {
return jquery(arg, rootEl);
}
}
}
} else if (args.length === 2 && typeof args[1] === 'string') {
return jquery(arg, this.getEl(args[1]));
} else if (args.length === 0) {
return jquery(this.el);
}
return jquery.apply(window, arguments);
}
};
widgetProto.elId = widgetProto.getElId;
inherit(Widget, EventEmitter);
module.exports = Widget;

110
widgets/src/WidgetDef.js Normal file
View File

@ -0,0 +1,110 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
require('raptor-polyfill/string/endsWith');
var repeatedId = require('./repeated-id');
/**
* A WidgetDef is used to hold the metadata collected at runtime for
* a single widget and this information is used to instantiate the widget
* later (after the rendered HTML has been added to the DOM)
*/
function WidgetDef(config, endFunc, out) {
this.type = config.type; // The widget module type name that is passed to the factory
this.id = config.id; // The unique ID of the widget
this.config = config.config; // Widget config object (may be null)
this.state = config.state; // Widget state object (may be null)
this.scope = config.scope; // The ID of the widget that this widget is scoped within
this.domEvents = null; // An array of DOM events that need to be added (in sets of three)
this.customEvents = config.customEvents; // An array containing information about custom events
this.bodyElId = config.bodyElId; // The ID for the default body element (if any any)
this.children = []; // An array of nested WidgetDef instances
this.end = endFunc; // A function that when called will pop this widget def off the stack
this.extend = config.extend; // Information about other widgets that extend this widget.
this.out = out; // The AsyncWriter that this widget is associated with
this.hasDomEvents = config.hasDomEvents; // A flag to indicate if this widget has any
// listeners for non-bubbling DOM events
this._nextId = 0; // The unique integer to use for the next scoped ID
}
WidgetDef.prototype = {
/**
* Register a nested widget for this widget. We maintain a tree of widgets
* so that we can instantiate nested widgets before their parents.
*/
addChild: function (widgetDef) {
this.children.push(widgetDef);
},
/**
* This helper method generates a unique and fully qualified DOM element ID
* that is unique within the scope of the current widget. This method prefixes
* the the nestedId with the ID of the current widget. If nestedId ends
* with `[]` then it is treated as a repeated ID and we will generate
* an ID with the current index for the current nestedId.
* (e.g. "myParentId-foo[0]", "myParentId-foo[1]", etc.)
*/
elId: function (nestedId) {
if (nestedId == null) {
return this.id;
} else {
if (typeof nestedId === 'string' && nestedId.endsWith('[]')) {
return repeatedId.nextId(this.out, this.id, nestedId);
} else {
return this.id + '-' + nestedId;
}
}
},
/**
* Registers a DOM event for a nested HTML element associated with the
* widget. This is only done for non-bubbling events that require
* direct event listeners to be added.
* @param {String} type The DOM event type ("mouseover", "mousemove", etc.)
* @param {String} targetMethod The name of the method to invoke on the scoped widget
* @param {String} elId The DOM element ID of the DOM element that the event listener needs to be added too
*/
addDomEvent: function(type, targetMethod, elId) {
if (!targetMethod) {
// The event handler method is allowed to be conditional. At render time if the target
// method is null then we do not attach any direct event listeners.
return;
}
if (!this.domEvents) {
this.domEvents = [];
}
this.domEvents.push(type);
this.domEvents.push(targetMethod);
this.domEvents.push(elId);
},
/**
* Returns a string representation of the DOM events data.
*/
getDomEventsAttr: function() {
if (this.domEvents) {
return this.domEvents.join(',');
}
},
/**
* Returns the next auto generated unique ID for a nested DOM element or nested DOM widget
*/
nextId: function() {
return this.id + '-w' + (this._nextId++);
}
};
module.exports = WidgetDef;

View File

@ -0,0 +1,145 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var WidgetDef = require('./WidgetDef');
var uniqueId = require('./uniqueId');
var initWidgets = require('./init-widgets');
var EventEmitter = require('events').EventEmitter;
var inherit = require('raptor-util/inherit');
var PRESERVE_EL = 1;
var PRESERVE_EL_BODY = 2;
var PRESERVE_EL_UNPRESERVED_BODY = 4;
function WidgetsContext(out) {
EventEmitter.call(this);
this.out = out;
this.widgets = [];
this.widgetStack = [];
this.preserved = null;
this.reusableWidgets = null;
this.reusableWidgetsById = null;
this.widgetsById = {};
}
WidgetsContext.prototype = {
getWidgets: function () {
return this.widgets;
},
getWidgetStack: function() {
return this.widgetStack;
},
getCurrentWidget: function() {
return this.widgetStack.length ? this.widgetStack[this.widgetStack.length - 1] : undefined;
},
beginWidget: function (widgetInfo, callback) {
var _this = this;
var widgetStack = _this.widgetStack;
var origLength = widgetStack.length;
var parent = origLength ? widgetStack[origLength - 1] : null;
if (!widgetInfo.id) {
widgetInfo.id = _this._nextWidgetId();
}
widgetInfo.parent = parent;
function end() {
widgetStack.length = origLength;
}
var widgetDef = new WidgetDef(widgetInfo, end, this.out);
this.widgetsById[widgetInfo.id] = widgetDef;
if (parent) {
//Check if it is a top-level widget
parent.addChild(widgetDef);
} else {
_this.widgets.push(widgetDef);
}
widgetStack.push(widgetDef);
this.emit('beginWidget', widgetDef);
return widgetDef;
},
getWidget: function(id) {
return this.widgetsById[id];
},
hasWidgets: function () {
return this.widgets.length !== 0;
},
clearWidgets: function () {
this.widgets = [];
this.widgetStack = [];
},
_nextWidgetId: function () {
return uniqueId(this.out);
},
initWidgets: function (document) {
var widgetDefs = this.widgets;
initWidgets.initClientRendered(widgetDefs, document);
this.clearWidgets();
},
onBeginWidget: function(listener) {
this.on('beginWidget', listener);
},
isPreservedEl: function(id) {
var preserved = this.preserved;
return preserved && (preserved[id] & PRESERVE_EL);
},
isPreservedBodyEl: function(id) {
var preserved = this.preserved;
return preserved && (preserved[id] & PRESERVE_EL_BODY);
},
hasUnpreservedBody: function(id) {
var preserved = this.preserved;
return preserved && (preserved[id] & PRESERVE_EL_UNPRESERVED_BODY);
},
addPreservedDOMNode: function(existingEl, bodyOnly, hasUnppreservedBody) {
var preserved = this.preserved || (this.preserved = {});
var value = bodyOnly ?
PRESERVE_EL_BODY :
PRESERVE_EL;
if (hasUnppreservedBody) {
value |= PRESERVE_EL_UNPRESERVED_BODY;
}
preserved[existingEl.id] = value;
}
};
inherit(WidgetsContext, EventEmitter);
WidgetsContext.getWidgetsContext = function (out) {
var global = out.global;
return out.data.widgets ||
global.widgets ||
(global.widgets = new WidgetsContext(out));
};
module.exports = WidgetsContext;

View File

@ -0,0 +1,94 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* This module provides a cross-browser solution for adding event listeners
* to DOM elements. This code is used to handle the differences between
* IE and standards browsers. Older IE browsers use "attachEvent" while
* newer browsers using "addEventListener".
*/
var testEl = document.body || document.createElement('div');
function IEListenerHandle(el, eventType, listener) {
this._info = [el, eventType, listener];
}
IEListenerHandle.prototype = {
remove: function() {
var info = this._info;
var el = info[0];
var eventType = info[1];
var listener = info[2];
el.detachEvent(eventType, listener);
}
};
function ListenerHandle(el, eventType, listener) {
this._info = [el, eventType, listener];
}
ListenerHandle.prototype = {
remove: function() {
var info = this._info;
var el = info[0];
var eventType = info[1];
var listener = info[2];
el.removeEventListener(eventType, listener);
}
};
/**
* Adapt an native IE event to a new event by monkey patching it
*/
function getIEEvent() {
var event = window.event;
// add event.target
event.target = event.target || event.srcElement;
event.preventDefault = event.preventDefault || function() {
event.returnValue = false;
};
event.stopPropagation = event.stopPropagation || function() {
event.cancelBubble = true;
};
event.key = (event.which + 1 || event.keyCode + 1) - 1 || 0;
return event;
}
if (!testEl.addEventListener) {
// IE8...
module.exports = function(el, eventType, listener) {
function wrappedListener() {
var event = getIEEvent();
listener(event);
}
eventType = 'on' + eventType;
el.attachEvent(eventType, wrappedListener);
return new IEListenerHandle(el, eventType, wrappedListener);
};
} else {
// Non-IE8...
module.exports = function(el, eventType, listener) {
el.addEventListener(eventType, listener, false);
return new ListenerHandle(el, eventType, listener);
};
}

10
widgets/src/browser.json Normal file
View File

@ -0,0 +1,10 @@
{
"dependencies": [
{
"type": "require",
"path": "./client-init",
"run": true,
"if": "!flags.contains('marko-widgets/no-client-init')"
}
]
}

47
widgets/src/bubble.js Normal file
View File

@ -0,0 +1,47 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
module.exports = [
/* Mouse Events */
'click',
'dblclick',
'mousedown',
'mouseup',
// 'mouseover',
// 'mousemove',
// 'mouseout',
'dragstart',
'drag',
// 'dragenter',
// 'dragleave',
// 'dragover',
'drop',
'dragend',
/* Keyboard Events */
'keydown',
'keypress',
'keyup',
/* Form Events */
'select',
'change',
'submit',
'reset'
// 'focus', <-- Does not bubble
// 'blur', <-- Does not bubble
// 'focusin', <-- Not supported in all browsers
// 'focusout' <-- Not supported in all browsers
];

View File

@ -0,0 +1,17 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
require('./init-widgets').initServerRendered();

View File

@ -0,0 +1,44 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Define a new UI component that includes widget and renderer.
*
* @param {Object} def The definition of the UI component (widget methods, widget constructor, rendering methods, etc.)
* @return {Widget} The resulting Widget with renderer
*/
var defineRenderer;
var defineWidget;
module.exports = function defineComponent(def) {
if (def._isWidget) {
return def;
}
var renderer;
if (def.template || def.renderer) {
renderer = defineRenderer(def);
} else {
throw new Error('Expected "template" or "renderer"');
}
return defineWidget(def, renderer);
};
defineRenderer = require('./defineRenderer');
defineWidget = require('./defineWidget');

View File

@ -0,0 +1,164 @@
var marko = require('marko');
var extend = require('raptor-util/extend');
module.exports = function defineRenderer(def) {
var renderer = def.renderer;
if (renderer && renderer._isRenderer) {
return renderer;
}
var template = def.template;
var getInitialProps = def.getInitialProps;
var getTemplateData = def.getTemplateData;
var getInitialState = def.getInitialState;
var getWidgetConfig = def.getWidgetConfig;
var getInitialBody = def.getInitialBody;
var extendWidget = def.extendWidget;
if (typeof template === 'string') {
template = marko.load(template);
}
var createOut;
if (template) {
createOut = template.createOut;
} else {
createOut = def.createOut || marko.createOut;
}
if (!renderer) {
// Create a renderer function that takes care of translating
// the input properties to a view state. Also, this renderer
// takes care of re-using existing widgets.
renderer = function renderer(input, out) {
var global = out.global;
var newProps = input;
if (!newProps) {
// Make sure we always have a non-null input object
newProps = {};
}
var widgetState;
if (getInitialState) {
// This is a state-ful widget. If this is a rerender then the "input"
// will be the new state. If we have state then we should use the input
// as the widget state and skip the steps of converting the input
// to a widget state.
if (global.__rerenderWidget && global.__rerenderState) {
var isFirstWidget = !global.__firstWidgetFound;
if (!isFirstWidget || extendWidget) {
// We are the not first top-level widget or we are being extended
// so use the merged rerender state as defaults for the input
// and use that to rebuild the new state. This is kind of a hack
// but extending widgets requires this hack since there is no
// single state since the widget state is split between the
// widget being extended and the widget doing the extending.
for (var k in global.__rerenderState) {
if (global.__rerenderState.hasOwnProperty(k) && !input.hasOwnProperty(k)) {
newProps[k] = global.__rerenderState[k];
}
}
} else {
// We are the first widget and we are not being extended
// and we are not extending so use the input as the state
widgetState = input;
newProps = null;
}
}
}
if (!widgetState) {
// If we do not have state then we need to go through the process
// of converting the input to a widget state, or simply normalizing
// the input using getInitialProps
if (getInitialProps) {
// This optional method is used to normalize input state
newProps = getInitialProps(newProps, out) || {};
}
if (getInitialState) {
// This optional method is used to derive the widget state
// from the input properties
widgetState = getInitialState(newProps, out);
}
}
global.__firstWidgetFound = true;
// Use getTemplateData(state, props, out) to get the template
// data. If that method is not provided then just use the
// the state (if provided) or the input data.
var templateData = getTemplateData ?
getTemplateData(widgetState, newProps, out) :
widgetState || newProps;
if (templateData) {
// We are going to be modifying the template data so we need to
// make a shallow clone of the object so that we don't
// mutate user provided data.
templateData = extend({}, templateData);
} else {
// We always should have some template data
templateData = {};
}
if (widgetState) {
// If we have widget state then pass it to the template
// so that it is available to the widget tag
templateData.widgetState = widgetState;
}
if (newProps) {
// If we have widget props then pass it to the template
// so that it is available to the widget tag. The widget props
// are only needed so that we can call widget.shouldUpdate(newProps)
templateData.widgetProps = newProps;
if (getInitialBody) {
// If we have widget a widget body then pass it to the template
// so that it is available to the widget tag and can be inserted
// at the w-body marker
templateData.widgetBody = getInitialBody(newProps, out);
} else {
// Default to using the nested content as the widget body
// getInitialBody was not implemented
templateData.widgetBody = newProps.renderBody;
}
if (getWidgetConfig) {
// If getWidgetConfig() was implemented then use that to
// get the widget config. The widget config will be passed
// to the widget constructor. If rendered on the server the
// widget config will be serialized to a JSON-like data
// structure and stored in a "data-w-config" attribute.
templateData.widgetConfig = getWidgetConfig(newProps, out);
}
}
// Render the template associated with the component using the final template
// data that we constructed
template.render(templateData, out);
};
}
renderer._isRenderer = true;
renderer.createOut = createOut;
renderer.render = function(input) {
var out = createOut();
renderer(input, out);
return out.end();
};
return renderer;
};

View File

@ -0,0 +1,115 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var BaseWidget;
var inherit;
module.exports = function defineWidget(def, renderer) {
if (def._isWidget) {
return def;
}
var extendWidget = def.extendWidget;
if (extendWidget) {
return {
renderer: renderer,
render: renderer.render,
extendWidget: function(widget) {
extendWidget(widget);
widget.renderer = renderer;
}
};
}
var WidgetClass;
var proto;
if (typeof def === 'function') {
WidgetClass = def;
proto = WidgetClass.prototype;
if (proto.render && proto.render.length === 2) {
throw new Error('"render(input, out)" is no longer supported. Use "renderer(input, out)" instead.');
}
} else if (typeof def === 'object') {
WidgetClass = def.init || function() {};
proto = WidgetClass.prototype = def;
} else {
throw new Error('Invalid widget');
}
// We don't use the constructor provided by the user
// since we don't invoke their constructor until
// we have had a chance to do our own initialization.
// Instead, we store their constructor in the "initWidget"
// property and that method gets called later inside
// init-widgets-browser.js
function Widget(id, document) {
BaseWidget.call(this, id, document);
}
if (!proto._isWidget) {
// Inherit from Widget if they didn't already
inherit(WidgetClass, BaseWidget);
}
// The same prototype will be used by our constructor after
// we he have set up the prototype chain using the inherit function
proto = Widget.prototype = WidgetClass.prototype;
proto.initWidget = WidgetClass;
proto.constructor = def.constructor = Widget;
// Set a flag on the constructor function to make it clear this is
// a widget so that we can short-circuit this work later
Widget._isWidget = true;
if (!renderer) {
renderer = WidgetClass.renderer || WidgetClass.prototype.renderer;
if (renderer) {
// Legacy support
var createOut = renderer.createOut;
if (typeof renderer !== 'function') {
var rendererObject = renderer;
renderer = function(input, out) {
var rendererFunc = rendererObject.renderer || rendererObject.render;
rendererFunc(input, out);
};
renderer.createOut = createOut;
}
renderer.render = function(input) {
var out = createOut();
renderer(input, out);
return out.end();
};
}
}
if (renderer) {
// Add the rendering related methods as statics on the
// new widget constructor function
Widget.renderer = proto.renderer = renderer;
Widget.render = renderer.render;
}
return Widget;
};
BaseWidget = require('./Widget');
inherit = require('raptor-util/inherit');

View File

@ -0,0 +1,31 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
module.exports = function defineWidget(def, renderer) {
if (def._isWidget) {
return def;
}
if (renderer) {
return {
_isWidget: true,
renderer: renderer,
render: renderer.render
};
} else {
return {_isWidget: true};
}
};

View File

@ -0,0 +1,83 @@
var _addEventListener = require('./addEventListener');
var updateManager = require('./update-manager');
var attachBubbleEventListeners = function() {
var body = document.body;
// Here's where we handle event delegation using our own mechanism
// for delegating events. For each event that we have white-listed
// as supporting bubble, we will attach a listener to the root
// document.body element. When we get notified of a triggered event,
// we again walk up the tree starting at the target associated
// with the event to find any mappings for event. Each mapping
// is from a DOM event type to a method of a widget.
require('./bubble').forEach(function addBubbleHandler(eventType) {
_addEventListener(body, eventType, function(event) {
var propagationStopped = false;
// Monkey-patch to fix #97
var oldStopPropagation = event.stopPropagation;
event.stopPropagation = function() {
oldStopPropagation.call(event);
propagationStopped = true;
};
updateManager.batchUpdate(function() {
var curNode = event.target;
if (!curNode) {
return;
}
// Search up the tree looking DOM events mapped to target
// widget methods
var attrName = 'data-w-on' + eventType;
var targetMethod;
var targetWidget;
// Attributes will have the following form:
// w-on<event_type>="<target_method>|<widget_id>"
do {
if ((targetMethod = curNode.getAttribute(attrName))) {
var separator = targetMethod.lastIndexOf('|');
var targetWidgetId = targetMethod.substring(separator+1);
var targetWidgetEl = document.getElementById(targetWidgetId);
if (!targetWidgetEl) {
// The target widget is not in the DOM anymore
// which can happen when the widget and its
// children are removed from the DOM while
// processing the event.
continue;
}
targetWidget = targetWidgetEl.__widget;
if (!targetWidget) {
throw new Error('Widget not found: ' + targetWidgetId);
}
targetMethod = targetMethod.substring(0, separator);
var targetFunc = targetWidget[targetMethod];
if (!targetFunc) {
throw new Error('Method not found on widget ' + targetWidget.id + ': ' + targetMethod);
}
// Invoke the widget method
targetWidget[targetMethod](event, curNode);
if (propagationStopped) {
break;
}
}
} while((curNode = curNode.parentNode) && curNode.getAttribute);
});
});
});
};
exports.init = function() {
if (attachBubbleEventListeners) {
// Only attach event listeners once...
attachBubbleEventListeners();
attachBubbleEventListeners = null; // This is a one time thing
}
};

View File

@ -0,0 +1,91 @@
var EventEmitter = require('events').EventEmitter;
exports = module.exports = new EventEmitter();
var dom = require('marko-dom');
var ready = dom.ready;
var EMPTY_OBJ = {};
var Widget = require('./Widget');
var initWidgets = require('./init-widgets');
var updateManager = require('./update-manager');
var WidgetsContext = exports.WidgetsContext = require('./WidgetsContext');
exports.getWidgetsContext = WidgetsContext.getWidgetsContext;
exports.Widget = Widget;
exports.ready = ready;
exports.onInitWidget = function(listener) {
exports.on('initWidget', listener);
};
exports.attrs = function() {
return EMPTY_OBJ;
};
exports.writeDomEventsEl = function() {
/* Intentionally empty in the browser */
};
function getWidgetForEl(id, document) {
if (!id) {
return undefined;
}
var node = typeof id === 'string' ? (document || window.document).getElementById(id) : id;
return (node && node.__widget) || undefined;
}
exports.get = exports.getWidgetForEl = getWidgetForEl;
exports.initAllWidgets = function() {
initWidgets.initServerRendered(true /* scan DOM */);
};
// Subscribe to DOM manipulate events to handle creating and destroying widgets
dom.on('beforeRemove', function(eventArgs) {
var el = eventArgs.el;
var widget = el.id ? getWidgetForEl(el) : null;
if (widget) {
widget.destroy({
removeNode: false,
recursive: true
});
}
})
.on('renderedToDOM', function(eventArgs) {
var out = eventArgs.out;
var widgetsContext = out.global.widgets;
if (widgetsContext) {
widgetsContext.initWidgets(eventArgs.document);
}
});
exports.initWidgets = window.$markoWidgets = function(ids) {
initWidgets.initServerRendered(ids);
};
var JQUERY = 'jquery';
var jquery = window.$;
if (!jquery) {
try {
jquery = require(JQUERY);
}
catch(e) {}
}
exports.$ = jquery;
exports.registerWidget = require('./registry').register;
exports.defineComponent = require('./defineComponent');
exports.defineWidget = require('./defineWidget');
exports.defineRenderer = require('./defineRenderer');
exports.makeRenderable = exports.renderable = require('./renderable');
exports.c = function(component, template) {
component.template = template;
return exports.defineComponent(component);
};
exports.batchUpdate = updateManager.batchUpdate;
exports.onAfterUpdate = updateManager.onAfterUpdate;
window.$MARKO_WIDGETS = exports; // Helpful when debugging... WARNING: DO NOT USE IN REAL CODE!

331
widgets/src/index.js Normal file
View File

@ -0,0 +1,331 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Module to manage the lifecycle of widgets
*
*/
'use strict';
var warp10 = require('warp10');
var isObjectEmpty = require('raptor-util/isObjectEmpty');
var WidgetsContext = require('./WidgetsContext');
const WARP10_STATE_SERIALIZATION_OPTIONS = {var: '$markoWidgetsState', additive: true};
const WARP10_CONFIG_SERIALIZATION_OPTIONS = {var: '$markoWidgetsConfig', additive: true};
const TAG_START = '<noscript id="markoWidgets" data-ids="';
const TAG_END = '"></noscript>';
function WrappedString(val) {
this.html = val;
}
WrappedString.prototype = {
toString: function() {
return this.html;
}
};
exports.WidgetsContext = WidgetsContext;
exports.getWidgetsContext = WidgetsContext.getWidgetsContext;
exports.uniqueId = require('./uniqueId');
exports.attrs = function(widgetDef) {
if (!widgetDef.type) {
return null;
}
var attrs = {
'data-widget': widgetDef.type
};
var hasDomEvents = widgetDef.hasDomEvents;
if (hasDomEvents) {
attrs['data-w-on'] = '1';
}
var customEvents = widgetDef.customEvents;
if (customEvents) {
attrs['data-w-events'] = widgetDef.scope + ',' + customEvents.join(',');
}
var extend = widgetDef.extend;
if (extend && extend.length) {
attrs['data-w-extend'] = new WrappedString(extend.join(','));
}
var bodyElId = widgetDef.bodyElId;
if (bodyElId != null) {
attrs['data-w-body'] = bodyElId === '' ? true : bodyElId;
}
return attrs;
};
exports.writeDomEventsEl = function(widgetDef, out) {
var domEvents = widgetDef.domEvents;
if (domEvents) {
out.write('<span id="' + widgetDef.elId('$on') + '" data-on="' + domEvents.join(',') + '"></span>');
}
};
exports.writeInitWidgetsCode = function(widgetsContext, out, options) {
var clearWidgets = true;
var scanDOM = false;
var immediate = false;
if (options) {
clearWidgets = options.clearWidgets !== false;
scanDOM = options.scanDOM === true;
immediate = options.immediate === true;
}
if (scanDOM) {
out.write(TAG_START + '*' + TAG_END);
} else {
var widgets = widgetsContext.getWidgets();
if (!widgets || !widgets.length) {
return;
}
var ids = '';
var commaRequired = false;
var writeWidgets;
// Build separate objects for storing widget state and widget config. These objects
// will be serialized and sent to the browser using warp10
var widgetStateStore = {};
var widgetConfigStore = {};
var writeWidget = function(widget) {
if (widget.children.length) {
// Depth-first search (children should be initialized before parent)
writeWidgets(widget.children);
}
if (commaRequired) {
ids += ',';
} else {
commaRequired = true;
}
var widgetConfig = widget.config;
if (widgetConfig) {
// Put the widget config in the store using the widget ID as the key
widgetConfigStore[widget.id] = widgetConfig;
}
var widgetState = widget.state;
if (widgetState) {
// Put the widget state in the store using the widget ID as the key
widgetStateStore[widget.id] = widgetState;
}
ids += widget.id;
};
writeWidgets = function(widgets) {
for (var i = 0, len = widgets.length; i < len; i++) {
writeWidget(widgets[i]);
}
};
writeWidgets(widgets);
var widgetStateDeserializationCode;
var widgetConfigDeserializationCode;
if (isObjectEmpty(widgetStateStore)) {
widgetStateDeserializationCode = '';
} else {
widgetStateDeserializationCode = warp10.serialize(widgetStateStore, WARP10_STATE_SERIALIZATION_OPTIONS) +
';\n';
}
if (isObjectEmpty(widgetConfigStore)) {
widgetConfigDeserializationCode = '';
} else {
widgetConfigDeserializationCode = warp10.serialize(widgetConfigStore,WARP10_CONFIG_SERIALIZATION_OPTIONS) +
';\n';
}
var cspNonce = out.global.cspNonce;
var nonceAttr = cspNonce ? ' nonce='+JSON.stringify(cspNonce) : '';
if (immediate) {
out.write('<script' + nonceAttr + '>' +
widgetStateDeserializationCode +
widgetConfigDeserializationCode +
'$markoWidgets("' + ids + '")</script>');
} else {
out.write('<script' + nonceAttr + '>' +
widgetStateDeserializationCode +
widgetConfigDeserializationCode +
'</script>');
out.write(TAG_START + ids + TAG_END);
}
}
if (clearWidgets !== false) {
widgetsContext.clearWidgets();
}
};
function getRenderedWidgets(widgetsContext) {
if (!widgetsContext) {
throw new Error('"widgetsContext" is required');
}
if (!(widgetsContext instanceof WidgetsContext)) {
// Assume that the provided "widgetsContext" argument is
// actually an AsyncWriter
var asyncWriter = widgetsContext;
if (!asyncWriter.global) {
throw new Error('Invalid argument: ' + widgetsContext);
}
widgetsContext = WidgetsContext.getWidgetsContext(asyncWriter);
}
var widgets = widgetsContext.getWidgets();
if (!widgets || !widgets.length) {
return;
}
var ids = '';
var commaRequired = false;
var widgetStateStore = {};
var widgetConfigStore = {};
function writeWidget(widget) {
if (widget.children.length) {
// Depth-first search (children should be initialized before parent)
writeWidgets(widget.children);
}
if (commaRequired) {
ids += ',';
} else {
commaRequired = true;
}
var widgetConfig = widget.config;
if (widgetConfig) {
widgetConfigStore[widget.id] = widgetConfig;
}
var widgetState = widget.state;
if (widgetState) {
widgetStateStore[widget.id] = widgetState;
}
ids += widget.id;
}
function writeWidgets(widgets) {
for (var i = 0, len = widgets.length; i < len; i++) {
writeWidget(widgets[i]);
}
}
writeWidgets(widgets);
return {
ids: ids,
state: widgetStateStore,
config: widgetConfigStore
};
}
exports.getInitWidgetsCode = function(widgetsContext) {
var renderedWidgets = getRenderedWidgets(widgetsContext);
var ids = renderedWidgets.ids;
var state = renderedWidgets.state;
var config = renderedWidgets.config;
var code = '';
if (!isObjectEmpty(state)) {
code += warp10.serialize(
state,
WARP10_STATE_SERIALIZATION_OPTIONS) +
';\n';
}
if (!isObjectEmpty(config)) {
code += warp10.serialize(
config,
WARP10_CONFIG_SERIALIZATION_OPTIONS) +
';\n';
}
code += '$markoWidgets("' + ids + '");';
return code;
};
/**
* Returns an object that can be sent to the browser using JSON.stringify. The parsed object should be
* passed to require('marko-widgets').initWidgets(...);
*
* @param {WidgetsContext|AsyncWriter} widgetsContext A WidgetsContext or an AsyncWriter
* @return {Object} An object with information about the rendered widgets that can be serialized to JSON. The object should be treated as opaque
*/
exports.getRenderedWidgets = exports.getRenderedWidgetIds /* deprecated */ = function(widgetsContext) {
var renderedWidgets = getRenderedWidgets(widgetsContext);
var ids = renderedWidgets.ids;
var state = renderedWidgets.state;
var config = renderedWidgets.config;
var result = {
ids: ids
};
// NOTE: Calling warp10.stringifyPrepare(obj) will produce a new object that is safe to serializing using
// JSON.stringify(). The deserialized/parsed object will need to be converted to the final object using
// warp10.finalize(obj)
if (!isObjectEmpty(state)) {
result.state = warp10.stringifyPrepare(state);
}
if (!isObjectEmpty(config)) {
result.config = warp10.stringifyPrepare(config);
}
return result;
};
exports.defineComponent = require('./defineComponent');
exports.defineWidget = require('./defineWidget');
exports.defineRenderer = require('./defineRenderer');
exports.makeRenderable = exports.renderable = require('./renderable');
exports.c = function(component, template) {
component.template = template;
return exports.defineComponent(component);
};
// registerWidget is a no-op on the server.
// Fixes https://github.com/marko-js/marko-widgets/issues/111
exports.registerWidget = function() {};

View File

@ -0,0 +1,396 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
require('raptor-polyfill/array/forEach');
require('raptor-polyfill/string/endsWith');
var logger = require('raptor-logging').logger(module);
var ready = require('marko-dom').ready;
var _addEventListener = require('./addEventListener');
var registry = require('./registry');
var warp10Finalize = require('warp10/finalize');
var eventDelegation = require('./event-delegation');
var defaultDocument = typeof document != 'undefined' && document;
var markoWidgets = require('../');
function invokeWidgetEventHandler(widget, targetMethodName, args) {
var method = widget[targetMethodName];
if (!method) {
throw new Error('Widget ' + widget.id + ' does not have method named "' + targetMethodName + '"');
}
method.apply(widget, args);
}
function addDOMEventListener(widget, el, eventType, targetMethodName) {
return _addEventListener(el, eventType, function(event) {
invokeWidgetEventHandler(widget, targetMethodName, [event, el]);
});
}
function getNestedEl(widget, nestedId, doc) {
if (nestedId == null) {
return null;
}
if (nestedId === '') {
return widget.getEl();
}
if (typeof nestedId === 'string' && nestedId.charAt(0) === '#') {
return doc.getElementById(nestedId.substring(1));
} else {
return widget.getEl(nestedId);
}
}
function initWidget(
type,
id,
config,
state,
scope,
domEvents,
customEvents,
extendList,
bodyElId,
existingWidget,
el,
doc) {
var i;
var len;
var eventType;
var targetMethodName;
var widget;
if (!el) {
el = doc.getElementById(id);
}
if (!existingWidget) {
existingWidget = el.__widget;
}
if (existingWidget && existingWidget.__type !== type) {
existingWidget = null;
}
if (existingWidget) {
existingWidget._removeDOMEventListeners();
existingWidget._reset();
widget = existingWidget;
} else {
widget = registry.createWidget(type, id, doc);
}
if (state) {
for (var k in state) {
if (state.hasOwnProperty(k)) {
var v = state[k];
if (typeof v === 'function' || v == null) {
delete state[k];
}
}
}
}
widget.state = state || {}; // First time rendering so use the provided state or an empty state object
// The user-provided constructor function
if (logger.isDebugEnabled()) {
logger.debug('Creating widget: ' + type + ' (' + id + ')');
}
if (!config) {
config = {};
}
el.__widget = widget;
if (widget._isWidget) {
widget.el = el;
widget.bodyEl = getNestedEl(widget, bodyElId, doc);
if (domEvents) {
var eventListenerHandles = [];
for (i=0, len=domEvents.length; i<len; i+=3) {
eventType = domEvents[i];
targetMethodName = domEvents[i+1];
var eventElId = domEvents[i+2];
var eventEl = getNestedEl(widget, eventElId, doc);
// The event mapping is for a DOM event (not a custom event)
var eventListenerHandle = addDOMEventListener(widget, eventEl, eventType, targetMethodName);
eventListenerHandles.push(eventListenerHandle);
}
if (eventListenerHandles.length) {
widget.__evHandles = eventListenerHandles;
}
}
if (customEvents) {
widget.__customEvents = {};
widget.__scope = scope;
for (i=0, len=customEvents.length; i<len; i+=2) {
eventType = customEvents[i];
targetMethodName = customEvents[i+1];
widget.__customEvents[eventType] = targetMethodName;
}
}
if (extendList) {
// If one or more "w-extend" attributes were used for this
// widget then call those modules to now extend the widget
// that we created
for (i=0, len=extendList.length; i<len; i++) {
var extendType = extendList[i];
if (!existingWidget) {
// Only extend a widget the first time the widget is created. If we are updating
// an existing widget then we don't re-extend it
var extendModule = registry.load(extendType);
var extendFunc = extendModule.extendWidget || extendModule.extend;
if (typeof extendFunc !== 'function') {
throw new Error('extendWidget(widget, cfg) method missing: ' + extendType);
}
extendFunc(widget);
}
}
}
} else {
config.elId = id;
config.el = el;
}
if (existingWidget) {
widget._emitLifecycleEvent('update');
widget._emitLifecycleEvent('render', {});
} else {
var initEventArgs = {
widget: widget,
config: config
};
markoWidgets.emit('marko-widgets/initWidget', initEventArgs);
widget._emitLifecycleEvent('beforeInit', initEventArgs);
widget.initWidget(config);
widget._emitLifecycleEvent('afterInit', initEventArgs);
widget._emitLifecycleEvent('render', { firstRender: true });
}
return widget;
}
function initWidgetFromEl(el, state, config) {
if (el.__widget != null) {
// A widget is already bound to this element. Nothing to do...
return;
}
var doc = el.ownerDocument;
var scope;
var id = el.id;
var type = el.getAttribute('data-widget');
el.removeAttribute('data-widget');
var domEvents;
var hasDomEvents = el.getAttribute('data-w-on');
if (hasDomEvents) {
var domEventsEl = doc.getElementById(id + '-$on');
if (domEventsEl) {
domEventsEl.parentNode.removeChild(domEventsEl);
domEvents = (domEventsEl.getAttribute('data-on') || '').split(',');
}
el.removeAttribute('data-w-on');
}
var customEvents = el.getAttribute('data-w-events');
if (customEvents) {
customEvents = customEvents.split(',');
scope = customEvents[0];
customEvents = customEvents.slice(1);
el.removeAttribute('data-w-events');
}
var extendList = el.getAttribute('data-w-extend');
if (extendList) {
extendList = extendList.split(',');
el.removeAttribute('data-w-extend');
}
var bodyElId = el.getAttribute('data-w-body');
initWidget(
type,
id,
config,
state,
scope,
domEvents,
customEvents,
extendList,
bodyElId,
null,
el,
doc);
}
// Create a helper function handle recursion
function initClientRendered(widgetDefs, doc) {
// Ensure that event handlers to handle delegating events are
// always attached before initializing any widgets
eventDelegation.init();
doc = doc || window.doc;
for (var i=0,len=widgetDefs.length; i<len; i++) {
var widgetDef = widgetDefs[i];
if (widgetDef.children.length) {
initClientRendered(widgetDef.children, doc);
}
var widget = initWidget(
widgetDef.type,
widgetDef.id,
widgetDef.config,
widgetDef.state,
widgetDef.scope,
widgetDef.domEvents,
widgetDef.customEvents,
widgetDef.extend,
widgetDef.bodyElId,
widgetDef.existingWidget,
null,
doc);
widgetDef.widget = widget;
}
}
/**
* This method is used to initialized widgets associated with UI components
* rendered in the browser. While rendering UI components a "widgets context"
* is added to the rendering context to keep up with which widgets are rendered.
* When ready, the widgets can then be initialized by walking the widget tree
* in the widgets context (nested widgets are initialized before ancestor widgets).
* @param {Array<marko-widgets/lib/WidgetDef>} widgetDefs An array of WidgetDef instances
*/
exports.initClientRendered = initClientRendered;
/**
* This method initializes all widgets that were rendered on the server by iterating over all
* of the widget IDs. This method supports two signatures:
*
* initServerRendered(dataIds : String) - dataIds is a comma separated list of widget IDs. The state and config come
* from the following globals:
* - window.$markoWidgetsState
* - window.$markoWidgetsConfig
* initServerRendered(renderedWidgets : Object) - dataIds is an object rendered by getRenderedWidgets with the following
* structure:
* {
* ids: "w0,w1,w2",
* state: { w0: {...}, ... }
* config: { w0: {...}, ... }
* }
*/
exports.initServerRendered = function(dataIds, doc) {
var stateStore;
var configStore;
if (!doc) {
doc = defaultDocument;
}
if (typeof dataIds === 'object') {
stateStore = dataIds.state ? warp10Finalize(dataIds.state) : null;
configStore = dataIds.config ? warp10Finalize(dataIds.config) : null;
dataIds = dataIds.ids;
}
function doInit() {
// Ensure that event handlers to handle delegating events are
// always attached before initializing any widgets
eventDelegation.init();
if (typeof dataIds !== 'string') {
var idsEl = doc.getElementById('markoWidgets');
if (!idsEl) { // If there is no index then do nothing
return;
}
// Make sure widgets are only initialized once by checking a flag
if (doc.markoWidgetsInitialized === true) {
return;
}
// Set flag to avoid trying to do this multiple times
doc.markoWidgetsInitialized = true;
dataIds = idsEl ? idsEl.getAttribute('data-ids') : null;
}
if (dataIds) {
stateStore = stateStore || window.$markoWidgetsState;
configStore = configStore || window.$markoWidgetsConfig;
// W have a comma-separated of widget element IDs that need to be initialized
var ids = dataIds.split(',');
var len = ids.length;
var state;
var config;
for (var i=0; i<len; i++) {
var id = ids[i];
var el = doc.getElementById(id);
if (!el) {
throw new Error('DOM node for widget with ID "' + id + '" not found');
}
if (stateStore) {
state = stateStore[id];
delete stateStore[id];
} else {
state = undefined;
}
if (configStore) {
config = configStore[id];
delete configStore[id];
} else {
config = undefined;
}
initWidgetFromEl(el, state, config);
}
}
}
if (typeof dataIds === 'string') {
doInit();
} else {
ready(doInit, null, doc);
}
};

View File

@ -0,0 +1 @@
// The server-side implementation of this module is intentionally empty

98
widgets/src/registry.js Normal file
View File

@ -0,0 +1,98 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var registered = {};
var loaded = {};
var widgetTypes = {};
var defineWidget;
var defineRenderer;
exports.register = function(typeName, type) {
if (arguments.length === 1) {
var widgetType = arguments[0];
typeName = widgetType.name;
type = widgetType.def();
}
registered[typeName] = type;
delete loaded[typeName];
delete widgetTypes[typeName];
};
function load(typeName) {
var target = loaded[typeName];
if (target === undefined) {
target = registered[typeName];
if (!target) {
target = require(typeName); // Assume the typeName has been fully resolved already
}
loaded[typeName] = target || null;
}
if (target == null) {
throw new Error('Unable to load: ' + typeName);
}
return target;
}
function getWidgetClass(typeName) {
var WidgetClass = widgetTypes[typeName];
if (WidgetClass) {
return WidgetClass;
}
WidgetClass = load(typeName);
var renderer;
if (WidgetClass.Widget) {
WidgetClass = WidgetClass.Widget;
}
if (WidgetClass.renderer) {
renderer = defineRenderer(WidgetClass);
}
if (!WidgetClass._isWidget) {
WidgetClass = defineWidget(WidgetClass, renderer);
}
// Make the widget "type" accessible on each widget instance
WidgetClass.prototype.__type = typeName;
widgetTypes[typeName] = WidgetClass;
return WidgetClass;
}
exports.load = load;
exports.createWidget = function(typeName, id, document) {
var WidgetClass = getWidgetClass(typeName);
var widget;
if (typeof WidgetClass === 'function') {
// The widget is a constructor function that we can invoke to create a new instance of the widget
widget = new WidgetClass(id, document);
} else if (WidgetClass.initWidget) {
widget = WidgetClass;
widget.__document = document;
}
return widget;
};
defineWidget = require('./defineWidget');
defineRenderer = require('./defineRenderer');

13
widgets/src/renderable.js Normal file
View File

@ -0,0 +1,13 @@
var marko = require('marko');
module.exports = function(target, renderer) {
var rendererFunc = renderer.renderer || renderer.render || renderer;
var createOut = renderer.createOut || marko.createOut;
target.renderer = rendererFunc;
target.render = function(input) {
var out = createOut();
rendererFunc(input, out);
return out.end();
};
};

View File

@ -0,0 +1,42 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
function RepeatedId() {
this.nextIdLookup = {};
}
RepeatedId.prototype = {
nextId: function(parentId, id) {
var indexLookupKey = parentId + '-' + id;
var currentIndex = this.nextIdLookup[indexLookupKey];
if (currentIndex == null) {
currentIndex = this.nextIdLookup[indexLookupKey] = 0;
} else {
currentIndex = ++this.nextIdLookup[indexLookupKey];
}
return indexLookupKey.slice(0, -2) + '[' + currentIndex + ']';
}
};
exports.nextId = function(out, parentId, id) {
var repeatedId = out.global.__repeatedId;
if (repeatedId == null) {
repeatedId = out.global.__repeatedId = new RepeatedId();
}
return repeatedId.nextId(parentId, id);
};

View File

@ -0,0 +1,24 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var uniqueId = window.MARKO_WIDGETS_UNIQUE_ID;
if (!uniqueId) {
var _nextUniqueId = 0;
window.MARKO_WIDGETS_UNIQUE_ID = uniqueId = function() {
return 'wc' + (_nextUniqueId++);
};
}
module.exports = uniqueId;

36
widgets/src/uniqueId.js Normal file
View File

@ -0,0 +1,36 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
function IdProvider(out) {
var global = this.global = out.global;
this.prefix = global.widgetIdPrefix || 'w';
if (global._nextWidgetId == null) {
global._nextWidgetId = 0;
}
}
IdProvider.prototype.nextId = function() {
return this.prefix + (this.global._nextWidgetId++);
};
module.exports = function (out) {
var global = out.global;
var idProvider = global._widgetIdProvider ||
(global._widgetIdProvider = new IdProvider(out));
return idProvider.nextId();
};

View File

@ -0,0 +1,161 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var AsyncValue = require('raptor-async/AsyncValue');
var afterUpdateAsyncValue = null;
var afterUpdateAsyncValue = null;
var updatesScheduled = false;
var batchStack = []; // A stack of batched updates
var unbatchedQueue = []; // Used for scheduled batched updates
/**
* This function is called when we schedule the update of "unbatched"
* updates to widgets.
*/
function updateUnbatchedWidgets() {
if (!unbatchedQueue.length) {
// No widgets to update
return;
}
try {
updateWidgets(unbatchedQueue);
} finally {
// Reset the flag now that this scheduled batch update
// is complete so that we can later schedule another
// batched update if needed
updatesScheduled = false;
}
}
function scheduleUpdates() {
if (updatesScheduled) {
// We have already scheduled a batched update for the
// process.nextTick so nothing to do
return;
}
updatesScheduled = true;
process.nextTick(updateUnbatchedWidgets);
}
function onAfterUpdate(callback) {
scheduleUpdates();
if (!afterUpdateAsyncValue) {
afterUpdateAsyncValue = new AsyncValue();
}
afterUpdateAsyncValue.done(callback);
}
function updateWidgets(queue) {
// Loop over the widgets in the queue and update them.
// NOTE: Is it okay if the queue grows during the iteration
// since we will still get to them at the end
for (var i=0; i<queue.length; i++) {
var widget = queue[i];
widget.__updateQueued = false; // Reset the "__updateQueued" flag
widget.update(); // Do the actual widget update
}
// Clear out the queue by setting the length to zero
queue.length = 0;
}
function batchUpdate(func) {
// If the batched update stack is empty then this
// is the outer batched update. After the outer
// batched update completes we invoke the "afterUpdate"
// event listeners.
var isOuter = batchStack.length === 0;
var batch = {
queue: null
};
batchStack.push(batch);
try {
func();
} finally {
try {
// Update all of the widgets that where queued up
// in this batch (if any)
if (batch.queue) {
updateWidgets(batch.queue);
}
} finally {
// Now that we have completed the update of all the widgets
// in this batch we need to remove it off the top of the stack
batchStack.length--;
if (isOuter) {
// If there were any listeners for the "afterUpdate" event
// then notify those listeners now
if (afterUpdateAsyncValue) {
afterUpdateAsyncValue.resolve();
afterUpdateAsyncValue = null;
}
}
}
}
}
function queueWidgetUpdate(widget) {
if (widget.__updateQueued) {
// The widget has already been queued up for an update. Once
// the widget has actually been updated we will reset the
// "__updateQueued" flag so that it can be queued up again.
// Since the widget has already been queued up there is nothing
// that needs to be done.
return;
}
widget.__updateQueued = true;
var batchStackLen = batchStack.length;
if (batchStackLen) {
// When a batch update is started we push a new batch on to a stack.
// If the stack has a non-zero length then we know that a batch has
// been started so we can just queue the widget on the top batch. When
// the batch is ended this widget will be updated.
var batch = batchStack[batchStackLen-1];
// We default the batch queue to null to avoid creating an Array instance
// unnecessarily. If it is null then we create a new Array, otherwise
// we push it onto the existing Array queue
if (batch.queue) {
batch.queue.push(widget);
} else {
batch.queue = [widget];
}
} else {
// We are not within a batched update. We need to schedule a batch update
// for the process.nextTick (if that hasn't been done already) and we will
// add the widget to the unbatched queued
scheduleUpdates();
unbatchedQueue.push(widget);
}
}
exports.queueWidgetUpdate = queueWidgetUpdate;
exports.batchUpdate = batchUpdate;
exports.onAfterUpdate = onAfterUpdate;

View File

@ -0,0 +1,40 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var repeatedId = require('./repeated-id');
module.exports = function widgetArgsId(widgetArgs) {
var widgetId = widgetArgs.id;
if (widgetId) {
var out = widgetArgs.out;
var scope = widgetArgs.scope;
if (widgetId.charAt(0) === '#') {
return widgetId.substring(1);
} else {
var resolvedId;
if (widgetId.endsWith('[]')) {
resolvedId = repeatedId.nextId(out, scope, widgetId);
} else {
resolvedId = scope + '-' + widgetId;
}
return resolvedId;
}
}
};

View File

@ -0,0 +1,143 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
var getRequirePath = require('../getRequirePath');
class WidgetArgs {
constructor() {
this.id = null;
this.customEvents = null;
this.extend = null;
this.extendConfig = null;
this.extendState = null;
this.empty = true;
}
setId(id) {
this.empty = false;
this.id = id;
}
getId() {
return this.id;
}
addCustomEvent(eventType, targetMethod) {
this.empty = false;
if (!this.customEvents) {
this.customEvents = [];
}
this.customEvents.push(eventType);
this.customEvents.push(targetMethod);
}
setExtend(extendType, extendConfig, extendState) {
this.empty = false;
this.extend = extendType;
this.extendConfig = extendConfig;
this.extendState = extendState;
}
compile(transformHelper) {
if (this.empty) {
return;
}
var el = transformHelper.el;
let widgetArgsFunctionCall = this.buildWidgetArgsFunctionCall(transformHelper);
let cleanupWidgetArgsFunctionCall = this.buildCleanupWidgetArgsFunctionCall(transformHelper);
el.onBeforeGenerateCode((event) => {
event.insertCode(widgetArgsFunctionCall);
});
el.onAfterGenerateCode((event) => {
event.insertCode(cleanupWidgetArgsFunctionCall);
});
}
buildWidgetArgsFunctionCall(transformHelper) {
var context = transformHelper.context;
var builder = transformHelper.builder;
var id = this.id;
var customEvents = this.customEvents;
var extend = this.extend;
var extendConfig = this.extendConfig;
var extendState = this.extendState;
// Make sure the nested widget has access to the ID of the containing
// widget if it is needed
var shouldProvideScope = id || customEvents;
let widgetArgsVar = context.addStaticVar('__widgetArgs',
'require("' + getRequirePath('marko-widgets/taglib/helpers/widgetArgs', context) + '")');
var functionCallArgs = [
builder.identifier('out')
];
if (shouldProvideScope) {
functionCallArgs.push(builder.memberExpression(
builder.identifier('widget'),
builder.identifier('id')
));
} else {
functionCallArgs.push(builder.literalNull());
}
if (id != null) {
functionCallArgs.push(id);
} else {
functionCallArgs.push(builder.literalNull());
}
if (customEvents) {
functionCallArgs.push(builder.literal(customEvents));
}
if (extend) {
if (!customEvents) {
functionCallArgs.push(builder.literalNull());
}
functionCallArgs.push(extend);
functionCallArgs.push(extendConfig || builder.literalNull());
functionCallArgs.push(extendState || builder.literalNull());
}
return builder.functionCall(widgetArgsVar, functionCallArgs);
}
buildCleanupWidgetArgsFunctionCall(transformHelper) {
var context = transformHelper.context;
var builder = transformHelper.builder;
var cleanupWidgetArgsVar = context.addStaticVar('_cleanupWidgetArgs',
'__widgetArgs.cleanup');
return builder.functionCall(cleanupWidgetArgsVar, [builder.identifierOut()]);
}
}
module.exports = WidgetArgs;

View File

@ -0,0 +1,152 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
module.exports = function assignWidgetId(isRepeated) {
// First check if we have already assigned an ID to thie element
var widgetIdInfo = this.widgetIdInfo;
if (widgetIdInfo) {
return this.widgetIdInfo;
}
var el = this.el;
var context = this.context;
var builder = this.builder;
var nestedIdExpression;
var idExpression;
var containingWidgetNode = this.getContainingWidgetNode();
if (!containingWidgetNode) {
// We are assigning a widget ID to a nested widget in a template that does not have a widget.
// That means we do not have access to the parent widget variable as part of a closure. We
// need to look it up out of the `out.data` map
if (!context.isFlagSet('hasWidgetVar')) {
context.setFlag('hasWidgetVar');
var getCurrentWidgetVar = context.importModule('__getCurrentWidget',
this.getMarkoWidgetsRequirePath('marko-widgets/taglib/helpers/getCurrentWidget'));
context.addVar('widget', builder.functionCall(getCurrentWidgetVar, [builder.identifierOut()]));
}
}
// In order to attach a DOM event listener directly we need to make sure
// the target HTML element has an ID that we can use to get a reference
// to the element during initialization. We generate this unique ID
// at compile-time to allow consistent IDs during rendering.
// We need to handle the following scenarios:
//
// 1) The HTML element already has an "id" attribute
// 2) The HTML element has a "w-id" attribute (we already converted this
// to an "id" attribute above)
// 3) The HTML does not have an "id" or "w-el-id" attribute. We must add
// an "id" attribute with a unique ID.
var isCustomTag = el.type !== 'HtmlElement';
if (el.hasAttribute('w-id')) {
let widgetId = el.getAttributeValue('w-id');
el.removeAttribute('w-id');
idExpression = this.buildWidgetElIdFunctionCall(widgetId);
nestedIdExpression = widgetId;
if (isCustomTag) {
// The element is a custom tag
this.getWidgetArgs().setId(nestedIdExpression);
} else {
if (el.hasAttribute('id')) {
this.addError('The "w-id" attribute cannot be used in conjuction with the "id" attribute');
return;
}
el.setAttributeValue('id', idExpression);
}
} else if (el.hasAttribute('id')) {
idExpression = el.getAttributeValue('id');
if (el.isFlagSet('hasWidgetBind') || el.isFlagSet('hasWidgetExtend')) {
// We have to attach a listener to the root element of the widget
// We will use an empty string as an indicator that it is the root widget
// element.
nestedIdExpression = builder.literal('');
} else {
// Convert the raw String to a JavaScript expression. we need to prefix
// with '#' to make it clear this is a fully resolved element ID
nestedIdExpression = builder.concat(
builder.literal('#'),
idExpression);
}
} else {
// Case 3 - We need to add a unique "id" attribute
let uniqueElId = this.nextUniqueId();
nestedIdExpression = isRepeated ? builder.literal(uniqueElId + '[]') : builder.literal(uniqueElId);
idExpression = this.buildWidgetElIdFunctionCall(nestedIdExpression);
if (isCustomTag) {
this.getWidgetArgs().setId(nestedIdExpression);
} else {
el.setAttributeValue('id', idExpression);
}
}
var transformHelper = this;
this.widgetIdInfo = {
idExpression: idExpression,
nestedIdExpression: nestedIdExpression,
idVarNode: null,
createIdVarNode: function() {
if (this.idVarNode) {
return this.idVarNode;
}
let uniqueElId = transformHelper.nextUniqueId();
let idVarName = '__widgetId' + uniqueElId;
let idVar = builder.identifier(idVarName);
this.idVarNode = builder.vars([
{
id: idVarName,
init: idExpression
}
]);
this.idExpression = idExpression = idVar;
this.nestedIdExpression = nestedIdExpression = builder.concat(
builder.literal('#'),
idVar);
if (isCustomTag) {
transformHelper.getWidgetArgs().setId(nestedIdExpression);
} else {
el.setAttributeValue('id', idExpression);
}
return this.idVarNode;
}
};
return this.widgetIdInfo;
};

View File

@ -0,0 +1,45 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
function getContainingWidgetNode() {
if (this.containingWidgetNode !== undefined) {
return this.containingWidgetNode;
}
var curNode = this.el;
while (true) {
if (curNode.tagName === 'w-widget') {
this.containingWidgetNode = curNode;
return this.containingWidgetNode;
} else if (curNode.isFlagSet('hasWidgetExtend')) {
this.containingWidgetNode = curNode;
return this.containingWidgetNode;
}
curNode = curNode.parentNode;
if (!curNode) {
break;
}
}
this.containingWidgetNode = null;
return null;
}
module.exports = getContainingWidgetNode;

View File

@ -0,0 +1,148 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
var path = require('path');
module.exports = function handleWidgetBind() {
var el = this.el;
var context = this.context;
var builder = this.builder;
var bindAttr = el.getAttribute('w-bind');
if (bindAttr == null) {
return;
}
// A widget is bound to the el...
// Remove the w-bind attribute since we don't want it showing up in the output DOM
el.removeAttribute('w-bind');
// Read the value for the w-bind attribute. This will be an AST node for the parsed JavaScript
var bindAttrValue = bindAttr.value;
var modulePath;
var widgetAttrs = {};
if (bindAttrValue == null) {
modulePath = this.getDefaultWidgetModule();
if (!modulePath) {
this.addError('Invalid "w-bind" attribute. No corresponding JavaScript module found in the same directory (either "widget.js" or "index.js"). Actual: ' + modulePath);
return;
}
} else if (bindAttr.isLiteralValue()) {
modulePath = bindAttr.literalValue; // The value of the literal value
if (typeof modulePath !== 'string') {
this.addError('The value for the "w-bind" attribute should be a string. Actual: ' + modulePath);
return;
}
} else {
// This is a dynamic expression. The <widget-types> should have been found.
if (!context.isFlagSet('hasWidgetTypes')) {
this.addError('The <widget-types> tag must be used to declare widgets when the value of the "w-bind" attribute is a dynamic expression.');
return;
}
widgetAttrs.type = builder.computedMemberExpression(
builder.identifier('__widgetTypes'),
bindAttrValue);
}
if (modulePath) {
let widgetTypeNode;
if(path.basename(modulePath) === 'component') {
widgetTypeNode = context.addStaticVar('__widgetType', builder.literal({
name: builder.literal(modulePath),
def: builder.functionDeclaration(null, [] /* params */, [
builder.returnStatement(
builder.memberExpression('module', 'exports')
)
])
}));
this.context.on('beforeGenerateCode:TemplateRoot', function(root) {
root.node.generateExports = function(template) {
return buildExport(builder, modulePath, template);
};
});
} else {
widgetTypeNode = context.addStaticVar('__widgetType', this.buildWidgetTypeNode(modulePath));
}
widgetAttrs.type = widgetTypeNode;
}
var id = el.getAttributeValue('id');
if (el.hasAttribute('w-config')) {
widgetAttrs.config = el.getAttributeValue('w-config');
el.removeAttribute('w-config');
}
if (id) {
widgetAttrs.id = id;
}
var widgetNode = context.createNodeForEl('w-widget', widgetAttrs);
el.wrapWith(widgetNode);
el.setAttributeValue('id', builder.memberExpression(builder.identifier('widget'), builder.identifier('id')));
// var _widgetAttrs = __markoWidgets.attrs;
var widgetAttrsVar = context.addStaticVar('__widgetAttrs',
builder.memberExpression(this.markoWidgetsVar, builder.identifier('attrs')));
el.addDynamicAttributes(builder.functionCall(widgetAttrsVar, [ builder.identifier('widget') ]));
this.widgetStack.push({
widgetNode: widgetNode,
el: el,
extend: false
});
};
function buildExport(builder, modulePath, template) {
return [
builder.assignment(
builder.var('component'),
builder.require(
builder.literal(modulePath)
)
),
builder.assignment(
builder.var('template'),
template
),
builder.assignment(
builder.memberExpression(
builder.identifier('module'),
builder.identifier('exports')
),
builder.functionCall(
builder.memberExpression(
builder.require(
builder.literal('marko-widgets')
),
builder.identifier('c')
),
[
builder.identifier('component'),
builder.identifier('template')
]
)
)
];
}

View File

@ -0,0 +1,67 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
module.exports = function handleWidgetBody() {
let el = this.el;
if (!el.hasAttribute('w-body')) {
return;
}
let context = this.context;
let builder = this.builder;
let widgetTagNode = this.getContainingWidgetNode();
if (!widgetTagNode) {
this.addError('w-body can only be used within the scope of w-bind');
return;
}
let widgetBodyExpression = el.getAttributeValue('w-body');
el.removeAttribute('w-body');
if (widgetBodyExpression) {
var widgetIdInfo = this.assignWidgetId(true /* repeated */);
if (!widgetIdInfo.idVarNode) {
let idVarNode = widgetIdInfo.createIdVarNode();
el.onBeforeGenerateCode((event) => {
event.insertCode(idVarNode);
});
}
} else {
this.assignWidgetId(false /* not repeated */);
widgetBodyExpression = builder.memberExpression(
builder.identifier('data'),
builder.identifier('widgetBody')
);
widgetTagNode.setAttributeValue('body', this.getNestedIdExpression());
}
let widgetBodyVar = context.importModule('__widgetBody', this.getMarkoWidgetsRequirePath('marko-widgets/taglib/helpers/widgetBody'));
let widgetBodyFunctionCall = builder.functionCall(widgetBodyVar, [
builder.identifierOut(),
this.getIdExpression(),
widgetBodyExpression,
builder.identifier('widget')
]);
el.appendChild(widgetBodyFunctionCall);
};

View File

@ -0,0 +1,177 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var bubbleEventsLookup = {};
require('../../src/bubble').forEach(function(eventType) {
bubbleEventsLookup[eventType] = true;
});
function isBubbleEvent(eventType) {
return bubbleEventsLookup.hasOwnProperty(eventType);
}
function isUpperCase(c) {
return c == c.toUpperCase();
}
function addBubblingEventListener(transformHelper, eventType, targetMethod) {
var containingWidgetNode = transformHelper.getContainingWidgetNode();
var el = transformHelper.el;
if (!containingWidgetNode) {
transformHelper.addError('Unable to handle event "' + eventType + '". HTML element is not nested within a widget.');
return;
}
var builder = transformHelper.builder;
var attrValue;
var widgetIdExpression = builder.memberExpression(
builder.identifier('widget'),
builder.identifier('id'));
if (targetMethod.type === 'Literal') {
// We know the event handler is method is no conditional so we set the attribute correctly at compile time
attrValue = builder.concat(
targetMethod,
builder.literal('|'),
widgetIdExpression);
} else {
// The event handler method is conditional and it may resolve to a null method name. Therefore,
// we need to use a runtime helper to set the value correctly.
var markoWidgetsEventFuncId = transformHelper.context.importModule('markoWidgets_event',
transformHelper.getMarkoWidgetsRequirePath('marko-widgets/taglib/helpers/event'));
attrValue = builder.functionCall(markoWidgetsEventFuncId, [
targetMethod,
widgetIdExpression
]);
}
el.setAttributeValue('data-w-on' + eventType.value, attrValue);
}
function addDirectEventListener(transformHelper, eventType, targetMethod) {
var builder = transformHelper.builder;
var el = transformHelper.el;
var addDomEvent = builder.memberExpression(
builder.identifier('widget'),
builder.identifier('addDomEvent'));
var addDomEventFunctionCall = builder.functionCall(
addDomEvent,
[
eventType,
targetMethod,
transformHelper.getNestedIdExpression()
]);
el.onBeforeGenerateCode((event) => {
event.insertCode(addDomEventFunctionCall);
});
// Also add another DOM element that will be used to
var containingWidgetNode = transformHelper.getContainingWidgetNode();
containingWidgetNode.setAttributeValue('hasDomEvents', builder.literal(1));
}
function addCustomEventListener(transformHelper, eventType, targetMethod) {
// Make sure the widget has an assigned scope ID so that we can bind the custom event listener
var widgetArgs = transformHelper.getWidgetArgs();
widgetArgs.addCustomEvent(eventType, targetMethod);
}
module.exports = function handleWidgetEvents() {
var el = this.el;
var builder = this.builder;
var isCustomTag = el.type !== 'HtmlElement';
// We configured the Marko compiler to attach a flag to nodes that
// have one or more attributes that match the "w-on*" pattern.
// We still need to loop over the properties to find and handle
// the properties corresponding to those attributes.
var hasWidgetEvents = this.el.isFlagSet('hasWidgetEvents') === true;
if (hasWidgetEvents) {
var attrs = el.getAttributes().concat([]);
attrs.forEach((attr) => {
var attrName = attr.name;
if (!attrName || !attrName.startsWith('w-on')) {
return;
}
el.removeAttribute(attrName);
var eventType = attrName.substring(4); // Chop off "w-on"
var targetMethod = attr.value;
if (isCustomTag) {
var widgetArgs = this.getWidgetArgs();
if (widgetArgs.getId() == null) {
widgetArgs.setId(builder.literal(this.nextUniqueId()));
}
// We are adding an event listener for a custom event (not a DOM event)
if (eventType.startsWith('-')) {
// Remove the leading dash.
// Example: w-on-before-show → before-show
eventType = eventType.substring(1);
} else if (isUpperCase(eventType.charAt(0))) {
// Convert first character to lower case:
// Example: w-onBeforeShow → beforeShow
eventType = eventType.charAt(0).toLowerCase() + eventType.substring(1);
}
eventType = builder.literal(eventType);
// Node is for a custom tag
addCustomEventListener(this, eventType, targetMethod);
} else {
// We are adding an event listener for a DOM event (not a custom event)
//
if (eventType.startsWith('-')) {
// Remove the leading dash.
// Example: w-on-before-show → before-show
eventType = eventType.substring(1);
}
// Normalize DOM event types to be all lower case
eventType = eventType.toLowerCase();
// Node is for an HTML element so treat the event as a DOM event
var willBubble = isBubbleEvent(eventType);
eventType = builder.literal(eventType);
if (willBubble) {
// The event is white listed for bubbling so we know that
// we have already attached a listener on document.body
// that can be used to handle the event. We will add
// a "data-w-on{eventType}" attribute to the output HTML
// for this element that will be used to map the event
// to a method on the containing widget.
addBubblingEventListener(this, eventType, targetMethod);
} else {
// The event does not bubble so we must attach a DOM
// event listener directly to the target element.
addDirectEventListener(this, eventType, targetMethod);
}
}
});
}
};

View File

@ -0,0 +1,83 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
module.exports = function handleWidgetExtend() {
var el = this.el;
if (!el.hasAttribute('w-extend')) {
return;
}
var extendAttr = el.getAttribute('w-extend');
var modulePath;
var widgetArgs = this.getWidgetArgs();
var context = this.context;
var builder = this.builder;
if (extendAttr.value == null) {
modulePath = this.getDefaultWidgetModule();
if (!modulePath) {
this.addError('Unable to find default widget module when using w-extend without a value');
return;
}
} else if (extendAttr.isLiteralValue()) {
modulePath = extendAttr.literalValue; // The value of the literal value
if (typeof modulePath !== 'string') {
this.addError('The value for the "w-extend" attribute should be a string.');
return;
}
} else {
throw new Error('Not yet implemented');
}
this.widgetStack.push({
el: el,
extend: true
});
el.addNestedVariable('widget');
var extendConfig = el.getAttributeValue('w-config');
if (!extendConfig) {
// extendConfig = data.widgetConfig
extendConfig = builder.memberExpression(
builder.identifier('data'),
builder.identifier('widgetConfig'));
}
var extendState = el.getAttributeValue('w-state');
if (!extendState) {
// extendState = data.widgetState
extendState = builder.memberExpression(
builder.identifier('data'),
builder.identifier('widgetState'));
}
var widgetTypeVar = context.addStaticVar('__widgetType', this.buildWidgetTypeNode(modulePath));
widgetArgs.setExtend(
widgetTypeVar,
extendConfig,
extendState);
// Do some cleanup
el.removeAttribute('w-extend');
el.removeAttribute('w-config');
el.removeAttribute('w-state');
};

View File

@ -0,0 +1,36 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
module.exports = function handleWidgetFor() {
var el = this.el;
var widgetFor = el.getAttributeValue('w-for');
el.removeAttribute('w-for');
if (widgetFor == null) {
return;
}
// Handle the "w-for" attribute
if (el.hasAttribute('for')) {
this.addError('The "w-for" attribute cannot be used in conjuction with the "for" attribute');
} else {
el.setAttributeValue(
'for',
this.buildWidgetElIdFunctionCall(widgetFor));
}
};

View File

@ -0,0 +1,89 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
function addPreserve(transformHelper, bodyOnly, condition) {
var el = transformHelper.el;
var context = transformHelper.context;
var builder = transformHelper.builder;
var preserveAttrs = {};
if (bodyOnly) {
preserveAttrs['body-only'] = builder.literal(bodyOnly);
}
if (condition) {
preserveAttrs['if'] = condition;
}
var widgetIdInfo = transformHelper.assignWidgetId(true /* repeated */);
var idVarNode = widgetIdInfo.idVarNode ? null : widgetIdInfo.createIdVarNode();
preserveAttrs.id = transformHelper.getIdExpression();
var preserveNode = context.createNodeForEl('w-preserve', preserveAttrs);
var idVarNodeTarget;
if (bodyOnly) {
el.moveChildrenTo(preserveNode);
el.appendChild(preserveNode);
idVarNodeTarget = el;
} else {
el.wrapWith(preserveNode);
idVarNodeTarget = preserveNode;
}
if (idVarNode) {
idVarNodeTarget.onBeforeGenerateCode((event) => {
event.insertCode(idVarNode);
});
}
return preserveNode;
}
module.exports = function handleWidgetPreserve() {
var el = this.el;
if (el.hasAttribute('w-preserve')) {
el.removeAttribute('w-preserve');
addPreserve(this, false);
} else if (el.hasAttribute('w-preserve-if')) {
let preserveIfAttr = el.getAttribute('w-preserve-if');
var preserveIfCondition = preserveIfAttr.argument;
if (!preserveIfCondition) {
this.addError('The `w-preserve-if` attribute should have an argument. For example: <div w-preserve-if(someCondition)>');
return;
}
addPreserve(this, false, this.builder.expression(preserveIfCondition));
el.removeAttribute('w-preserve-if');
} else if (el.hasAttribute('w-preserve-body')) {
el.removeAttribute('w-preserve-body');
addPreserve(this, true);
} else if (el.hasAttribute('w-preserve-body-if')) {
let preserveBodyIfAttr = el.getAttribute('w-preserve-body-if');
var preserveBodyIfCondition = preserveBodyIfAttr.argument;
if (!preserveBodyIfCondition) {
this.addError('The `w-preserve-body-if` attribute should have an argument. For example: <div w-preserve-body-if(someCondition)>');
return;
}
addPreserve(this, true, this.builder.expression(preserveBodyIfCondition));
el.removeAttribute('w-preserve-body-if');
}
};

View File

@ -0,0 +1,24 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
function handleWidgetEvents() {
var el = this.el;
var preserveAttrsExpression = el.getAttributeValue('w-preserve-attrs');
el.removeAttribute('w-preserve-attrs');
el.setAttributeValue('data-preserve-attrs', preserveAttrsExpression);
}
module.exports = handleWidgetEvents;

View File

@ -0,0 +1,121 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
var tryRequire = require('try-require');
var fs = tryRequire('fs', require);
var nodePath = require('path');
var WidgetArgs = require('./WidgetArgs');
var getRequirePath = require('../getRequirePath');
var buildWidgetTypeNode = require('../util/buildWidgetTypeNode');
class TransformHelper {
constructor(el, context) {
this.el = el;
this.context = context;
this.builder = context.builder;
this.dirname = context.dirname;
this.widgetNextElId = 0;
this.widgetIdInfo = undefined;
this.widgetArgs = undefined;
this.containingWidgetNode = undefined;
this._markoWidgetsVar = undefined;
this.widgetStack = context.data.widgetStack || (context.data.widgetStack = []);
}
addError(message, code) {
this.context.addError(this.el, message, code);
}
getWidgetArgs() {
return this.widgetArgs || (this.widgetArgs = new WidgetArgs());
}
nextUniqueId() {
var widgetNextElId = this.context.data.widgetNextElId;
if (widgetNextElId == null) {
this.context.data.widgetNextElId = 0;
}
return (this.context.data.widgetNextElId++);
}
getNestedIdExpression() {
this.assignWidgetId();
return this.getWidgetIdInfo().nestedIdExpression;
}
getIdExpression() {
this.assignWidgetId();
return this.getWidgetIdInfo().idExpression;
}
getWidgetIdInfo() {
return this.widgetIdInfo;
}
getDefaultWidgetModule() {
var dirname = this.dirname;
if (fs.existsSync(nodePath.join(dirname, 'component.js'))) {
return './component';
} else if (fs.existsSync(nodePath.join(dirname, 'widget.js'))) {
return './widget';
} else if (fs.existsSync(nodePath.join(dirname, 'index.js'))) {
return './';
} else {
return null;
}
}
getMarkoWidgetsRequirePath(target) {
return getRequirePath(target, this.context);
}
get markoWidgetsVar() {
if (!this._markoWidgetsVar) {
this._markoWidgetsVar = this.context.importModule('__markoWidgets', this.getMarkoWidgetsRequirePath('marko-widgets'));
}
return this._markoWidgetsVar;
}
buildWidgetElIdFunctionCall(id) {
var builder = this.builder;
var widgetElId = builder.memberExpression(
builder.identifier('widget'),
builder.identifier('elId'));
return builder.functionCall(widgetElId, arguments.length === 0 ? [] : [ id ]);
}
buildWidgetTypeNode(path) {
return buildWidgetTypeNode(path, this.dirname, this.builder);
}
}
TransformHelper.prototype.assignWidgetId = require('./assignWidgetId');
TransformHelper.prototype.getContainingWidgetNode = require('./getContainingWidgetNode');
TransformHelper.prototype.handleWidgetEvents = require('./handleWidgetEvents');
TransformHelper.prototype.handleWidgetPreserve = require('./handleWidgetPreserve');
TransformHelper.prototype.handleWidgetPreserveAttrs = require('./handleWidgetPreserveAttrs');
TransformHelper.prototype.handleWidgetBody = require('./handleWidgetBody');
TransformHelper.prototype.handleWidgetBind = require('./handleWidgetBind');
TransformHelper.prototype.handleWidgetExtend = require('./handleWidgetExtend');
TransformHelper.prototype.handleWidgetFor = require('./handleWidgetFor');
module.exports = TransformHelper;

View File

@ -0,0 +1,3 @@
module.exports = function getRequirePath(target, template) {
return template.getRequirePath(target);
};

View File

@ -0,0 +1,13 @@
var path = require('path');
var markoWidgetsDir = path.join(__dirname, '../');
var resolveFrom = require('resolve-from');
module.exports = function getRequirePath(target, context) {
var relPath = target === 'marko-widgets' ?
'./' :
'.' + target.substring(target.indexOf('/'));
var resolvedTarget = resolveFrom(markoWidgetsDir, relPath);
var requirePath = context.getRequirePath(resolvedTarget);
return requirePath;
};

View File

@ -0,0 +1,3 @@
module.exports = function(handlerMethodName, widgetId) {
return handlerMethodName && (handlerMethodName + '|' + widgetId);
};

View File

@ -0,0 +1,35 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Helper method to return the metadata for the current widget being rendered.
* This is, it returns the widget at the top of the widget stack.
* @param {AsyncWriter} out The current rendering context that holds info about rendered widgets.
* @return {WidgetDef} The WidgetDef instance
*/
module.exports = function getCurrentWidget(out) {
var widgets = out.global.widgets;
if (!widgets) {
throw new Error('No widget found');
}
var widget = widgets.getCurrentWidget();
if (!widget) {
throw new Error('No widget found');
}
return widget;
};

View File

@ -0,0 +1,5 @@
{
"browser": {
"./getDynamicClientWidgetPath.js": "./getDynamicClientWidgetPath-browser.js"
}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var extend = require('raptor-util/extend');
var widgetArgs = module.exports = function widgetArgs(out, scope, assignedId, customEvents, extendModule, extendConfig, extendState) {
var data = out.data;
var __widgetArgs = data.widgetArgs;
var id;
if (!__widgetArgs) {
if (assignedId != null) {
id = assignedId.toString();
}
__widgetArgs = data.widgetArgs = {
out: out,
id: id,
scope: scope,
customEvents: customEvents
};
}
if (extendModule) {
if (__widgetArgs.extend) {
// The nested extends should come before the outer extends
// since the extends are applied from left to right and the
// outer widget will expect for the inner widget to have been
// patched
__widgetArgs.extend.push(extendModule);
} else {
__widgetArgs.extend = [extendModule];
}
}
// Merge in the extend config...
if (extendConfig) {
__widgetArgs.extendConfig = __widgetArgs.extendConfig ?
extend(extendConfig, __widgetArgs.extendConfig) :
extendConfig;
}
// Merge in the extend state...
if (extendState) {
__widgetArgs.extendState = __widgetArgs.extendState ?
extend(extendState, __widgetArgs.extendState) :
extendState;
}
};
widgetArgs.cleanup = function(out) {
delete out.data.widgetArgs;
};

View File

@ -0,0 +1,22 @@
var markoWidgets = require('../../');
var isBrowser = typeof window !== 'undefined';
module.exports = function widgetBody(out, id, content, widget) {
if (id != null && content == null) {
if (isBrowser) {
// There is no body content so let's see if we should reuse
// the existing body content in the DOM
var existingEl = document.getElementById(id);
if (existingEl) {
var widgetsContext = markoWidgets.getWidgetsContext(out);
widgetsContext.addPreservedDOMNode(existingEl, true /* body only */);
}
}
} else if (typeof content === 'function') {
content(out, widget);
} else if (typeof content === 'string') {
out.text(content);
} else {
throw new Error('Unexpected content');
}
};

View File

@ -0,0 +1,52 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var markoWidgets = require('../');
var WidgetContext = markoWidgets.WidgetsContext;
module.exports = function render(input, out) {
var widgetsContext = markoWidgets.getWidgetsContext(out);
var options = input.immediate ? {immediate: true} : null;
if (input.immediate === true) {
out.on('await:beforeRender', function(eventArgs) {
if (eventArgs.clientReorder) {
var asyncFragmentOut = eventArgs.out;
asyncFragmentOut.data.widgets = new WidgetContext(asyncFragmentOut);
}
});
out.on('await:finish', function(eventArgs) {
var asyncFragmentOut = eventArgs.out;
var widgetsContext = asyncFragmentOut.data.widgets || asyncFragmentOut.global.widgets;
if (widgetsContext) {
markoWidgets.writeInitWidgetsCode(widgetsContext, asyncFragmentOut, options);
}
});
}
var asyncOut = out.beginAsync({ last: true, timeout: -1 });
out.onLast(function(next) {
if (widgetsContext.hasWidgets()) {
markoWidgets.writeInitWidgetsCode(widgetsContext, asyncOut, options);
}
asyncOut.end();
next();
});
};

View File

@ -0,0 +1,5 @@
{
"browser": {
"./getRequirePath.js": "./getRequirePath-browser.js"
}
}

View File

@ -0,0 +1,59 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var widgets = require('../');
module.exports = function render(input, out) {
var global = out.global;
if (global.__rerender === true) {
var id = input.id;
// See if the DOM node with the given ID already exists.
// If so, then reuse the existing DOM node instead of re-rendering
// the children. We have to put a placeholder node that will get
// replaced out if we find that the DOM node has already been rendered
var condition = input['if'];
if (condition !== false) {
var existingEl = document.getElementById(id);
if (existingEl) {
var widgetsContext = widgets.getWidgetsContext(out);
var bodyOnly = input.bodyOnly === true;
// Don't actually render anything since the element is already in the DOM,
// but keep track that the node is being preserved so that we can ignore
// it while transforming the old DOM
if (!bodyOnly) {
var tagName = existingEl.tagName;
// If we are preserving the entire DOM node (not just the body)
// then that means that we have need to render a placeholder to
// mark the target location. We can then replace the placeholder
// node with the existing DOM node
out.beginElement(tagName, { id: id });
out.endElement();
}
widgetsContext.addPreservedDOMNode(existingEl, bodyOnly);
return;
}
}
}
if (input.renderBody) {
input.renderBody(out);
}
};

View File

@ -0,0 +1,26 @@
var tryRequire = require('try-require');
var lassoModulesClientTransport = tryRequire('lasso-modules-client/transport', require);
var resolveFrom = tryRequire('resolve-from', require);
var ok = require('assert').ok;
module.exports = function buildWidgetTypeNode(path, from, builder) {
ok(typeof path === 'string', '"path" should be a string');
ok(typeof from === 'string', '"from" should be a string');
var typeName;
if (lassoModulesClientTransport) {
var targetPath = resolveFrom(from, path);
typeName = lassoModulesClientTransport.getClientPath(targetPath);
} else {
typeName = path;
}
return builder.literal({
name: builder.literal(typeName),
def: builder.functionDeclaration(null, [] /* params */, [
builder.returnStatement(builder.require(builder.literal(path)))
])
});
};

View File

@ -0,0 +1,239 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
var markoWidgets = require('../');
var extend = require('raptor-util/extend');
var widgetArgsId = require('../src/widget-args-id');
var widgetBodyHelper = require('./helpers/widgetBody');
var DUMMY_WIDGET_DEF = {
elId: function () {
}
};
/**
* Look in in the DOM to see if a widget with the same ID and type already exists.
*/
function getExistingWidget(id, type) {
var existingEl = document.getElementById(id);
var existingWidget;
if (existingEl && (existingWidget = existingEl.__widget) && existingWidget.__type === type) {
return existingWidget;
}
return null;
}
function registerWidgetType(widgetType) {
if (!widgetType.registered) {
// Only need to register the widget type once
widgetType.registered = true;
markoWidgets.registerWidget(widgetType);
}
}
function preserveWidgetEl(existingWidget, out, widgetsContext, widgetBody) {
var tagName = existingWidget.el.tagName;
var hasUnpreservedBody = false;
// We put a placeholder element in the output stream to ensure that the existing
// DOM node is matched up correctly when using morphdom.
out.beginElement(tagName, { id: existingWidget.id });
if (widgetBody && existingWidget.bodyEl) {
hasUnpreservedBody = true;
widgetBodyHelper(out, existingWidget.bodyEl.id, widgetBody, existingWidget);
}
out.endElement();
existingWidget._reset(); // The widget is no longer dirty so reset internal flags
widgetsContext.addPreservedDOMNode(existingWidget.el, null, hasUnpreservedBody); // Mark the element as being preserved (for morphdom)
}
function handleBeginAsync(event) {
var parentOut = event.parentOut;
var asyncOut = event.out;
var widgetsContext = asyncOut.global.widgets;
var widgetStack;
if (widgetsContext && (widgetStack = widgetsContext.widgetStack).length) {
// All of the widgets in this async block should be
// initialized after the widgets in the parent. Therefore,
// we will create a new WidgetsContext for the nested
// async block and will create a new widget stack where the current
// widget in the parent block is the only widget in the nested
// stack (to begin with). This will result in top-level widgets
// of the async block being added as children of the widget in the
// parent block.
var nestedWidgetsContext = new markoWidgets.WidgetsContext(asyncOut);
nestedWidgetsContext.widgetStack = [widgetStack[widgetStack.length-1]];
asyncOut.data.widgets = nestedWidgetsContext;
}
asyncOut.data.widgetArgs = parentOut.data.widgetArgs;
}
module.exports = function widgetTag(input, out) {
var global = out.global;
if (!global.__widgetsBeginAsyncAdded) {
global.__widgetsBeginAsyncAdded = true;
out.on('beginAsync', handleBeginAsync);
}
var type = input.type;
var config = input.config || input._cfg;
var state = input.state || input._state;
var props = input.props || input._props;
var widgetArgs = out.data.widgetArgs;
var bodyElId = input.body;
var widgetBody = input._body;
var typeName = type && type.name;
var id = input.id;
var extendList;
var hasDomEvents = input.hasDomEvents;
var customEvents;
var scope;
var extendState;
var extendConfig;
if (widgetArgs) {
delete out.data.widgetArgs;
scope = widgetArgs.scope;
id = id || widgetArgsId(widgetArgs);
extendList = widgetArgs.extend;
customEvents = widgetArgs.customEvents;
if (extendList) {
extendList = extendList.map(function(extendType) {
registerWidgetType(extendType);
return extendType.name;
});
}
if ((extendState = widgetArgs.extendState)) {
if (state) {
extend(state, extendState);
} else {
state = extendState;
}
}
if ((extendConfig = widgetArgs.extendConfig)) {
if (config) {
extend(config, extendConfig);
} else {
config = extendConfig;
}
}
}
var rerenderWidget = global.__rerenderWidget;
var isRerender = global.__rerender === true;
var widgetsContext = markoWidgets.getWidgetsContext(out);
if (!id) {
var parentWidget = widgetsContext.getCurrentWidget();
if (parentWidget) {
id = parentWidget.nextId();
}
}
var existingWidget;
if (rerenderWidget) {
existingWidget = rerenderWidget;
id = rerenderWidget.id;
delete global.__rerenderWidget;
} else if (isRerender) {
existingWidget = getExistingWidget(id, typeName);
}
if (!id && input.hasOwnProperty('id')) {
throw new Error('Invalid widget ID for "' + typeName + '"');
}
if (typeName) {
registerWidgetType(type);
var shouldRenderBody = true;
if (existingWidget && !rerenderWidget) {
// This is a nested widget found during a rerender. We don't want to needlessly
// rerender the widget if that is not necessary. If the widget is a stateful
// widget then we update the existing widget with the new state.
if (state) {
existingWidget._replaceState(state); // Update the existing widget state using the internal/private
// method to ensure that another update is not queued up
// If the widget has custom state update handlers then we will use those methods
// to update the widget.
if (existingWidget._processUpdateHandlers() === true) {
// If _processUpdateHandlers() returns true then that means
// that the widget is now up-to-date and we can skip rerendering it.
shouldRenderBody = false;
preserveWidgetEl(existingWidget, out, widgetsContext, widgetBody);
return;
}
}
// If the widget is not dirty (no state changes) and shouldUpdate() returns false
// then skip rerendering the widget.
if (!existingWidget.isDirty() && !existingWidget.shouldUpdate(props, state)) {
shouldRenderBody = false;
preserveWidgetEl(existingWidget, out, widgetsContext, widgetBody);
return;
}
}
if (existingWidget) {
existingWidget._emitLifecycleEvent('beforeUpdate');
}
var widgetDef = widgetsContext.beginWidget({
type: typeName,
id: id,
config: config,
state: state,
hasDomEvents: hasDomEvents,
customEvents: customEvents,
scope: scope,
createWidget: input.createWidget,
extend: extendList,
existingWidget: existingWidget,
bodyElId: bodyElId
});
// Only render the widget if it needs to be rerendered
if (shouldRenderBody) {
input.renderBody(out, widgetDef);
markoWidgets.writeDomEventsEl(widgetDef, out);
}
widgetDef.end();
} else {
input.renderBody(out, DUMMY_WIDGET_DEF);
}
};

View File

@ -0,0 +1,20 @@
var buildWidgetTypeNode = require('./util/buildWidgetTypeNode');
module.exports = function codeGenerator(el, codegen) {
var builder = codegen.builder;
var attrs = el.getAttributes();
var typesObject = {};
attrs.forEach((attr) => {
if (!attr.isLiteralString()) {
codegen.addError('Widget type should be a string');
return;
}
typesObject[attr.name] = buildWidgetTypeNode(attr.literalValue, codegen.context.dirname, codegen.builder);
});
codegen.addStaticVar('__widgetTypes', builder.literal(typesObject));
};

View File

@ -0,0 +1,77 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
var TransformHelper = require('./TransformHelper');
module.exports = function transform(el, context) {
var transformHelper = new TransformHelper(el, context);
if (el.tagName === 'widget-types') {
context.setFlag('hasWidgetTypes');
}
if (el.hasAttribute('w-el-id')) {
transformHelper.addError('"w-el-id" attribute is no longer allowed. Use "w-id" instead.');
return;
}
if (el.hasAttribute('w-bind')) {
el.setFlag('hasWidgetBind');
if (el.hasAttribute('w-id')) {
transformHelper.addError('The "w-id" attribute cannot be used in conjuntion with the "w-bind" attribute.');
}
transformHelper.handleWidgetBind();
} else if (el.hasAttribute('w-extend')) {
el.setFlag('hasWidgetExtend');
transformHelper.handleWidgetExtend();
}
if (el.hasAttribute('w-preserve') ||
el.hasAttribute('w-preserve-body') ||
el.hasAttribute('w-preserve-if') ||
el.hasAttribute('w-preserve-body-if')) {
transformHelper.handleWidgetPreserve();
}
if (el.hasAttribute('w-id')) {
transformHelper.assignWidgetId();
}
if (el.hasAttribute('w-for')) {
transformHelper.handleWidgetFor();
}
if (el.hasAttribute('w-preserve-attrs')) {
transformHelper.handleWidgetPreserveAttrs();
}
if (el.hasAttribute('w-body')) {
transformHelper.handleWidgetBody();
}
// Handle w-on* properties
transformHelper.handleWidgetEvents();
// If we need to pass any information to a nested widget then
// we start that information in the "out" so that it can be picked
// up later by the nested widget. We call this "widget args" and
// we generate compiled code that stores the widget args in the out
// for the next widget and then we also insert cleanup code to remove
// the data out of the out
if (el.type !== 'HtmlElement') { // Only custom tags can have nested widgets
transformHelper.getWidgetArgs().compile(transformHelper);
}
};

5
widgets/test/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
actual.js
actual.html
*.actual.js
*.actual.html
/generated

45
widgets/test/.jshintrc Normal file
View File

@ -0,0 +1,45 @@
{
"predef": [
"it",
"xit",
"console",
"describe",
"xdescribe",
"beforeEach",
"before",
"after",
"waits",
"waitsFor",
"runs",
"registerTest"
],
"node" : true,
"es5" : false,
"esnext": false,
"browser" : true,
"boss" : false,
"curly": false,
"debug": false,
"devel": false,
"eqeqeq": true,
"evil": true,
"forin": false,
"immed": true,
"laxbreak": false,
"newcap": true,
"noarg": true,
"noempty": false,
"nonew": true,
"nomen": false,
"onevar": false,
"plusplus": false,
"regexp": false,
"undef": true,
"sub": false,
"white": false,
"eqeqeq": false,
"latedef": true,
"unused": "vars",
"eqnull": true,
"expr": true
}

View File

@ -0,0 +1,7 @@
module.exports = require('marko-widgets').defineComponent({
template: require('./template.marko'),
init: function() {
}
});

View File

@ -0,0 +1,6 @@
<div w-bind>
<div.unpreserved-counter>${data.counter}</div>
<span class="preserve" data-counter=data.counter w-preserve-body>
<div.preserved-counter>${data.counter}</div>
</span>
</div>

View File

@ -0,0 +1,22 @@
var expect = require('chai').expect;
module.exports = function(helpers) {
var counter = 0;
var widget = helpers.mount(require('./index'), {
counter: counter
});
expect(widget.el.querySelector('.unpreserved-counter').innerHTML).to.equal('0');
expect(widget.el.querySelector('.preserve').getAttribute('data-counter')).to.equal('0');
expect(widget.el.querySelector('.preserved-counter').innerHTML).to.equal('0');
widget.rerender({
counter: ++counter
});
expect(widget.el.querySelector('.unpreserved-counter').innerHTML).to.equal('1');
expect(widget.el.querySelector('.preserve').getAttribute('data-counter')).to.equal('1');
expect(widget.el.querySelector('.preserved-counter').innerHTML).to.equal('0');
};

View File

@ -0,0 +1,7 @@
module.exports = require('marko-widgets').defineComponent({
template: require('./template.marko'),
init: function() {
}
});

View File

@ -0,0 +1,6 @@
<div w-bind>
<div.unpreserved-counter>${data.counter}</div>
<span w-id="preserve" data-counter=data.counter w-preserve-body>
<div.preserved-counter>${data.counter}</div>
</span>
</div>

View File

@ -0,0 +1,22 @@
var expect = require('chai').expect;
module.exports = function(helpers) {
var counter = 0;
var widget = helpers.mount(require('./index'), {
counter: counter
});
expect(widget.el.querySelector('.unpreserved-counter').innerHTML).to.equal('0');
expect(widget.getEl('preserve').getAttribute('data-counter')).to.equal('0');
expect(widget.el.querySelector('.preserved-counter').innerHTML).to.equal('0');
widget.rerender({
counter: ++counter
});
expect(widget.el.querySelector('.unpreserved-counter').innerHTML).to.equal('1');
expect(widget.getEl('preserve').getAttribute('data-counter')).to.equal('1');
expect(widget.el.querySelector('.preserved-counter').innerHTML).to.equal('0');
};

View File

@ -0,0 +1,7 @@
module.exports = require('marko-widgets').defineComponent({
template: require('./template.marko'),
init: function() {
}
});

View File

@ -0,0 +1,6 @@
<div w-bind>
<div.unpreserved-counter>${data.counter}</div>
<span class="preserve" data-counter=data.counter w-preserve>
<div.preserved-counter>${data.counter}</div>
</span>
</div>

View File

@ -0,0 +1,22 @@
var expect = require('chai').expect;
module.exports = function(helpers) {
var counter = 0;
var widget = helpers.mount(require('./index'), {
counter: counter
});
expect(widget.el.querySelector('.unpreserved-counter').innerHTML).to.equal('0');
expect(widget.el.querySelector('.preserve').getAttribute('data-counter')).to.equal('0');
expect(widget.el.querySelector('.preserved-counter').innerHTML).to.equal('0');
widget.rerender({
counter: ++counter
});
expect(widget.el.querySelector('.unpreserved-counter').innerHTML).to.equal('1');
expect(widget.el.querySelector('.preserve').getAttribute('data-counter')).to.equal('0');
expect(widget.el.querySelector('.preserved-counter').innerHTML).to.equal('0');
};

View File

@ -0,0 +1,7 @@
module.exports = require('marko-widgets').defineComponent({
template: require('./template.marko'),
init: function() {
}
});

View File

@ -0,0 +1,6 @@
<div w-bind>
<div.unpreserved-counter>${data.counter}</div>
<span w-id="preserve" data-counter=data.counter w-preserve>
<div.preserved-counter>${data.counter}</div>
</span>
</div>

View File

@ -0,0 +1,22 @@
var expect = require('chai').expect;
module.exports = function(helpers) {
var counter = 0;
var widget = helpers.mount(require('./index'), {
counter: counter
});
expect(widget.el.querySelector('.unpreserved-counter').innerHTML).to.equal('0');
expect(widget.getEl('preserve').getAttribute('data-counter')).to.equal('0');
expect(widget.el.querySelector('.preserved-counter').innerHTML).to.equal('0');
widget.rerender({
counter: ++counter
});
expect(widget.el.querySelector('.unpreserved-counter').innerHTML).to.equal('1');
expect(widget.getEl('preserve').getAttribute('data-counter')).to.equal('0');
expect(widget.el.querySelector('.preserved-counter').innerHTML).to.equal('0');
};

View File

@ -0,0 +1,13 @@
module.exports = {
getInitialState: function(input) {
return {
name: input.name
};
},
getTemplateData: function(state, input) {
return state;
},
setName: function(newName) {
this.setState('name', newName);
}
};

View File

@ -0,0 +1,3 @@
<div w-bind>
Hello ${data.name}!
</div>

View File

@ -0,0 +1,13 @@
var expect = require('chai').expect;
module.exports = function(helpers) {
var widget = helpers.mount(require('./index.marko'), { name: 'Frank' });
expect(widget.el.innerHTML).to.contain('Hello Frank!');
widget.setName('Jane');
widget.update();
expect(widget.el.innerHTML).to.contain('Hello Jane!');
};

View File

@ -0,0 +1,14 @@
module.exports = require('marko-widgets').defineComponent({
template: require.resolve('./template.marko'),
getInitialState: function(input) {
return {
name: input.name
};
},
getTemplateData: function(state, input) {
return state;
},
setName: function(newName) {
this.setState('name', newName);
}
});

View File

@ -0,0 +1,3 @@
<div w-bind>
Hello ${data.name}!
</div>

View File

@ -0,0 +1,13 @@
var expect = require('chai').expect;
module.exports = function(helpers) {
var widget = helpers.mount(require('./index'), { name: 'Frank' });
expect(widget.el.innerHTML).to.contain('Hello Frank!');
widget.setName('Jane');
widget.update();
expect(widget.el.innerHTML).to.contain('Hello Jane!');
};

View File

@ -0,0 +1,79 @@
module.exports = require('marko-widgets').defineComponent({
template: require.resolve('./template.marko'),
getInitialState: function(input) {
return {
size: input.size || 'normal',
variant: input.variant || 'primary',
className: input['class'],
attrs: input['*']
};
},
getInitialBody: function(input) {
return input.label || input.renderBody;
},
getTemplateData: function(state, input) {
var rootAttrs = {};
var classParts = ['app-button'];
var type = 'button';
var variant = state.variant;
if (variant !== 'primary') {
classParts.push('app-button-' + variant);
}
var size = state.size;
if (size !== 'normal') {
classParts.push('app-button-' + size);
}
var className = state.className;
if (className) {
classParts.push(className);
}
var splatAttrs = state.attrs;
if (splatAttrs) {
for (var splatAttr in splatAttrs) {
if (splatAttrs.hasOwnProperty(splatAttr)) {
rootAttrs[splatAttr] = splatAttrs[splatAttr];
}
}
}
rootAttrs['class'] = classParts.join(' ');
return {
type: type,
rootAttrs: rootAttrs
};
},
handleClick: function(event) {
// Every Widget instance is also an EventEmitter instance.
// We will emit a custom "click" event when a DOM click event
// is triggered
this.emit('click', {
event: event // Pass along the DOM event in case it is helpful to others
});
},
// Add any other methods here
setVariant: function(variant) {
this.setState('variant', variant);
},
setSize: function(size) {
this.setState('size', size);
},
setLabel: function(label) {
this.setState('label', label);
},
getSize: function() {
return this.state.size;
}
});

View File

@ -0,0 +1,4 @@
<button ${data.rootAttrs}
w-onclick="handleClick"
w-bind
w-body></button>

View File

@ -0,0 +1,16 @@
var expect = require('chai').expect;
module.exports = function(helpers) {
var widget = helpers.mount(require('./index'), {
size: 'large',
label: 'Initial Label'
});
expect(widget.el.className).to.contain('large');
require('marko-widgets').batchUpdate(function() {
widget.setSize('small');
});
expect(widget.el.className).to.contain('small');
};

View File

@ -0,0 +1,79 @@
module.exports = require('marko-widgets').defineComponent({
template: require.resolve('./template.marko'),
getInitialState: function(input) {
return {
size: input.size || 'normal',
variant: input.variant || 'primary',
className: input['class'],
attrs: input['*']
};
},
getInitialBody: function(input) {
return input.label || input.renderBody;
},
getTemplateData: function(state, input) {
var rootAttrs = {};
var classParts = ['app-button'];
var type = 'button';
var variant = state.variant;
if (variant !== 'primary') {
classParts.push('app-button-' + variant);
}
var size = state.size;
if (size !== 'normal') {
classParts.push('app-button-' + size);
}
var className = state.className;
if (className) {
classParts.push(className);
}
var splatAttrs = state.attrs;
if (splatAttrs) {
for (var splatAttr in splatAttrs) {
if (splatAttrs.hasOwnProperty(splatAttr)) {
rootAttrs[splatAttr] = splatAttrs[splatAttr];
}
}
}
rootAttrs['class'] = classParts.join(' ');
return {
type: type,
rootAttrs: rootAttrs
};
},
handleClick: function(event) {
// Every Widget instance is also an EventEmitter instance.
// We will emit a custom "click" event when a DOM click event
// is triggered
this.emit('click', {
event: event // Pass along the DOM event in case it is helpful to others
});
},
// Add any other methods here
setVariant: function(variant) {
this.setState('variant', variant);
},
setSize: function(size) {
this.setState('size', size);
},
setLabel: function(label) {
this.setState('label', label);
},
getSize: function() {
return this.state.size;
}
});

View File

@ -0,0 +1,4 @@
<button ${data.rootAttrs}
w-onclick="handleClick"
w-bind
w-body></button>

View File

@ -0,0 +1,17 @@
var expect = require('chai').expect;
module.exports = function(helpers, done) {
var widget = helpers.mount(require('./index'), {
size: 'large',
label: 'Initial Label'
});
expect(widget.el.className).to.contain('large');
widget.setSize('small');
expect(widget.el.className).to.not.contain('small');
require('marko-widgets').onAfterUpdate(function() {
expect(widget.el.className).to.contain('small');
done();
});
};

View File

@ -0,0 +1,7 @@
module.exports = require('marko-widgets').defineComponent({
template: require('./template.marko'),
init: function() {
}
});

View File

@ -0,0 +1 @@
div w-bind

View File

@ -0,0 +1,26 @@
var expect = require('chai').expect;
module.exports = function(helpers) {
var widget = helpers.mount(require('./index'), {});
expect(widget.id).to.be.a('string');
expect(widget.emit).to.be.a('function');
expect(widget.on).to.be.a('function');
expect(widget.once).to.be.a('function');
expect(widget.addListener).to.be.a('function');
expect(widget.subscribeTo).to.be.a('function');
expect(widget.getElId).to.be.a('function');
expect(widget.elId).to.be.a('function');
expect(widget.getEl).to.be.a('function');
expect(widget.destroy).to.be.a('function');
expect(widget.isDestroyed).to.be.a('function');
expect(widget.rerender).to.be.a('function');
expect(widget.detach).to.be.a('function');
expect(widget.appendTo).to.be.a('function');
expect(widget.replace).to.be.a('function');
expect(widget.replaceChildrenOf).to.be.a('function');
expect(widget.insertBefore).to.be.a('function');
expect(widget.insertAfter).to.be.a('function');
expect(widget.prependTo).to.be.a('function');
expect(widget.ready).to.be.a('function');
expect(widget.$).to.be.a('function');
};

View File

@ -0,0 +1,13 @@
module.exports = require('marko-widgets').defineComponent({
template: require('./template.marko'),
getTemplateData: function(state, input) {
return {
includeWidget: input.includeWidget
};
},
init: function() {
}
});

View File

@ -0,0 +1,5 @@
<widget-types default="./"/>
<div w-bind=(data.includeWidget ? 'default' : null)>
[app-conditional-widget]
</div>

View File

@ -0,0 +1,17 @@
var expect = require('chai').expect;
module.exports = function(helpers) {
var widget = helpers.mount(require('./index'), {
includeWidget: false
});
expect(widget).to.equal(undefined);
expect(helpers.targetEl.innerHTML).contain('[app-conditional-widget]');
widget = helpers.mount(require('./index'), {
includeWidget: true
});
expect(widget != null).to.equal(true);
};

View File

@ -0,0 +1,3 @@
// Export a render(input, callback) method that can be used
// to render this UI component on the client or server
require('marko-widgets').renderable(exports, require('./renderer'));

Some files were not shown because too many files have changed in this diff Show More