Multi-Dimensional Arrays in Ractive.js - javascript

I've been playing around with the fantastic Ractive.js library (not to be confused with Facebook's Reactive.js). I worked out that you can render a two-dimensional using the following template code:
<div class="container">
{{ #frameContainer:i }}
<div class="row">
{{ #frameContainer[i] }}
<div on-click="cell-click" class="cell {{ . ? 'on' : 'off' }}"></div>
{{ /frameContainer[] }}
</div>
{{ /frameContainer }}
</div>
This works exactly as I'd expect and the inner cell-click event returns the correct keypath (e.g. frameContainer.2.4).
I then wanted to turn this into a three-dimensional array (to add a "time" axis). However, the following did not work:
<div class="container">
{{ #frameContainer[time]:i }}
<div class="row">
{{ #frameContainer[time][i] }}
<div on-click="cell-click" class="cell {{ . ? 'on' : 'off' }}"></div>
{{ /frameContainer[][] }}
</div>
{{ /frameContainer }}
</div>
Where time represents the current time value (it will only ever show one "time" at a time - seems obvious when you say it like that...).
This... sort of works. It displays the grid as it should, but the returned keypath for the cell-click event is no longer correct, returning something like ${frameContainer-time-8-}.2 - which has lost the time value (and has gone a bit weird).
Obviously I could just do it by having a currentFrame value which I render from and switch around with ractive.set(), but that seems less elegant. Is there a way to do it purely within the template? And, if not, what's the most efficient way of doing it otherwise?
Thanks!

This is quite a tricky one. Basically, the ${frameContainer-time-8-} keypath is how Ractive uniquely identifies an expression, and ${frameContainer-time-8-}.2 means 'the third member of whatever that expression evaluates to'.
What's happening here is this: When Ractive's parser sees the {{ #frameContainer[time][i] }} section, it parses frameContainer[time][i] as a JavaScript expression and turns it into the following (you can try this yourself - Ractive.parse('{{#frameContainer[time][i]}}')):
{
r: ['i','time','frameContainer'],
s: '${2}[${1}][${0}]'
}
When the template is rendered, Ractive creates an evaluator for that expression, which has a function generated from the string (the s property), and which watches the time and frameContainer values (it doesn't need to watch i because it can't change). When either or both of those values change, the function is executed with them as arguments. If it returns a changed value, Ractive needs to update the view.
Since there's a good mechanism for propagating viewmodel changes to the view - keypaths - that's what the evaluator uses. In order to do so, it needs to create a unique keypath, hence ${frameContainer-time-8-} (it can't contain dots or square brackets, because Ractive would try to split on those).
So here's the thing: expression keypaths are one-way. You can't do ractive.set('${frameContainer-time-8-}.2', 'true') like you can with regular keypaths, because Ractive can't figure out what underlying property that corresponds to (or if there even is one - it might be a derivative value for all it knows).
Solution 1
The simplest workaround would probably be to do something like this:
<div on-click="cell-click:{{time}},{{i}}" class="cell {{ . ? 'on' : 'off' }}"></div>
You can then use the time and i values in your cell-click handler (they will be the second and third arguments, after event).
I've done a simplified demo here (assuming I understood you correctly): http://jsfiddle.net/rich_harris/LYEGX/
Solution 2
The other way would be to do away with keypaths altogether and use an adaptor. I won't go into all the details here as it may not be the answer you're looking for, but there's some documentation and an example (some of the links are currently out of date, sorry...). In short, this method assumes you're able to use non-POJOs in your app.

Related

Passing data through nested components in Vue.js

I am writing a page using Vue.js and am attempting some custom components. I've started with a very simple one:
Vue.component('single-field', {
template: '<b>{{ key }}:</b> {{ value }}',
props: ['key', 'value']
});
This works fine on its own: it is intended to take two values and return them in a key: value format. Initially, when I was passing the value as an element from the Vue data object (specifically, pickup.dateTime it wasn't working. It's easier to show than explain:
<div id="app">
<single-field key="Date & Time" value="pickup.dateTime"></single-field>
</div>
I was able to fix this by binding the value (notice the colon by value):
<single-field key="Date & Time" :value="pickup.dateTime"></single-field>
I then wrote a second component which is designed to take an object and a title and return each element of the object as a set of <single-field> components, with a title above them. Here is what I wrote:
Vue.component('field-block', {
template: '<h2>{{title}}</h2>\
<div v-for="(p-key, p-value) in parent">\
<single-field key="p-key" value="p-value"></single-field>\
</div>',
props: ['parent', 'title']
});
Now, I think there must be something wrong with the way I am binding (or not binding?) which is causing the data to be displayed in a strange way. Here is my HTML:
<div id="app">
<single-field key="DateTime" :value="pickup.dateTime"></single-field>
<single-field key="Name" :value="pickup.name"></single-field>
<field-block title="Address" :parent="pickup.address"></field-block>
</div>
The field pickup.address is an object, so I was hoping that the component would iterate through the elements and return them as single fields, but what it's currently doing is just returning the variable names, like p-key: p-value. I tried doing a bind on both of them in the component definition (on both because they are now both variables being passed whereas previously I was using a static string for the key field):
<div v-for="(p-key, p-value) in parent">
<single-field :key="p-key" :value="p-value"></single-field>
</div>
But what this returns is NaN: NaN, and I can't figure out why it would do that. I don't fully understand the bind operation, but I expect that it only binds to data in the data option when defining the view, which is not where p-key and p-value are defined. How would I get the variables to 'carry through' from the outer component to the inner ones? Thanks.
Okay this one was tricky.
Several problems with your code:
Your components should have only one root element. So pack your components into a <div>.
Then, - is not a valid character to put into a javascript variable name. So you can't write (p-key, p-value) in parent, write something else, like (pKey, pValue) in parent.
On the other hand, html properties are not case-sensitive, so you should actually write key-name (for example) instead of keyName, but only for the property names. This limitation does apply only in your html files (not in your string templates).
Finally, here is a working js-fiddle.
https://jsfiddle.net/6juwLd3b/
Additionally, I would advise you that you look at your console while developing. Many error where displayed that could have lead you to a working version.

Angular 2 DOM Binding Data is not accurate

I am trying to compare two variables who are constantly change due to an API call.
Think a stock counter. If the stock increase I want to know this as well if it decreases.
Before the API call i store an instance of the call (object array) and then compare it to a new version of the call. This all works and as a result I can get the console to tell me if there was an increase or decrease.
I store this information right inside the array where I change a boolean to true or false depending on the result.
perfect, all is well and working. However when I bind these exact values to the DOM using a *ngFor loop as I want to cycle through all objects, they values are always false (default value). They never actually change even those console.log says they are.
Why would the DOM not bind the values correctly?
DOM
<div class="ticker" *ngFor="let coinresult of coinResults; let beforecoinresult of beforeCoinResults; let aftercoinresult of afterCoinResults;">
<div class="wrapper" *ngIf="coinresult.name != step2Selection">
<!--<h1 *ngIf="match === false">Before: {{beforecoinresult.amount}} - After: {{aftercoinresult.amount}}</h1>-->
<div class="pin" (click)="pinTitle(coinresult.amount, coinresult.name)">
<i class="material-icons" *ngIf="pinnedCoinAmount != coinresult.amount">gps_not_fixed</i>
<i class="material-icons selectedCoin" *ngIf="pinnedCoinAmount === coinresult.amount">gps_fixed</i>
</div>
<div class="amount" [ngClass]="{amountpinned: pinnedCoinAmount === coinresult.amount,
amountincrease: beforecoinresult.increase,
amountdecrease: beforecoinresult.decrease}">
{{coinresult.amount}}
</div>
<div class="name" [ngClass]="{ namepinned: pinnedCoinAmount === coinresult.amount,
nameincrease: beforecoinresult.increase,
namedecrease: beforecoinresult.decrease}">
{{coinresult.name}}
</div>
{{beforecoinresult.increase}} {{beforecoinresult.decrease}}
</div>
interface
export interface coinResultsType{
name: string,
amount: number,
increase: boolean,
decrease: boolean
}
Theres a Ton of logic in my component, but I don't see value in posting it, it works and works great. So I attached an image of the console who is logging:
console.log(beforeCoinResults[0].increase);
console.log(beforeCoinResults[0].decrease);
However in the CAD ticker you can see there both false. Now, they info happens quick so you may say that it had changed to false, but rest assured I am watching but they all remain false.
Instead of using console.log to debug your code, use debugger to stop the code execution and inspect the values manually. The reason is that console.log might not be in-sync with your code execution, and at the time the data is displayed on the console, it might have already changed. I know, it sounds confusing, specially thinking about console.log from a sync point of view, and not async point of view.
Your code might be working fine, and might be console.log not showing the values correctly.

More efficient way to interpret data from route in view with Angular over Swig?

I have an array of JSON objects being passed from my Node route into its respective view. For example:
res.render("path/to/view", { data: result, data2: result2 })
where both result and result2 are arrays of JSON objects. In my view, I am using them in an ng-init function like so: ( {{}} designates swig, where I have changed [[]] to designate Angular, so in the below example, we are using swig before using my defined Angular init function)
<div ng-init="init( {{ data|json }} )"> </div>
I should say that the above example works fine, but when "data" becomes a very large dataset, SO much time is spend on the swig part - that is converting it to JSON (again..?). Like I said, the "data" is already in the form of JSON, but when I remove the |json from the swig part above, I get a [$parse:syntax] error, and it give me the above line but evaluated:
<div ng-init="init( [object Object],[object Object] )"> </div>
I have tried variations of using ng-init="init( [[ JSON.parse( {{ data }} ) ]] )" so that I evaluate the output to JSON (even though the output already is..?) but cant get anything to work.
Any ideas? Maybe I have the syntax wrong? I don't understand because the "data" is JSON when I pass it to the view, but I can't pass it directly into my init function without getting that syntax error.
Take whatever you're calling with ng-init out of your view and put it in a controller or service.
There are tons of reasons not to use ng-init, so much so that the Angular team basically recommends you don't use it: https://docs.angularjs.org/api/ng/directive/ngInit
Update
I finally think I see what you're trying to do, and it's called bootstrapping. You can embed data into your view server-side like so:
<script>
angular.module("app").constant("myBootstrap", {{ data.stringified }});
</script>
Where data.stringified is your stringified data. Then in Angular you can inject into any controllers you want as a constant with myBootstrap, same as you would for $rootScope, etc., and the data will be available.
So while I am not entirely sure what was causing it to go so slow, I have found a solution, although 'hack' might more accurately describe it. The problem I was having was with Swig taking ages to template my data before passing it into the init function (which I now know is bad practice; that is, to use ng-init to call a function).
I should note for future readers that I am using Node.js, Swig for templating, and Angular for handling the the MVC side of things.
See the above question to see what my code was like before (slow version). Below is my solution:
Routing side
var rawSQLData = makeSQLCall();
var parsedAsJsonSQLData = parseData(rawSQLData);
var parsedDataAsString = JSON.stringify(parsedAsJsonSQLData);
res.render("path/to/view", { data: parsedDataAsString });
HTML
<div ng-init=" init( {{ data|json }} )"> </div>
Angular Controller
$scope.init = function(data){
$scope.dataInScope = JSON.parse(data);
}
As you can see, the only thing I changed was stringifying the JSON before shooting it to the HTML and then parsing it once it gets to the controller.
The reason I think this works is because I am essentially doing all of the JSON processing in parts other than the templating engine (Swig). I tried using no filter, but Swig defaults to converting the data to JSON which was breaking my HTML. The reason that I want to call this a hack rather than a solution is because we stringify the data, and then use the json filter for Swig, which, according to the documentation:
Return a string representation of an JavaScript object.
If I had to guess, I would say that by using the json filter on a string, Swig decides it has nothing to do as the object is already a string and just passes it along (just what I wanted all along!). It is incredible how much faster it is when Swig doesn't touch the data. The JSON stringify and parse are really quick for the size of data, while Swig was taking up to 40 seconds

Change how data is represented in AngularJS

I'm actually not sure how to ask this question, and I'm probably using incorrect terms, so bear with me.
Angular sets up a 2-way data binding so that it makes it easy to work with the data on both sides. But what if I want to change how that data is represented?
Let me give a concrete example.
I want a form with a checkbox, which if bound directly to a model, would be stored as true or false by Angular. However, in another part of the webpage I want that value to show up as 0 or 1, not true or false.
Now sure I could go and make them use two different variables, and use ng-change or something like that to update one based on the other, but that seems overkill and convoluted.
Is there some special meta function or something I can define that lets me essentially translate the data as it goes back and forth?
Use the ngTrueValue and ngFalseValue directives. They define what should be treated as true and false on a checkbox: https://docs.angularjs.org/api/ng/input/input%5Bcheckbox%5D
Example:
<input type="checkbox" ng-model="foo"
ng-true-value="OK"
ng-false-value="BOO-HOO">
The model will either have a value of "OK" or "BOO-HOO" instead of the default true and false in the above example.
Alternatively, if you want the original model to retain its default values and only draw the custom ones from another variable, you could also use the ngChange directive:
<input type="checkbox" ng-model="foo"
ng-change="bar=foo&&'OK'||'BOO-HOO'">
Now, whenever foo changes, bar will have the corresponding alternative value. Just remember to assign bar an initial value (it will start out with no value at all).
in your controller...
$scope.getvalue(data)
{
if(data===true)
return 1; // write what ever you want...
return 0;
}
in your html page..
bind the normal one as {{$scope.data1}} and other as {{getvalue($scope.data1)}}
You can do some nice things with ngBind:
Check this plunker: http://plnkr.co/edit/cRhLN2p5N4PmI65ps6Gp?p=preview
<input type="checkbox" ng-model="ok"> OK?
<h2>true or false: {{ ok }}</h2>
<h2>0 or 1: {{ ok ? 1 : 0 }}</h2>

KnockoutJS if binding with multiple elements

I thought this would be a simple thing, but I basically have an observable array of objects which contain data like:
{
BasicType: "some-type",
ExtendedType: "some-extended-type",
DisplayType: "some-display-type"
}
The data fields are always in the model, just they will be empty strings if there is no data for them to display.
Now this model is used throughout the system and is a simple POJO and the view model just contains many of these as and when they are needed in the views. So I dont want to put any view specific concerns on "this" object however can put them in the view model which is composed of lots of these smaller models.
Anyway in the view all fields are added but each has an if to disable it if there is a more applicable type available. So an example would be something like:
<div data-bind="text: BasicType, if: (BasicType && !ExtendedType && !DisplayType)"/>
<div data-bind="text: ExtendedType, if: (!BasicType && ExtendedType && !DisplayType)"/>
<div data-bind="text: DisplayType, if: (!BasicType && !ExtendedType && DisplayType)"/>
However for some reason it doesn't do as I would expect, I have tried changing the !xType for xType == '' however no such luck there either, and also used the parentheses to force evaluation but nothing. So am I missing something or can the knockoutjs if not handle multiple elements?
I would be happy to just convert these into computed observables if there was a simple way to do it, but as these models are used across pages and as a contract with a webservice (they are like the data layer) I don't want to change them, and I do not know how I could otherwise add the computed values to the objects unless when I get them I loop round each one adding the computed observables to each then using that.
Anyway that's the scenario, any advice would be great!
Your code will work if the model properties are not observables. If the model properties are observables, you need to execute the observables to get access to the values before comparing.
<div data-bind="foreach: types">
<div data-bind="text: BasicType, if: (BasicType() && !ExtendedType() && !DisplayType())"></div>
<div data-bind="text: ExtendedType, if: (!BasicType() && ExtendedType() && !DisplayType())"></div>
<div data-bind="text: DisplayType, if: (!BasicType() && !ExtendedType() && DisplayType())"></div>
</div>
Here's an example: http://jsfiddle.net/5WMVb/1/

Categories