Open go4cas opened 3 years ago
Hi, there and thank you for being, as far as I know, the first developer to test Snowblade! In all of my testing, I've been bringing-in Alpine via CDN by including a <script src="...">
in the <head>
of index.html
. As far as organization, there's a couple of methods with which I've been playing in this regard.
If you're using Alpine's magic properties, $event
and $dispatch
to send data between your application's different sections, you can include a <script>
tag or script component in the <head>
of that component's primary entry point.
For example, at this moment, I'm working on an app that provides an interface to "Reports," "Clients," and "Settings." Each of these items is their own component, revealed using Alpine's x-if
property and wrapped with a template
tag by Snowblade's $$wrap
attribute.
In the project directory, they're organized like this:
├── components
│ ├── clients
│ │ ├── clients.html
│ │ ├── clientthumbnail.html
│ │ ├── scripts.html
│ │ └── sidebar.html
│ ├── reports
│ │ ├── reports.html
│ │ ├── reportviewer.html
│ │ ├── scripts.html
│ │ └── sidebar.html
│ └── settings
│ ├── colorscheme.html
│ ├── scripts.html
│ ├── settings.html
│ └── templates.html
└── shared
├── buttons
│ ├── basicbutton.html
│ └── iconbutton.html
└── inputs
├── basicinput.html
├── labeledinput.html
├── onebuttoninput.html
├── search.html
└── twobuttoninput.html
You may notice each section contains an instance of scripts.html
. This is where I'm presently keeping the Alpine logic and any additional helpers. Because Snowblade fulfills explicit imports before implied imports, I can write my component definition for each section like this:
<meta snowblade name="Clients">
<link snowblade href="./scripts.html">
<head>
<Scripts />
</head>
<body>
<!-- ... markup for clients section goes here ... -->
</body>
At runtime, Snowblade will render and then set-aside anything in the <head>
of each component. Right before delivering its final output, these scripts are carried to the <head>
of index.html
, so they're made available to your components before your body is loaded in the browser.
It makes for a very heavy head section, but... who cares? Once it's in the browser, it doesn't really matter. If you'd prefer to keep the scripts in-line throughout the document, you can instead apply your <Scripts />
expression at the end of your component, and completely omit the <head>
and <body>
tags.
If you're using @ryangjchandler's Spruce state-management library to operate a stateful frontend, you can extract your Alpine scripts into completely separate js
files that you then import like regular <script src="...">
tags. This method is quite nice, because I can leverage the capabilities of TypeScript, import Alpine as a dependency, and package it with my app's logic using Rollup. I'm currently playing around with this and trying to find what approach I like best, but I still haven't quite found the workflow I want to use permanently.
As I've started to use Snowblade in production, I've learned many things about what I want out of it and how I want my projects' structures to evolve moving forward. There's been no 100% right answer for me, so as you get a chance to experiment with it, please feel free to share your insights!
Also, a pretty cool new feature and several bugfixes are getting pushed today, so please keep an eye out for that. 🥳
Thanks for the detailed reply, @stephancasas!
Yeah, I've done a proof-of-concept with alpine component logic. Also using Rollup to build and package. So, the release of Snowblade came at the right time.
I'm going to play around a bit more, and I'm sure I'll bugging you soon with more questions.
Keep up the great work!!
PS: Have you ever thought about client side routing in alpine apps? I'm trying with that idea at the moment.
Outstanding. I'd definitely be interested in seeing what structure you wind up adopting. Every time I think I've landed on a decision, I think of another consideration.
Please feel free to keep the questions coming. Having pushed the latest feature, attribute coalescence control, I'm going to put a demo video together and share it with the Alpine community on Discord. With any luck, Snowblade will get some clicks and some additional test users.
Thank you for the kind words!
I'm definitely interested in client-side routing, and had that goal in-mind at the start of this project. As I start to work with Snowblade a bit more in production, this is something with which I'd like to experiment heavily. Any considerations you've had in this regard are of interest!
Thanks again, @stephancasas! So, I finally got some time to work on this again.
This is the draft model I came up with:
My components are organised like this:
├── components
│ ├── my-component1
│ │ ├── index.html
│ │ ├── index.js
│ │ └── store.js
│ ├── my-component2
│ │ ├── index.html
│ │ ├── index.js
│ │ └── store.js
│ └── my-component3
│ │ ├── index.html
│ │ ├── index.js
│ │ └── store.js
└── shared
├── button
│ ├── index.html
│ ├── index.js
│ └── store.js
As you can see, for each component I could have the following:
index.html
: the template of the componentindex.js
: the logic of the componentstore.js
: the local state of the component
My app.js
looks like this:
import { registerComponents, registerStores } from './utils.js'
import * as components from 'glob:./components/**/index.js'
import * as stores from 'glob:./components/**/store.js'
;(async () => { await registerComponents(components) await registerStores(stores) await import('alpinejs') })()
I have some helper methods in `utils.js`:
```javascript
import Spruce from '@ryangjchandler/spruce'
export const buildComponent = (data, methods = {}, init = () => {}) => {
return () => { return { init, ...data, ...methods } }
}
export const registerComponents = components => {
Object.values(components).forEach(component => Object.assign(window, component))
}
export const registerStores = stores => {
for (let store in stores) {
Spruce.store(Object.keys(stores[store])[0], stores[store][Object.keys(stores[store])[0]])
}
}
So, during build time, the packaging will recursively work through all components, and import the logic and component state objects, and presto will sort out the markup.
This allows me to have n-number of nested components in my layout, and I can decided to have any permutation of:
I would really appreciate your feedback on this, and if you could point out any potential issues my approach may have.
Something in the back of my mind is how to effectively package load these ... at the moment all templates and all logic and all state objects will be packaged. What impact would this have on bigger apps, with 100's of components? Is there a way to lazy load?
I really like this idea! I've got a somewhat similar experimental setup going which uses the same forEach()
method to work through bringing in component properties and methods. At the time of writing it, I thought I was crazy, so I'm glad I'm not the only one who had the same idea, haha.
My original concept was a class-based method where each component was its own class and App
was the root class. At runtime, the constructor for App
received an API key (or session token, etc.) as an argument, and then each component/module was initialized.
The constructor for each component received instance
as an argument, which is to be the single instance of the root App
class. As Alpine can only handle (to my knowledge) objects with a null
prototype or the prototype Object
, when each component was done initializing, I used Object.keys()
and Object.getOwnPropertyNames(Object.getPrototypeOf())
to cast each class-based component to an object with null prototype.
After that process was completed, each instance of this
was reassigned to a Spruce store, and reactivity worked at least up to the two levels of nesting that I tested. Without question, this was a whole lot of extra work, but I've gotten far too comfortable with being able to use TypeScript to consider abandoning all of the benefits that using interface
and class
offer.
As of yet, I'm not totally sure on how I'd fractionalize the loading of this. With regards to Presto, lazy loading is something I wanted to have working on day one, but getting things working in a Node environment alone proved to be far more challenging than I'd initially expected.
In my mind, I'd like to be able to either bring in Presto via CDN or have it rolled-up as a dependency. Once it's loaded in the browser, it would listen for DOMNodeInserted
to see if injected markup matched a component to which it had access. If a component name matches, it would render and yield the required markup as described by the app logic.
Right now, Presto uses cheerio to handle DOM traversal and markup injection, so making the jump to jQuery or another selector library for the browser shouldn't be too far of a stretch. However, I'd really like to get away from using either because a good 30% of my time developing was in writing different escape sequences and reordering execution to combat the formatting and logic that cheerio applies. It's almost like Presto should be using an XML library, which may eventually be what happens.
Regarding performance of larger apps, I don't presently have an answer. Presto essentially yields "one giant DOM," with Alpine running the show behind the scenes. I'm currently running one of my client's apps in production in this way, and performance hasn't been an issue thus far.
This kind of development sort of turns the current standard model upside-down in that the HTML becomes the larger payload instead of the bundled app logic. I've reverse-engineered plenty of Angular apps where their logic bundles were gigantic, but the entry point markup was only the body
tag with a <script>
initializing the logic, and thus the eventual markup contained within.
I may be wrong, but there may actually be performance benefits to supplying the markup up-front in that the browser doesn't have to first load the app bundle, and then also render the templates contained within it.
I've got a demo app up on GitHub now which takes a loose approach to what I described above, but I'd really like to see how things can be improved in the ways we've discussed here. Your organization structure is really good in that components are nested in directories that provide for relevant context, and thus messing around with very-specific component file names isn't necessary. In the next couple of weeks, I've got a new project on my desk where I'll be using Presto to do 100% of the component management. As I get to working on this, I'm going to try implementing what you've mentioned as well as some of the things I've tried on my own, too. Throughout that process, I'll share what I find works well!
Thank you for your feedback and patience. The last week has been busy with trying to get the documentation in-shape for a good preview and then for a YouTube demo video. For sure, please keep me updated with any progress you're making!
@stephancasas, here's the first draft of my planned boilerplate: https://github.com/go4cas/wasp-template. Some feedback would be great, please.
This is superb!
Your approach to isolating component markup; logic; and store, then zipping it all up with the helper functions and rolling-up with Vite is excellent. Being completely transparent, I hadn't even thought that far ahead myself!
In the next week, I've got a report due for a client where I think I can leverage your model as the scaffolding. As I work through building it, I'll definitely share any of my findings.
Really keen to give this lib a try! Well done!
I want to also split my alpine js logic, similar to how you do withe the HTML views., i.e. having a folder structure similar to the following:
My questions:
index.js
that is included in the mainindex.html
?