yarray / AviPost.web

Web app of AviPost
Apache License 2.0
0 stars 0 forks source link

Think about UI engine #3

Closed yarray closed 9 years ago

yarray commented 9 years ago

Think about the "ideal" UI engine for us, including:

What parts should a UI engine contain? What are the best ways to do that?

The conclusion should contain a conceptual model. The solving of this question should learn from other UI frameworks and can explain them with the final model, to justify their suitability for us.

Other UI frameworks/libraries

yarray commented 9 years ago

1.

Concepts to be thought:

yarray commented 9 years ago

2.

Thoughts:

  1. State is History (part of Stream)

    e.g. for system S = on|off -> S, = last event is "on"

  2. Data is State

    e.g. select(index) = select(data[index]) = do('select', index) = select_(). The boundary can be defined freely, we just cannot distinguish Data and State without context.

  3. Action is alphabet (possible events) for input Stream

    You can organize Actions (with Data and State and their patches) to create input Stream, but cannot transform it.

  4. Event is alphabet for output Stream

    You can transform the output Stream, but it's not created by you. Listening Event is equal to set up fmap, a specific way to transform the Stream.

Questions:

"is" means that only one the two concepts on both sides should be chosen, or if combined the boundary to orchestra them can be changed.

yarray commented 9 years ago

3.

The conceptual model should be able to describe:

  1. Side effect (DOM operations require)
  2. Callback style control flow (DOM events require)

For simplicity we can offer only two types of control flow in traditional sense:

  1. Functional invoke (query and return, or "library style")
  2. CPS style (f(…, g), where g will be only in the last step of f, and g has no return value

2 can be transformed to promise or RP. If all side effects of f(…, g) are in g, f is also can be transformed to an FRP component

yarray commented 9 years ago

4.

UI's complexity resides in the fact that it is the center of 4 aspects: Dom IO and program IO, e.g:

A toggle button may:

  1. Listen to the onclick DOM event.
  2. Add/remove class for button in DOM
  3. Provide "toggle()" so we can toggle programmatically
  4. Provide "onToggled(f)" so we can respond when it's toggled
yarray commented 9 years ago

5.

Regarding Actions as input and Events as output, the most confusing part happens in following scenario:

PlusOne:

var onPlused = function() {};

// Event
function listenPlused(handler) {
    onPlused = handler;
}

// Action
function plus(num) {
    onPlused(num + 1);
}

Printer:

// Action
function print(num) {
    console.log(num);
}

// no Event since it's end of the flow

If we bind as listenPlused(print) then it's clear. However, suppose we only want to print the number when it's greater than 5, we may write:

listenPlused(function(num) {
    if (num > 5) {
        print(num);
    }
});

Here Action (input) and Event (output) are not combined directly. We can define a new concept as "binder" or "adapter". However, there are some questions: Is it also a module? If it is, what's its input and output? It's obvious that we can migrate logic between "adapter" and our Action/Event module, so what's the boundary?

The problem is that we are inconsistent here. In fact the "adapter" can be transformed to standard module as:

var onGreaterThanFive = function() {};

// Event
function listenGreaterThanFive(handler) {
    onGreaterThanFive = handler;
}

// Action
function putNum(num) {
    if (num > 5) {
        onGreaterThanFive(num);
    }
}

The problem is: is it worthy to strictly obey the Action/Event abstraction if we use this style to do flow fontrol?

yarray commented 9 years ago

6.

With Action and Event, a module is equal to a stream transformation in RP. However, RP is more data centric while Action/Event are more operation centric. Also, how input becomes output is expressed with stream operators in RP, but with normal imperative control structures in Action/Events. The two conceptual models seem to always raise some incompatibilities. Need to think over again.

yarray commented 9 years ago

7.

There is a confusing case when thinking about DOM + UI component. It looks like a variation of Action/Event model. Use Toggler as an example:

function Toggler(button /* DOM */ ) {
    this.on = true;
    this.onToggled = () => {};

    this.button.addEventListener('click', () => { 
        this.on = !this.on;
        this.onToggled(this.on); 
    });
}

// Event
Toggler.prototype.listenToggled = handler => {
    this.onToggled = handler;
}

There is no Action. Input is a DOM element and it can be regarded as a collection of Events. So it seems that it's an Event/Event component. But that's not concise.

The key point is Toggler without DOM makes no sense (What is Toggler control without underlying DOM is). You cannot use it separately. Given a object with a 'click' event, the code works, but it makes no sense and the concept is solid.

Now suppose we change the code to Action/Event:

function Toggler() {
    this.on = true;
    this.onToggled = () => {};
}

// Action
Toggler.prototype.toggle = () => this.on = !this.on

// Event
Toggler.prototype.listenToggled = handler => {
    this.onToggled = handler;
}

// trivial binding code
var t = new Toggler();
// for DOM
this.button.addEventListener('click', t.toggle);
// for other event
x.listenYEvent(t.toggle);

Now we can find that Toggler is meaningful WITHOUT DOM. It is a abstract toggler now and can be combined with both DOM click events and other events without any conceptual confliction

So the options of conceptual models here are:

  1. Treat Toggler with DOM as a whole, do not split components here. Introduce User Action comparing to Programmable Action as Input, and User perceivable DOM changes as Output (Event).
  2. Create an Action for every DOM event, then trivial binding happens in constructor.
  3. Create a separate Abstract UI logic component and bind outside as the second code block do
yarray commented 9 years ago

8.

rp-action-event

Blue boxes are concepts from the Action/Event model, while green ones are concepts from RP model. It can be seen that the black box logic resides in components for the Action/Event model, while it resides in binding part for the RP model. This part of logic in RP model is more clear but may be limited, since it uses stream operators instead of normal code.

We do not introduce non-trivial binding for Action/Event model. It will offer no advantages but may cause confusion as stated in the 5th comment.

There are also other conclusions here:

  1. There's no corresponding concept of Action in RP.
  2. Stream binder + components in RP is not equal to Action/Event's components, since it starts with data from Events rather than Action.
  3. Stream binder + components is like a UI control waiting for DOM, as discussed in the previous comment. That means stream binder + components generally cannot be understood without its upstream. A stream binder + component is not a component. (Saying "not a component" means that it's less individual.)
  4. Trivial binding + components (A/E) == Stream binding + components (RP) != components
yarray commented 9 years ago

9.

Explain concepts:

  1. State

    State is the history, including past data and events in FRP State is directly modeled in A/E

  2. Data

    Data is a special state, only differentiate itself from other states in concept

    Data binding is a transform from data changes to DOM transitions, or, is a transform from data to some kinds of state (virtual DOM), which can be in turn flushed to DOM changes (render in React)

    FRP itself can only support the second type till "some kinds of state", but we can combine it with other mechanisms.

yarray commented 9 years ago

10.

"Not a component" in previous comments is not exact, it should be rather expressed as "interface coupled" versus "no coupling". Example:

Version 1:

function A(b) {
    return doSomething(b.B());
}

Version 2:

function A(value) {
    return doSomething(value);

// binder
A(b.B());

Version 1 is interface coupled with b, while version 2 is not coupled with anything, all composition logic resides in binder.

yarray commented 9 years ago

11.

A possible way to combine two models is to use Action/Event on the top level, and implement it inside using FRP. It requires a little hack but is totally doable, an example with Bacon.js is:

function actionStream() {
    var bus = new Bacon.Bus();
    var f = function(data) {
        bus.push(data);
    };
    f.stream = bus;
    return f;
}

function createPluser() {
    var plused = () => {};
    var push = actionStream();

    // frp inside
    push.stream
        .bufferWithCount(2).map(l => l[0] + l[1])
        .onValue(a => plused(a)); // output to Event

    return {
        push,
        onPlused: handler => plused = handler
    };
}

var pluser = createPluser();

pluser.onPlused(res => console.log(res));

pluser.push(1);
pluser.push(2);
yarray commented 9 years ago

12.

Rethink FRP: If we treat the stream as data stream rather than event stream, then the binding CAN be a part of component, or can be "no coupling". The structure in comment 8 changes to:

rp-action-event

where semantics of Events are trimmed out of stream, and how to explain the data depends on each side of binders.

yarray commented 9 years ago

13

Another note on UI's complexity: interfaces for both shared Data/State and DOM are two way flow, and probably with side effect. It is just imperative programming without ANY restrictions!

Possible solution:

  1. Do not share data or DOM (CSS selection cannot be avoided so clear interface is still needed). By doing this side effects on Data are restricted to affect only inside the component. However it may be less intuitive in some cases, and makes data binding nearly meaningless.
  2. Model Data as component and bind it outside. This does not touch the fact that it's two way flow with side effects, but at least the flows are more explicit.
  3. Split Data consumers and Data modifiers if Data have to be shared. This is perhaps the best choice and is the essence of one-way binding. However it's no way to enforce the manner without watching the interior of components.
  4. Use technologies like VirtualDOM to deal with DOM, do not use or do not share state, use FRP to avoid mutable data. This way data binding can hardly be defined as changes -> transitions. Also it needs extra layer and paradigm shifts.
yarray commented 9 years ago

14

Concepts:

Options are things that can be changed on a UI control, while even if they are not changed the main functionality can still be accomplished.

Container can be well understood as an Action/Event component. Its problem in concept is discussed in comment 7. For each type of UI control conceptual models:

  1. The Action/Event component is considered only inside, which makes this part of data flow unclear.
  2. will be more verbose.
  3. will be more verbose.

Structure of DOM is a fact and can be equally expressed as an emmet hierarchy string. Different CSS and JS use different sub hierarchy as interface.

DOM generation is like dynamically change data schema, which is quite error prone. How to deal with it?

Sometimes Data is a little superior abstraction than general State. Think about list with detail, while an item can be marked as "important", which will in turn represented by color in both sides.

Mutable data with events passed in is something like a Container.

State in DOM is hard to parse and full of noise. So a better choice is to keep another state, and sync (part of) it with (part of) DOM state, as in the case of data binding (view model vs. DOM state).

All cases where self-created stateful members vs. passed in stateful members can be discussed like the discussion on DOM in comment 4. Generated members are hard to be shared with others since its interface tend to be unstable.

yarray commented 9 years ago

15

Multipart controls are trivial, which need only some way to direct flows (Action <-> Event or bound common data). The complexity may happen if the flows are all two way.

Parent controls are just those of which Container interfaces are top part of the real DOM. Things get harder if DOM generation involves.

yarray commented 9 years ago

16. React

yarray commented 9 years ago

17. Masonry

yarray commented 9 years ago

18

Challenge:

A list page with every element containing a button "edit". The editing page can be a modal, a expaned div in the same page, or another page, or even another website. How can we design the interface to be flexible? Several options:

  1. Page with an Event: "edit item". This Event is then hooked with some Action in an orchesting component.
  2. Page with a object injected in. This object contains a method called "edit item".
  3. Directly write the page redirection or others in the page
  4. Send a message in public channel, or give an intent as in Android. The editing component listens to the message.

Problems:

  1. It seems the editing page is there is no universal mechanism to deal with the editing part both in/out of the page.
  2. 1 and 4 seems alike, only that in 4 the channel can be passed in, but the events are normally represented as text (rather than api)
  3. For 1, "edit item" as an Event is strange in concept. Also the Event and Action share the same name which is strange.
  4. For 2, dependencies are harder to reason and conflict to our design that api call with side event should be composed by Action/Event or Stream
  5. 3 is Not flexible and should be avoided unless there are some restrictions provided.
yarray commented 9 years ago

19

Best way to solve comment 18 should be solution 1 or just setting the button as a link. Other methods suffer from unclear control flow. For problem 3, editButtonClicked can be used as the event name.

1 can be solved. We can inject a redirection to the event, and different handlers can handle both cases. It reminds us the importance of routing design.

yarray commented 9 years ago

20

This thread produces enough insights till now. Remaining are decisions, the most important concerns are:

  1. Which paradigm to use, FRP or Action/Event?
  2. Is the Markup provided or generated?
  3. What's the State? Is there Data in concept and what is it?
  4. How to binding Data to DOM states, initial states + diff to diff or merely states to states?
  5. How to express the data binding, in markup or in code?

Rough answers now:

  1. FRP. We should try new things with such solid abstractions.
  2. Provided. This does not matter so much, but it will be much easier for parent UI.
  3. Depends on cases.
  4. Initial States + diff. We may want to add fancy transitions later.
  5. Template with resulting structure should be offered, so that the required structure will be more clear. bindings are expressed in Markup, but how to apply the changes will be determined later.

Libraries used:

Bacon for FRP.

Additional notes:

  1. It's highly likely that 5 will be too complex for ourselves to handle. In that case handlebar or others may involve
  2. 4 can require too much work. VirtualDOM with states -> states binding can be a candidate.