Skip to content

Commit

Permalink
FEATURE: dynamic ajaxify identifiers for content-nodes
Browse files Browse the repository at this point in the history
* `Psmb.Ajaxify:Ajaxify` can configure the identifier for out-of-band
  rendered content with runtime values to discriminate by content-nodes
  and more.
* The URI builder receives an additional argument `contentNode` with the
  contextPath of the current content-node and hydrates it as context
  node `node` when rendering.
* The prototype implementations relax their hardcoded dependance on
  "Psmb.Ajaxify:Ajaxify" for ease of extensibility.

This change introduces `@apply` and `Neos.Fusion:Component`, so we break
support with neos/neos < 4.2.

While this addresses psmb#7, it doesn't further cachable context values that
might be required for other use-cases.
  • Loading branch information
PRGfx committed Jan 19, 2022
1 parent 3f16834 commit 8811c03
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 18 deletions.
63 changes: 56 additions & 7 deletions Classes/Fusion/RenderPathImplementation.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

use Neos\Flow\Annotations as Flow;
use Neos\Fusion\FusionObjects\AbstractFusionObject;
use Neos\Neos\Exception as NeosException;

/**
* Returns the path to the parent Fusion object
Expand All @@ -16,16 +15,66 @@ class RenderPathImplementation extends AbstractFusionObject {
*/
protected $pathsCache;

/**
* Returns the part of the fusion path that calls the {prototypeName}, i.e.
* the key in `@process.{key} = Psmb.Ajaxify:Ajaxify`
* @param string $prototypeName
* @return string
*/
protected function getFusionPathKey($prototypeName = 'Psmb.Ajaxify:Ajaxify')
{
$pathParts = explode('/', $this->path);
$prototypeLength = strlen($prototypeName);
// look for "*<{prototypeName}>" segments
while (count($pathParts)) {
$last = array_pop($pathParts);
if (substr($last, -$prototypeLength - 1, $prototypeLength) === $prototypeName) {
// trim the <{prototypeName}> part
return substr($last, 0, strlen($last) - $prototypeLength - 2);
}
}
return '';
}

/**
* Retrieves the part of the path that contains the content that is to be
* rendered asynchronously
* @return string
*/
protected function getRenderPath()
{
$prototypeName = $this->fusionValue('prototypeName');
$prototypeInPath = '<' . $prototypeName . '>';
$pathParts = explode($prototypeInPath, $this->path);
// we split everything up to .../__meta/process/{key}<prototypeName> and
// need to remove the trailing segments
return dirname($pathParts[0], 3);
}

/**
* Evaluates the `entryIdentifier` fusion value while providing the name of
* the fusion key defining the ajaxify rendering.
* @see getFusionPathKey
* @return string
*/
protected function getEntryIdentifier()
{
$prototypeName = $this->fusionValue('prototypeName');
$key = $this->getFusionPathKey($prototypeName);
$this->runtime->pushContext('key', $key);
$entryIdentifier = $this->fusionValue('entryIdentifier');
$this->runtime->popContext();
return urlencode($entryIdentifier);
}

public function evaluate() {
// TODO: Find this ugly? Know how to do better? Submit a PR!
$pathExploded = explode('/', $this->path);
$key = explode('<', $pathExploded[sizeof($pathExploded) - 5])[0];
$path = dirname($this->path, 7);
$cacheIdentifier = $this->getEntryIdentifier();
$path = $this->getRenderPath();
$this->pathsCache->set(
$key,
$cacheIdentifier,
$path
);
return $key;
return $cacheIdentifier;
}

}
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ If you want to reuse some EEL expression in your code base, don't put it into co
5. You may override the `Psmb.Ajaxify:Loader` object in order to customize the loader.


## Usage with Content-Nodes
By default, the lazy-loaded content is discriminated by the `@process` key and is thus globally the same for every occurrence of an ajaxified block of content, as it would be the same for a specific node-type.
You can override the `entryIdentifier` on the `Psmb.Ajaxify:Ajaxify` on an instance basis as you would for cache segments.
All `entryIdentifier` parts are concatenated and hashed.
```
@process.ajaxify = Psmb.Ajaxify:Ajaxify {
entryIdentifier {
node = ${node}
segment = 'content-details'
}
}
```

## Usage in the Wild

- https://pokayanie1917.ru/
Expand Down
5 changes: 4 additions & 1 deletion Resources/Private/Fusion/Override.fusion
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
prototype(Neos.Fusion:GlobalCacheIdentifiers).ajaxPathKey = ${request.arguments.ajaxPathKey}
prototype(Neos.Fusion:GlobalCacheIdentifiers) {
ajaxPathKey = ${request.arguments.ajaxPathKey}
contentNode = ${request.arguments.contentNode}
}
43 changes: 34 additions & 9 deletions Resources/Private/Fusion/Root.fusion
Original file line number Diff line number Diff line change
@@ -1,30 +1,55 @@
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'
# every path resolution is relative to the Ajaxify prototype, so if you
# overwrite it, make sure to adjust this property
prototypeName = 'Psmb.Ajaxify:Ajaxify'
entryIdentifier = Neos.Fusion:Join {
key = ${key}
@process.hash {
expression = ${String.sha1(value)}
@position = 'end'
}
}
}

prototype(Psmb.Ajaxify:Loader) < prototype(Neos.Fusion:Value) {
value = ${'<div class="spinner"><div class="bounce1"></div><div class="bounce2"></div><div class="bounce3"></div></div>'}
}

# Use this object as a processor on any path
prototype(Psmb.Ajaxify:Ajaxify) < prototype(Neos.Fusion:Tag) {
prototype(Psmb.Ajaxify:Ajaxify) < prototype(Neos.Fusion:Component) {
# 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

entryIdentifier = Neos.Fusion:DataStructure

renderer = Neos.Fusion:Tag {
tagName = 'a'
attributes.data-ajaxify = ${true}
attributes.href = Neos.Neos:NodeUri {
node = ${documentNode}
additionalParams {
# The argument "node" is already used by the uri builder
contentNode = ${node.contextPath}
ajaxPathKey = Psmb.Ajaxify:RenderPath {
[email protected] = ${props.entryIdentifier}
context = ${props.context}
}
}
}
content = Psmb.Ajaxify:Loader
}
content = Psmb.Ajaxify:Loader
}

prototype(Psmb.Ajaxify:Renderer) {
@class = 'Psmb\\Ajaxify\\Fusion\\RendererImplementation'
node = ${documentNode}
# The argument "node" is already used
# We receive a context-path and need to find the respective node
contentNode = ${String.split(request.arguments.contentNode || '', '@')[0]}
[email protected] = ${q(documentNode).find(value).get(0) || documentNode}
node = ${this.contentNode}
pathKey = ${request.arguments.ajaxPathKey}
@cache {
mode = 'uncached'
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"type": "neos-package",
"name": "psmb/ajaxify",
"require": {
"neos/neos": "^3.0 | ^4.0 | ^5.0 | ^7.0"
"neos/neos": "^4.2 | ^5.0 | ^7.0"
},
"license": "LGPL-3.0+",
"autoload": {
Expand Down

0 comments on commit 8811c03

Please sign in to comment.