Skip to content

Commit

Permalink
amp-render bento component (#33189)
Browse files Browse the repository at this point in the history
Addressed in this PR:

1. fetch remote JSON specified via src
2. fetch JSON from amp-state
3. unit tests
4. storybook tests
  • Loading branch information
dmanek committed Mar 22, 2021
1 parent ae162df commit 7b339ba
Show file tree
Hide file tree
Showing 12 changed files with 626 additions and 0 deletions.
1 change: 1 addition & 0 deletions build-system/compile/bundles.config.extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@
},
{"name": "amp-redbull-player", "version": "0.1", "latestVersion": "0.1"},
{"name": "amp-reddit", "version": "0.1", "latestVersion": "0.1"},
{"name": "amp-render", "version": "1.0", "latestVersion": "1.0"},
{
"name": "amp-resize-observer-polyfill",
"version": "0.1",
Expand Down
3 changes: 3 additions & 0 deletions examples/amp-render-data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "Joe"
}
188 changes: 188 additions & 0 deletions extensions/amp-render/1.0/amp-render.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/**
* Copyright 2021 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {BaseElement} from './base-element';
import {Services} from '../../../src/services';
import {batchFetchJsonFor} from '../../../src/batched-json';
import {dev, user, userAssert} from '../../../src/log';
import {dict} from '../../../src/utils/object';
import {isExperimentOn} from '../../../src/experiments';

/** @const {string} */
const TAG = 'amp-render';

const AMP_STATE_URI_SCHEME = 'amp-state:';
const AMP_SCRIPT_URI_SCHEME = 'amp-script:';

/**
* Returns true if element's src points to amp-state.
* @param {?string} src
* @return {boolean}
*/
const isAmpStateSrc = (src) => src && src.startsWith(AMP_STATE_URI_SCHEME);

/**
* Returns true if element's src points to an amp-script function.
* @param {?string} src
* @return {boolean}
*/
const isAmpScriptSrc = (src) => src && src.startsWith(AMP_SCRIPT_URI_SCHEME);

class AmpRender extends BaseElement {
/** @param {!AmpElement} element */
constructor(element) {
super(element);

/** @private {?../../../src/service/template-impl.Templates} */
this.templates_ = null;

/** @private {?Element} */
this.template_ = null;
}

/** @override */
isLayoutSupported(layout) {
userAssert(
isExperimentOn(this.win, 'amp-render'),
'Experiment "amp-render" is not turned on.'
);
return super.isLayoutSupported(layout);
}

/** @override */
init() {
return dict({
'getJson': this.getJsonFn_(),
});
}

/**
* Returns the correct fetch function for amp-state, amp-script or
* to fetch remote JSON.
*
* @return {Function}
* @private
*/
getJsonFn_() {
const src = this.element.getAttribute('src');
if (!src) {
// TODO(dmanek): assert that src is provided instead of silently failing below.
return () => {};
}
if (isAmpStateSrc(src)) {
return this.getAmpStateJson.bind(null, this.element);
}
if (isAmpScriptSrc(src)) {
// TODO(dmanek): implement this
return () => {};
}
return batchFetchJsonFor.bind(null, this.getAmpDoc(), this.element);
}

/**
* TODO: this implementation is identical to one in amp-data-display &
* amp-date-countdown. Move it to a common file and import it.
*
* @override
*/
checkPropsPostMutations() {
const templates =
this.templates_ ||
(this.templates_ = Services.templatesForDoc(this.element));
const template = templates.maybeFindTemplate(this.element);
if (template != this.template_) {
this.template_ = template;
if (template) {
// Only overwrite `render` when template is ready to minimize FOUC.
templates.whenReady(template).then(() => {
if (template != this.template_) {
// A new template has been set while the old one was initializing.
return;
}
this.mutateProps(
dict({
'render': (data) => {
return templates
.renderTemplateAsString(dev().assertElement(template), data)
.then((html) => dict({'__html': html}));
},
})
);
});
} else {
this.mutateProps(dict({'render': null}));
}
}
}

/**
* TODO: this implementation is identical to one in amp-data-display &
* amp-date-countdown. Move it to a common file and import it.
*
* @override
*/
isReady(props) {
if (this.template_ && !('render' in props)) {
// The template is specified, but not available yet.
return false;
}
return true;
}

/**
* Gets the json an amp-list that has an "amp-state:" uri. For example,
* src="amp-state:json.path".
*
* TODO: this implementation is identical to one in amp-list. Move it
* to a common file and import it
*
* TODO: Add src as a param once this method is moved out of this class
* to support src binding (https://github.com/ampproject/amphtml/pull/33189#discussion_r598743971).
*
* @param {!AmpElement} element
* @return {Promise<!JsonObject>}
*/
getAmpStateJson(element) {
const src = element.getAttribute('src');
if (!src) {
return Promise.resolve({});
}
return Services.bindForDocOrNull(element)
.then((bind) => {
userAssert(bind, '"amp-state:" URLs require amp-bind to be installed.');
const ampStatePath = src.slice(AMP_STATE_URI_SCHEME.length);
return bind.getStateAsync(ampStatePath).catch((err) => {
const stateKey = ampStatePath.split('.')[0];
user().error(
TAG,
`'amp-state' element with id '${stateKey}' was not found.`
);
throw err;
});
})
.then((json) => {
userAssert(
typeof json !== 'undefined',
`[amp-render] No data was found at provided uri: ${src}`
);
return json;
});
}
}

AMP.extension(TAG, '1.0', (AMP) => {
AMP.registerElement(TAG, AmpRender);
});
37 changes: 37 additions & 0 deletions extensions/amp-render/1.0/base-element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Copyright 2021 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {PreactBaseElement} from '../../../src/preact/base-element';
import {Render} from './component';

export class BaseElement extends PreactBaseElement {}

/** @override */
BaseElement['Component'] = Render;

/** @override */
BaseElement['props'] = {
'src': {attr: 'src'},
};

/** @override */
BaseElement['usesTemplate'] = true;

/** @override */
BaseElement['lightDomTag'] = 'div';

/** @override */
BaseElement['layoutSizeDefined'] = true;
71 changes: 71 additions & 0 deletions extensions/amp-render/1.0/component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Copyright 2021 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as Preact from '../../../src/preact';
import {Wrapper, useRenderer} from '../../../src/preact/component';
import {useEffect, useState} from '../../../src/preact';
import {useResourcesNotify} from '../../../src/preact/utils';

/**
* @param {!JsonObject} data
* @return {string}
*/
const DEFAULT_RENDER = (data) => JSON.stringify(data);

/**
* @param {string} url
* @return {!Promise<!JsonObject>}
*/
const DEFAULT_FETCH = (url) => {
return fetch(url).then((res) => res.json());
};

/**
* @param {!RenderDef.Props} props
* @return {PreactDef.Renderable}
*/
export function Render({
src = '',
getJson = DEFAULT_FETCH,
render = DEFAULT_RENDER,
...rest
}) {
useResourcesNotify();

const [data, setData] = useState({});

useEffect(() => {
// TODO(dmanek): Add additional validation for src
// when adding url replacement logic.
if (!src) {
return;
}

getJson(src).then((data) => {
setData(data);
});
}, [src, getJson]);

const rendered = useRenderer(render, data);
const isHtml =
rendered && typeof rendered == 'object' && '__html' in rendered;

return (
<Wrapper {...rest} dangerouslySetInnerHTML={isHtml ? rendered : null}>
{isHtml ? null : rendered}
</Wrapper>
);
}
29 changes: 29 additions & 0 deletions extensions/amp-render/1.0/component.type.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Copyright 2021 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/** @externs */

/** @const */
var RenderDef = {};

/**
* @typedef {{
* src: (!string),
* getJson: (!Function)
* render: (?RendererFunctionType|undefined),
* }}
*/
RenderDef.Props;

0 comments on commit 7b339ba

Please sign in to comment.