Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Initial PreactBaseElement #25969

Merged
merged 54 commits into from
Jan 15, 2020
Merged

Conversation

jridgewell
Copy link
Contributor

No description provided.

@amp-owners-bot
Copy link

amp-owners-bot bot commented Dec 10, 2019

Hey @rsimha, these files were changed:

  • build-system/compile/compile.js

Hey @erwinmombay, these files were changed:

  • build-system/eslint-rules/no-import.js

@lgtm-com
Copy link

lgtm-com bot commented Dec 10, 2019

This pull request introduces 1 alert when merging 9cbb946 into 67d1415 - view on LGTM.com

new alerts:

  • 1 for Unreachable statement

examples/amp-date-display.amp.html Show resolved Hide resolved
examples/amp-date-display.amp.html Outdated Show resolved Hide resolved
extensions/amp-date-display/0.2/amp-date-display.js Outdated Show resolved Hide resolved
extensions/amp-date-display/0.2/amp-date-display.js Outdated Show resolved Hide resolved
src/preact-base-element.js Outdated Show resolved Hide resolved
src/preact-base-element.js Outdated Show resolved Hide resolved
src/preact/context.js Outdated Show resolved Hide resolved
src/preact/utils.js Outdated Show resolved Hide resolved
src/preact/utils.js Outdated Show resolved Hide resolved
const data = /** @type {!JsonObject} */ (getDataForTemplate(props));
useResourcesNotify();

return render(data, props['children']);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, what are the children in this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a <slot> JSX node in AMP/Bento mode, and whatever JSX nodes the dev passes in React mode.

);
host.dispatchEvent(event);

return null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify, this does not causes Preact to clear out all DOM parent's children, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct.

* @return {*} TODO
*/
function AmpDateDisplayComponent(props) {
const render = props['render'];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't JsonObject there to be able to just use dot property access, object pattern, etc? This might matter I think also because there sometimes d.ts files used to define the props and thus they could be open to minification.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JsonObject is the opposite, if forces computed foo['bar'] access. As a public interface, I think it's required that these never be minified.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But props are really meant to be compilable symbols. Even further, I've recently seen mainly the following form used and find it very helpful:

- function AmpDateDisplayComponent(props) {
-   const { render } = props;
+ function AmpDateDisplayComponent( { render } ) {

It really helps to get the shape of the component at a glance.

We don't necessarily need to "get there" on the first PR. But I think we need to have a general agreement on the direction.

*/
function AsyncRender(props) {
const children = props['children'];
const {0: state, 1: set} = useState(children);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just array form?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Array destructuring requires the iterator protocol (which also requires symbols). Using object destructuring will be smaller for legacy builds, and faster all the time.

* @param {!JsonObject} props
* @return {*} TODO
*/
function AmpDateDisplayComponent(props) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A nit, but I'd like the React-only stuff to go into a separate module.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

* limitations under the License.
*/

import {Deferred} from './utils/promise';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT about this one?

/** @override */
isLayoutSupported(layout) {
const layoutSupported = opts.isLayoutSupported;
return layoutSupported ? layoutSupported.call(this, layout) : true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. It's ok to delay this to followup PRs, but let's touch base one why this shouldn't just be a simple class extends way. Is it only because of props?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just haven't converted it yet. We can immediately change it in a follow up.

export function getAmpContext() {
return (
context ||
(context = createContext({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense for us to do an exception for side-effects in this case? I'm thinking this class is part of the AMP's React base that could be exportable as it's own library. So the closer we could get to the canonical state for React the better for these modules.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For use internally, this will be no different. If we export it externally, we could do a code mod? But either way, they'll be different context instances in different modules, and changing it doesn't help.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. So I'm wondering why not just allow side-effects in this case to make this more-or-less a classical Preact/React-style declaration?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of my goals this year is to eliminate all side-effects, so that transitive dependencies don't accidentally cause a bunch of unintentional headaches. So, I'd rather we leave this as a function call.

/**
* @return {function()}
*/
export function useRerenderer() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove this for now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

{current: element.getRootNode().getElementById(value)}
: value;
props[name] = v;
}
Copy link
Member

@samouri samouri Jan 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: What do you think about flattening the ternary into a series of else-ifs?

if (value == null) {
  props[name] = def.default;
} else if (def.type == 'number') {
  props[name] = Number(value)
} else if (def.type == 'Element') {
  // TBD: what's the best way for element referencing compat between
  // React and AMP? Currently modeled as a Ref.
  props[name] = {current: element.getRootNode().getElementById(value)}
} else {
  props[name] = value;
}

export function getAmpContext() {
return (
context ||
(context = createContext({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. So I'm wondering why not just allow side-effects in this case to make this more-or-less a classical Preact/React-style declaration?

* @param {string} attr
* @param {boolean} on
*/
function toggleAttribute(element, attr, on) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should already be in dom.js. I think I stole it from there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

* @param {*} o2
* @return {boolean}
*/
function objectsEqualShallow(o1, o2) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move to object.js?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

export function DateDisplay(props) {
const render = props['render'];
const data = /** @type {!JsonObject} */ (getDataForTemplate(props));
useResourcesNotify();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still struggling this this call. I feel like if we constraint it strictly to only be in the RenderDomTree that should be sufficient? Then we have a nice separation of concerns: DateDisplay functions more or less strictly within the React-style. Whole RenderDomTree adopts it to AMP.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, yes.

But calling useResourcesNotify() will be a requirement for every Component, whether or not they actually rendering anything. It's because we'll be adding a lint rule for it, and there's no way for me to tell from the linter whether an DOM mutation could occur.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is ok to start this way. But I'll still be asking and looking for something more composite here. We have pretty clear touchpoints where we deal with actual light/shadow DOMs between Slot and RenderDomTree. Also, per our React prototype, it's very likely that we will decide to wrap all/most of AMP elements into some form of a Root component to preserve the common layout semantics. The bottom line: let's roll with it for now, but we'll likely come back to it again.

* @param {!JsonObject} props
* @return {null}
*/
export function RenderDomTree(props) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we move this to src/preact/ somewhere along with AsyncRender? These seem to be important classes to support a particular type of our components.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to wait until we had other components to move it. Yes, it'll likely end up there.

* @return {*} TODO
*/
function AmpDateDisplayComponent(props) {
const render = props['render'];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But props are really meant to be compilable symbols. Even further, I've recently seen mainly the following form used and find it very helpful:

- function AmpDateDisplayComponent(props) {
-   const { render } = props;
+ function AmpDateDisplayComponent( { render } ) {

It really helps to get the shape of the component at a glance.

We don't necessarily need to "get there" on the first PR. But I think we need to have a general agreement on the direction.

export function DateDisplay(props) {
const render = props['render'];
const data = /** @type {!JsonObject} */ (getDataForTemplate(props));
useResourcesNotify();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, yes.

But calling useResourcesNotify() will be a requirement for every Component, whether or not they actually rendering anything. It's because we'll be adding a lint rule for it, and there's no way for me to tell from the linter whether an DOM mutation could occur.

* @param {!JsonObject} props
* @return {null}
*/
export function RenderDomTree(props) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to wait until we had other components to move it. Yes, it'll likely end up there.

export function getAmpContext() {
return (
context ||
(context = createContext({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of my goals this year is to eliminate all side-effects, so that transitive dependencies don't accidentally cause a bunch of unintentional headaches. So, I'd rather we leave this as a function call.

* @param {*} o2
* @return {boolean}
*/
function objectsEqualShallow(o1, o2) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

* @param {string} attr
* @param {boolean} on
*/
function toggleAttribute(element, attr, on) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

* @return {Preact.Renderable}
*/
export function DateDisplay(props) {
const render = props['render'];
Copy link
Contributor Author

@jridgewell jridgewell Jan 11, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Continuing #25969 (comment):

But props are really meant to be compilable symbols.

We cannot change this. If we allow non-string access, then mangling will break the components between the amp-to-react boundary. Eg, we retrieve the display-in attribute, and set it to props['displayIn'] as part of collect props. But Closure will mangle the const { displayIn } = props that it receives in the Preact component (to QD, in my case). So we get undefined instead of the value we needed.

// Collect props does this:
const props = { ['displayIn']: el.getAttribute('display-in')};

// Preact does this due to mangling:
const displayIn = props.QD;

Even further, I've recently seen mainly the following form used and find it very helpful:

We can still do destructuring, if you'd like:

const { 'render': render } = props

It just has to use string keys and not the shorthand.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I see your point. But it's so unidiomatic. What's our long-term approach here? Can we instruct closure to ignore minification for props? For these modules in their entirety? Should we transform these modules to {render} to {'render': render} before feeding them to Closure? I.e. let's take the step we need now, but what's our north star here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see two solutions:

  1. We transform any props access to string form before closure. This has the downside that any foo(props) call must also use props as the parameter name, which is extremely difficult to lint for. We may be able to get away with it if we force the type to still be a !JsonObject, but then our devs will sometimes have to write string keys for JsonObjects and sometimes not.

  2. We extern the props definition for every component. I think this is actually the better solution. It just requires us to locate the extern file somewhere else, which is annoying.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO, I agree. I think either solution (even maybe converting d.ts to externs) are good for what we want. React-style is a bit of a different domain language, especially if we allow jsx. So special transformers are not a crazy idea for certain.

@dvoytenko
Copy link
Contributor

One of my goals this year is to eliminate all side-effects, so that transitive dependencies don't accidentally cause a bunch of unintentional headaches. So, I'd rather we leave this as a function call.

But any major concerns about moving too far away from the "canonical" React style for this? E.g. we could explicitly allow this use for contexts and nothing else.

@Gregable Gregable added the WG: Caching: Triaged Initial triage from wg-caching complete. Remove label if new input required. label Jan 14, 2020
@jridgewell
Copy link
Contributor Author

But any major concerns about moving too far away from the "canonical" React style for this? E.g. we could explicitly allow this use for contexts and nothing else.

For this, I don't see useContext(getContext()) call as being materially different than useContext(Context). I don't think anyone will be confused by this.

This plugin transforms the collquial name into the fully qualified import path.

This is necessary for the new preact integrations, since `preact/hooks` actually does `import * from 'preact'`. We can't directly import `preact` like that, it has to be the `preact/dist/preact.module.js` path so Closure will work correctly.
@jridgewell jridgewell changed the title WIP: ✨ Initial PreactBaseElement ✨ Initial PreactBaseElement Jan 15, 2020
@jridgewell jridgewell merged commit 5ea027c into ampproject:master Jan 15, 2020
@jridgewell jridgewell deleted the react-base-element branch January 15, 2020 08:26
@dvoytenko
Copy link
Contributor

For this, I don't see useContext(getContext()) call as being materially different than useContext(Context). I don't think anyone will be confused by this.

Certainly in this case it doesn't matter. We should even simply create a static hook useAmpContext() for this. But I'm bringing this up because:

  1. Contexts are used inline potentially a lot for some type of components. E.g. List->ListItem, and similar. Sometimes such context objects could even be confined to a single module. If we don't allow side-effects for this, this if (!exists) {create()} factories will spread pretty quickly.
  2. It's a pattern in React. Other side-effects include const Component = forwardRef(...) and high-order components, and maybe some other.
  3. Otherwise, React and AMP are very much aligned: we both hate side effects. The forwardRef, createContext and such are simply not side-effects for React, but rather a declaration type. So if we can bridge this gap - we'll be more idiomatic, but both ecosystems would "get what they want".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla: yes PR use: In Beta / Experimental WG: Caching: Triaged Initial triage from wg-caching complete. Remove label if new input required.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants