Skip to content
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

Components #238

Closed
jorgebucaran opened this issue Jun 21, 2017 · 171 comments
Closed

Components #238

jorgebucaran opened this issue Jun 21, 2017 · 171 comments
Milestone

Comments

@jorgebucaran
Copy link
Owner

jorgebucaran commented Jun 21, 2017

Let's discuss how components could look like in a future version of HyperApp.

Submitted Proposals


Example

counter.js

const Counter = (props, children) => ({
  state: {
    value: props.value
  },
  view: ({ value }, { inc, dec }) =>
    <div>
      <h1>{state.value}</h1>
      <button onclick={inc}>+</button>
      <button onclick={dec}>-</button>
    </div>,
  actions: {
    inc: state => state + 1,
    dec: state => state - 1
  }
})

index.js

app({
  state: {
    title: "Hello."
  },
  view: (state, actions) =>
    <main>
      <h1>{state.title}</h1>
      <Counter value={1} />
    </main>
})

Credits

All credit goes to @matejmazur:

Related

@MatejBransky
Copy link

MatejBransky commented Jun 21, 2017

Yesterday I've created partially (see issues below) working solution without components: []. You can just import component and immediately use it but I don't know how to track components without publicly exposed identification. I need some kind of hidden tracking.

Here is actual and working variant of writing apps with this version:

Counter example

with initial values
Below is without initial values

app.js

import { h } from 'hyperapp'
import Counter from './counter'
import Logger from './logger'

app({
  state: {
    who: 'world',
    // variant without initial values:
    countersId: [0, 1, 2]  // only ids of counters
  },

  actions: {
    addCounter: (state) => {
      const countersId = state.countersId
      counters.push(countersId[countersId.length - 1] + 1 })
      return { countersId }
    },
    removeCounter: (state, actions, index) => {
      const countersId = state.countersId
      countersId.splice(index, 1)
      return { countersId }
    }
  },

  view: (state, actions) => ( // arguments: (state, actions, props, children)
    <div>
      <div>Hello {state.who}!</div>
      <button onclick={actions.addCounter}>Add counter</button>

      {state.countersId.map((id, index) => (
        <div key={id}>
          <Counter id={id} />
          <button onclick={() => actions.removeCounter(index)}>Delete</button>
        </div>
      ))}

    </div>
  ),

  mixins: [Logger]
})

counter.js

import { h } from 'hyperapp'

export default {
  name: 'Counter', 

  state: { // you don't need functional state but you can use POJO or primitive
    sum: 0,
    number: 1
  },

  actions: {
    increase: (state) => ({
      sum: state.sum + state.number,
    })
  },

  view: (state, actions, props, children) => (
    <div>
      <div>Counter: {state.sum}</div>
      <button onclick={actions.increase}>
        Add {state.number}
      </button>
    </div>
  )
}

Gif example

example02

In the example above you can see app.state with all "local" states of "stateful" components under @Counter: {}. Every component instance is stored under its ID. It has single source of truth.

My goals

  • single source of truth (then it will be easy to implement HMR, time travelling, Undo/Redo history and everything will be simple stateless functions)
  • component based architecture (you can work with state and actions without knowing parent)
  • easy to use (component structure should be as close as possible to the body of app())
  • less boilerplate (avoid new user features)
  • no classes and this keyword
  • ideal code

How did I achieve this?

In a nutshell I've achieved this by creating and sharing outerEvents between app() and h(). When you call h(tag, data) it will verify if tag is an object and if it is, it calls `outerEvents.get("child", ({ component: tag, data }))` which will get response from app() where is outerEvents.set("child", ({ component, data }) => ...) and here is look for existing instance by ID in app.state[componentName][instanceId] or it creates new one. Actions are almost the same as in the first version (component's actions are connected to app.actions with first loading of component). ...It's a little bit less code than in my previous version but I've found out that I can't use components without ID (that problem is even with first version).

Known issues (TODOs)

  • unable remove instance (I need something like event listener for onremove but firstly I need to understand whole logic behind the virtual DOM)
  • unable generate instance without ID (maybe we can store ID in DOM attribute and use it for calling instance)
  • replacing third libraries (only Ramda)
  • optimizations (too much code in the core...but I think that you can help me with it if it would be interesting approach)

P.S.: Bear in mind that I'm newbie in programming (I started one year ago) so if you find this approach interesting I would be thankful for any help or guidance! 🙏 👍 (sorry for my poor English)

@Swizz
Copy link
Contributor

Swizz commented Jun 21, 2017

This one looks too magical. For someone like me who is new into the Hyperapp community.

I am pretty in love about the fractal idea for state/actions. The main concern is to handle list of same component with their own state.

@MatejBransky
Copy link

MatejBransky commented Jun 21, 2017

@Swizz Thanks for your answer. 👍 But I think that only magic is ID part (we need some identification for instances of component if we want keep single source of truth but maybe we can hide it somehow). If you look closer. You will see that component counter (POJO) is "stateful component". So as user you write components almost the same as app() (you work with component's state and actions just like in stateful components). Only difference is adding name: key (maybe we find some solution without naming) and props with children but they are fundamental for component based approach (you need somehow process inserted properties and children from parent). But these concerns can be easily suppressed in docs with proper guidance.

@jorgebucaran
Copy link
Owner Author

@matejmazur The magical part is what you are doing inside h to make this miracle happen. 💯

The reason it is magical is that custom tags return a virtual node tree, not an object. So, although I like this I also agree with @Swizz.

ids

I don't think we need to get rid of ids. The id is not really the core of how components work, but rather how index.js keeps track of them. Let's keep thinking. I've updated my example to fill in the missing code.

@jorgebucaran
Copy link
Owner Author

jorgebucaran commented Jun 21, 2017

@matejmazur How would you pass initial values / props to a component in your example?

@jamen
Copy link

jamen commented Jun 21, 2017

Awesome @matejmazur. Looks like a good start. A clean solution to me.

Like @jbucaran, I'm wondering how you get more control over the props and children. I do see you pass them through view, but I think you would need more "open access" over them for all fields, because it is your only way to get global state and global actions.

What if you did the exact same thing, but wrapped it in a function that was passed (props, children)?:

const Component = (props, children) => ({
  // ...
})

Instead of passing them through every single avenue like with state/actions. Do you think this would work?

@leeoniya
Copy link

What if you did the exact same thing, but wrapped it in a function that was passed (props, children)?:

FWIW, we recently had the same discussion in domvm [1], since it works this way. the only issue is that if either of those values ever get replaced by a parent, they become stale in the closure.

[1] domvm/domvm#147 (comment)

@MatejBransky
Copy link

MatejBransky commented Jun 21, 2017

@jbucaran How would you pass initial values / props to a component in your example?

state: {
  ...,
  counters: [
    { // props are just everything inserted to custom tag
      someValue: 0, 
      someText: 'foo',
      initial: 1 // I want to keep eye specifically on "initial" because everytime you update it, 
                   //  you'll get to the problem with previous state of component...
                   //  what to do in such situation? ..reset state?
                   // I think that it's important to keep such important value separately 
                   // from props inserted only to view part of component which don't 
                   // affect component state but hey..we can change that :-D
    }
  ]
}
...
{state.counters.map((data, index) => (
  <Counter {...data} /> // variant A
  <Counter 
      value={data.someValue} 
      text={data.someText} 
      initial={data.initial} /> // variant B
))}

@jamen

Like @jbucaran, I'm wondering how you get more control over the props and children. I do see you pass them through view, but I think you would need more "open access" over them for all fields, because it is your only way to get global state and global actions.

Send example of some situation where you need more "open access". Now the props.initial is key for more open access.

What if you did the exact same thing, but wrapped it in a function that was passed (props, children)?

I think that it's important to keep ordinary props separately from component state and actions because if you want somehow to interfere to component actions then why would you do that? You can change behavior of component actions by initial value (it can be str, num, obj, arr) inserted in component state then you can use it through component state in args of action. The same way as you do these things in app actions.

@zaceno
Copy link
Contributor

zaceno commented Jun 21, 2017

FWIW, with one really tiny change in app.js + a simple function that can live in userland, it becomes possible to write code like @matejmazur 's example above. See PR #241

... of course this approach breaks the "single source of truth" notion. In my setup each component is it's own app with it's own state. Communication between parents need to happen via props (pass notification actions down to children).

So it's not the perfect solution. (For example, time-travel debugging a bunch of apps that can come and go through the life of an app seems pretty tough, if not impossible) But I thought it relevant to mention in this thread anyway.

@naugtur
Copy link

naugtur commented Jun 25, 2017

Hi everyone. I saw this thread after I already had some expectations as to how components would work and I thought I'd show you what they were.

In a nutshell:

  • don't change how components work
  • keep one state and one source of actions
  • I went with context for passing anything instead of adding a dispatch function; seemed more powerful

See #245

@jorgebucaran
Copy link
Owner Author

jorgebucaran commented Jun 25, 2017

@naugtur Thanks for the feedback. This thread is to speculate and discuss how the component API could look like.

keep one state and one source of actions

Absolutely! 💯

@Swizz
Copy link
Contributor

Swizz commented Jun 26, 2017

My contribution to the discussion, with a little bit of my Cycle.js thoughts (Onion, Isolate, etc...)
Like I said in favor of Fragmented State and Actions for a Single Source of Truth.

Here is a working example on Hyperapp 0.9.3 🎉

Counter.js

const Counter = (props, children) => () => ({
  state: {
    Counter: {
      [props.key] : {
        count: 0
      }
    }
  },

  actions: {
    Counter: {
      up: state => ({ Counter: { [props.key] : { count: state.Counter[props.key].count + 1 } } }),
      down: state => ({ Counter: { [props.key] : { count: state.Counter[props.key].count - 1 } } })
    }
  },
	
  view: (state, actions) => 
    h('div', { class: 'counter' }, [
      h('button', { onclick: actions.Counter.up }, '+'),
      h('span', null, state.Counter[props.key].count),
      h('button', { onclick: actions.Counter.down }, '-')
    ])
})

index.js

app({
  view: (state, actions) =>
    h('div', null, [
      Counter({ key: 1337 })().view(state, actions)
    ]),
		
   mixins: [Counter({ key: 1337 })]
})

As you see, Mixins allow already a lot of things regarding fractal state and actions.
So, I vote in favor of a Component api point :

app({
 //...
 depends: [Counter({ key: 1337 })]
 //...
})

That will be just a super powered mixins that will do the following stuffs :

  • Wrap component state into :
state : Counter: { [props.key] : { STATE } }
  • Give to a component action a fragmented state according to Component and Key and wrap the result into the single global state
{ state: Counter[props.key] } => ({ Counter: { [props.key] : { count: state.count - 1 } } })
  • Give to the view caller an array of components view by Component and props.key
view: (state, actions, { Counter }) =>
  h('div', null, [
    Counter({ key: 1337 })
  ]),

jorgebucaran pushed a commit that referenced this issue Jun 27, 2017
As #238 is on the way, mixins may be removed or its 
behavior may change.
@Swizz
Copy link
Contributor

Swizz commented Jun 27, 2017

A little bit more neat with JSX. 👍
I think I will battletest this on the app I am working on.
(With some helpers for actions to avoid this long-long merge chain)

Counter.js

const Counter = (props, children) => () => ({
  state: {
    Counter: {
      [props.key] : {
        count: 0
      }
    }
  },

  actions: {
    Counter: {
      up: ({Counter: state}) => ({ Counter: merge(state, { [props.key] : merge(state[props.key], { count: state[props.key].count + 1 }) }) }),
      down: ({Counter: state}) => ({ Counter: merge(state, { [props.key] : merge(state[props.key], { count: state[props.key].count - 1 }) }) })
    }
  },
	
  view: ({[props.key] : state}, actions) => (
    <div class={"counter"}>
      <button onclick={actions.up} />
      <span>{state.count}</span>
      <button onclick={actions.down} />
    </div>
  )
})

index.js

app({
  view: (state, actions) =>
    h('div', null, [
      (<Counter key={1337}/>).view(state.Counter, actions.Counter),
      (<Counter key={8888}/>).view(state.Counter, actions.Counter)
    ]),
		
   mixins: [<Counter key={1337}/>, <Counter key={8888}/>]
})

@Dohxis
Copy link

Dohxis commented Jun 27, 2017

I would like to jump into this conversation by saying that I really like where this little framework is going. I have been using Elm for some time now and the best thing for me is that Elm is elegant and simple to use. Its syntactically beautiful. As Hyperapp is trying to be Elm-like I guess seeing those objects nesting one into another just tooks all the beauty. Why we cannot use new Javascript features to have beautiful looking framework. In my opinion, getting things out of those objects and at least letting components have a nice syntax would be a great decision. I have been working on my app and wrote some helper functions and classes to help me work with it. Here is the simple component which I thing looks simple and easy understand what its doing by just looking at it.

class Counter extends Component {

    constructor(props){
        super(props);
    }

    render(){
        return (
            <div>
                <button onclick={this.actions.add}>+</button>
                {this.state.num}
                <button onclick={this.actions.sub}>-</button>
            </div>
        );
    }
    
};

Yes it looks like `React but its the syntax people have been using for a long time right now, and they like it. Projects always keeps growing and having a nice way to write components will make our lives just easier.

This is only my opinion. Keep up with a great work 👍

@naugtur
Copy link

naugtur commented Jun 27, 2017

Classes bring side effects and all the mess were avoiding with functional programming. It's all about references to functions and the main structure of the app could be declared once while each component is a pure function. That's what came with elm, classes would hurt that.

@jorgebucaran
Copy link
Owner Author

jorgebucaran commented Jun 28, 2017

@Dohxis We want a single state architecture. So, using this goes against this very principle. :)

@Dohxis
Copy link

Dohxis commented Jun 28, 2017

I can understand that classes would bring unwanted things, did not experienced on my small app though. But I am keeping single state and single actions. They are passed as props.

@Swizz
Copy link
Contributor

Swizz commented Jun 28, 2017

I was thinking about how to implement interactive doc, when I was punch by an awesome thing :
Hyperapp is already ready for HTML component by design

<body>
  <app-counter></app-counter>
  <app-counter></app-counter>
  <app-counter></app-counter>
</body>
const { h, app } = hyperapp

const Counter = (props, children, element=document.body) => ({
  root: element,
  state: {
    value: props.value
  },
  view: ({ value }, { inc, dec }) =>
    h('div', { class: 'counter' }, [
      h('button', { onclick: inc }, '+'),
      h('span', null, value),
      h('button', { onclick: dec }, '-')
    ]),
  actions: {
    inc: state => ({ value: state.value + 1 }),
    dec: state => ({ value: state.value - 1 })
  }
})

document.querySelectorAll('app-counter').forEach((elem) => {
  app(Counter({value: 0}, [], elem));
})

This is not really great to see ; but it works well :

app({
  state: {
    title: "Hello."
  },
  view: (state, actions) =>
    h('main', null, [
      h('h1', null, state.title),
      h('app-counter', null)
     ]),
  events: {
    render(state, actions, view, emit) {
      document.querySelectorAll('app-counter').forEach((elem) => {
        app(Counter({value: 0}, [], elem));
      })
    }
  }
})

PS : Im doing a lot of experiments with hyperapp to use it into a large scale project, if you find my every day comments here a lot annoying, do not hesitate to ask me to stfu.

@jorgebucaran
Copy link
Owner Author

@Swizz Please keep them coming! It adds a lot to the discussion. 🙏

@nitin42
Copy link

nitin42 commented Jun 28, 2017

We could also think about adding Dynamic Components, using the same mount point and switching between different components by binding them to an attribute like Vue.js does.

Example

app({
  state: {
    title: "Hello."
  },
  data: {
    currentView: 'home'
  },
  components: {
    home: { doSomethingHere },
    About: { doSomethingHere },
    Contact: { doSomethingHere } 
  }
})

So depending upon the UI logic we could switch between different component using a wrapper or a method.

This would be useful in doing transitions (transitioning between the components or elements) and other stuff.

@jorgebucaran
Copy link
Owner Author

Why do you want to pass props into the view ?

I think is is more intuitive to do it in the view. Let me see harder 🔎 🤓.

The component hook is the only one responsible do deal with component logic.

Sorry, what is the component hook?

@Swizz
Copy link
Contributor

Swizz commented Jun 30, 2017

I dont know how to call these things ^^

app({
  components: ...
})

I think is is more intuitive to do it in the view

You, are the boss. But I am still against 🥊 😄

@jorgebucaran
Copy link
Owner Author

jorgebucaran commented Jun 30, 2017

They are called properties.

The following code looks like when you instantiate a component in React.

  components: state => ({
    Counter: (
      <Counter initialValue={5}, unit={state.unit}, onchange={value => emit('change', value)}/>
    )
  }),

It's also not clear to me when is this function called and how many times is it called. I guess only once so it's intializing the view?

I think this can and will put off people, specially beginners or those coming from other libraries. I, myself, am very confused.

@Swizz
Copy link
Contributor

Swizz commented Jun 30, 2017

I am not clear about that, in fact. Initialisation maybe. But what about conditional component or rendering a list of components according to an HTTP request result ?

@jorgebucaran
Copy link
Owner Author

@Swizz What about them? I don't see an issue with that because the wrapped component view is passed to the application view in the function like:

  view: (state, actions, { Counter, ... }) => ...

@jorgebucaran
Copy link
Owner Author

@Swizz What about this?

const Counter = (props = { initialValue: 0, unit: "" }, children) => ({
  state: {
    count: props.initialValue,
    unit: props.unit
  },

  actions: {
    inc: (state, actions, data, emit) =>
      emit("change", { count: state.count + 1 }),
    dec: (state, actions, data, emit) =>
      emit("change", { count: state.count + 1 })
  },

  view: (state, actions) =>
    <div class={"counter"}>
      <button onclick={actions.inc}> + </button>
      <span>{state.count} {state.unit}</span>
      <button onclick={actions.dec}> - </button>
    </div>
})

app({
  state: {
    title: "Best counter ever",
    unit: "monkeys"
  },

  view: (state, actions, { Counter }, emit) =>
    <main>
      <h1>{state.title}</h1>
      <Counter
        initialValue={5}
        unit={state.unit}
        onchange={value => emit("change", value)}
      />
    </main>,

  components: { Counter },

  events: {
    change: (state, actions, value) => {
      alert(value)
    }
  }
})

@Swizz
Copy link
Contributor

Swizz commented Jun 30, 2017

How can I use 3 counters ? An a unknown number of counters based on an initialValues array ?

@jorgebucaran
Copy link
Owner Author

@Swizz Is that not a problem also in your example?

@Swizz
Copy link
Contributor

Swizz commented Jun 30, 2017

I am not highlighting problem, I am asking about your thought on it ^^

 components: state => ({
    Counters: [
      <Counter initialValue={1} unit={state.unit} onchange={value => emit('change', {i: 1, value})}/>,
      <Counter initialValue={2} unit={state.unit} onchange={value => emit('change', {i: 2, value})}/>
      <Counter initialValue={3} unit={state.unit} onchange={value => emit('change', {i: 3, value})}/>
    ]
  }),
 components: state => ({
    Counters: state.initialValues.map((value, i) =>
      <Counter initialValue={value} unit={state.unit} onchange={value => emit('change', {i, value})}/>
    )
  }),

With your

view: (state, actions, { Counter }, emit) =>
    <main>
      <h1>{state.title}</h1>
      <Counter initialValue={1} unit={state.unit} onchange={value => emit('change', {i: 1, value})}/>
      <Counter initialValue={2} unit={state.unit} onchange={value => emit('change', {i: 1, value})}/>
      <Counter initialValue={3} unit={state.unit} onchange={value => emit('change', {i: 1, value})}/>
    </main>,
view: (state, actions, { Counter }, emit) =>
    <main>
      <h1>{state.title}</h1>
      {state.initialValues.map((value, i) =>
        <Counter initialValue={value} unit={state.unit} onchange={value => emit('change', {i, value})}/>
      )}
    </main>,

Both works the same, the main purpose is all about, the power we want to give to the view property.

My mind in stucks about separation and isolation ; your is on simplicity and intutivity.
Choose one. I am not responsible of that 😛

@jorgebucaran
Copy link
Owner Author

@Swizz What is unit by the way? And won't we have to provide an index to the component?

@Swizz
Copy link
Contributor

Swizz commented Jun 30, 2017

Unit is just another attribute to illustrate parent->child communication and the index is only for the events example.

The id key could be set randomly or by given a key to the component like it does for vnode.

@jorgebucaran
Copy link
Owner Author

jorgebucaran commented Jun 30, 2017

@Swizz What do you think of a random id?

EDIT: Not an actual #id or a keyed-dom key, but a unique identifier among siblings.

@Swizz
Copy link
Contributor

Swizz commented Jun 30, 2017

Not pure but easy to use and to think about

@jorgebucaran
Copy link
Owner Author

What would be a pure way to do it then?

@Swizz
Copy link
Contributor

Swizz commented Jun 30, 2017

<Component attr={value}/> is impure (the component key iscalculated)
<Component attr={value} key={1234}/> is pure (the component key is a function input)

@jorgebucaran
Copy link
Owner Author

jorgebucaran commented Jun 30, 2017

@Swizz Just to be clear, we're not talking about keys as in keyed-vdom keys. Right?

@Swizz
Copy link
Contributor

Swizz commented Jun 30, 2017

The purpose could be the same "identify a thing". But the implementation is différente. The only purpose here is about isolation.

@naugtur
Copy link

naugtur commented Jun 30, 2017

God this is long. I'm in a different timezone apparently. I'm half way through and I need to comment on the image from @Swizz

I really don't think the state should be built based on the parent-child relationships of components.

State is the most important part of the architecture and I (as an app developer) will never give up my decisions on the data strucutres used in state.

State should be structured the way developer wants and subtrees of state passed to components consciously. Anything else is a guaranteed disaster.

There are 3 sane ways to choose from, to pass a fragment of state into a component in order to scope it:

  1. component('key.in.state',...)
  2. <Component scope={state.key.in.state}>
  3. 'connect' pattern from redux-react

Second option is the simplest, but it's based on a convention and might be easy to break.

Option 3 is implemented in my fork. https://github.com/naugtur/hyperapp/blob/master/poc/index.js

@zaceno
Copy link
Contributor

zaceno commented Jun 30, 2017

Good point about state structure being sacred to the developer, @naugtur

I'm not familiar with the connect-pattern from redux react, so let me check if I follow your poc correctly:

The "context" available to "connected" components, is defined in the <Provider.. tag. And the provider doesn't output anything. It's just a function to define the context. And then any "connected" component will automatically have access to the context (without it needing to be passed as a prop), and other props work the same.

Is that correct?

That doesn't seem all that different from my proposal. The one difference I can see is that you have the provider defining what the context is, whereas I just bind the apps full state and actions to components. Do you agree or disagree?

Also like my proposal (well almost), it seems yours does not require changes in the app or h functions, and could be used via a separate package. True?

@jorgebucaran
Copy link
Owner Author

@naugtur In your example, the app has the actions that can up and down the counter. If that's going to be always the case, then what is the advantage of your controls/components from using custom tags?

@naugtur
Copy link

naugtur commented Jun 30, 2017

@jbucaran actually, I wouldn't want them to be much more than custom tags. Components (in my view) are custom tags that you don't have to pass all properties to, at least not explicitly.

@jorgebucaran
Copy link
Owner Author

@naugtur So they are custom tags that are bound to the state and actions?

@jorgebucaran
Copy link
Owner Author

Please have a look at @zaceno's proposal and tell me if you two are on the same page?

@naugtur
Copy link

naugtur commented Jul 1, 2017

Yes.

I would add limiting scope of state passed in, but overall it's what I'd like to use.

@jorgebucaran
Copy link
Owner Author

jorgebucaran commented Jul 1, 2017

I am having mixed feelings about components and I am not even sure we should add a new core concept to the framework. I am not even sure we even have a problem. I say, let's build one or two cool apps first and then revisit this issue. 🤔😅😎

Now, I'm still interested to see @naugtur's #245's final form and the door is open to anyone who wants to add more to this discussion.

I am glad this conversation happened, though, because it motivated me to revisit hyperapp core values and I can confidently say I like it now more than ever.

To summarize my feelings about components:

@zaceno's But the patterns I've seen so far have state and actions that are defined very independently. Even though we're merging their state and actions to the main tree, they don't seem to have any access to the main state tree. So how are the components meant to communicate? If the answer is: "via the props", then they are effectually no different to locally stateful components. And if that's the case, then what's the point of the "one-state-to-rule-them-all"?

But nothing is lost. On the contrary, we've gained the insight that there exist patterns that can help you create complex applications without losing the benefits of a single state tree.

Patterns

Widgets

@zaceno's state/action bound custom tags is interesting and could be published as an independent module. I like to think of them as "widgets". 🤓

See: https://codepen.io/zaceno/pen/gRvWPx

Split-mixins

@matejmazur's Split-mixins is a clever trick that requires no changes to core and lets you use mixins to achieve something similar to stateful components.

const MyComponent = ({ state, view, actions }) => view
   ? ({ state, actions, events, mixins }) // good ol' mixin
    : <main>...</main> // a custom tag

Mixins

@Swizz Super-powered mixins.

See: #238 (comment).

Other

Final remarks

We're all (I am!) on a learning curve and tyring to figure this out. I am sure more patterns will show up, so please share your research and findings.

Thank you all, for your feedback and such an epic thread! 💥 🎉🥇

@MatejBransky
Copy link

MatejBransky commented Jul 2, 2017

Here is package for mixins with view (it's just wrapper for my trick which @jbucaran mentioned in this comment. With this you can write mixins like this:

const Counter = mixin({
  view: (state, actions, children) => (
    <div>
      <div>Number of counters: {state.length}</div>
      <button onclick={actions.addCounter}>Add counter</button>

      {state.map((instance, index) => (
        <div key={instance.id}>
          <button onclick={() => actions.increase(index)}>
            Increase
          </button>
          <span>{instance.sum}</span>
          <button onclick={() => actions.decrease(index)}>
            Decrease
          </button>
          <button onclick={() => actions.removeCounter(index)}>Remove</button>
        </div>
      ))}
    </div>
  ),

  state: {
    counter: R.times(() => initial(), 2) // default number of loaded counters
  },

  actions: {
    counter: {
      addCounter: (state) => ({
        counter: R.append(initial(), state.counter)
      }),
      removeCounter: (state, actions, index) => ({
        counter: R.remove(index, 1, state.counter)
      }),
      increase: (state, actions, index) => R.over(
        R.lensPath(['counter', index, 'sum']),
        R.inc,
        state
      ),
      decrease: (state, actions, index) => R.over(
        R.lensPath(['counter', index, 'sum']),
        R.dec,
        state
      )
    }
  }
})

and then it can be used in app like this:

app({
  view: (state, actions) => (
    <Counter state={state.counter} actions={actions.counter} />
  ),
  ...,
  mixins: [Counter]
})

That's it!

jorgebucaran added a commit that referenced this issue Jan 7, 2018
As #238 is on the way, mixins may be removed or its 
behavior may change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants