Raynos / mercury

A truly modular frontend framework
http://raynos.github.io/mercury/
MIT License
2.82k stars 142 forks source link

Standing Challenges #140

Open kumavis opened 9 years ago

kumavis commented 9 years ago

Here are a few things we're still trying to figure out how to do best, in no particular order

Animations / CSSTransitionGroup

e.g. a removal animation that keeps a removed item in the dom long enough to animate out. mostly solved

Not duplicating state serialization

With shared state passed down the hierarchy you end up with duplicated serialization

Rendering into modals or other places

You can put a render function or vdom into state to hand it off to a modal component, but it doesnt serialize.

Formalized component interface

If we can set expectations of component interface, it will be easier to make reusable pieces for the community.

Non-DOM events

Triggering events on components that don't come from the DOM. see thread.

cc: @neonstalwart @Raynos some chat logs

kuraga commented 9 years ago

About the third and the fourth... Are there some techniques about immutables with object oriented programming?

We have a (static) render function with a state argument. Event handler is a (static) function with a state argument. Can we use classes here?

state is an immutable object - can't we have methods in it? But can we use classes here?

kumavis commented 9 years ago

I had an idea for Rendering into modals, but haven't tried it

If your components that want inject stuff into modals registered their modal content rendering functions at definition time, then they could set the content state of the modal-component to use their rendering function. Bad pseudo code:

var SaveChangesComponent = function(){
  app.modalController.register( 'save-changes', function(){ return h('.modal-content') } )

  var channels = {
    confirm: function(){ app.modalController.content.set('save-changes') }
  }
}
kuraga commented 9 years ago

Components... Is set of channels the one for the whole app? Or can events be component-specific?

kumavis commented 9 years ago

@neonstalwart differentiates between channels and events. Both are component specific and don't bubble.

neonstalwart commented 9 years ago

typically for generic components (like a text input component) they either generate generic events (like change) that a parent element can listen to (textInput.events.change(state.firstName.set)) or they accept an observable (like value) that will be updated by the input (state.firstNameEditor.set(textInput({ value: state.firstName }))). usually i've implemented both options but maybe i should just choose one and be consistent with it - that's the kind of thing to decide as part of trying to come up with a component API. if we can agree on a suitable API, i could probably start contributing a few generic components to use as a starting point.

i also use events for my application-level events (loginComponent.events.success(app.events.login) i.e. when a generic login component has a success event, trigger the more specific application-level login event) which is something that can't be done with channels since channels can only be triggered from DOM events. you may wonder why i have a generic loginComponent.events.success and a more specific app.events.login and the reason is that i prefer not to embed application logic directly in components if i can avoid it. this makes it easier to isolate components from the application logic when it comes to testing the components and it also means that i could have multiple places/components that may trigger the same application event - e.g. ctrl+s, File->Save menu, Save button, could all trigger app.events.save(thing) without embedding the save logic in any of those places.

Is set of channels the one for the whole app? Or can events be component-specific?

so, i have events for both - some are for the whole app and some are component-specific and the component-specific ones trigger the ones for the whole app.

@kuraga i don't know if i understand your question properly... no part of that line of code is a handler.

function parentComponent(options) {
    var events = hg.input([ ... ]),
        firstName = hg.value(options.firstName),
        state = hg.struct({
            events: events,
            firstName: firstName,
            // equivalent to: state.firstNameEditor.set(textInput({ value: state.firstName }))
            firstNameEditor: textInput({ value: firstName })
        }),
        ...

    return state;
})

does that help at all?

kuraga commented 9 years ago

@neonstalwart thanks, very useful! But sorry my question/thought is different.

Is here state - state of the whole app, or is it component's part only?

Acccording to this, context (i.e. state above) is set by channels function. Who does call it? state function does and nobody calls it in examples explicity.

So, channels' context is set by state function. And we're calling it when we're creating whole app state. So, here state is state of the whole app, it's not component specific.

Correct?

kumavis commented 9 years ago

@kuraga are you talking about my example code or...

kuraga commented 9 years ago

@kumavis no. See links in the message.

P.S. Originally it wasn't a FAQ, I wanted to talk about "good way to isolate parts of state when handling a channel event". But seems like we don't understand each other. Just re-read my last message, please... Thanks.

neonstalwart commented 9 years ago

Is here state - state of the whole app, or is it component's part only?

the example is trivial so it's no surprise that it doesn't help answer the question. the general pattern would be that state is a single component's state. in such a trivial example, the whole application is just one component so that example doesn't help clarify whether this pattern is for the whole app or just a single component.

Acccording to this, context (i.e. state above) is set by channels function. Who does call it? state function does and nobody calls it in examples explicity.

right, again, trivial examples are misleading. think of that hg.state function as a component factory which wires together state and channels. the todomvc example is less trivial and it shows a TodoItem component which uses hg.state to produce a component. the state which gets passed to each channel is the relevant state for a single TodoItem. at the application level, TodoApp also uses hg.state but embeds a number of TodoItem states at state.todos

So, channels' context is set by state function. And we're calling it when we're creating whole app state. So, here state is state of the whole app, it's not component specific.

this is the beauty of all of this... a simple pattern is applicable to a single component and to the whole application (which is really just another component). so, there is no rule that hg.state is only for components or only for the whole app - it is a pattern that can be applied at the macro and micro level because the idea of a component is applicable at both levels. everything is a component.

kuraga commented 9 years ago

@neonstalwart

the todomvc example is less trivial and it shows a TodoItem component which uses hg.state to produce a component. the state which gets passed to each channel is the relevant state for a single TodoItem. at the application level, TodoApp also uses hg.state but embeds a number of TodoItem states at state.todos

This is a key. Thanks very much! So, there were nothinng to discuss - it was my misunderstanding only. Feel free to clean up our messages. Thanks!

alexmingoia commented 9 years ago

Can we add routing to the list of standing challenges? Switch statements in views leads to lots of duplicated code... IMO component-based routing approaches like react-router avoid this problem.

Raynos commented 9 years ago

https://github.com/twilson63/mercury-router

crabmusket commented 9 years ago

Rendering into modals or other places

You can put a render function or vdom into state to hand it off to a modal component, but it doesnt serialize.

Why would you ever need to change the contents? If you need two different modal contents, render two modals and enable whichever of them is appropriate.

kumavis commented 9 years ago

Why would you ever need to change the contents?

its more about a deeper component (widgets list) interacting with a component higher in the DOM ( widgets deletion confirm modal )

Right now im thinking of a top-level modal controller that other components that need modals register with when they are defined. Having a single modal controller also guarantees you wont endup with two modals showing at once, overlapping.

crabmusket commented 9 years ago

Something I've come to realise is related to #132. Mercury doesn't just have a problem dealing with non-DOM events, but also non-DOM rendering. For example, you could view notifications as a result of rendering your application state, but virtual-dom doesn't include notifications, so you have to manage them manually somehow. Similarly, network calls are a result of 'rendering' your application state to the network, and the events that come back from the network are analogous to DOM events.

Having a single modal controller also guarantees you wont endup with two modals showing at once, overlapping.

Fair enough, but if that happens unintentionally you probably have a bug elsewhere you'd need to fix anyway.

kumavis commented 9 years ago

@eightyeight very insightful, hadnt thought of notifications / network requests as a sort of rendering

crabmusket commented 9 years ago

I've been trying to attach audio to the page by rendering an <audio> tag when certain parts of the state exist, but ran into the problem that requestAnimationFrame is often paused when a tab is unfocused. If you're using audio as a notification of, for example, chat messages appearing, then rendering audio as part of your application rendering isn't going to cut it, because that rendering won't happen in the RAF until the user visits the application tab.

I haven't decided how we'll solve this yet; probably just hang a listener on the app state that uses the Audio API directly, rather than rendering an audio tag. Not sure if this is enough of a problem to warrant thoughts about an entire non-DOM 'rendering' pipeline.

yoshuawuyts commented 9 years ago

just hang a listener on the app state that uses the Audio API directly

Though it's a good idea I believe the Audio API exposes a subset of what an audio tag is capable of doing, so attaching a tag might be what you want to do regardless. I think having a direct listener on the state outside mercury might be the best solution for this.

crabmusket commented 9 years ago

The audio API does enough for our purposes that I don't want to bother with the DOM.

Side-note, observ makes it really easy to listen to state changes accurately, versus Angular's listeners which basically just dump the entire new object on you and let you diff it yourself.