Raynos / mercury

A truly modular frontend framework
http://raynos.github.io/mercury/
MIT License
2.82k stars 142 forks source link

Why static render methods? #191

Closed shoover closed 9 years ago

shoover commented 9 years ago

Just curious why mercury component render methods are static since naively it would be convenient for a parent component to call foo.render() as an instance method. Is that mainly to enforce separation from the state object and discourage mutating local state?

Thanks very much for the excellent docs and examples. Mercury is an enlightening project.

crabmusket commented 9 years ago

You can definitely do that. I advocate separating render functions completely into their own files because

  1. Hot reloading is easier to do
  2. It makes it more difficult for the view to update model state in unexpected ways (the only sanctioned way is by binding channels to the DOM) There is always the possibility for local state in renderers, what you want is for the model to control access to itself via channels
  3. Ideologically, model and view should be separate. There are many ways one might want to render a boolean state; so instead of duplicating the state in several components (tickbox component, switch component), separate state and renderer (boolean state + tickbox renderer or switch renderer)

2 and 3 are just my opinion. Raynos does seem to lean more componentwards in some of the examples, for example how most render methods are attached to an object, not completely free.

criloz commented 9 years ago

@shoover is not a requirement of the library just a way in which Raynos do the examples, because he likes more functional programming, in your case I see that you want to use and OO paradigm, a option then is create local state in each component, and attach them to his parent component state, and the state of the parent to the parent-parent state, and soon on till the root State (making you State tree in the process) when the class are loaded, using observ-struct, observ-array, or observ-varhash. if you are careful and understand the code that is really small and easy to comprehend, you will archive the same using the OO paradigm.

this approximation is good for me because In my case when I think about the model of the app, I relate it more with the db data (users, tasks, messages, etc.) very similar to what I should have in my db. the ui for me can represent that data in different ways, so the state of db like data should not be tied to a UI component. and the Ui component should has its own state. (is just my way of thinking)

I archive those things in this way (Warning: this is pseudo code can contains many errors)

// Create global variable that you will use  ref the db models
GLOBAL_VARIABLE = {};

// load your model here, you can define all your model structure like
// you do when use db-schemes using for example an observ-struct (I prefer this way),
// or in scheme-less approximation  using observ-varhash  
// and reference to the db model in the global variable

GLOBAL_VARIABLE.model = mercury.varhash({});

// load the ui root component, you should create the UI state tree in the process
var ui = new UI()

// Create the app state
var appState = mercury.struct({
    "model": GLOBAL_VARIABLE.model,
    "ui": ui.state
});

//start mercury
mercury.app(document.body, appState, ui.render);

// Ui class implementation 
class UI {
    constructor() {
        // we create ui root state
        let state = {
            show: mercury.value("c1");
        }

        // load your component 1
        this.c1 = new Component1();
        state["c1"] = c1.state
            .
            .
            .
            .
            // load your component n
        this.cn = new Component_n();
        state["cn"] = cn.state

        // the magic occurs here 
        this.state = mercury.struct(state);

        // subscribe to child events
        this.c1.onClose(this.closeView);
        .
        .
        .
        this.cn.onClose(this.closeView);

    }
    closeView(event, data) {
        this.state.show.set("c1");
    }
    changeView(component = "c1") {
        this.state.show.set(component);
    }

    //=============================================================
    // An important note is that the UI.render should has  the appState as argument,  
    // but this is ignored because each component has access to its own state
    // and already know where to look for render itself.
    //=============================================================
    render(state) {
            // just ignore the state argument
            if (this.state.show() == "c1") {
                return c1.render();
            }
            .
            .
            .
            .
            if(this.state.show() == "cn") {
                return cn.render();
            }
        }
   // =======================================================
   // in case that you want to use the immutable state
   //   (more mercury way also more safest)
   //  this will avoid to accidentally or by bad practices  change the state
   //  in the render procedure, if you know what are you doing 
   // just pick the method that you are comfortable.   
   // =======================================================
    render(state) {
        if (state.show == "c1") {
            return c1.render(state.c1);
        }
        .
        .
        .
        .
        if(this.state.show == "cn") {
            return cn.render(state.cn);
        }
    }
  }

tips1: use typescript is of great help in this kind of cases

Sorry for my broken English I hope that this example can help you.

shoover commented 9 years ago

Thank you, @eightyeight and @criloz, for the helpful explanations. I can see there is room for changing designs to suit the application but the examples seem to do it one particular way that ensures a functional perspective. I see this separation enforced in many Elm designs, as well.