1
votes

Hey I'm having two different issues in my ember app, both of which involve bindings.

First, I have a binding firing when I don't want it to. Basically what I'm trying to achieve (I'm building a survey creator front-end app) is that when any text is entered into the 'name' field of a question, I want to add a new question object, which will render out another blank question at the end of the list of questions that the user is adding. This has the effect of there always being a new question, so an add question button is not required. The binding is working, and a new object is being added: however, since the binding is from the newest question object, the binding is triggered again when the new object is created, which in turn creates a new object, which triggers the binding again....which obviously eventually crashes the browser. I've tried using the Ember._suspendObserver function, but there isn't a lot of documentation on this, and I think I'm using it wrong - anyhow it isn't suspending the observer or pausing the binding. The observer in the code is around line 27 (contentsNameObserver)

The other issue I'm having -- I have a selection drop down box which selects what type of question the user wants (single answer, multi-choice, etc.) but the binding between the select box and the {{#each}} helper which renders the kind of question isn't triggering. I'm using the Ember.Select view helper, so there shouldn't be any issues with using get/set to fire the binding. I'm using a computed property to return an array of fields for the question type based on the value of the question type id. The computed property is in line 13 (App.SurveyContent.types), and the template templates/step3. Quick heads up that this app may be extended for more than surveys, hence 'questions' are often referred to in the code as 'content'.

I'm pretty new to ember (this is my first real app) so my code most likely has a lot of issues outside of these problems...so any comments on how I've structured my app would be hugely appreciated as well!

Javascript ember app:

App = Ember.Application.create({
  rootElement: '#emberContainer'
});

App.SurveyContent = Ember.Object.extend({
  name: "",
  content_type: 1,
  content_pos: 1,
  hash: Em.A([]),

  types: function() {
    alert("redraw");
    return App.ContentTypes[this.content_type-1].hash;
  }.property()

});

App.Surveys = Ember.Object.create({
  name: null,
  start: $.datepicker.formatDate('mm/dd/yy' , new Date()),
  end: $.datepicker.formatDate('mm/dd/yy' , new Date()),
  themeID: 0,
  contents: [App.SurveyContent.create()],    //Pushing an instance of App.SurveyContent onto this

  contentsNameObserver: function() {
    context = this;
    console.log("entering");
    Em._suspendObserver(App.Surveys, "contents.lastObject.name", false, false, function() {
      console.log("suspend handler");
      context.contents.pushObject(App.SurveyContent.create());
    })
  }.observes("contents.lastObject.name")

});

App.ContentTypes = [
  Ember.Object.create({name: 'Text question', id:1, hash: [Ember.Object.create({name: 'Question', help: 'Enter the question here', type: 'text'})]}),

  Ember.Object.create({name: 'Multichoice question', id:2, hash: [Ember.Object.create({name: 'Question', help: 'Enter the question here', type: 'text'}), 
                        Ember.Object.create({name: 'Answer', help: 'Enter possible answers here', type: 'text', multiple: true})]})
];

App.ViewTypeConvention = Ember.Mixin.create({
  viewType: function() {
    console.log(this);
    return Em.get("Ember.TextField");
  }.property().cacheable()
});


App.CRMData = Ember.Object.extend();

App.CRMData.reopenClass ({
  crm_data: [],
  org_data: [],
  org_display_data: [],

  loadData: function() {
    context = this;
    context.crm_data = [];
    $.getJSON ("ajax/crm_data", function(data) {
      data.forEach(function(crm) {
        context.crm_data.pushObject(App.CRMData.create({id: crm.crm_id, name: crm.crm_name}));
        crm.orgs.forEach(function(org) {
          context.org_data.pushObject(App.CRMData.create({id: org.org_id, name: org.org_name, crm_id: crm.crm_id}));
        }, context)
      }, context)
      context.updateOrganisations(5);
    }); 
    return this.crm_data;
  },
  updateOrganisations: function(crm_id) {
    context = this;
    this.org_display_data.clear();
    console.log("clearing the buffer")
    console.log(this.org_display_data)
    context.org_data.forEach(function(org) {
      if(org.crm_id == crm_id) {
        context.org_display_data.pushObject(App.CRMData.create({id: org.id, name: org.name}));
      }
    }, context)
  }
});

App.DateField = Ember.TextField.extend({
  attributeBindings: ['id', 'class']
});

App.CRMSelect = Ember.Select.extend({
  attributeBindings: ['id'],
  change: function(evt) {
    console.log(evt)
    App.CRMData.updateOrganisations($('#crm').val())
  }
});

App.ApplicationController = Ember.Controller.extend();

App.Step1Controller = Ember.ArrayController.extend({});

App.Step2Controller = Ember.ArrayController.extend({});

App.Step2Controller = Ember.ArrayController.extend({});

App.ApplicationView = Ember.View.extend({
  templateName: 'app'
});

App.Step0View = Ember.View.extend ({
  templateName: 'templates/step0'
});

App.Step1View = Ember.View.extend ({
  templateName: 'templates/step1'
});

App.Step2View = Ember.View.extend ({
  templateName: 'templates/step2',
  didInsertElement: function() {
    $( ".jquery-ui-datepicker" ).datepicker();
  }
});

App.Step3View = Ember.View.extend ({
  templateName: 'templates/step3',
});



App.Router = Em.Router.extend ({
  enableLogging: true,

  root: Em.Route.extend ({
    showstep1: Ember.Route.transitionTo('step1'),
    showstep2: Ember.Route.transitionTo('step2'),
    showstep3: Ember.Route.transitionTo('step3'),

    index: Ember.Route.extend({
      route: '/',
      connectOutlets: function(router){
        router.get('applicationController').connectOutlet( 'step0');
      }      
    }),

    step1: Ember.Route.extend ({
      route: 'step1',
      connectOutlets: function(router){
        router.get('applicationController').connectOutlet( 'step1', App.CRMData.loadData());
      }
    }),

    step2: Ember.Route.extend ({
      route: 'step2',
      connectOutlets: function(router) {
        router.get('applicationController').connectOutlet('step2')
      },
    }),

    step3: Ember.Route.extend ({
      route: 'step3',
      connectOutlets: function(router) {
        router.get('applicationController').connectOutlet('step3')
      },
    })
  })
});


Ember.LOG_BINDINGS=true;

App.LOG_BINDINGS = true;

App.ContentTypes.forEach(function(object) {
  object.hash.forEach(function(hash) {
    hash.reopen(App.ViewTypeConvention);
  }, this);
}, this);

Html templates (I've got these in haml, so this is just a representation of the important ones)

<script  type="text/x-handlebars" data-template-name="app"> 
{{outlet}}
</script>

<script  type="text/x-handlebars" data-template-name="templates/step3"> 
<h1> Add content to {{App.Surveys.name}} </h1>
<br>

<div id = "accordion2" class = "accordion">
  {{#each content in App.Surveys.contents}}
  <div class="accordion-group">
    <div class = "accordion-heading">
      <a class = "accordion-toggle" data-parent = "#accordion2" data-toggle = "collapse" href = "#collapseOne">
        {{content.name}}
      </a>
    </div>  
    <div id = "collapseOne" class = "accordion-body collapse in">
      {{view Ember.TextField valueBinding="content.name" class="txtName"}}
      <form class = "form-horizontal">
        <div class = "accordion-inner">
          <div class = "control-group">
            <label class = "control-label" for ="organisation"> 
              Content Type
              <div class = "controls">
                {{view Ember.Select contentBinding="App.ContentTypes" optionValuePath="content.id" optionLabelPath="content.name" valueBinding="content.content_type"}}
              </div>  
            </div>  
          </div>  
          {{#each item in content.types }}
          <div class = "control-group" >
            <label class = "control-label" for = "organisation">
              {{item.name}}
              <div class = "controls">
                {{view item.viewType }}
              </div>  
          {{/each}}
          </div>  
      </form> 
    </div> 
  {{/each}}
  </div>
</div>

<br>

<div class = "btn" {:_action => 'showstep3'}>  Next Step > </div>
</script>
2

2 Answers

0
votes

I've solved the first issue, although I didn't get the suspendObserver property working I used an if statement to check the previous element, removing the infinite loop.

contentsNameObserver: function() {
  context = this;
  if(this.get('contents.lastObject').name) {
    context.contents.pushObject(App.SurveyContent.create());  
  }
}.observes("contents.lastObject.name")

Any comments on how to get the _suspendObserver handler working would be appreciated though, it is something that should work but I'm doing something wrong

I've created a stripped down jsfiddle at http://jsfiddle.net/reubenposthuma/sHPv4/

It is set up to go straight to the problem step, step 3, so that I don't need to include all the previous templates.

I'm still stuck on the issue of the binding not firing though. The behaviour I'm expecting is that when the 'Content Type' dropdown box is changed, the text box underneath should change, it should re-render with two text boxes.

0
votes

I realise this is an old question, but there is no documenation and precious little information I could find searching either, hence sharing what I found worked here.

What I found worked was to call Ember._suspendObserver as follows:

somePropertyDidChange: function(key) {
  var that = this;

  Ember._suspendObserver(this, key, null,
    'somePropertyDidChange', function() {

    // do stuff which would normally cause feedback loops
    that.set('some.property', 'immune to feedback');
  });
}.observes('some.property');

You can also use the multiple observer variant as follows:

somePropertiesDidChange: function(key) {
  var that = this;
  Ember._suspendObservers(this, ['some.property', 'another.property'],
    null, 'somePropertiesDidChange', function() {

    // do stuff which would normally cause feedback loops
    that.set('some.property', 'immune to feedback');
    that.set('another.property', 'also immune to feedback');
  });
}.observes('some.property', 'another.property');

In my exact use case I actually called Ember._suspendObservers from an Ember.run.once() function which was setup by the observer since I wanted to make sure a number of dependant properties had settled before doing calculations which in turn would mutate some of those properties.