This question is pretty old so you probably found the answer but I'm still going to try to explain it as other people might find it useful.
There are three things to remember when working with isolated scopes and transcluding content with ng-transcludedirective.
- Isolated scope means that it doesn't inherit properties of its parent. It only inherits properties that you explicitly bind inside the scope object.
- Transcluded content gets compiled and linked only after it's inserted.
ng-transclude creates a new sibling scope of the directive which transcludes the content. Although, this may change from version 1.3.0 ng-transclude should not create new sibling scope.
The best way to understand what's going on in your example is to look at your scope tree.
< Scope (002) : ng-app
< Scope (003) ng-controller
< Scope (004) : b
< Scope (005) : ng-transclude <--- Content rendered under this scope
< Scope (006) : aIsolated
< Scope (007) : b
< Scope (008) : ng-transclude
< Scope (009) : ng-transclude <--- Content rendered under this scope
< Scope (00A) : aNotIsolated
< Scope (00B) : b
< Scope (00C) : ng-transclude
< Scope (00D) : ng-transclude <--- Content rendered under this scope
In your first example Angular finds the b directive. It rips out the content, creates an isolated scope and compiles its template. When compiling the template it finds the ng-transclude directive. It creates a new sibling scope and inserts the content into it. Since ng-transclude's scope is a sibling, its parent is an ng-controller. Therefore, the content inherits all the properties of your MainCtrl controller.
In the second example, Angular finds the aIsolated directive. It rips out the content, creates an isolated scope and compiles its template. When compiling the template it finds the b directive and starts compiling b. It rips out the content wrapped by the b directive, creates an isolated child scope for b and compiles the template. When compiling the b template it finds the ng-transclude directive. It creates a new sibling scope and inserts the content into it. I.e. this new sibling scope is passed to a transclude function which makes the content a child of the ng-transclude scope. Then it starts compiling the content where it finds another ng-transclude directive so it creates a new scope and inserts the content with {{message}} and {{private}} expressions into it. Finally it gets to compiling and linking the expressions. But if you look at the inheritance tree you will find that the content inherits properties of aIsolated scope, which brings us to isolated scopes.
aIsolated directive has an isolated scope so it cannot access private property defined in the parent scope so the content reads the property you defined in aIsolated controller. Directive b then creates an isolated scope as well and adds a private property to it but because it's isolated it's not overriding its parent's property. Therefore, when all directives finish compiling and linking, the properties that are defined in the aIsolated directive are put into {{message}} and {{private}} expressions. message property doesn't exist so it's empty.
The compilation process of the third example is exactly the same as in the second one. It only differs by the aNotIsolated directive which simply creates its own scope thus inheriting properties defined in MainCtrl controller.
If directive's scope is not isolated, then the private property does
not leak through and the greeting is shown.
That's not entirely true. The reason it may appear this way is because aNotIsolated does not define its own private property. Here in my example aNotIsolated defines private property in its controller so you can see the private message is different to your example.
Also leaking is not a good word to describe what's happening. Expressions are evaluated under the scope where they were inserted into. So {{message}} and {{private}} expressions are evaluated under ng-transclude scopes which in turn inherit either from the MainCtrl, aIsolated or aNotIsolated scopes.