kbrsh / moon

🌙 The minimal & fast library for functional user interfaces
https://moonjs.org
MIT License
6k stars 200 forks source link

How to reset recursive time driver #272

Open mrEvgenX opened 4 years ago

mrEvgenX commented 4 years ago

I need to increase counter by timer, then go to another view goSeeingCounted to see result for further processing. Seems recursed timer is unstoppable. Any help?

Example of code to be fixed (playground%3B%0A%0AMoon.run(tick)%3B%0A)):

const { div, p, button } = Moon.view.m;

const goSeeingCounted = ({ data }) => {
  return {
      data,
      time: undefined,
      view: <viewSeeCounted counter=data.counter/>
    };
};

const viewCounter = ({ counter }) => (
    <div>
        <p>{counter}</p>
        <button @click=goSeeingCounted>Create</button>
    </div>
);

const viewSeeCounted = ({ counter }) => (
    <div>
        <p>Result: {counter}</p>
    </div>
);

const tick = ({data}) => {
  let newData = {
    counter: 0
  }
  if (data !== undefined) {
    newData.counter = data.counter + 1;
  }  
  return {
    data: newData,
    time: { 1000: tick },
    view: <viewCounter counter=newData.counter/>
  };
}

Moon.use({
    data: Moon.data.driver,
    time: Moon.time.driver,
    view: Moon.view.driver("#root")
});

Moon.run(tick);
kbrsh commented 4 years ago

This is a good exercise. Thanks for posting it, I think some revisions I have of Moon's API can help the ergonomics of things like this. The main problem is that tick outputs to a driver which sets a timeout, so setting time: undefined won't cancel any previous timeouts already set. The logic will have to go into tick or another function. Here's a quick way I was able to get it working:

const { div, p, button }
    = Moon.view.m;

const goSeeingCounted = ({ data }) => ({
    data: {...data, isTicking: false},
    view: <viewSeeCounted counter=data.counter/>
});

const viewCounter = ({ counter }) => (
    <div>
        <p>{counter}</p>
        <button @click=goSeeingCounted>Create</button>
    </div>
);

const viewSeeCounted = ({ counter }) => (
    <div>
        <p>Result: {counter}</p>
    </div>
);

const tick = ({data}) => {
    const newData = {...data, counter: data.counter + 1};
    return {
        data: newData,
        time: {1000: main},
        view: <viewCounter counter=newData.counter/>
    }
};

const main = (input) => {
    const data = input.data === undefined ?
        {counter: 0, isTicking: true} :
        input.data;
    const newInput = {...input, data};
    return data.isTicking ? tick(newInput) : goSeeingCounted(newInput);
};

Moon.use({
    data: Moon.data.driver,
    time: Moon.time.driver,
    view: Moon.view.driver("#root")
});

Moon.run(main);

Playground%3B%0A%0AMoon.run(main)%3B%0A)

mrEvgenX commented 4 years ago

Thanks for your solution. It look quite well and works nice. But another problem begins if I increase delay up to, say, 3 seconds and add the next step. Please, take a look at an example in playground%3B%0A%0AMoon.run(main)%3B%0A). Try to click "Create" and "Get thanks" whithin 3 seconds.

I think I'm able to fix it now. But I'm sure, a solution for this is gonna look not so nice and pretty. What do you think?

And I'm sure it should be mentioned somewhere on moonjs.org, may be in guide time or examples. May be I can help with it, are moonjs.org's sources available for PRs?

kbrsh commented 4 years ago

@mrEvgenX Ah I see, you're right, this is lacking too much elegance for my liking as well. First off, I'd like to thank you for both creating and extending this issue — it has pushed me to rethink of how Moon's functional approach can be used more practically. This is a perfect candidate for a new app structure and API I've been conceptualizing for the past few weeks. I tweeted a code sample that used this approach a few days ago (except with a React driver), so I'm excited to see how it can be applied here!

Basically, you structure an application with two transformations: input to state and state to output. Check this solution out:

const { div, p, button }
    = Moon.view.m;

const go = page => input => main({
    ...input.data,
    page
});

const increment = input => 
    input.data.page === "index" ?
        main({
            ...input.data,
            counter: input.data.counter + 1
        }) :
        main(input.data);

const pageIndex = state => ({
    data: state,
    time: {3000: increment},
    view: (
        <div>
            <p>{state.counter}</p>
            <button @click=(go("counted"))>Create</button>
        </div>
    )
});

const pageCounted = state => ({
    data: state,
    view: (
        <div>
            <p>Result: {state.counter}</p>
            <button @click=(go("thanks"))>Get Thanks</button>
        </div>
    )
});

const pageThanks = state => ({
    data: state,
    view: (
        <div>
            <p>Thank you</p>
        </div>
    )
});

const main = state => {
    switch (state.page) {
        case "index": return pageIndex(state);
        case "counted": return pageCounted(state);
        case "thanks": return pageThanks(state);
    }
};

Moon.use({
    data: Moon.data.driver,
    time: Moon.time.driver,
    view: Moon.view.driver("#root")
});

Moon.run(input => main({
    page: "index",
    counter: 0
}));

Playground%3ECreate%3C%2Fbutton%3E%0A%09%09%3C%2Fdiv%3E%0A%09)%0A%7D)%3B%0A%0Aconst%20pageCounted%20%3D%20state%20%3D%3E%20(%7B%0A%09data%3A%20state%2C%0A%09view%3A%20(%0A%09%09%3Cdiv%3E%0A%09%09%09%3Cp%3EResult%3A%20%7Bstate.counter%7D%3C%2Fp%3E%0A%09%09%09%3Cbutton%20%40click%3D(go(%22thanks%22))%3EGet%20Thanks%3C%2Fbutton%3E%0A%09%09%3C%2Fdiv%3E%0A%09)%0A%7D)%3B%0A%0Aconst%20pageThanks%20%3D%20state%20%3D%3E%20(%7B%0A%09data%3A%20state%2C%0A%09view%3A%20(%0A%09%09%3Cdiv%3E%0A%09%09%09%3Cp%3EThank%20you%3C%2Fp%3E%0A%09%09%3C%2Fdiv%3E%0A%09)%0A%7D)%3B%0A%0Aconst%20main%20%3D%20state%20%3D%3E%20%7B%0A%09switch%20(state.page)%20%7B%0A%09%09case%20%22index%22%3A%20return%20pageIndex(state)%3B%0A%09%09case%20%22counted%22%3A%20return%20pageCounted(state)%3B%0A%09%09case%20%22thanks%22%3A%20return%20pageThanks(state)%3B%0A%09%7D%0A%7D%3B%0A%0AMoon.use(%7B%0A%09data%3A%20Moon.data.driver%2C%0A%20%20%09time%3A%20Moon.time.driver%2C%0A%09view%3A%20Moon.view.driver(%22%23root%22)%0A%7D)%3B%0A%0AMoon.run(input%20%3D%3E%20main(%7B%0A%09page%3A%20%22index%22%2C%0A%09counter%3A%200%0A%7D))%3B%0A)

In this case, any event handlers transform from input to state and then hand it off to main, which transforms state back into output. With this, event handlers and the initial run are responsible for the dirty work of converting inputs to a state that reflects an application much more accurately. Then, the main function can transform it back to driver outputs.

As I work on a few changes to the API and codebase structure, I'll be adding this methodology to the docs. If you'd like, you can try adding documentation; the code is on the gh-pages branch.

Let me know what you think!