yysun / apprun

AppRun is a JavaScript library for developing high-performance and reliable web applications using the elm inspired architecture, events and components.
https://apprun.js.org
MIT License
1.18k stars 57 forks source link

Example of child component updating parent's state? #111

Closed Geordi7 closed 3 years ago

Geordi7 commented 3 years ago

I have a component that has its own state/view/update. The component validates user input according to rules it knows. Once a valid input is received it should notify its parent of the state change.

Here is a simplified example:

class C extends Component {
  state = {v: '', i: '', rules: []};
  view = s => <input $bind="i" />
  mounted = (p,c,s) => {
    s.v = p.choice
    s.i = p.choice
    s.rules = p.rules
  }
  update = {
    ???
  }
}

class App extends Component {
  state = {choice: '', rules: [...]}
  view = s => <div><C choice={s.choice} rules={s.rules} /></div>;
  update = {
    new_choice: (s,n) => {choice: n}
  }
}

How can C notify App of the new choice?

yysun commented 3 years ago

It looks like there are questions here. 1) How to validate input? You can use $oninput (or $onchange) instead of $bind 2) How to notify the parent? You can fire a global event

class C extends Component {
  state = {v: '', rules: []};
  view = s => <input $oninput="input" />
  mounted = (p,c,s) => {
    s.v = p.choice
    s.i = p.choice
    s.rules = p.rules
  }
  update = {
    input: (s, e) => {
      if(e.target.value === '1') app.run('@new_choice', 1);
    }
  }
}

class App extends Component {
  state = {choice: '', rules: []}
  view = s => <div>
    <div>{s.choice}</div>
    <C choice={s.choice} rules={s.rules} />
  </div>;
  update = {
    '@new_choice': (s,n) => ({choice: n})
  }
}
app.render(document.body, <App/>);

Or, you can pass the parent into the child if you want to keep the event local.

class C extends Component {
  state = {v: '', rules: []};
  view = s => <input $oninput="input" />
  mounted = (p,c,s) => p
  update = {
    input: (s, e) => {
      if(e.target.value === '1') s.parent.run('new_choice', 1);
    }
  }
}

class App extends Component {
  state = {choice: '', rules: []}
  view = s => <div>
    <div>{s.choice}</div>
    <C choice={s.choice} rules={s.rules} parent={this}/>
  </div>;
  update = {
    new_choice: (s,n) => ({choice: n})
  }
}
app.render(document.body, <App/>);

Also BTW, you can return a new state from the mounted function.

The above code has been tested in the Playground. https://apprun.js.org/#play

Geordi7 commented 3 years ago

Thank you for this information, it is useful.

The problem I have with stateful components is when they send a message to the parent, all siblings are reset:

class Counter extends Component {
  state = {v: 0, i: 0};
    mounted = p => p;
    view = state => <>
        <h1>{state.i}: {state.v}</h1>
        <button $onclick={s => (s.v+=1, s)}>-1</button>
        <button $onclick={s => (s.v+=1, s)}>+1</button>
        <button $onclick='ok'>ok</button>
      </>;
    update = {
      ok: s => (app.run('@set', s.i, s.v),s),
    }
}

class App extends Component {
  state = [0,0,0];
    view = s => <div>
        {s.join(',')}
        <hr />
      {s.map((n,i) => <Counter v={n} i={i} />)}
      </div>
    update = {
      '@set': (s,i,v) => (s[i] = v, s),
    }
}

new App().start(document.body)

Try this in the playground: click all the + buttons, and then click one of the ok buttons to update the parent.