stephancasas / presto

HTML componentization, specialized for Alpine JS.
MIT License
26 stars 0 forks source link

[QUESTION] How to include alpine scripts? #1

Open go4cas opened 3 years ago

go4cas commented 3 years ago

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:

src
|--components
|  |--my-content
|     |--index.html
|     |--index.js
|  |--my-footer
|     |--index.html
|     |--index.js
|  |--my-header
|     |--index.html
|     |--index.js
|--index.html   (this is the main entry point)

My questions:

  1. How would you suggest that I include the individual scripts with their views?
  2. How would I import alpine.js? Should I have a operate index.js that is included in the main index.html?
stephancasas commented 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.

Event-based Logic

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.

State-based Logic

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. 🥳

go4cas commented 3 years ago

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.

stephancasas commented 3 years ago

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!

go4cas commented 3 years ago

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:

;(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?

stephancasas commented 3 years ago

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!

go4cas commented 3 years ago

@stephancasas, here's the first draft of my planned boilerplate: https://github.com/go4cas/wasp-template. Some feedback would be great, please.

stephancasas commented 3 years ago

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.