lib/preact.rb in isomorfeus-preact-10.9.0 vs lib/preact.rb in isomorfeus-preact-22.9.0.rc1
- old
+ new
@@ -1,296 +1,1587 @@
+if RUBY_ENGINE == 'opal'
+ class VNode
+ # just a empty place holder to make is_a?(VNode) work
+ # internally using the js implementation
+ end
+else
+ class VNode
+ # full ruby implementation
+ attr_accessor :__c
+ attr_reader :_children
+ attr_reader :_component
+ attr_reader :_depth
+ attr_reader :_dom
+ attr_reader :_hydrating
+ attr_reader :_nextDom
+ attr_reader :_original
+ attr_reader :constructor
+ attr_reader :key
+ attr_reader :props
+ attr_reader :ref
+ attr_reader :type
+
+ def initialize(type, props, key, ref, original)
+ @type = type
+ @props = props
+ @key = key
+ @ref = ref
+ @_depth = 0
+ @_original = original.nil? ? Preact._vnode_id += 1 : original
+ end
+ end
+end
+
+class Fragment
+ def initialize(props, _context)
+ @props = props
+ end
+
+ def render
+ @props[:children]
+ end
+end
+
module Preact
- %x{
- self.render_buffer = [];
+ EMPTY_ARR = []
+ UNSAFE_NAME = /[\s\n\\\/='"\0<>]/
- self.set_validate_prop = function(component, prop_name) {
- let core = component.preact_component;
- if (typeof core.propTypes == "undefined") {
- core.propTypes = {};
- core.propValidations = {};
- core.propValidations[prop_name] = {};
+ if RUBY_ENGINE == 'opal'
+ %x{
+ function _catchError(error, vnode, oldVNode) {
+ let component, ctor, handled;
+
+ for (; (vnode = vnode._parent); ) {
+ if ((component = vnode._component) && !component._processingException) {
+ try {
+ if (component["$respond_to?"]("get_derived_state_from_error")) {
+ component.$set_state(component.$get_derived_state_from_error(error));
+ handled = component._dirty;
+ }
+
+ if (component["$respond_to?"]("component_did_catch")) {
+ component.$component_did_catch(error);
+ handled = component._dirty;
+ }
+
+ // This is an error boundary. Mark it as having bailed out, and whether it was mid-hydration.
+ if (handled) {
+ return (component._pendingError = component);
+ }
+ } catch (e) {
+ error = e;
+ }
+ }
+ }
+
+ throw error;
}
- core.propTypes[prop_name] = core.prototype.validateProp;
- };
- self.props_are_equal = function(this_props, next_props) {
- let counter = 0;
- for (var property in next_props) {
- counter++;
- if (next_props.hasOwnProperty(property)) {
- if (!this_props.hasOwnProperty(property)) { return false; };
- if (property === "children") { if (next_props.children !== this_props.children) { return false; }}
- else if (typeof next_props[property] === "object" && next_props[property] !== null && typeof next_props[property]['$!='] === "function" &&
- typeof this_props[property] !== "undefined" && this_props[property] !== null ) {
- if (#{ !! (`next_props[property]` != `this_props[property]`) }) { return false; }
- } else if (next_props[property] !== this_props[property]) { return false; }
+ const EMPTY_OBJ = {};
+ const EMPTY_ARR = [];
+ const slice = EMPTY_ARR.slice;
+
+ function assign(obj, props) {
+ for (let i in props) obj[i] = props[i];
+ return obj;
+ }
+
+ function applyRef(ref, value, vnode) {
+ try {
+ let converted_value;
+ if (value == null || typeof(value) === 'undefined' ) { converted_value = nil; }
+ else if (typeof value.$$class !== 'undefined') { converted_value = value; }
+ else if (value instanceof Element || value instanceof Node) { converted_value = #{Browser::Element.new(`value`)}; }
+ if (typeof ref === "function") ref.$call(converted_value);
+ else ref["$[]="]("current", converted_value);
+ } catch (e) {
+ _catchError(e, vnode);
}
}
- if (counter !== Object.keys(this_props).length) { return false; }
- return true;
- };
- self.state_is_not_equal = function(this_state, next_state) {
- let counter = 0;
- for (var property in next_state) {
- counter++;
- if (next_state.hasOwnProperty(property)) {
- if (!this_state.hasOwnProperty(property)) { return true; };
- if (typeof next_state[property] === "object" && next_state[property] !== null && typeof next_state[property]['$!='] === "function" &&
- typeof this_state[property] !== "undefined" && this_state[property] !== null) {
- if (#{ !! (`next_state[property]` != `this_state[property]`) }) { return true }
- } else if (next_state[property] !== this_state[property]) { return true }
+ function getDomSibling(vnode, childIndex) {
+ if (childIndex == null) {
+ // Use childIndex==null as a signal to resume the search from the vnode's sibling
+ return vnode._parent
+ ? getDomSibling(vnode._parent, vnode._parent._children.indexOf(vnode) + 1)
+ : null;
}
+
+ let sibling;
+ for (; childIndex < vnode._children.length; childIndex++) {
+ sibling = vnode._children[childIndex];
+
+ if (sibling != null && sibling._dom != null) {
+ // Since updateParentDomPointers keeps _dom pointer correct,
+ // we can rely on _dom to tell us if this subtree contains a
+ // rendered DOM node, and what the first rendered DOM node is
+ return sibling._dom;
+ }
+ }
+
+ // If we get here, we have not found a DOM node in this vnode's children.
+ // We must resume from this vnode's sibling (in it's parent _children array)
+ // Only climb up and search the parent if we aren't searching through a DOM
+ // VNode (meaning we reached the DOM parent of the original vnode that began
+ // the search)
+ return typeof vnode.type == 'function' ? getDomSibling(vnode) : null;
}
- if (counter !== Object.keys(this_state).length) { return true; }
- return false;
- };
- self.lower_camelize = function(snake_cased_word) {
- if (self.prop_dictionary[snake_cased_word]) { return self.prop_dictionary[snake_cased_word]; }
- let parts = snake_cased_word.split('_');
- let res = parts[0];
- for (let i = 1; i < parts.length; i++) {
- res += parts[i][0].toUpperCase() + parts[i].slice(1);
+ function placeChild(
+ parentDom,
+ childVNode,
+ oldVNode,
+ oldChildren,
+ newDom,
+ oldDom
+ ) {
+ let nextDom;
+ if (childVNode._nextDom !== undefined) {
+ // Only Fragments or components that return Fragment like VNodes will
+ // have a non-undefined _nextDom. Continue the diff from the sibling
+ // of last DOM child of this child VNode
+ nextDom = childVNode._nextDom;
+
+ // Eagerly cleanup _nextDom. We don't need to persist the value because
+ // it is only used by `diffChildren` to determine where to resume the diff after
+ // diffing Components and Fragments. Once we store it the nextDOM local var, we
+ // can clean up the property
+ childVNode._nextDom = undefined;
+ } else if (oldVNode == null || oldVNode === nil || newDom != oldDom || newDom.parentNode == null || newDom.parentNode === nil) {
+ outer: if (oldDom == null || oldDom === nil || oldDom.parentNode !== parentDom) {
+ parentDom.appendChild(newDom);
+ nextDom = null;
+ } else {
+ // `j<oldChildrenLength; j+=2` is an alternative to `j++<oldChildrenLength/2`
+ for (
+ let sibDom = oldDom, j = 0;
+ (sibDom = sibDom.nextSibling) && j < oldChildren.length;
+ j += 2
+ ) {
+ if (sibDom == newDom) {
+ break outer;
+ }
+ }
+ parentDom.insertBefore(newDom, oldDom);
+ nextDom = oldDom;
+ }
+ }
+
+ // If we have pre-calculated the nextDOM node, use it. Else calculate it now
+ // Strictly check for `undefined` here cuz `null` is a valid value of `nextDom`.
+ // See more detail in create-element.js:createVNode
+ if (nextDom !== undefined) {
+ oldDom = nextDom;
+ } else {
+ oldDom = newDom.nextSibling;
+ }
+
+ return oldDom;
}
- self.prop_dictionary[snake_cased_word] = res;
- return res;
- };
- self.native_element_or_component_to_ruby = function (element) {
- if (element == null || typeof(element) === 'undefined' ) { return nil; }
- if (typeof element.__ruby_instance !== 'undefined') { return element.__ruby_instance; }
- if (element instanceof Element || element instanceof Node) { return #{Browser::Element.new(`element`)}; }
- return element;
- };
+ function reorderChildren(childVNode, oldDom, parentDom) {
+ // Note: VNodes in nested suspended trees may be missing _children.
+ let c = childVNode._children;
+ let tmp = 0;
+ for (; c && tmp < c.length; tmp++) {
+ let vnode = c[tmp];
+ if (vnode) {
+ // We typically enter this code path on sCU bailout, where we copy
+ // oldVNode._children to newVNode._children. If that is the case, we need
+ // to update the old children's _parent pointer to point to the newVNode
+ // (childVNode here).
+ vnode._parent = childVNode;
- self.native_to_ruby_event = function(event) {
- if ('target' in event) { return #{::Browser::Event.new(`event`)}; }
- else if (Array.isArray(event)) { return event; }
- else { return Opal.Hash.$new(event); }
- };
+ if (typeof vnode.type == 'function') {
+ oldDom = reorderChildren(vnode, oldDom, parentDom);
+ } else {
+ oldDom = placeChild(
+ parentDom,
+ vnode,
+ vnode,
+ c,
+ vnode._dom,
+ oldDom
+ );
+ }
+ }
+ }
- self.internal_prepare_args_and_render = function(component, args, block) {
- const operain = self.internal_render;
- if (args.length > 0) {
- let last_arg = args[args.length - 1];
- if (last_arg && last_arg.constructor === String) {
- if (args.length === 1) { return operain(component, null, last_arg, null); }
- else { operain(component, args[0], last_arg, null); }
- } else { operain(component, args[0], null, block); }
- } else { operain(component, null, null, block); }
- };
+ return oldDom;
+ }
- self.using_did_catch = false;
+ function removeNode(node) {
+ let parentNode = node.parentNode;
+ if (parentNode) parentNode.removeChild(node);
+ }
- self.active_components = [];
+ self.unmount = function(vnode, parentVNode, skipRemove) {
+ let r;
- self.active_component = function() {
- let length = self.active_components.length;
- if (length === 0) { return null; };
- return self.active_components[length-1];
- };
+ if ((r = vnode.ref) && r && r !== nil) {
+ try {
+ if (typeof r === "function") {
+ applyRef(r, null, parentVNode);
+ } else {
+ let rc = r["$[]"]("current");
+ if (rc === nil || rc === vnode._dom) { applyRef(r, null, parentVNode); }
+ }
+ } catch (e) {
+ // ignore error, continue unmount
+ }
+ }
- self.active_redux_components = [];
+ if ((r = vnode._component) != null && r !== nil) {
+ if (r["$respond_to?"]("component_will_unmount")) {
+ try {
+ r.$component_will_unmount();
+ } catch (e) {
+ _catchError(e, parentVNode);
+ }
+ }
- self.active_redux_component = function() {
- let length = self.active_redux_components.length;
- if (length === 0) { return null; };
- return self.active_redux_components[length-1];
- };
+ r.base = r._parentDom = null;
+ }
- self.register_active_component = function(component) {
- self.active_components.push(component);
- if (typeof(component.data_access) === 'function') {
- self.active_redux_components.push(component);
- }
- };
+ if ((r = vnode._children)) {
+ for (let i = 0; i < r.length; i++) {
+ if (r[i]) {
+ self.unmount(r[i], parentVNode, typeof vnode.type != 'function');
+ }
+ }
+ }
- self.unregister_active_component = function(component) {
- if (typeof(component.data_access) === 'function') {
- self.active_redux_components.pop();
+ if (!skipRemove && vnode._dom != null && vnode._dom !== nil) removeNode(vnode._dom);
+
+ // Must be set to `undefined` to properly clean up `_nextDom`
+ // for which `null` is a valid value. See comment in `create-element.js`
+ vnode._parent = vnode._dom = vnode._nextDom = undefined;
}
- self.active_components.pop();
- };
- self.prop_dictionary = {};
+ function diffChildren(
+ parentDom,
+ renderResult,
+ newParentVNode,
+ oldParentVNode,
+ globalContext,
+ isSvg,
+ excessDomChildren,
+ commitQueue,
+ oldDom,
+ isHydrating
+ ) {
+ let i, j, oldVNode, childVNode, newDom, firstChildDom, refs;
- self.to_native_preact_props = function(ruby_style_props) {
- let result = {};
- let keys = ruby_style_props.$$keys;
- let keys_length = keys.length;
- let key = '';
- for (let i = 0; i < keys_length; i++) {
- key = keys[i];
- let value = ruby_style_props.$$smap[key];
- if (key[0] === 'o' && key[1] === 'n' && key[2] === '_') {
- let type = typeof value;
- if (type === "function") {
- let active_c = self.active_component();
- result[self.lower_camelize(key)] = function(event, info) {
- let ruby_event = self.native_to_ruby_event(event);
- #{`active_c.__ruby_instance`.instance_exec(`ruby_event`, `info`, &`value`)};
+ // This is a compression of oldParentVNode!=null && oldParentVNode != EMPTY_OBJ && oldParentVNode._children || EMPTY_ARR
+ // as EMPTY_OBJ._children should be `undefined`.
+ let oldChildren = (oldParentVNode && oldParentVNode !== nil && oldParentVNode._children) ? oldParentVNode._children : EMPTY_ARR;
+
+ let oldChildrenLength = oldChildren.length;
+
+ newParentVNode._children = [];
+ for (i = 0; i < renderResult.length; i++) {
+ childVNode = renderResult[i];
+
+ if (childVNode === nil || childVNode == null || typeof childVNode == 'boolean' || childVNode.$$is_boolean) {
+ childVNode = newParentVNode._children[i] = null;
+ }
+ // If this newVNode is being reused (e.g. <div>{reuse}{reuse}</div>) in the same diff,
+ // or we are rendering a component (e.g. setState) copy the oldVNodes so it can have
+ // it's own DOM & etc. pointers
+ else if (typeof childVNode == 'string' || typeof childVNode == 'number' || typeof childVNode == 'bigint') {
+ childVNode = newParentVNode._children[i] = self.createVNode(
+ null,
+ childVNode,
+ null,
+ null,
+ childVNode
+ );
+ } else if (childVNode.$$is_string || childVNode.$$is_number) {
+ let str = childVNode.valueOf();
+ childVNode = newParentVNode._children[i] = self.createVNode(
+ null,
+ str,
+ null,
+ null,
+ str
+ );
+ } else if (Array.isArray(childVNode)) {
+ childVNode = newParentVNode._children[i] = self.createVNode(
+ Opal.Fragment,
+ #{{ children: `childVNode` }},
+ null,
+ null,
+ null
+ );
+ } else if (childVNode._depth > 0) {
+ // VNode is already in use, clone it. This can happen in the following
+ // scenario:
+ // const reuse = <div />
+ // <div>{reuse}<span />{reuse}</div>
+ childVNode = newParentVNode._children[i] = self.createVNode(
+ childVNode.type,
+ childVNode.props,
+ childVNode.key,
+ childVNode.ref ? childVNode.ref : null,
+ childVNode._original
+ );
+ } else {
+ childVNode = newParentVNode._children[i] = childVNode;
+ }
+
+ if (childVNode === nil || childVNode == null) {
+ continue;
+ }
+ childVNode._parent = newParentVNode;
+ childVNode._depth = newParentVNode._depth + 1;
+
+ // Check if we find a corresponding element in oldChildren.
+ // If found, delete the array item by setting to `undefined`.
+ // We use `undefined`, as `null` is reserved for empty placeholders
+ // (holes).
+ oldVNode = oldChildren[i];
+
+ if (
+ oldVNode === null || oldVNode === nil ||
+ (oldVNode && oldVNode !== nil &&
+ childVNode.key == oldVNode.key &&
+ childVNode.type === oldVNode.type)
+ ) {
+ oldChildren[i] = undefined;
+ } else {
+ // Either oldVNode === undefined or oldChildrenLength > 0,
+ // so after this loop oldVNode == null or oldVNode is a valid value.
+ for (j = 0; j < oldChildrenLength; j++) {
+ oldVNode = oldChildren[j];
+ // If childVNode is unkeyed, we only match similarly unkeyed nodes, otherwise we match by key.
+ // We always match by type (in either case).
+ if (
+ oldVNode && oldVNode !== nil &&
+ childVNode.key == oldVNode.key &&
+ childVNode.type === oldVNode.type
+ ) {
+ oldChildren[j] = undefined;
+ break;
+ }
+ oldVNode = null;
}
- } else if (typeof value.preact_event_handler_function === "function") {
- result[self.lower_camelize(key)] = value.preact_event_handler_function;
- } else if (type === "string" ) {
- let active_component = self.active_component();
- let method_ref;
- let method_name = '$' + value;
- if (typeof active_component[method_name] === "function") {
- // got a ruby instance
- if (active_component.native?.method_refs?.[value]) { method_ref = active_component.native.method_refs[value]; } // ruby instance with native
- else if (active_component.method_refs?.[value]) { method_ref = active_component.method_refs[value]; } // ruby function component
- else { method_ref = active_component.$method_ref(value); } // create the ref
- } else if (typeof active_component.__ruby_instance[method_name] === "function") {
- // got a native instance
- if (active_component.method_refs?.[value]) { method_ref = active_component.method_refs[value]; }
- else { method_ref = active_component.__ruby_instance.$method_ref(value); } // create ref for native
+ }
+
+ oldVNode = (oldVNode && oldVNode !== nil) ? oldVNode : EMPTY_OBJ;
+
+ // Morph the old element into the new one, but don't append it to the dom yet
+ diff(
+ parentDom,
+ childVNode,
+ oldVNode,
+ globalContext,
+ isSvg,
+ excessDomChildren,
+ commitQueue,
+ oldDom,
+ isHydrating
+ );
+
+ newDom = childVNode._dom;
+
+ if ((j = childVNode.ref) && j !== nil && oldVNode.ref != j) {
+ if (!refs) refs = [];
+ if (oldVNode.ref && oldVNode.ref !== nil) refs.push(oldVNode.ref, null, childVNode);
+ refs.push(j, childVNode._component || newDom, childVNode);
+ }
+
+ if (newDom != null) {
+ if (firstChildDom == null) {
+ firstChildDom = newDom;
}
- if (method_ref) {
- result[self.lower_camelize(key)] = method_ref.preact_event_handler_function;
+
+ if (
+ typeof childVNode.type == 'function' &&
+ childVNode._children === oldVNode._children
+ ) {
+ childVNode._nextDom = oldDom = reorderChildren(
+ childVNode,
+ oldDom,
+ parentDom
+ );
} else {
- let component_name;
- if (active_component.__ruby_instance) { component_name = active_component.__ruby_instance.$to_s(); }
- else { component_name = active_component.$to_s(); }
- #{Isomorfeus.raise_error(message: "Is #{`value`} a valid method of #{`component_name`}? If so then please use: #{`key`}: method_ref(:#{`value`}) within component: #{`component_name`}")}
+ oldDom = placeChild(
+ parentDom,
+ childVNode,
+ oldVNode,
+ oldChildren,
+ newDom,
+ oldDom
+ );
}
- } else if (type === "object" && value === nil) {
- result[self.lower_camelize(key)] = null;
+
+ if (typeof newParentVNode.type == 'function') {
+ // Because the newParentVNode is Fragment-like, we need to set it's
+ // _nextDom property to the nextSibling of its last child DOM node.
+ //
+ // `oldDom` contains the correct value here because if the last child
+ // is a Fragment-like, then oldDom has already been set to that child's _nextDom.
+ // If the last child is a DOM VNode, then oldDom will be set to that DOM
+ // node's nextSibling.
+ newParentVNode._nextDom = oldDom;
+ }
+ } else if (
+ oldDom &&
+ oldVNode._dom == oldDom &&
+ oldDom.parentNode != parentDom
+ ) {
+ // The above condition is to handle null placeholders. See test in placeholder.test.js:
+ // `efficiently replace null placeholders in parent rerenders`
+ oldDom = getDomSibling(oldVNode);
+ }
+ }
+
+ newParentVNode._dom = firstChildDom;
+
+ // Remove remaining oldChildren if there are any.
+ for (i = oldChildrenLength; i--; ) {
+ if (oldChildren[i] != null) {
+ if (
+ typeof newParentVNode.type == 'function' &&
+ oldChildren[i]._dom != null &&
+ oldChildren[i]._dom == newParentVNode._nextDom
+ ) {
+ // If the newParentVNode.__nextDom points to a dom node that is about to
+ // be unmounted, then get the next sibling of that vnode and set
+ // _nextDom to it
+ newParentVNode._nextDom = getDomSibling(oldParentVNode, i + 1);
+ }
+
+ self.unmount(oldChildren[i], oldChildren[i]);
+ }
+ }
+
+ // Set refs only after unmount
+ if (refs) {
+ for (i = 0; i < refs.length; i++) {
+ applyRef(refs[i], refs[++i], refs[++i]);
+ }
+ }
+ }
+
+ function eventProxy(e) {
+ this._listeners[e.type + false].$call(#{Browser::Event.new(`e`)});
+ }
+
+ function eventProxyCapture(e) {
+ this._listeners[e.type + true].$call(#{Browser::Event.new(`e`)});
+ }
+
+ const IS_NON_DIMENSIONAL = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;
+
+ function setStyle(style, key, value) {
+ if (key[0] === '-') {
+ style.setProperty(key, value);
+ } else if (value == null || value === nil) {
+ style[key] = '';
+ } else if (typeof value != 'number' || IS_NON_DIMENSIONAL.test(key)) {
+ style[key] = value;
+ } else {
+ style[key] = value + 'px';
+ }
+ }
+
+ self.setProperty = function(dom, name, value, oldValue, isSvg) {
+ let useCapture;
+
+ o: if (name === 'style') {
+ if (typeof value === 'string') {
+ dom.style.cssText = value;
} else {
- let active_component = self.active_component();
- let component_name;
- if (active_component.__ruby_instance) { component_name = active_component.__ruby_instance.$to_s(); }
- else { component_name = active_component.$to_s(); }
- #{Isomorfeus.raise_error(message: "Received invalid value for #{`key`} with #{`value`} within component: #{`component_name`}")}
- console.error( + key + " event handler:", value, " within component:", self.active_component());
+ if (typeof oldValue === 'string') {
+ dom.style.cssText = oldValue = '';
+ }
+
+ if (value && value !== nil && value["$is_a?"](Opal.Hash)) {
+ value = value.$to_n();
+ }
+
+ if (oldValue && oldValue !== nil && oldValue["$is_a?"](Opal.Hash)) {
+ oldValue = oldValue.$to_n();
+ }
+
+ if (oldValue && oldValue !== nil) {
+ for (name in oldValue) {
+ if (!(value && name in value)) {
+ setStyle(dom.style, name, '');
+ }
+ }
+ }
+
+ if (value && value !== nil) {
+ for (name in value) {
+ if (!oldValue || oldValue === nil || value[name] !== oldValue[name]) {
+ setStyle(dom.style, name, value[name]);
+ }
+ }
+ }
}
- } else if (key[0] === 'a' && key.startsWith("aria_")) {
- result[key.replace("_", "-")] = value;
- } else if (key === "style" || key === "theme") {
- if (typeof value.$to_n === "function") { value = value.$to_n() }
- result[key] = value;
+ }
+ // Benchmark for comparison: https://esbench.com/bench/574c954bdb965b9a00965ac6
+ else if (name[0] === 'o' && name[1] === 'n' && name[2] === '_') {
+ useCapture = name !== (name = name.replace(/_capture$/, ''));
+
+ // Infer correct casing for DOM built-in events:
+ let namesl = name.slice(3);
+ let domname = 'on' + namesl;
+ if (domname in dom) name = namesl;
+ else name = namesl;
+
+ let evhandler = value;
+
+ if (!dom._listeners) dom._listeners = {};
+ dom._listeners[name + useCapture] = evhandler;
+
+ if (value && value !== nil) {
+ if (!oldValue || oldValue === nil) {
+ const handler = useCapture ? eventProxyCapture : eventProxy;
+ dom.addEventListener(name, handler, useCapture);
+ }
+ } else {
+ const handler = useCapture ? eventProxyCapture : eventProxy;
+ dom.removeEventListener(name, handler, useCapture);
+ }
+ } else if (name !== 'dangerouslySetInnerHTML') {
+ if (isSvg) {
+ // Normalize incorrect prop usage for SVG:
+ // - xlink:href / xlinkHref --> href (xlink:href was removed from SVG and isn't needed)
+ // - className --> class
+ name = name.replace(/xlink(H|:h)/, 'h').replace(/sName$/, 's');
+ } else if (
+ name !== 'href' &&
+ name !== 'list' &&
+ name !== 'form' &&
+ // Default value in browsers is `-1` and an empty string is
+ // cast to `0` instead
+ name !== 'tabIndex' &&
+ name !== 'download' &&
+ name in dom
+ ) {
+ try {
+ dom[name] = (value == null || value === nil) ? '' : value;
+ // labelled break is 1b smaller here than a return statement (sorry)
+ break o;
+ } catch (e) {}
+ }
+
+ // ARIA-attributes have a different notion of boolean values.
+ // The value `false` is different from the attribute not
+ // existing on the DOM, so we can't remove it. For non-boolean
+ // ARIA-attributes we could treat false as a removal, but the
+ // amount of exceptions would cost us too many bytes. On top of
+ // that other VDOM frameworks also always stringify `false`.
+
+ if (typeof value === 'function') {
+ // never serialize functions as attribute values
+ } else if (
+ value != null && value !== nil &&
+ (value !== false || (name[0] === 'a' && name[1] === 'r'))
+ ) {
+ dom.setAttribute(name, value);
+ } else {
+ dom.removeAttribute(name);
+ }
+ }
+ }
+
+ function diff_props(dom, new_props, old_props, is_svg, hydrate) {
+ #{`old_props`.each do |prop, value|
+ `if (prop !== "children" && prop !== "key" && !(prop.$$is_string && Object.hasOwnProperty.call(new_props.$$smap, prop))) { self.setProperty(dom, prop, null, value, is_svg); }`
+ nil
+ end
+ `new_props`.each do |prop, value|
+ if (`!hydrate || (prop[0] === 'o' && prop[1] === 'n' && prop[2] === '_')` || value.is_a?(Proc)) &&
+ `prop !== "children" && prop !== "key" && prop !== "value" && prop !== "checked"` &&
+ ((p = `old_props`[prop]) ? p : nil) != value
+ `self.setProperty(dom, prop, value, old_props["$[]"](prop), is_svg)`
+ end
+ end
+ }
+ }
+
+ function diffElementNodes(
+ dom,
+ newVNode,
+ oldVNode,
+ globalContext,
+ isSvg,
+ excessDomChildren,
+ commitQueue,
+ isHydrating
+ ) {
+ let oldProps = oldVNode.props;
+ let newProps = newVNode.props;
+ let nodeType = newVNode.type;
+ let i = 0;
+
+ // Tracks entering and exiting SVG namespace when descending through the tree.
+ if (nodeType === 'svg') isSvg = true;
+
+ if (excessDomChildren != null) {
+ for (; i < excessDomChildren.length; i++) {
+ const child = excessDomChildren[i];
+
+ // if newVNode matches an element in excessDomChildren or the `dom`
+ // argument matches an element in excessDomChildren, remove it from
+ // excessDomChildren so it isn't later removed in diffChildren
+ if (
+ child &&
+ 'setAttribute' in child === !!nodeType &&
+ (nodeType ? child.localName === nodeType : child.nodeType === 3)
+ ) {
+ dom = child;
+ excessDomChildren[i] = null;
+ break;
+ }
+ }
+ }
+
+ if (dom == null) {
+ if (nodeType === null) {
+ // createTextNode returns Text, we expect PreactElement
+ return document.createTextNode(newProps);
+ }
+
+ if (isSvg) {
+ dom = document.createElementNS(
+ 'http://www.w3.org/2000/svg',
+ // We know `newVNode.type` is a string
+ nodeType
+ );
+ } else {
+ let np = newProps.$to_n();
+ dom = document.createElement(
+ // We know `newVNode.type` is a string
+ nodeType,
+ np.is && np
+ );
+ }
+
+ // we created a new parent, so none of the previously attached children can be reused:
+ excessDomChildren = null;
+ // we are creating a new node, so we can assume this is a new subtree (in case we are hydrating), this deopts the hydrate
+ isHydrating = false;
+ }
+
+ if (nodeType === null) {
+ // During hydration, we still have to split merged text from SSR'd HTML.
+ if (!oldProps || oldProps === nil || (!oldProps["$=="](newProps) && (!isHydrating || !dom.data["$=="](newProps)))) {
+ dom.data = newProps;
+ }
} else {
- result[self.lower_camelize(key)] = value;
+ // If excessDomChildren was not null, repopulate it with the current element's children:
+ excessDomChildren = excessDomChildren && slice.call(dom.childNodes);
+
+ oldProps = oldVNode.props || Opal.Hash.$new();
+
+ let oldHtml = oldProps["$[]"]("dangerouslySetInnerHTML");
+ let newHtml = newProps["$[]"]("dangerouslySetInnerHTML");
+
+ // During hydration, props are not diffed at all (including dangerouslySetInnerHTML)
+ // @TODO we should warn in debug mode when props don't match here.
+ if (!isHydrating) {
+ // But, if we are in a situation where we are using existing DOM (e.g. replaceNode)
+ // we should read the existing DOM attributes to diff them
+ if (excessDomChildren != null) {
+ oldProps = Opal.Hash.$new();
+ for (i = 0; i < dom.attributes.length; i++) {
+ oldProps["$[]="](dom.attributes[i].name, dom.attributes[i].value);
+ }
+ }
+
+ if (newHtml !== nil || oldHtml !== nil) {
+ // Avoid re-applying the same '__html' if it has not changed between re-render
+ if (
+ newHtml === nil ||
+ ((oldHtml === nil || newHtml["$[]"]("__html") != oldHtml["$[]"]("__html")) &&
+ newHtml["$[]"]("__html") !== dom.innerHTML)
+ ) {
+ dom.innerHTML = (newHtml !== nil && newHtml["$[]"]("__html")) || '';
+ }
+ }
+ }
+
+ diff_props(dom, newProps, oldProps, isSvg, isHydrating);
+
+ // If the new vnode didn't have dangerouslySetInnerHTML, diff its children
+ if (newHtml !== nil) {
+ newVNode._children = [];
+ } else {
+ i = newVNode.props["$[]"]("children");
+ diffChildren(
+ dom,
+ Array.isArray(i) ? i : [i],
+ newVNode,
+ oldVNode,
+ globalContext,
+ isSvg && nodeType !== 'foreignObject',
+ excessDomChildren,
+ commitQueue,
+ excessDomChildren
+ ? excessDomChildren[0]
+ : oldVNode._children && getDomSibling(oldVNode, 0),
+ isHydrating
+ );
+
+ // Remove children that are not part of any vnode.
+ if (excessDomChildren != null) {
+ for (i = excessDomChildren.length; i--; ) {
+ if (excessDomChildren[i] != null) removeNode(excessDomChildren[i]);
+ }
+ }
+ }
+
+ // (as above, don't diff props during hydration)
+ if (!isHydrating) {
+ if (
+ // instead of newProps["$key?"]("value")
+ Object.hasOwnProperty.call(newProps.$$smap, "value") &&
+ (i = newProps["$[]"]("value")) !== nil &&
+ // #2756 For the <progress>-element the initial value is 0,
+ // despite the attribute not being present. When the attribute
+ // is missing the progress bar is treated as indeterminate.
+ // To fix that we'll always update it when it is 0 for progress elements
+ (i !== dom.value || (nodeType === 'progress' && !i) ||
+ // This is only for IE 11 to fix <select> value not being updated.
+ // To avoid a stale select value we need to set the option.value
+ // again, which triggers IE11 to re-evaluate the select value
+ (nodeType === 'option' && i !== oldProps["$[]"]("value")))
+ ) {
+ self.setProperty(dom, 'value', i, oldProps["$[]"]("value"), false, null);
+ }
+ if (
+ Object.hasOwnProperty.call(newProps.$$smap, "checked") &&
+ (i = newProps["$[]"]("checked")) !== nil &&
+ i !== dom.checked
+ ) {
+ self.setProperty(dom, 'checked', i, oldProps["$[]"]("checked"), false, null);
+ }
+ }
}
+
+ return dom;
}
- return result;
- };
- self.render_block_result = function(block_result) {
- if (block_result.constructor === String || block_result.constructor === Number) {
- Opal.Preact.render_buffer[Opal.Preact.render_buffer.length - 1].push(block_result);
+ function validate_props(newType, newProps) {
+ if (newType.declared_props && newType.declared_props !== nil) {
+ #{
+ `newType.declared_props`.each do |prop, value|
+ `newProps`[prop] = value[:default] if value.key?(:default) && !`newProps`.key?(prop)
+ end
+ }
+ if (Opal.Isomorfeus.development) { #{`newType`.validate_props(`newProps`)} }
+ }
}
- };
- self.internal_render = function(component, props, string_child, block) {
- const oper = Opal.global.Preact;
- const operabu = self.render_buffer;
- let native_props;
- if (props && props !== nil) { native_props = self.to_native_preact_props(props); }
- if (string_child) {
- operabu[operabu.length - 1].push(oper.createElement(component, native_props, string_child));
- } else if (block && block !== nil) {
- operabu.push([]);
- // console.log("internal_render pushed", Opal.Preact.render_buffer, Opal.Preact.render_buffer.toString());
- let block_result = block.$call();
- if (block_result && block_result !== nil) { Opal.Preact.render_block_result(block_result); }
- // console.log("internal_render popping", Opal.Preact.render_buffer, Opal.Preact.render_buffer.toString());
- let children = operabu.pop();
- operabu[operabu.length - 1].push(oper.createElement.apply(this, [component, native_props].concat(children)));
- } else {
- operabu[operabu.length - 1].push(oper.createElement(component, native_props));
+ function diff(
+ parentDom,
+ newVNode,
+ oldVNode,
+ globalContext,
+ isSvg,
+ excessDomChildren,
+ commitQueue,
+ oldDom,
+ isHydrating
+ ) {
+ let newType = newVNode.type;
+
+ // When passing through createElement it assigns the object
+ // constructor as undefined. This to prevent JSON-injection.
+ if (newVNode.constructor !== undefined) return null;
+
+ // If the previous diff bailed out, resume creating/hydrating.
+ if (oldVNode._hydrating != null) {
+ isHydrating = oldVNode._hydrating;
+ oldDom = newVNode._dom = oldVNode._dom;
+ // if we resume, we want the tree to be "unlocked"
+ newVNode._hydrating = null;
+ excessDomChildren = [oldDom];
+ }
+
+ try {
+ outer: if (typeof newType == 'function') {
+ let c, ctxType, isNew, oldProps, oldState, renderResult, snapshot, clearProcessingException;
+ let newProps = newVNode.props;
+
+ // Necessary for createContext api. Setting this property will pass
+ // the context value as `this.context` just for this component.
+ ctxType = newType.context_type;
+ let provider = (ctxType && ctxType !== nil) && globalContext["$[]"](ctxType.context_id);
+ let componentContext = (ctxType && ctxType !== nil) ? ((provider && provider !== nil) ? provider.props["$[]"]("value") : ctxType.value) : globalContext;
+
+ // Get component and set it to `c`
+ if (oldVNode._component) {
+ c = newVNode._component = oldVNode._component;
+ clearProcessingException = c._processingException = c._pendingError;
+ } else {
+ // Instantiate the new component
+ // validate props
+ validate_props(newType, newProps);
+ // The check above verifies that newType is suppose to be constructed
+ newVNode._component = c = newType.$new(newProps, componentContext);
+
+ if (provider && provider !== nil) provider.$sub(c);
+
+ c.props = newProps;
+ if (c.state === nil || !c.state) c.state = Opal.Hash.$new();
+ c.context = componentContext;
+ c._globalContext = globalContext;
+ isNew = c._dirty = true;
+ c._renderCallbacks = [];
+ }
+
+ // Invoke get_derived_state_from_props
+ if (c._nextState === nil) {
+ c._nextState = c.state;
+ }
+
+ if (!isNew) { validate_props(newType, newProps); }
+ if (c["$respond_to?"]("get_derived_state_from_props")) {
+ if (c._nextState == c.state) {
+ c._nextState = c._nextState.$dup();
+ }
+ c._nextState["$merge!"](c.$get_derived_state_from_props(newProps, c._nextState));
+ }
+
+ oldProps = c.props;
+ oldState = c.state;
+
+ // Invoke pre-render lifecycle methods
+ if (isNew) {
+ if (c["$respond_to?"]("component_did_mount")) {
+ c._renderCallbacks.push(c.$component_did_mount);
+ }
+ } else {
+ if (
+ (!c._force &&
+ c["$respond_to?"]("should_component_update?") &&
+ c["$should_component_update?"](
+ newProps,
+ c._nextState,
+ componentContext
+ ) === false) ||
+ newVNode._original === oldVNode._original
+ ) {
+ c.props = newProps;
+ c.state = c._nextState;
+ // More info about this here: https://gist.github.com/JoviDeCroock/bec5f2ce93544d2e6070ef8e0036e4e8
+ if (newVNode._original !== oldVNode._original) c._dirty = false;
+ c._vnode = newVNode;
+ newVNode._dom = oldVNode._dom;
+ newVNode._children = oldVNode._children;
+ newVNode._children.forEach(vnode => {
+ if (vnode) vnode._parent = newVNode;
+ });
+ if (c._renderCallbacks.length) {
+ commitQueue.push(c);
+ }
+
+ break outer;
+ }
+
+ if (c["$respond_to?"]("component_did_update")) {
+ c._renderCallbacks.push(() => {
+ c.$component_did_update(oldProps, oldState, snapshot);
+ });
+ }
+ }
+
+ c.context = componentContext;
+ c.props = newProps;
+ c._vnode = newVNode;
+ c._parentDom = parentDom;
+ c.state = c._nextState;
+ c._dirty = false;
+
+ renderResult = c.$render();
+
+ // Handle setState called in render, see #2553
+ c.state = c._nextState;
+
+ if (c.getChildContext != null) {
+ globalContext = globalContext.$merge(c.$get_child_context());
+ }
+
+ if (!isNew && c["$respond_to?"]("get_snapshot_before_update")) {
+ snapshot = c.$get_snapshot_before_update(oldProps, oldState);
+ }
+
+ if (renderResult !== nil && renderResult != null && renderResult.type === Opal.Fragment && renderResult.key == null) {
+ renderResult = renderResult.props["$[]"]("children");
+ }
+
+ diffChildren(
+ parentDom,
+ Array.isArray(renderResult) ? renderResult : [renderResult],
+ newVNode,
+ oldVNode,
+ globalContext,
+ isSvg,
+ excessDomChildren,
+ commitQueue,
+ oldDom,
+ isHydrating
+ );
+
+ c.base = newVNode._dom;
+
+ // We successfully rendered this VNode, unset any stored hydration/bailout state:
+ newVNode._hydrating = null;
+
+ if (c._renderCallbacks.length) {
+ commitQueue.push(c);
+ }
+
+ if (clearProcessingException) {
+ c._pendingError = c._processingException = null;
+ }
+
+ c._force = false;
+
+ } else if (
+ excessDomChildren == null &&
+ newVNode._original === oldVNode._original
+ ) {
+ newVNode._children = oldVNode._children;
+ newVNode._dom = oldVNode._dom;
+ } else {
+ newVNode._dom = diffElementNodes(
+ oldVNode._dom,
+ newVNode,
+ oldVNode,
+ globalContext,
+ isSvg,
+ excessDomChildren,
+ commitQueue,
+ isHydrating
+ );
+ }
+ } catch (e) {
+ newVNode._original = null;
+ // if hydrating or creating initial tree, bailout preserves DOM:
+ if (isHydrating || excessDomChildren != null) {
+ newVNode._dom = oldDom;
+ newVNode._hydrating = !!isHydrating;
+ excessDomChildren[excessDomChildren.indexOf(oldDom)] = null;
+ // ^ could possibly be simplified to:
+ // excessDomChildren.length = 0;
+ }
+ _catchError(e, newVNode, oldVNode);
+ }
}
- };
- self.deep_force_update = function(vnode) {
- vnode?.__c?.forceUpdate?.();
- if (vnode?.__k) {
- for (let i=0; i<vnode.__k.length; i++) {
- self.deep_force_update(vnode.__k[i]);
+ function commitRoot(commitQueue, root) {
+ commitQueue.some(c => {
+ try {
+ // Reuse the commitQueue variable here so the type changes
+ commitQueue = c._renderCallbacks;
+ c._renderCallbacks = [];
+ commitQueue.some(cb => {
+ // See above comment on commitQueue
+ cb.call(c);
+ });
+ } catch (e) {
+ _catchError(e, c._vnode);
+ }
+ });
+ }
+
+ let vnodeId = 0;
+ const vnode_class = #{VNode};
+
+ function is_a_vnode(type) { return type === vnode_class; }
+ function is_nil() { return false; }
+
+ self.createVNode = function(type, props, key, ref, original) {
+ // V8 seems to be better at detecting type shapes if the object is allocated from the same call site
+ // Do not inline into createElement and coerceToVNode!
+ let eql;
+ const vnode = {
+ type,
+ props,
+ key,
+ ref,
+ _children: null,
+ _parent: null,
+ _depth: 0,
+ _dom: null,
+ // _nextDom must be initialized to undefined b/c it will eventually
+ // be set to dom.nextSibling which can return `null` and it is important
+ // to be able to distinguish between an uninitialized _nextDom and
+ // a _nextDom that has been set to `null`
+ _nextDom: undefined,
+ _component: null,
+ _hydrating: null,
+ constructor: undefined,
+ _original: original == null ? ++vnodeId : original,
+ "$is_a?": is_a_vnode,
+ "$==": eql = function(other) {
+ for(let prop in vnode) {
+ if (prop === 'props') {
+ let res = vnode[prop]["$=="](other[prop]);
+ if (!res) return false;
+ } else if (vnode[prop] != other[prop]) {
+ return false;
+ }
+ }
+ return true;
+ },
+ "$eql?": eql,
+ "$nil?": is_nil
+ };
+
+ return vnode;
+ }
+
+ self.render = function(vnode, parentDom, replaceNode) {
+ // We abuse the `replaceNode` parameter in `hydrate()` to signal if we are in
+ // hydration mode or not by passing the `hydrate` function instead of a DOM
+ // element..
+ let isHydrating = typeof replaceNode === 'function';
+
+ let reno = (replaceNode !== nil && replaceNode);
+ let nohy_reno = (!isHydrating && reno);
+
+ // To be able to support calling `render()` multiple times on the same
+ // DOM node, we need to obtain a reference to the previous tree. We do
+ // this by assigning a new `_children` property to DOM nodes which points
+ // to the last rendered tree. By default this property is not present, which
+ // means that we are mounting a new tree for the first time.
+ let oldVNode = isHydrating
+ ? null
+ : (reno && replaceNode._children) || parentDom._children;
+
+ let ov = (oldVNode && oldVNode !== nil);
+
+ vnode = (
+ nohy_reno || parentDom
+ )._children = self.$create_element(Opal.Fragment, nil, [vnode]);
+
+ // List of effects that need to be called after diffing.
+ let commitQueue = [];
+ diff(
+ parentDom,
+ // Determine the new vnode tree and store it on the DOM element on
+ // our custom `_children` property.
+ vnode,
+ ov ? oldVNode : EMPTY_OBJ,
+ Opal.Hash.$new(),
+ parentDom.ownerSVGElement !== undefined,
+ nohy_reno ? [replaceNode] : ov ? null : parentDom.firstChild ? slice.call(parentDom.childNodes) : null,
+ commitQueue,
+ nohy_reno ? replaceNode : ov ? oldVNode._dom : parentDom.firstChild,
+ isHydrating
+ );
+
+ // Flush all queued effects
+ commitRoot(commitQueue, vnode);
+ };
+
+ function updateParentDomPointers(vnode) {
+ if ((vnode = vnode._parent) != null && vnode._component != null) {
+ vnode._dom = vnode._component.base = null;
+ for (let i = 0; i < vnode._children.length; i++) {
+ let child = vnode._children[i];
+ if (child != null && child._dom != null) {
+ vnode._dom = vnode._component.base = child._dom;
+ break;
+ }
+ }
+
+ return updateParentDomPointers(vnode);
}
}
- };
- }
- def self.create_element(type, props = nil, children = nil, &block)
- %x{
- const operabu = self.render_buffer;
- let component = null;
- let native_props = null;
- if (typeof type.preact_component !== 'undefined') { component = type.preact_component; }
- else { component = type; }
- if (block !== nil) {
- operabu.push([]);
- // console.log("create_element pushed", Opal.Preact.render_buffer, Opal.Preact.render_buffer.toString());
- let block_result = block.$call();
- if (block_result && block_result !== nil) { Opal.Preact.render_block_result(block_result); }
- // console.log("create_element popping", Opal.Preact.render_buffer, Opal.Preact.render_buffer.toString());
- children = operabu.pop();
- } else if (children === nil) { children = []; }
- else if (typeof children === 'string') { children = [children]; }
- if (props && props !== nil) { native_props = self.to_native_preact_props(props); }
- return Opal.global.Preact.createElement.apply(this, [component, native_props].concat(children));
+ function renderComponent(component) {
+ let vnode = component._vnode,
+ oldDom = vnode._dom,
+ parentDom = component._parentDom;
+
+ if (parentDom) {
+ let commitQueue = [];
+ const oldVNode = assign({}, vnode);
+ oldVNode._original = vnode._original + 1;
+
+ diff(
+ parentDom,
+ vnode,
+ oldVNode,
+ component._globalContext,
+ parentDom.ownerSVGElement !== undefined,
+ vnode._hydrating != null ? [oldDom] : null,
+ commitQueue,
+ (oldDom == null || oldDom === nil) ? getDomSibling(vnode) : oldDom,
+ vnode._hydrating
+ );
+ commitRoot(commitQueue, vnode);
+
+ if (vnode._dom != oldDom) {
+ updateParentDomPointers(vnode);
+ }
+ }
+ }
+
+ self.process = function() {
+ let queue;
+ while ((self.process._rerenderCount = self.rerender_queue.length)) {
+ queue = self.rerender_queue.sort((a, b) => a._vnode._depth - b._vnode._depth);
+ self.rerender_queue = [];
+ // Don't update `renderCount` yet. Keep its value non-zero to prevent unnecessary
+ // process() calls from getting scheduled while `queue` is still being consumed.
+ queue.some(c => {
+ if (c._dirty) renderComponent(c);
+ });
+ }
+ }
+ self.process._rerenderCount = 0;
}
+ else
+ IGNORED_PROPS = %i[key ref __self __source].freeze
+ IS_NON_DIMENSIONAL = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|^--/i
+ JS_TO_CSS = {}
+ ENCODED_ENTITIES = /[&<>"]/
+ VOID_ELEMENTS = /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/
end
- def self.to_child_array(props_children)
- `Opal.global.Preact.toChildArray(children)`
- end
+ class << self
+ def _ctxi
+ @_ctxi ||= 0
+ end
- def self.clone_element(native_preact_element, props = nil, children = nil, &block)
- block_result = `null`
- if block_given?
- block_result = block.call
- block_result = `null` unless block_result
+ def _ctxi=(i)
+ @_ctxi = i
end
- native_props = props ? `Opal.Preact.to_native_preact_props(props)` : `null`
- `Opal.global.Preact.cloneElement(native_preact_element, native_props, block_result)`
- end
- def self.create_context(const_name, default_value)
- %x{
- Opal.global[const_name] = Opal.global.Preact.createContext(default_value);
- var new_const = #{Preact::ContextWrapper.new(`Opal.global[const_name]`)};
- #{Object.const_set(const_name, `new_const`)};
- return new_const;
- }
- end
+ def _context_id
+ "__cC#{self._ctxi += 1}"
+ end
- def self.create_ref
- Preact::Ref.new(`Opal.global.Preact.createRef()`)
- end
+ def clone_element(vnode, props = nil, children = nil)
+ normalized_props = {}
+ if RUBY_ENGINE == 'opal'
+ normalized_props.merge!(`vnode.props`)
+ else
+ normalized_props.merge!(vnode.props)
+ end
- def self.hydrate(native_preact_element, container_node, replace_node)
- `Opal.global.Preact.hydrate(native_preact_element, container_node)`
- end
+ if props
+ normalized_props.merge!(props)
+ key = normalized_props.delete(:key)
+ ref = normalized_props.delete(:ref)
+ else
+ key = nil
+ ref = nil
+ end
- def self.location_hook(location)
- %x{
- if (Opal.global.locationHook) { return Opal.global.locationHook; }
- else if (Opal.global.staticLocationHook) { return Opal.global.staticLocationHook(location); }
- else { #{raise "Wouter Location Hooks not imported!"}; }
- }
- end
+ normalized_props[:children] = children unless children.nil?
- def self.render(native_preact_element, container_node, replace_node)
- # container is a native DOM element
- if block_given?
- `Opal.global.Preact.render(native_preact_element, container_node, function() { block.$call() })`
- else
- `Opal.global.Preact.render(native_preact_element, container_node)`
+ if RUBY_ENGINE == 'opal'
+ `self.createVNode(vnode.type, normalized_props, #{key || `vnode.key`}, #{ref || `vnode.ref`}, null)`
+ else
+ _create_vnode(vnode.type, normalized_props, key || vnode.key, ref || vnode.ref, nil)
+ end
end
- end
- # render_to_string is in top_level_ssr.rb
+ def create_element(type, props = nil, children = nil, &block)
+ if props
+ if props.is_a?(Hash)
+ normalized_props = props.dup
+ key = normalized_props.delete(:key)
+ ref = normalized_props.delete(:ref)
+ else
+ children = props
+ normalized_props = {}
+ key = nil
+ ref = nil
+ end
+ else
+ normalized_props = {}
+ key = nil
+ ref = nil
+ end
- def self.unmount_component_at_node(element_or_query)
- if `(typeof element_or_query === 'string')` || (`(typeof element_or_query.$class === 'function')` && element_or_query.class == String)
- element = `document.body.querySelector(element_or_query)`
- elsif `(typeof element_or_query.$is_a === 'function')` && element_or_query.is_a?(Browser::Element)
- element = element_or_query.to_n
- else
- element = element_or_query
+ if block_given?
+ pr = render_buffer
+ pr.push([])
+ block_result = block.call
+ children = pr.pop
+ if Preact.is_renderable?(block_result)
+ children.push(block_result)
+ end
+ end
+
+ normalized_props[:children] = children unless children.nil?
+
+ if RUBY_ENGINE == 'opal'
+ `self.createVNode(type, normalized_props, key, ref, null)`
+ else
+ _create_vnode(type, normalized_props, key, ref, nil)
+ end
end
- `Opal.global.Preact.render(null, element)`
+
+ def create_context(const_name, default_value = nil)
+ context = Preact::Context.new(default_value)
+ Object.const_set(const_name, context)
+ end
+
+ def _init_render
+ self.render_buffer = []
+ self.rerender_queue = []
+ Isomorfeus.reset_something_loading
+ end
+
+ def is_renderable?(block_result)
+ block_result &&
+ (block_result.is_a?(VNode) || block_result.is_a?(String) || block_result.is_a?(Numeric) ||
+ (block_result.is_a?(Array) && block_result.length > 0 && is_renderable?(block_result[0])))
+ end
+
+ if RUBY_ENGINE == 'opal'
+ attr_accessor :_vnode_id
+ attr_accessor :render_buffer
+ attr_accessor :rerender_queue
+
+ def _enqueue_render(c)
+ if ((`!c._dirty` && (`c._dirty = true`) && rerender_queue.push(c) && `!self.process._rerenderCount++`))
+ `setTimeout(self.process)`
+ end
+ end
+
+ def _render_element(component, props, &block)
+ %x{
+ let opr = Opal.Preact.render_buffer;
+ opr[opr.length-1].push(#{self.create_element(component, props, nil, &block)});
+ }
+ nil
+ end
+
+ def hydrate(vnode, container_node)
+ render(vnode, container_node, `self.render`)
+ end
+
+ def element_or_query_to_n(element_or_query)
+ if `!element_or_query || element_or_query === nil`
+ return `null`
+ elsif `(element_or_query instanceof HTMLElement)`
+ return element_or_query
+ elsif `(typeof element_or_query === 'string')` || element_or_query.is_a?(String)
+ return `document.body.querySelector(element_or_query)`
+ elsif `(typeof element_or_query === 'function')`
+ return element_or_query
+ elsif element_or_query.is_a?(Browser::Element)
+ return element_or_query.to_n
+ else
+ return element_or_query
+ end
+ end
+
+ def render(vnode, container_node, replace_node = nil)
+ _init_render
+ container_node = element_or_query_to_n(container_node)
+ replace_node = element_or_query_to_n(replace_node)
+ `self.render(vnode, container_node, replace_node)`
+ end
+
+ def unmount_component_at_node(element_or_query)
+ element_or_query = element_or_query_to_n(element_or_query)
+ `self.render(null, element_or_query)`
+ end
+ else # RUBY_ENGINE
+ def _vnode_id
+ Thread.current[:@_isomorfeus_preact_vnode_id] ||= 0
+ end
+
+ def _vnode_id=(i)
+ Thread.current[:@_isomorfeus_preact_vnode_id] = i
+ end
+
+ def render_buffer
+ Thread.current[:@_isomorfeus_preact_render_buffer]
+ end
+
+ def render_buffer=(i)
+ Thread.current[:@_isomorfeus_preact_render_buffer] = i
+ end
+
+ def rerender_queue
+ Thread.current[:@_isomorfeus_preact_rerender_queue]
+ end
+
+ def rerender_queue=(i)
+ Thread.current[:@_isomorfeus_preact_rerender_queue] = i
+ end
+
+ def _create_vnode(type, props, key, ref, original)
+ VNode.new(type, props, key, ref, original)
+ end
+
+ def _encode_entities(input)
+ s = input.to_s
+ return s unless ENCODED_ENTITIES.match?(s)
+ s.gsub(/&/, '&').gsub(/</, '<').gsub(/>/, '>').gsub(/"/, '"')
+ # TODO performance maybe, maybe similar to new js way, need to measure
+ # for (; i<str.length; i++) {
+ # switch (str.charCodeAt(i)) {
+ # case 60: ch = '<'; break;
+ # case 62: ch = '>'; break;
+ # case 34: ch = '"'; break;
+ # case 38: ch = '&'; break;
+ # default: continue;
+ # }
+ # if (i > start) out += str.slice(start, i);
+ # out += ch;
+ # start = i + 1;
+ # }
+ end
+
+ def _get_children(accumulator, children)
+ if children.is_a?(Array)
+ children.reduce(accumulator) { |accumulator, child| _get_children(accumulator, child) }
+ elsif children != nil && children != false
+ accumulator.push(children)
+ end
+ accumulator
+ end
+
+ def _style_obj_to_css(v)
+ str = ''
+ v.each do |prop, val|
+ if val != nil && val != ''
+ str << ' ' if !str.empty?
+ prop_s = prop.to_s
+ str << prop_s[0] == '-' ? prop_s : JS_TO_CSS[prop] || (JS_TO_CSS[prop] = prop.gsub(/([A-Z])/, "-\\1").downcase)
+ str << ': '
+ str << "#{val}"
+ str << 'px' if val.is_a?(Numeric) && IS_NON_DIMENSIONAL.match?(prop_s) == false
+ str << ';'
+ end
+ end
+ return str.empty? ? nil : str
+ end
+
+ def _render_element(element, props, &block)
+ pr = Preact.render_buffer
+ pr[pr.length-1].push(create_element(element, props, nil, &block))
+ nil
+ end
+
+ def _render_to_string(vnode, context, opts, inner = nil, is_svg_mode = false, select_value = nil)
+ return '' if !vnode || vnode == true
+
+ if vnode.is_a?(String) # text nodes
+ return _encode_entities(vnode)
+ elsif vnode.is_a?(Numeric) # numeric nodes
+ return vnode.to_s
+ end
+
+ if vnode.is_a?(Array) # array of nodes
+ rendered = ''
+ vnode.each_index do |i|
+ rendered << _render_to_string(vnode[i], context, opts, inner, is_svg_mode, select_value)
+ end
+ return rendered
+ end
+
+ node_name = vnode.type
+ props = vnode.props
+ is_component = false
+ is_fragment = false
+
+ # components
+ if !node_name.is_a?(String) && (node_name.ancestors.include?(Preact::Component) || is_fragment = node_name.ancestors.include?(Fragment))
+ is_component = true
+ if opts && opts[:shallow] && (inner || opts[:render_root_component] == false)
+ node_name = node_name.class.to_s
+ elsif is_fragment
+ children = []
+ _get_children(children, vnode.props[:children])
+ return _render_to_string(children, context, opts, opts&.fetch(:shallow_high_order) != false, is_svg_mode, select_value)
+ else
+ rendered = nil
+
+ vnode.__c = {
+ __v: vnode,
+ context: context,
+ props: vnode.props,
+ __d: true
+ }
+
+ c = vnode.__c
+ # silently drop state updates
+ mark_as_dirty = proc { c[:__d] = true }
+ c[:set_state] = mark_as_dirty,
+ c[:force_update] = mark_as_dirty,
+
+ # class-based components
+ cx_type = node_name.context_type
+ provider = cx_type && context[cx_type.context_id]
+ cctx = cx_type ? (provider ? provider.props[:value] : cx_type.value) : context
+
+ # validate props
+ declared_props = node_name.instance_variable_get(:@declared_props)
+ if declared_props
+ declared_props.each do |prop, value|
+ props[prop] = value[:default] if !props.key?(prop) && value.key?(:default)
+ end
+ node_name.validate_props(props) if Isomorfeus.development?
+ end
+
+ c = vnode.__c = node_name.new(props, cctx)
+ c.__v = vnode
+ # turn off stateful re-rendering:
+ c._dirty = c.__d = true
+ c.instance_variable_set(:@props, props) if c.props.nil?
+ c.instance_variable_set(:@state, {}) if c.state.nil?
+
+ c._next_state = c.__s = c.state if c._next_state == nil && c.__s == nil
+
+ c.instance_variable_set(:@context, cctx)
+
+ if c.respond_to?(:get_derived_state_from_props)
+ c.instance_variable_set(:@state, {}.merge!(c.state, c.get_derived_state_from_props(c.props, c.state)))
+ end
+
+ rendered = c.render
+
+ context = {}.merge!(context, c.get_child_context) if c.respond_to?(:get_child_context)
+
+ return _render_to_string(rendered, context, opts, opts&.fetch(:shallow_high_order) != false, is_svg_mode, select_value)
+ end
+ end
+
+ # render JSX to HTML
+ node_name_s = node_name.to_s
+ s = "<#{node_name_s}"
+ prop_children = nil
+ html = nil
+
+ if props
+ attrs = props.keys
+
+ # allow sorting lexicographically for more determinism (useful for tests, such as via preact-jsx-chai)
+ attrs.sort! if opts && opts[:sort_attributes] == true
+
+ attrs.each do |name|
+ v = props[name]
+ if name == :children
+ prop_children = v
+ next
+ end
+
+ next if UNSAFE_NAME.match?(name.to_s)
+ next if !(opts && opts[:all_attributes]) && IGNORED_PROPS.include?(name)
+
+ if name == :default_value
+ name = :value
+ elsif name == :class_name
+ next if props[:class] != nil
+ name = :class
+ elsif is_svg_mode && name.to_s.match?(/^xlink:?./)
+ name = name.to_s.downcase.gsub(/^xlink:?/, 'xlink:')
+ end
+
+ if name == :html_for
+ next if props.key?(:for)
+ name = :for
+ end
+
+ v = _style_obj_to_css(v) if name == :style && v && v.is_a?(Hash)
+
+ # always use string values instead of booleans for aria attributes
+ # also see https://github.com/preactjs/preact/pull/2347/files
+ v = v.to_s if name.to_s.start_with?('aria') && (v == true || v == false)
+
+ if name == :dangerouslySetInnerHTML
+ html = v && v[:__html]
+ elsif node_name == :textarea && name == :value
+ # <textarea value="a&b"> --> <textarea>a&b</textarea>
+ prop_children = v
+ elsif name.to_s.start_with?('on_')
+ next
+ elsif v || v == 0 || v == '' && !v.is_a?(Proc)
+ if v == true || v == ''
+ v = name
+ # in non-xml mode, allow boolean attributes
+ if !opts || !opts[:xml]
+ s << " #{name}"
+ next
+ end
+ end
+
+ if name == :value
+ if node_name == :select
+ select_value = v
+ next
+ elsif (
+ # If we're looking at an <option> and it's the currently selected one
+ node_name == :option &&
+ select_value == v &&
+ # and the <option> doesn't already have a selected attribute on it
+ props[:selected] == nil
+ )
+ s << ' selected'
+ end
+ end
+ s << " #{name}=\"#{_encode_entities(v)}\""
+ end
+ end
+ end
+
+ s << '>'
+
+ raise "#{node_name_s} is not a valid HTML tag name in #{s}" if (UNSAFE_NAME.match?(node_name_s))
+
+ is_void = VOID_ELEMENTS.match?(node_name_s)
+ pieces = []
+
+ if html
+ s << html
+ elsif prop_children
+ children = []
+ if _get_children(children, prop_children).size > 0
+ last_was_text = false
+
+ children.each do |child|
+ if child != nil && child != false
+ child_svg_mode = node_name == :svg ? true : ((node_name == :foreignObject) ? false : is_svg_mode)
+ ret = _render_to_string(child, context, opts, true, child_svg_mode, select_value)
+
+ # Skip if we received an empty string
+ if ret
+ pieces.push(ret)
+ end
+ end
+ end
+ end
+ end
+
+ if (pieces.length > 0) || html
+ s << pieces.join('')
+ elsif opts && opts[:xml]
+ return s.chop! << ' />'
+ end
+
+ if is_void && !children && !html
+ s = s.sub(/>$/, ' />')
+ else
+ s << "</#{node_name}>"
+ end
+
+ return s
+ end
+
+ def render_to_string(vnode, context = nil, opts = nil)
+ _init_render
+ context = {} unless context
+ _render_to_string(vnode, context, opts)
+ end
+ end # RUBY_ENGINE
end
end
+
+Preact._vnode_id = 0