Mixing Alpine.js with 'static' serverside markup, while getting the benefits of binding, etc - alpine.js

I'm new to Alpine and struggling to wrap my head around how to make a scenario like this work:
Let's say I have a serverside built page, that contains some buttons, that represent newsletters, the user can sign up to.
The user might have signed up to some, and we need to indicate that as well, by adding a css-class, .i.e is-signed-up.
The initial serverside markup could be something like this:
<button id='newsletter-1' class='newsletter-signup'>Newsletter 1</button>
<div>some content here...</div>
<button id='newsletter-2' class='newsletter-signup'>Newsletter 2</button>
<div>more content here...</div>
<button id='newsletter-3' class='newsletter-signup'>Newsletter 3</button>
<div>and here...</div>
<button id='newsletter-4' class='newsletter-signup'>Newsletter 4</button>
(When all has loaded, the <button>'s should later allow the user to subscribe or unsubscribe to a newsletter directly, by clicking on one of the buttons, which should toggle the is-signed-up css-class accordingly.)
Anyway, then I fetch some json from an endpoint, that could look like this:
{"newsletters":[
{"newsletter":"newsletter-1"},
{"newsletter":"newsletter-2"},
{"newsletter":"newsletter-4"}
]}
I guess it could look something like this also:
{"newsletters":["newsletter-1", "newsletter-2", "newsletter-4"]}
Or some other structure, but the situation would be, that the user have signed up to newsletter 1, 2 and 4, but not newsletter 3, and we don't know that, until we get the JSON from the endpoint.
(But maybe the first variation is easier to map to a model, I guess...)
Anyway, I would like to do three things:
Make Alpine get the relation between the model and the dom elements with the specific newsletter id (i.e. 'newsletter-2') - even if that exact id doesn't exist in the model.
If the user has signed up to a newsletter, add the is-signed-up css-class to the corresponding <button> to show its status to the user.
Bind to each newsletter-button, so all of them โ€“ not just the ones, the user has signed up to โ€“ listens for a 'click' and update the model accordingly.
I have a notion, that I might need to 'prepare' each newsletter-button beforehand with some Alpine-attributes, like 'x-model='newsletter-2', but I'm still unsure how to bind them together when Alpine has initialising, and I have the data from the endpoint,
How do I go about something like this?
Many thanks in advance! ๐Ÿ˜Š

So our basic task here is to add/remove a specific item to/from a list on a button click. Here I defined two component: the newsletter component using Alpine.data() creates the data (subs array), provides the toggling method (toggle_subscription(which)) and the checking method (is_subscribed(which)) that we can use to set the correct CSS class to a button. It also handles the data fetching in the init() method that executes automatically after the component is initialized. I have also created a save method that we can use to send the subscription list back to the backend.
The second component, subButton with Alpine.bind() is just to make the HTML code more compact and readable. (We can put each attribute from this directly to the buttons.) So on click event it calls the toggle_subscription with the current newsletter's key as the argument to add/remove it. Additionally it binds the bg-red CSS class to the button if the current newsletter is in the list. For that we use the is_subscribed method defined in our main component.
.bg-red {
background-color: Tomato;
}
<script src="https://unpkg.com/alpinejs#3.x.x/dist/cdn.min.js" defer></script>
<div x-data="newsletter">
<button x-bind="subButton('newsletter-1')">Newsletter 1</button>
<button x-bind="subButton('newsletter-2')">Newsletter 2</button>
<button x-bind="subButton('newsletter-3')">Newsletter 3</button>
<button x-bind="subButton('newsletter-4')">Newsletter 4</button>
<div>
<button #click="save">Save</button>
</div>
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('newsletter', () => ({
subs: [],
init() {
// Fetch list of subscribed newsletters from backend
this.subs = ['newsletter-1', 'newsletter-2', 'newsletter-4']
},
toggle_subscription(which) {
if (this.subs.includes(which)) {
this.subs = this.subs.filter(item => item !== which)
}
else {
this.subs.push(which)
}
},
is_subscribed(which) {
return this.subs.includes(which)
},
save() {
// Send this.sub to the backend to save active state.
}
}))
Alpine.bind('subButton', (key) => ({
'#click'() {
this.toggle_subscription(key)
},
':class'() {
return this.is_subscribed(key) && 'bg-red'
}
}))
})
</script>

Related

Get Data back from Vue Component

Is it possible to get Data back from a vue component?
Laravel blade.php code:
...
<div>
<component1></component1>
</div>
...
In component1 is a selectbox which i need (only the selected item/value) in the blade.php
A vue component, when rendered in the browser, is still valid HTML. If you make sure your component is wrapped in a form element and has a valid input element, and the form can be submitted, the PHP endpoint can consume the formโ€™s data without problems. It could look like this:
Layout/view:
<form method="post" action="/blade.php">
<component1></component1>
<button type="submit">Submit form</button>
</form>
Component (<component1/>):
<fieldset>
<input type="checkbox" name="my_option" id="my_option">
<label for="my_option">I have checked this checkbox</label>
</fieldset>
PHP script (blade.php):
echo $_POST["my_option"] // if checked, should print "on"
If you are looking for a JavaScript centered approach, you may want to serialize the form and fetch the endpoint; it could look similar to this:
import serialize from 'form-serialize';
const formData = serialize(form)
fetch(form.action, { method: 'POST' }, body: JSON.stringify(formData) })
.then(response => {
// update page with happy flow
})
.catch(error => {
// update page with unhappy flow
})
Building from an accessible and standardized basis using proper HTML semantics will likely lead to more understandable code and easier enhancements down the road. Good luck!
(Edit: if you require a complete, working solution to your question, you should post more code, both from the Vue app as well as the PHP script.)

Scripts not working on partial view after Ajax call

I have called scripts on _Layout.cshtml page and my Index.cshtml page has partial view into it. So on page load, SignalR scripts working perfect on partial view, on page end I make another ajax request and load the partial view with another data filled in that and embed under already displayed data, and then the SignalR does not work on the newly embedded record.
This is my index page code:
<div class="col-md-6">
<div class="profile-body">
<div class="row infinite-scroll">
#Html.Partial("_AlbumRow", Model)
</div>
</div>
</div>
This is my partial View Code:
#model IEnumerable<SmartKids.Lib.Core.ViewModels.FileMediaAlbumsVM>
#foreach (var item in Model)
{
<div class="widget">
<div class="block rounded">
<img src="#Url.Content(item.ImageUrl)" alt="#item.Title">
<input type="button" data-image-id="#item.imageId" class="btn btn-sm btn-default">Like</input>
</div>
</div>
}
Kindly help me how to resolve this issue that after making an ajax request I am not able to get those SignalR working. Here is more to say when I put the SignalR scripts on PartialView that works but it also sucks that on each ajax request there is again SignalR loaded on the page and when I click on LIke button it makes many calls to the function behind it.
Kindly help me to resolve this issue, I am stuck at this point since 1 week.
Here is signalR Code:
$(".btn.btn-sm.btn-default").on("click", function () {
var imageId = $(this).attr("data-image-id");
albumClient.server.like(imageId);
});
Problem: You are binding event to elements directly, So when you remove this element and replace it with a different one the events are also removed along with that element, This is something like strongly coupled.
Solution: Use Jquery event delegation. This will make sure the events will be triggered on the current elements and also all the elements that can come in future.
syntax is as below.
$(document).on("click", ".btn.btn-sm.btn-default",function () {
var imageId = $(this).attr("data-image-id");
albumClient.server.like(iamgeId);
});
NOTE: This was never a singlaR issue, it was Jquery issue.
Efficient Way: The problem in using $(document).on("click"... is that when ever there is a click happening on the entire page the Jquery framework will bubble the events from the clicked element upwards(its parent, and its parent and so on..) unless the element specified in the selector arrives, So its kind of performance hit as we don't want this check's to run if we are clicking outside the required area ( button .btn.btn-sm.btn-default in this example).
So best practice is to bind this event delegation to the closest parent possible which will not be removed, <div class="row infinite-scroll"> in this question. So that only when the click happens within this element the event bubbling will happen and also will be stopped once it reaches the parent element,it acts kind of a boundary for event bubbling.
$('.row.infinite-scroll').on("click", ".btn.btn-sm.btn-default",function () {
var imageId = $(this).attr("data-image-id");
albumClient.server.like(iamgeId);
});

Ember. How to delete a view's instance

Im struggling to delete a view's instance. On view hbs i use each loop to show view hbs. On another field click i add a " . " to a json object, which then adds another field to the template.
>js>App.ApplicationView = Ember.View.extend({
anotherField: [{name: 'testname'}],
actions: {
moreFields: function(){
this.get('anotherField').pushObject({name: ''});
},
less: function(){
var counter = this.get('anotherField');
counter.shift();
this.set('anotherField', counter);
And hbs
{{#each view.anotherField}}
{{view Ember.TextField}}
{{/each}}
<button {{action 'moreFields' target='view'}}> ++ </button>
<button {{action 'less' target='view'}}> -- </button>
http://jsbin.com/iSUdiCaX/17/edit
Cheers
Kristjan
When you use the shift method Ember doesn't get notified that the anotherField property changed, and therefore it doesn't update the template. You can check this by adding this.rerender() at the end of the less action.
You could:
call this.propertyDidChange('anotherField') to notify the property changed: http://emberjs.com/api/classes/Ember.Object.html#method_propertyDidChange
use the slice method: http://emberjs.com/api/classes/Ember.Array.html#method_slice
var sliced = this.get('anotherField').slice(0, this.get('anotherField').length - 1);
this.set('anotherField' sliced);
I also noticed you're using the View to handle the actions whereas I believe the Controller would be a better place to do so.
EDIT
Well it depends.... I believe the controllers are a good place because they have knowledge of the model (the view also has it via the controller). if your anotherField property is only needed for displaying or event handling logic then I believe it is a good idea to leave it in the view. from docs
Views in Ember.js are typically only created for the following
reasons:
When you need sophisticated handling of user events
When you want to create a re-usable component
But if instead the anotherField property is used the held application state (user selections, needed for computed properties or other actions) then I believe it's better placed inside the controller (and therefore the actions modifying it).
Have in mind your view can handle one part of the action and send it to the controller:
actions: {
something: function() {
.....
this.get('controller').send('something') // calls send action in controller
}
}
I hope this helps!

Understanding Ember Views

I'm looking for some clarification on views in Ember.js
Coming from a rails background and I'm trying to ignore any preconceptions. From what I understand of the ember framework there are 5 components:
Routes: This is where we define the state of the application. The state is reflected in the URL. We can also define data loading here. Route classes are defined and on startup ember creates route objects which last for the duration of the application.
Models: This is where object data is defined. Can also define computed properties. A model object is created for each json object returned from the server.
Controllers: This mediates interactions between the models and templates/views. Controller classes are defined and on startup ember creates controller objects which last for the duration of the application. There is only ever a single instance of each controller class.
Templates: These describe the generated markup.
Views: These are specific templates or dom elements relating to a model. These are used to define interface events and send them to the controller for handling. Not sure when to create these.
As an example lets say I have a EventsController that has data loaded on the applicationRoute:
ScheduleApp.EventsController = Ember.ArrayController.extend();
ScheduleApp.ApplicationRoute = Ember.Route.extend({
setupController: function() {
this.controllerFor('events').set('content', ScheduleApp.Event.find());
}
});
Now in my template instead of iterating over each and displaying the information I want to iterate over each and create an associated view so I can add interactions to each event. I presume I would need to create a new view for each event and have it display in my template. However, I'm not sure where I create these views. Do I define a view class and then ember will create a new view object each time I call it using the view helper? Eventually I would like to use the appendTo on the view to inject my events to different places in the dom. Where would this be defined?
I've tried reading over the ember.js guide for views but it describes the context of a creating a single view. I think I want to make many views for each event and then dynamically interact with those objects.
Up to now ember has been outrageously clever so I would assume there is a built in method for generating these views. After all, most user interfaces are full of lists that require interactions. The problem is the list I'm trying to make I then want to spread over the dom depending on its attributes.
As per your code, App.EventsController has a list of events, now let us say we want the UI to have a list of events displayed and for each event say we want the view to have a delete button which deletes the event when the user clicks
One way to accomplish is by using Ember.CollectionView, the collection view as the name suggests is tailored for these sort of requirements, in many Ember examples the usage of view is not defined because ember auto-generates it for you but in some cases we might need to explicitly define a view to meed our requirements
App.EventsView = Ember.CollectionView.extend({
// It needs a list of data to iterate upon
// We are binding it to the controllers content data which
// is a list of events
contentBinding: "controller.content",
appendSpan: function(){
view = Ember.View.create({tagName: 'span'});
this.get("childViews").forEach(function(child){
view.appendTo(child);
});
},
// Now we need to also define a view template that will be
// generated for all the elements in the content array
// This could in turn be another collection view if required
// I am going to keep it simple for now
itemViewClass: Ember.View.extend({
templateName: "event",
deleteEvent: function(){
// Implement Delete
this.get("content").deleteRecord();
},
notifyUser: function(){
// The record doesn't get deleted as soon as user clicks, the request goes to
// server and server deletes the record and sends back the response to the
// client, Hence I'm using an observer here on isDeleted property of the record
if(this.get('content.isDeleted')){
alert("Record deleted Successfully");
}
}.observes('content.isDeleted')
})
})
Important Note Inside the CollectionView definition this.get("content") refers to the array of events, while in itemViewClass this.get("content") refers to the single event object
//Defining the template
//Assuming the event model has a name property
<script type="text/x-handlebars" data-template-name="event">
Name: {{view.content.name}}
<a {{action deleteEvent target="view"}} href="#">Delete Event</a>
</script>
Now when you hit your application_url/events
you'll a list of events each event has a delete button, I hope this clears some concepts
For more information about CollectionView
Update as per the comment:
If you want to append another view to each child view, you can do so by editing the template of itemViewClass as follows
<script type="text/x-handlebars" data-template-name="event">
Name: {{view.content.name}}
<a {{action deleteEvent target="view"}} href="#">Delete Event</a>
{{ view App.SomeOtherView }}
</script>
it can be a partial too as follows
<script type="text/x-handlebars" data-template-name="event">
Name: {{view.content.name}}
<a {{action deleteEvent target="view"}} href="#">Delete Event</a>
{{ partial "somePartial" }}
</script>
or if you want to do it programmatically say, you click a button in the EventsView template and on click all the childs view must have a span tag appended to it(I am very bad at giving examples)
//This is the template for App.EventsController,
//template-name = "events" => view is App.EventsView and controller App.EventsController
<script type="text/x-handlebars" data-template-name="events">
<a {{action appendSpan target="view"}} href="#"> Append </a>
</script>
appendSpan is defined in the CollectionView

How to update a label from a postback in MVC3/Razor

MVC/Razor/Javascript newbie question:
I have a MVC3/Razor form where the use can select a single product from a drop down list.
<div class="editor-label">
Product
</div>
<div class="editor-field">
#Html.DropDownList("ProductID", (IEnumerable<SelectListItem>)ViewBag.Products, "--Select One--")
#Html.ValidationMessageFor(model => model.ProductID)
</div>
What I then want is to display the price of the selected product on a label just below the drop down list (model property name is Amount).
This should be pretty easy, but I am pretty new at Razor, and know almost nothing about Javascript, so I would appreciate any verbose explanations of how do do it, and how it all hangs together.
Add a div/span under the Dropdown .
#Html.DropDownList("ProductID", (IEnumerable<SelectListItem>)ViewBag.Products, "--Select One--")
<div id="itemPrice"></div>
and in your Script, make an ajax call to one of your controller action where you return the price.
$(function(){
$("#ProductId").change(function(){
var val=$(this).val();
$("#itemPrice").load("#Url.Action("GetPrice","Product")", { itemId : val });
});
});
and have a controller action like this in your Product controller
public string GetPrice(int itemId)
{
decimal itemPrice=0.0M;
//using the Id, get the price of the product from your data layer and set that to itemPrice variable.
return itemPrice.ToString();
}
That is it ! Make sure you have jQuery loaded in your page and this will work fine.
EDIT : Include this line in your page to load jQuery library ( If it is not already loaded),
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
The Amount isn't available to your view when the user selects a product (remember the page is rendered on the server, but actually executes on the client; your model isn't available in the page on the client-side). So you would either have to render in a JavaScript array that contains a lookup of the amount based on the product which gets passed down to the client (so it's available via client-side JavaScript), or you would have to make a callback to the server to retrieve this information.
I would use jQuery to do this.
Here's a simple example of what the jQuery/Javascript code might look like if you used an array.
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script>
$(document).ready(function() {
// This code can easily be built up server side as a string, then
// embedded here using #Html.Raw(Model.NameOfPropertyWithString)
var list = new Array();
list[0] = "";
list[1] = "$1.00";
list[2] = "$1.25";
$("#ProductID").change(displayAmount).keypress(displayAmount);
function displayAmount() {
var amount = list[($(this).prop('selectedIndex'))];
$("#amount").html(amount);
}
});
</script>
<select id="ProductID" name="ProductID">
<option value="" selected>-- Select --</option>
<option value="1">First</option>
<option value="2">Second</option>
</select>
<div id="amount"></div>
You'll want to spend some time looking at the docs for jQuery. You'll end up using it quite a bit. The code basically "selects" the dropdown and attaches handlers to the change and keypress events. When they fire, it calls the displayAmount function. displayAmount() retrieves the selected index, then grabs the value out of the list. Finally it sets the HTML to the amount retrieved.
Instead of the local array, you could call your controller. You would create an action (method) on your controller that returned the value as a JsonResult. You would do a callback using jquery.ajax(). Do some searching here and the jQuery site, I'm sure you'll find a ton of examples on how to do this.

Resources