Skip to content
Martin Hradil edited this page May 14, 2019 · 3 revisions

This describes an old version of forms, for current forms, go to Forms. For other kinds of forms, go to Forms (kinds)


(TODO: similar style to the rest) (currently just taken from https://github.com/ManageIQ/guides/pull/267)

Stage 2 - Angular conversion from before 1.5

characteristics

  • (all the changes from ruby TODO)
  • uses $scope
  • oftern still actually submits the form the old way
  • TODO

actions

TODO

code

app/controllers/foo_controller.rb
def foo_new
  assert_privileges('foo_new')
  @record = Model.new
  @in_a_form = true
  replace_right_cell
end

def foo_edit
  assert_privileges('foo_edit')
  @record = find_record_with_rbac(Model, from_cid(params[:id]))
  @in_a_form = true
  replace_right_cell
end


def foo_create
  assert_privileges('foo_new')
  @record = Model.new
  create_update
end

def foo_update
  assert_privileges('foo_edit')
  @record = find_record_with_rbac(Model, from_cid(params[:id]))
  create_update
end

def create_update
  TODO this changes

  case params[:button]
  when 'cancel'
    javascript_redirect(:action    => 'show_list',
                        :flash_msg => _("Add of new %{model} was cancelled by the user"))

  when 'add', 'save'
    set_record_vars

    if @record.valid && @record.save
      add_flash(_('Saved'))
      @edit = nil
      replace_right_cell
    else
      TODO flash co
      javascript_flash
    end
end

def set_record_vars
  @record[:name] = params[:name] if params.key?(:name)
  @record[:choice] = params[:choice] if params.key?(:choice)
end
app/assets/javascripts/controllers/foo/foo_form_controller.js
TODO
ManageIQ.angular.app.controller('hostFormController', ['$http', '$scope', '$attrs', 'hostFormId', 'miqService', function($http, $scope, $attrs, hostFormId, miqService) {
  var init = function() {
    $scope.hostModel = {
      name: '',
      hostname: '',
      ipmi_address: '',
      custom_1: '',
      user_assigned_os: '',
      operating_system: false,
      mac_address: '',
      default_userid: '',
      default_password: '',
      remote_userid: '',
      remote_password: '',
      ws_userid: '',
      ws_password: '',
      ipmi_userid: '',
      ipmi_password: '',
      validate_id: '',
    };

    $scope.modelCopy = angular.copy( $scope.hostModel );
    $scope.afterGet = false;
    $scope.formId = hostFormId;
    $scope.validateClicked = miqService.validateWithAjax;
    $scope.formFieldsUrl = $attrs.formFieldsUrl;
    $scope.createUrl = $attrs.createUrl;
    $scope.updateUrl = $attrs.updateUrl;
    $scope.model = "hostModel";
    ManageIQ.angular.scope = $scope;

    if (hostFormId == 'new') {
      $scope.newRecord = true;
      $scope.hostModel.name = "";
      $scope.hostModel.hostname = "";
      $scope.hostModel.ipmi_address = "";
      $scope.hostModel.custom_1 = "";
      $scope.hostModel.user_assigned_os = "";
      $scope.hostModel.operating_system = false;
      $scope.hostModel.mac_address = "";
      $scope.hostModel.default_userid = "";
      $scope.hostModel.default_password = "";
      $scope.hostModel.remote_userid = "";
      $scope.hostModel.remote_password = "";
      $scope.hostModel.ws_userid = "";
      $scope.hostModel.ws_password = "";
      $scope.hostModel.ipmi_userid = "";
      $scope.hostModel.ipmi_password = "";
      $scope.hostModel.validate_id = "";
      $scope.afterGet = true;
    } else if (hostFormId.split(",").length == 1) {
        miqService.sparkleOn();
        $http.get($scope.formFieldsUrl + hostFormId)
          .then(getHostFormDataComplete)
          .catch(miqService.handleFailure);
     } else if (hostFormId.split(",").length > 1) {
      $scope.afterGet = true;
    }

     $scope.currentTab = "default";
  };

  $scope.changeAuthTab = function(id) {
    $scope.currentTab = id;
  }

  $scope.addClicked = function() {
    miqService.sparkleOn();
    var url = 'create/new' + '?button=add';
    miqService.miqAjaxButton(url, true);
  };

  $scope.cancelClicked = function() {
    miqService.sparkleOn();
    if (hostFormId == 'new') {
      var url = $scope.createUrl + 'new?button=cancel';
    } else if (hostFormId.split(",").length == 1) {
      var url = $scope.updateUrl + hostFormId + '?button=cancel';
    } else if (hostFormId.split(",").length > 1) {
      var url = $scope.updateUrl + '?button=cancel';
    }
    miqService.miqAjaxButton(url);
  };

  $scope.saveClicked = function() {
    miqService.sparkleOn();
    if (hostFormId.split(",").length > 1) {
      var url = $scope.updateUrl + '?button=save';
    } else {
      var url = $scope.updateUrl + hostFormId + '?button=save';
    }
    miqService.miqAjaxButton(url, true);
  };

  $scope.resetClicked = function() {
    $scope.$broadcast ('resetClicked');
    $scope.hostModel = angular.copy( $scope.modelCopy );
    $scope.angularForm.$setUntouched(true);
    $scope.angularForm.$setPristine(true);
    miqService.miqFlash("warn", __("All changes have been reset"));
  };

  $scope.isBasicInfoValid = function() {
    if(($scope.currentTab == "default") &&
      ($scope.hostModel.hostname || $scope.hostModel.validate_id) &&
      ($scope.hostModel.default_userid != '' && $scope.angularForm.default_userid.$valid &&
      $scope.angularForm.default_password.$valid)) {
      return true;
    } else if(($scope.currentTab == "remote") &&
      ($scope.hostModel.hostname || $scope.hostModel.validate_id) &&
      ($scope.hostModel.remote_userid != '' && $scope.angularForm.remote_userid.$valid &&
      $scope.angularForm.remote_password.$valid)) {
      return true;
    } else if(($scope.currentTab == "ws") &&
      ($scope.hostModel.hostname || $scope.hostModel.validate_id) &&
      ($scope.hostModel.ws_userid != '' && $scope.angularForm.ws_userid.$valid &&
      $scope.angularForm.ws_password.$valid)) {
      return true;
    } else if(($scope.currentTab == "ipmi") &&
      ($scope.hostModel.ipmi_address) &&
      ($scope.hostModel.ipmi_userid != '' && $scope.angularForm.ipmi_userid.$valid &&
      $scope.angularForm.ipmi_password.$valid)) {
      return true;
    } else
      return false;
  };

  $scope.canValidate = function () {
    if ($scope.isBasicInfoValid() && $scope.validateFieldsDirty())
      return true;
    else
      return false;
  }

  $scope.canValidateBasicInfo = function () {
    if ($scope.isBasicInfoValid())
      return true;
    else
      return false;
  }

  $scope.validateFieldsDirty = function () {
    if(($scope.currentTab == "default") &&
      (($scope.angularForm.hostname.$dirty || $scope.angularForm.validate_id.$dirty) &&
      $scope.angularForm.default_userid.$dirty &&
      $scope.angularForm.default_password.$dirty)) {
      return true;
    } else if(($scope.currentTab == "remote") &&
      (($scope.angularForm.hostname.$dirty || $scope.angularForm.validate_id.$dirty) &&
      $scope.angularForm.remote_userid.$dirty &&
      $scope.angularForm.remote_password.$dirty)) {
      return true;
    } else if(($scope.currentTab == "ws") &&
      (($scope.angularForm.hostname.$dirty || $scope.angularForm.validate_id.$dirty) &&
      $scope.angularForm.ws_userid.$dirty &&
      $scope.angularForm.ws_password.$dirty)) {
      return true;
    } else if(($scope.currentTab == "ipmi") &&
      ($scope.angularForm.ipmi_address.$dirty &&
      $scope.angularForm.ipmi_userid.$dirty &&
      $scope.angularForm.ipmi_password.$dirty)) {
      return true;
    } else
      return false;
  }

  function getHostFormDataComplete(response) {
    var data = response.data;

    $scope.hostModel.name = data.name;
    $scope.hostModel.hostname = data.hostname;
    $scope.hostModel.ipmi_address = data.ipmi_address;
    $scope.hostModel.custom_1 = data.custom_1;
    $scope.hostModel.user_assigned_os = data.user_assigned_os;
    $scope.hostModel.operating_system = data.operating_system;
    $scope.hostModel.mac_address = data.mac_address;
    $scope.hostModel.default_userid = data.default_userid;
    $scope.hostModel.remote_userid = data.remote_userid;
    $scope.hostModel.ws_userid = data.ws_userid;
    $scope.hostModel.ipmi_userid = data.ipmi_userid;
    $scope.hostModel.validate_id = data.validate_id;

    if ($scope.hostModel.default_userid !== '') {
      $scope.hostModel.default_password = miqService.storedPasswordPlaceholder;
    }
    if ($scope.hostModel.remote_userid !== '') {
      $scope.hostModel.remote_password = miqService.storedPasswordPlaceholder;
    }
    if ($scope.hostModel.ws_userid !== '') {
      $scope.hostModel.ws_password = miqService.storedPasswordPlaceholder;
    }
    if ($scope.hostModel.ipmi_userid !== '') {
      $scope.hostModel.ipmi_password = miqService.storedPasswordPlaceholder;
    }

    $scope.afterGet = true;

    $scope.modelCopy = angular.copy( $scope.hostModel );
    miqService.sparkleOff();
  }

  init();
}]);
app/views/foo/_form.html.haml
- @angular_form = true

.form-horizontal
  %form#form_div{"name"            => "angularForm",
                 "ng-controller"   => "hostFormController",
                 'ng-cloak'        => '',
                 "ng-show"         => "afterGet",
                 "novalidate"      => true}

    = render :partial => "layouts/flash_msg"

    - if session[:host_items].nil?
      %div
        %div
          .form-group{"ng-class" => "{'has-error': angularForm.name.$invalid}"}
            %label.col-md-2.control-label{"for" => "name"}
              = _("Name")
            .col-md-8
              %input.form-control{"type"        => "text",
                                  "id"          => "name",
                                  "name"        => "name",
                                  "ng-model"    => "hostModel.name",
                                  "maxlength"   => "#{MAX_NAME_LEN}",
                                  "miqrequired" => "",
                                  "checkchange" => "",
                                  "auto-focus"  => ""}
              %span.help-block{"ng-show" => "angularForm.name.$error.miqrequired"}
                = _("Required")
          .form-group{"ng-class" => "{'has-error': angularForm.hostname.$invalid}"}
            %label.col-md-2.control-label{"for" => "hostname"}
              = _("Hostname (or IPv4 or IPv6 address)")
            .col-md-4
              %input.form-control{"type"        => "text",
                                  "id"          => "hostname",
                                  "name"        => "hostname",
                                  "ng-model"    => "hostModel.hostname",
                                  "maxlength"   => "#{MAX_HOSTNAME_LEN}",
                                  "miqrequired" => "",
                                  "checkchange" => ""}
              %span.help-block{"ng-show" => "angularForm.hostname.$error.miqrequired"}
                = _("Required")
          .form-group{"ng-class" => "{'has-error': angularForm.user_assigned_os.$invalid}", "ng-hide" => "hostModel.operating_system"}
            %label.col-md-2.control-label
              = _("Host platform")
            .col-md-8
              = select_tag('user_assigned_os',
                           options_for_select([["<#{_('Choose')}>", nil]] + Host.host_create_os_types.to_a, disabled: ["<#{_('Choose')}>", nil]),
                           "ng-model"                    => "hostModel.user_assigned_os",
                           "checkchange"                 => "",
                           "ng-required"                 => "!hostModel.operating_system",
                           "selectpicker-for-select-tag" => "")
              %span.help-block{"ng-show" => "angularForm.user_assigned_os.$error.required"}
                = _("Required")
          .form-group
            %label.col-md-2.control-label
              = _("Custom Identifier")
            .col-md-8
              %input#custom_1.form-control{"type"        => "text",
                                           "name"        => "custom_1",
                                           "ng-model"    => "hostModel.custom_1",
                                           "maxlength"   => 50,
                                           "checkchange" => ""}
          .form-group{"ng-class" => "{'has-error': angularForm.ipmi_address.$error.requiredDependsOn}"}
            %label.col-md-2.control-label{"for" => "ipmi_address"}
              = _("IPMI IP Address")
            .col-md-8
              %input.form-control#ipmi_address{"type"                => "text",
                                               "id"                  => "ipmi_address",
                                               "name"                => "ipmi_address",
                                               "ng-model"            => "hostModel.ipmi_address",
                                               "required-depends-on" => "hostModel.ipmi_userid",
                                               "required-if-exists"  => "ipmi_userid",
                                               "maxlength"           => 15,
                                               "checkchange"         => ""}
              %span.help-block{"ng-show" => "angularForm.ipmi_address.$error.requiredDependsOn"}
                = _("Required")
          .form-group
            %label.col-md-2.control-label
              = _("MAC Address")
            .col-md-8
              %input#mac_address.form-control{"type"        => "text",
                                              "name"        => "mac_address",
                                              "ng-model"    => "hostModel.mac_address",
                                              "maxlength"   => "#{MAX_NAME_LEN}",
                                              "checkchange" => ""}
    %hr
    = render(:partial => "/layouts/angular/multi_auth_credentials",
             :locals  => {:record => @host, :ng_model => "hostModel"})
    = render :partial => "layouts/angular/x_edit_buttons_angular"

  - unless session[:host_items].nil?
    %h3
      = n_("Host", "Hosts", session[:host_items].length)
      = _('Selected')
    = _('Click on a Host to fetch its settings')
    %table.admittable{:height => '75'}
      %tbody
        %tr
          %td
            - if session[:host_items]
              - @embedded = true
              - @gtl_type = settings(:views, :host)
              = render :partial => 'layouts/gtl'

:javascript
  ManageIQ.angular.app.value('hostFormId', '#{(@host.id || (session[:host_items] && session[:host_items].join(","))) || "new"}');
  miq_bootstrap('#form_div');
%form#form_div{'name' => 'angularForm', '
  = render :partial => "layouts/flash_msg"

  %h3
    = _("Foo")

  .form-horizontal
    .form-group
      %label.col-md-2.control-label
        = _('Name')
      .col.md-8
        = text_field_tag('name',
                         @edit[:new][:name],
                         :class             => 'form-control',
                         'data-miq_observe' => {:url => url}.to_json)

    .form-group
      %label.col-md-2.control-label
        = _('Choice')
      .col.md-8
        = select_tag('choice',
                     options_for_select([["<#{_('Choose')}>", nil]] + @choices),
                     "data-miq_sparkle_on" => true,
                     :class                => "selectpicker")
        :javascript
          miqInitSelectPicker();
          miqSelectPickerEvent('choice', '#{url}')

    #dynamic_div
      = render :partial => "dynamic"
app/views/foo/_dynamic.html.haml
%div{'ng-if' => 'choice == "foo"'}
  = _("This could be a few more inputs")

%div{'ng-if' => 'choice == "bar"'}
  = _("Or just a harmless message")

migration

TODO

(from https://github.com/ManageIQ/manageiq-ui-classic/pull/1997#discussion_r140758135 :)

Working on the guide, but for now, pretty much all you need to do is:

rename init to $onInit - that gets called automagically by angular, so you no longer need to call it manually. move the templates under app/views/static/ - that way, you can reference it via templateUrl: '/static/....' from angular remove any controller-dependent logic from the moved template - that just means you can't use @record.id in your case make it into a component... ManageIQ.angular.app.controller("foo", controller); should become

ManageIQ.angular.app.component("foo", { bindings: { recordId: "@", } templateUrl: "/static/foo.html.haml", controller: controller, }); then you no longer need to pass the id using value, you can provide it to the component as a parameter - hence the bindings entry - so no injecting vmCloudRemoveSecurityGroupFormId any more ;) replace the original template with just something that uses the component .. so, %my-component{:record-id => @record.id}, and initializes angular (so that miq_bootstrap stays here, not in static - we usually use the component name as the first arg in those cases (so you don't need an extra #div)) don't leave the code in app/assets/javascripts/controllers, use the components dir for that - or if you're up to speed with the recent webpacker changes (talk), you can move it to app/javascript/ and use newer JS features. EDIT: oh, and remove ng-controller from the template too

Also, that miqAjaxButton is an anti-pattern now, with angular components, you should no longer rely on the server generating javascript to redirect you/show a flash message/whatever... instead, you should be using render :json => ... in ruby, and read that json in JS to do the right thing.

(But, it's not miqAjaxButton(url, true) which is the worst, so.. if the server-side logic to do that is not trivial, feel free to ignore this bit for now.)

( TODO all angular stages:

with $scope - angular pre-1.5, traditionally json, sometimes not and jquery controllerAs - clearer boundaries between controllers components - components, json, paramaters via component args )