im trying to pass variables from Twig to a JS File to work with them.
No matter what I do the console log is always an empty array.
But that array is not empty as the dump shows.
What am I doing wrong?
PHP:
$user = $this->getUser();
$markers = $doctrine->getRepository(Marker::class)->findBy(["relatedUser" => $user]);
return $this->render("map/index.html.twig", [
"controller_name" => "MapController",
"title" => "Map",
"markers" => $markers
]);
HTML:
{% block javascripts %}
<script>
var markers = '{{ markers|json_encode }}';
</script>
{{ encore_entry_script_tags('mapLogic') }}
{% endblock %}
Javascript:
document.addEventListener('DOMContentLoaded', () => {
console.log(JSON.parse(window.markers));
});
Web Console:
Dump:
Here is the entire code from my Marker Entity.
But it does not implement JsonSerializabe:
<?php
namespace App\Entity;
use App\Repository\MarkerRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: MarkerRepository::class)]
class Marker
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'markers')]
#[ORM\JoinColumn(nullable: false)]
private ?User $relatedUser = null;
#[ORM\Column]
private array $geoData = [];
#[ORM\Column]
private ?\DateTimeImmutable $createdAt = null;
public function getId(): ?int
{
return $this->id;
}
public function getRelatedUser(): ?User
{
return $this->relatedUser;
}
public function setRelatedUser(?User $relatedUser): self
{
$this->relatedUser = $relatedUser;
return $this;
}
public function getGeoData(): array
{
return $this->geoData;
}
public function setGeoData(array $geoData): self
{
$this->geoData = $geoData;
return $this;
}
public function getCreatedAt(): ?\DateTimeImmutable
{
return $this->createdAt;
}
public function setCreatedAt(\DateTimeImmutable $createdAt): self
{
$this->createdAt = $createdAt;
return $this;
}
}
All the properties in your Marker entity are set to private, which means that they will be omitted when you encode it using json_encode().
One way of solving it is to set all properties as public in that entity.
If you don't want to do that, there are another (and in my opinion, better) alternative. You can implement the JsonSerializable interface. Then you can decide what to serialize.
// Import the interface to the current namespace
use JsonSerializable;
// Now implement the interface
class Marker implements JsonSerializable
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'markers')]
#[ORM\JoinColumn(nullable: false)]
private ?User $relatedUser = null;
#[ORM\Column]
private array $geoData = [];
#[ORM\Column]
private ?\DateTimeImmutable $createdAt = null;
// Now we need to implement the method defined in the interface.
public function jsonSerialize() {
// All you need to do here is to return the data you want to encode
// in the format you want.
return [
'id' => $this->id,
'relatedUser' => $this->relatedUser,
'geoData' => $this->geoData,
'createdAt' => $this->createdAt,
];
}
// The rest of your object
Now it should return some actual data.
The manual for JsonSerializable: https://www.php.net/manual/en/class.jsonserializable.php
The manual for jsonSerialize(): https://www.php.net/manual/en/jsonserializable.jsonserialize.php
Related
I create a class in js which create a MySQL model like this :
class Model {
constructor(options = {}, table) {
this.options = options;
this.table = table;
this.create();
}
create() {
let queryString = `INSERT INTO ${this.table}`;
let fieldsString = ``;
let valuesString = ``;
for (let prop in this.options) {
fieldsString += `${prop},`;
valuesString += `${this.options[prop]},`;
//console.log(prop, this.options[prop]);
}
fieldsString = fieldsString.slice(0, -1);
valuesString = valuesString.slice(0, -1);
queryString = `${queryString} (${fieldsString}) VALUES (${valuesString})`;
console.log(queryString);
}
}
class UsersModel extends Model {
constructor(options = {}, table) {
super(options, table);
this.table = "users";
}
}
const u1 = new UsersModel({
username: "test",
mail: "darya",
});
when I run the constructor variable queryString looks like this: INSERT INTO undefined (username, mail) VALUES (test, Darya) why this.table is undefined? what I missed?
I will appreciate any help!
Because you first call is create (in super) and only after you set this.table.
class Model {
constructor(table) {
// table is undefined, call create..
this.table = table;
this.create();
}
create() {
let queryString = `INSERT INTO ${this.table}`;
console.log(queryString);
}
}
class UsersModel extends Model {
constructor(table) {
// table is undefined, call super..
super(table);
this.table = "users";
// here you already have table name, so create works with it
this.create();
}
}
new UsersModel();
I would remove the table parameter from the UsersModel constructor, because you aren't passing it in. It also shouldn't change, a table name will usually not be dynamic. Here's what I would change it to:
class UsersModel extends Model {
constructor(options) {
super(options, "users")
}
}
I also chose not to make the options parameter optional (pun not intended). I would even recommend taking the username and mail as separate parameters, then passing them down as an object to the base Model constructor.
I am loading images from Wikipedia into a Grid view. For the most part this is working correctly. Because there could possible be up to 200 or more images being loaded I am try to run it in a new thread. I see a definite delay when scrolling from my Album tab to the Artist tab that is loading the images. I am also see some lag as images are still getting load while scrolling up and down the list. Also when I scroll back to the top of the list place holders that previously occupied by the default image because I am unable to get an image from Wikipedia are now occupied by images from another artist.
When I scroll back to the song list and then back to the artist list the view is reset but it still has a lot of delay when going into the artist tab.
This image is what the screen looks like when first entering the Artist tab.
This image is what the screen looks like after scrolling to the bottom of the list and back to the top.
As you can see the <unknow. and AJR have had their default image replaced.
Here is my code that I am calling to load the images from Wikipedia.
#Override
public void onBindViewHolder(#NonNull ARV holder, int position) {
Artist artist = artistList.get(position);
if(artist!=null) {
holder.artistName.setText(artist.artistName);
String bandName = artist.artistName;
bandName = bandName.replace(' ','_');
try {
String imageUrl = cutImg(getUrlSource("https://en.wikipedia.org/w/api.php?action=query&titles="+bandName+"&prop=pageimages&format=json&pithumbsize=250"));
URL url = new URL(imageUrl);
ImageLoader.getInstance().displayImage(imageUrl, holder.artistImage,
new DisplayImageOptions.Builder().cacheInMemory(true).showImageOnLoading(R.drawable.album)
.resetViewBeforeLoading(true).build());
} catch (IOException e) {
e.printStackTrace();
}
/*ImageLoader.getInstance().displayImage(getCoverArtPath(context,artist.id),holder.artistImage,
new DisplayImageOptions.Builder().cacheInMemory(true).showImageOnLoading(R.drawable.album)
.resetViewBeforeLoading(false).build());*/
}
}
private StringBuilder getUrlSource(String site) throws IOException {
StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
StrictMode.setThreadPolicy(policy);
URL localUrl = null;
localUrl = new URL(site);
URLConnection conn = localUrl.openConnection();
BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream()));
String line = "";
String html;
StringBuilder ma = new StringBuilder();
while ((line = reader.readLine()) != null) {
ma.append(line);
Log.i(ContentValues.TAG, "StringBuilder " + ma);
}
Log.i(ContentValues.TAG, "Final StringBuilder " + ma);
return ma;
}
public static String cutImg(StringBuilder split){
int start=split.indexOf("\"source\":")+new String("\"source\":\"").length();
split.delete(0, start);
split.delete(split.indexOf("\""), split.length());
Log.i(ContentValues.TAG, "StringBuilder " + split);
return split.toString();
}
Here is the code that is call the Artist Fragment.
public class ArtistFragment extends Fragment {
int spanCount = 3; // 2 columns
int spacing = 20; // 20px
boolean includeEdge = true;
private RecyclerView recyclerView;
private ArtistAdapter adapter;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_artist, container, false);
recyclerView = view.findViewById(R.id.artistFragment);
recyclerView.setLayoutManager(new GridLayoutManager(getActivity(), 3));
Thread t = new Thread()
{
public void run()
{
// put whatever code you want to run inside the thread here.
new LoadData().execute("");
}
};
t.start();
return view;
}
public class LoadData extends AsyncTask<String, Void, String> {
#Override
protected String doInBackground(String... strings) {
if(getActivity()!=null) {
adapter=new ArtistAdapter(getActivity(),new ArtistLoader().artistList(getActivity()));
}
return "Executed";
}
#Override
protected void onPostExecute(String s) {
recyclerView.setAdapter(adapter);
if(getActivity()!=null) {
recyclerView.addItemDecoration(new GridSpacingItemDecoration(spanCount, spacing, includeEdge));
}
}
#Override
protected void onPreExecute() {
super.onPreExecute();
}
}
}
I have also tried this using Picasso using the following code:
bandName = artist.artistName;
bandName = bandName.replace(' ','_');
try {
String imageUrl = cutImg(getUrlSource("https://en.wikipedia.org/w/api.php?action=query&titles="+bandName+"&prop=pageimages&format=json&pithumbsize=250"));
URL url = new URL(imageUrl);
Picasso.get().load(imageUrl).placeholder(R.drawable.album)
.error(R.drawable.artistdefault).into(holder.artistImage);
} catch (IOException e) {
e.printStackTrace();
}
The results are pretty much the same as when I used Android-Universal-Image-Loader. I have been try for several days to fix this, I have tried several different examples that I found on Stack overflow but none of them seem to resolve the issues I am seeing. I am hoping that someone will be able to identify what I am doing incorrectly.
Thanks in advance.
ArtistFragmentconverted to Kotlin
class ArtistFragment : Fragment() {
var spanCount = 3 // 2 columns
var spacing = 20 // 20px
var includeEdge = true
var retrofit: Retrofit? = null
var wikiService: WikiService? = null
var adapter: ArtistAdapter? = null
private var recyclerView: RecyclerView? = null
private var viewModelJob = Job()
private val viewModelScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private var progress_view: ProgressBar? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_artist, container, false)
recyclerView = view.findViewById(R.id.artistFragment)
recyclerView?.setLayoutManager(GridLayoutManager(activity, 3))
progress_view = view.findViewById(R.id.progress_view)
initWikiService()
initList()
//LoadData().execute("")
return view
}
override fun onDestroy() {
super.onDestroy()
viewModelJob.cancel()
}
private fun initList() {
recyclerView!!.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, includeEdge))
adapter = ArtistAdapter(this)
adapter?.items = ArrayList()
adapter?.listener = this
recyclerView?.adapter = adapter
viewModelScope.launch {
progress_view.visibility = View.VISIBLE
val wikiPages = getWikiPages()
adapter?.items = wikiPages
progress_view?.visibility = View.GONE
}
}
private suspend fun getWikiPages(): ArrayList<Artist> {
val newItems = ArrayList<Artist>()
withContext(Dispatchers.IO) {
ArtistData.artists.map { artist ->
async { wikiService?.getWikiData(artist) }
}.awaitAll().forEach { response ->
val pages = response?.body()?.query?.pages
pages?.let {
for (page in pages) {
val value = page.value
val id = value.pageid?.toLong() ?: value.title.hashCode().toLong()
val title = value.title ?: "Unknown"
val url = value.thumbnail?.source
newItems.add(Artist(id, title, albumCount = 0, songCount = 0, artistUrl = url!!))
}
}
}
}
return newItems
}
private fun initWikiService() {
retrofit = Retrofit.Builder()
.baseUrl("https://en.wikipedia.org/")
.addConverterFactory(GsonConverterFactory.create())
.build()
wikiService = retrofit?.create(WikiService::class.java)
}
I believe I have resolved most of the issues I was previously seeing I am now down to the following problems:
Artist.item.map { artist -> - Not sure how this should be called, Unresolved reference: item
}.awaitAll().forEach { response -> = forEach is telling me Overload resolution ambiguity. All these functions match.
public inline fun Iterable<TypeVariable(T)>.forEach(action: (TypeVariable(T)) → Unit): Unit defined in kotlin.collections
public inline fun <K, V> Map<out TypeVariable(K), TypeVariable(V)>.forEach(action: (Map.Entry<TypeVariable(K), TypeVariable(V)>) → Unit): Unit defined in kotlin.collections
newItems.add(Artist(id, title, url)) - I know that the variables for the Artist Model need to go here, but when I put them there they are unresolved.
I have reworked the ArtistAdapter not sure if it is correct though.
class ArtistAdapter(private val context: ArtistFragment, private val artistList: List<Artist>?) : RecyclerView.Adapter<ArtistAdapter.ARV>() {
private var dimension: Int = 64
init {
val density = context.resources.displayMetrics.density
dimension = (density * 64).toInt()
hasStableIds()
}
var items: MutableList<Artist> = ArrayList()
set(value) {
field = value
notifyDataSetChanged()
}
var listener: Listener? = null
interface Listener {
fun onItemClicked(item: Artist)
abstract fun ArtistAdapter(context: ArtistFragment): ArtistAdapter
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ARV {
return ARV(LayoutInflater.from(parent.context).inflate(R.layout.artist_gride_item, parent,
false))
}
override fun onBindViewHolder(holder: ARV, position: Int) {
holder.onBind(getItem(position))
}
private fun getItem(position: Int): Artist = items[position]
override fun getItemId(position: Int): Long = items[position].id
override fun getItemCount(): Int {
return artistList?.size ?: 0
}
inner class ARV(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
private val artistNameView: TextView = itemView.findViewById(R.id.artistName)
private val artistAlbumArtView: SquareCellView = itemView.findViewById(R.id.artistAlbumArt)
fun onBind(item: Artist) {
artistNameView.text=item.artistName
if(item.artistURL!=null) {
Picasso.get()
.load(item.artistURL)
.resize(dimension, dimension)
.centerCrop()
.error(R.drawable.artistdefualt)
.into(artistAlbumArtView)
} else {
artistAlbumArtView.setImageResource(R.drawable.artistdefualt)
}
itemView.setOnClickListener(this)
}
override fun onClick(view: View) {
val artistId = artistList!![bindingAdapterPosition].id
val fragmentManager = (context as AppCompatActivity).supportFragmentManager
val transaction = fragmentManager.beginTransaction()
val fragment: Fragment
transaction.setCustomAnimations(R.anim.layout_fad_in, R.anim.layout_fad_out,
R.anim.layout_fad_in, R.anim.layout_fad_out)
fragment = ArtistDetailsFragment.newInstance(artistId)
transaction.hide(context.supportFragmentManager
.findFragmentById(R.id.main_container)!!)
transaction.add(R.id.main_container, fragment)
transaction.addToBackStack(null).commit()
}
}
}
Logcat Snippet
java.lang.NoSuchMethodError: No static method metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; in class Ljava/lang/invoke/LambdaMetafactory; or it
s super classes (declaration of 'java.lang.invoke.LambdaMetafactory' appears in /apex/com.android.art/javalib/core-oj.jar)
at okhttp3.internal.Util.<clinit>(Util.java:87)
at okhttp3.internal.Util.skipLeadingAsciiWhitespace(Util.java:321)
at okhttp3.HttpUrl$Builder.parse(HttpUrl.java:1313)
at okhttp3.HttpUrl.get(HttpUrl.java:917)
at retrofit2.Retrofit$Builder.baseUrl(Retrofit.java:506)
at com.rvogl.androidaudioplayer.fragments.ArtistFragment.initWikiService(ArtistFragment.kt:103)
at com.rvogl.androidaudioplayer.fragments.ArtistFragment.onCreateView(ArtistFragment.kt:43)
It looks to me as a known bug with Picasso.
Try to load default image manually so it won't be replaced with cached one.
Update 14.10.20:
I think the main problem is that you load network content in adapter in rather ineffective way. I suggest to form a list of all urls at first, leaving only image load in adapter.
Also reccomend you to use rerofit2 for network calls and something for async work instead of AsyncTask: rxJava, courutines, flow etc.
I created a sample project to load data async using retrofit2+coroutines.
In activity:
private val viewModelScope = CoroutineScope(Dispatchers.Main)
private fun initWikiService() {
retrofit = Retrofit.Builder()
.baseUrl("https://en.wikipedia.org/")
.addConverterFactory(GsonConverterFactory.create())
.build()
wikiService = retrofit?.create(WikiService::class.java)
}
private fun initList() {
viewModelScope.launch {
val wikiPages = getWikiPages()
adapter?.items = wikiPages
}
}
private val viewModelScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private suspend fun getWikiPages(): ArrayList<Item> {
val newItems = ArrayList<Item>()
withContext(IO) {
ArtistData.artists.map { artist ->
async { wikiService?.getWikiData(artist) }
}.awaitAll().forEach { response ->
val pages = response?.body()?.query?.pages
pages?.let {
for (page in pages) {
val value = page.value
val id = value.pageid?.toLong() ?: value.title.hashCode().toLong()
val title = value.title ?: "Unknown"
val url = value.thumbnail?.source
newItems.add(Item(id, title, url))
}
}
}
}
return newItems
}
In viewHolder:
fun onBind(item: Item) {
if (item.url != null) {
Picasso.get()
.load(item.url)
.resize(dimension, dimension)
.centerCrop()
.error(R.drawable.ic_baseline_broken_image_24)
.into(pictureView)
} else {
pictureView.setImageResource(R.drawable.ic_baseline_image_24)
}
}
In adapter: add hasStableIds() to constructor and override getItemId method:
init {
hasStableIds()
}
override fun getItemId(position: Int): Long = items[position].id
Retrofit Service:
interface WikiService {
#GET("/w/api.php?action=query&prop=pageimages&format=json&pithumbsize=250")
suspend fun getWikiData(#Query("titles") band: String): Response<WikipediaResponse?>
}
How can I run some Javascript functions in a Blazor app, without using the JS interop or jQuery? Just plain old Javascript functions that interact with the DOM, independently of Blazor.
I added my script right before the closing </body> tag:
<script src="app.js"></script>
And in app.js I have the following:
var elements = document.querySelectorAll(".some-element");
elements.forEach(function (element) {
element.addEventListener("click", (e) => {
alert("Hello");
});
});
Of course the selector finds no element. I'm guessing they aren't yet present in the DOM at that point? How can I run that script without using the JS interop or jQuery?
If your okay with using jquery you can use an on handler, which will apply to dynamically added elements
$("body").on("click", ".some-element", function(){
alert("Hello");
});
You can do natively too, but seems sketchy IMHO:
document.querySelector('body').addEventListener('click', function(event) {
if (event.target.className.toLowerCase() === 'some-element') {
alert("Hello");
}
});
Note you have weird javascripty things to worry about now like z-index if the click event actually gets up to the body, but it should work for simple stuff.
Main question is WHY would you want to do this in Blazor - whole point is you can use C# events instead you crazy person!!
Create a page like this:
<div height="#($"{Height}px")" width="#($"{Width}px")">
Test
</div>
<button #onclick="DoubleSize">Double Size</button>
#code
{
public int Width { get; set; } = 50;
public int Height { get; set; } = 50;
private void DoubleSize()
{
Width = Width * 2;
Height = Height * 2;
}
}
On render the height and width are 50px:
Then you click the Button, and they change to 100px:
Similarly with CSS if that's what you were talking about:
<div style="width: #($"{Width}px"); height: #($"{Height}px")">
Test
</div>
This is interacting with the properties of the DOM elements without using Javascript... you can do this with any property. You don't need to use Javascript - you just bind them to your properties in C#...
You shouldn't need to 'retrieve the final properties of the rendered DOM elements' as you should be controlling them from C# and not worrying about the DOM
I'm pretty new in Blazor, so sorry if it's not the correct way to do it in Balzor. But I think that you need always use JS interop to use JavaScript function. You want to execute some script, but the question is: when do you want to execute the script? I imagine you want to execute an action after navigate to a page, after make a click in a button... and all this events happens in Blazor
If you ask about relationate an blazor element with the doom you need use #ref
A little example. You create a .js like
var auxiliarJs = auxiliarJs || {};
auxiliarJs.getBoundingClientRect = function (elementRef) {
let result = elementRef? elementRef.getBoundingClientRect():
{left: 0,top: 0,right: 0,bottom: 0,x: 0,y: 0,width: 0,height: 0 };
return result;
}
auxiliarJs.executeFunction = function (elementRef, funciones) {
let res = null;
try {
if (Array.isArray(funciones)) {
functiones.forEach(funcion => {
elementRef[funcion]()
});
}
else
res = elementRef[funciones]();
}
catch (e) { }
if (res)
return res;
}
auxiliarJs.setDocumentTitle = function (title) {
document.title = title;
};
And a service.cs with his interface
public interface IDocumentService
{
Task<ClientRect> getBoundingClientRect(ElementReference id);
Task setDocumentTitle(string title);
Task<JsonElement> executeFunction(ElementReference id, string funcion);
Task executeFunction(ElementReference id, string[] funciones);
}
public class DocumentService:IDocumentService
{
private IJSRuntime jsRuntime;
public DocumentService(IJSRuntime jsRuntime)
{
this.jsRuntime = jsRuntime;
}
public Dictionary<string, object> JSonElementToDictionary(JsonElement result)
{
Dictionary<string, object> obj = new Dictionary<string, object>();
JsonProperty[] enumerador = result.EnumerateObject().GetEnumerator().ToArray();
foreach (JsonProperty prop in enumerador)
{
obj.Add(prop.Name, prop.Value);
}
return obj;
}
public async Task<ClientRect> getBoundingClientRect(ElementReference id)
{
return await jsRuntime.InvokeAsync<ClientRect>("auxiliarJs.getBoundingClientRect", id);
}
public async Task setDocumentTitle(string title)
{
await jsRuntime.InvokeVoidAsync("auxiliarJs.setDocumentTitle", title);
}
public async Task<JsonElement> executeFunction(ElementReference id,string funcion)
{
var result= await jsRuntime.InvokeAsync<JsonElement>
("auxiliarJs.executeFunction", id, funcion);
return result;
}
public async Task executeFunction(ElementReference id, string[] funciones)
{
await jsRuntime.InvokeVoidAsync("auxiliarJs.executeFunction", id, funciones);
}
}
public class ClientRect
{
public float left{ get; set; }
public float top { get; set; }
public float right { get; set; }
public float bottom { get; set; }
public float x { get; set; }
public float y { get; set; }
public float width { get; set; }
public float height { get; set; }
}
Well, you inject the service as usual in program.cs
public static async Task Main(string[] args){
....
builder.Services.AddSingleton<IDocumentService, DocumentService>();
}
And in your component razor
#inject IDocumentService document
<div #ref="mydiv"></div>
<input #ref="myinput">
<button #onclick="click">click</button>
#code{
private ElementReference mydiv;
private ElementReference myinput;
click(){
ClientRect rect = await document.getBoundingClientRect(mydiv);
document.setDocumentTitle("New Title");
document.executeFunction(myinput 'focus')
}
}
Blazor "overwrites" all attached JS Events during render process
window.attachHandlers = () => {
var elements = document.querySelectorAll(".some-element");
elements.forEach(function (element) {
element.addEventListener("click", (e) => {
alert("Hello");
});
});
and in razor page
protected override void OnAfterRender(bool firstRender)
{
if(firstRender)
{
JSRuntime.InvokeVoidAsync("attachHandlers");
}
}
i have two Models, first:
class Tutorial extends Eloquent {
protected $table = 'tutorials';
public function rating()
{
return $this->hasMany('Rating');
}
}
and:
class Rating extends Eloquent {
protected $table = 'ratings';
public $timestamps = false;
public function tutorial()
{
return $this->belongsTo('Tutorial');
}
}
now in my controller i have this:
public function get_index() {
$tutorials = tutorial::orderBy('created_at', 'desc')
->with('rating')
->paginate(25);
return View::make('site/home/index')->with('tutorials', $tutorials);
}
So how do i get all ratings from one tutorial in my View?!
EDIT:
Now i have this:
public function ratings()
{
return $this->hasMany('Rating');
}
public function getRating()
{
// Grab the ratings from this tutorial
$ratings = $this->ratings;
$summedRatings = 0;
// Loop through them all and add them together
foreach($ratings as $rating)
{
console.log($rating->value);
$summedRatings += $rating->value;
}
// Return the calculated average
return $summedRatings / count($ratings);
}
public function get_index() {
$tutorials = Tutorial::with('ratings')
->with('user')
->orderBy('created_at', 'desc')
->paginate(25);
return View::make('site/home/index')->with('tutorials', $tutorials);
}
and in my View:
#foreach($tutorials as $tutorial)
<span>{{$tutorial->rating}}</span>
#endforeach
But all my < span >´s are empty!
UPDATE: if i do this:
#foreach($tutorials as $tutorial)
#foreach($tutorial->ratings as $rate)
<span>{{$rate->value}}</span>
#endforeach
everything is good....So what´s wrong?
Depending on the platform you're site is on you should always use the correct case.
$tutorials = tutorial::orderBy(...) // Wrong
$tutorials = Tutorial::orderBy(...) // Correct
To eager load the ratings you should always declare your 'with' method before anything else.
$tutorials = Tutorial::with('rating')
->orderBy('created_at', 'DESC')
->paginate(25);
This has, for some reason, been left out of the L4 docs.
In your view you can now access the rating with this
foreach($tutorials as $tutorial)
{
echo $tutorial->rating->{rating table column name};
}
First, as far as naming conventions go, to make things easier to understand: The rating() method within your tutorial method should be called ratings(), so when you grab your ratings, it will look better ($tutorial->ratings)
After renaming this, in your view, while looping through the array of $tutorials, you could access the ratings of each one like this:
foreach($tutorials as $tutorial)
{
$ratings = $tutorial->ratings;
}
Which would retrieve the ratings object of each.
What you should know is that you can create properties for your model if you need to return the calculation of the ratings, instead of the ORM objects
For example, if each rating is a number from 1-5 in the ratings table stored in an amount column, you can do this to set the average of each rating as a property:
class Tutorial extends Eloquent {
protected $table = 'tutorials';
public function ratings()
{
return $this->hasMany('Rating');
}
public function getRating()
{
// Grab the ratings from this tutorial
$ratings = $this->ratings;
$summedRatings = 0;
// Loop through them all and add them together
foreach($ratings as $rating)
{
$summedRatings += $rating->amount;
}
// Return the calculated average
return $summedRatings / count($ratings);
}
}
Then in your view, you can echo out the property as if it were part of the database
foreach($tutorials as $tutorial)
{
echo $tutorial->rating;
}
I have created a variable length list according to the many great posts by Steve Sanderson on how to do this in MVC 2. His blog has a lot of great tutorials.
I then created a custom "requiredif" conditional validator following this overview http://blogs.msdn.com/b/simonince/archive/2010/06/11/adding-client-side-script-to-an-mvc-conditional-validator.aspx
I used the JQuery validation handler from the MSDN blog entry which adds the following to a conditional-validators.js I include on my page's scripts:
(function ($) {
$.validator.addMethod('requiredif', function (value, element, parameters) {
var id = '#' + parameters['dependentProperty'];
// Get the target value (as a string, as that's what actual value will be)
var targetvalue = parameters['targetValue'];
targetvalue = (targetvalue == null ? '' : targetvalue).toString().toLowerCase();
// Get the actual value of the target control
var actualvalue = ($(id).val() == null ? '' : $(id).val()).toLowerCase();
// If the condition is true, reuse the existing required field validator functionality
if (targetvalue === actualvalue)
return $.validator.methods.required.call(this, value, element, parameters);
return true;
});
})(jQuery);
Alas, this does not cause a client-side validation to fire ... only the server-side validation fires. The inherent "required" validators DO fire client-side, meaning I have my script includes set-up correctly for basic validation. Has anyone accomplished custom validators in a variable length list in MVC 2 using JQuery as the client-side validation method?
NOTE that this same custom validator works client-side using the exact same set-up on a non-variable length list.
Turns out that it was a field ID naming issue with the way that collection IDs render in a variable length list. The validator was attempting to name the element ID of the dependent property with the expected statement of:
string depProp = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(Attribute.DependentProperty);
I analyzed the HTML viewsource (posted in my comment, above), and actually, [ and ] characters are not output in the HTML of the collection-index elements... they're replaced with _... so, when I changed my CustomValidator.cs to have the dependent property set to:
string depProp = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(Attribute.DependentProperty).Replace("[", "_").Replace("]", "_");
... then the client-side validator works since the name matches. I'll have to dig deeper to see WHY the ID is getting renamed in Sanderson's collection index method, below...
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace Sendz.WebUI.Helpers
{
public static class HtmlPrefixScopeExtensions
{
private const string IdsToReuseKey = "__htmlPrefixScopeExtensions_IdsToReuse_";
public static IDisposable BeginCollectionItem(this HtmlHelper html, string collectionName)
{
var idsToReuse = GetIdsToReuse(html.ViewContext.HttpContext, collectionName);
var itemIndex = idsToReuse.Count > 0 ? idsToReuse.Dequeue() : Guid.NewGuid().ToString();
// autocomplete="off" is needed to work around a very annoying Chrome behaviour whereby it reuses old values after the user clicks "Back", which causes the xyz.index and xyz[...] values to get out of sync.
html.ViewContext.Writer.WriteLine(
string.Format("<input type=\"hidden\" name=\"{0}.index\" autocomplete=\"off\" value=\"{1}\" />",
collectionName, html.Encode(itemIndex)));
return BeginHtmlFieldPrefixScope(html, string.Format("{0}[{1}]", collectionName, itemIndex));
}
public static IDisposable BeginHtmlFieldPrefixScope(this HtmlHelper html, string htmlFieldPrefix)
{
return new HtmlFieldPrefixScope(html.ViewData.TemplateInfo, htmlFieldPrefix);
}
private static Queue<string> GetIdsToReuse(HttpContextBase httpContext, string collectionName)
{
// We need to use the same sequence of IDs following a server-side validation failure,
// otherwise the framework won't render the validation error messages next to each item.
var key = IdsToReuseKey + collectionName;
var queue = (Queue<string>)httpContext.Items[key];
if (queue == null)
{
httpContext.Items[key] = queue = new Queue<string>();
var previouslyUsedIds = httpContext.Request[collectionName + ".index"];
if (!string.IsNullOrEmpty(previouslyUsedIds))
foreach (var previouslyUsedId in previouslyUsedIds.Split(','))
queue.Enqueue(previouslyUsedId);
}
return queue;
}
#region Nested type: HtmlFieldPrefixScope
private class HtmlFieldPrefixScope : IDisposable
{
private readonly string _previousHtmlFieldPrefix;
private readonly TemplateInfo _templateInfo;
public HtmlFieldPrefixScope(TemplateInfo templateInfo, string htmlFieldPrefix)
{
_templateInfo = templateInfo;
_previousHtmlFieldPrefix = templateInfo.HtmlFieldPrefix;
templateInfo.HtmlFieldPrefix = htmlFieldPrefix;
}
#region IDisposable Members
public void Dispose()
{
_templateInfo.HtmlFieldPrefix = _previousHtmlFieldPrefix;
}
#endregion
}
#endregion
}
}
A complete validator / attribute reference...
public class RequiredIfAttribute : ValidationAttribute
{
private RequiredAttribute innerAttribute = new RequiredAttribute();
public string DependentProperty { get; set; }
public object TargetValue { get; set; }
public RequiredIfAttribute(string dependentProperty, object targetValue)
{
this.DependentProperty = dependentProperty;
this.TargetValue = targetValue;
}
public override bool IsValid(object value)
{
return innerAttribute.IsValid(value);
}
}
public RequiredIfValidator(ModelMetadata metadata, ControllerContext context, RequiredIfAttribute attribute)
: base(metadata, context, attribute) { }
public override IEnumerable<ModelValidationResult> Validate(object container)
{
// Get a reference to the property this validation depends upon
var field = Metadata.ContainerType.GetProperty(Attribute.DependentProperty);
if (field != null)
{
// Get the value of the dependent property
var value = field.GetValue(container, null);
// Compare the value against the target value
if ((value == null && Attribute.TargetValue == null) ||
(value != null && value.ToString().ToLowerInvariant().Equals(Attribute.TargetValue.ToString().ToLowerInvariant())))
{
// A match => means we should try validating this field
if (!Attribute.IsValid(Metadata.Model))
// Validation failed - return an error
yield return new ModelValidationResult { Message = ErrorMessage };
}
}
}
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
var rule = new ModelClientValidationRule()
{
ErrorMessage = ErrorMessage,
ValidationType = "requiredif"
};
var viewContext = (ControllerContext as ViewContext);
var depProp = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(Attribute.DependentProperty).Replace("[", "_").Replace("]", "_");
rule.ValidationParameters.Add("dependentProperty", depProp);
rule.ValidationParameters.Add("targetValue", Attribute.TargetValue.ToString());
yield return rule;
}