Raynos / mercury

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

How to attach state to renderer? #218

Open KoderFPV opened 7 years ago

KoderFPV commented 7 years ago

I have part table component, this component will use part_row component so lets imagine:

Part_row component:

State:

export default (part) => {
    return hg.state({
        part_number: hg.value(part.part_number),
        part_type: hg.value(part.part_type),
        description: hg.value(part.description),
        quantity: hg.value(part.quantity),
        suggested_quantity: hg.value(part.suggested_quantity),
        unit_price: hg.value(part.unit_price),
        total_list_price: hg.value(part.total_list_price)
    })
}

Renderer:

export default (state) => {
    return h('div.row.part', [
        h('div.col-xs-2.part_number', state.part_number),
        h('div.col-xs-1.part_type', state.part_type),
        h('div.col-xs-2.description', state.description),
        h('div.col-xs-1.quantity', h('input', { type: 'number', value: state.quantity })),
        h('div.col-xs-2.suggested_quantity', state.suggested_quantity),
        h('div.col-xs-2.unit_price', state.unit_price),
        h('div.col-xs-2.total_list_price', state.total_list_price)
    ]);
}

Table component

State:

export default (opts) => {
    return hg.state({
        parts_list: opts.parts
    })
}

And finally renderer:

export const part_table_renderer = (state) => {
    return h('div#part_table', [
        h('div.row.header', [
            h('div.col-xs-2', 'PART NUMBER'),
            h('div.col-xs-1', 'PART TYPE'),
            h('div.col-xs-2', 'DESCRIPTION'),
            h('div.col-xs-1', 'QUANTITY'),
            h('div.col-xs-2', 'SUGGESTED QUANTITY'),
            h('div.col-xs-2', 'UNIT PRICE (USD)'),
            h('div.col-xs-2', 'TOTAL LIST PRICE (USD)')
        ]),
        h('div.body', [
            state.parts_list.map((part) => part_component_renderer(part));
        ])
}

Sooooo how to "connect" or "attach" in this pattern part state? I don understand that. Why I should create state for part_row component if I pass state to renderer. The only stupid solution what I can imagine would be

Table component State:

export default (opts) => {
    return hg.state({
        part1: opts.parts[0]
        part2: opts.parts[1]
        part3: opts.parts[2]
        part4: opts.parts[3]
        part5: opts.parts[4]
    })
}

Thanks for help :) Regards

crabmusket commented 7 years ago

I'm not sure exactly what your question is. Are you already running a mercury app that these components will be put inside?

It looks like you want parts_list in the table component to be an array of objects like the ones created by the part_row component, right? In that case I think you could just import the part_row function and do something like:

export default (opts) => {
    return hg.state({
        parts_list: opts.parts.map(part_row_state),
    })
}
KoderFPV commented 7 years ago

@crabmusket Thanks it is nearly what I need. But I had to do this:

     parts: Store.parts.map((part) => { 
            return part_model(part)**()**
        })

And now when I have in part_row component :

    channels: {
            changeQuantity: changeQuantity
        }

function changeQuantity(state, value) {
    const quantity = parseInt(value.quantity)
    state.quantity.set(quantity)
    state.total_list_price.set(quantity * state.unit_price())
}

changeQuantity is triggered on event, it works, but it does not re render part_row. Do you know why?

Here is repo: https://github.com/Tarvald/bom Here part row state: https://github.com/Tarvald/bom/blob/master/src/components/parts_list/part_table/part/part.model.js Here part table state: https://github.com/Tarvald/bom/blob/master/src/components/parts_list/part_table/part_table.model.js

crabmusket commented 7 years ago

Hmm that doesn't look right - part_model(part) will return a hg.state, right? Then calling part_model(part)() will get the value of the state as a plain JS object. But don't you want a tree of hg.state objects? I imagine part_row is not re-rendered for that reason? It's been too long since I worked with mercury :sweat_smile:

KoderFPV commented 7 years ago

@crabmusket I thought that hg.state return ready-to-ready object with values. I don't understand why I should not get values of state as a plain JS object. And why should I want hg.state object? And I would like to re render part_row, that why I am doing this channels, and states tricks.

Sorry I'm quite new in FRP, mercury, states etc.

KoderFPV commented 7 years ago

So even I did
parts_list: opts.parts.map(part_row_state),

In the render I had to do state.part_number() instead of just state.part_number And still row does not re render

crabmusket commented 7 years ago

Maybe I'm misremembering, and you can't nest hg.state objects. The application state should be a tree of observable objects, and then when rendering happens, you turn the observ tree into a non-observ object (which is what all the render functions get passed).

ashnur commented 7 years ago

Ok, so please take the following as a very subjective opinion: I personally found that it becomes very hard to manage observ-* trees when they become large, so at first I tried to fix it by throwing code at it (https://github.com/ashnur/observ-change, https://www.npmjs.com/package/observ-confined)

However this still was not quite good enough. So I went back and thought a lot about this, what is the best way to deal with state on the frontend when it becomes large and complicated, and since then I am using a mercury where I replaced the observ-* tree with https://github.com/typeetfunc/datascript-mori

Now, this has it's own issues, mainly that writing datalog in js and using mori functions is still a pita, but at least now state is easy. It's in one place, it's fast, and I can ask anything from it with a cool query language. So writing new views although is very verbose and takes longer than it should, it also gives a very flat structure that's easy to understand and grasp once you know what's happening.

Again, this fits my needs so far, when it doesn't, I tend to not use mercury but go for react/redux.

gcallaghan commented 7 years ago

What I did to compose trees of state is write a base component that binds the state to a render function. I have the module here, npm here.

This allows me to have child components as part of the state tree, and call render on those child components directly without worrying about what their immediate render function is.

crabmusket commented 7 years ago

See also this section of the FAQ. Here Raynos seems to use struct instead of state for subcomponent states... but calling state should return a struct anyway so I'm not sure what the difference is :/

EDIT: oh hey. Back in my original comment I made a mistake. @Tarvald, maybe try this:

export default (opts) => {
    return hg.state({
        parts_list: hg.array(opts.parts.map(part_row_state)),
    })
}

E.g. you need to convert the result of map to an observable hg.array before using it in the state. Maybe that will have some effect.

ashnur commented 7 years ago

@crabmusket this kind of trial and error that I got bored with eventually and decided to read up on state management and ended up here: https://www.infoq.com/presentations/Value-Values :)