-
Notifications
You must be signed in to change notification settings - Fork 38
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
Melody streams #102
Conversation
biz: 'baz', | ||
arr: [1, 2, 3, 4], | ||
}; | ||
testWith(mergeObject(spec), () => {}, []); |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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({}); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 = {}) { |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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 => { |
There was a problem hiding this comment.
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']); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- what if
handler
is not a function - what if
handler
doesn't return an observable
@@ -0,0 +1,65 @@ | |||
/** |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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, [ |
There was a problem hiding this comment.
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 => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- what happens when an error occurs?
- what happens when the stream completes early?
- 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()), |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
packages/melody-streams/src/index.js
Outdated
|
||
export { createComponent } from './component'; | ||
export { createState } from './operators/createState'; | ||
export { mergeObject } from './operators/mergeObject'; |
There was a problem hiding this comment.
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.
Let's also address this issue directly: #76 |
export const createState = initialValue => { | ||
const subj = new BehaviorSubject(initialValue); | ||
|
||
const mutator = newValue => { |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 = {}) { |
There was a problem hiding this comment.
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()), |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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'
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename suggestion: args2
-> _args
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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.
There was a problem hiding this 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 frommelody-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 forcreateState
?
describe('createState', () => { | ||
it('should create an Observable from initial data', () => { | ||
const [count] = createState(0); | ||
count.subscribe(x => { |
There was a problem hiding this comment.
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 @@ | |||
/** |
There was a problem hiding this comment.
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()), |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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 => { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
.
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'; |
There was a problem hiding this comment.
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
.
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.