Replies: 117 comments
-
Here's how this could look like for Hyperapp. First, define your button module: import { h } from "hyperapp"
const Increment = state => state + 1
const Decrement = state => state - 1
export const init = 0
export const view = state =>
h("div", {}, [
h("button", { onclick: Decrement }, "-"),
h("div", {}, state),
h("button", { onclick: Increment }, "+")
]) In your main app, import the button and a new core function, e.g., import { h, app, map } from "hyperapp"
import * as Button from "./Button"
app({
init: {
button: Button.init()
},
view: state =>
h("div", {}, [
h("h1", {}, "Here's a button"),
map(state => ({ button: state }), Button.view(state.button))
]),
node: document.getElementById("app")
})
|
Beta Was this translation helpful? Give feedback.
-
Another example, using the same module twice. I had to ask in the Elm-forum because it wasn't clear to me at the time. In hindsight, I realized I had misunderstood parts of how consuming a module works in elm and this cleared things up for me at least: https://discourse.elm-lang.org/t/using-multiple-instances-of-modules/4578 |
Beta Was this translation helpful? Give feedback.
-
@zaceno How would you translate that into Hyperapp using what's currently proposed in #896 (comment)? |
Beta Was this translation helpful? Give feedback.
-
@jorgebucaran It doesn't really change anything about your proposal. If I read it correctly (and understand what Elm's map does), it is basically:
Any Right? |
Beta Was this translation helpful? Give feedback.
-
This approach requires no implementation in core and is so straightforward that I've certainly tried it before. But I can't recall what the problem was. const namedScope = name =>
action =>
(state, payload) =>
({...state, [name]: action(state[name], payload)})
const Counter = scope => {
const init = 0
const incr = scope(state => state + 1)
const decr = scope(state => state - 1)
const view = state => (
<p>
<button onclick={decr}>-</button>
{state}
<button onclick={incr}>+</button>
</p>
)
return {init, view}
}
const Main = (() => {
const A = Counter(namedScope('A'))
const B = Counter(namedScope('B'))
app({
init: {A: A.init, B: B.init},
view: state => (
<body>
<h1>Here's two counters</h1>
{A.view(state.A)}
{B.view(state.B)}
</body>
),
node: document.body,
})
})() EDIT IV: removed edits II - III , since I don't think they added much to the discussion and make you scroll a lot 😛 |
Beta Was this translation helpful? Give feedback.
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
-
@zaceno @sergey-shpak This feature must be in core. |
Beta Was this translation helpful? Give feedback.
-
I'm not disagreeing, but could you clarify: Do you mean it is something you don't want to leave up to userland, or are you aware of a technical reason why it must be in core? Here's a possible implementation of const map = (f, vnode) => ({
...vnode,
props: Object.entries(vnode.props)
.map(([key, val]) => [key, key[0] === 'o' && key[1] === 'n' ? f(val) : val])
.reduce((o, [key, val]) => ({...o, [key]: val}), {}),
children: vnode.children.map(child => map(f, child))
}) It will impact performance the more it is used, but I don't know of a way to avoid that by implementing it in core. |
Beta Was this translation helpful? Give feedback.
-
@sergey-shpak Is your solution really much different than #896 (comment) ? Also: what are the pros/cons of that approach? |
Beta Was this translation helpful? Give feedback.
-
@zaceno we are working on the same task some approaches can look similar,
cons:
|
Beta Was this translation helpful? Give feedback.
-
Don't want to leave up to userland. No technical reason comes to mind, either. Well, maybe that it'd be hard to implement in userland? |
Beta Was this translation helpful? Give feedback.
-
Doesn't look like it. At least, the code I pasted above seems to do the trick. See this codepen where I use it: |
Beta Was this translation helpful? Give feedback.
-
@zaceno Can we match this API #896 (comment)? And does this solution look efficient to you? |
Beta Was this translation helpful? Give feedback.
-
Ah ok so given an id, the counter will use the id to insert its own state into the global state? Ok I think I understand what you're suggesting now, but I don't personally see the value of tying the |
Beta Was this translation helpful? Give feedback.
-
I think you might be misunderstanding what map does. All it does is it walks through a vnode, transforming all the actions it finds, according to a given defininition. So it operates on vnodes and not components. As such it doesn't make it easier or harder to pass properties to components. Consider this: counter.js
main:
|
Beta Was this translation helpful? Give feedback.
-
@zaceno Ahhhhh I see, thank you for the example. I didn't understand that the mapping function does nothing to props. I thought that passing down a more precise slice of state to a "view" was part of the problem it was solving, and I jumped gun on the attribute thing haha. I understand that it's only for the actions now. I think I just miss the was we could access the whole state in components from v1 haha |
Beta Was this translation helpful? Give feedback.
-
@loteoo In your approach, do you have a thought about how components can react to eachother? Like, how would you implement this example: https://codepen.io/zaceno/pen/ExxdzJZ?editors=0011 |
Beta Was this translation helpful? Give feedback.
-
@mshgh I'd love to see the source for your latest "weird counters" example. Got a link? I'm also quite interested in seeing how you go about loading a module dynamically. (I doubt it makes any difference wether using static- or instance-style modules, and whatever you work out should work for both). You are correct that even in the instance-style approach to modules. map can help with effects. In fact I'm about to make you even happier :) I plan on releasing a new version of hyperapp map with a more elaborate but also more explicit and less confusing api. It will export four functions:
For those of us still using static-style modules, we can map views like: And here's something you'll like too @mshgh: I eventually came to the same realization you did early on, that we need some way of "protecting" parts of the vdom from being mapped by mappers higher up. That's what {map.view(popupMap, (
<popup.fancy state={state.popup} title="Edit profile">{map.pass(
<h1>Edit Profile</h1>
{map.view(profileMap, <profile.editor state={state.profile} />)}
)}</popup.fancy>
)} What |
Beta Was this translation helpful? Give feedback.
-
@zaceno It would be the same way as it does with map, here's what the example from your codepen would look like using this approach: // 1. Isolated counter
const Decrement = (slice) => slice - 1
const Increment = (slice) => slice + 1
const Counter = (props, children, slice) => (
<div>
<h1>{slice}</h1>
<button onclick={Decrement}>-</button>
<button onclick={Increment}>+</button>
</div>
)
// 2. Wire the components with CheckEqual action
const CheckEqual = state => ({
...state,
timesEqual: state.timesEqual + (state.A === state.B ? 1 : 0)
})
const CounterA = Counter
CounterA.getter = (state) => state.A
CounterA.setter = (state, A) => CheckEqual({ ...state, A })
const CounterB = Counter
CounterB.getter = (state) => state.B
CounterB.setter = (state, B) => CheckEqual({ ...state, B })
// 3. Use the components in you app
app({
init: {
timesEqual: 0,
A: 0,
B: 0
},
view: state => [
<h1>Times equal: {state.timesEqual}</h1>
<CounterA />
<CounterB />
],
node: document.getElementById("app")
}) |
Beta Was this translation helpful? Give feedback.
-
@loteoo Ah I see. I misunderstood first. So your idea is that users from the outside will define how to map a component. I first thought the component defined its own getters and setters. This makes more sense. Still, I don’t think it works because it looks like you’re not creating new instances for each counter, so you’re just overwriting the setter & getter on the same function. |
Beta Was this translation helpful? Give feedback.
-
@zaceno Oops! Soory, made the link too hidden in the text. The word 'version' is a link https://rawcdn.githack.com/mshgh/ha2-samples/counter-map-v5/index.html If you prefer, you can check my repo - https://github.com/mshgh/ha2-samples/tree/counter-map-v5
Me too ;) I am still not 100% clear how to approach it. Objective is to have another Page where you can provide Counter Name, Initial Amount and specify if this is Positive Counter + every Counter to have Delete Me button which is not part of the Counter itself. Counter must not know anything about its usage. Need to start coding to get better idea. I expect the main problem in need of some sort of "dynamic" |
Beta Was this translation helpful? Give feedback.
-
Here is initial NOT WORKING version of dynamically added counters https://rawcdn.githack.com/mshgh/ha2-samples/2b990cd6ef159433fa9117d1be3fc2603359f341/index.html (GitHub repo https://github.com/mshgh/ha2-samples/tree/counter-map) working parts
missing parts
this demo is meant to give you idea about the way I expect the dynamically added counters should work |
Beta Was this translation helpful? Give feedback.
-
I have the dynamically added counters demo ready. See https://rawcdn.githack.com/mshgh/ha2-samples/f1f8c248226a729d9e1de96a17d3d613e032274a/index.html I realized there is one more enhancement necessary. To properly support save/load of state into local store (situation when initial state doesn't match initial composition) so application can continue after page re-load where it finished. I have an idea how to do it, just need to verify it. |
Beta Was this translation helpful? Give feedback.
-
Hi, All. So my proposition is to easily generate the "props" function that will manage props passed to a component (and it actions) with this semantics: Calling this function without arguments will return the expected by component data structure. Calling this function with an argument means that component want to change state and want to construct the full application state that can be returned from the action. Here is a working example of that. Pay attention to "mount" and "mnt" functions at the bottom ("mnt" is an easy way to use with "mount" function is used to construct explained above "props()". It also introduces data comparison that can eliminate redundant state changes when data values are not actually changed. This solution is very similar to what was proposed by @jorgebucaran but a little bit more simpler (no tricks around component's "view") In very short the API is like this: ...
let initialState = {
counter1: 0,
counter2: 0,
}
...
funcion view(state) {
return h(Counter, mnt(s => s.counter1, (s,v)=> s.counter1 = v, state)),
}
....
Below is a more complex example with dynamic counters and "CustomEdit" component that can edit any string (every counter has a "greeting" property in the example below). import { h, app } from 'hyperapp';
/////////////// Counter component ///////////////
//Model: { greeting: string, counter: number }
function cntIncrement(state, props) {
const p = props();
return props({ ...p, counter: p.counter + 1});
}
function cntDecrement(state, props) {
const p = props();
return props({ ...p, counter: p.counter - 1});
}
function cntReset(state, props) {
const p = props();
return props({ ...p, counter: 0});
}
function Counter({props}, children) {
const p = props(), {greeting, counter} = p;
return h('div', {}, [
...children,
h('h2', {}, greeting),
h('p', {}, counter),
h('button', {onclick: [cntIncrement, () => props]}, 'INC'),
h('button', {onclick: [cntDecrement, () => props]}, 'DEC'),
h('button', {onclick: [cntReset, () => props]}, 'Reset'),
])
}
/////////////// Counter Manager component ///////////////
//Model: { greeting: string, counter: number } []
function newCounter(s, props) {
const p = props();
let newValue = [...p];
newValue.push({greeting: 'New counter ' + p.length, counter: p.length});
return props(newValue);
}
function deleteCounter(props) {
const p = [...props()];
p.splice(0, 1);
return props(p);
}
function CounterManager({props}) {
const p = props();
return h('div', {}, [
h('button', {onclick: [newCounter, () => props]}, 'New counter'),
h('button', {onclick: () => deleteCounter(props)}, 'Remove counter'),
h('ul', {},
p.map( (c, i) => h('li', {},
h( Counter, mnt(s => s[i], (s, v) => s[i] = v, props) )
))
)]
);
}
/////////////// Custom edit box. Can edit any string value ///////////////
// model: string
function CustomEditBox({props}) {
return h('input', {
type: 'text',
oninput: [(s, v) => {
return props(v);
}, e => e.target.value],
value: props()
});
}
/////////////// Main View function ///////////////
function renameMainCounter(s, value) {
return {...s, mainCounter: {...s.mainCounter, greeting: value}};
}
function view(state) {
return h('div', {}, [
h('h1', {}, 'Hello Hyperapp'),
h('p', {}, `Hi ${state.name}!`),
h(CustomEditBox, mnt(s => s.name, (s, v) => s.name = v, state)),
h('h2', {}, 'Counter'),
h('input', {type: 'text', oninput: [renameMainCounter, e => e.target.value], value: state.mainCounter.greeting }),
(state.counters && !!state.counters.length) && h(CustomEditBox, mnt(s => s.counters[0].greeting, (s, v) => s.counters[0].greeting = v, state)),
(state.counters && !!state.counters.length) && [
h('h3', {}, 'Rename all managed counters'),
h(CustomEditBox, mnt(s => s.counters[0].greeting, (s, v) => s.counters = s.counters.map(c => ({...c, greeting: v})), state), 'Paragrph 2'),
],
h(Counter, mnt(s => s.mainCounter, (s, v) => s.mainCounter = v, state),
h('p', {}, 'This is the main counter!')
),
h('h3', {}, 'Counter Manager'),
h(CounterManager, mnt(s =>s.counters, (s, v) => s.counters = v, state)),
]);
}
let initialState = {
name: 'World',
mainCounter: {
greeting: 'Main Counter',
counter: 0,
},
counters: [
{
greeting: 'Managed cnt 1',
counter: 0,
},
{
greeting: 'Managed cnt 2',
counter: 0,
},
{
greeting: 'Managed cnt 3',
counter: 0,
},
]
}
let node = document.getElementById('app');
app(
{
init: initialState,
node: node,
view: view,
}
);
function areSame(a, b) {
if (a === b) {
return true;
}
if (a.__proto__ != b.__proto__) {
return false;
}
if (a.__proto__ != {}.__proto__ && a.__proto__ != [].__proto__ ) {
return false; //don't know how to compare non objects and non arrays
}
const pa = Object.keys(a);
const pb = Object.keys(b);
return pa.length === pb.length
&& pa.every(p => pb.indexOf(p) >= 0)
&& pa.every(p => a[p] === b[p]);
}
function mount(getter, setter, state) {
function calcNextState(s, v) {
let newState = Array.isArray(s) ? [...s] : {...s};
setter(newState, v);
return newState;
}
return function (newPropsValue) {
if (arguments.length == 0) {
return getter( typeof(state) === "function" ? state() : state );
} else {
if (typeof(state) === "function") {
let current = getter(state());
if (areSame(current, newPropsValue)) {
return state(state());
} else {
return state(calcNextState(state(), newPropsValue));
}
} else {
let current = getter(state);
if (areSame(current, newPropsValue)) {
return state;
} else {
return calcNextState(state, newPropsValue);
}
}
}
}
}
function mnt(getter, setter, state) {
return {props: mount(getter, setter, state)};
}
|
Beta Was this translation helpful? Give feedback.
-
Hi, All. There is only one single ...
const initialState = {
greetingCounter: {
greeting: 'Hwllo world!',
counter: 21
}
}
...
function view (state) {
return h(CounterWithTitle,
{
greeting: state.greetingCounter.greeting,
counter: state.greetingCounter.counter,
map: mnt(s => s.greetingCounter, (s, v) => s.greetingCounter = v)
}
),
}
...
// state is {greeting: string, counter: number}
const IncrementAction = state => ({ ...state, counter: state.counter + 1 })
const DecrementAction = state => ({ ...state, counter: state.counter - 1 })
const ResetAction = state => ({ ...state, counter: 0 })
const CounterWithTitle = ({ greeting, counter, map }, children) =>
h('div', {}, [
h('h2', {}, greeting),
h('p', {}, counter),
h('button', { onclick: map(IncrementAction) }, '+'),
h('button', { onclick: map(DecrementAction) }, '-'),
h('button', { onclick: map(ResetAction) }, '0'),
...children
]) Here is the CodePen with working example. Proposed "mnt" function can be described using TypeScript like this (type Mount). type Action<S> = (state: S, options?: any) => S
type Mapper<X,Y> = ( (s: X) => Y )
| ( (s: X, v: Y) => X )
| ( (action: Action<Y>) => Action<X> )
type Getter<S,U> = (s: S) => U
type Setter<S,U> = (s: S, v: U) => S
type Mount<S,U,G> = ( (get: Getter<S,U>, set: Setter<S,U>, parent: Mapper<G,S>) => Mapper<G,U> )
| ( (get: Getter<G,U>, set: Setter<G,U> ) => Mapper<G,U> )
// S - is type of current state
// U - is type of state that we want to extract from current and inject to children
// G - type of application global state
// if parent is not specified then it is assumed that S is G
function mnt<S,U,G> (get: Getter<S,U>, set: Setter<S,U>, parent: Mapper<G,S>): Mapper<G,U>;
function mnt<U,G> (get: Getter<G,U>, set: Setter<G,U>): Mapper<G,U>; |
Beta Was this translation helpful? Give feedback.
-
I like the sound of that. Could you elaborate? Why are you using |
Beta Was this translation helpful? Give feedback.
-
View doesn't know anything (and should not) about app state. The only thing
The proposed Below is 80 lines example of how components can be composed to each other: /// //////////// Counter component ///////////////
const IncrementAction = counter => counter + 1
const DecrementAction = counter => counter - 1
const ResetAction = counter => 0
const Counter = ({ counter, map }, children) =>
h('div', {}, [
h('h3', {}, counter),
h('button', { onclick: map(IncrementAction) }, '+'),
h('button', { onclick: map(DecrementAction) }, '-'),
h('button', { onclick: map(ResetAction) }, '0'),
...children
])
/// //////////// Custom edit box. Can edit any string value ///////////////
// model: string
const SetTextAction = (state, text) => text
const Edit = ({ value, map }) =>
h('input', {
type: 'text',
oninput: [
map(SetTextAction),
e => e.target.value
],
value: value
})
// Counter with editable title
const ToggleEditing = state => ({ ...state, editing: !state.editing })
const CounterWithTitle = ({ title, counter, editing, map }) =>
h('div', {}, [
!editing && h('p', {}, title),
editing && h(Edit, { value: title, map: mnt(s => s.title, (s, v) => s.title = v, map) }),
h(Counter, { counter: counter, map: mnt(s => s.counter, (s, v) => s.counter = v, map) }),
h('button', { onclick: map(ToggleEditing) }, 'Toggle title edit')
])
// Main view
const initialState = {
counter: 0,
counterWithTitle: {
title: 'Hello Hyperapp!',
counter: 21
}
}
const CounterWithTitleGetter = s => s.counterWithTitle
const mainView = state =>
h('div', {}, [
h(Counter,
{
counter: state.counter,
map: mnt(s => s.counter, (s, v) => s.counter = v)
}
),
h(CounterWithTitle,
{
...CounterWithTitleGetter(state),
map: mnt(CounterWithTitleGetter, (s, v) => s.counterWithTitle = v)
}
)
])
app(
{
init: initialState,
node: document.getElementById('app'),
view: mainView
}
) |
Beta Was this translation helpful? Give feedback.
-
About const ResetCounter = state => {...state, counter: 0} We will have a new state (the references will be different) while the resulting VDOM will be exactly the same as current one. So I tried to detect those situations and instead of calling Anyway object comparison code is not the best and doesn't cover all variants. So may be we can skip this optimization for now. In this case the export const mnt = (getter, setter, parentMapper) => function mapper (globalState, ...newValue) {
return typeof (globalState) === 'function'
? mapAction(globalState, mapper)
: newValue.length === 0
? parentMapper
? getter(parentMapper(globalState))
: getter(globalState)
: parentMapper
? parentMapper(globalState, setValue(parentMapper(globalState), newValue[0], setter))
: setValue(globalState, newValue[0], setter)
}
const mapAction = (action, map) => (state, options) => map(state, action(map(state), options))
const setValue = (state, value, setter) => {
const newState = Array.isArray(state) ? [...state] : { ...state }
setter(newState, value)
return newState
} |
Beta Was this translation helpful? Give feedback.
-
💁♂ Check out the current proposal here.
Consider this minimal example of a modular Elm app (copied from here):
Button.elm
Main.elm
Notice two things:
A)
Increment
andDecrement
are completely unaware of the shape of the global app state. Their definitions are completely self contained.B)
Main.elm
never once explicitly referencesIncrement
orDecrement
.As far as I've been able to tell, this is not possible to achieve using Hyperapp 2. You can have A or B but not both. This has frightening implications for large scale apps.
I would like to see a convention, add-on library or core change -- or some combination of all three -- which enabled us to replicate the example above in Hyperapp v2.
That essentially means being able to use a module which defines its actions in a self-contained, app-agnostic way, without being required to export them. They should not need to be explicitly referenced anywhere else.
Additionally, whatever we come up with, I would also want it to work in a recursive fashion, that is to say: a module can use a module which uses modules ...
EDIT: Also, of course, this shouldn't just apply to actions dispatched from views. It should be the same for actions dispatched from Effects (whatever action initiated the effect), and subscriptions.
Beta Was this translation helpful? Give feedback.
All reactions