Open Emasoft opened 9 years ago
I don't know any good way to make it "isomorphic" in Dart right now, but I've removed comment that it won't be isomorphic in the future. If Dart introduces something that it will be possible to make it work as easy as in Javascript, I'll add this feature.
Checkbox/TextInput components should be removed, there are easier way to implement them now. They were implemented because I've tried to implement all important features that React has, and in this case I just implemented them in "React-way" as Components.
Yes, there is a boilerplate for "virtual" constructor and "property" propagation, and I think that it should stay the way as it is for this low-level API and just create some other API on top of that, or maybe code generation with pub transformer.
It would be also quite nice to have pub transformer for virtual dom, so it is possible to create virtual dom with something like v.tpl('<div id="x"></div>')
Transformers are bad, because they destroy the rapidity and efficiency of a script language that doesn't need any compiling phase, like javascript. Dart should use transformers only for debug or for testing. If you want to make Liquid good, you need to avoid the use of any transformers. The boilerplate could be easily avoided automating things. There is nothing in the verbose syntax you are currently using for defining components that cannot be automatically inferred by some good heuristic code in the liquid lib. As my university professor always said: "The secret for good code is to never ask an human to decide anything that can be decided by the computer with a good algorithm, but to allow him to ovverride that decision at anytime".
What do you think of the syntax/API that clean's react wrapper exposes: (using functions for components and 'native' tags)
see: https://github.com/cleandart/react-dart
div({"className": "somehing"}, [
h1({"style": {"height": "20px"}}, "Headline"),
a({"href":"something.com"}, "Something"),
"Some text"
])
(obviously can also easily be namespaced if needed to sthg like r.div etc)
I like the idea of something like jsx for readability but all in all I also prefer to don't have transformers that aren't necessarily needed. For debugging something like the react chrome devtools extension would be nice:
Not sure how hard that would be though.
I agree that transformers is a terrible idea.
I like clean's API, but how much performance and features we can sacrifice to make all this magic behind such API. For example, I don't want to treat classes like it is just a string, I want to add classes to the root element from the outside of the Component and from the inside (like it is done in Polymer). Applying styles is also a special case. So for this simple case we will need to make two get operations in the LinkedHashMap for each node, and as we know, string hashmaps in the dart2js world is slower then {} in js.
I've looked at different ways on how to remove boilerplate with virtual dom nodes for components, and I can't find any reasonable way to implement it right now.
There is even no support for varargs, and smoke doesn't support creating new instances by Type
.
I will add optional library `liquid/dynamic.dart' that will use mirrors, and API will look something like that:
class MyComponent extends Component { ... }
final vMyComponent = vComponentFactory(MyComponent);
class App extends Component {
build() => vdom.Root([vMyComponent(#key, [arg1, arg2]]);
}
but it will work 6x times slower (vdom benchmark in Chrome).
Mirrors are even worse! Please never use those if not for the debug version. You must use the classic object oriented way to create components, extending a base class. This base class can be bloated as you like, including all that the developers would ever need. In this way when they will subclass the base class for making a new component, they would only change some parameters and adding a very small number of methods. Include an autoconfiguration method in the base class, to set every member of the component automatically according to common usage patterns. Drop maps, use typed object properties. Use enums to give semantics to all kind of predefined usage patterns. Create a central, shared event manager, and make the base component already wired to it, so all the subclasses would have already an event system in place. They would only need to override an existing event handler if they need to change something. Think in a OO way, and you will make things simple and the code non redundant.
Some thoughts/observations on API from playing with clean's react wrapper: (sorry if they don't make sense, I'm a bit jet-lagged atm) :)
1) Maps vs JS Objects: It felt a bit weird (a bit of a regression to React.js) to use Dart Maps
for props. I can't put my finger on it why, might just be that syntactically it is easier to parse when you have an object literal (clear differentiation between the key and the value (often a string) in comparison to having a Map with strings as keys.
div({style: {width: '100%', margin: '0 auto'}}, children);
vs
div({'style': {'width': '100%', 'margin': '0 auto'}}, children);
2) Event Streams: I got used to dealing with events like change
, input
, mouseEnter
etc as streams (I don't do heavy functional reactive stuff but occasional throttling, mapping, filtering). I guess the JavaScript API uses callbacks because there is no agreed upon streaming library that everyone uses (?) or callbacks might be 'good enough' for React components (?) you probably have more experience here.
div({onClick: this.handleClick}, children); // handleClick callback
div({onClick: this.clickStream}, children); // (?) not sure if this feels right, ideas?
3) Props, clarity & validation: One thing I was missing a bit was that by looking at a component it isn't really visible what properties a component expects/requires.
It seems like there is some best practice (if you want to make props explicit) around listing all of the props you use at the beginning of the render method (?).
I guess part of this is because JS doesn't have support for named parameters and therefore everyone is (ab)using configuration objects.
The cool thing about this is that you can get started easily and it feels lightweight. If you have a data structure that is handed to your component that already has all the things you need you can just define a simple render method and you are finished. Nice for prototyping and you can make the component and its props more explicit later. Not sure what a nice and idiomatic Dart solution would look like here.
It is also neat that React has a concept of contracts/validation where you can optionally define what you expect of certain properties which goes beyond type checking.
Additional thoughts:
Obviously defining a nice and elegant API + considering performance in a dart2js scenario is quite a challenge here. Yet I have the feeling that potentially we should be able to take the React concepts and not only translate them into Dart but actually leverage idiomatic Dart to show how elegant React could have felt like if it was created in Dart.
(Sorry for the rambling, I hope some of the above thoughts make sense to you)
I just saw how you expose/use streams in liquid via the component's element
in the constructor:
What about API for components like this:
class TodoItem extends Component {
@property
Item item;
TodoItem(Context context) : super(context);
build() => new VRoot([vdom.t(item.text)]);
}
final vTodoItem = vComponentFactory(TodoItem);
vTodoItem(#key, {#item: new Item('aaa')}); // will create virtual node for TodoItem component
It works right now using mirrors for development mode, and I can create a transformer that will generate all boilerplate in the most efficient way for production mode.
It will be possible to create different kinds of annotations for properties, like immutableProperty
that will check for identity, and if it is identical it won't mark component as dirty. I think that this way will be much better then what it is implemented in React right now. Or annotations for property validation.
And instead of implementing shouldComponentUpdate
, user can implement custom setter that will check if change to this property invalidates the view.
And to those who likes OOP inheritance I am almost implemented Component Decorators :)
class Button extends Paper {
Ripple _ripple;
html.DivElement content;
@property
bool disabled;
html.DivElement get container => content;
Button(Context context) : super(context);
void create() {
super.create();
_ripple = new Ripple(this);
element.classes.add('mui-paper-button');
content = new html.DivElement()
..classes.add('mui-paper-button-content');
inner
..append(_ripple.element)
..append(content);
element.onMouseDown.listen(handleMouseDown);
}
build() {
super.build().decorate(vRootDecorator(null, classes: disabled ? ['mui-disabled'] : null));
}
void handleMouseDown(html.MouseEvent ev) {
var x = ev.offset.x / element.clientWidth;
var y = ev.offset.y / element.clientHeight;
_ripple.animate(x, y);
}
}
final vButton = vComponentContainerFactory(Button);
and now it is possible to create Button like this:
vButton(#key, [v.t('MyButton')]);
or subclass it
class MyButton extends Button {
MyButton(Context context) : super(context);
build() {
super.build().decorate(vRoot([v.t('MyButton')]));
}
}
Please avoid mirrors and transformers at all costs. Use those only for debug or tests. The moment you use one of those two, people will be forced to stop using your library. I and I want to use it, because I like it very much. You should keep it slim and rapid. There are many ways to solve those problems without using mirrors and transformers.
I don't see any problems with mirrors for development mode and replacing them with transformers for production. Polymer and AngularDart using this model.
Even if you don't use mirrors in production mode, you should avoid using a different technique in production mode and in development mode. It is a recipe for unexpected bugs. I use mirrors only in tests. Transformers force us to recompile every time we change something, and this cancels the usefulness of using a script language. Changing something and being able to immediately see the effects is very important. I don't use Polymer and Angular for this reason. I need a development framework lean and quick. This is the thing that makes javascript much loved even on the desktop, where node.js is gaining traction. It seems a small thing at the beginning, but when the project grows you start to appreciate it.
I don't see any other way to implement good API, and all other solutions that I can think of is way much worse.
Javascript world is moving toward ES6/7 compilers like traceur, TypeScript, CoffeeScript etc. Even with old javascript most of the projects are using different preprocessors + gulp/grunt etc. The same on the desktop, projects like Atom are using CoffeeScript, etc.
Didn't tried it yet, maybe use call()
method for children.
v.div(#key, styles: {'width': '100%', 'margin': '0 auto'})([
v.div(1)('line one'),
v.div(2)('line two')
]);
If I learned something in my life as a programmer is that there is always a way. Give me a couple of days to study this. I'll be back to you with some ideas.
Implemented new API for basic virtual dom elements, didn't notice any performance regressions in the VDom Benchmark.
v.div(#key, styles: {'width': '100%', 'margin': '0 auto'})([
v.div(1)('line one'),
v.div(2)('line two')
]);
Looks nice, what if I have an empty element? Would it then be v.div(1)
or v.div(1)()
?
Is there a reason for having key
as positional (& mandatory?) parameter? Can it be an optional named parameter as well?
Just v.div(1)
for empty element.
I thought about making it implicit, and it is a tough choice. If I make it implicit, there will be different kinds of problems for people who doesn't understand how vdom works, and there will be tons of questions why it doesn't work as they expect.
In the React they've chose to make it implicit and in the dev mode they just write in the console that keys should be added when there is something terribly wrong, yet there are still many confusion about it for newcomers.
Very elegant using call
.
It is a tough decision. I don't know enough about the diffing & vdom yet to fully understand all the trade-offs.
I expect I personally will often want to use immutable data structures if possible so having an API where the key is mandatory even if I just fill it out with a dummy key is a bit weird. Should I still provide keys in this case? Would I need to make sure to use the same keys for nodes that have the same props in them?
Is there a way to automatically generate unique keys for every element or would that mean that nodes which might actually be practically the same to no longer be regarded as the same and therefore adding more diffing work and memory consumption?
To create complex web apps, we will need to create stateful components like this button, and it doesn't matter which way we choose to do it, with WebComponents that described in the futurice articles, or with lightweight liquid components.
So, everytime we rerender list of this buttons, we need to preserve state for ripples and make sure that this ripples doesn't migrate to other button when we add one more button at the beginning. It is the most important case, and the second one is that this way you can guarantee "minimal" number of dom mutations, because even simple changes with implicit keys will result in destroying all performance.
Implicit keys are good for static trees, when nothing is changed, but I prefer to use real dom to create static trees, and I am using virtual dom only for mutations (Button example) and keys is just an easy way to control this mutations.
Immutable data structures doesn't change anything in this case, everytime you have list of entities, you need to create unique key for each entity in this list. It is like primary key in databases, it doesn't matter what data structures beneath it(mutable or immutable), entity will always have some unique key that identifies it. And VirtualDOM node key is used to create a relationship between node and this entity.
I think that if you try to render simple immutable list in React, and then add one item to it, React will tell you that you should add unique keys to your entities in immutable list, but I am not sure when they start to warn users that they're doing it wrong.
It is also possible to implement hashCode
and operator==
in your entity, and use it as a key for vdom node.
Thanks for your explanation, here is the related React documentation just for reference:
http://facebook.github.io/react/docs/multiple-components.html
I think your decision to have it explicitly in the API is wise because users need to understand and know about it. On the other hand I personally still prefer an API that only on-demand reflects the complexity of the case I'm dealing with allowing me to not provide keys if they are not needed.
If we zoom out a bit conceptually what I don't like about key
being in the API (and having to be documented and explained) is because it is an implementation detail of diffing stateful components that leaks into what the user of the lib has to manage. Therefore the developer can no longer ignore how the diffing works. The developer has to watch out for internal state making the solution more complex.
This might be a bit radical but I like quiescent
's take on local state:
https://github.com/levand/quiescent (see the readme on differences to Om
)
If state of a component is not located/hidden/stored in the component itself (meaning it has to be passed in again) makes the rendering way easier to understand and consistent to what usually happens (conceptually just 'dumb' re-rendering, obviously this is not what happens but what actually happens is a performance implementation detail I don't have to know about then).
The disadvantage is (like David Nolen of Om
says) is that it causes state that would otherwise be part of the component itself to leak outside of it 'polluting' application data (that said it is possible to make things explicit and have a separation between business data and ui state data).
In other words It seems like quiescent is trading locality/encapsulation for simplicity. Yet the approach is still quite modular as you can easily mount and use the components wherever you want and they control their own behaviour and business logic, you just have to provide them with all their state (application data & ui data).
On a scale this would move the hypothetical component I'm describing between React & vdom
(local: state & logic) (external: state, local: logic) (external: state & logic)
React <=============================> Liquid <=============================> vdom
quiescent
still seems to require the key
information. TodoMVC example:
https://github.com/levand/todomvc/blob/gh-pages/architecture-examples/quiescent/src/todomvc_quiescent/render.cljs#L74)
I think I don't understand the diffing/reconciliation good enough yet. (I need to go through it). http://facebook.github.io/react/docs/reconciliation.html
That said I feel the ClojureScript projects that leverage React (Om, quiescent, reagent) are a better source for inspiration than React itself because of their simplicity.
I'm also looking at Mithril and a couple of others that seem to improve on React.
The whole point of vdom is to easily manage stateful trees, otherwise it is just a terrible "performance optimization".
Just imagine simple user list, and now you want to render it in two views, each user list view is a list of buttons, each button has a ripple effect. In this stateless ui model the easiest way will be to add state for each button to the user entities, but it is wrong in so many ways. So now you need to create ViewModel for each view that will have state for ripple effects of all buttons, and now you need some way to synchronize data between Model and ViewModel and now you're implementing data-binding, or something else.
http://futurice.com/blog/reactive-mvc-and-the-virtual-dom/ "No reusable UI components. React has a strong emphasis on "reusable UI components", which is unmatched in this current MVI proposal."
The idea of pure stateless UI components is so old, and I don't understand why it is hyped right now, it just doesn't work for general-purpose UI libraries that is all about building reusable UI components to reduce complexity when building large applications.
Without keys you just rerender the whole real DOM from scratch when something is changed. It is the easiest way to think about it, because algorithm for implicit keys is dumb and works only for static trees.
I just read through the reconciliation documentation. Interesting heuristics.
I now also get why we can't just have the component generate a unique key
upon creation because the key
needs to be stable to make it possible to recognize a component when lists of children are compared (main use-case like you mentioned is new insertion at the top of the list).
I think usually when you have a component (let's say UserAvatar
) the component itself should be able to pick a stable key for itself from its props (eg in this case the id
of the User might work well).
Wouldn't it feel more natural to have the component be responsible for this instead of the owner?
One advantage of allowing the owner to be responsible is that if a component author didn't think of their component being used in a list of children the owner component can still set keys.
How about having key
as optional named parameter (therefore allowing it to be set from the outside upon creation) as well as having a way to define it internally (by overwriting the attribute or defining a getter)?
That way the API is clean and simple except in the rare use-case when a component is used many times in a list and did not internally define a key.
It is actually a good idea, it is possible only as a static method, or using some annotation that will mark one of the properties as a key, because we don't have access to the component instance at this moment.
But it will be a problem for components like buttons, because they don't have properties from which they can generate unique keys, and basic vdom elements wouldn't have the ability to guess keys, so there will be inconsistency between elements and different kinds of components.
Good point @ basic vdom elements, haven't thought of that.
Sounds like the way React implemented it (passing the key from outside) is simpler because at least it is uniformly always the same concept (basic elements & custom components).
I fear we just have to be good in explaining/documenting the scenario of child lists and when not providing a key might cause problems and what those problems are.
Since lists appear regularly but not all the time and not all child lists cause these problems I'd still prefer the API to not force me to pass a key. This makes the 'internal DSL' easier to read for humans.
I think this is really important and the API is already quite elegant (call
was a great idea to simplify the syntax). Having key
as named parameter also has the advantage that if I set a key it is obvious that what I set is actually a key, making it less magic.
The problem will be with this patterns:
var patternOne = featureIsEnabled ? [featureElement, mainElement] : [mainElement];
var patternTwo = statefulChildOne ? [childOne] : [childTwo];
It is easy for developer to remember that he should provide keys when he renders lists like items.map((i) => vItem(i))
, but in this case it feels so natural to ignore keys. I've made such mistakes even when I read about diff internals and reconciliation :)
I really want to add implicit keys, but the choice here is: less bugs/verbose api, or more bugs/more hard to understand internal magic.
It is easy choice for us to make it implicit, because we know about internals, but most of the people who is using React doesn't have a clue how it works and they won't read internal documentation.
So if we just say that this key should be unique among its sibling, we don't need to provide any detailed explanation about its internals and why it is important to understand, developers will just use it without making any serious mistakes.
Ah, I always forget that either way it will require explanation that the key should also be the same between updates. Ok, I'll implement this implicit magic :)
What about such implicit behavior:
Awesome, looking fwd to play around with the new API, I think it will feel very clean & I'll try to get a feeling for the trade-offs.
Thanks for the examples for when it might be easy to forget about setting keys. Having Dart at your full disposal to build the virtual dom can sometimes lead to not being aware that a part you are describing ends up in a list of children.
I think if there is a lightweight way to detect this and to add it to the tooling it would be great (+ initially linking to the React documentation of the conceptual problem until we have a better one).
Stupid question: Could the Dart analyzer detect something like this and warn us about it?
This way we could get first class support in all IDEs that use the analyzer (sooner or later all popular ones).
That said logging it to the console also sounds pragmatic. If there is a way for me to shoot myself into the foot I'm thankful if you warn me about it.
Would be great if we could set a logging level (?) to make sure information like this isn't shown in production though.
I am not sure that there is a good way to detect such bugs using static analyzer. Just runtime checking that can be disabled in production.
There is one last thing that I want to fix. That is how virtual nodes for components created right now:
vMain({#shownTodos: shownTodos,
#activeCount: activeCount,
#model: model},
key: #main)
and I want to make it like this:
vMain(key: #main,
shownTodos: shownTodos,
activeCount: activeCount,
model: model)
But I think that it will work like 10x times slower in development mode :)
And there will be reserved keywords that can't be used as a component properties: key, id, classes, attributes, styles, children.
I prefer the API of named parameters over using a Map as well. It is syntactically simpler and easier to type and you get better tooling support for autocompletion, refactoring etc.
One advantage of using Maps is that they are easier to construct programmatically. For example you might have a basic configuration that makes sense for a component and you can use addAll
or other ways to alter it.
Unfortunately if I remember correctly Dart doesn't allow to expand a Map to be used as source for named parameters (I think that is possible in Python) which would give us the best of both.
If the API switches to named parameters do we need to define explicitly activeCount
, shownTodos
and model
in the constructor or in a function signature of the component? Or can we just pass them in as named parameters (lightweight prototyping)?
An advantage of React using JS objects is that it is easy to get started when you build a new component, you just add things you need to the object instead of also having to define the same in a function signature or constructor.
I'm not sure where the sweet-spot is here as either way seem to have advantages that aren't possible in the other (lightweight prototyping vs more explicit and robust to maintain and work with).
It seems to be working, updated all examples.
It is necessary to declare properties using @property
annotations. But constructors are simple, just pass context to the super constructor.
Awesome.
Stupid question: If I don't define anything in the body of the constructor do I have to explicitly declare one that is calling super
or can I avoid defining a constructor for the component at all?
I guess what I'm saying is that apparently the constructor of the superclass isn't inherited (probably for good reasons).
Another thing that I've noticed from the examples is that for create
, attached
, update
and so on I have to call super
This requires me to remember that I have to do it and also know where to place the call to super
in the method body.
I'm not entirely sure but I think there is an OOP pattern that helps to avoid this. I thought I've seen it in Polymer
but by looking through the codebase right now I can't find it again (it seems like it also requires to call super
from attached).
I think this is a related description of what I'm trying to say: http://en.wikipedia.org/wiki/Call_super
I'm not sure if the template pattern works here though as it sounds like that it is intended for methods that are required to be implemented whereas in the components they don't have to be implemented (?)
A Martin Fowler article about the template pattern: http://www.martinfowler.com/bliki/CallSuper.html
I also want to know how to get rid of this constructor in Dart :)
It is possible to completely override render
and update
and render component using raw dom, it is useful for components that will implement things like infinite scrolling, etc.
And it is only for advanced use-cases like optimal dom-read, so if someone is overriding them, they should know what they are doing.
create
is just a simple method that creates DivElement
for the root element by default, so if you want to create another element, like SvgElement it isn't necessary to call super
. This one small change to the API solves so many problems that React has.
I am actually don't remember why I didn't want to get rid of super in attached/detached (something with animations), but I thought about them, initially everything looked like in React, but there are several problems in React because of their API.
And this attached
, detached
have slightly different meaning, it is not the same as in Polymer.
When component is detached, it means that it is removed from the virtual dom, semantically detached but its real dom parts can be still in the document, but as a Component author you shouldn't care when this chunk of real dom is used for some cool animation.
In React, attached
and detached
is actually slightly better(if we forget about differences that matter for animation components).
They are split in componentWillMount
/componentDidMount
, same with unmount. But then I've looked at Polymer, and implemented it like it is in Polymer :)
I think that super.attached()
and super.detached()
can be removed, I really don't remember why it is here. Maybe some old problems.
This new API is ~70x times slower(and it still sometimes faster then React), but with transformer it will be possible to generate code that will work with the same speed as before.
Interesting, I don't know the React lifecycle methods and animation use cases well enough yet to understand the implications.
Good to know that it still is in the ballpark of React because that means it should be quite fast still. It can get complicated if the performance characteristics of dev mode and production mode diverge too much (I had some headaches with dev environments where that was the case).
Is there a way to work around having to use reflection & transformers and still achieve the same API or would that be impossible or only possible with a significant performance regression? (also curious if @Emasoft finds an elegant solution for this).
That said, as far as the complexity overhead to maintain liquid isn't getting problematic from the reflection and transformer I think it is ok if it allows for the clean API and decent performance because both are important in production as well as during development.
I think that this dynamic code that is using reflection can be optimized, right now it is implemented as a proof of concept to check if it is even possible to make such api.
It is possible to create API that will use hashmaps everywhere(like in React), but it will have the same terrible performance and it will be very hard to write transformer for such api, right now it is implemented in a way that it shouldn't be so hard to write transformer.
super.attached/detached and constructors are gone.
final vHello = vComponentFactory(Hello);
class Hello extends Component {
@property String name;
build() => vRoot()('Hello $name');
}
what about function / method names?
I am not sure that build
is a good method name for constructing virtual dom. render
is used for the actual rendering when component touches real dom.
attached
/detached
when component is attached/detached in virtual dom.
entered
/left
when root-element of the component is attached/detached in the real dom.
It is probably better to make attached
/detached
to match standard WebComponents behavior, and call them when component is attached/detached in the document, so there won't be any confusion.
I tried it and I liked it (even if it is, unfortunately, non isomorphic), but I think that there should be a simpler syntax to define components. 59 lines to define a checkbox component? Really? This isn't gonna work. Simplification is the only way to make this working.