marko/widgets/renderer.js

306 lines
11 KiB
JavaScript

var widgetLookup = require('./lookup').widgets;
var includeTag = require('./taglib/include-tag');
var repeatedId = require('./repeated-id');
var getRootEls = require('./getRootEls');
var repeatedRegExp = /\[\]$/;
var WidgetsContext = require('./WidgetsContext');
var RERENDER_WIDGET_INDEX = 0;
var RERENDER_WIDGET_STATE_INDEX = 1;
var WIDGETS_BEGIN_ASYNC_ADDED_KEY = '$wa';
function resolveWidgetRef(out, ref, scope) {
if (ref.charAt(0) === '#') {
return ref.substring(1);
} else {
var resolvedId;
if (repeatedRegExp.test(ref)) {
resolvedId = repeatedId.$__nextId(out, scope, ref);
} else {
resolvedId = scope + '-' + ref;
}
return resolvedId;
}
}
function preserveWidgetEls(existingWidget, out, widgetsContext, widgetBody) {
var rootEls = getRootEls(existingWidget, {});
var rootElIds = Object.keys(rootEls);
var bodyEl;
if (widgetBody && (bodyEl = existingWidget.$__bodyEl)) {
if (rootElIds.length > 1) {
// If there are multiple roots then we don't know which root el
// contains the body element so we will just need to rerender the
// UI component
return false;
}
}
for (var elId in rootEls) {
var el = rootEls[elId];
var tagName = el.tagName;
// 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: elId });
if (bodyEl) {
includeTag({
_target: widgetBody
}, out);
}
out.endElement();
widgetsContext.$__preserveDOMNode(el, false, bodyEl); // Mark the element as being preserved (for morphdom)
}
existingWidget._reset(); // The widget is no longer dirty so reset internal flags
return true;
}
function handleBeginAsync(event) {
var parentOut = event.parentOut;
var asyncOut = event.out;
var widgetsContext = asyncOut.global.widgets;
var widgetStack;
if (widgetsContext && (widgetStack = widgetsContext.$__widgetStack)) {
// 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 WidgetsContext(asyncOut, widgetStack[widgetStack.length-1]);
asyncOut.data.widgets = nestedWidgetsContext;
}
asyncOut.data.$w = parentOut.data.$w;
}
module.exports = function createRendererFunc(templateRenderFunc, widgetProps, renderingLogic) {
var onInput;
var getInitialProps;
var getTemplateData;
var getInitialState;
var getWidgetConfig;
var getInitialBody;
function initRendereringLogic() {
onInput = renderingLogic.onInput;
getInitialProps = renderingLogic.getInitialProps; //deprecate
getTemplateData = renderingLogic.getTemplateData;
getInitialState = renderingLogic.getInitialState; //deprecate
getWidgetConfig = renderingLogic.getWidgetConfig; //deprecate
getInitialBody = renderingLogic.getInitialBody;
}
if (renderingLogic) {
initRendereringLogic();
}
var typeName = widgetProps.type;
var bodyElId = widgetProps.body;
var roots = widgetProps.roots;
var assignedId = widgetProps.id;
return function renderer(input, out, renderingLogicLegacy /* needed by defineRenderer */) {
var outGlobal = out.global;
if (!outGlobal[WIDGETS_BEGIN_ASYNC_ADDED_KEY]) {
outGlobal[WIDGETS_BEGIN_ASYNC_ADDED_KEY] = true;
out.on('beginAsync', handleBeginAsync);
}
if (renderingLogic === undefined) {
// LEGACY - This should be removed when `defineRenderer` is removed but we use it
// now to run the rendering logic that is passed in at render time. The reason we don't
// get the rendering logic until now is that in older versions the `defineRenderer` was
// invoked before template rendering
if ((renderingLogic = renderingLogicLegacy)) {
initRendereringLogic();
}
}
var widgetState;
var widgetConfig;
var widgetBody;
var rerenderInfo = outGlobal.$w;
var rerenderWidget;
if (rerenderInfo && (rerenderWidget = rerenderInfo[RERENDER_WIDGET_INDEX])) {
rerenderInfo[RERENDER_WIDGET_INDEX] = null;
if (!input) {
widgetState = rerenderInfo[RERENDER_WIDGET_STATE_INDEX];
input = null;
}
} else if (!input) {
// Make sure we always have a non-null input object
input = {};
}
var widgetArgs;
if (input) {
if (onInput) {
var lightweightWidget = Object.create(renderingLogic);
lightweightWidget.onInput(input);
widgetState = lightweightWidget.state;
widgetBody = lightweightWidget.body;
widgetConfig = lightweightWidget;
delete widgetConfig.state;
delete widgetConfig.body;
} else {
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.
widgetConfig = getWidgetConfig(input, out);
} else {
widgetConfig = input.widgetConfig;
}
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
widgetBody = getInitialBody(input, out);
}
}
if (!widgetBody) {
// Default to using the nested content as the widget body
widgetBody = input.renderBody;
}
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
input = getInitialProps(input, out) || {};
}
if (getInitialState) {
// This optional method is used to derive the widget state
// from the input properties
widgetState = getInitialState(input, out);
}
}
widgetArgs = input.$w;
}
var customEvents;
var scope;
var id = assignedId;
if (!widgetArgs) {
widgetArgs = out.data.$w;
}
if (widgetArgs) {
scope = widgetArgs[0];
if (scope) {
scope = scope.id;
}
var ref = widgetArgs[1];
if (ref != null) {
ref = ref.toString();
}
id = id || resolveWidgetRef(out, ref, scope);
customEvents = widgetArgs[2];
}
var widgetsContext = WidgetsContext.$__getWidgetsContext(out);
if (!id) {
id = widgetsContext.$__nextWidgetId();
}
var existingWidget;
if (rerenderWidget) {
existingWidget = rerenderWidget;
id = rerenderWidget.id;
} else if (rerenderInfo) {
// Look in in the DOM to see if a widget with the same ID and type already exists.
existingWidget = widgetLookup[id];
if (existingWidget && existingWidget.$__type !== typeName) {
existingWidget = undefined;
}
}
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 (widgetState) {
if (existingWidget.$__replaceState(widgetState)) {
// If _processUpdateHandlers() returns true then that means
// that the widget is now up-to-date and we can skip rerendering it, but only if we
// are able to preserve the existing widget els
if (preserveWidgetEls(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.$__dirty && !existingWidget.shouldUpdate(input, widgetState)) {
if (preserveWidgetEls(existingWidget, out, widgetsContext, widgetBody)) {
return;
}
}
}
// 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, input, out) :
(getInitialState && widgetState /*legacy*/) || input) || {};
if (existingWidget) {
existingWidget.$__emitLifecycleEvent('beforeUpdate');
}
var widgetDef = widgetsContext.$__beginWidget(id);
widgetDef.$__type = typeName;
widgetDef.$__config = widgetConfig;
widgetDef.$__state = widgetState;
widgetDef.$__customEvents = customEvents;
widgetDef.$__scope = scope;
widgetDef.$__existingWidget = existingWidget;
widgetDef.$__bodyElId = bodyElId;
widgetDef.$__roots = roots;
widgetDef.b = widgetBody;
// Render the template associated with the component using the final template
// data that we constructed
templateRenderFunc(templateData, out, widgetDef, widgetState);
widgetDef.$__end();
};
};