Skip to content

Melody streams #102

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

Merged
merged 10 commits into from
Mar 6, 2019
Merged

Melody streams #102

merged 10 commits into from
Mar 6, 2019

Conversation

krakenfuss
Copy link
Contributor

What changed in this PR:

Adding new core package melody-streams.
This package aims to reduce the API surface and the amount of technologies an engineer has to know to be productive with Melody to a single one: Rx.js
It is based on the simplicity concept of hooks, while it works without any hooks and abstracts data handling and component updating completely to streams.

biz: 'baz',
arr: [1, 2, 3, 4],
};
testWith(mergeObject(spec), () => {}, []);
Copy link
Contributor

Choose a reason for hiding this comment

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

Since the outcomes of these test are quite small, could we inline the expect(...).toEqual(...) instead of using expect and toMatchSnapshot inside testWith?

Copy link
Contributor

Choose a reason for hiding this comment

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

This would make reading the tests way easier.

*/

export { createComponent } from './component';
export { createState } from './operators/createState';
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think about separating the functions in ./operators into helpers and operators?

Actual operators are:

combine: Observable
mergeIntoObjects: Observable
mergeObject: Observable

Helpers have a different API:

withElement: [Element => Subscription, Subject]
withEvent: [Element => Subscription, Subject]; 

createState: [Subject, state => void, () => state];

And maybe use withEvent instead of attachEvent for consistency?

Copy link
Contributor

Choose a reason for hiding this comment

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

I also like the name withState

// part of the public API
this.el = element;
this.refs = {};
this.props = new BehaviorSubject({});
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure, but this.props should be a plain object, otherwise this could break the dev tools.
Maybe we should name it this.propsStream and set this.props inside apply as soon as it receives new 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.

Good point. Changed it.

this.props.next(props);
if (this.subscriptions.length === 0) {
const t = this.getTransform({
dispatch(eventName, detail, options = {}) {
Copy link
Contributor

Choose a reason for hiding this comment

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

dispatchEvent or dispatchToDom or dispatchDomEvent ?

Copy link
Contributor

Choose a reason for hiding this comment

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

@pago suggested dispatchCustomEvent, since highlights that we’re not dispatching standard DOM events like 'click'. What do you think?


import { Subject } from 'rxjs';

export const withElement = handler => {
Copy link
Contributor

Choose a reason for hiding this comment

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

The name handler seems a bit confusing to me, I would expect that to be a callback function. Maybe createObservable or something would make it clearer.

const exec = refHandler();
testWith(subj, next(exec), ['foo', 'bar']);
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. what if handler is not a function
  2. what if handler doesn't return an observable

@@ -0,0 +1,65 @@
/**
Copy link
Contributor

Choose a reason for hiding this comment

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

Can be removed I guess

Copy link
Contributor

Choose a reason for hiding this comment

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

Can this be removed?

return props;
}, template);
// Snapshot should have 2 entries
testWithArguments(subj, render, [
Copy link
Contributor

Choose a reason for hiding this comment

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

We should also check the actual DOM outcome with root.outerHtml

});
const s = t
.pipe(distinctUntilChanged(shallowEqual))
.subscribe(state => {
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. what happens when an error occurs?
  2. what happens when the stream completes early?
  3. would it make sense to check for a valid state shape (plain object with only primitives as values) here?

},
props: this.props,
updates: this.updates,
subscribe: obs => this.subscriptions.push(obs.subscribe()),
Copy link
Contributor

Choose a reason for hiding this comment

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

What if obs is not an observable?

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we should provide a custom error here, something like Error: subscription object must be an Observable.

Copy link
Contributor

Choose a reason for hiding this comment

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

We can probably do that later if we really want to do it. If obs doesn't have a subscribe method. Or we "just" provide a .d.ts file to automate that.

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 we should throw an error if obs is not an Observable. This will make it easier for engineers, that are not familiar with the API. Otherwise you would get something like subscribe is not a function and that would be confusing, at least to me.


export { createComponent } from './component';
export { createState } from './operators/createState';
export { mergeObject } from './operators/mergeObject';
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd argue that mergeObject and mergeIntoObject should not be part of the public API. combine can do both their jobs and its easier to learn just one function instead of 3.

@pago
Copy link
Contributor

pago commented Feb 25, 2019

Let's also address this issue directly: #76
i.e. let's add a render method so that we won't have to teach melody-component just to render a component.

export const createState = initialValue => {
const subj = new BehaviorSubject(initialValue);

const mutator = newValue => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we really want to allow the mutator argument to be a function? If the same work newValue(subj.getValue()) can be done on the client side I think it's a bit redundant to add this option. Passing only the raw value on the other hand would simplify the implementation and create a more uniform API for the mutator.

Copy link
Contributor

Choose a reason for hiding this comment

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

I like the extra affordance of this particular overload and believe it could enable some very nice patterns (though I'm not sure yet which those would be). Adding it later would not be an option. Removing it also won't be possible.

Would you object strongly against keeping it? Or is it rather a preference?

Copy link
Contributor

Choose a reason for hiding this comment

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

If you ask me, this is a quite useful feature, e.g. for implementing a state reducer.

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 also believe this is a super useful thing. Having an option to pass in a function makes the API quite flexible and super powerful. I would strongly advise to keep it.

import { BehaviorSubject } from 'rxjs';

export const createState = initialValue => {
const subj = new BehaviorSubject(initialValue);
Copy link
Contributor

Choose a reason for hiding this comment

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

Just an idea maybe for future development. We could make use of ReplaySubject and have a different API than createState, let's say it would be called createTimeTravelerState that would allow us to easily to expose something like a .rewind() method on the component to restore previous states. Might be a little farfetched tough 😄

this.props.next(props);
if (this.subscriptions.length === 0) {
const t = this.getTransform({
dispatch(eventName, detail, options = {}) {
Copy link
Contributor

Choose a reason for hiding this comment

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

@pago suggested dispatchCustomEvent, since highlights that we’re not dispatching standard DOM events like 'click'. What do you think?

},
props: this.props,
updates: this.updates,
subscribe: obs => this.subscriptions.push(obs.subscribe()),
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we should provide a custom error here, something like Error: subscription object must be an Observable.

};

export const baseCreateComponent = (transform, templateFnOrObj) => {
const template = templateFnOrObj.render
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 check for type here? typeof templateFnOrObj.render === 'function'

Copy link
Contributor

Choose a reason for hiding this comment

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

No. If it's not a function, it'll throw. The only way to get into this code path is if you're not using twig templates but instead code the templates in JavaScript directly. It's a legacy thing.

if (args.length >= baseCreateComponent.length) {
return baseCreateComponent.apply(null, args);
}
return (...args2) => createComponent.apply(null, args.concat(args2));
Copy link
Contributor

Choose a reason for hiding this comment

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

Rename suggestion: args2 -> _args

Copy link
Contributor

Choose a reason for hiding this comment

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

Or use lodash and its curry function directly. _args is not much better though. I'd go for args => preloadedArgs and args2 => nextArgs or something like that if it was a concern. But I don't think this needs to be done now.

* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { assert } from 'chai';
Copy link
Collaborator

Choose a reason for hiding this comment

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

There is already an issue (#88) to remove chai's matchers with Jest's matchers.

Copy link
Contributor

@pago pago left a comment

Choose a reason for hiding this comment

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

Still open and needs to be done before merge:

  • Export a render method (can be copy-pasted from melody-component)
  • Support returning object from the transform
  • Potentially remove packages/melody-streams/__tests__/util/createTestComponent.js if not needed
  • Do we keep the setState(oldState => newState) overload for createState?

describe('createState', () => {
it('should create an Observable from initial data', () => {
const [count] = createState(0);
count.subscribe(x => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe more reliable with a test spy

@@ -0,0 +1,65 @@
/**
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this be removed?

},
props: this.props,
updates: this.updates,
subscribe: obs => this.subscriptions.push(obs.subscribe()),
Copy link
Contributor

Choose a reason for hiding this comment

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

We can probably do that later if we really want to do it. If obs doesn't have a subscribe method. Or we "just" provide a .d.ts file to automate that.

};

export const baseCreateComponent = (transform, templateFnOrObj) => {
const template = templateFnOrObj.render
Copy link
Contributor

Choose a reason for hiding this comment

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

No. If it's not a function, it'll throw. The only way to get into this code path is if you're not using twig templates but instead code the templates in JavaScript directly. It's a legacy thing.

if (args.length >= baseCreateComponent.length) {
return baseCreateComponent.apply(null, args);
}
return (...args2) => createComponent.apply(null, args.concat(args2));
Copy link
Contributor

Choose a reason for hiding this comment

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

Or use lodash and its curry function directly. _args is not much better though. I'd go for args => preloadedArgs and args2 => nextArgs or something like that if it was a concern. But I don't think this needs to be done now.

export const createState = initialValue => {
const subj = new BehaviorSubject(initialValue);

const mutator = newValue => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I like the extra affordance of this particular overload and believe it could enable some very nice patterns (though I'm not sure yet which those would be). Adding it later would not be an option. Removing it also won't be possible.

Would you object strongly against keeping it? Or is it rather a preference?

import { BehaviorSubject } from 'rxjs';

export const withElement = (handler, initialValue) => {
const subj = initialValue
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 we need to check for undefined explicitly. Otherwise withElement(fn, false) and similar cases won't work.

ChildComponent.prototype.render = function() {
return template(this.state);
};
ChildComponent.prototype.getTransform = transform;
Copy link
Contributor

Choose a reason for hiding this comment

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

If we want to support cases where transform returns an object of streams we'll need to ensure that the return value is always wrapped in combine.

Suggested change
ChildComponent.prototype.getTransform = transform;
ChildComponent.prototype.getTransform = api => combine(transform(api));

If the transform returns an observable, combine will simply return that. Otherwise it'll do its magic on the returned object.

* limitations under the License.
*/

export { createComponent } from './component';
Copy link
Contributor

Choose a reason for hiding this comment

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

We'll still need to expose a render function to avoid having to also depend on melody-component.

@krakenfuss krakenfuss requested a review from pago March 5, 2019 07:50
@pago pago merged commit 06c7e8c into trivago:master Mar 6, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants