99

I have a string I have gotten from a routeParam or a directive attribute or whatever, and I want to create a variable on the scope based on this. So:

$scope.<the_string> = "something".

However, if the string contains one or more dots I want to split it and actually "drill down" into the scope. So 'foo.bar' should become $scope.foo.bar. This means that the simple version won't work!

// This will not work as assigning variables like this will not "drill down"
// It will assign to a variables named the exact string, dots and all.
var the_string = 'life.meaning';
$scope[the_string] = 42;
console.log($scope.life.meaning);  // <-- Nope! This is undefined.
console.log($scope['life.meaning']);  // <-- It is in here instead!

When reading a variable based on a string you can get this behavior by doing $scope.$eval(the_string), but how to do it when assigning a value?

Andrew Gray
  • 3,593
  • 3
  • 35
  • 62
Erik Honn
  • 7,576
  • 5
  • 33
  • 42

8 Answers8

176

The solution I have found is to use $parse.

"Converts Angular expression into a function."

If anyone has a better one please add a new answer to the question!

Here is the example:

var the_string = 'life.meaning';

// Get the model
var model = $parse(the_string);

// Assigns a value to it
model.assign($scope, 42);

// Apply it to the scope
// $scope.$apply(); <- According to comments, this is no longer needed

console.log($scope.life.meaning);  // logs 42
Erik Honn
  • 7,576
  • 5
  • 33
  • 42
  • 1
    So this is beautiful! But (and there almost always is one) I cannot get the $scope.$apply() to propagate my data. I can see it in a console log, but my view does not show the newly added data (I have a footer bar where I show the $scope) - Any ideas? – william e schroeder Sep 12 '14 at 20:07
  • Hard to know without seeing code, but my first guess would be that the footer is not in the same scope. If the footer is outside the element with ng-controller="YourController" then it won't have access to its variables. – Erik Honn Sep 15 '14 at 11:26
  • 3
    Thanks for the answer. I'd like to point out that assign also works with complex objects, not just primitive types. – Patrick Salami Sep 19 '14 at 00:59
  • 1
    Why is $scope.$apply necessary? – sinθ Nov 20 '14 at 17:09
  • I don't quite recall actually. I think it was because model.assign does not trigger data binding, so you need to apply manually once you have done everything you need to do. Try it out and see if it is possible to remove it or not (might have changed in newer versions of Angular as well, I think this was one or two versions back). – Erik Honn Nov 21 '14 at 09:37
  • This is brilliant! Don't know how else I could have set $scope variables dynamically. – Daniel Bonnell Jun 12 '15 at 02:36
  • I tested and, in my particular case, `$scope.$apply();` wasn't necessary. In fact, it triggered a **digest already in progress** error. – Bianca Rosa Mar 30 '16 at 14:11
  • Probably something that has changed since then in that case. I'll update the answer. Thanks! – Erik Honn Mar 30 '16 at 14:19
  • Can you guys help me with my same issue? Its not working. – Sagar Oct 24 '16 at 07:39
  • helps me! Thanks! – Ihor Khomiak Jun 17 '17 at 15:51
  • @ErikHonn : will this only work in directives? can't i use $parse in plain javascript context inside a function which is getting invoked on click. When am trying its saying $parse not defined – NeverGiveUp161 Aug 07 '18 at 16:16
  • @NeverGiveUp161 $parse is an Angular specific service so you need to be in a context where you have injected it. But you should be able to inject basically anywhere, like a service, a controller, etc. – Erik Honn Aug 09 '18 at 12:00
20

Using Erik's answer, as a starting point. I found a simpler solution that worked for me.

In my ng-click function I have:

var the_string = 'lifeMeaning';
if ($scope[the_string] === undefined) {
   //Valid in my application for first usage
   $scope[the_string] = true;
} else {
   $scope[the_string] = !$scope[the_string];
}
//$scope.$apply

I've tested it with and without $scope.$apply. Works correctly without it!

Andrew Gray
  • 3,593
  • 3
  • 35
  • 62
  • 13
    Note that this is something completely different than what the question is about. The goal of the question is to create a dynamic scope variable with more than 1 depth based on a string. So to create "scope.life.meaning" and not "scope.lifeMeaning". Using $scope[the_string] will not do that. And for your specific case, remember that !undefined is actually true in javascrip, so you could actually skip the entire if statement and just do $scope[the_string] = !$scope[the_string];. Might muddle the code a bit though, so I am not sure if it is really something you would want. – Erik Honn Feb 25 '15 at 13:37
  • @ErikHonn, actually, $scope[life.meaning]=true worked for me! – Jesse P Francis Jan 04 '17 at 07:55
  • 1
    It "works", but unless something changed in 1.6 it doesn't do the same thing as the question asks for. If you do $scope[life.meaning]=true the name of the property will be "life.meaning", and that is not what we want. We want a property called life which has a child property called meaning. – Erik Honn Jan 04 '17 at 09:07
13

Create Dynamic angular variables from results

angular.forEach(results, function (value, key) {          
  if (key != null) {                       
    $parse(key).assign($scope, value);                                
  }          
});

ps. don't forget to pass in the $parse attribute into your controller's function

Demodave
  • 6,242
  • 6
  • 43
  • 58
5

If you are ok with using Lodash, you can do the thing you wanted in one line using _.set():

_.set(object, path, value) Sets the property value of path on object. If a portion of path does not exist it’s created.

https://lodash.com/docs#set

So your example would simply be: _.set($scope, the_string, something);

NorthernStar
  • 51
  • 1
  • 2
4

Just to add into alread given answers, the following worked for me:

HTML:

<div id="div{{$index+1}}" data-ng-show="val{{$index}}">

Where $index is the loop index.

Javascript (where value is the passed parameter to the function and it will be the value of $index, current loop index):

var variable = "val"+value;
if ($scope[variable] === undefined)
{
    $scope[variable] = true;
}else {
    $scope[variable] = !$scope[variable];
}
Riaz
  • 104
  • 4
  • This has nothing to do with the question, please stay on topic. – Erik Honn Dec 09 '16 at 10:16
  • Sorry, thought it might help in creating dynamic variables using strings. This thread helped me out to write the above code which is working fine for me. – Riaz Dec 10 '16 at 12:18
  • No worries, wanting to provide answers is a good start, just make sure to read the question first :) In this case it's about parsing an unknown string "a.b.c" to the object {a: {b: {c: ...}}}. But handling repeating items is a common problem so I'm glad you found something healpfull in the thread. – Erik Honn Dec 11 '16 at 17:30
  • Oh, and as I noted on another answer, if the values are just supposed to be true/false (or undefined) you could just skip the entire logic and do $scope[variable] === !$scope[variable], since !undefined is true in javascript. Although if you are using typescript I suspect it would get mad at you! – Erik Honn Dec 11 '16 at 17:33
3

Please keep in mind: this is just a JavaScript thing and has nothing to do with Angular JS. So don't be confused about the magical '$' sign ;)

The main problem is that this is an hierarchical structure.

console.log($scope.life.meaning);  // <-- Nope! This is undefined.
=> a.b.c

This is undefined because "$scope.life" is not existing but the term above want to solve "meaning".

A solution should be

var the_string = 'lifeMeaning';
$scope[the_string] = 42;
console.log($scope.lifeMeaning);
console.log($scope['lifeMeaning']);

or with a little more efford.

var the_string_level_one = 'life';
var the_string_level_two = the_string_level_one + '.meaning';
$scope[the_string_level_two ] = 42;
console.log($scope.life.meaning);
console.log($scope['the_string_level_two ']);

Since you can access a structural objecte with

var a = {};
a.b = "ab";
console.log(a.b === a['b']);

There are several good tutorials about this which guide you well through the fun with JavaScript.

There is something about the

$scope.$apply();
do...somthing...bla...bla

Go and search the web for 'angular $apply' and you will find information about the $apply function. And you should use is wisely more this way (if you are not alreay with a $apply phase).

$scope.$apply(function (){
    do...somthing...bla...bla
})
BenMorel
  • 34,448
  • 50
  • 182
  • 322
Raxa
  • 372
  • 3
  • 7
  • 3
    Thanks for the answer, but the question was not why the "simple version" would not work, that was known from the start. The question was what the alternative was. I am afraid neither of your two suggestions will work, both of them require you to know the structure of the string beforehand, and the point is for it to be dynamic (and to be able to handle arbitrary strings). See the accepted answer for a solution. – Erik Honn Jun 12 '14 at 07:24
  • 1
    Also, neither answer actually solves the issue. The first fails the basic prerequisite, that we should assign to $scope.a.b.c and not to $scope['a.b.c'], and the second one also fails this and result in literally the exact same thing as the "simple version" from the question, just with unneeded syntax, but perhaps that is a typo? :) – Erik Honn Jun 12 '14 at 07:42
0

If you are using Lodash library below is the way to set a dynamic variable in the angular scope.

To set the value in the angular scope.

_.set($scope, the_string, 'life.meaning')

To get the value from the angular scope.

_.get($scope, 'life.meaning')
Sagar Vaghela
  • 1,165
  • 5
  • 20
  • 38
-1

If you were trying to do what I imagine you were trying to do, then you only have to treat scope like a regular JS object.

This is what I use for an API success response for JSON data array...

function(data){

    $scope.subjects = [];

    $.each(data, function(i,subject){
        //Store array of data types
        $scope.subjects.push(subject.name);

        //Split data in to arrays
        $scope[subject.name] = subject.data;
    });
}

Now {{subjects}} will return an array of data subject names, and in my example there would be a scope attribute for {{jobs}}, {{customers}}, {{staff}}, etc. from $scope.jobs, $scope.customers, $scope.staff

  • 4
    Thanks for the answer but that is not the same thing. It is about creating $scope.subject.name from a string without hard-coding $scope.subject. This is relevant in some directives where you shouldn't assume anything about the scope if you want the directive to be reusable. Also, angular.forEach()! Don't let jQuery get in the way of using angular :P – Erik Honn Dec 02 '13 at 08:16