diff --git a/Classes/Fusion/PartialResolverImplementation.php b/Classes/Fusion/PartialResolverImplementation.php new file mode 100644 index 0000000..14a0579 --- /dev/null +++ b/Classes/Fusion/PartialResolverImplementation.php @@ -0,0 +1,47 @@ +fusionValue('partialKey'); + + $partialContext = $this->partialCache->get($partialKey); + + if (!$partialContext) { + throw new Exception( + sprintf('The partial context could not be resolved for identifier "%s".', $partialKey), + 1678108923 + ); + } + + return $partialContext; + } +} diff --git a/Classes/Fusion/PartialSerializerImplementation.php b/Classes/Fusion/PartialSerializerImplementation.php new file mode 100644 index 0000000..4b54baf --- /dev/null +++ b/Classes/Fusion/PartialSerializerImplementation.php @@ -0,0 +1,83 @@ +fusionValue('node'); + + $nodeIdentifier = (string)$node->getNodeAggregateIdentifier(); + $fusionPath = $this->getPartialFusionPath(); + $partialContext = [ + 'nodeIdentifier' => $nodeIdentifier, + 'fusionPath' => $fusionPath, + ]; + $partialKey = sha1(implode(';', $partialContext)); + $this->partialCache->set( + $partialKey, + $partialContext + ); + return $partialKey; + } + + /** + * Returns the Fusion path of the wrapping partial. + * + * Note that this Psmb.Ajaxify:PartialSerializer object may be nested inside a + * Psmb.Ajaxify:Ajaxify object. It is assumed that the outer of the two + * objects is in turn called at the first nesting depth of the wrapping + * partial. + * + * @return string + * @see \Neos\Neos\Fusion\ContentElementWrappingImplementation::getContentElementFusionPath + */ + protected function getPartialFusionPath() + { + $pos = strrpos($this->path, ""); + if ($pos === false) { + $pos = strrpos($this->path, ""); + } + + $fusionPathSegments = explode('/', substr($this->path, 0, $pos)); + $numberOfFusionPathSegments = count($fusionPathSegments); + if (isset($fusionPathSegments[$numberOfFusionPathSegments - 3]) + && $fusionPathSegments[$numberOfFusionPathSegments - 3] === '__meta' + && isset($fusionPathSegments[$numberOfFusionPathSegments - 2]) + && $fusionPathSegments[$numberOfFusionPathSegments - 2] === 'process') { + + // cut off the SHORT processing syntax "__meta/process/ajaxify" + return implode('/', array_slice($fusionPathSegments, 0, -3)); + } + elseif (isset($fusionPathSegments[$numberOfFusionPathSegments - 4]) + && $fusionPathSegments[$numberOfFusionPathSegments - 4] === '__meta' + && isset($fusionPathSegments[$numberOfFusionPathSegments - 3]) + && $fusionPathSegments[$numberOfFusionPathSegments - 3] === 'process') { + + // cut off the LONG processing syntax "__meta/process/ajaxify/expression" + return implode('/', array_slice($fusionPathSegments, 0, -4)); + } + return implode('/', array_slice($fusionPathSegments, 0, -1)); + } +} diff --git a/Classes/Fusion/RenderPathImplementation.php b/Classes/Fusion/RenderPathImplementation.php deleted file mode 100644 index ec671ea..0000000 --- a/Classes/Fusion/RenderPathImplementation.php +++ /dev/null @@ -1,31 +0,0 @@ -path); - $key = explode('<', $pathExploded[sizeof($pathExploded) - 5])[0]; - $path = dirname($this->path, 7); - $this->pathsCache->set( - $key, - $path - ); - return $key; - } - -} diff --git a/Classes/Fusion/RendererImplementation.php b/Classes/Fusion/RendererImplementation.php index 0e36851..27f0eba 100644 --- a/Classes/Fusion/RendererImplementation.php +++ b/Classes/Fusion/RendererImplementation.php @@ -1,39 +1,42 @@ fusionValue('node'); + $fusionPath = $this->fusionValue('fusionPath'); - public function evaluate() { - $this->view->setControllerContext($this->runtime->getControllerContext()); - $node = $this->fusionValue('node'); - $pathKey = $this->fusionValue('pathKey'); - $renderPath = $this->pathsCache->get($pathKey); - if (!$renderPath) { - throw new \Exception(sprintf('Render path not found for key %s', $pathKey)); - } - $this->view->setFusionPath($renderPath); - $this->view->assign('value', $node); - return $this->view->render(); - } + if (!$node instanceof NodeInterface) { + throw new Exception(sprintf('The node could not be resolved.'), 1677856609); + } + if (!$fusionPath) { + throw new Exception(sprintf('The Fusion path could not be resolved.'), 1677857018); + } + $this->view->setControllerContext($this->runtime->getControllerContext()); + $this->view->setFusionPath($fusionPath); + $this->view->assign('value', $node); + return $this->view->render(); + } } diff --git a/Configuration/Caches.yaml b/Configuration/Caches.yaml index 1e251bb..067519a 100644 --- a/Configuration/Caches.yaml +++ b/Configuration/Caches.yaml @@ -1,4 +1,4 @@ -Psmb_Ajaxify_PathsCache: +Psmb_Ajaxify_PartialCache: backendOptions: defaultLifetime: 0 persistent: true diff --git a/Configuration/Objects.yaml b/Configuration/Objects.yaml index e7adaf3..fb9789c 100644 --- a/Configuration/Objects.yaml +++ b/Configuration/Objects.yaml @@ -1,19 +1,19 @@ -Psmb\Ajaxify\Fusion\RenderPathImplementation: +Psmb\Ajaxify\Fusion\PartialSerializerImplementation: properties: - pathsCache: + partialCache: object: factoryObjectName: Neos\Flow\Cache\CacheManager factoryMethodName: getCache arguments: 1: - value: Psmb_Ajaxify_PathsCache + value: Psmb_Ajaxify_PartialCache -Psmb\Ajaxify\Fusion\RendererImplementation: +Psmb\Ajaxify\Fusion\PartialResolverImplementation: properties: - pathsCache: + partialCache: object: factoryObjectName: Neos\Flow\Cache\CacheManager factoryMethodName: getCache arguments: 1: - value: Psmb_Ajaxify_PathsCache + value: Psmb_Ajaxify_PartialCache diff --git a/README.md b/README.md index 4a15d2c..3beb75e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # Psmb.Ajaxify This package allows you to mark any part of page for asynchronous loading via AJAX with just one line of Fusion code. -Why? It helps you to speed up initial page load by delaying the load of some less relevant parts of the page, e.g. comments. +Why? It helps you to speed up initial page load by delaying the load of some less relevant parts of the page, e.g. +comments. ![demo](https://cloud.githubusercontent.com/assets/837032/25178402/5b011f40-250e-11e7-9e6c-462b8e912893.gif) @@ -14,7 +15,7 @@ Why? It helps you to speed up initial page load by delaying the load of some les composer require psmb/ajaxify ``` -2. Add `@process.myUniqueKey = Psmb.Ajaxify:Ajaxify` on any Fusion path. **The `myUniqueKey` key of the processor MUST be globally unique.** +2. Add `@process.ajaxify = Psmb.Ajaxify:Ajaxify` on any Fusion path. 3. Add this anywhere in your Fusion code to include the sample AJAX loading script: @@ -29,10 +30,43 @@ Or include these assets via your build tool. Or just write your own loader. 4. Done. Now part of your pages will be lazily loaded via an AJAX request. **Note:** the Fusion component should not depend on any context variables, other than the standard ones. -If you want to reuse some EEL expression in your code base, don't put it into context, rather wrap it into `Neos.Fusion:Value` object and use it everywhere you like. +If you want to reuse some EEL expression in your code base, don't put it into context, rather wrap it +into `Neos.Fusion:Value` object and use it everywhere you like. 5. You may override the `Psmb.Ajaxify:Loader` object in order to customize the loader. +## Partial rendering in custom AJAX application + +You may want to use only the partial rendering feature of this package in your custom AJAX implementation. +Therefore, get the unique partial key with `partialKey = Psmb.Ajaxify:PartialSerializer` in your Fusion path +and append it as `ajaxPartialKey` parameter to a self-reflecting URL. When you send an AJAX request to +this URL, only the rendered partial will be returned. Additional parameters can be used to fine-tune +the rendering, for example to allow pagination: + +``` +prototype(MyWebsite.Site:Content.PaginatedContent) < prototype(Neos.Neos:ContentComponent) { + from = ${String.toInteger(request.arguments.from) || 0} + num = 5 + partialKey = Psmb.Ajaxify:PartialSerializer + + renderer = Neos.Neos:ContentComponent { + items = .. + partialUrl = Neos.Neos:NodeUri { + node = ${documentNode} + additionalParams.from = ${props.from + props.num} + additionalParams.ajaxPartialKey = ${props.partialKey} + } + + renderer = afx` + + .. + + Load next page + ` + } +} +``` + ## Usage in the Wild diff --git a/Resources/Private/Fusion/Override.fusion b/Resources/Private/Fusion/Override.fusion index c913976..30be4bd 100644 --- a/Resources/Private/Fusion/Override.fusion +++ b/Resources/Private/Fusion/Override.fusion @@ -1 +1 @@ -prototype(Neos.Fusion:GlobalCacheIdentifiers).ajaxPathKey = ${request.arguments.ajaxPathKey} +prototype(Neos.Fusion:GlobalCacheIdentifiers).ajaxPartialKey = ${request.arguments.ajaxPartialKey} diff --git a/Resources/Private/Fusion/Root.fusion b/Resources/Private/Fusion/Root.fusion index 42b67b5..88f0f6f 100644 --- a/Resources/Private/Fusion/Root.fusion +++ b/Resources/Private/Fusion/Root.fusion @@ -1,64 +1,75 @@ include: Override.fusion -# TODO: Watch out, this is hardcoded to be used with Psmb.Ajaxify:Ajaxify -prototype(Psmb.Ajaxify:RenderPath) { - @class = 'Psmb\\Ajaxify\\Fusion\\RenderPathImplementation' +prototype(Psmb.Ajaxify:PartialSerializer) { + @class = 'Psmb\\Ajaxify\\Fusion\\PartialSerializerImplementation' + node = ${node} +} + +prototype(Psmb.Ajaxify:PartialResolver) { + @class = 'Psmb\\Ajaxify\\Fusion\\PartialResolverImplementation' + partialKey = null } prototype(Psmb.Ajaxify:Loader) < prototype(Neos.Fusion:Value) { - value = ${'
'} + value = ${'
'} } # Use this object as a processor on any path prototype(Psmb.Ajaxify:Ajaxify) < prototype(Neos.Fusion:Tag) { - # The processor is disabled in BE or when rendering the AJAX request - @if.disableProcessor = ${!request.arguments.ajaxPathKey && !documentNode.context.inBackend} - tagName = 'a' - attributes.data-ajaxify = ${true} - attributes.href = Neos.Neos:NodeUri { - node = ${documentNode} - additionalParams.ajaxPathKey = Psmb.Ajaxify:RenderPath - } - content = Psmb.Ajaxify:Loader + # The processor is disabled in BE or when rendering the AJAX request + @if.disableProcessor = ${!request.arguments.ajaxPartialKey && !documentNode.context.inBackend} + tagName = 'a' + attributes.data-ajaxify = ${true} + attributes.href = Neos.Neos:NodeUri { + node = ${documentNode} + additionalParams.ajaxPartialKey = Psmb.Ajaxify:PartialSerializer + } + content = Psmb.Ajaxify:Loader } prototype(Psmb.Ajaxify:Renderer) { - @class = 'Psmb\\Ajaxify\\Fusion\\RendererImplementation' - node = ${documentNode} - pathKey = ${request.arguments.ajaxPathKey} - @cache { - mode = 'uncached' - context { - 1 = 'documentNode' - 2 = 'node' - } - } + @class = 'Psmb\\Ajaxify\\Fusion\\RendererImplementation' + + partialContext = Psmb.Ajaxify:PartialResolver { + partialKey = ${request.arguments.ajaxPartialKey} + } + + node = ${q(documentNode).find("#" + this.partialContext.nodeIdentifier).get(0)} + fusionPath = ${this.partialContext.fusionPath} + + @cache { + mode = 'uncached' + context { + 1 = 'documentNode' + 2 = 'node' + } + } } # Prevents search engines from indexing the partly rendered document content prototype(Psmb.Ajaxify:UnindexedResponse) < prototype(Neos.Fusion:Http.Message) { - httpResponseHead.headers { - X-Robots-Tag = 'noindex, follow' - } - content = Psmb.Ajaxify:Renderer + httpResponseHead.headers { + X-Robots-Tag = 'noindex, follow' + } + content = Psmb.Ajaxify:Renderer } root.ajaxify { - @position = 'start' - condition = ${request.arguments.ajaxPathKey} - renderer = Psmb.Ajaxify:UnindexedResponse + @position = 'start' + condition = ${request.arguments.ajaxPartialKey} + renderer = Psmb.Ajaxify:UnindexedResponse } prototype(Psmb.Ajaxify:JsTag) < prototype(Neos.Fusion:Tag) { - tagName = 'script' - attributes.src = Neos.Fusion:ResourceUri { - path = 'resource://Psmb.Ajaxify/Public/ajax.js' - } + tagName = 'script' + attributes.src = Neos.Fusion:ResourceUri { + path = 'resource://Psmb.Ajaxify/Public/ajax.js' + } } prototype(Psmb.Ajaxify:CssTag) < prototype(Neos.Fusion:Tag) { - tagName = 'link' - attributes.rel = 'stylesheet' - attributes.href = Neos.Fusion:ResourceUri { - path = 'resource://Psmb.Ajaxify/Public/loader.css' - } + tagName = 'link' + attributes.rel = 'stylesheet' + attributes.href = Neos.Fusion:ResourceUri { + path = 'resource://Psmb.Ajaxify/Public/loader.css' + } } diff --git a/Resources/Public/ajax.js b/Resources/Public/ajax.js index 1a04423..80f713c 100644 --- a/Resources/Public/ajax.js +++ b/Resources/Public/ajax.js @@ -1,27 +1,27 @@ (function () { - var containers = document.querySelectorAll('[data-ajaxify]'); + var containers = document.querySelectorAll('[data-ajaxify]'); - [].slice.call(containers).forEach(function(el) { - loadContainer(el); - }); + [].slice.call(containers).forEach(function (el) { + loadContainer(el); + }); - function loadContainer(container) { - var request = new XMLHttpRequest(); - var url = container.href; - request.open('GET', url, true); + function loadContainer(container) { + var request = new XMLHttpRequest(); + var url = container.href; + request.open('GET', url, true); - request.onload = function() { - if (request.status >= 200 && request.status < 400) { - container.outerHTML = request.responseText; - } else { - container.innerHTML = 'Content failed to load, please refresh the page'; - } - }; + request.onload = function () { + if (request.status >= 200 && request.status < 400) { + container.outerHTML = request.responseText; + } else { + container.innerHTML = 'Content failed to load, please refresh the page'; + } + }; - request.onerror = function() { - container.innerHTML = 'Content failed to load, please refresh the page'; - }; + request.onerror = function () { + container.innerHTML = 'Content failed to load, please refresh the page'; + }; - request.send(); - } + request.send(); + } })(); diff --git a/Resources/Public/loader.css b/Resources/Public/loader.css index 758ddce..98a3811 100644 --- a/Resources/Public/loader.css +++ b/Resources/Public/loader.css @@ -26,15 +26,20 @@ } @-webkit-keyframes sk-bouncedelay { - 0%, 80%, 100% { -webkit-transform: scale(0) } - 40% { -webkit-transform: scale(1.0) } + 0%, 80%, 100% { + -webkit-transform: scale(0) + } + 40% { + -webkit-transform: scale(1.0) + } } @keyframes sk-bouncedelay { 0%, 80%, 100% { -webkit-transform: scale(0); transform: scale(0); - } 40% { + } + 40% { -webkit-transform: scale(1.0); transform: scale(1.0); }