Directive acting differently on page load vs reload

2,359 views
Skip to first unread message

Neil Larson

unread,
Dec 11, 2012, 4:48:50 PM12/11/12
to ang...@googlegroups.com
Hello All,

I apologize in advance for not having a fiddle that can show this overly well as I am working with a third party plugin that I cannot find a link to embed it into the fiddle without getting errors.

I am trying to wrap the Veritco Timeline in a directive and display it on my page. I have it mostly working but have encountered a weird issue that I dont understand and am hoping someone might be able to shed some light on. 

When I load the page from the menu, it loads the outer container from the timeline plugin (I see a blank background color, gray in my case as I have a custom css) but if I am on the page and hit the reload button, everything fires fine, the timeline is populated and I can interact with it completely normal. 

Can anyone point me in a different direction or see if how I am approaching it is causing it to fire too early on a page load or something? 

Thanks,
Neil

The code I am using (or a very close approximation) is here:

JS:

var app angular.module('timelineTest'[]);

app.directive('ngTimeline'[function(){
    return function(scopeelmattrs{
        scope.$on('timelineReady'function(e,args){
            angular.extend(scope[attrs.ngTimeline]{'source':args});
            createStoryJS(scope[attrs.ngTimeline]);
        });
    }
}]);

function Controller($scope{
    //test data to verify angular is working
    $scope.someData 'initial value';
    //config object that will be passed into the directive
    $scope.timelineConfig={
        type'timeline',
        width'100%',
        height'500',
        embed_id'timeline',
        css'timeline.css'
    }
    //this is where I am showing code that I know will not work but it will at least show you what I am doing
    //data is retrieved from an injected service
    service.getData(id).then(function(d{
        $scope.rawData d;
        $scope.timelineData parseRawData($scope.rawData);
        $scope.$watch('scope.timelineData'function(){
            $scope.$broadcast('timelineReady'$scope.timelineData);
        });
    });
    
}

<div ng-controller="Controller">
  <span ng-hide="editMode" ng-click="editMode=true">{{someData}}</span>
  <div id='timeline' ng-timeline='timelineConfig'></div>
</div>


broken out in a non-working fiddle here : http://jsfiddle.net/5QPvZ/1/
(fiddle does not work as not all service dependencies are in there and I cannot get it to load the veriteco files without throwing errors)

    

Sean Hess

unread,
Dec 11, 2012, 6:14:36 PM12/11/12
to ang...@googlegroups.com
I'm not sure why it works at all. You have 

        $scope.timelineData parseRawData($scope.rawData);
        $scope.$watch('scope.timelineData'function(){

I would guess that would never fire, because you're setting timelineData right before you start listening for it. 

When I make a directive like this, I don't pass the whole config option in. Not sure if this is best practice or not, but I have a main directive attribute, like "me-timeline" (ng implies that it is part of angularjs core). I pass in the data I should react to to that attribute. 

   <div me-timeline='timelineData'>

Then in my directive, I watch that data:

   // erase scope.$on
   scope.$watch(attrs.meTimeline, function(value) {
     // call your function here
   })

The nice thing about data binding is you can stop thinking in terms of events like that. Usually by writing it this way those kinds of problems just go away on their own. Think about it: you want the thing to update any time the data changes right? So have your directive bind to some data. Also makes it less coupled, because your scope doesn't have to define any particular property. 

If you need to pass more options into your plugin (usually you don't need all of them, use some sensible defaults for your app), just use other attributes, like me-timeline-height="500". Although I usually prefer to use plugins that don't specify things like that - I'd rather have CSS do it. Then you can just attach classes. 

Neil Larson

unread,
Dec 11, 2012, 7:33:30 PM12/11/12
to ang...@googlegroups.com
Hi Sean

Thanks for the reply. I am new to angular and very new to writing directive and still getting my head wrapped around the different parts and options etc. I have updated my code to the simpler approach you mentioned and retested with the same overall results. My code now looks like the following:

JS:

var app angular.module('timelineTest'[]);

app.directive('qcTimeline'[function(){
    return function(scopeelmattrs{
        scope.$watch('attrs.qcTimeline'function(){
            if(scope[attrs.ngTimeline]){
                var timelineConfig = {
                    type: 'timeline',
                    width: '100%',
                    height: '500',
                    embed_id: 'timeline',
                    css: 'css/themes/metro.css',
                    source: scope[attrs.qcTimeline]
                };
                createStoryJS(timelineConfig);
                    };
        });
    }
}]);

function Controller($scope{
    //test data to verify angular is working
    $scope.someData 'initial value';
    //this is where I am showing code that I know will not work but it will at least show you what I am doing
    //data is retrieved from an injected service
    service.getData(id).then(function(d{
        $scope.rawData d;
        $scope.timelineData parseRawData($scope.rawData);
    });
    
}

<div ng-controller="Controller">
  <span ng-hide="editMode" ng-click="editMode=true">{{someData}}</span>
  <div id='timeline' ng-timeline='timelineData'></div>
</div>


so again when I click the link from the home page to the page displaying this data, I get a blank panel where the timeline should appear. If I hit refresh it loads fine. 


Ok so I just found something interesting that I will have to leave for tomorrow to look into more (if anyone has any ideas I am open to hearing them).

Ok so my overall app hierarchy looks like this

Main page with a menu for the user to select type of data to view (car data, house data, or boat data). Each option has its own page with links to specific records showing data of that type (show all cars, all houses, or all boats). The timeline is available for viewing data within a specific record (show details on a specific car, timeline would show when purchased, and when maintenance was done etc). The top of each details page has a menu that can be used to go from a details page to any other page (home, all cars, all houses, all boats).

What I just found is, if I hit refresh at the home page and click cars, then click a specific link to that car details, the timeline loads fine. If I select 'all cars' from the details page to go back to the 'all cars' page and go to the same details page again the timeline does not work. However if I click to go back to the 'all cars' page from the details page and click refresh there as I did on the home page, then go into the details page again, the timeline works. 

This tells me it has something to do with how the library I am using is being initialized or something and once it has been loaded it wont allow it fire properly after that until the files are reloaded. I realize this is beyond the scope of the angular group here. I will have to put a post on the veriteco group and see if they can offer any advice.

Thanks for the help and input on the directive though, I like this simpler solution better, even if I dont completely understand how/why it works

Sean Hess

unread,
Dec 12, 2012, 9:42:41 AM12/12/12
to ang...@googlegroups.com
// not this. This looks for scope.attrs.qcTimeline
scope.$watch('attrs.qcTimeline'function(){

// do this. value is the same as scope[attrs.qcTimeline]
scope.$watch(attrs.qcTimelinefunction(value){

Also, you should try to use a plugin, or change the one you are using, so that it doesn't take an id. Good jquery plugins should work on any element with element.initMyPlugin(), not jQuery.plugin({id:"xxx"}). That id way of doing it kind of breaks angular. 

There might be other stuff wrong. It's hard to know. Maybe in a fiddle people could help you debug it. 


On Tuesday, December 11, 2012 2:48:50 PM UTC-7, Neil Larson wrote:

Neil Larson

unread,
Dec 12, 2012, 10:08:34 AM12/12/12
to ang...@googlegroups.com
Sorry that was a typo, I had scope.$watch(attrs.qcTimeline, function(){}) but did miss the passing of "value" into the function. This is much more clean than having scope[attrs.qcTimeline] throughout the directive, thank you. I am developing on a standalone network and so when I provide code snippets they are hand typed across and at times I misstype, I apologize for this.

As for a fiddle, I am sorry I cant provide one, I prefer to if I can but the plugin I am using wont load into jsfiddle properly and even if it did, to emulate my entire application with service calls would be heavy in a fiddle (this is the only timeline plugin that gives the control I need, but I am going to spend some more time searching to see if I can find any others as the design on this was more for open internet use and I am writing my application on a closed, intranet type network... It may come to me digging into the source code and pulling out the parts that I need out of the larger plugin and writing a native timeline directive). I was hoping that this would be a simpler solution but its not going to be.  

Can you explain more what you mean by the second to last line? I am still new to angular and am trying learn as much as I can but am not following what you mean by the more angular way to init a plugin. 

Thanks for your thoughts and comments so far, I have learned a lot already.

Neil

Sean Clark Hess

unread,
Dec 12, 2012, 10:13:21 AM12/12/12
to ang...@googlegroups.com
Sure. Your plugin expects you to pass in the <div id="something">. That's an old jQuery way of doing it. The new plugins are all like this: $("#something").myPlugin() so you can attach it to anything, even without an id. 

That is probably not what is wrong with your code, but the idea behind directives is that you can use them on any element, simply by adding the directive attribute. 

If your directive has to specify an id, this breaks, because it will only work on an element with that id, or might even work on a different element than the one you put the angular directive on. You have to make sure the id matches the element you specified it on, and you're not supposed to have to do that. 

Directives are meant to be modular: not global. Ids are global. I'd look for a better jquery plugin unless this is the only one you can use. 




--
You received this message because you are subscribed to the Google Groups "AngularJS" group.
To post to this group, send email to ang...@googlegroups.com.
To unsubscribe from this group, send email to angular+u...@googlegroups.com.
Visit this group at http://groups.google.com/group/angular?hl=en-US.
 
 

Neil Larson

unread,
Dec 12, 2012, 10:31:00 AM12/12/12
to ang...@googlegroups.com
Gotcha, thanks for the deeper explanation. 

I have actually already kinda addressed that by setting the "embed_id" config option to be 'embed_id: jQuery(elm).attr('id')', so it pulls the id of whatever element the directive is declared on. Now this does not really address the issue as it still needs to have an id or else it will break. Though I COULD go a step further to say 

if (!jQuery(elm).attr('id')){
    jQuery(elm).attr('id', 'someValue');
}

but that is simply a hack to give the directive the modular nature the plugin lacks. 

This was always one of my complaints with this particular plugin but I have not been overly successful finding good timeline plugins that are robust where I need them to be and still supported/developed. This was designed as a more media centric timeline with support for embedding streaming video etc natively. There is a lot about it I dont need and I think the solution is going to be to use it as a basis for my own plugin/directive using their knowledge on the animations and structure but pulling it out into my own logic. Then its native to my app not an external plugin that I am depending on and having definite load issues with. I am still the most uncomfortable with writing directives though. I am pretty comfortable with services, filters, and controllers, but directives are more complex and I am trying to start small by simply wrapping this plugin and then go from there.
Reply all
Reply to author
Forward
0 new messages