Closed Duckers closed 7 years ago
I think we should go for a more natural API than this variables arguments syntax (though lacking ES6 we have to lose a few niceties).
For example:
module.get("user").get("things").add("airplane")
You can store this accessors as variables (they look like observables, but are actually just wrappers to modify the backing data)
var user = module.get("user")
user.get("things").insertAt(1, "dog")
I'm not sure get
is the correct name, mabye val
orvar
instead. But the syntax is a lot more readable. I can understand what is happening here.
@COCPORN and myself spent 3 months this spring evaluating different API designs, and concluded that the variable arguments one is the nicest, for various reasons. It is easy to build the API you are proposing (and many other APIs) on top as an abstraction layer. This is the low-level API with the minimal JS->Native API surface and overhead.
For example, with this low-level API it is easy to build a differ-based component system similar to Angular
The propsed API looks totally fine to me :)
I'd prefer to have the path argument be an array rather than leaning on arguments
again. This makes a path a type, simplifies the method signature, and lets a path be referenced instead of done with literals or currying, if that is a design choice I want to make.
I also think it will teach and document better.
module.set(["foo", "bar"], true);
Other than that I think this is golden.
Again, this can be a convenience API on top. I want to keep the base API as minimal as possible
I feel like the varargs based API performs more work in terms of manipulating the arguments
.
I have to agree with @Sunjammer here. Doing it with an array seems a lot less magical.
Candidate implementation found here: https://github.com/fusetools/fuselibs-public/pull/208
I think this looks really weird:
module.removeAt(["foo", "bar", 1])
The array looks very redundant then
I agree, it should be module.removeAt(["foo","bar"], 1)
Well, that messes it up, as 1
is part of the path. That would be very inconsistent
Not if you imagine the path as terminating at an array, removeAt
as a function that operates on arrays, and 1 to be the index at which to remove. Then your statement is the straightforward do X to the item at path Y using N arguments.
This is consistent: At path X, set Y. At path X, remove at index Y. At path X, insert Y at index Z.
Well I'm still in favor of the varargs api.
A rewrite would mean this feature not making it to 1.2 as that deadline is tonight.
There is no issue introducing an "overload" later that accepts an array for path. No difficulties creating a wrapper api on top either.
Yep, that can be easily implemented at a later point as checking if argument 0 is an array anyway
More of a question, but how would you handle a bit more complex structure? Something like this:
exports.list = [
{
userId: 12,
userName: "User 1",
properties: {
permissions: ["read"],
groups: [222, 333, 444]
}
},
{
userId: 15,
userName: "User 2",
properties: {
permissions: ["read", "write", "execute"],
groups: [222, 333, 444]
}
}
];
exports.addGroupToUser() = function(userId, groupId) {
// how would I add group 555 to userId 15?
}
@eksperts :
With the raw API, that would be something like
module.add("list", exports.list.indexof(...), "properties", "groups", groupId)
However, the intention is not for this API to be used much directly. It is meant as a low-level API that we build other features on top.
For example, something like:
var Component = require("FuseJS/Component")
Component(module, function() {
this.list = [ ... ]
this.addGroupToUser = function() {
// Here, simply modify the list, and the component will update automatically through diffing
}
})
Hence, I'm not too worried about what the low-level API looks like, as long as it is lighweight and general purpose.
Note that in the above example, Component
can be implemented as a pure JS utility class
Alright, no objections. I see a point in @Sunjammer's argument, but other than that it's legit!
I just looked at the PR for this and am concered about adding these simple names directly to the module
object. I know it's our object, but seeing sometihng like module.set
and module.add
makes it seem so "standard", yet it's not.
Does this use of exports
prevent anything else from being put in the exports? Can I still add callback functions as normal? Or can I use an OBservable as well?
The examples only go one-level deep with the JS object trees. IS an arbitrary depth intended?
makes it seem so "standard", yet it's not.
I agree, however, rooted modules in <JavaScript>
tags are special, its not vanilla CommonJS, so it doesn't hurt to let it show. The modules have a life time, interact with the data context, can be instantiated multiple times etc. We already have module.disposed
. It is the natural place to put things.
The examples only go one-level deep with the JS object trees. IS an arbitrary depth intended?
Yes
Does this use of exports prevent anything else from being put in the exports? Can I still add callback functions as normal? Or can I use an OBservable as well?
You can use Observables anywhere, like before. No feature regression
I'm not clear on how I share state between modules. I have a list of users that I will need to display in multiple pages. I additionally have shared user objects in that list. They also have to appear on their on other pages. I don't see how this is done in this framework.
I'm not clear on how I share state between modules.
You have to use the dependency injection feature to acheive that:
<JavaScript dep:users="{users}">
This imports the users
list from the data context
This means any structures I wish to store have to be exported at the root of the app. I'm not sure I like that since it makes modularization more difficult.
How do I export part of that structure in another module then, that is share the data?
<JavaScript dep:users="{users}" dep:param="parameter()">
exports.user = users[param.user_id]
This means any structures I wish to store have to be exported at the root of the app.
Not true - you can mount this at any level in the tree.
This makes modularization easier as the data is tree-local. For example, a component can expose local data that is used within its subtree only.
How do I export part of that structure in another module then, that is share the data?
Didn't understand the question
exports.user = users[param.user_id]
Alternatively:
<JavaScript dep:user="{users[parameter().user_id]}">
What I mean is that any value I wish to use in sibling components must be mounted at a higher level.
If I do <JavaScript dep:user="{users[parameter().user_id]}">
will this be a proper two-way binding to this object?
How can I do a similar binding at the JS level?
I'm missing the insight into how one actually structures the data of an app using this system.
Another example, in the apps I've written so far I have something like a user DB stored in memory. You call a JS function getUser(id)
that provides an Observable for that user. I don't see how to create such a function with this new system.
What I mean is that any value I wish to use in sibling components must be mounted at a higher level.
Yes, this creates a natural component-local data sharing scheme. If you want true globals, nothing is stopping you from doing the old :Bundle
and require()
trick.
If I do
will this be a proper two-way binding to this object?
This is a one-way binding, but you will get the actual object and are free to modify it. However, this won't call the reactive module API, so any changes you do remotely won't be reflected. Mutation of imported state should happen through functions exported by the owning module.
How can I do a similar binding at the JS level?
You cannot. This is what the dependency injection system is for.
I'm missing the insight into how one actually structures the data of an app using this system.
We have detailed plans for this. Once this is merged we can write new best practices docs.
Another example, in the apps I've written so far I have something like a user DB stored in memory. You call a JS function getUser(id) that provides an Observable for that user. I don't see how to create such a function with this new system.
You can still provide observables if you wish, but the whole point is to reduce reliance on observables.
In this system you would for example do `
The high level recommended structure is as follows:
<App>
<JavaScript>
// This is the main state container module - like a redux store
// it contains and exports all the app-global state, and exports
// functions for all the possible transactions
exports.things = ["one", "two", "three"]
exports.addThing(thing) {
module.add("things", thing)
}
</JavaScript>
<Navigator>
<Page>
<JavaScript dep:things="{things}" dep:addThing="{addThing}" >
// This module can compute derived state used
// locally on this Page
exports.thingCount = things.length
exports.buttonClicked = function() {
addThing("lol")
}
</JavaScript>
How does one bridge from a require
module into this system? Say I have a user
module that I want several components to have access to. How do I actually structure this sharing?
Try to extend your example to include several modules. Say we have three modules, user
, task
and config
. How would we instantiate all three of these and provide them to a component?
Something like this?
<App>
<JavaScript>
exports.user = require("MyApp/user")
exports.task = require("MyApp/task")
exports.config = require("MyApp/config")
</JavaScript>
<Navigator>
<Page>
<JavaScript dep:user="{user}">
...
The nice thing about this is that now it is easy to mock the environment for the script in the page for testing purposes.
If you want the modules to have access to the data context, you can do:
<JavaScript File="MyApp/user.js" />
<JavaScript File="MyApp/task.js" />
<JavaScript File="MyApp/config.js" />
<Navigator>
<Page>
<JavaScript dep:user="{user}">
...```
Okay, after a long discussion in chat with @mortoray we've decided to redesign this feature slightly.
I will open a new ticket to discuss the new proposed design.
Posted here for discussion:
Reactive modules
A module related with a
<JavaScript>
tag supports the Reactive Modules API. This is a set of methods onmodule
that allows the module to change the exported values and notifying the data context about the change. This allows reactive programming between modules.Each of these APIs accept a
path
to the node in theexports
tree. The path is a sequence of either string keys (to look up into objects) or number indices (to look up into arrays).module.set(... path, value)
Sets the value at the
path
tovalue
. Example:module.add(... path, item)
Adds
item
to the end of the array at thepath
. Example:module.removeAt(... path)
Removes the item at the
path
. The last item in thepath
must be a numeric index into an array. Example:module.insertAt(... path, value)
Inserts
value
at the path. The last item in thepath
must be a numeric index into an array. Example: