aem-react icon indicating copy to clipboard operation
aem-react copied to clipboard

SPA Implementation issue with mappings enabled

Open jdhrnndz opened this issue 8 years ago • 14 comments

Good day! I would like to ask for help regarding SPA implementation. We have been using AEM mappings in our publish server, causing the path used by the ResourceRoute component become incorrect because it doesnt match any resource in the AEM instance (A screenshot of the console error is attached to the bottom of this post). This is the mappings config we're using:

# /project.company.com/.content.xml

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
    jcr:primaryType="sling:Mapping"
    sling:internalRedirect="[/content/project/home,/]">
    <redirect/>
</jcr:root>

# /project.company.com/redirect/.content.xml

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
    jcr:primaryType="sling:Mapping"
    sling:internalRedirect="[/content/project/home/$1,/$1]"
    sling:match="(.+)$"/>

Are there any modifications I need to make on the source code to enable support for mappings like the one mentioned above? Thanks in advance.

spaerror

jdhrnndz avatar Jul 03 '17 07:07 jdhrnndz

That seems to be a missing piece. The ResourceMapping should actually handle this but it is not used in the ClientSling. You should override/extend ClientSling like this:


export class MyClientSling extends ClientSling {
  public subscribe(
    listener: ResourceComponent<any, any, any>,
    path: string,
    options?: SlingResourceOptions
  ): void {
  let mappedPath = resourceMapping.map(path);
  super.subscribe(listener, mappedPath, options);
}

And then register it in client.tsx. The ResourceMapping must be environment specific (remove the "/content/project" path on publisher only).

stemey avatar Jul 03 '17 08:07 stemey

Should I just create a new ResourceMapping instance or do I need to get it in this manner: this.context.aemContext.container.get("resourceMapping")?

And do I replace

let clientSling: ClientSling = new ClientSling(cache, host);
container.register("sling", clientSling);

with

let clientSling: MyClientSling = new MyClientSling(cache, host);
container.register("sling", clientSling);

?

jdhrnndz avatar Jul 03 '17 09:07 jdhrnndz

  1. No, just use the instance that you are registering. They are both defined in the same file.
  2. Yes, replace it.

Please have a look at client.tsx: Both ClientSling and ResourceMapping are instantiated and registered with the container here. So you can just use the ResourceMapping instance in the creation of the MyClientSling (e.g. as constructor args) .

stemey avatar Jul 03 '17 09:07 stemey

Okay, now I have these codes

my-client-sling.tsx

import ClientSling from "aem-react-js-fork/store/ClientSling";
import { ResourceComponent } from "aem-react-js-fork/component/ResourceComponent";
import { SlingResourceOptions } from "aem-react-js-fork/store/Sling";

export default class MyClientSling extends ClientSling {
  private resourceMapping;

  constructor(cache, host, resourceMapping) {
    super(cache, host);

    this.resourceMapping = resourceMapping;
  }

  public subscribe(
    listener: ResourceComponent<any, any, any>,
    path: string,
    options?: SlingResourceOptions): void {
    let mappedPath = this.resourceMapping.map(path);
    super.subscribe(listener, mappedPath, options);
  }
}

client.tsx

import MyClientSling from "./my-client-sling";
//...
let resourceMapping = new ResourceMappingImpl(".html");
let clientSling: UnimartClientSling = new UnimartClientSling(cache, host, resourceMapping);
container.register("sling", clientSling);
container.register("resourceMapping", resourceMapping);

However, the problem is still not fixed and the page failed to render correctly. There are three errors in the console which all contains: Uncaught (in promise) SyntaxError: Unexpected token < in JSON at position 0.

This is what the page renders:

wrongrender ...the rest of the page is blank.

jdhrnndz avatar Jul 03 '17 09:07 jdhrnndz

You must change the Implementation of ResourceMapping to remove the "/content/${project}/" part of the resource path to create the path for the ajax call. The standard implementation just appends the ".html". e.g add another basePath prop which you set to "/content/xxxxx":

export default class BetterResourceMappingImpl {

    constructor(basePath: string="", extension?: string) {
        if (extension) {
            this.extension = extension;
        }
       this.basePath=basePath;
    }

    private extension: string = ".html";
     private basePath: string ="";

    public resolve(path: string): string {
        return basePath+path.substring(0, path.length - this.extension.length);
    }

    public map(path: string): string {
        return path.substring(basePath.length) + this.extension;
    }

}

stemey avatar Jul 03 '17 10:07 stemey

I have applied the changes indicated above but I noticed that the state.resource of the DashboardPage (component right under RootComponent) is null, and the state.absolutePath having an .html extension.

I tried to revert the changes made to ResourceMappingImpl and used the default ClientSling again to find out what was the previous value of state.absolutePath, and it is the same url but without the .html extension. Also the state.resource has the correct values again.

I have also discovered the correct way of accessing the contents of the components under the DashboardPage.

  1. HeaderComponent - http://localhost:4502/dashboard/subscriptions/jcr:content/structure/header.json.html
  2. Dashboard - http://localhost:4502/dashboard/subscriptions/jcr:content/structure/dashboard/content.json.html
  3. ContentFooterComponent - http://localhost:4502/dashboard/subscriptions/jcr:content/structure/footer.json.html

However, with the current setup (meaning all changes indicated above are applied), each component are accessed this way

  1. http://localhost:4502/content/projectname/home/dashboard/subscriptions/jcr:content/structure/header.html
  2. http://localhost:4502/content/projectname/home/dashboard/subscriptions/jcr:content/structure.html/dashboard
  3. http://localhost:4502/content/projectname/home/dashboard/subscriptions/jcr:content/structure.html/footer

I apologize for bombarding you with questions but please bear with me a little bit more :)

This is the react debugger to visualize the component hierarchy: react-debugger

jdhrnndz avatar Jul 04 '17 03:07 jdhrnndz

This issue is really important and I am grateful for you raising it. Let's solve this and then I will try to find a way to make it work in the lib.

Use a custom mapping in ClientSling instead of ResourceMapping. The path that is passed to ClientSling should be /content/projectname/home/dashboard/subscriptions/jcr:content/structure/header. ClientSling appends ".json.html". So you just need to remove /content/projectname. ResourceMapping is also changing the extension which is not appropriate in this case. The following should be ok:

export class MyClientSling extends ClientSling {
  public subscribe(
    listener: ResourceComponent<any, any, any>,
    path: string,
    options?: SlingResourceOptions
  ): void {
  let mappedPath = path.substring('/content/projectname'.length);
  super.subscribe(listener, mappedPath, options);
}

The only thing I don't get is why the ".html" is appended after "structure" and not at the end.

stemey avatar Jul 04 '17 06:07 stemey

I narrowed down the error to the ClientSling.subscribe method. If I pass the mappedPath to the parent constructor of MyClientSling instead of the path only, the whole page doesn't load. But if I pass the path and resolve it manually before calling this.fetch.fetch() then everything works as expected. What do you think?

jdhrnndz avatar Jul 04 '17 10:07 jdhrnndz

Yes, you need to override subscribe and pass the mappedPath to fetch - path is different for each call. Can you share the code?

stemey avatar Jul 04 '17 10:07 stemey

This is inside the default ClientSling.subscribe

let pathMod = path;
// this is what I mean when I say "resolve it manually"
if (pathMod.startsWith("/content/projectname/home")) {
  pathMod = pathMod.substring("/content/projectname/home".length);
}

var url = this.origin + pathMod + ".json.html"; // + depthAsString + ".json";
var serverRenderingParam = "serverRendering=disabled";
var serverRendering = window.location.search.indexOf(serverRenderingParam) >= 0;
if (serverRendering) {
  url += "?" + serverRenderingParam;
}

return this.fetch.fetch(url, { credentials: "same-origin" }).then(function (response) {
  if (response.status === 404) {
    return {};
  }
  else {
   // everything else here
  }
}

jdhrnndz avatar Jul 04 '17 10:07 jdhrnndz

I now see the problem, mappedPath has an .html extension because it was processed thru resourceMapping.map, while path isn't. Now I just remove the extension from mappedPath. Is there a more appropriate way to do this?

jdhrnndz avatar Jul 04 '17 10:07 jdhrnndz

ResourceMapping is really where this code should reside. Maybe there should be another special method for this purpose. I will have a look in the next couple days.

stemey avatar Jul 04 '17 11:07 stemey

I have come up with a temporary solution. Please note that I am changing codes inside the node_modules directory but it's pretty easy to see how it translates to the code base.

MyClientSling.js

export default class MyClientSling extends ClientSling {
  private resourceMapping;

  constructor(cache, host, resourceMapping) {
    super(cache, host);

    this.resourceMapping = resourceMapping;
  }
}

ResourceMappingImpl.js

var ResourceMappingImpl = (function () {
    function ResourceMappingImpl(extension, basePath) {
        this.extension = ".html";
        if (extension) {
            this.extension = extension;
        }
        this.basePath = basePath;
    }
    ResourceMappingImpl.prototype.resolve = function (path) {
        if (path.endsWith(this.extension))
            path = path.substring(0, path.length - this.extension.length);

        if (!path.startsWith(this.basePath))
            path = this.basePath + path;
        return path;
    };
    ResourceMappingImpl.prototype.map = function (path) {
        if (path.startsWith(this.basePath)) {
            path = path.substring(this.basePath.length);
        }

        return path + this.extension;
    };
    return ResourceMappingImpl;
}());

ClientSling.js

ClientSling.prototype.subscribe = function (listener, path, options) {
  // some code here...
  if (resource === null || typeof resource === "undefined") {
    var depthAsString = void 0;
    if (depth < 0) {
      depthAsString = "infinity";
    }
    else {
      depthAsString = options.depth + "";
    }
    let pathMod = this.resourceMapping.map(path);
    var url = this.origin + pathMod + ".json.html";
    var serverRenderingParam = "serverRendering=disabled";
    // fetch...
  }
  // some code here...
}

client.tsx

const BASE_PATH = "/content/projectname/home";
let resourceMapping = new ResourceMappingImpl(".html", BASE_PATH);
let clientSling: MyClientSling = new MyClientSling(cache, host, resourceMapping);
container.register("sling", clientSling);
container.register("resourceMapping", resourceMapping);

I haven't pushed this to the publish server yet but it looks good on localhost.

I observed that using resourceMapping.map inside ClientSling (when the resource is not found in the cache) works because we're supposed access resource sans the basepath specified. That's how I came up with this temporary solution. I am not sure if the reason why on page load, resources can be found in the cache, is because the page is rendered on the server, where the mappings are still completely bypassed? This is just a speculation though

jdhrnndz avatar Jul 04 '17 16:07 jdhrnndz

You are probably right, that the ServerSling doesn't care about the mapping. So the cache's keys are resourcePaths. Im happy to accept a Merge Request, we just need to create a test for this first.

stemey avatar Jul 05 '17 07:07 stemey