Jump to content

Alpine.js help


jploch
 Share

Recommended Posts

@kongondo This is a continued conversation from this thread I had with you about using Alpine.js in module development. Thanks again for your help!

I want to refactor a module that is build using jquery with alpine. The main goal is to make the code simpler and more maintainable (It's working fine with jQuery, so not sure if it's worth the effort). Alpine is still very new to me. I feel more compftable working with jQuery, but I can see the benefit of using a reactive framework. Do you think it makes sense to refactor my module at this stage? 

Here is a simplified description of what my module does:
The module is a pagebuilder module based on pagetable that uses the PW API to generate some inputfields. These inputfields are than used to set some css styling on the pagebuilder items/blocks. So when a block is clicked, the inputfields get populatet with data from a JSON object based on the block that was clicked (each block has a unique ID). With jQuery I use a on.change event to listen to changes to the inputs and than save the new value to the corresponding ID inside my JSON object. This code works fine, but is quite long now, and I think it maybe good to use the data binding ability of a reactive framework like alpine.js instead.

With the help of you I was able to listen to the change event and call my save function to update the JSON. Now Iam looking for a way to set the values of the inputfields. The values have to be set when a block is clicked. Hope that makes sense ?

here is my JS code for alpine so far (I added the attributes inside my php template vie PW API):

import * as save from './save';
import Alpine from "alpinejs";

// this is my json object
var jsonData = $('#Inputfield_pgrid_settings').val();
var objData = $.parseJSON(jsonData);

// ALPINE (version 3+)
document.addEventListener("alpine:init", () => {

  console.log('alpine!');

  // this is just for testing, it works to set the value for one field
  // I want a way to get the value of a field based on its name
  Alpine.store("mystore", {
    settingsData: {
      testValue: 22
    },
  })
  Alpine.data("settingsData", () => ({
    cssChange(event) {
      let value = event.target.value;
      let name = event.target.name;
      console.log(name + ': ' + value);
      this.$store.mystore.settingsData.testValue = value;
      // caling a function to save the json
      setStyle(name, value);
    }
  }))
})
Alpine.start();

JSON example:

{
  "items": [{
    "id": "block_editor-4976",
    "cssClass": "block_editor-4976",
    "cssClasses": "new-class",
    "tagName": "div",
    "state": "none",
    "breakpoints": {
      "base": {
        "css": {
          "grid-column-start": "auto",
          "grid-row-start": "auto",
          "grid-column-end": "span 6",
          "grid-row-end": "span 1"
        },
        "size": "@media (min-width: 640px)",
        "name": "base"
      },
      "m": {
        "css": {
          "grid-column-start": "auto",
          "grid-row-start": "auto",
          "grid-column-end": "span 6",
          "grid-row-end": "span 1"
        },
        "size": "@media (max-width: 960px)",
        "name": "m"
      }
    }
  }, {
    "id": "block_editor-4980",
    "cssClass": "block_editor-4980",
    "cssClasses": "new-class",
    "tagName": "div",
    "state": "none",
    "breakpoints": {
      "base": {
        "css": {
          "grid-column-start": "auto",
          "grid-row-start": "auto"
        },
        "size": "@media (min-width: 640px)",
        "name": "base"
      }
    }
  }],
  "breakpointActive": "s",
  "breakpointActiveSize": "@media (max-width: 960px)"
}

 

Link to comment
Share on other sites

Let's start from here:

Quote

EDIT: Still having some trouble figuring things out with Alpine. For example I have JSON data saved inside a PW field. Now I want to use that data to pupulate my inputfields. Looking at your example, I guess I have to use alpine.store for that. How would that work?

No. you don't have to use Alpine.store(). However, store comes with some advantages as explained here and here. I prefer to use the store, maybe due to vuex influence.

16 hours ago, jploch said:

Do you think it makes sense to refactor my module at this stage? 

Not sure. When do you want to release it? Soon? Then I'd release it and refactor it later. A potential challenge here is that you may not find the time 'later' to refactor it ?. Do you have to use Alpine JS? No you don't. Personally though, I will be removing jQuery from everything I've built. It will take time though. This has nothing to do with the new tech such as Alpine really, but I now prefer to use vanilla JS and/or Alpine/Vue, etc. Modern vanilla JS syntax is just clearer to me. Anyway, I digress.

 

16 hours ago, jploch said:

With jQuery I use a on.change event to listen to changes to the inputs and than save the new value to the corresponding ID inside my JSON object. This code works fine, but is quite long now, and I think it maybe good to use the data binding ability of a reactive framework like alpine.js instead.

Using reactive frameworks, you would have at least two options:

#1. model the value: E.g. v-model or x-model in vue and alpine respectively. This two-way data bind means you don't have to listen to the on change event. That will be taken care for you, unless you also want to listen to the event and do something based on that. This approach will also trigger changes in other areas of the app that depend on the changed value. Your data will be changed (instead of the input value) and behind the scenes vue/alpine will change the value of the input for you. Everything stays in sync. By the way, if you want one-way bind, have a look at x-bind:value

#2. on:handler: You handle the event yourself. E.g. with Alpine, @click='doSomethingWithClick' or x-on:click='doSomethingWithClick'. In this case as well, the idea is generally to modify your data based on some event INSTEAD OF  modifying the input value directly based on the event. If you modify the data, you are back to #1, where everything is synced for you.

Using Alpine, you won't need to work with the JSON object. You will be working with JavaScript objects, arrays, bools, etc.

16 hours ago, jploch said:

I was able to listen to the change event and call my save function to update the JSON.

You won't have to do this. If modelling your data (x-model) to inputs, Alpine will update your data for you automatically. Your work will be to validate values and check other app states if required. 

16 hours ago, jploch said:

The values have to be set when a block is clicked.

This is very easy to do with Alpine/Vue. Just use an @click handler and handle that. I could give you a basic example but I would prefer to see your use case/code example as you might not need the click handler at all.

16 hours ago, jploch said:

Now Iam looking for a way to set the values of the inputfields.

As mentioned above, you don't have to do this yourself. Let x-model take care of it for you. When the form is saved, it will save with the modelled values.

E.g. 

<input type='text' name='your_name' x-model='person_name'/>

In the above example, when you type into the input box, Alpine will update the value of person_name to what you input. If you want to see it live as you type, you could do:

<input type='text' name='your_name' x-model='person_name'/>
<p x-text='person_name'></p>

The text inside the paragraph will be synced to and output the current value of 'person_name'.

16 hours ago, jploch said:

Here is a simplified description of what my module does:
The module is a pagebuilder module based on pagetable that uses the PW API to generate some inputfields. These inputfields are than used to set some css styling on the pagebuilder items/blocks. So when a block is clicked, the inputfields get populatet with data from a JSON object based on the block that was clicked (each block has a unique ID).

I would need to see this in action to get my head around how you would model it in Alpine ?.

Data

You have a number of choices to pass your data from ProcessWire to your Alpine app.

$config->js

If your data was static, you could use ProcessWire's config->js to get it populated in the browser. E.g, send the current page ID or the URL to some backend API, etc. You could then access these in your JS code/Alpine as, e.g.

// configs object
const configs = ProcessWire.config.InputfieldPageBuilder

Inline Script

You can have your inputfield, in render(), output inline script that has your data. This is useful if you data is dynamic. You can see this usage in ProcessWire, e.g. in InputfieldRepeater. E.g.

<?php
// e.g. inside ___render()
$data = ['age' => 24, 'gender' => 'female', 'department' => 'design'];
$out = "<p>Some Inputfield Render Output</p>";
$script = "<script>ProcessWire.config.InputfieldPageBuilder = " . json_encode($data) . ';</script>';
$out .= $script;
return $out;

You can then access access the data as in the configs object in JS above.

Inline x-data

Attach your data directly to the x-data value in ProcessWire markup. JSON or 'object' (e.g. a string that resembles a JavaScript object) both work.

<?php

$json = json_encode(['age' => 27, 'gender' => 'female', 'department' => 'ai']);
$out = "<div x-data='{$json}'><p x-text='age'></p></div>";
return $out;

In all of the above, you don't need this:

16 hours ago, jploch said:
var objData = $.parseJSON(jsonData);

Or this:

16 hours ago, jploch said:
var jsonData = $('#Inputfield_pgrid_settings').val();

Is there a reason you are using Alpine as a module here?

16 hours ago, jploch said:
import Alpine from "alpinejs";

 

16 hours ago, jploch said:

JSON example:

I'd probably send this data to the browser using an inline <script> instead of as a value of '#Inputfield_pgrid_settings'. Just makes life easier. I would then set the data as a property in my Alpine.data() or in Alpine.store().

Quick Tips

  1. If using $store inside a ProcessWire module, I create a class property and set my store value to it. Then, I only have to call it like this where I need it: $this->xstore. Saves with typing plus change it once, change it everywhere.
  2. In JS, I use getters and setters, either separately or combined into once function to handle data and store values. Saves from typing this.$store.mystore.settingsData.someValue = 1234, all the time. E.g.
// below could be converted to single getter/setter methods
// could also be adapted to handle nested properties or create separate methods for such
setStoreValue(property, value){
  this.$store.mystore[property] = value;
}

getStoreValue(property){
return this.$store.mystore[property];
}

Hope this helps.

  • Like 3
  • Thanks 1
Link to comment
Share on other sites

@kongondo Wow! Thanks again for taking the time to explain this in detail. It helps me a lot! I have decided to release my module in it's current state first and do a refactor later. I already went down the rabbit hole with my module and refactoring it at this stage will take up too much time. But with your examples I will try to reafactor it at some point. 

On 8/3/2021 at 12:11 PM, kongondo said:

No. you don't have to use Alpine.store(). However, store comes with some advantages as explained here and here. I prefer to use the store, maybe due to vuex influence.

That makes it clearer (I was reading the docs but it was not clear to me, how $store works). So using store is a nice way to acess the data globally across multiple js files: 
"Creating a store with Alpine.store allows you to access global state in your components using a $store property. That same Alpine.store method can be used to retrieve a store in your external scripts."

On 8/3/2021 at 12:11 PM, kongondo said:

This has nothing to do with the new tech such as Alpine really, but I now prefer to use vanilla JS and/or Alpine/Vue, etc. Modern vanilla JS syntax is just clearer to me. Anyway, I digress.

I code most new projects in vanilla js, but since PW uses jquery internally already, I fall into old habbits. Also in my experience, the code with vanilla is often longer and browser compatibility can be an issue as well (Sorry this is a little bit off topic, we don't have to get into this). But I will definitely try going all vanilla js + alpine/vue for new projects, if it makes sense.

On 8/3/2021 at 12:11 PM, kongondo said:

Is there a reason you are using Alpine as a module here?

It's just the way Iam used to work nowadays, installing external plugins/frameworks with node and importing them as js modules and than bundle them with webpack.

On 8/3/2021 at 12:11 PM, kongondo said:

Attach your data directly to the x-data value in ProcessWire markup. JSON or 'object' (e.g. a string that resembles a JavaScript object) both work.

Nice! will try that and get back at you if I hit another road block ?

Also another question, this is more about JS objects/arrays in generall.
With my current data structure is it possible to access an item by ID without looping over it (eg. $store.items[id].value)? I access the value like this now, which is more complicated:

// looking for the values of this ID
let itemId = 'block_editor-4980';

$.each(data['items'], function (idx, item) {
// can access value in here like this
if (itemId == item.id) {
// access values of item
}
});

Thanks again!

Link to comment
Share on other sites

 

On 8/3/2021 at 12:11 PM, kongondo said:
<?php
$json = json_encode(['age' => 27, 'gender' => 'female', 'department' => 'ai']);
$out = "<div x-data='{$json}'><p x-text='age'></p></div>";
return $out;

 

ok I just made a quick test with inlining my data in the x-data attribute. With your simple data object above it works great. But with my nested data (see JSON code above), how would the x-text attribute look if I want to output block_editor-4980.tagname for example. Do I have to structure my object diffrently, to acess the values directly?

Link to comment
Share on other sites

1 hour ago, diogo said:

there's an error in your json.

I shortend the JSON manually and must have messed it up. I have now updated the post above with a code generated version, that validates ?

Link to comment
Share on other sites

6 hours ago, jploch said:

With my current data structure is it possible to access an item by ID without looping over it (eg. $store.items[id].value)? I access the value like this now, which is more complicated:

// looking for the values of this ID
let itemId = 'block_editor-4980';

$.each(data['items'], function (idx, item) {
// can access value in here like this
if (itemId == item.id) {
// access values of item
}
});

You have several possibilities depending on whether you are looping through items or whether you just needed a single item from your items. Here are examples of both scenarios.

Inside a loop

<div>
    <!-- getGridItems here is a function that returns the current grid items -->
<template x-for="(item, index) in getGridItems">
    <!-- bind id to item id and its text to item state -->
<span :id='item.id' x-text='item.state' :key="index"></span>
</template>
</div>

Get One Item

Several possibilities here:

// use filter and since expecting only one item, get it at the first array index
const myItem = items.filter((item) => item.id === itemId)[0];
// loop
let myItem;
for (const item of items) {
	if (item.id === itemId) {
		// you have found your item
		myItem = item;
		// break out of your loop
		break;
	}
}
// find
const myItem = items.find((item) => item.id === itemId);

// do some sanity checks here before using myItem

For more on loops and iteration, have a look at the Mozilla docs

Depending on the app you are building, you might want to keep a caniuse tab open ?.

I see that you are storing tagName (div, etc), meaning you are creating elements on the fly? You can use vanilla JS for this or depending on the number of element possibilities (div, span, ...n), you could use a number of x-ifs. Note, though, that there is no x-else in Alpine.

3 hours ago, jploch said:

how would the x-text attribute look if I want to output block_editor-4980.tagname for example. Do I have to structure my object diffrently, to acess the values directly?

The flatter an object, usually the easier your work. However, sometimes this is not always possible. To access nested data, again, you have several options. You could create a function that returns the object property you want. You can also use either of brackets or dot notation (myItem.breakpoints.base.css.color). However these can get messy if the nesting is very deep. Maybe you could flatten the breakpoints object since really, it is all mainly CSS? It would make it easier to bind styles to an element.

 

  • Like 2
  • Thanks 1
Link to comment
Share on other sites

 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...