Git Product home page Git Product logo

Comments (16)

creationix avatar creationix commented on July 20, 2024

Closures are objects of a sort, just a different flavor. http://c2.com/cgi/wiki?ClosuresAndObjectsAreEquivalent. So components are instanced, they just aren't defined in the typical way using JS constructors, prototypes, and this.

But you do have some valid ideas here and have hit in a rough area in domchanger. The instances of the components are only exposed internally to the virtual dom tree.

Maybe we can add a new input type that represents an instanced component. Maybe any object containing a function property named "render"? Then you could use whatever object system you want to create the objects and pass in the instances instead of the function at the first json-ml element.

from domchanger.

leeoniya avatar leeoniya commented on July 20, 2024

Re: 2,3: You can leave users to create regular vanilla instances (or objects) and just declare an interface they must expose.

some ideas, probably lots of things that dont quite make sense, but...:

function IDE(foo, bar, subCompA, subCompB) {
    this.foo = foo;
    this.bar = bar;
    this.subCompA = subCompA;
    this.subCompB = subCompB;
    this.optA = 'hello';
    this.optB = 'world';
}

IDE.prototype = {
    init: function(refresh, emit, refs) {

    },
    render: function() {
        return [
            ['div', this.foo],
            ['div', this.bar],
            this.subCompA,
            [subCompB, this.optA, this.optB],
        ]
    },
    doSomething: function() {
    }
}

var myIDE = new IDE('lazy', 'dog', ...);

domChanger(myIDE, document.body);

from domchanger.

creationix avatar creationix commented on July 20, 2024

yes, something like this, I'll look it over later.

from domchanger.

leeoniya avatar leeoniya commented on July 20, 2024

nice.

another idea is for {refresh, emit, refs} to be tagged onto the component's view property, which would start out null/absent, but populate on initial render();, this would then allow the user to call this.view.refresh() from anywhere, even external to the component myThing.view.refresh(). and just scap the whole init:.

so...

function IDE(foo, bar, subCompA, subCompB) {
    this.foo = foo;
    this.bar = bar;
    this.subCompA = subCompA;
    this.subCompB = subCompB;
    this.optA = 'hello';
    this.optB = 'world';
    this.selStart = 4;
    this.selEnd = 10;
}

IDE.prototype = {
    on: {},                   // handlers for emitted events
    view: null,               // will contain {refresh, emit, refs} after initial render
    render: function() {
        var setSelection = function(el, isInitial) {
            isInitial && el.setSelectionRange(this.selStart, this.selEnd);
        }.bind(this);

        return [
            ['div', this.foo],
            ['div', this.bar],
            this.subCompA,
            [subCompB, this.optA, this.optB],
            ['textarea.some-area', "sometext", setSelection]     // provided fn is a per-element afterRefresh callback
        ];
    },
    doSomething: function() {
        // something....
        this.view.refresh();
    }
}

var myIDE = new IDE('lazy', 'dog', ...);

domChanger(myIDE, document.body);

myIDE.doSomething();

This construction opens a lot of doors and offers a ton of flexibility, leaving the rendering aspects almost entirely decoupled and swappable from the rest of the app logic.

from domchanger.

leeoniya avatar leeoniya commented on July 20, 2024

i've implemented most of the stuff proposed above in new branch [1]. check out the examples/ide.html.

still todo:

  • isInitial passing to component-level afterRefresh()
  • per-element afterRefresh(isInitial) (in the JSON-ML tree)
  • remove leftover bits related to initial instantiation with "data"
  • ensure to bind this for all ops to object/component instance
  • move ref tags to attr object to clean up tag defs? eg ['div#foo$myRef'] -> ['div#foo', {ref: "myRef"}]
  • remove ability to define components with fresh data? eg: [MyComponentFn, param1, param2]. or maybe not, need to discuss if this is still a useful pattern.
  • auto-suffixing of "px" to css props
  • various util functions that eliminate boilerplate and manual .refresh() such as deferred rendering, 2-way data binding, array & object observation (similar to Mithril's m.withAttr, m.prop(), etc...)
  • general javascriptesque optimizations like String(number) -> ""+number
  • unit tests, browser compat testing, jshint
  • inter-component pub/sub for emitted events?
  • SVG nodes?

[1] https://github.com/leeoniya/domchanger/tree/rework-components

from domchanger.

creationix avatar creationix commented on July 20, 2024

It seems that you're building a different framework than I originally envisioned for domchanger, but still want to be able to use the dom diff algorithm. I was thinking of a system that had strict one-way data channels where events would bubble up only and data would travel down only. It appears you're looking for more of a traditional system with 2-way bindings, methods on component objects, etc.

Would it make sense to split out the core diff algorithm into it's own module? I don't really want to change the direction of domchanger, but I also want to enable you to build the system you envision.

from domchanger.

leeoniya avatar leeoniya commented on July 20, 2024

TBH, i'm not sure it would be worth it since 90% of domChanger is the diff strategy.

The changes i'm making (and in the todo) do not increase the size and in fact can retain 100% of the current functionality while also remaining flexible enough to do these things (i've removed very little but none of it was necessary to implement the features). I like a lot of things about Mithril except how it tries to awkwardly shoehorn some MVC/controlller concepts that dont quite make sense and has a redraw system that's a bit too magical at times.

IMO, the current component strategy of domChanger makes it a very niche and monolithic commitment. If you feel like the changes i'm proposing are too far from your original intent, I promise not to be offended and will rename my fork and maintain it separately :)

I will note that I'm not making these changes for the sake of making changes, I'm exposing functionality/sugar which I'm finding necessary for a pleasant DRY/SRP/API experience while writing an app (personal opinion of course).

from domchanger.

leeoniya avatar leeoniya commented on July 20, 2024

btw, the entire code that mimics Mithril's m.prop() and m.withAttr() (aside from deferred stuff and edge cases/old browsers) is defined externally like this:

domChanger.prop = function(initVal) {
    var curVal = initVal;

    return function(newVal, noRefresh) {
        if (!arguments.length)
            return curVal;
        else if (newVal !== curVal) {
            curVal = newVal;
            !noRefresh && this.view.refresh();          // todo: rAF-debounce?
        }
    }.bind(this);
};

domChanger.sync = function(elProp, objProp, noRefresh) {
    return function(e) {
        var ep = e.target[elProp],
            op = this[objProp];

        if (typeof op == "function")
            op(ep, noRefresh);
        else if (op !== ep) {
            this[objProp] = ep;
            !noRefresh && this.view.refresh();      // todo: rAF-debounce?
        }
    }.bind(this);
};

var sync = domChanger.sync;
var prop = domChanger.prop;

and requires a 3-liner core addition to make work.

from domchanger.

leeoniya avatar leeoniya commented on July 20, 2024

Hi again :)

In my ongoing quest for component and view-data independence, I've tried to wrap the unmodified domChanger to serve as the decoupled view layer in a structure of independently instantiated and assembled components. What resulted is pretty good - component-wrapped views with all data required by render() being closured, allowing render() to accept 0 params, eliminating the need for update(), externally refreshable components, support for external data modification and retaining the ability for each component to expose its own API.

Here's the code: https://jsfiddle.net/gmkhzxba/1/

Some notes:

  1. A decent amount of view boilerplate is required per component here. This can probably be reduced/automated by using an anon function for the view constructor and returning a key rather than relying on an undefined Function.name in domChanger's internal component naming strategy.
  2. There seems to be a bug in the diffing where modified data is not rendered correctly after calling refresh(), despite the console.log() from within the render() functions showing that the generated JSON-ML is correct. If you open up the console in the example and look at the output, the header text is modified and rendered correctly, but the array is modified and rendered weird.

plz lemme know what you think (especially about point 2, hopefully it's an easy fix), thanks!

from domchanger.

leeoniya avatar leeoniya commented on July 20, 2024

Re: 1, I've added a function that does some of the boilerplate: https://jsfiddle.net/gmkhzxba/4/

Unfortunately, it turns out that Function.name is read-only, so I've had to add an alternate .fnName property that can be tagged onto anon functions (and modify domChanger slightly [1] to be aware of it). This allows anon functions to be view generators without screwing up the internal component naming strategy.

EDIT: updated the helper to implicitly bind the provided render and any on handlers to the wrapping component, eliminating the need to alias var self = this; each time [2]

[1] https://github.com/leeoniya/domchanger/commit/4c7e3aa8dbff233581897b0c609d0afa4a6f4cdf
[2] https://jsfiddle.net/gmkhzxba/8/

from domchanger.

leeoniya avatar leeoniya commented on July 20, 2024

Another update, Re: 2,

I realized from the filtered list example that subcomponents must be tagged with a unique key to be re-rendered properly. This key is initialized by the wrapping component and tagged onto the array of [componentFn, arg1...] [1].

I wanted the unique key initialized not in the parent component. What I did was add a small change to domChanger [2] that would allow tagging the .key property to the component constructor directly so it did not need to be handled in the parent. This fixed the diffing [3].

I'm submitting a pull request for the anon-components branch, which has only minimal changes to the lib while enabling full component/data separation with no breaking changes.

[1] https://github.com/creationix/domchanger/blob/master/examples/filterable-product-table.js#L78
[2] https://github.com/leeoniya/domchanger/commit/499847ec432b7c06cdeb14875c0f9e66426c79b3
[3] https://jsfiddle.net/gmkhzxba/10/

from domchanger.

creationix avatar creationix commented on July 20, 2024

Sorry, I'm super busy this week (working till about 1:30AM every day). Maybe I'll have a chance to look at this in depth later. Feel free to ping me in a few days.

from domchanger.

sprat avatar sprat commented on July 20, 2024

At work, we use a component-based framework called Nagare and written in python, so I can give some insights on how it works and how it translates to javascript, it could inspire this discussion about the future domchanger API.

I share the opinion that domchanger should let the users do what they want with the constructor. It allows users to inject services, configuration parameters, state data, persistence layer into the objects that will be rendered. However, I don't really like the API proposed in the last comments: in my opinion, we must not force users to create View classes when a regular function is probably enough. It's up to the users to create classes if they want to.

In Nagare, the views are defined completely outside the components objects. The components are just pure python objects with business-related data and methods, and the views are added afterwards on the class, like a mixin. Basically, it would translate to that in Javascript:

// define a pure javascript class
function Counter(value) {
    this.value = value || 0;
}

Counter.prototype.increment = function() {
    this.value += 1;
};

// attach a render function on the Counter class: the default view
render_for(Counter, function(component) {
    function onclick() {
        this.increment();
        component.emit('incremented', this);
    }

    return [
        component.render('readonly'),
        ["a.increment", { onclick: onclick }]
    ];
});

// attach another render function on the Counter class: the "readonly" view
render_for(Counter, 'readonly', function(/*component*/) {
    return ["div.counter", this.value];
});

We attach render functions to a class with an optional view name. In javascript, we should probably also allow attaching render functions to objects directly.

The rendering function receives a component object. With this object, we can emit an event that will propagate to the parent component, or we can render another view of the same component, like I've shown above.

In order to render a sub-component in Nagare, we have to wrap the object in a Component instance and keep this instance in the object state due to technical constraints: it's quite annoying in practice. So I would do that instead:

component.render(mySubComponent, 'view_name');

To catch an event sent by a sub-component in Nagare, we set an on_answer callback on the sub-component like this :

Component(mySubComponent).on_answer(callback)

I would translate that this way (in the render function):

component.on(mySubComponent, 'incremented', function (data) {
    // ...
});

Or:

component.onEvent(mySubComponent, function (event) {
    // ...
});

It depends how you want to handle the emit event: send an object or send a event type (string) plus some data.

The render functions are attached to a class, but it can walk up the class hierarchy: if you implement a subclass, the same views as the base class are available in the subclasses. There's a mechanism allowing the subclasses to extend the inherited views: if the render function has a next_method
special parameter, calling this function will render the inherited view. In javascript, views inheritance means attaching the views as attributes of the objects and/or prototypes.

Here is a full example of the proposed API:

function App(headerText, things) {
    this.header = new Header(headerText);
    this.thingList = new ThingList(things);
}

render_for(App, function(component) {
    function onclick() {
        component.emit('clicked');
    };

    component.on(this.thingList, 'elementAdded', function() {
        // ...
    });

    return [
        ["div", { onclick: onclick }],
        [component.render(this.header)],
        [component.render(this.thingList)]
    ];
});

from domchanger.

creationix avatar creationix commented on July 20, 2024

I'm not sure what render_for does technically. ES5 doesn't have WeakMaps or Symbols, so I'm not sure how you would associate the function with the object.

Also I think walking up inheritance chains is outside the scope of domchanger and unclear. There is no one class system in JavaScript and people write their objects in many different styles.

I think the key is we need a way to initialize a component that is simple and flexible. I'll see if I can come up with a proposal.

from domchanger.

creationix avatar creationix commented on July 20, 2024

So here is the problem from the renderer's point of view:

Suppose on one pass, you get the following data to render

[Foo, 1, 2, 3]

You've never seen a Foo component at this path, so you create a new component instance.

But then on a refresh, the parameters changed:

[Foo, 1, 2, 5]

Since you have an existing component at this path, we don't want to destroy the old one and create a new one, it would better to reuse the existing component (we think). So we need to find the component we created last round and update it with the new data.

Then at a later pass the component is gone or changed to another type. In this case, we do want to destroy the old component and let it clean up any state.

So for each component, there are 4 types of lifecycle events, create, update, afterRender, and destroy.

In addition to these lifecycle events, it would be nice to pass in parameters to the component's constructor apart from the update data so that a component constructor can be used in multiple places, but have different internal properties.

We need some sort of unique and persistent identifier for a component to know when the same instance should be reused and when a new instance should be created. This is why I went with the function as the component. Also it makes parsing less ambiguous because functions at the start of a list are always components, while objects can be different things depending on it's shape.

The reason I combined constructor data with update data is so that the user doesn't have to keep track of their component instances, they just declare the type and data and we'll handle it for them.

One possible solution with the existing semantics is to have a function that returns the component function:

[MakeFoo(1, 2), 3, 4]

function MakeFoo(a, b) {
  return function Foo(c, d) {
    ...
  }
}

But as a user, when do you call MakeFoo and when do you simply pass in the resulting Foo? It's now up to the user to manage the life-cycle of their components (which may be what you want).

We could even go a step farther and allow an object to be returned instead of the current pseudo-constructor function. We would just need to define the shape and interface of this object as discussed previously. But again, moving from a function to an object moves the burden of managing lifecycles to the user and outside the library. Maybe we should simply allow both and let users decide which is appropriate for their use case?

from domchanger.

sprat avatar sprat commented on July 20, 2024

I guess I was not clear enough. Here is an (rough) example to illustrate my point of view. Imagine I want to build an application having a menu and a main content area whose content change whenever the user click on a menu item. I would implement it like this:

function Menu(items) {
    this.items = items;
    this.selectedItem = items[0];
}

Menu.prototype.render = function (emit, refresh) {
    // render the items
    var items = this.items.map(function (item) {
        var selected = (item === this.selectedItem) ? '.selected' : '';
        return ['div.menu-item' + selected, {
            onclick: function() {
                // when an item is clicked, we change the state of the
                // component, so that it highlights the clicked item
                this.selectedItem = item;
                // we also send an event to our parent so that it can update
                // its state too
                emit('itemSelected', item);
                // refresh the components tree
                refresh();
            }
        }, item];
    });
    // wrap the items into the menu div
    return ['div.menu', items];
};

function Application() {
    this.menu = new Menu(['Home', 'Presentation', 'Contact']);
    this.content = new HomeContent();
}

Application.prototype.onMenuItemSelected = function (item) {
    var Content = {
        'Home': HomeContent,
        'Presentation': PresentationContent,
        'Contact': ContactContent 
    }[item];
    this.content = new Content();
}

// the default view
Application.prototype.render = function (emit, refresh, renderChild) {
    return ['div.application',
        // we render the menu component
        renderChild(this.menu, {
            // when we receive an 'itemSelected' event from the menu
            // sub-component, we update our state by swapping the content
            // object
            'itemSelected': this.onMenuItemSelected
        }),
        // then, we render the content component
        renderChild(this.content);
    ]
};

var application = new Application();
domChanger(application, document.body);

I hope this example properly expresses what I mean. In this approach, the developer deals with the components life-cycle: instead of using factories to build the sub-components in the render function, you'd use objects, i.e. instances of user-defined classes. Then, you can pass whatever you want into the constructor of the components. Compared to your approach, it justs invert the control: the constructors are now used to pass data or services to the instances and the render functions receive the parameters to render the sub-components, refresh the component tree or emit some events that will trigger changes up in the components tree. In my approach, the application is just a tree of components (pure Javascript objects), the components change their state when an external event occur (user interaction, data arriving from a webservice or websocket, ...), then you render the whole tree and send an update to the DOM.

So, to answer your questions: render_for just declares a view and provide the function that renders this view. In the example above, I have implemented it as a regular method of the component, but it's an implementation detail. As for the views inheritance, it's an advanced use-case rarely needed in practice (especially if you prefer composition over inheritance), that why I didn't give much details. You can remove it from the discussion ;)

from domchanger.

Related Issues (9)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.