Skip to content

Commit

Permalink
Freshen up remote-ui/core APIs (Shopify#197)
Browse files Browse the repository at this point in the history
  • Loading branch information
lemonmade authored Dec 7, 2022
1 parent 3ec78e0 commit e15d142
Show file tree
Hide file tree
Showing 20 changed files with 374 additions and 123 deletions.
18 changes: 18 additions & 0 deletions .changeset/loud-kangaroos-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@remote-ui/core': minor
'@remote-ui/htm': minor
'@remote-ui/mini-react': minor
'@remote-ui/react': minor
'@remote-ui/rpc': minor
'@remote-ui/testing': minor
'@remote-ui/traversal': minor
'@remote-ui/vue': minor
---

Added a number of methods that align more closely with the corresponding DOM API, and deprecated a few existing methods with overlapping functionality:

- `RemoteParent.appendChild` is deprecated, with a new `RemoteParent.append` API recommended instead. This new API matches the [`Element.append`](https://developer.mozilla.org/en-US/docs/Web/API/Element/append) DOM API: it allows you to pass multiple children, including strings that are converted to text nodes.
- `RemoteParent.insertChildBefore` is deprecated, with a new `RemoteParent.insertBefore` API recommended instead. This matches the [`Node.insertBefore`](https://developer.mozilla.org/en-US/docs/Web/API/Node/insertBefore) DOM API, including the fact that the second argument can be null (in which case, the method behaves the same as `append`
- `RemoteParent.replaceChildren` is new, and matches the [`Element.replaceChildren`](https://developer.mozilla.org/en-US/docs/Web/API/Element/replaceChildren) DOM API. It allows passing any number of children/ strings, and those are used to fully replace the existing children.
- `RemoteComponent.remove` and `RemoteText.remove` are new, and match the [`Element.remove`](https://developer.mozilla.org/en-US/docs/Web/API/Element/remove) DOM API.
- `RemoteText.updateText` is deprecated in favor of a new `RemoteText.update` method, which is a little shorter.
2 changes: 1 addition & 1 deletion documentation/component-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ However, remote-ui has one important feature that can impact some component APIs
const button = root.createComponent(Button, {
icon: root.createComponent(Icon),
});
root.appendChild(button);
root.append(button);
root.mount();
```

Expand Down
8 changes: 4 additions & 4 deletions documentation/comprehensive-example.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,17 +155,17 @@ Our remote environment is now fully constructed and can load third-party code. T

self.onRender((root) => {
const card = root.createComponent('Card');
card.appendChild('Card contents');
card.append('Card contents');

const button = root.createComponent('Button', {
onPress() {
console.log('Pressed!');
},
});
button.appendChild('Click me');
button.append('Click me');

card.appendChild(button);
root.appendChild(card);
card.append(button);
root.append(card);
root.mount();
});
```
Expand Down
2 changes: 1 addition & 1 deletion examples/vanilla-dom/app/remote.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ endpoint.expose({

const root = createRemoteRoot(receiver);

root.appendChild(
root.append(
root.createComponent(
'Button',
{
Expand Down
94 changes: 68 additions & 26 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,12 @@ const button = root.createComponent('Button', {

If you are familiar with React, or if the “host” side of these components is implemented in React, you may be tempted to pass a function as the `children` prop. This pattern is commonly referred to as “render props” in React. However, this pattern likely is not doing what you think it is. Remember that functions passed as props will always be asynchronous when called by the host, because they are implemented with message passing. This makes them poorly suited for rendering UI, where you generally need to run synchronous functions.

To prevent this kind of mistake, any `children` prop will be deleted from the props object. If you want to implement a render prop-style API, you can do so without potentially causing confusion by using a different prop name, and ensuring that you handle the fact that the host will receive a promise whenever they call this function. If you are just trying to append other `RemoteComponent` and `RemoteText` instances to your tree, use `RemoteComponent#appendChild()`.
To prevent this kind of mistake, any `children` prop will be deleted from the props object. If you want to implement a render prop-style API, you can do so without potentially causing confusion by using a different prop name, and ensuring that you handle the fact that the host will receive a promise whenever they call this function. If you are just trying to append other `RemoteComponent` and `RemoteText` instances to your tree, use `RemoteComponent#append()`.

`createComponent` also allows you to pass initial children for the created component. If you have only one child, you can pass it directly as the third argument. If you have more than one child, you can either pass them as an array for the third argument, or as additional positional arguments. You can also pass a string directly, and it will be normalized into a `RemoteText` object for you.

```ts
root.appendChild(
root.append(
root.createComponent('BlockStack', undefined, [
root.createComponent('Text', undefined, 'This will be fun!'),
root.createComponent(
Expand All @@ -97,35 +97,35 @@ root.appendChild(
```ts
const iconFragment = root.createFragment();
const icon = root.createComponent('Icon');
iconFragment.appendChild(icon);
iconFragment.append(icon);

const headerFragment = root.createFragment();
const header = root.createText('Hello world!');
headerFragment.appendChild(header);
headerFragment.append(header);
const card = root.createComponent('Card', {
icon: iconFragment,
header: headerFragment,
});
```

##### `RemoteRoot#appendChild()`
##### `RemoteRoot#append()`

This method appends a `RemoteComponent` or `RemoteText` to the remote root as the last child. This method returns a promise for when the update has been applied in the host.
This method appends one or more `RemoteComponent` or `RemoteText` to the remote root as the last children. This method returns a promise for when the update has been applied in the host.

```ts
const card = root.createComponent('Card');
root.appendChild(card);
root.append(card);
```

##### `RemoteRoot#insertChildBefore()`
##### `RemoteRoot#insertBefore()`

This method inserts a `RemoteComponent` or `RemoteText` in the remote root before the specified child. This method returns a promise for when the update has been applied in the host.
This method inserts a `RemoteComponent` or `RemoteText` in the remote root before the specified child. This method returns a promise for when the update has been applied in the host. If the second argument is excluded, this method behaves identically to `append()`.

```ts
const card = root.createComponent('Card');
const earlierCard = root.createComponent('Card');
root.appendChild(card);
root.insertChildBefore(earlierCard, card);
root.append(card);
root.insertBefore(earlierCard, card);
```

##### `RemoteRoot#removeChild()`
Expand All @@ -134,13 +134,27 @@ This method removes a `RemoteComponent` or `RemoteText` from the remote root. Th

```ts
const card = root.createComponent('Card');
root.appendChild(card);
root.append(card);

// later...

root.removeChild(card);
```

##### `RemoteRoot#replaceChildren()`

This method removes all children from the root, and replaces them with the list of children passed to this method. This method returns a promise for when the update has been applied in the host.

```ts
const card = root.createComponent('Card');
root.append(card);

// later...

const newCard = root.createComponent('Card');
root.replaceChildren(newCard);
```

##### `RemoteRoot#children`

The `children` property is a readonly list of the components mounted to the tree. It does not necessarily represent the state of the host, as updates are reflected immediately to the local tree of components, but can be applied asynchronously in the host.
Expand All @@ -151,7 +165,7 @@ The `mount` method flushes the initial tree to the host. Before `mount` is calle

```ts
const card = root.createComponent('Card');
root.appendChild(card);
root.append(card);
root.mount();
```

Expand Down Expand Up @@ -197,25 +211,39 @@ if (SHOULD_BE_DISABLED) {
}
```

##### `RemoteComponent#appendChild()`
##### `RemoteComponent#remove()`

Removes the component from its parent, if it is attached to one.

Just like [`RemoteRoot#appendChild`](#remoterootappendchild), but appending a child for a single component rather than the root.
```ts
const card = root.createComponent('Card');
root.append(card);
card.remove();
```

##### `RemoteComponent#insertChildBefore()`
##### `RemoteComponent#append()`

Just like [`RemoteRoot#insertChildBefore`](#remoterootinsertchildbefore), but inserting a child for a single component rather than the root.
Just like [`RemoteRoot#append`](#remoterootappend), but appending children for a single component rather than the root.

##### `RemoteComponent#insertBefore()`

Just like [`RemoteRoot#insertBefore`](#remoterootinsertbefore), but inserting a child for a single component rather than the root.

##### `RemoteComponent#removeChild()`

Just like [`RemoteRoot#removeChild`](#remoterootremovechild), but removing a child for a single component rather than the root.

##### `RemoteComponent#replaceChildren()`

Just like [`RemoteRoot#replaceChildren`](#remoterootreplacechildren), but replacing children for a single component rather than the root.

#### `RemoteText`

A `RemoteText` object represents a text element being rendered to the host UI. It has the following properties and methods:

##### `RemoteText#text`

The current text content of the element. This representation is not mutable, so changing any value on this object will have no effect (use `updateText` instead)
The current text content of the element. This representation is not mutable, so changing any value on this object will have no effect (use `update` instead)

##### `RemoteText#root`

Expand All @@ -225,18 +253,28 @@ A readonly reference to the root that constructed this component.

A readonly reference to the parent of this component in the tree (or `null`, if it has no parent).

##### `RemoteText#updateText()`
##### `RemoteText#update()`

Updates the text content.

```ts
const text = root.createText('Hello');

if (LOCALE === 'fr') {
text.updateText('Bonjour');
text.update('Bonjour');
}
```

##### `RemoteComponent#remove()`

Removes the text from its parent, if it is attached to one.

```ts
const text = root.createText('Hello');
root.append(card);
text.remove();
```

#### `RemoteFragment`

The `Root#createFragment` method creates a sub tree that can be used as a prop of any `RemoteComponent`. `RemoteFragment` does not any props.
Expand All @@ -257,18 +295,22 @@ A readonly reference to the root that constructed this fragment.

A readonly reference to the parent of this fragment in the tree (or `null`, if it has no parent).

##### `RemoteFragment#appendChild()`
##### `RemoteFragment#append()`

Just like [`RemoteRoot#appendChild`](#remoterootappendchild), but appending a child for a single fragment rather than the root.
Just like [`RemoteRoot#append`](#remoterootappend), but appending a child for a single fragment rather than the root.

##### `RemoteFragment#insertChildBefore()`
##### `RemoteFragment#insertBefore()`

Just like [`RemoteRoot#insertChildBefore`](#remoterootinsertchildbefore), but inserting a child for a single fragment rather than the root.
Just like [`RemoteRoot#insertBefore`](#remoterootinsertbefore), but inserting a child for a single fragment rather than the root.

##### `RemoteFragment#removeChild()`

Just like [`RemoteRoot#removeChild`](#remoterootremovechild), but removing a child for a single fragment rather than the root.

##### `RemoteFragment#replaceChildren()`

Just like [`RemoteRoot#replaceChildren`](#remoterootreplacechildren), but replacing children for a single fragment rather than the root.

### `createRemoteReceiver()`

#### `RemoteReceiver`
Expand Down Expand Up @@ -386,7 +428,7 @@ const Card = createRemoteComponent<'Card', {title: string}, typeof CardSection>(
);
```
These types are used to validate the passed arguments in `RemoteRoot#createComponent`, `RemoteRoot#appendChild`, and the other mutation APIs. With the example above, TypeScript would complain about the following calls, because we are not providing the mandatory `title` prop to `createComponent`, and we are appending a component other than `CardSection` to `Card`.
These types are used to validate the passed arguments in `RemoteRoot#createComponent`, `RemoteRoot#append`, and the other mutation APIs. With the example above, TypeScript would complain about the following calls, because we are not providing the mandatory `title` prop to `createComponent`, and we are appending a component other than `CardSection` to `Card`.
```ts
import {createRemoteRoot} from '@remote-ui/core';
Expand All @@ -396,7 +438,7 @@ const button = root.createComponent(Button, {
onPress: () => console.log('Clicked!'),
});
const card = root.createComponent(Card);
card.appendChild(button);
card.append(button);
```
### Other exports
Expand Down
Loading

0 comments on commit e15d142

Please sign in to comment.