19

I would like to dynamically build a component tree basing on some information received from AJAX calls.

How to programmatically add a component to the DOM from inside of other component? I have <outer-comp> and I would like, basing on some logic, insert an <inner-comp>. The following code just inserts the elements <inner-comp></inner-comp> to the DOM, and not actual <inner-comp> representation.

@NgComponent(
  selector: 'outer-comp',
  templateUrl: 'view/outer_component.html',
  cssUrl: 'view/outer_component.css',
  publishAs: 'outer'
)
class AppComponent extends NgShadowRootAware {      
  void onShadowRoot(ShadowRoot shadowRoot) {
    DivElement inner = shadowRoot.querySelector("#inner");
    inner.appendHtml("<inner-comp></inner-comp>");
  }
}

Update: I managed to render the inner component correctly in the following way, but I'm still not sure if this is the proper way:

class AppComponent extends NgShadowRootAware {
  Compiler compiler;
  Injector injector;
  AppComponent(this.compiler, this.injector);

  void onShadowRoot(ShadowRoot shadowRoot) {
    DivElement inner = shadowRoot.querySelector("#inner");
    inner.appendHtml("<inner-comp></inner-comp>");    
    BlockFactory template = compiler(inner.nodes);
    var block = template(injector);
    inner.replaceWith(block.elements[0]); 
  }

}

3
  • could you show how you call your AppComponent constructor? how do you create a new compiler and a new injector? Commented Apr 21, 2014 at 9:47
  • @0xor1 no need to call the constructor - that's what dependency injection is for ;-) Commented Apr 25, 2014 at 20:10
  • so how would you achieve this in code that was not in a NgComponent class? say in the main method you wanted to call document.body.appendHtml('<my-comp></my-comp>') how would you then get angular to compile that? Commented Apr 27, 2014 at 11:25

5 Answers 5

10

The API has changed in AngularDart 0.9.9:

  • BlockFactory now is ViewFactory
  • scope.$new now seems to be scope.createChild(scope.context)
  • injector.createChild(modules) now requires a list of modules (instead of a single one)

AngularDart 0.10.0 introduces these changes:

  • NgShadowRootAware not is ShadowRootAware
  • ..value() now is ..bind(., toValue: .)

So the code of pavelgj now looks like so:

class AppComponent extends ShadowRootAware {
  Compiler compiler;
  Injector injector;
  Scope scope;
  DirectiveMap directives;

  AppComponent(this.compiler, this.injector, this.scope, this.directives);

  void onShadowRoot(ShadowRoot shadowRoot) {
    DivElement inner = shadowRoot.querySelector("#inner");
    inner.appendHtml("<inner-comp></inner-comp>");    
    ViewFactory template = compiler([inner], directives);
    Scope childScope = scope.createChild(scope.context);
    Injector childInjector = 
        injector.createChild([new Module()..bind(Scope, toValue: childScope)]);
    template(childInjector, [inner]);
    }
  }
Sign up to request clarification or add additional context in comments.

3 Comments

I think you accidentally just copy-paste @pavelgj answer.
Ouch! You're right. Hope I managed to fix the code - seems I've got rid of my tested version again. In any case, ng-include does something similar, so it's always a good idea to have a look at the implementation of NgIncludeDirective.
What about ng-repeat? When I tried to append something like "<inner-comp ng-repeat='rows in ctrl.list'></inner-comp>" the NgRepeat directive didn't work. Even the constructor of NgRepeat wouldn't be called. After a while I began to suspect it's not my fault. What do you think, should ng-repeat work, or is there an important initialization missing in Pavel's example?
8

This would be a proper use of the block API.

class AppComponent extends NgShadowRootAware {
  Compiler compiler;
  Injector injector;
  Scope scope;
  DirectiveMap directives;

  AppComponent(this.compiler, this.injector, this.scope, this.directives);

  void onShadowRoot(ShadowRoot shadowRoot) {
    DivElement inner = shadowRoot.querySelector("#inner");
    inner.appendHtml("<inner-comp></inner-comp>");    
    BlockFactory template = compiler([inner], directives);
    Scope childScope = scope.$new();
    Injector childInjector = 
        injector.createChild(new Module()..value(Scope, childScope));
    template(childInjector, [inner]);
  }
}

Also, if you ever need to recompile the inner template make sure you do childScope.$destroy() on the previous childScope.

5 Comments

Thank you for the very fast response, your solution works fine.
@pavelgi: This code works fine in angular 0.9.4. But in angular 0.9.6, compiler(...) require second parameter (DirectiveMap directives). What is DirectiveMap and what should I pass into the function?
@pavelgj: Could you, please, update your response to match with the changes introduced in angular 0.9.9?
how do you new up the arguments to the AppComponent constructor?
You just declare any arguments you need - the dependency injection mechanism takes care of supplying proper values for you.
3

The above code samples on longer work because of changes in the Angular Dart library. Specifically ViewFactory.call which no longer takes an injector but takes a Scope and a DirectiveInjector. I've tried adapting what's above and I get very close. The component shows up but none of the bindings are replaced (I see {{cmp.value}} for example.

Here's the code I'm using. I think the issue here is that DirectiveInjector is coming in as null.

void main() {
  IBMModule module = new IBMModule();
  AngularModule angularModule = new AngularModule();

  Injector injector = applicationFactory()
  .addModule(module)
  .run();

  AppComponent appComponent = injector.get(AppComponent);
  appComponent.addElement("<brazos-input-string label='test'/>");
}

@Injectable()
class AppComponent {
  NodeValidator validator;
  Compiler _compiler;
  DirectiveInjector _injector;
  DirectiveMap _directiveMap;
  NodeTreeSanitizer _nodeTreeSanitizer;
  Scope _scope;

  AppComponent(this._injector, this._compiler, this._directiveMap, this._scope, this._nodeTreeSanitizer) {
    validator = new NodeValidatorBuilder.common()
                      ..allowCustomElement("BRAZOS-INPUT-STRING")
                      ..allowHtml5()
                      ..allowTemplating();
  }

  void addElement(String elementHTML) {
    DivElement container = querySelector("#container");
    DivElement inner = new DivElement();
    inner.setInnerHtml(elementHTML, validator: validator);
    ViewFactory viewFactory = _compiler.call([inner], _directiveMap);
    Scope childScope = _scope.createChild(new PrototypeMap(_scope.context));
    if (_injector == null) {
      print("injector is null");
    }
    View newView = viewFactory.call(childScope, _injector);
    container.append(inner);
    newView.nodes.forEach((node) => inner.append(node));
  }
}


class IBMModule extends Module {
  IBMModule() {
    bind(BrazosInputStringComponent);
    bind(BrazosTextAreaComponent);
    bind(BrazosButtonComponent);
    bind(ProcessDataProvider, toImplementation: ActivitiDataProvider);
    bind(AppComponent);
  }
}

Comments

2

I did finally get this to work but was not happy with having to add a timer:

@Injectable()
class AppComponent{
  NodeValidator validator;
  Compiler _compiler;
  DirectiveInjector _directiveInjector;
  DirectiveMap _directiveMap;
  NodeTreeSanitizer _nodeTreeSanitizer;
  Injector _appInjector;
  Scope _scope;

  AppComponent(this._directiveInjector, this._compiler, this._directiveMap, this._nodeTreeSanitizer, this._appInjector, this._scope) {
    validator = new MyValidator();
  }

  void addElement(String id, String elementHTML) {
    DivElement container = querySelector(id);
    DivElement inner = new DivElement();
    container.append(inner);
    Element element = new Element.html(elementHTML, validator: validator);
    ViewFactory viewFactory = _compiler.call([element], _directiveMap);
    if (_scope != null) {
      Scope childScope = _scope.createProtoChild();
      View newView = viewFactory.call(childScope, _directiveInjector);
      newView.nodes.forEach((node) => inner.append(node));
      Timer.run(() => childScope.apply());
    } else {
      print("scope is null");
    }
  }
}

3 Comments

I think new Future(() => childScope.apply()); would be a slightly better way or scheduleMicroTask(() => childScope.apply()); (might not work though).
@GünterZöchbauer I believe the correct way would be to schedule it as a microtask. Why do you think this would not work though?
ScheduleMicrotask might execute it too early.
1

EDIT

The package http://pub.dartlang.org/packages/bwu_angular contains this decorator/directive as bwu-safe-html

------

I use a custom directive for that

@NgDirective(
  selector: '[my-bind-html]'
)
class MyBindHtmlDirective {
  static dom.NodeValidator validator;

  dom.Element _element;
  Compiler _compiler;
  Injector _injector;
  DirectiveMap _directiveMap;

  MyBindHtmlDirective(this._element, this._injector, this._compiler, this._directiveMap) {
    validator = new dom.NodeValidatorBuilder.common()
        ..allowHtml5()
        ..allowImages();
  }

  @NgOneWay('my-bind-html')
  set value(value) {
    if(value == null) {
      _element.nodes.clear();
      return;
    }
    _element.setInnerHtml((value == null ? '' : value.toString()),
                                             validator: validator);
    if(value != null) {
      _compiler(_element.childNodes, _directiveMap)(_injector, _element.childNodes);
    }
  }
}

It can be used like

my-bind-html='ctrl.somehtml'

Angular issue
I created an issue to include this functionality into Angulars ng-bind-html https://github.com/angular/angular.dart/issues/742 (declined)

1 Comment

The issue is closed. This functionality will not be added to Angular for security reasons.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.