gadicc / meteor-famous-views

Famous, the Meteor Way (with Reactive Blaze Templates/Views)
http://famous-views.meteor.com/
GNU Lesser General Public License v3.0
334 stars 27 forks source link

Mixed Mode #254

Open trusktr opened 9 years ago

trusktr commented 9 years ago

In Mixed Mode, things like transforms, align, size, etc, revolve around a RenderNode. Each of those things (align, size, etc) are called "components", which replace Modifiers, let's call them "node components".

Perhaps instead of Modifiers and Surfaces being the primary "Blaze components" of famous-views like it is now, the primary Blaze component can be RenderNode, and instances of each node component to be associated with a RenderNode can be assigned via reactive properties on the Blaze component. For example:

{{>RenderNode dom-element="new DOMElement()" align="alignRef" size="new Size([20,20,20])" position="positionRef" components="arrayOfExtraComponents"}}
{{/RenderNode}}
trusktr commented 9 years ago

Famous-views could be smart enough that if it detect array literals, etc, it can instantiate the respective component:

{{>RenderNode size="[20,20,20]"}}
{{/RenderNode}}

and behind the scenes famous-views does

var Size = require('famous/core/Size')
var instantiatedComponent = new Size(valueFromProperty)
/* Add instantiatedComponent to the node, etc */
gadicc commented 9 years ago

Yes, exactly! I want minimal markup for rapid development :) We'll work out what's required and store references to all the instantiated object for easy access via fview or whatever. I'm also considering something "out there" for sizing, to (more briefly) accommodate all the different possibilities. The API I have in mind for now is:

+FamousContext
  +Node size="A:100,P:0.5,R:-200"
    p Hello

I guess the sizing looks weird but just that sizing alone translates to some 5 lines of code! For simpler cases it would just be size="A:100,100" or whatever. As for HTML, we can automatically set up the DomElement if there is any HTML included inside the template. For Mesch maybe type="mesch", not sure yet :)

Lastly, for cases where no JS is involved, I think we can probably keep backwards compatible with (State)Modifier and Surface to ease the upgrade process for our users.

trusktr commented 9 years ago

I'm not sure I like the idea of backwards compatibility for Modifier and Surface, it seems like more surface area for bugs and more time needed for maintenance. Maybe the docs can state install version XXX or below for Famo.us 0.3.x, and XXX and above for Famo.us Mixed Mode (whatever that version will be).

Maybe

+FamousContext
  +Node size="A:100,P:0.5,R:-200"
    p Hello

can be short for

+FamousContext
  +Node absolute-size="100" relative-size="0.5" render-size="-200"
    p Hello

and of course leaving them of means they take their default values.

By the way, what's the P in A:100,P:0.5,R:-200 stand for?

trusktr commented 9 years ago

Deleted my last comment, nevermind, people will likely mix them to have different types on each axis for sure.

gadicc commented 9 years ago

I hear you. I'll see how easy it is. Just nice for people to be able to upgrade and not have all their old stuff break. (Well, anything that touches famous JS, definitely, but it's quite cool to be able to say that markup will protect you against breakages). We'll see :) It's not a priority but if we can do it easily, I'd like to.

P is proportional :)   guide | docs

I didn't like needing to do so much code for mixes, e.g.

var size = new Size(node);
size.setMode(Node.PROPORTIONAL_SIZE, Node.RENDER_SIZE, Node.ABSOLUTE_SIZE);
size.setProportional(0.5, 0.5, 0.5);
size.setAbsolute(100, 100, 100);

It's not really clear that some of this stuff isn't used. I'd like to just write:

size="P:0.5,R,A:100"

I didn't see the relativeSize before, I guess that will require a little more work (it's parentSize * proportional + differential). Maybe size="R:0.5:-10, A:10, A:10" but I agree that's not clear nor ideal. And decide if we use maybe "RS" for renderSize and "R" for "relative".

We could also accomodate animate="{duration: ... }" as a default for everything and animateSize for size specifically (as a default). Of course the size=reactiveHelper should once again allow returning an object as per bottom of http://famous-views.meteor.com/features/reactivity:

// Reactive getSize() helper
Template.reactivity.helpers({
  getSize: function() {
    return {
      value: [ Session.get('sizeX'), Session.get('sizeY') ],
      transition: { curve: 'easeIn', duration: 1000 },
      halt: true,
      done: function() {
        console.log('getSize transform finished');
      }
    };
  }
});
trusktr commented 9 years ago

I think the usefulness of the unused values would be that you can toggle the size type and then those values will come into effect instantly. I guess it could be useful for toggling between a vertically responsive or horizontally responsive layout, etc.

Based on https://github.com/Famous/engine/blob/master/core/Size.js#L35, would it be size="P:0.5,D:20,A:100" without the R, but with D for differential? We'd also have to consider that P and D can be applied to the same axis while in relative mode. Maybe something like size="P:0.5,D:-10;P:1.2,D:20;A:100" with , to separate multiple size types per axis, and ; to separate the axes. The user could leave off some types to let them to their defaults, like size="D:-10;P:1.2;A:100". And in that case, R for render would make sense, like size="D:-10;P:1.2;R". There might then be a separate property for modes, like size-mode="absolute;relative;render". What do you think about that idea?

It could be interesting if those are reactive, so the values would be strings. We'd have size="'D:-10;P:1.2;R'" and size-mode="'absolute;relative;render'", or size="reactiveSize" and size-mode="reactiveSizeMode" where the reactive variables contain the string values?

Oh! So if reactiveSize is a string, then we assume it's like 'D:-10;P:1.2;R', but yeah, if it's a Size object, then that'd be nice to support too! The dev will manipulate that size directly.

What if instead of strings it accepts an array, like `size="[{D: 10, P: 1.2}, {P: 1.2}, {R: true}]" for X, Y, and Z? Would that be nicer than having to do string manipulation? Is it possible to have a reactive array/object for a component's property like that?

gadicc commented 9 years ago

Morning :) It's all done! Yesterday :) The sizing, I mean... hope to have an early release out by the of today, still have a few other MVP things to do :> But yes, it's all reactive, and manipulating nodes like this is so easy, it's great!

Yeah I didn't think about using ; like that, it's actually quite nice. Only problem I see is user problem where they decide on their own that it's possible to mix all kinds of values that don't make sense and then open bug reports when it doesn't work :) I'll release what I've done so far and see what initial comments are... only problem with current way is to remember for "R", the order of proportional:differential.

Yes, "P" implies D:0 and "D" implies "P:1". Hope that makes sense :) It is possible to use arrays, objects, etc.... helpers can use them directly, but for quoting inside of templates, for static values, it's a pain with properly escaped JSON (all the object names have to be quoted, etc, although I guess we could/should shortcut that anyways). I just like keeping things short and easy to type.

I did think about size-mode, but it gets a bit complicated.... because then the user will need to remember past sizes they've sent in the past. I'm not sure it fits will with the "simple" reactivity model. However, it's no problem to directly act on the size component from within the helper itself too, for more advanced stuff.

Hope that all made sense... should have something out by tonight (my time).

trusktr commented 9 years ago

Yeah, it makes sense! Cool! Yeah, the reason I thought of size-mode was to easily change modes without having to worry about or save size values behind the scenes. Setting size values once, then change size-modes without changing the size values seems like a plausible use case.

For example, instead of changing doing size="A:100,P:0.5,R:-200" then changing it to size="P:1.2,P:0.5,R:-200" (and having to remember the Absolute value behind the scenes if I want to change back), we could just set it all once in advance if we already know what we'll want, then change only the size-mode value, so we'd do something like size="A:100,P:1.2;P:0.5;R:-200" one time, then we can start with size-mode="A;P;R" and change it to size-mode="P;P;R" and then change it back to size-mode="A;P;R".

gadicc commented 9 years ago

We're up! Official announcement on the forums. There's a link to some live examples. Went with ; and , for relative in the end. Actually, spaces and long names are all allowed, so people can do size="absolute: 10; renderSize; proportional: 1" and it will have the same affect as size="A:10; RS; P:1" (or abs, prop, render, whatever... we actually just look at the first letter :)).

Yeah I see what you're saying. I'll definitely consider this once I've had more time to play and see how common this kind of use case is. Either way, as you'll see in the README, you'll still always be able to access the size instance directly and do anything famous style. In the meantime, if you can think of a few examples where this kind of size change would be useful that would be great.

trusktr commented 9 years ago

I gotta dive in. I've been battling with rocket:module. It's tricky to bend around Meteor's build system. Can't wait to dive back into Engine+famous-views!! It's going to be so much better this time around with all the more free time I have.

gadicc commented 9 years ago

amazing! where you at with rocket:module, what are you struggling with in particular?

yeah, meteor's build system is amazingly flexible with some use cases and completely unusable with others. the biggest problem I had is that a package gets built before publish, so even when I'd solved problems with local packages, the method didn't work once it was on atmosphere. there are a number of proposals for the future builder which I think I sent you (let me know if I didn't). i think the only way around this now is by forking the meteor build tool, raix did it if you recall. there's still the issue of only sending the code once and picking the right version, i think raix might have solved the first part.

i actually had another idea for something mad hacky which could just work. i tried to put it into words now but it's a little complex :> let me know where you're at. because it's not something i'd want to pursue if you're getting somewhere.

trusktr commented 9 years ago

Well, the problem I'm having right now is that rocket:module needs to know what packages are installed in an application so that it can do code-splitting to make a bundle of shared libraries. Meteor build plugins run during meteor publish, so at that time there is no app in context to gather info from. The build plugin will also run during app-build-time, but only for app files, not package files (due to Meteor's reproducible build mantra). Packages that aren't local, installed from Atmosphere, are always shipped pre-built without knowledge of the app they run in as far as build steps are concerned. So, rocket:module can't use build plugins in their current form to compile files that are added with api.addFiles into bundles that have dependencies that would be shared with other packages code-splitted out. cosmos:browserify compiles bundles, but if two packages depend on the same NPM dependency, then both bundles will have the same code (possibly two incompatible versions, double singletons, etc).

I can do what I want with local packages, because rocket:module's build plugin will have access to the filesystem of the current app and can gather info like what (local) packages are installed in the app that depend on rocket:module then it can compile accordingly, adding all the bundles of each package to the last file handled of all packages.

When running during meteor publish, rocket:module can't do this. So now, in rocket:module's build plugin I can detect when it runs app-side with this function, but I'm not sure what I'll have to do next. Meteor doesn't have any build hooks (as far as I know, since I haven't read all the source), which would make things very easy, but possibly defeat their reproducible build mantra.

What I know I can do on the app-side build is detect if all packages listed in .meteor/versions already exist in ~/.meteor/packages (system-wide) or in .meteor/local/isopacks (local to the app). If not, I can output a message telling the user to restart meteor after the app has finished starting. This will ensure that all dependencies have been built into one of those two locations. rocket:module's build plugin can add a file to the app to signify that this happens. The next time the app starts, the build plugin can get all the sources (the ones depending on rocket:module will have been skipped and their entry point sources left intact, possibly with some meta comments for rocket:module to look at during this next run)...

... then, this is where I'm stuck in the thought process (I haven't implemented all that since I don't know if it it'll work, I usually wait until I can see the whole solution from start to finish first). Now what? Do I modify the isopack build inline? This is surely not a good idea. Those packages will probably be shipped untouched to the deployed apps, and app-builds don't run on the deployed side of things. Maybe we can get lucky and the same steps can be run during meteor deploy before Meteor actually grabs the files to deploy them? In that case, it might work!!

I got the idea of running app-side and manipulating things from meteorhacks:npm's plugin.

Does that all make sense?

It'd be so cool if package could do something like

api.addFiles(['path/to/file.js'], {
  appSideBuild: true
})

which would tell Meteor to run the build for the package's file during app-build, not during publish-build.

trusktr commented 9 years ago

Aha! And I just read your second paragraph:

the biggest problem I had is that a package gets built before publish, so even when I'd solved problems with local packages, the method didn't work once it was on atmosphere

Exactly! hehe. So, was that other idea you had similar? Put it into words, maybe I can get another idea from it!

trusktr commented 9 years ago

What about wrapping entry points with a function during publish build, so they don't execute, then during app build grabbing all these functions from the detected isopacks (on second run of meteor), stripping the dummy function wrappers, compiling the bundles, and finally writing the bundles to the application itself in some folder we tell the user not to touch (or to a local package like what meteorhacks:npm does with the local npm-container package)??!!?!?!?!!??!!!

hmmmmmmmm....... I think I see a light at the end of the tunnel!! Now, when the user meteor deploys, the bundles will be deployed and the original entry point files won't execute (slight waste of space, but at least it'd work). What do you think?

trusktr commented 9 years ago

Ah, not dummy function wrappers, just commented code! The meteor deploy will strip the comments right!? Wooooo!. I'm going to make this work.

trusktr commented 9 years ago

Wow, I just discovered Plugin.registerBatchHandler!! That might be the thing that lets me do what I want! The proposal says

During the “package compilation” stage, source files that are associated with a “batch handler” extension are not immediately processed by the batch handler

and they are handled during the app-build by a single handler, which solves exactly the problem I am trying to work around!! wow!!

gadicc commented 9 years ago

@trusktr, it's really amazing how in such a short time you've got from being a meteor novice to having such a deep understanding of meteor internals... kudos :)

so yeah, you understand exactly what's going on. and i think, like me, it's just about reaching a point where you think it can work and then writing the code to see if you're right. often while writing the code you reach unforeseen problems, which can either be worked around or are a show stopper. what you wrote sounds ok, but I haven't put as much thought into it as you have and it gets a bit complicated towards the end... but yeah, could be worth pursuing.

... unless Plugin.registerBatchHandler is coming out soon. well done on finding this. I didn't read the whole proposal yet, but if as you/they said, it doesn't bundle at publish time, but rather at app build time, yes, this solves oh but soooo many problems, and avoids the need for very complicated hacks. let's see what he says about time (I know that I, like many other developers, hate that question though :)). I did see him mention there's already a branch for it though, which is very promising. but that doesn't mean it's around the corner.

trusktr commented 9 years ago

hehe, thanks. I just really wanted to make it happen. :blush:

Yep, with Plugin.registerBatchHandler I wouldn't have to write all that hack!! Depending on how long it'll take I'll see if it's worth making the hack so rocket:module works right now, then just rewrite when Plugin.registerBatchHandler comes out. It doesn't seem too complicated now that I have the concept of what to do. It was just a matter of finding and knowing what path to take. Alright, my night is done. I'll get figure out my course of action tomorrow. :D

gadicc commented 9 years ago

Your night is done when I see rocket:module on Atmosphere!

jk :) Night man haha.

trusktr commented 9 years ago

haha. Okay, so I'm going to do some experiments. I'm going to see if Meteor notices if I change isopack code. If Meteor doesn't notice, I might be able to take advantage of this (very hacky) to inject the compiled bundles in place so load order is maintained. This would only be for the meantime until Plugin.registerBatchHandler comes out.

trusktr commented 9 years ago

Alright, I've done some toying around. It seems like you can freely modify isopacks, and Meteor seems to be completely oblivious to it (which is good if you're making an IDE or something and needs to do custom things to the isopacks or something). So I'm making a function that runs once during app-build, adds each entry point found in the isopacks of packages that depend on rocket:module and lists those as entry points for webpack, then webpack compiles them all into a destination folder specified in Webpack. Then I'll simply write each output file back into into its original isopack file, and voila! It's going to work. It will be a small hack similar to a batch plugin, and won't be hard to migrate to that once Plugin.registerBatchPlugin is released. Next I'll handle npm dependencies and code splitting across the bundles.

Wooooooo! Getting closer!!