Skip to content
This repository has been archived by the owner on Jan 16, 2023. It is now read-only.

fix(app): add logic for component inheritance #80

Merged
merged 1 commit into from
Apr 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions app/aem/src/client/preview/helpers/ComponentLoader.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,37 @@
export default class ComponentLoader {
resolve(type, resources) {
const comps = resources || [];
return comps.find(c => c.resourceType === type);
private components: any;

constructor(components: any) {
this.components = components || [];
}

/**
* Resolves the component for the given resource type.
* @param {string} type Resource type
* @return {object} component info or {@code null}.
*/
resolve(type: string): any {
if (!type) {
return null;
}
return this.components.find(c => c.resourceType === type);
}

/**
* Resolves the HTL script for the given resource type, respecting the `sling:resourceSuperType`
* property.
*
* @param {string} type Resource Type.
* @return {function} the script function or {@code null}
*/
resolveScript(type: string): Function {
const component = this.resolve(type);
if (!component) {
return null;
}
if (component.module) {
return component.module;
}
return this.resolveScript(component.properties['sling:resourceSuperType']);
}
}
7 changes: 2 additions & 5 deletions app/aem/src/client/preview/helpers/ResourceResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,9 @@ export default class ResourceResolver {

loader = null;

components = null;

constructor(content, componentLoader, components) {
constructor(content, componentLoader) {
this.content = content;
this.loader = componentLoader;
this.components = components;
}

createResourceLoader(passedPath: any) {
Expand All @@ -41,7 +38,7 @@ export default class ResourceResolver {

// try to get component
const type = content[':type'];
const comp = this.loader.resolve(type, this.components);
const comp = this.loader.resolve(type);
if (!comp) {
// todo: remove debug
return `no such component: ${type}`;
Expand Down
18 changes: 12 additions & 6 deletions app/aem/src/client/preview/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const createRuntime = (
.setGlobal({ models, wcmmode, component: { properties: {} }, content })
.withDomFactory(new VDOMFactory(window.document.implementation).withKeepFragment(true))
.withResourceLoader(
new ResourceResolver(content || {}, new ComponentLoader(), components).createResourceLoader(
new ResourceResolver(content || {}, new ComponentLoader(components)).createResourceLoader(
resourceLoaderPath || '/'
)
);
Expand Down Expand Up @@ -116,14 +116,20 @@ const resetRoot = () => {
* Gets The HTL template
* @param storyFn
* @param resourceType
* @param components
*/
const getTemplate = async (storyFn: any, resourceType: any, aemMetadata: AemMetadata) => {
const { template } = (await storyFn()) as any;
const components: any[] = aemMetadata ? aemMetadata.components : [];
let info = resourceType ? new ComponentLoader().resolve(resourceType, components) : null;
info = info && info.module ? info.module : `unable to load ${resourceType}`;
return !template ? info : template;
// if the story exposes a template property, always use it.
if (template) {
return template;
}
// else, try to resolve via component loader
const componentLoader = new ComponentLoader(aemMetadata ? aemMetadata.components : []);
const script = componentLoader.resolveScript(resourceType);
if (!script) {
return `unable to load template for ${resourceType}`;
}
return script;
};

export default async function renderMain({
Expand Down
79 changes: 60 additions & 19 deletions app/aem/src/server/aem-component-loader.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import { basename } from 'path';
import { basename, relative, sep as pathSeparator } from 'path';
import { toJson } from 'xml2json';
import { existsSync } from 'fs';

const txtLoader = require.resolve('./aem-clientlib-txt-loader.js');
const JCR_ROOT_KEY = 'jcr:root';
const JCR_TITLE_KEY = 'jcr:title';

const getRequiredHTL = (component, context, pathBaseName) => {
return `
var component = ${JSON.stringify(component)};
component.module = require('${context}/${pathBaseName}.html');
module.exports = component;`;

const KEY_JCR_ROOT = 'jcr:root';
const KEY_JCR_TITLE = 'jcr:title';
const NAME_JCR_ROOT = 'jcr_root';

const NAME_APPS = 'apps';
const NAME_LIBS = 'libs';
const NAME_NODE_MODULES = 'node_modules';

const getRequiredHTL = (logger, component, context, pathBaseName) => {
const htlFile = `${context}/${pathBaseName}.html`;
if (!existsSync(htlFile)) {
logger.info(`No HTL script for ${pathBaseName}`);
return '';
}
return `component.module = require('${htlFile}');`;
};

const getRequiredClientLibs = componentDir => {
Expand All @@ -31,20 +40,52 @@ const getRequiredClientLibs = componentDir => {
return loadClientLibCode;
};

export default async function aemComponentLoader(source) {
const resourceType = this.context.substring(this.rootContext.length + 1);
const json = JSON.parse(toJson(source));
const pathBaseName = basename(this.context);
/**
* Computes the resource type given the webpack context and root-context.
* This is based on heuristic and best practices. It would be better if
* the respective roots could be specified via configuration.
*
* @param {string} rootContext compilation context (i.e. the directory of the compilation)
* @param {string} context compile context (i.e. the directory of the .content.xml)
* @returns {string} The resource type.
*/
const getResourceType = (rootContext, context) => {
const segs = relative(rootContext, context).split(pathSeparator);
// if component is in different module, it will be below a node_modules
let idx = segs.indexOf(NAME_NODE_MODULES);
if (idx >= 0) {
// remove all segments including node_modules
segs.splice(0, idx + 1);
}
// if component is below a jcr_root
idx = segs.indexOf(NAME_JCR_ROOT);
if (idx >= 0) {
// remove all segments including jcr_root
segs.splice(0, idx + 1);
if (segs[0] === NAME_APPS || segs[1] === NAME_LIBS) {
segs.splice(0, 1);
}
}
return segs.join('/');
};

export default async function aemComponentLoader(source) {
const { context, rootContext } = this;
const logger = this.getLogger();
const componentData = JSON.parse(toJson(source));
const pathBaseName = basename(context);
const component = {
resourceType,
properties: {
[JCR_TITLE_KEY]: `${json[JCR_ROOT_KEY] ? json[JCR_ROOT_KEY][JCR_TITLE_KEY] : pathBaseName}`,
},
resourceType: getResourceType(rootContext, context),
properties: componentData[KEY_JCR_ROOT] || {},
};
if (!component.properties[KEY_JCR_TITLE]) {
component.properties[KEY_JCR_TITLE] = pathBaseName;
}

return [
getRequiredClientLibs(this.context),
getRequiredHTL(component, this.context, pathBaseName),
`var component = ${JSON.stringify(component)};`,
'module.exports = component;',
getRequiredClientLibs(context),
getRequiredHTL(logger, component, context, pathBaseName),
].join('\n');
}
8 changes: 8 additions & 0 deletions examples/aem-core-components/components/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// todo: generate automatically
module.exports = [
require('../jcr_root/apps/core/wcm/components/accordion/v1/accordion/clientlibs/.content.xml'),
require('../jcr_root/apps/core/wcm/components/accordion/v1/accordion/clientlibs/site/.content.xml'),
require('../jcr_root/apps/core/wcm/components/accordion/v1/accordion/.content.xml'),
require('../jcr_root/apps/core/wcm/components/list/v2/list/.content.xml'),
require('../jcr_root/apps/core/wcm/components/text/v2/text/.content.xml'),
];
4 changes: 4 additions & 0 deletions examples/aem-core-components/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
models: require('./models'),
components: require('./components'),
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<!--/* todo: fix template resolution for extended components */-->
<ul data-sly-use.list="com.adobe.cq.wcm.core.components.models.List"
data-sly-list.item="${list.listItems}"
data-sly-use.template="core/wcm/components/commons/v1/templates.html"
data-sly-use.template="../../../../../../core/wcm/components/commons/v1/templates.html"
data-sly-use.itemTemplate="item.html"
class="cmp-list">
<li class="cmp-list__item" data-sly-call="${itemTemplate.item @ list = list, item = item}"></li>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<!--/* todo: fix template resolution for extended components */-->
<div data-sly-use.textModel="com.adobe.cq.wcm.core.components.models.Text"
data-sly-use.templates="core/wcm/components/commons/v1/templates.html"
data-sly-use.templates="../../../../../../core/wcm/components/commons/v1/templates.html"
data-sly-test.text="${textModel.text}"
class="cmp-text">
<p class="cmp-text__paragraph"
Expand Down
2 changes: 1 addition & 1 deletion examples/aem-core-components/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "aem-core-components",
"name": "@adobe/aem-core-components-storified",
"private": true,
"version": "1.0.0",
"description": "",
Expand Down
7 changes: 6 additions & 1 deletion examples/aem-kitchen-sink/.storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,23 @@ import { addParameters, addDecorator } from '@storybook/client-api';
import { withA11y } from '@storybook/addon-a11y';
import { aemMetadata, GenericModel } from '@storybook/aem';

import AEMCoreComponents from '@adobe/aem-core-components-storified';

addDecorator(withA11y);
addDecorator(aemMetadata({
components: [
require('../components/accordion/.content.xml'),
require('../components/list/.content.xml'),
require('../components/text/.content.xml'),
require('../components/aemtext/.content.xml'),
...AEMCoreComponents.components,
],
models: {
'Accordion': GenericModel,
'Text': GenericModel,
'List': GenericModel,
'person': require('../models/person'),
...AEMCoreComponents.models,
}
}));

Expand All @@ -31,4 +36,4 @@ addParameters({
docs: {
iframeHeight: '200px',
},
});
});
5 changes: 5 additions & 0 deletions examples/aem-kitchen-sink/components/aemtext/.content.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="cq:Component"
jcr:title="AEM Text"
sling:resourceSuperType="core/wcm/components/text/v2/text"/>
43 changes: 43 additions & 0 deletions examples/aem-kitchen-sink/components/aemtext/aemtext.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { withKnobs, text, boolean } from "@storybook/addon-knobs";
import { aemMetadata, GenericModel } from '@storybook/aem';

export default {
title: 'AEM Text',
decorators: [
withKnobs,
aemMetadata({
decorationTag: {
cssClasses: ['text','component'],
tagName: 'article'
}
}),
],
parameters: {
knobs: {
escapeHTML: false,
},
},
};

export const Text = () => {
return {
content: {
text: text('text', 'Hello, world.' ),
isRichText: boolean('isRichText', false),
},
resourceType: 'components/aemtext',
};
};

export const RichText = () => {
return {
content: {
text: text('text', '<h1>Hello, world.</h1>' ),
isRichText: boolean('isRichText', true),
},
resourceType: 'components/aemtext',
aemMetadata: {
decorationTag: null
},
};
};
3 changes: 3 additions & 0 deletions examples/aem-kitchen-sink/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
"storybook": "start-storybook -p 9001"
},
"license": "MIT",
"dependencies": {
"@adobe/aem-core-components-storified": "*"
},
"devDependencies": {
"@storybook/aem": "*",
"@storybook/addon-a11y": "6.0.0-alpha.2",
Expand Down